Repository navigation
4.0.0-rc.4
Pre-releaseImageScript 4.0.0-rc.4
The fourth release candidate for ImageScript 4.0 is ready for evaluation. This is a major release with breaking API
changes from 3.x. Before upgrading, update to the latest 3.x release (3.20.3) and address its deprecation warnings, then
follow the migration guide.
This release candidate comes out of a full pre-release check of the public API, the packaging, the runtimes and about
370,000 fuzzed decodes. It fixes what that found, and makes the remaining numeric arguments behave like the rest.
Breaking changes since rc.3
- Renamed: the
sizeoption ofcropCircle()isfit('contain'or'cover'), likeresize()'s. Out-of-range
feathering(outside 0-1) throws instead of being clamped. - Numbers have to be numbers, everywhere: a numeric string (
opacity('0.5'),quality: '50',scale('2'),
frame.duration = '40'),nullor a boolean where a number goes is aTypeError. 3.x converted them in some methods
and rejected them in others; rc.3 still accepted a few.NaNand the infinities are aRangeError. - Values that did nothing useful throw:
roundCorners({ radius })below 0 (it was clamped to 0),fisheye({ strength })
of 0 or less (an empty image), andrenderText({ size: 0 })(a 1x1 image). - Gradients blend transparency premultiplied, like CSS: a gradient from transparent to red is
ff00007fin the
middle, no longer a dark7f00007f. Gradients between opaque colors are unchanged. image.gainMap = mapstores a copy, so images given one map no longer change each other's by resizing or cropping.resize({ background })is only typed withfit: 'contain'(it was ignored otherwise).Animation.resize()andscale()size the canvas likeImagedoes: sides are cut to whole pixels, andscale()
never goes below one pixel.- Cut-off animations keep the frames that arrived whole, and throw when none did: a GIF cut before its first frame
decoded as a blank image, and the last frame of a cut GIF, lossy WebP or lossy JPEG XL animation was half decoded.
A cut APNG showed only its first frame. - A canvas that doesn't fit the format throws: GIF over 65535 pixels (70000 was written as 4464), and an animation
without a canvas.
Security and robustness
- SVG strokes with round joins took 9 s and 1 GB for a 17 KB path (2.7 GB for 50 KB): a join was a whole circle at every
vertex of a flattened curve. Joins are wedges now (1.6 s and 220 MB), and a render stops with aLimitErroronce its
shapes' edges cross the rows of the image more than 2^27 times (thousands of huge overlapping strokes took 38 s). - A million dashes (157 bytes of SVG) took 650 MB: dashes beside the image are left out and the rest are stroked in
batches. Text of 20000 glyphs took 15 s and now 0.4 s, 8000 random curves 18 s and now 2.3 s. - TIFF files that ask for far more memory than they are (YCbCr subsampling of 65534x65535: 4 GB for 32 KB; 1024
samples per pixel: 1 GB for 33 KB) are refused before anything is allocated. - Decoding an animation takes 1.3 times the result's memory, not 2.2 (APNG and WebP composite each frame as it is
decoded).dropShadow()andoutline()throw aLimitErrorfor sizes an image can't have. - Damaged JPEG, TIFF, GIF and TGA headers are decoding errors, where some read missing bytes or threw a
RangeError.
Fixes
- Bundlers keep only what a page uses:
import { Colors }bundled the whole library (201 kB minified); it is 3.4 kB now,
because the build'sdist/package.jsonno longer hides"sideEffects": false. Animation.resize()changes a frame once when an animation lists it more than once, resizes frames that share an
image at different offsets to their own sizes, and throws below one pixel.Animation.decode()of a JPEG XL animation counts all frames againstmaxPixels.resize({ fit: 'cover' })andfill(fn)leave the image and its gain map untouched when they throw.- The animation encoders cut frames to the canvas like
Animation.encode*()does withoptimize: false;add()rejects
its promise for invalid frames;Colors.gradient()with one color throws forNaN. - Documentation: the README, the migration guide and the doc comments match the behavior (counts, error types, defaults).
Performance
Against 3.20.3 and rc.3, one build per process on a 1920x1080 photo, nothing changed beyond noise except: animated WebP
decode is 4.6% slower than rc.3 (the price of the lower memory), and quantize() with its new default dithering is
3.3 times slower than 3.x's (dither: 'none' is equal).
Known limits
- TypeScript 5.7 or newer is needed (older versions can't read the
Uint8Array<ArrayBuffer>return types). - Single-file bundles (esbuild, webpack) and CDN builds served from another origin run without worker threads: the
output is identical, only slower. Servedist/util/worker.jsnext to the bundle for threads. - The default
maxPixels(16383 x 16383) still allows a GIF of 300 bytes to allocate a 1 GB canvas.
Feedback on this release candidate is welcome: open an issue.