Library / SDK
matmen/ImageScript avatar
matmen/ImageScript

ImageScript: image manipulation with no dependencies at all

zero-dependency JavaScript image manipulation

680 stars34 forksTypeScriptNOASSERTION

At a glance

What is it?
ImageScript is a bitmap image library for JavaScript and TypeScript in which every codec and renderer is written from scratch. There is no WebAssembly, no native binary, no npm dependency, and no install script, and the package is around 160 kB compressed. It decodes and encodes PNG, JPEG, TIFF, WebP, TGA, GIF and APNG, renders SVG and vector fonts, and does all of it in one process.
Who is it for?
ImageScript fits a serverless function, a container with a musl based image, or a browser bundle where a native image library cannot be installed, and it fits a team that does not want to audit a tree of native dependencies. It does not fit a workload that needs the last drop of decoding speed, since the benchmarks in the repository exist precisely because a compiled library is faster.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What zero dependencies actually means here

The readme describes ImageScript as a zero-dependency alternative to the usual JavaScript image manipulation tools, and then specifies what that means in unusually concrete terms: every codec and renderer is written in TypeScript, with no WebAssembly, no native binaries, no npm dependencies, and no install scripts.

Those four exclusions are each a separate engineering decision, and the last two are the ones people usually forget to care about. A native binding means a prebuilt binary for every platform you ship to, and a mismatch means either a build step or a crash. An install script means code that runs with your privileges before you have read it, and it is the reason many teams have a policy against packages that have one. ImageScript has neither, so it installs the same way in a container, in a serverless function, and on a developer laptop.

The size is the other half of the claim. The package is about 160 kB compressed, which is a figure worth holding next to the install sizes in the benchmark table.

And the runtimes are named: Node.js 22.13 or newer, including Alpine with musl, ARM, and bundled serverless functions, plus Deno, Bun, and browsers. The musl and ARM mentions are the details that matter, because they are the two cases where a library that depends on a native binary usually breaks. The module works with both the import and the require syntax, and the type definitions are included rather than being written by the user.

The install is one line in each ecosystem.

sh
npm install imagescript              # Node.js, Bun and bundlers
deno add jsr:@matmen/imagescript     # Deno (npm:imagescript works too)

The project is MIT licensed and also offered under the AGPL, with both licence files in the repository, and the last push was on 2026-10-17.

The format coverage, and the limit that protects you

The decoding support is broad, and the readme lists it in detail rather than saying most formats.

For PNG there is grayscale, RGB, indexed colour, one to sixteen bit depth, and interlaced files, with and without an alpha channel. For JPEG there is baseline and progressive, in grayscale, RGB, and CMYK. For TIFF there are strips and tiles, the uncompressed, LZW, deflate, PackBits and JPEG compressed variants, and the BigTIFF extension. For WebP there is lossy, lossless, and with alpha, and the readme claims the output is bit-exact with the reference implementation. For TGA there is colour mapped, truecolour, and greyscale, uncompressed and run length encoded.

One behaviour is worth calling out because it is a correctness detail many libraries get wrong. JPEGs, TIFFs, PNGs, and WebPs are rotated as their Exif orientation says, and for PNGs the XMP orientation is used instead. A photo taken sideways comes out the right way up without the caller having to know anything about it.

And then there is the limit that matters for a service. Images larger than 268 megapixels are rejected before decoding, and the reason given is that small files declaring huge images cannot exhaust the memory. That is a decompression bomb defence, and it is checked at the header rather than after the pixels are allocated, which is the only place it can be checked cheaply.

The consequence for an adopter is that the format support is broad enough for real files rather than for the easy subset, and the failure mode on a hostile input is a refusal rather than an out of memory error.

Animations, and storing only what changed

The animation support covers GIF, APNG, and WebP, and the decoder gives you all frames, the durations, and the disposal modes, which are the three things you need in order to composite an animation correctly.

The disposal mode is the part that trips people up. It says what to do with the previous frame before drawing the next one, and getting it wrong produces the classic artefact where a frame bleeds into the next. A library that exposes the modes and does the compositing for you is doing something a hand-rolled decoder rarely gets right.

On the encoding side, the library produces GIF, APNG, and lossless WebP, and the readme describes the approach as storing only what changes from frame to frame, which is how all three formats work when they are used well, and which is the difference between a small animation and a large one.

The 3.2 release note is about smaller animations, which suggests the encoder was tightened at that point, and the 3.3 note is about text in every script, which is a separate feature rather than a codec one.

The release cadence is worth noting on its own. Three releases in two days in October 2026, with notes about faster resizing, faster codecs, blur and GIF decoding, smaller animations, options objects, text rendering, and colour emoji, is a project in the middle of an active rewrite of its own internals, with the version numbers moving quickly enough that the changelog is the thing to read before upgrading.

SVG rendering and fonts, and what each one leaves out

Two of the features are vector work, and both have explicit limits that are worth knowing before you rely on them.

The SVG renderer handles shapes, paths, gradients, strokes, clip paths, masks, and CSS, and it does not handle text, images, or filters. That is a large subset of what SVG is used for in practice, and the omissions are the three that need rasterisation of something else or a much larger renderer.

The font renderer is the more ambitious of the two. It handles TrueType and OpenType, and the readme lists complex text shaping in every script, right to left and bidirectional text, outlines, TrueType hinting, fallback fonts, and colour emoji. Shaping in every script means the library does the joining and positioning work itself rather than assuming that the characters are independent, and the bidirectional and right to left cases are where naive text rendering fails visibly.

The hinting support is worth a note, because hinting is the part of font rendering that exists to make small text legible on a low resolution display, and it is a large amount of work to do properly. The credits section says the TrueType hinting follows the interpreter of a well known free font library, which is a sensible way to get it right.

Between them, the two renderers make the library useful for the kind of task that usually needs a headless browser or a native canvas binding: putting a label on a thumbnail, or rendering a diagram, in a runtime where neither is available.

The API, the coordinates, and what version 3 broke

The example in the readme is four lines of logic, and the shape of it is the API in miniature.

js
import { Image } from 'imagescript';
import { readFile, writeFile } from 'node:fs/promises';

const image = await Image.decode(await readFile('input.jpg'));
image.resize(256, Image.RESIZE_AUTO).roundCorners(16);
await writeFile('output.png', await image.encode());

The pattern is decode, mutate in place by chaining, encode. The resizing call takes a width and a mode constant, with an automatic mode that preserves the aspect ratio, and the rounding call takes a corner radius. The operations are methods on the image rather than functions that return a new image, which is what makes the chain read the way it does.

One rule is stated plainly and catches people: all coordinates are zero-based, and the origin is the top left pixel. Version 3 changed this, along with removing lossy WebP encoding and raising the Node requirement.

The readme also explains the gap in the version history. There is no 2.x release, because the number was skipped to leave room for an old experimental API published under a different path, and the upgrade notes for people coming from 1.x are in the changelog.

The feature list covers the rest of the surface: resizing in four modes, from bilinear through bicubic and area averaging to nearest neighbour, the manipulation operations such as cropping, rotating, circular cropping, rounded corners, and trimming, the filters such as blur, sharpen, brightness, and hue shifting, compositing with opacity and blend modes, background removal by turning a colour into transparency, and the colour information functions such as the average and dominant colour.

The benchmarks, the licence, and the borrowed algorithms

Three things in the repository are worth a reader's attention, and one of them is a warning.

The benchmark directory compares ImageScript against Jimp, image-js, Photon, sharp, and the native canvas binding, across decoding, encoding, image operations, rendering, the size of the encoded files, and the size of the install. It runs for every release, which is the important detail, because a benchmark that is only run once is a number rather than a guardrail.

The second is the licence, and there is a subtlety in it. The package metadata declares a disjunction: the AGPL version 3 or later, or the MIT licence, and the repository carries both licence files. So the choice is yours, and the point to notice is that the two are not equivalent in what they oblige you to do.

The third is the credits, and they explain where the code came from. The compression implementation is derived from a small JavaScript compression library and the checksum from a JavaScript CRC implementation, both by one author. The JPEG codec is based in part on the work of the independent JPEG group and on a PDF library. The glyph rasteriser is translated from a font rasteriser written in Rust. The WebP codecs follow two RFCs and reproduce the colour conversion of the reference implementation. The TrueType hinting follows the interpreter of a well known font engine, with the copyright notice called out, and the text shaping is ported from a shaping engine using the Unicode character database.

So this is not a from-scratch invention, and the readme says so plainly. It is a careful reimplementation in one language of a set of well known algorithms, which is exactly the kind of project that has to be explicit about provenance, and the repository carries a separate file with the third party licences.

Editorial conclusion

ImageScript fits a serverless function, a container with a musl based image, or a browser bundle where a native image library cannot be installed, and it fits a team that does not want to audit a tree of native dependencies. It does not fit a workload that needs the last drop of decoding speed, since the benchmarks in the repository exist precisely because a compiled library is faster. Verify first that the formats you need are in the supported list, since the codec set is broad but not complete, that the platform is one of the supported runtimes with a recent enough Node, and that the licence choice suits you, because the package is offered under both the AGPL and the MIT terms and picking the wrong one is easy to do in a hurry.

Frequently asked questions

What is ImageScript?

It is a JavaScript and TypeScript library for bitmap image manipulation with no dependencies at all. Every codec and renderer is written in TypeScript, with no WebAssembly, no native binaries, no npm dependencies, and no install scripts, and the package is about 160 kB compressed.

Which image formats does ImageScript support?

For decoding, PNG in grayscale, RGB, indexed, one to sixteen bit and interlaced forms, JPEG in baseline and progressive and CMYK, TIFF with strips and tiles and several compression schemes including BigTIFF, WebP in lossy and lossless forms, and TGA. It also decodes GIF, APNG, and WebP animations, and it can encode all of them apart from lossy WebP.

Which runtimes does ImageScript work on?

Node.js 22.13 or newer, including Alpine with musl, ARM, and bundled serverless functions, plus Deno, Bun, and browsers. It works with both the import and the require syntax, and it includes type definitions.

What does ImageScript do about malicious image files?

Images larger than 268 megapixels are rejected before decoding, so that a small file declaring a huge image cannot exhaust the memory of the process. The limit is exposed as the maxPixels option.

How is ImageScript licensed?

The package metadata offers a disjunction of the AGPL version 3 or later, or the MIT licence, and the repository contains both licence files. The credits also record the third party code the codecs derive from, including a compression library, a JPEG library, a PDF library, a font rasteriser, the WebP reference implementation, a font engine, and a shaping engine, with their licences in a separate notices file.

Official sources

  1. Issues
  2. matmen/ImageScript on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/matmen-imagescript.svg)](https://hysenlabs.com/projects/matmen-imagescript)

Was this useful?