Skip to content

Releases

3.20.2

Choose a tag to compare

@matmen matmen released this 08 Nov 20:20
Immutable release. Only release title and notes can be modified.

Fixes

  • Loop counts that don't fit the file format are refused with a RangeError naming the format's largest count, instead of wrapping around: a GIF with loopCount: 65536 was written with 0, which plays forever (GIF and animated WebP store up to 65535 and 65534, APNG up to 2147483646). This applies to encodeGIF(), encodeWebP() and encodeAPNG() of animations and to the streaming encoders.

3.20.1

Choose a tag to compare

@matmen matmen released this 08 Nov 14:37
Immutable release. Only release title and notes can be modified.

Performance

  • Drawing on an image with a gain map (an HDR photo) is much faster: drawBox() and drawCircle() only recompute the part of the map they cover, instead of the whole map for every call. 400 scanlines over a 1820x1820 Ultra HDR photo take 0.3 s instead of 5.8 s, with the same result.
  • composite() onto or from an image with a gain map is up to twice as fast: 1820x1820 with a blend mode like 'add' takes 0.22 s instead of 0.48 s, with the same result. Gain maps stay in sync with the pixels as before.

3.20.0

Choose a tag to compare

@matmen matmen released this 08 Nov 13:11
Immutable release. Only release title and notes can be modified.

New

  • image.quantize({ colors, palette, dither, ditherStrength }): reduces the image to a palette, in place: image.quantize({ colors: 16 }) for the 16 colors that best represent it, or a fixed one: image.quantize({ palette: [0x0f380fff, 0x306230ff, 0x8bac0fff, 0x9bbc0fff] }) for a Game Boy look. dither: 'floyd-steinberg' (or 'atkinson', or 'ordered' for a regular pattern) makes gradients look smooth with few colors. Transparent pixels keep their color and don't count towards the palette.
  • Image.renderSVG(svg, { images }): images for the <image> elements by their href, without data URIs: Image.renderSVG('<svg ...><image href="logo.png" .../></svg>', { images: { 'logo.png': bytes } }). The values are encoded images or Images; they are decoded only when the SVG uses them. Nothing is read from files or URLs.
  • image.colorMatrix(matrix): mixes the colors of every pixel with a color matrix, like SVG's feColorMatrix: greyscale, sepia, tints, swapped channels, color blindness simulation or any mix of them in one pass. Twenty values (four rows of [red, green, blue, alpha, offset], 0 to 1) or nine for just the colors: image.colorMatrix([0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0]) swaps red and blue.
  • image.transform(matrix, { origin, expand, mode }): moves every point of the image with an affine transform in one resampling: rotation, scaling, skew, flips and shifts in any mix and around any point. The six values are SVG's matrix(a b c d e f): image.transform([1, 0, -0.3, 1, 0, 0]) slants an image like italics. The image grows to hold the result unless expand: false, and mode is 'nearest-neighbor', 'bilinear' (default) or 'bicubic'.

3.19.0

Choose a tag to compare

@matmen matmen released this 08 Nov 12:10
Immutable release. Only release title and notes can be modified.

3.19.0

New

  • image.dropShadow({ x, y, blur, spread, color, expand }): a shadow behind the image, shaped by its alpha. Without an offset it is a glow: image.dropShadow({ blur: 8, spread: 2, color: 0xff2fb9ff }). The image grows to fit it, unless expand: false.
  • image.outline({ width, color, expand }): an outline around the image's shape, the sticker effect for cut-out images or an outline for rendered text. It follows the edge's antialiasing, like the strokeWidth of renderText() does.
  • Image.linearGradient(width, height, { stops, angle }) and Image.radialGradient(width, height, { stops, x, y, radius }): images with a gradient, with the angle like CSS' linear-gradient(). stops are positions and colors like Colors.gradient() takes them, or just the colors, evenly spaced.
  • image.recolor(otherImage): takes the colors from another image of the same size and keeps the alpha. With a gradient it fills a shape, e.g. gradient text: text.recolor(Image.linearGradient(text.width, text.height, { stops: [0x22d3eeff, 0xfb923cff], angle: 90 })).

3.18.0

Choose a tag to compare

@matmen matmen released this 08 Nov 10:49
Immutable release. Only release title and notes can be modified.

3.18.0

Changes

  • Resize and SVG scale modes are lowercase names: 'nearest-neighbor', 'bilinear', 'bicubic', 'area' and
    'scale', 'width', 'height', also as the values of ResizeModes and SVGScaleModes (3.17.0 gave them the old
    values). The old values ('RESIZE_BILINEAR', 1-3, still the values of the deprecated Image.RESIZE_* and
    Image.SVG_MODE_*) keep working until 4.0: image.resize(256, 256, 'bicubic').

  • Animation.decode() applies the orientation of APNGs and animated WebPs (their eXIf and EXIF chunks, and XMP
    in PNGs), like Image.decode() does (it already did for JPEG XLs). applyOrientation: false keeps the stored
    frames. Still PNGs decoded by Animation.decode() are rotated as well now.

New

  • Animation.decode() takes applyOrientation, toneMapping and gainMap like Image.decode(): JPEG XL
    animations can be tone mapped with 'clamp', and their jhgm gain map is read into the gainMap of every frame.
  • resize({ width }) and resize({ height }) on images, frames and animations: the missing side keeps the aspect
    ratio, resize({ width: 256, mode: 'bicubic' }).
  • image.coordinates() yields [x, y] and image.pixels() yields [x, y, color] for every pixel, row by row.
  • cropCircle({ size: 'contain' | 'cover' }): 'contain' (the default, as before) keeps the circle inside the image.
  • Image.renderText(text, { font, size, color, layout, ...style }): the text first, everything else in the options.
    layout is TextLayoutOptions or a TextLayout: Image.renderText('Hi', { font, size: 32, layout: { maxWidth: 400 } }).
  • Infinity as the loop count of animations that loop forever (new Animation(frames, { loopCount: Infinity }),
    and the encoders'). Animations that loop forever still have -1 as their loopCount (also the decoded ones).
  • Animation.webpEncoder(), gifEncoder() and apngEncoder() take the options of encodeWebP(), encodeGIF() and
    encodeAPNG(), plus loopCount: the options are documented once.
  • trim({ color, tolerance }) and colorToAlpha({ color, transparencyThreshold, opacityThreshold }): the color goes into the options, like the other options. GainMap.encode({ quality }) likewise.

Deprecated

  • Positional optional arguments, the options take the same values: trim(color, options), colorToAlpha(color, options), GainMap.encode(quality), resize/scale/contain/fit/cover with a mode argument (use ResizeOptions), Frame constructor and Frame.from() with duration and offsets (use FrameOptions), Animation constructor with loopCount (use AnimationOptions), Animation.decode(input, onlyExtractFirstFrame), Animation.resize(width, height, mode), Animation.encodeGIF(quality) and encodeAPNG(compression), encodePNG(compression, metadata) and encodePNG(metadata), encodeJPEG(quality), renderSVG(svg, size, mode), composite(source, x, y, options), rotate(angle, resize), sharpen(amount, radius), roundCorners(radius), fisheye(strength), dominantColor(ignoreBlack, ignoreWhite, threshold). All of them still work until 4.0.
  • The numeric frame disposal ids (0-3): use the names ('any', 'keep', 'previous', 'background'). Removed in 4.0.
  • ResizeModes.AUTO (and Image.RESIZE_AUTO, Frame.RESIZE_AUTO): pass only the width or the height instead.
    Removed in 4.0.
  • Iterating over an image (for (const [x, y] of image)) and iterateWithColors(): use coordinates() and
    pixels(). Removed in 4.0.
  • Image.renderText(font, size, text, ...): use renderText(text, { font, size, ... }). Removed in 4.0, as is the
    swapped use of horizontalAlign and verticalAlign values of TextLayout.
  • -1 as a loop count, use Infinity. Removed in 4.0, when animations that loop forever have Infinity.
  • AnimationEncoderOptions, WebPAnimationEncoderOptions, GIFAnimationEncoderOptions and
    APNGAnimationEncoderOptions: they are the same as AnimationEncodeOptions, WebPAnimationEncodeOptions,
    GIFEncodeOptions and APNGEncodeOptions with loopCount, which are all the types there are in 4.0.
  • encode(): use encodePNG(), which takes the same arguments. Removed in 4.0.
  • cropCircle(max, feathering) and the max option: use cropCircle({ size: 'contain' | 'cover' }). Removed in 4.0.

Documentation

  • Deprecated overloads are tagged in the reference as well: a Deprecated label sits in front of each one, with the replacement named below it, also for methods that have other overloads that aren't deprecated.

3.17.0

Choose a tag to compare

@matmen matmen released this 07 Nov 22:20
Immutable release. Only release title and notes can be modified.

3.17.0

New

  • Image.recolor(color): sets the RGB of every pixel to a color and keeps each pixel's alpha, for silhouettes
    and tints: image.recolor(0xffffffff) makes every pixel white, keeping its transparency.
  • AnimationEncoder.frameCount: how many frames were added. AnimationEncoder.frames is deprecated: it's a
    number, unlike Animation.frames, which is an array. It will be removed in 4.0.
  • Colors: the color helpers moved out of Image into their own class, with the same results:
    Colors.fromRGBA(), fromRGB(), fromHSLA(), fromHSL(), toRGBA(), toRGB(), toHSLA() (new: a color as
    HSLA values) and gradient().
  • ResizeModes, SVGScaleModes and FrameDisposalModes: the constants have classes of their own, with the same
    values: image.resize(256, ResizeModes.AUTO, ResizeModes.BICUBIC), Image.renderSVG(svg, 512, SVGScaleModes.WIDTH),
    new Frame(w, h, {disposalMode: FrameDisposalModes.BACKGROUND}).

Deprecated

  • Image.RESIZE_*, Image.SVG_MODE_* and Frame.DISPOSAL_*: use ResizeModes.*, SVGScaleModes.* and
    FrameDisposalModes.* (Image.RESIZE_AUTO becomes ResizeModes.AUTO). The old names keep working and will be
    removed in 4.0. They are plain read-only properties now instead of getters.
  • The absolute argument of opacity(), red(), green(), blue(), lightness(), saturation() (and the
    absolute option of AdjustOptions): these methods scale by default; setting a fixed value is removed in 4.0.
    For a fixed color, use recolor().
  • Image.rgbaToColor(), rgbToColor(), hslaToColor(), hslToColor(), rgbaToHSLA(), colorToRGBA(),
    colorToRGB() and gradient()
    : use the Colors methods of the same role (Colors.fromRGBA() and so on).

Documentation

  • imagescript.dev has a new look: the logo's colors, clearer type, signatures and
    parameters that are easier to scan, and a sidebar grouped into classes, functions, interfaces and types. Inherited
    members are hidden by default (Settings → Inherited shows them), deprecated APIs are tagged and grouped at the end.
  • Every parameter is described now.

3.16.1

Choose a tag to compare

@matmen matmen released this 06 Nov 23:41
Immutable release. Only release title and notes can be modified.

3.16.1

Performance

  • encodeJPEG() is about 4% faster again: 3.16.0's greyscale JPEG support (for gain maps) had slowed down its block
    loop. Output is unchanged, byte for byte.

3.16.0

Choose a tag to compare

@matmen matmen released this 06 Nov 20:48
Immutable release. Only release title and notes can be modified.

New

  • HDR photos (Ultra HDR / JPEG-R JPEGs, as phones take them: an SDR JPEG with a gain map saying how much brighter
    each part gets on HDR displays) stay HDR. Image.decode reads the gain map into the new image.gainMap (a
    GainMap: its pixels, decoded only when needed, and its parameters, from ISO 21496-1 or Adobe's hdrgm XMP), and
    encodeJPEG() writes it back with both kinds of metadata (Android, Chrome and Apple read them): decoding and
    re-encoding a photo keeps the gain map. Before, it was dropped and HDR photos came out SDR only.
    • The gain map follows the image: resizing, cover/contain/fit, cropping, region, trimming, rotating,
      flipping, fisheye and the Exif orientation transform it alike (at its own resolution, a quarter of the width
      and height in photos from phones).
    • Drawing keeps it right: what composite() puts over the image brings its own gain map (or none: no boost there),
      blended with the image's by the light of both; drawBox, drawCircle and fill leave no boost where they
      draw. Images without a gain map get one when an HDR image is composited onto them.
    • Color changes (brightness, contrast, gamma, lightness, red/green/blue, and for maps with a gain
      per channel also saturation, hueShift, greyscale, sepia) are made to the HDR rendition too, on values
      with SDR white as 1 and the highlights above it: what HDR displays don't boost changes like on SDR ones, the
      highlights change alike. The inverts leave no boost (the highlights would become the darkest parts); blur
      blurs the gain map alike.
    • new GainMap(image, options) makes gain maps (image.gainMap = ..., null removes it), GainMap.decode() and
      gainMap.encode() read and write one alone. encodeJPEG({ gainMap: { quality, scale } }) re-encodes the map
      at a quality or size, { gainMap: false } leaves it out; DecodeOptions.gainMap: false doesn't read it.
    • Files with the gain map cut off (some chat apps and uploads keep the index but drop the map) and broken gain map
      metadata decode as before, without one.
    • AVIFs and JPEG XLs carry gain maps too (AVIF 1.2's tone mapped item, JPEG XL's jhgm box): Image.decode reads
      them, encodeAVIF() and encodeJXL() write them (with the same gainMap option), so Ultra HDR JPEGs convert to
      HDR AVIFs and JPEG XLs a fraction of the size.
    • iPhone JPEGs carry Apple's own kind of gain map (with its headroom in Apple's XMP or Exif maker note): it's read
      too, converted to the same parameters, and written as an Ultra HDR gain map.

3.15.0

Choose a tag to compare

@matmen matmen released this 06 Nov 06:08
Immutable release. Only release title and notes can be modified.

3.15.0

New

  • effort applies to lossless encodeWebP() (and lossless animation frames) too: 0 is a fast mode (3 predictors,
    a short search for repeated pixels, one prefix code, no color transform), 1 is in between, 2 (the default) and
    up compress the most. A 1920x1080 photo with 8 threads: 0.20 s for 1.32 MB at effort 0, 0.57 s for 1.20 MB at 1,
    0.70 s for 1.22 MB at 2; a 1434x1434 graphic: 76 ms for 346 kB, 188 ms for 301 kB, 199 ms for 277 kB. Over a
    corpus of photos and graphics, effort 0 takes about a fifth (photos) to a third (graphics) of the time for files
    6-8% larger, effort 1 about two thirds for files up to 2% larger.

Performance

  • Lossless encodeWebP() makes photos about 3-4% smaller and graphics about 1.5%, and photos above a megapixel take
    less time (the 1920x1080 bench photo: 1.22 MB in 0.70 s instead of 1.34 MB in 0.80 s, with 8 threads): predicted
    images also try referencing only the pixel to the left and the one above, kept where that's estimated smaller
    (short matches further away take more bits than the pixels they replace; above a megapixel the estimate is made on
    three bands of rows, and the search for matches further away is left out).
  • cover() only resamples the part of the image it keeps instead of resizing all of it and then cropping: a 400x400
    thumbnail of a 1920x1080 photo takes 10 ms instead of 15.6 ms. The pixels are the same.
  • Bicubic resize() of opaque images is about 1.4 times as fast (a 1920x1080 photo to half: 31 ms instead of 43 ms):
    it leaves out the alpha channel and reads the source rows in order. The pixels are the same.
  • Decoding baseline (non-progressive) JPEGs takes less memory: the blocks are turned into pixels a row at a time
    instead of keeping every coefficient until the end (a 1920x1080 photo: 15 MB at the peak instead of 23 MB). The
    pixels are the same.

3.14.0

Choose a tag to compare

@matmen matmen released this 04 Nov 20:40
Immutable release. Only release title and notes can be modified.

3.14.0

New

  • Lossless encodeJXL() compresses like libjxl's cjxl -e 3 (effort 3) by default: about 15% smaller files for
    photos (a 1920x1080 photo: 1.17 MB instead of 1.39 MB) and 13% for graphics, within 1-2% of cjxl's sizes. It
    uses a palette of up to 1024 colors where it fits (else palettes of the channels that use few of their values),
    YCoCg and the weighted predictor in contexts of its error, and codes the groups in worker threads
    (JXLEncodeOptions.threads, the same files): the photo takes 0.24 s with 8 threads, 0.65 s with one. effort: 0
    keeps the fast mode, byte for byte what cjxl -e 1 writes (0.11 s for the photo); effort applies to lossless
    compression now too.

Performance

  • Lossy encodeJXL() is faster: a 1920x1080 photo at quality 90 takes 1.6 s instead of 2.3 s at effort 1 and 2.4 s
    instead of 4.8 s at effort 2 on one thread; with 8 threads 0.72 s instead of 0.81 s and 0.95 s instead of 1.46 s
    (3840x2160: 1.6 s instead of 2.0 s and 2.5 s instead of 4.1 s). The 4x8 and AFV transforms are computed directly
    instead of by a 64x64 matrix, the varblock search rounds without Math.round() (slow in V8), patch search skips
    large components after a quick labeling, and the search is split into smaller pieces for the threads. The files stay
    the same, except for rare coefficients the new transforms round differently (2 of 108 test encodes, both the same
    size).

Fixes

  • Invalid options throw a RangeError naming them before any work, instead of failing inside an encoder or being
    ignored:
    • encodeAVIF(): effort other than 0, 1 or 2 failed with a TypeError deep in the encoder, quality and
      alphaQuality outside 0-100 were accepted, and an unknown subsampling silently meant 4:4:4 (now a
      TypeError).
    • encodeWebP(), Animation.encodeWebP() and Animation.webpEncoder() check effort for lossless images too
      (which don't use it).
    • NaN is refused as GIF quality and APNG compression (out of range values are still clamped, like PNG's), as
      dominantColor()'s threshold and as renderSVG()'s size (which threw about the image's width); negative font
      sizes in renderText() (which rendered an empty image); maxPixels that isn't positive in Image.decode() and
      Animation.decode() (which reported every image as too large); fractional loop counts in new Animation().
  • Animation.decode() reports invalid threads and maxPixels as such, not as damaged image data.
  • Image.decode() refuses GIFs with a hint at Animation.decode() instead of "Unsupported image type".
  • SVG feDisplacementMap applies its scale once, like the specification and browsers (it multiplied by it twice, like
    resvg, so at typical scales the whole input was displaced out of the filter region and disappeared), and reads the
    map's colors unpremultiplied.