-
Notifications
You must be signed in to change notification settings - Fork 0
Formats
Six formats, read and write. Five are pure TypeScript; WebP is libwebp compiled to WASM.
| Format | Read | Write | Alpha | Loaded |
|---|---|---|---|---|
| PNG | ✅ | ✅ | yes | always |
| JPEG | ✅ | ✅ | no | always |
| GIF | ✅ | ✅ | 1-bit | always |
| BMP | ✅ | ✅ | no (on write) | always |
| TIFF | ✅ | ✅ | yes | always |
| WebP | ✅ | ✅ | yes | on first use |
Options are typed per format. Passing one that does not exist is a compile error and a runtime error — the types are for you at 3pm, the validation is for production at 3am.
await SImg.fromBuffer(b).toFormat('png', { quality: 80 }).toBuffer();
// compile: Type 'number' is not assignable to type 'never'
// runtime: InvalidOptionError: png takes no optionsNo options. Lossless, alpha, and nothing to tune. toFormat('png').
Uses node:zlib rather than a bundled inflate — pako is ~45 KB and would be a third of the
whole size budget for something the runtime already has.
Reads: 8-bit and 16-bit, greyscale, palette, RGB, RGBA, interlaced (Adam7), tRNS
transparency. Writes 8-bit RGBA.
| Option | Type | Default |
|---|---|---|
quality |
1–100 | 82 |
background |
RGBA |
white |
82 is high enough that nobody complains about artifacts, low enough to be meaningfully smaller than 95, and roughly where the rest of the ecosystem sits.
JPEG has no alpha, so transparency is composited onto background at encode time.
EXIF orientation is applied on decode. A photo tagged "rotate 90" decodes rotated, so
what you get is what the user sees. probe() does not do this — it reports the stored
dimensions.
JPEG is the format the preview path is really about. It can downsample during decode by inverse-transforming only part of each block, which makes the decode genuinely cheaper rather than just throwing pixels away afterwards. See Previews and Performance.
Progressive JPEGs are not supported yet. They throw UnsupportedFormatError with a clear
message rather than producing garbage. This is a known gap, and real files are progressive.
| Option | Type | Default |
|---|---|---|
quality |
1–100 | 80 |
lossless |
boolean |
false |
WebP's quality scale is not JPEG's. The same number means something else. 80 is roughly where WebP sits for visually-lossless-ish photos.
Passing quality and lossless: true throws, rather than silently ignoring one of them.
You asked for two incompatible things and the library will not guess which you meant.
.toFormat('webp', { quality: 60 }) // lossy
.toFormat('webp', { lossless: true }) // lossless, and much bigger — often 30×Reads all three container flavours: VP8 (lossy), VP8L (lossless), VP8X (extended, with
alpha).
WebP is the only codec that is not there from the start. It arrives via a dynamic import()
of a base64'd WASM module, and it is 66 KB gzipped for the decoder, 168 KB for the encoder,
and 0 KB for anyone who touches neither.
supportedFormats().pending; // ['webp'] — not loaded, expected to work
await preload('webp'); // load it now, on your schedule
supportedFormats().read; // includes 'webp' either wayIf the load fails, WebP moves to unavailable and stays there. It does not retry on the
next file: a codec that failed to load will fail again, and retrying it once per image in a
batch of 500 turns one error into 500 slow ones.
| Option | Type | Default |
|---|---|---|
colors |
2–256 | 256 |
dither |
boolean |
true |
Not quality. Quality is a lossy-format dial; GIF's loss is quantisation, which is a
different thing and gets a different name.
colors is the palette size. dither is Floyd-Steinberg error diffusion — on by default,
because a photo quantised to 256 colours without it bands visibly.
Animation: reads the first frame only. Writes a single frame.
| Option | Type | Default |
|---|---|---|
background |
RGBA |
white |
Reads 24-bit, 32-bit, 16-bit (RGB565), and 8-bit palette. Writes 24-bit, so transparency is
composited onto background.
| Option | Type | Default |
|---|---|---|
compression |
'none' | 'lzw' |
'lzw' |
TIFF is lossless either way; this only trades size for speed.
Reads: uncompressed, LZW (with predictor), PackBits, greyscale, palette, RGB, RGBA, 16-bit, both endiannesses, striped and tiled.
Multi-page: reads the first page only.
TIFF's own orientation tag is not applied yet — TIFF carries the same tag number (274) as EXIF, but in its IFD rather than an APP1 segment, and only the JPEG path reads it today. Known gap.
await SImg.fromBuffer(jpeg).toFormat('webp', { quality: 80 }).toBuffer();Any format to any other. Two things to watch:
-
Alpha to no-alpha (PNG/WebP → JPEG/BMP): transparency is composited onto
background, white unless you say otherwise. - Photo to GIF: 16.7M colours down to 256. Dithering is on by default and it still will not look like the original, because it cannot.
HEIC, deliberately. It is a patent-encumbered codec in a container format, and a pure-TS decoder would be a project of its own — larger than the rest of this library put together, which is a straight contradiction of the 150 KB budget that justifies the whole thing.
Pure TypeScript image editing for Node and Bun. Core: 23.2 KB min+gzip, replacing a 7 MB ImageMagick bundle.
Using it
Working on it