jpegtran-js
Lossless JPEG rotation as a WebAssembly module, built from the Independent JPEG Group's libjpeg,
release 10 (jpegsrc.v10.tar.gz) together with its transupp.c transform engine.
The quantized DCT coefficients are rearranged — never dequantized, never requantized — so a rotation
causes no quality loss and no generation loss. It is the WebAssembly equivalent of
I will maintaine and update this package, so don't worry because it's new.
jpegtran -rotate 90 -perfect -copy all -outfile output.jpg input.jpg
except that everything happens in memory (via jpeg_mem_src / jpeg_mem_dest): no filesystem, no
subprocess, no native binary.
Layout
src/jpegtran_wrapper.c | the exported C API: transform, perfect query, error reporting |
index.mjs | JavaScript wrapper (createRotator()) and the transform/copy/status constants |
build.sh | builds dist/jpegtran.mjs + dist/jpegtran.wasm from the libjpeg sources |
test.mjs | differential test against the system jpegtran/djpeg |
test/simple.mjs | small smoke test on the bundled sample image |
test/fixtures/lenna.jpg | the sample image (512×512, baseline 4:2:0) |
LICENSE | the IJG licence, plus the notes this project has to add |
jpeg/ | throwaway build copy of the libjpeg sources (regenerated by build.sh) |
dist/ | the build output |
Build
Requires Emscripten. On this machine it is a system package that is only on the PATH of login
shells, so build.sh sources it itself.
./build.sh
The libjpeg sources are taken from, in this order:
- a build tree that is already there under
jpeg/,
- the source tree given in
JPEG_SRC,
- the official release archive, which is downloaded from
https://www.ijg.org/files/jpegsr10.zip into jpegsr10.zip and unpacked under jpeg/.
JPEG_SRC=/path/to/jpeg-10 ./build.sh
JPEG_URL=https://example.org/jpegsr10.zip ./build.sh
The archive is kept in the project directory, so a second build does not download again. Two quirks of
the IJG archive are handled automatically: it is packed without Unix permissions and with CRLF line
endings, which would otherwise break the shebang of configure.
The result is tiny: ~176 KB wasm plus a 13 KB JS loader.
Usage
import { createRotator, TRANSFORM, COPY } from "./index.mjs";
import { readFileSync, writeFileSync } from "node:fs";
const rotator = await createRotator();
console.log(rotator.version());
const input = readFileSync("photo.jpg");
const { data, perfect } = rotator.rotate(input, 90, { copy: COPY.all });
writeFileSync("photo-rotated.jpg", data);
console.log(perfect);
Lossless cropping, on its own or combined with a transform:
const cropped = rotator.crop(input, { width: 200, height: 150, x: 100, y: 50 });
writeFileSync("photo-cropped.jpg", cropped.data);
console.log(cropped.width, cropped.height);
rotator.crop(input, "200x150+100+50");
rotator.crop(input, "200x150-10-10");
rotator.crop(input, "200x150f+100+50");
rotator.transformDetailed(input, { transform: TRANSFORM.rotate90, crop: "200x150+0+0", copy: COPY.all });
Lower-level entry points:
rotator.perfectPossible(input, TRANSFORM.rotate90);
rotator.transform(input, { transform: TRANSFORM.rotate270, copy: COPY.all, perfect: false });
rotator.transformDetailed(input, { transform: TRANSFORM.none, crop: "100x100" });
rotate() mirrors the previous jpegtran behaviour of jpegtran exactly: it reports whether the
transform was perfect (-perfect would have accepted it) and performs the transform either way,
which is what jpegtran does when -perfect is omitted.
Transform numbers: none, rotate90, rotate180, rotate270, flipHorizontal, flipVertical,
transpose, transverse.
Errors are thrown as TransformError with a status of STATUS.notJpeg, STATUS.notPerfect,
STATUS.badCrop, STATUS.library or STATUS.badArgument, plus the IJG message and its message code.
About cropping
Like jpegtran -crop, cropping is lossless, but it is not exact unless you ask for it: JPEG
coefficients are stored in iMCU blocks, so IJG silently moves the upper left corner of the region to
an iMCU boundary and grows the region so that it still covers what you asked for. The lower right
corner stays where it was. Add the f suffix (or use JCROP_FORCE semantics via the string spec) to
get exactly the requested dimensions instead. -perfect looks at the whole image, not at the crop
region, exactly like jpegtran.
Test
A quick smoke test on the bundled sample image:
node test/simple.mjs
It rotates test/fixtures/lenna.jpg, writes the results to test/out/ and checks that the output is
a valid JPEG, that the JFIF segment survives, that 90 + 270 degrees decodes back to the original
pixels, and that the bytes match jpegtran -rotate 90 -copy all exactly. The two comparisons need
djpeg/jpegtran and are skipped when those are not installed.
The thorough test compares against the system jpegtran on a whole fixture corpus:
node test.mjs [fixture-dir]
The corpus covers 4:2:0 / 4:2:2 / 4:4:4, baseline and progressive, MCU-aligned and not, with and
without Exif.
Current result on that corpus:
- byte-identical output to
jpegtran for every fixture and every rotation (201/201),
- the "perfect" predicate agrees with
jpegtran -perfect in every case,
- Exif and the other APPn markers survive unchanged,
- non-JPEG input is rejected with
STATUS.notJpeg.
Notes
-copy all must be requested explicitly. The IJG default is JCOPYOPT_COMMENTS, i.e. only COM
markers — Exif would be dropped. COPY.all maps to JCOPYOPT_ALL.
- The output uses the standard Annex K Huffman tables, like
jpegtran without -optimize, so it can
be slightly larger than the input. That is expected and still lossless.
- Exif orientation is a tag inside a copied marker; the transform does not rewrite it (same as
jpegtran). Rotating a photo that already carries an orientation tag leaves that tag untouched.
- 8-bit samples only (
BITS_IN_JSAMPLE == 8). 12/16-bit JPEG would need a second build.
Licence and provenance
This software is based in part on the work of the Independent JPEG Group.
The IJG licence is reproduced verbatim in LICENSE, together with the notes the licence
requires this project to add (which files are new and which IJG code is used unmodified).
- libjpeg release 10, 25-Jan-2026 — Independent JPEG Group (Thomas G. Lane, Guido Vollbeding),
including its
transupp.c transform engine. Used unmodified; the libjpeg sources are not vendored
in this repository.
- Emscripten 6.0.9 (system package), no SIMD/nasm — the IJG code is pure C, which is why it builds
for WebAssembly without changes.
test/fixtures/lenna.jpg is the classical Lenna test image, included only as a sample photograph
for the smoke test. It is not covered by the licence above.