Skip to content

Releases

3.9.0

Choose a tag to compare

@matmen matmen released this 29 Oct 13:50
Immutable release. Only release title and notes can be modified.

This release renders SVG text, converts images with embedded ICC color profiles to sRGB when decoding, hints glyphs exactly like FreeType, and adds patches to lossy JPEG XL and alpha quality to lossy WebP. SVG rendering also gains blend modes, transform-origin, paint-order and context paints. Like every codec in ImageScript, it's plain TypeScript: no WebAssembly and no native code. There are no breaking API changes, but decoded pixels of images with a non-sRGB ICC profile change (see below).

await Image.renderSVG(svg, { fonts: [await fs.readFile('Inter.ttf')] }); // <text>, <tspan>, <textPath>, ...
await Image.decode(p3Photo); // now converted from Display P3 to sRGB
await Image.decode(p3Photo, { applyColorProfile: false }); // the stored values, as before
await image.encodeJXL({ quality: 90, effort: 2 }); // patches for screenshots and text
await image.encodeWebP({ quality: 80, alphaQuality: 50 });

New

SVG text

Image.renderSVG() renders <text>, <tspan>, <tref> and <textPath> with the fonts passed as SVGOptions.fonts and the ones the SVG embeds with @font-face (TrueType, OpenType and WOFF data: URIs).

  • Font matching like browsers match installed fonts: font-family, then font-stretch, font-style and font-weight by CSS Fonts' algorithm, generic families by the fonts' panose and pitch flags, and missing characters from the other fonts
  • Layout ported from resvg (usvg): chunks shaped as a whole (kerning and ligatures across spans), bidirectional text, x/y/dx/dy/rotate lists, text-anchor, letter-spacing, word-spacing, textLength, baseline-shift, dominant-baseline, text-decoration, text along paths, vertical text, font-variant: small-caps, the font shorthand and xml:space
  • Painting like shapes (gradients, patterns, strokes, paint-order), in clip paths and bounding boxes, with color glyphs (COLR v0/v1, CBDT, sbix, SVG)
  • 359 of resvg's 431 text tests pass, plus 41 of its other tests that contain text. Variable fonts aren't supported yet

ICC color profiles

Decoding converts images with an embedded ICC profile to sRGB, the way browsers display them: PNG (iCCP), JPEG (APP2), WebP (ICCP, animations too), AVIF (colr) and TIFF. Display P3 photos from phones and Adobe RGB or ProPhoto images no longer look washed out.

  • Matrix/TRC profiles (RGB and gray) are applied relative colorimetric. The results are within 1 of LittleCMS's
  • Profiles with only lookup tables, CMYK profiles and sRGB profiles leave the pixels as they are
  • applyColorProfile: false (in Image.decode() and Animation.decode() options) keeps the stored values
  • A 1920x1080 image takes about 35 ms to convert

Patches in lossy JPEG XL

encodeJXL({ quality, effort: 2 }) stores repeated text-like features (glyphs on the flat backgrounds of screenshots) once in a reference frame and adds them back where they occur, like libjxl's patches (cjxl -e 7), ported from it. A page of text takes 62% fewer bytes, screenshots 1-2% fewer. Other images come out the same.

Alpha quality in lossy WebP

encodeWebP({ quality, alphaQuality }) (0-100, default 100; also for animations and Animation.webpEncoder()) keeps fewer alpha levels below 100, with the same alpha values as cwebp's -alpha_q. For example, potato.png's alpha plane takes 1.3 kB at 50 instead of 1.9 kB, and a gradient takes 4.3 kB instead of 16.7 kB.

SVG rendering

  • Blending: mix-blend-mode (all 16 modes and plus-lighter) and isolation: isolate. All 23 of resvg's tests for them pass
  • transform-origin on elements, clip paths, gradients and patterns (21 of 23 tests)
  • paint-order (fill, stroke, markers; 12 of 14 tests)
  • context-fill / context-stroke in markers and <use> (13 of 16 tests)
  • shape-rendering: crispEdges/optimizeSpeed draw without antialiasing, and the root's background-color is used
  • font-size resolves em, ex, percentages and keywords against the parent's size everywhere; font-weight accepts bolder/lighter

Text rendering

Hinting (renderText with hinting) now places every point exactly where FreeType's TrueType interpreter (v35) puts it. This was checked on 18 fonts at 8-48 px and on all 6190 glyphs of DejaVu Sans at 4 sizes. Before, up to 30% of the glyphs of some fonts were off by fractions of a pixel, and a few were off by up to 4 pixels.

Fixes

  • SVG: zero-length dashes at the start of a path get caps (square ones turned along the path)
  • SVG: shapes without a bounding box painted with an objectBoundingBox gradient or pattern use its fallback color
  • SVG: display: none elements no longer count towards group bounding boxes
  • SVG: overflow="inherit" on markers and nested viewports takes the parent's value
  • SVG: path data ends at the first character that isn't part of it, instead of that character being skipped
  • SVG: CR LF and CR line ends read as line feeds, like in XML parsers

3.8.1

Choose a tag to compare

@matmen matmen released this 28 Oct 14:25
Immutable release. Only release title and notes can be modified.

A performance release: decoding JPEG, PNG, GIF and lossy WebP and encoding JPEG are faster, and the results are identical to 3.8.0 (the same pixels and the same files). There are no API changes.

Performance

  • JPEG decoding is about 14% faster (a 1920x1080 photo: 47 ms instead of 55 ms). YCbCr to RGB conversion runs in its own loop, clamps through a lookup table and stores whole pixels
  • JPEG encoding is about 10% faster. Coefficients are stored in zigzag order along with each block's last nonzero position, so writing a block stops there instead of scanning all 64 coefficients twice
  • PNG decoding is about 14% faster (a 1920x1080 photo: 36 ms instead of 41 ms). The Paeth filter keeps the neighbours of 3- and 4-byte pixels in variables, and RGB rows are stored as RGBA one whole pixel at a time
  • GIF decoding is about 14% faster (101 frames of 300x300: 40 ms instead of 46 ms). LZW strings are copied from where they were decoded earlier instead of following the code chain back one byte at a time
  • Lossy WebP decoding is about 8% faster. Upsampled pixels are stored one whole pixel at a time

3.8.0

Choose a tag to compare

@matmen matmen released this 28 Oct 11:17
Immutable release. Only release title and notes can be modified.

This release encodes animations a frame at a time, adds OpenType features to text rendering and sharp YUV conversion to lossy WebP, and brings SVG rendering much closer to resvg: filters, patterns, markers and embedded images. It also fixes lossless WebPs that 3.7.0 could write undecodable (#82). Like every codec in ImageScript, it's plain TypeScript: no WebAssembly and no native code. There are no breaking API changes.

const encoder = Animation.webpEncoder(1000, 1000, { quality: 85 });
for (const frame of frames) await encoder.add(frame, { duration: 40 });
const webp = await encoder.finish();

await Image.renderText(font, 32, 'Office 1/2', { features: { smcp: true, frac: true } });
await image.encodeWebP({ quality: 80, sharpYuv: true });
await Image.renderSVG(svg); // now with filters, patterns, markers and <image>

New

Animations a frame at a time

Animation.webpEncoder(), Animation.gifEncoder() and Animation.apngEncoder() (width, height, options) encode animations without holding all frames in memory: await encoder.add(frame, options) for each frame (a Frame or any image; options like { duration: 40 } override the frame's), then await encoder.finish().

  • Same files as encodeWebP(), encodeGIF() and encodeAPNG() produce for the same frames
  • Memory: only the canvas and the previous frame are kept (APNG keeps its frames compressed until the end): 120 frames of 1000x1000 take 159 MB instead of 1066 MB

OpenType features in renderText

features: { tnum: true, smcp: true, liga: false, salt: 2 } turns features on for the whole text (tabular numbers, small caps, stylistic sets, ...), turns them off (default ones like ligatures and kerning included) or picks an alternate by number. It works like HarfBuzz's user features and shapes exactly like hb-shape --features. Fonts that lack a feature ignore it.

Sharp YUV for lossy WebP

encodeWebP({ quality, sharpYuv: true }) (for images, animations and Animation.webpEncoder) converts to YUV with libwebp's sharp conversion, ported so the files match cwebp -sharp_yuv. Colored edges stay sharp. Encoding takes about twice as long. Photos come out about the same size (+0.3% on a 1920x1080 photo), but images with many hard color edges get considerably larger.

Image#region()

region(x, y, width, height) copies a region into a new image without changing the original. It's like crop on a clone, but without copying the whole image first.

SVG rendering

  • Filters: all 17 primitives (blur, offset, flood, composite, merge, drop shadow, color matrix, component transfer, tile, blend with all 16 modes, morphology, convolve matrix, turbulence, displacement map, diffuse and specular lighting with all three lights, image), plus the CSS filter functions (blur(), drop-shadow(), grayscale(), ...). Filter regions, color-interpolation-filters and href inheritance are supported. Ported from resvg: 396 of its 408 filter tests pass, up from 81
  • Patterns: in both units, with viewBox, preserveAspectRatio, patternTransform and href inheritance. 32 of resvg's 33 pattern tests pass, up from 4
  • Markers: start, mid and end markers, with orient (including auto-start-reverse), markerUnits, refX/refY, viewBox and clipping, placed like resvg places them. 61 of 64 tests pass, up from 11
  • Images (<image>) from data URIs: every format Image.decode() reads (GIFs show their first frame) and nested SVGs, gzipped ones included. External files and URLs aren't loaded

Text in SVGs isn't rendered yet.

Improvements

  • Optimized lossy WebP animations are smaller and look better. Like libwebp, they skip pixels that changed by less than a tolerance derived from the quality, and make unchanged pixels transparent only in whole 8x8 blocks. On cross-fading animations the files are up to 13% smaller than before, at 0.1 dB PSNR below unoptimized ones (before: 0.8-1 dB below)

Fixes

  • Lossless WebPs, and lossy ones with alpha (most optimized animations), could be undecodable since 3.7.0 (#82)

Performance

  • composite() with an opacity is about 40% faster in normal mode (7.6 ms instead of 12.6 ms for a 1100x680 layer), and 5-30% faster without an opacity

3.7.0

Choose a tag to compare

@matmen matmen released this 26 Oct 07:24
Immutable release. Only release title and notes can be modified.

This release encodes lossy JPEG XLs, makes lossless WebPs and the alpha of lossy ones smaller, and decodes lossy JPEG XLs about 15% faster. Like every codec in ImageScript, it's plain TypeScript: no WebAssembly and no native code. There are no breaking API changes.

await image.encodeJXL();                          // lossless, as before
await image.encodeJXL({ quality: 90 });           // lossy, like cjxl -q 90
await image.encodeJXL({ quality: 75, effort: 2 });

New

Lossy JPEG XL encoding

encodeJXL({ quality, effort }) encodes lossy JPEG XLs (VarDCT) when given a quality. Without it, images are encoded losslessly as before.

  • quality (0-100) maps to the Butteraugli distance like cjxl -q: 90 is cjxl's default, 100 is lossless
  • effort (0-2, default 1): 1 chooses the transforms' sizes like libjxl's effort 5 (from 4x4 up to 64x64, chroma from luma per tile), 2 also tries the 4x8 and AFV transforms, 0 uses 8x8 DCTs only (like effort 3)
  • Alpha is lossless
  • Compression: at the same SSIMULACRA2 score the files are about 1.5% smaller than cjxl -e 5's at effort 1 (3.5% at effort 2), and within 1-4% of cjxl -e 7's, the default (up to 9% for screenshots)
  • Speed: a 1920x1080 photo takes about 1 s at effort 0, 5 s at effort 1, 10 s at effort 2

Improvements

  • Lossless WebP files are about 6% smaller (within about 7% of cwebp's), the alpha of lossy WebPs about 17% (at the default effort smaller than cwebp's): blocks are grouped by their statistics with prefix codes per group, the color cache's size is chosen, matches look one pixel ahead, smooth alpha is predicted instead of indexed. Lossless encoding takes about 65% longer

Performance

  • JPEG XL decoding of lossy images is about 15% faster (2048x1536: 0.73 s instead of 0.87 s), with identical results
  • AVIF encoding is about 8% faster, with files about as large as before

Fixes

  • AVIFEncodeOptions is exported, with the new JXLEncodeOptions

3.6.0

Choose a tag to compare

@matmen matmen released this 25 Oct 19:44
Immutable release. Only release title and notes can be modified.

This release writes metadata into AVIFs and JPEG XLs, makes PNGs of graphics up to 30% smaller and lossy WebP encoding about four times faster by default. There are no breaking API changes.

await image.encodeAVIF({ metadata: { author: 'me', description: 'a cat' } });
await image.encodeJXL({ metadata: { software: '' } });   // without the software tag
await image.encodeWebP({ quality: 75 });                 // effort 2 now, about 250 ms for 1920x1080
await image.encodeWebP({ quality: 75, effort: 4 });      // like cwebp's default, as before

New

Metadata in AVIF and JPEG XL

encodeAVIF() and encodeJXL() take metadata like the other encoders: author, description, copyright, creation time and software (by default ImageScript and its version) as Exif.

  • AVIF: an Exif item that describes the image, like libavif writes it
  • JPEG XL: the codestream goes into the container, with an Exif box. The codestream is still byte for byte what cjxl -e 1 writes. As the creation time is always written, JPEG XLs are now always in the container (about 250 bytes more), like JPEGs and WebPs always have Exif

Improvements

  • PNG (and APNG): rows are left unfiltered when that compresses better than the paeth filter, as it often does for graphics and screenshots: their files are up to 30% smaller, and encoded faster. Photos are encoded as before, about 8% slower
  • Lossy WebP defaults to effort: 2 instead of 4: about a quarter of the time (about 250 ms instead of 900 ms for a 1920×1080 photo) for files about 5% larger, at a slightly lower quality (about 0.5 dB PSNR). effort: 4 still encodes byte for byte like cwebp's default
  • AVIF encoding also splits blocks into halves where that pays off: files about 2% smaller at the default effort, about 20% slower. effort: 2 also chooses smaller transforms within blocks: about 1% smaller (up to 4% for graphics)

Performance

  • Lossy WebP with effort 3 to 6 is about 25% faster, with byte for byte the same files

v3.5.0: AVIF and JPEG XL encoding

Choose a tag to compare

@matmen matmen released this 25 Oct 13:45
Immutable release. Only release title and notes can be modified.

This release encodes AVIF and lossless JPEG XL, and decodes JPEG XL about 25% faster. Like every codec in ImageScript, both encoders are plain TypeScript: no WebAssembly and no native code. There are no breaking API changes.

await image.encodeAVIF();                                   // quality 60, like avifenc -q 60
await image.encodeAVIF({ quality: 80, subsampling: '4:2:0' });
await image.encodeAVIF({ quality: 100 });                   // lossless
await image.encodeJXL();                                    // lossless, like cjxl -e 1

New

AVIF encoding

encodeAVIF({ quality, alphaQuality, subsampling, effort }) encodes AVIFs with an AV1 encoder of its own. Its syntax mirrors the dav1d based decoder, so dav1d decodes the files to exactly the encoder's reconstruction.

  • Rate-distortion search: partitions from 64×64 to 8×8, the intra modes with their angle deltas, chroma from luma, palettes and the transform types, coefficients quantized by rate and distortion, and the deblocking level and CDEF strengths chosen by the filtered image
  • Screenshots and graphics get palettes: such images are often 10% to nearly 50% smaller
  • Greyscale images are encoded as monochrome
  • Transparency becomes a monochrome alpha image, lossless by default (alphaQuality). In lossy images, fully transparent pixels get smooth colors that cost fewer bits
  • quality (0-100, default 60) maps to the quantizer like avifenc's -q. 100 is lossless
  • subsampling: '4:4:4' (default) or '4:2:0'
  • effort (0-2, default 1): how many modes and partitions are tried. 0 is about a third faster, 2 takes about three times as long for files 1-3% smaller

On photos the files are within a few percent of libaom's (avifenc's default speed) at the same PSNR. Encoding is slower, about 3 seconds for a 1024×768 photo at the default effort.

JPEG XL encoding

encodeJXL() encodes lossless JPEG XLs with a port of libjxl's fast lossless encoder. The codestream is byte for byte what cjxl -e 1 writes on CPUs with AVX2 or NEON (libjxl's output depends on the CPU). Images of at most 511 colors besides black get a palette, others YCoCg. Greyscale images are encoded as one channel and alpha only if the image has transparency. Unlike libjxl, two colors whose hashes collide within 8 pixels don't end up as one: such images are encoded with YCoCg instead.

Performance

  • JPEG XL decoding is about 25% faster: lossy 2048×1536 images take about 0.87 s instead of 1.2 s, lossless ones of the default effort 2.6 s instead of 3.7 s. The output is the same, except for rounding: linear light becomes sRGB like libjxl does it.

The encoders grow the package from 1.8 MB to 1.9 MB unpacked (506 kB packed).

v3.4.0: AVIF, JPEG XL, lossy WebP

Choose a tag to compare

@matmen matmen released this 25 Oct 06:39
Immutable release. Only release title and notes can be modified.

This release reads AVIF and JPEG XL, encodes lossy WebP and adds BMP, ICO/CUR, QOI, Netpbm and Radiance HDR. Every codec is plain TypeScript: no WebAssembly and no native code. There are no breaking API changes.

New

AVIF

Image.decode reads AVIFs through an AV1 decoder ported from dav1d. It decodes exactly like dav1d, and the conversion to RGB is libavif's, so the pixels are the ones libavif gives.

  • 4:2:0, 4:2:2, 4:4:4 and monochrome, 8, 10 and 12 bits
  • Every intra coding tool and post filter: palettes, intra block copy, filter intra, chroma from luma, lossless, tiles, deblocking, CDEF, superres, loop restoration and film grain
  • The container: grids, alpha (also premultiplied), crop, rotation and mirroring
  • HDR (PQ and HLG, BT.2020 and Display P3) is converted to sRGB, see toneMapping

Image sequences give their first frame, also from Animation.decode. Frames that reference other frames (inter frames) aren't decoded.

JPEG XL

Image.decode reads JPEG XLs and Animation.decode their animations, with durations and loop count. The decoder is ported from jxl-oxide and libjxl and supports every coding tool:

  • Modular and VarDCT, lossless and lossy, including recompressed JPEGs
  • Progressive passes and LF frames, patches, splines, noise, Gaborish and the edge-preserving filter, upsampling
  • Layers: cropped and blended frames and reference frames
  • Alpha (also premultiplied), spot colors, CMYK and the orientation

Every file of the JPEG XL conformance suite decodes within 1 of libjxl's djxl. XYB images are rendered to sRGB and HDR ones are tone mapped. ICC profiles aren't applied, so CMYK images become their colors times black, like CMYK JPEGs.

decoding (Node, one thread) lossy (VarDCT) lossless (-e 1) lossless (-e 7)
1024×1024 0.46 s 0.25 s 1.2 s
4000×3000 4.8 s 2.6 s 11.5 s

Lossy WebP

encodeWebP({ quality }) encodes lossy WebPs. The encoder is ported from libwebp and writes the same VP8 bitstream, byte for byte, as cwebp with the same quality. Transparent images get a losslessly compressed alpha plane.

await image.encodeWebP({ quality: 80 });              // like cwebp -q 80
await image.encodeWebP({ quality: 80, effort: 0 });   // like cwebp -q 80 -m 0: several times faster
await animation.encodeWebP({ quality: 80 });          // lossy frames

effort (0-6, default 4) is cwebp's -m. Without a quality, WebPs stay lossless.

Small formats

  • BMP: 1 to 64 bits, channel masks of any size, alpha, RLE4 and RLE8, embedded JPEGs and PNGs, and OS/2 headers. The output matches BMP Suite's reference images.
  • ICO and CUR: the largest image, as a bitmap with its mask or as a PNG.
  • QOI
  • Netpbm: PBM, PGM, PPM (plain and raw, up to 16 bits), PAM and PFM.
  • Radiance HDR: RGBE and XYZE, every orientation.

Image.encodeBMP, Image.encodeQOI and Image.encodePNM write them. The QOI encoder's output is byte for byte the reference encoder's. encodePNM picks PBM, PGM, PPM or PAM by what the image needs.

ImageType recognizes all new formats: 'avif', 'jxl', 'bmp', 'ico', 'cur', 'qoi', 'pnm' and 'hdr'.

HDR and toneMapping

Radiance HDRs, PFMs and HDR AVIFs and JPEG XLs can be brighter than white. By default ('reinhard'), the brightest parts are compressed like Reinhard's tone mapping operator does and the brightest pixel becomes white. Images no brighter than white stay as they are. {toneMapping: 'clamp'} clips instead.

Fixes

  • TrueType hinting matches FreeType in more fonts. SPVTL, SFVTL and SDPVTL pointed their vectors the wrong way, S45ROUND now rounds to FreeType's 45° grid, and ISECT and ALIGNPTS compute like FreeType's integer arithmetic.
  • rand alternates: the randomly picked alternates are now the ones HarfBuzz picks.
  • SVG presentation attributes: spaces around their values (fill=" url(#gradient) ") are ignored, as in CSS.

The new codecs grow the package from 1.1 MB to 1.8 MB unpacked (472 kB packed).

v3.3.0: text in every script, color emoji, faster resizing

Choose a tag to compare

@matmen matmen released this 17 Oct 22:05
Immutable release. Only release title and notes can be modified.

This release renders text in every script and color emoji, makes resizing and rotating a lot faster and hardens decoding and rendering against hostile input. There are no breaking API changes, but some results differ slightly from 3.2 (see Upgrading).

New

Text in every script

Image.renderText now shapes text like browsers do, through a port of HarfBuzz's OpenType shaping. On the tested texts, it produces the same glyphs and positions as HarfBuzz 14.6.

  • Right to left and bidirectional text with the Unicode Bidirectional Algorithm: Arabic, Hebrew and others, mixed with left to right text and numbers, with mirrored brackets.
  • Joining scripts: Arabic, Syriac, N'Ko, Mongolian, Adlam and others. Fonts without shaping features get presentation forms.
  • Indic scripts: Devanagari, Bengali, Tamil, Telugu, Malayalam and the others, with conjuncts, reph and matra reordering.
  • Other complex scripts: Khmer, Myanmar, Thai, Lao, Hangul, and the scripts of the Universal Shaping Engine, such as Sinhala, Tibetan, Javanese and Balinese.
  • All GSUB and GPOS lookups: mark positioning, cursive attachment, kerning, and contextual substitutions and positioning. Fonts without GPOS kerning use their kern table.
  • Line breaking follows the Unicode Line Breaking Algorithm, so text also wraps in CJK and around punctuation, not only at spaces.

The Unicode 18.0 data and the shaping add about 80 kB (gzipped) to the package. Nothing is loaded at runtime: no WebAssembly and no native code.

Color emoji and fallback fonts

renderText takes several fonts in order of priority:

Image.renderText([textFont, emojiFont], 32, 'Hello 👋🏽 مرحبا');

Every character comes from the first font that has it. The fallback font is chosen per grapheme cluster, so emoji sequences such as skin tones, flags, families and keycaps stay together.

Color glyphs keep their colors in all of these formats:

  • COLR v0 and v1, including gradients and composite modes (Twemoji, Noto-COLRv1)
  • CBDT bitmaps (Noto Color Emoji)
  • sbix bitmaps (Apple's format)

Faster resizing and rotating

resize() (bilinear and area) and rotate() by angles that aren't multiples of 90° now compute in 8 bits by default.

1920×1080 photo 3.2 3.3
halve (opaque) 15 ms 12 ms
double (opaque) 80 ms 61 ms
halve (with alpha) 43 ms 24 ms
double (with alpha) 260 ms 103 ms
rotate 30° 119 ms 51 ms

Opaque pixels differ from 3.2 by at most 1. {fast: false} keeps the previous, more precise results.

maxPixels

Image.decode and Animation.decode reject images over 268 megapixels (16383², like sharp) before allocating anything. A 250-byte JPEG could previously claim 65500×65500 pixels and take gigabytes. {maxPixels: Infinity} lifts the limit.

Performance

  • Lossless WebP: encoding is about 40% faster, decoding about 15% faster. The output is identical.
  • PNG decoding uses less memory: at most 785 MB instead of 1272 MB for an 8000×8000 RGBA image.
  • Still PNGs in Animation.decode decode about 10 times as fast when they're large.

Fixes

  • NaN and infinite arguments throw a RangeError that names the argument. They used to silently draw black, transparent or nothing. For example, Image.rgbToColor(NaN, 0, 0) used to return black. Numeric strings keep working.
  • Denial of service: SVG rendering no longer backtracks on long malformed styles, url() references, lengths or colors. A 200 kB stylesheet without braces used to take a minute. <use> reference bombs, huge dash patterns and deep nesting are limited as well.
  • Infinite or huge blur and sharpen radii: blur(Infinity) used to hang and blur(1e9) took minutes.
  • Damaged images and fonts throw a clear error instead of a RangeError, and can no longer take unbounded memory or time.
  • Text outlines (strokeWidth) follow the antialiased edges of the glyphs, so they are smooth. They no longer show through translucent text.
  • Empty text: renderText used to throw for text with nothing to show. It now renders a transparent image one pixel wide.
  • Unclosed CSS rules: an SVG style rule that isn't closed at the end of the stylesheet now applies, as it does in browsers.
  • JSR docs no longer list internal members.

ImageScript is now also tested in Deno and Bun, on Linux, macOS and Windows.

Upgrading

No code changes are needed. Some results differ from 3.2:

  • Bilinear and area resizing and rotating can produce pixel values off by 1, or by about 2 for translucent pixels. Pass {fast: false} for the previous results.
  • NaN arguments throw instead of drawing nothing.
  • Images over 268 megapixels need {maxPixels: Infinity} to decode.

v3.2.1: faster resizing, blur and GIF decoding

Choose a tag to compare

@matmen matmen released this 16 Oct 09:53
Immutable release. Only release title and notes can be modified.

This release makes resizing, blurring and GIF decoding faster, and fixes a bug when enlarging with RESIZE_AREA. Nothing changes in the API.

Performance

1920×1080 photo 3.2.0 3.2.1
resize() to half (bilinear) 49 ms 15 ms
resize() to half (area) 33 ms 9 ms
resize() to double (bilinear) 392 ms 89 ms
blur(5) 205 ms 86 ms
decoding a 101-frame GIF 134 ms 83 ms

Resizing (#68)

  • The arithmetic is integer and the vertical pass runs first.
  • For opaque images, two color channels share one multiplication.
  • Bicubic resizing and images with alpha get 20–40% faster.

Blur (#69)

The three box blurs work directly on the pixels as 32-bit words, two channels per sum. Transparent images are blurred premultiplied in 8 bits, as browsers do.

Smaller speedups

  • GIF decoding (#70): frames are composited from their color indices instead of an RGBA copy of each, and they're no longer copied a second time. The output is bit-identical.
  • clone(): about 50% faster.

Fixes

  • Enlarging with RESIZE_AREA left transparent rows and columns where a pixel's center fell exactly on the edge of the filter (#68).

Upgrading

No code changes are needed. If you compare resized or blurred images against stored reference images pixel by pixel, expect small differences:

  • most values differ by at most 1 per channel (blur: 2 for radii above 10)
  • larger differences are limited to the color of pixels that are almost fully transparent, which doesn't show once they're composited

v3.2.0: smaller animations, options objects, faster codecs

Choose a tag to compare

@matmen matmen released this 16 Oct 08:01
Immutable release. Only release title and notes can be modified.

This release makes animations a lot smaller, makes options easier to pass and speeds up the most common codecs. There are no breaking changes.

New

Smaller animations (#59)

encodeGIF(), encodeAPNG() and encodeWebP() now store each frame as only the area that changed since the frame before, and merge identical frames. Within that area, unchanged pixels become transparent where that compresses better.

Re-encoding 762 animations took this share of the previous size in total:

GIF APNG WebP
51% 63% 54%
  • The less moves in an animation, the smaller it gets. Animations where everything moves stay about the same size.
  • Large animations encode faster, small ones up to about twice as slow.
  • The files show the same animation in browsers, Pillow and libwebp. They don't decode to the same frames anymore, though. {optimize: false} stores the frames as they are.

Options objects (#58)

Every method with optional arguments now also takes them as an object, so you can leave options out without counting positions:

image.resize(100, 100, {mode: Image.RESIZE_BICUBIC});
image.composite(logo, {x: 10, y: 10, opacity: 0.5});
Image.renderText(font, 32, 'Hi', {color: 0xff0000ff, strokeWidth: 2});
animation.encodeGIF({quality: 80});

The positional arguments keep working until 4.0.

Metadata in every format (#56)

encodeJPEG(), encodeWebP(), encodeTGA() and the animation encoders take the same metadata as encode(), as the metadata option:

image.encodeJPEG({quality: 80, metadata: {author: 'me'}});

Each format stores the fields it has a place for: Exif and a comment for JPEG, Exif for WebP, comments for GIF, the extension area for TGA, text chunks for PNG and APNG.

All files now name ImageScript as the software and include the creation time, as PNGs already did. {software: ''} leaves the software out.

trim() (#67)

Removes borders of one color, for example the empty space around rendered text or a solid background around a logo.

  • Without a color, the top left pixel's color is used.
  • tolerance also trims near colors, such as JPEG artifacts.
  • Frames keep their position in the animation.

Animation.frames (#60)

The frames of an animation as an array. Changing the array (push(), splice(), …) changes the animation, and you can replace it as a whole.

Performance (#62–#66)

Every change gives bit-identical results, checked against 3.1 on about 1600 images and the resvg test suite.

Faster by about
GIF decoding 25%
SVG rendering 15–20%
PNG decoding 25%
PNG encoding 10%
Lossless WebP encoding 15%
Resizing 10%

The package is also about 10 kB smaller, because the type declarations of internal modules are no longer included.

Benchmarks (#61)

bench/ compares ImageScript with Jimp, image-js, Photon, sharp, @napi-rs/canvas and ImageScript 1.x. It covers:

  • decoding and encoding
  • image operations
  • text and SVG rendering
  • animations
  • file sizes and install sizes

The results are refreshed automatically after every release.

Deprecated

  • Animations as arrays: animation[0], animation.length, animation.map() and the other array methods on an animation still work, but in 4.0 Animation won't extend Array anymore. Use animation.frames.
  • PNGMetadata is now called Metadata. The old name stays an alias until 4.0.

Fixes

  • TGA encoding: run length encoded packets end with the row, as TGA 2.0 requires. Some readers rejected the files before, Pillow among them (#57).
  • PNG text metadata: text that isn't Latin-1 (such as Ⓒ or emoji) is written as UTF-8 into iTXt chunks. Readers showed it garbled before (#56).
  • creationTime: an invalid value throws a RangeError instead of writing "Invalid Date" (#56).

Upgrading

No code changes are needed. If you compare the frames of re-encoded animations with the originals, pass {optimize: false} to the encoder, because optimized files decode to different (smaller) frames.