Skip to content

Releases

v3.1.0: WebP, APNG, TGA and TrueType hinting

Choose a tag to compare

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

This release adds three image formats, crisper small text and a format-independent animation API. Everything is still plain TypeScript without dependencies.

New

WebP (#19, #20)

  • Decoding of lossy, lossless, transparent and animated WebPs. The output is bit-exact with libwebp: every reference image of libwebp's official test suite matches.
    • Still WebPs: Image.decode()
    • Animated WebPs: Animation.decode()
  • Lossless encoding: Image.encodeWebP() and Animation.encodeWebP(). The files are typically 20–30% smaller than PNG.
  • Works in Deno too (jsr:@matmen/imagescript).

Animations and APNG (#6)

  • Animation, the new base class of GIF, works with every animated format:
    • Animation.decode() reads GIF, APNG and WebP; still images become a single frame.
    • encodeGIF(), encodeAPNG() and encodeWebP() write them.
  • APNG decoding and encoding with full alpha. Broken animations fall back to the default image, like browsers do. Image.decode() still returns an APNG's default image.

TGA (#21)

Decoding of color mapped, truecolor and greyscale TGAs, uncompressed or run length encoded, in all four orientations. Image.decode() recognizes them automatically. encodeTGA() writes them.

TrueType hinting (#31)

renderText() now runs the hinting instructions of TrueType fonts, so small text comes out crisp: stems and edges are aligned with the pixel grid, like on Windows. The results match FreeType point for point.

  • Glyphs of hinted fonts are placed on whole pixels.
  • Fonts without hinting instructions (and OpenType/CFF fonts) render exactly as before.

colorToAlpha() (#39)

Makes a color transparent, for example to remove a solid background, like GIMP's "Color to Alpha". Antialiased edges become semi-transparent instead of keeping a fringe of the old background.

  • Without a color, the top left pixel's color is used.
  • Thresholds control how close a color has to be to become fully transparent.

Deprecated

  • GIF: use Animation, which works the same and also handles APNG and WebP:

    Before Now
    new GIF(frames).encode() new Animation(frames).encodeGIF()
    GIF.decode() Animation.decode()

    GIF keeps working in 3.x and will be removed in 4.0.

Fixes

  • Text rendering: glyphs touching the left edge of their bitmap could come out inverted.

Upgrading

No code changes are needed. Text rendered with hinted TrueType fonts looks slightly different (crisper, on whole pixels). To keep the previous rendering, pass {hinting: false}:

Image.renderText(font, size, text, color, layout, {hinting: false})

v3.0.0

Choose a tag to compare

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

3.0.0

ImageScript is now written in TypeScript and has no WebAssembly or native binaries anymore. Every codec and renderer
was rewritten, the npm package shrank from 3 MB to about 120 kB, and it installs and runs on any platform Node.js
supports (including Alpine/musl and bundled serverless functions).

On JSR, @matmen/imagescript is now the same package as on npm (it used to be the Deno-only 1.x with WebAssembly),
published from the TypeScript sources with every release: deno add jsr:@matmen/imagescript.

Breaking changes

  • Node.js 22.13 or newer is required. The package is ES modules only; require('imagescript') keeps working
    through Node's require(esm).
  • All coordinates are 0-based. getPixelAt, setPixelAt, getRGBAAt, iteration, fill callbacks, drawBox
    and drawCircle used 1-based coordinates while crop and composite used 0-based offsets. Code that addressed
    pixels starting at 1 now has to subtract 1; out of range coordinates throw.
  • WebP encoding and encodeWEBP() were removed, use encode() (PNG) or encodeJPEG().
  • invertSaturation() and invertHue() were removed. invertHue() mirrored the hue (h -> 1 - h); for the
    complementary color use hueShift(180).
  • The experimental imagescript/v2 API was removed.
  • TextLayout alignment options are named correctly: horizontalAlign takes left/center/right,
    verticalAlign takes top/middle/bottom. Values passed under the old (swapped) names are still accepted.
    Alignment and wrapStyle: 'char' actually work now, which changes the output of code that set them.
  • GIF.encode(quality) scales the palette size: 1 = 8 colors, 50 = 55, 90 and above = 256.
  • GIF.decode returns the GIF's real loop count (it always returned -1); GIFs without a loop extension play once (0).
  • Resizing defaults to bilinear filtering (Image.RESIZE_BILINEAR) instead of nearest neighbor, which aliased
    badly when shrinking photos. Pass Image.RESIZE_NEAREST_NEIGHBOR for pixel art; it now samples the pixel centers,
    which shifts its output by up to a pixel. RESIZE_AUTO rounds the computed side instead of flooring it.
  • drawCircle(), cropCircle() and roundCorners() are antialiased. drawCircle(x, y, r) is centered on the
    center of pixel (x, y) and has a radius of exactly r. cropCircle(max, feathering): feathering is now the share
    of the radius (0..1) over which the edge fades out; it used to fade the whole circle.
  • dominantColor() groups the colors into clusters (median cut and k-means) and returns the average color of the
    largest one, instead of the most common bucket of a fine HSL histogram (which returned shifted colors, e.g. #fb0000
    for pure red, and let small flat areas beat large varied ones). bwThreshold (still 0-64) now ignores colors whose
    channels are all below / above the threshold, so dark saturated colors like navy are no longer discarded as black.
  • Text is laid out with subpixel precision and kerning (GPOS and kern tables). Glyph advances used to be rounded
    up to whole pixels, so text gets narrower and more evenly spaced.
  • encode() writes the smallest lossless PNG color type: a palette for images with up to 256 colors, otherwise
    greyscale or RGB, with an alpha channel only if needed. Rows of non-palette images are paeth filtered, which makes
    photos about 25% and gradients up to 95% smaller. Compression uses the runtime's native zlib when available
    (node:zlib), which is several times faster than the JS fallback.
  • JPEGs, TIFFs and PNGs are rotated/mirrored as their orientation tag says, like browsers display them. Photos
    taken with a rotated camera used to come out sideways. PNGs take it from the eXIf chunk, or without one from
    ImageMagick's Exif text or the XMP packet (which browsers ignore). Image.decode(data, {applyOrientation: false}) keeps the
    stored pixels.
  • rgbaToColor() / rgbToColor() round and clamp the components to 0..255. Out of range values used to wrap
    around (256 became 0) and fractions were truncated.
  • Frame.disposalMode returns the mode's name ('keep', ...), matching the Frame.DISPOSAL_* constants and what
    it is set to; it used to return the numeric id.
  • invertValue() inverts the HSV value (the brightest channel v becomes 255 - v, black becomes white). It used
    to invert HSL lightness, which invertLightness() does now.
  • rotate() is documented as counterclockwise, which it always was.
  • averageColor() weights pixels by their opacity.

Fixes

  • rotate(90) / rotate(270) scrambled non-square images (#29); free rotation lost the image edges
  • composite() darkened semi-transparent pixels drawn onto transparent ones
  • crop() read pixels from the next row when the region exceeded the image
  • drawBox() threw when the box was outside the image
  • cropCircle(max) ignored max
  • roundCorners() was asymmetric and did nothing to the right/bottom corners for fractional radii
  • Frame.resize() returned a frame without its duration, offsets and disposal mode; Frame.clone() returned an Image
  • GIF.slice() without an end, GIF.map() and GIF.filter() threw
  • GIF.resize() stretched partial frames to the full size
  • encode({...metadata}) ignored the metadata
  • fisheye() shifted the image by a pixel
  • HSL conversions truncated instead of rounding, so lightness(1), saturation(1) or hueShift(360) changed the
    image and repeated adjustments drifted darker; negative hues (e.g. hueShift(-300)) produced wrong colors
  • cover() could return an image a pixel smaller than requested, contain() one a pixel smaller than the box
  • Big endian TIFFs weren't recognized, and most TIFFs failed to decode
  • Text alignment was ignored (#37)
  • Assigning image.bitmap left getPixelAt() and other methods reading the old pixels
  • GIF frame durations were truncated to centiseconds (16 ms became 10 ms), they are rounded now
  • invert(), dominantColor(), rotate(), hueShift(), lightness(), saturation() and the other HSL based
    functions are several times faster; PNG decoding uses native zlib when available

New

  • blur(radius) (gaussian, radius = standard deviation like CSS' blur()) and sharpen(amount, radius) (unsharp mask)
  • brightness(factor), contrast(factor), gamma(gamma), greyscale() (ITU-R 601 luma, like Pillow) and sepia()
  • composite(source, x, y, {opacity, mode}) with the blend modes of CSS' mix-blend-mode (multiply, screen,
    overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion)
    and add
  • Outlined text: renderText(font, size, text, color, layout, {strokeWidth, strokeColor})
  • invertLightness(), which does what invertValue() used to do (invert HSL lightness)
  • encodePNG(), an alias of encode()
  • Options objects as an alternative to positional arguments: cropCircle({max, feathering}),
    dominantColor({ignoreBlack, ignoreWhite, threshold}), rotate(angle, {resize})
  • Frame.DISPOSAL_ANY; ImageType.isPNG() and the other checks accept any binary data, not only a DataView
  • Parameter names describe what they are: red/green/blue(factor), renderText(font, size, ...), fisheye(strength)
  • Resize modes Image.RESIZE_BILINEAR (default), Image.RESIZE_BICUBIC and Image.RESIZE_AREA, all usable with
    resize, scale, contain, fit, cover and GIF.resize

Codecs

  • PNG: all color types and bit depths, interlacing, tRNS transparency, checksum validation; inflating uses the
    runtime's native zlib when available
  • JPEG: baseline and progressive decoding (bit-exact with libjpeg-turbo), encoding with optimized Huffman tables
  • GIF: encoding with a median cut quantizer and dithering, tolerant decoding
  • TIFF: strips and tiles, LZW/deflate/PackBits/JPEG compression, 1-32 bit and float samples, CMYK and YCbCr, BigTIFF
  • Fonts: TrueType and OpenType (CFF) outlines; much faster for large fonts
  • SVG: shapes, paths, transforms, use/symbol, CSS, gradients, strokes, clip paths and masks. Text, images, filters
    and patterns are not rendered.

Full Changelog: v1.3.1...v3.0.0

1.4.0

Choose a tag to compare

@matmen matmen released this 14 Jun 19:17

Full Changelog: 1.3.0...1.4.0

v1.3.1

Choose a tag to compare

@matmen matmen released this 14 Jun 19:12

Full Changelog: v1.3.0...v1.3.1

1.2.15: documentation update

Choose a tag to compare

@matmen matmen released this 27 Nov 20:46
17afb48
chore: update domain

1.2.9: gif: override slice()

Choose a tag to compare

@matmen matmen released this 20 Jul 21:17
(cherry picked from commit 41532d2678b1a27905869638ec08e528541131b3)

1.2.7: backport TextLayout types fix

Choose a tag to compare

@matmen matmen released this 30 May 19:08
Co-Authored-By: Hideo Hattori <hhatto.jp@gmail.com>

1.2.6

Choose a tag to compare

@matmen matmen released this 07 May 15:30
drawBox: floor components

gif: support disposal modes

Choose a tag to compare

@matmen matmen released this 24 Apr 19:13
api: support disposal modes

(cherry picked from commit 3f09bcf2ff00550707940d920839c56e531435b4)

1.2.0

Choose a tag to compare

@matmen matmen released this 05 Apr 15:14
fix porting errors