Repository navigation
v3.3.0: text in every script, color emoji, faster resizing
This release renders text in every script and color emoji, makes resizing and rotating a lot faster and hardens decoding and rendering against hostile input. There are no breaking API changes, but some results differ slightly from 3.2 (see Upgrading).
New
Text in every script
Image.renderText now shapes text like browsers do, through a port of HarfBuzz's OpenType shaping. On the tested texts, it produces the same glyphs and positions as HarfBuzz 14.6.
- Right to left and bidirectional text with the Unicode Bidirectional Algorithm: Arabic, Hebrew and others, mixed with left to right text and numbers, with mirrored brackets.
- Joining scripts: Arabic, Syriac, N'Ko, Mongolian, Adlam and others. Fonts without shaping features get presentation forms.
- Indic scripts: Devanagari, Bengali, Tamil, Telugu, Malayalam and the others, with conjuncts, reph and matra reordering.
- Other complex scripts: Khmer, Myanmar, Thai, Lao, Hangul, and the scripts of the Universal Shaping Engine, such as Sinhala, Tibetan, Javanese and Balinese.
- All GSUB and GPOS lookups: mark positioning, cursive attachment, kerning, and contextual substitutions and positioning. Fonts without GPOS kerning use their
kerntable. - Line breaking follows the Unicode Line Breaking Algorithm, so text also wraps in CJK and around punctuation, not only at spaces.
The Unicode 18.0 data and the shaping add about 80 kB (gzipped) to the package. Nothing is loaded at runtime: no WebAssembly and no native code.
Color emoji and fallback fonts
renderText takes several fonts in order of priority:
Image.renderText([textFont, emojiFont], 32, 'Hello 👋🏽 مرحبا');Every character comes from the first font that has it. The fallback font is chosen per grapheme cluster, so emoji sequences such as skin tones, flags, families and keycaps stay together.
Color glyphs keep their colors in all of these formats:
- COLR v0 and v1, including gradients and composite modes (Twemoji, Noto-COLRv1)
- CBDT bitmaps (Noto Color Emoji)
- sbix bitmaps (Apple's format)
Faster resizing and rotating
resize() (bilinear and area) and rotate() by angles that aren't multiples of 90° now compute in 8 bits by default.
| 1920×1080 photo | 3.2 | 3.3 |
|---|---|---|
| halve (opaque) | 15 ms | 12 ms |
| double (opaque) | 80 ms | 61 ms |
| halve (with alpha) | 43 ms | 24 ms |
| double (with alpha) | 260 ms | 103 ms |
| rotate 30° | 119 ms | 51 ms |
Opaque pixels differ from 3.2 by at most 1. {fast: false} keeps the previous, more precise results.
maxPixels
Image.decode and Animation.decode reject images over 268 megapixels (16383², like sharp) before allocating anything. A 250-byte JPEG could previously claim 65500×65500 pixels and take gigabytes. {maxPixels: Infinity} lifts the limit.
Performance
- Lossless WebP: encoding is about 40% faster, decoding about 15% faster. The output is identical.
- PNG decoding uses less memory: at most 785 MB instead of 1272 MB for an 8000×8000 RGBA image.
- Still PNGs in
Animation.decodedecode about 10 times as fast when they're large.
Fixes
- NaN and infinite arguments throw a
RangeErrorthat names the argument. They used to silently draw black, transparent or nothing. For example,Image.rgbToColor(NaN, 0, 0)used to return black. Numeric strings keep working. - Denial of service: SVG rendering no longer backtracks on long malformed styles,
url()references, lengths or colors. A 200 kB stylesheet without braces used to take a minute.<use>reference bombs, huge dash patterns and deep nesting are limited as well. - Infinite or huge blur and sharpen radii:
blur(Infinity)used to hang andblur(1e9)took minutes. - Damaged images and fonts throw a clear error instead of a
RangeError, and can no longer take unbounded memory or time. - Text outlines (
strokeWidth) follow the antialiased edges of the glyphs, so they are smooth. They no longer show through translucent text. - Empty text:
renderTextused to throw for text with nothing to show. It now renders a transparent image one pixel wide. - Unclosed CSS rules: an SVG style rule that isn't closed at the end of the stylesheet now applies, as it does in browsers.
- JSR docs no longer list internal members.
ImageScript is now also tested in Deno and Bun, on Linux, macOS and Windows.
Upgrading
No code changes are needed. Some results differ from 3.2:
- Bilinear and area resizing and rotating can produce pixel values off by 1, or by about 2 for translucent pixels. Pass
{fast: false}for the previous results. - NaN arguments throw instead of drawing nothing.
- Images over 268 megapixels need
{maxPixels: Infinity}to decode.