Repository navigation
Releases
Releases · imagescript/ImageScript
3.20.2
Fixes
- Loop counts that don't fit the file format are refused with a
RangeErrornaming the format's largest count, instead of wrapping around: a GIF withloopCount: 65536was written with 0, which plays forever (GIF and animated WebP store up to 65535 and 65534, APNG up to 2147483646). This applies toencodeGIF(),encodeWebP()andencodeAPNG()of animations and to the streaming encoders.
3.20.1
Performance
- Drawing on an image with a gain map (an HDR photo) is much faster:
drawBox()anddrawCircle()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
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 theirhref, without data URIs:Image.renderSVG('<svg ...><image href="logo.png" .../></svg>', { images: { 'logo.png': bytes } }). The values are encoded images orImages; 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'sfeColorMatrix: 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'smatrix(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 unlessexpand: false, andmodeis'nearest-neighbor','bilinear'(default) or'bicubic'.
3.19.0
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, unlessexpand: 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 thestrokeWidthofrenderText()does.Image.linearGradient(width, height, { stops, angle })andImage.radialGradient(width, height, { stops, x, y, radius }): images with a gradient, with the angle like CSS'linear-gradient().stopsare positions and colors likeColors.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
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 ofResizeModesandSVGScaleModes(3.17.0 gave them the old
values). The old values ('RESIZE_BILINEAR',1-3, still the values of the deprecatedImage.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), likeImage.decode()does (it already did for JPEG XLs).applyOrientation: falsekeeps the stored
frames. Still PNGs decoded byAnimation.decode()are rotated as well now.
New
Animation.decode()takesapplyOrientation,toneMappingandgainMaplikeImage.decode(): JPEG XL
animations can be tone mapped with'clamp', and theirjhgmgain map is read into thegainMapof every frame.resize({ width })andresize({ height })on images, frames and animations: the missing side keeps the aspect
ratio,resize({ width: 256, mode: 'bicubic' }).image.coordinates()yields[x, y]andimage.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.
layoutisTextLayoutOptionsor aTextLayout:Image.renderText('Hi', { font, size: 32, layout: { maxWidth: 400 } }).Infinityas the loop count of animations that loop forever (new Animation(frames, { loopCount: Infinity }),
and the encoders'). Animations that loop forever still have-1as theirloopCount(also the decoded ones).Animation.webpEncoder(),gifEncoder()andapngEncoder()take the options ofencodeWebP(),encodeGIF()and
encodeAPNG(), plusloopCount: the options are documented once.trim({ color, tolerance })andcolorToAlpha({ 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/coverwith amodeargument (useResizeOptions),Frameconstructor andFrame.from()with duration and offsets (useFrameOptions),Animationconstructor withloopCount(useAnimationOptions),Animation.decode(input, onlyExtractFirstFrame),Animation.resize(width, height, mode),Animation.encodeGIF(quality)andencodeAPNG(compression),encodePNG(compression, metadata)andencodePNG(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(andImage.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)) anditerateWithColors(): usecoordinates()and
pixels(). Removed in 4.0. Image.renderText(font, size, text, ...): userenderText(text, { font, size, ... }). Removed in 4.0, as is the
swapped use ofhorizontalAlignandverticalAlignvalues ofTextLayout.-1as a loop count, useInfinity. Removed in 4.0, when animations that loop forever haveInfinity.AnimationEncoderOptions,WebPAnimationEncoderOptions,GIFAnimationEncoderOptionsand
APNGAnimationEncoderOptions: they are the same asAnimationEncodeOptions,WebPAnimationEncodeOptions,
GIFEncodeOptionsandAPNGEncodeOptionswithloopCount, which are all the types there are in 4.0.encode(): useencodePNG(), which takes the same arguments. Removed in 4.0.cropCircle(max, feathering)and themaxoption: usecropCircle({ 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
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.framesis deprecated: it's a
number, unlikeAnimation.frames, which is an array. It will be removed in 4.0.Colors: the color helpers moved out ofImageinto their own class, with the same results:
Colors.fromRGBA(),fromRGB(),fromHSLA(),fromHSL(),toRGBA(),toRGB(),toHSLA()(new: a color as
HSLA values) andgradient().ResizeModes,SVGScaleModesandFrameDisposalModes: 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_*andFrame.DISPOSAL_*: useResizeModes.*,SVGScaleModes.*and
FrameDisposalModes.*(Image.RESIZE_AUTObecomesResizeModes.AUTO). The old names keep working and will be
removed in 4.0. They are plain read-only properties now instead of getters.- The
absoluteargument ofopacity(),red(),green(),blue(),lightness(),saturation()(and the
absoluteoption ofAdjustOptions): these methods scale by default; setting a fixed value is removed in 4.0.
For a fixed color, userecolor(). Image.rgbaToColor(),rgbToColor(),hslaToColor(),hslToColor(),rgbaToHSLA(),colorToRGBA(),
colorToRGB()andgradient(): use theColorsmethods 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
3.16.0
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.decodereads the gain map into the newimage.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,fisheyeand 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,drawCircleandfillleave 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 alsosaturation,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 = ...,nullremoves 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: falsedoesn'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.decodereads
them,encodeAVIF()andencodeJXL()write them (with the samegainMapoption), 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.
- The gain map follows the image: resizing,
3.15.0
3.15.0
New
effortapplies to losslessencodeWebP()(and lossless animation frames) too:0is a fast mode (3 predictors,
a short search for repeated pixels, one prefix code, no color transform),1is 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
3.14.0
New
- Lossless
encodeJXL()compresses like libjxl'scjxl -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 whatcjxl -e 1writes (0.11 s for the photo);effortapplies 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 withoutMath.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
RangeErrornaming them before any work, instead of failing inside an encoder or being
ignored:encodeAVIF():effortother than 0, 1 or 2 failed with aTypeErrordeep in the encoder,qualityand
alphaQualityoutside 0-100 were accepted, and an unknownsubsamplingsilently meant 4:4:4 (now a
TypeError).encodeWebP(),Animation.encodeWebP()andAnimation.webpEncoder()checkeffortfor lossless images too
(which don't use it).NaNis refused as GIFqualityand APNGcompression(out of range values are still clamped, like PNG's), as
dominantColor()'sthresholdand asrenderSVG()'s size (which threw about the image's width); negative font
sizes inrenderText()(which rendered an empty image);maxPixelsthat isn't positive inImage.decode()and
Animation.decode()(which reported every image as too large); fractional loop counts innew Animation().
Animation.decode()reports invalidthreadsandmaxPixelsas such, not as damaged image data.Image.decode()refuses GIFs with a hint atAnimation.decode()instead of "Unsupported image type".- SVG
feDisplacementMapapplies itsscaleonce, 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.