Repository navigation
Releases
3.9.0
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, thenfont-stretch,font-styleandfont-weightby 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/rotatelists,text-anchor,letter-spacing,word-spacing,textLength,baseline-shift,dominant-baseline,text-decoration, text along paths, vertical text,font-variant: small-caps, thefontshorthand andxml: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(inImage.decode()andAnimation.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 andplus-lighter) andisolation: isolate. All 23 of resvg's tests for them pass transform-originon elements, clip paths, gradients and patterns (21 of 23 tests)paint-order(fill, stroke, markers; 12 of 14 tests)context-fill/context-strokein markers and<use>(13 of 16 tests)shape-rendering: crispEdges/optimizeSpeeddraw without antialiasing, and the root'sbackground-coloris usedfont-sizeresolvesem,ex, percentages and keywords against the parent's size everywhere;font-weightacceptsbolder/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
objectBoundingBoxgradient or pattern use its fallback color - SVG:
display: noneelements 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
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
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()andencodeAPNG()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-filtersandhrefinheritance are supported. Ported from resvg: 396 of its 408 filter tests pass, up from 81 - Patterns: in both units, with
viewBox,preserveAspectRatio,patternTransformandhrefinheritance. 32 of resvg's 33 pattern tests pass, up from 4 - Markers: start, mid and end markers, with
orient(includingauto-start-reverse),markerUnits,refX/refY,viewBoxand clipping, placed like resvg places them. 61 of 64 tests pass, up from 11 - Images (
<image>) from data URIs: every formatImage.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 innormalmode (7.6 ms instead of 12.6 ms for a 1100x680 layer), and 5-30% faster without an opacity
3.7.0
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 likecjxl -q: 90 is cjxl's default, 100 is losslesseffort(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% ofcjxl -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
AVIFEncodeOptionsis exported, with the newJXLEncodeOptions
3.6.0
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 beforeNew
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 1writes. 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: 2instead 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: 4still 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: 2also chooses smaller transforms within blocks: about 1% smaller (up to 4% for graphics)
Performance
- Lossy WebP with
effort3 to 6 is about 25% faster, with byte for byte the same files
v3.5.0: AVIF and JPEG XL encoding
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 1New
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 losslesssubsampling:'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
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 frameseffort (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.
randalternates: 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
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
kerntable. - 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.decodedecode about 10 times as fast when they're large.
Fixes
- NaN and infinite arguments throw a
RangeErrorthat 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 andblur(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:
renderTextused 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
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_AREAleft 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
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.
tolerancealso 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.0Animationwon't extendArrayanymore. Useanimation.frames. PNGMetadatais now calledMetadata. 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 intoiTXtchunks. Readers showed it garbled before (#56). creationTime: an invalid value throws aRangeErrorinstead 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.