Skip to content

Releases

4.0.0-rc.2

4.0.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

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

ImageScript 4.0.0-rc.2

The second 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 and address its deprecation warnings, then follow
the migration guide.

What changed

  • Values that were wrapped, clamped or ignored throw: colors have to be integers from -2^31 to 2^32 - 1, also the ones a fill() function returns (they wrapped to 32 bits; negative ones, which bitwise operators return, keep working); pixel coordinates are truncated without wrapping at 2^32; compression is an integer 0-9 like effort (it was clamped); frame durations that GIF, APNG or WebP can't store throw a RangeError like loop counts (they were cut; unchanged frames merged by the encoders are split at the limit); frame.xOffset and frame.yOffset are truncated to integers when set; image.bitmap = view throws a TypeError for a view that doesn't start at a multiple of 4 bytes (it was copied, so changes to the view were lost); Colors.gradient() throws for a NaN position with any number of stops; encodeGIF() of an animation without frames throws like the other encoders.

  • Invalid choices throw a TypeError naming the valid ones: the resize mode (an Error, thrown once resizing started, so resize({ fit: 'cover' }) could move a frame's offsets before failing), composite()'s mode, flip(), disposalMode and wrapStyle (RangeErrors; an empty or null wrapStyle was the default).

  • No creation time and no version in encoded files by default: the encoders only write a creation time when metadata.creationTime is given, so the same image encodes to the same bytes, and the default software is 'ImageScript' without the version.

  • Drawing an SVG image that can't be drawn throws: an href that isn't a data URI or one of the images, an image that can't be decoded (with the decoding error as its cause), an unsupported data URI type or an embedded SVG that can't be read throw once they are drawn (images in <defs> or hidden ones don't); they drew nothing before.

  • renderSVG({ maxLayerPixels }) limits the layers SVG rendering uses at once (groups with opacity, masks, filters, ...; 16 bytes per pixel each), so nested ones can't multiply the memory of a small SVG: default 4 * maxPixels. renderSVG({ maxImagePixels }) limits the raster images an SVG references together (default maxPixels; each was limited on its own before).

  • renderText({ maxPixels }), default 16383², counting the outline, checked before anything is allocated.

See the migration guide for the full list of removed and reshaped APIs, behavior
changes, and migration examples.

4.0.0-rc.1

4.0.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

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

ImageScript 4.0.0-rc.1

The first 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 and address its deprecation warnings, then follow
the migration guide.

What changed

  • A more consistent API: methods that take options now use options objects instead of positional arguments.
    Resize modes and frame disposal modes are plain strings; color conversion helpers are grouped under Colors.
  • Unified frames and animations: Frame now contains an Image at frame.image; Animation stores frames in
    animation.frames instead of extending Array. The separate GIF class is replaced by Animation.
  • Flexible resizing: resize({ width, height, fit }) supports stretching, aspect-preserving inside, padded
    contain, and cropped cover behavior on images, frames, and animations.
  • More predictable animation handling: decode() recognizes animation markers, including one-frame animations;
    Image.decode() returns the first frame. Infinite looping is represented by Infinity instead of -1.
  • Behavior changes to check: rotate() now turns clockwise; HSL hue values use degrees; JPEG encoding composites
    transparency onto black by default; quantize() dithers by default; and size-changing operations on Frame move
    its offsets to keep it positioned in the animation.
  • Expanded encoding and image operations: single images can encode as GIF, GIF encoders expose color-count and
    dithering options, animations gain more image operations, and SVG rendering accepts fonts and embedded images.
  • Tighter types and validation: fixed-length tuple types, checked frame and gain-map values, and
    Promise<Uint8Array<ArrayBuffer>> encoder results improve type safety. The encoder result type requires TypeScript
    5.7 or newer.

See the migration guide for the full list of removed and reshaped APIs, behavior
changes, and migration examples.

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.