Repository navigation
v3.0.0
·
197 commits
to master
since this release
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'srequire(esm). - All coordinates are 0-based.
getPixelAt,setPixelAt,getRGBAAt, iteration,fillcallbacks,drawBox
anddrawCircleused 1-based coordinates whilecropandcompositeused 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, useencode()(PNG) orencodeJPEG(). invertSaturation()andinvertHue()were removed.invertHue()mirrored the hue (h -> 1 - h); for the
complementary color usehueShift(180).- The experimental
imagescript/v2API was removed. TextLayoutalignment options are named correctly:horizontalAligntakesleft/center/right,
verticalAligntakestop/middle/bottom. Values passed under the old (swapped) names are still accepted.
Alignment andwrapStyle: '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.decodereturns 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. PassImage.RESIZE_NEAREST_NEIGHBORfor pixel art; it now samples the pixel centers,
which shifts its output by up to a pixel.RESIZE_AUTOrounds the computed side instead of flooring it. drawCircle(),cropCircle()androundCorners()are antialiased.drawCircle(x, y, r)is centered on the
center of pixel (x, y) and has a radius of exactlyr.cropCircle(max, feathering):featheringis 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.disposalModereturns the mode's name ('keep', ...), matching theFrame.DISPOSAL_*constants and what
it is set to; it used to return the numeric id.invertValue()inverts the HSV value (the brightest channelvbecomes255 - v, black becomes white). It used
to invert HSL lightness, whichinvertLightness()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 edgescomposite()darkened semi-transparent pixels drawn onto transparent onescrop()read pixels from the next row when the region exceeded the imagedrawBox()threw when the box was outside the imagecropCircle(max)ignoredmaxroundCorners()was asymmetric and did nothing to the right/bottom corners for fractional radiiFrame.resize()returned a frame without its duration, offsets and disposal mode;Frame.clone()returned an ImageGIF.slice()without an end,GIF.map()andGIF.filter()threwGIF.resize()stretched partial frames to the full sizeencode({...metadata})ignored the metadatafisheye()shifted the image by a pixel- HSL conversions truncated instead of rounding, so
lightness(1),saturation(1)orhueShift(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.bitmapleftgetPixelAt()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()) andsharpen(amount, radius)(unsharp mask)brightness(factor),contrast(factor),gamma(gamma),greyscale()(ITU-R 601 luma, like Pillow) andsepia()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)
andadd- Outlined text:
renderText(font, size, text, color, layout, {strokeWidth, strokeColor}) invertLightness(), which does whatinvertValue()used to do (invert HSL lightness)encodePNG(), an alias ofencode()- 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 aDataView- Parameter names describe what they are:
red/green/blue(factor),renderText(font, size, ...),fisheye(strength) - Resize modes
Image.RESIZE_BILINEAR(default),Image.RESIZE_BICUBICandImage.RESIZE_AREA, all usable with
resize,scale,contain,fit,coverandGIF.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