npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chimok/jpegtran-js

v0.9.0

Published

Lossless JPEG transformations (rotate, flip, crop) in the DCT domain as a WebAssembly module, built from IJG libjpeg 10.

Readme

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

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.

I will maintain and update this package, so don't worry because it's new.

Installation

npm install @chimok/jpegtran-js

Layout

| path | what | |---|---| | src/jpegtran_wrapper.c | the exported C API: transform, perfect query, error reporting | | src/index.ts | the wrapper source: createRotator() and the transform/copy/status constants | | src/cli.ts | the CLI source | | dist/index.js | compiled from src/index.ts, the entry point of the package | | dist/index.d.ts | compiled from src/index.ts, the type declarations of the package | | dist/cli.js | compiled from src/cli.ts, the jpegtran-js binary | | dist/jpegtran.mjs + dist/jpegtran.wasm | the WebAssembly build, produced by build.sh | | tsconfig.json | type checking (tsc, no emit) | | tsconfig.build.json | emits dist/index.js, dist/index.d.ts and dist/cli.js | | build.sh | builds dist/jpegtran.mjs + dist/jpegtran.wasm from the libjpeg sources | | test.ts | differential test against the system jpegtran/djpeg | | test/simple.ts | small smoke test on the bundled sample image | | test/crop.ts | differential test against jpegtran -crop | | test/fixtures/ | the fixture corpus (see Test) | | LICENSE | the IJG and MIT licences, plus the notes this project has to add | | jpeg/ | throwaway build copy of the libjpeg sources (regenerated by build.sh) |

Sources live in src/, everything the build emits lives in dist/. The whole of dist/ is in .gitignore — nothing generated is committed.

Build

Requires Emscripten. build.sh looks for emcc in the PATH and otherwise sources an emsdk_env.sh from the EMSDK environment variable, from /usr/share/emscripten or from ~/emsdk, so a toolchain that is only on the PATH of login shells works as well.

./build.sh

The libjpeg sources are taken from, in this order:

  1. a build tree that is already there under jpeg/,
  2. the source tree given in JPEG_SRC,
  3. 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        # use a local source tree
JPEG_URL=https://example.org/jpegsr10.zip ./build.sh   # use a different mirror

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.

JavaScript and types

dist/index.js, dist/index.d.ts and dist/cli.js are compiled from the TypeScript sources:

pnpm run build:js        # tsc -p tsconfig.build.json

pnpm run build runs both steps (./build.sh and build:js). The compiled files are not committed, so a fresh clone runs pnpm run build:js before the tests; CI builds first and publishes exactly what it tested.

The sources are written in "native" TypeScript: only erasable syntax is used (erasableSyntaxOnly enforces it), so Node runs src/index.ts, src/cli.ts and the tests directly by stripping the types — no loader, no bundler. Type checking is pnpm run typecheck.

Only the compiled output is published. files in package.json lists dist and src/jpegtran_wrapper.c; no .ts file is part of the package.

The type declarations in dist/index.d.ts are generated from src/index.ts, so the documented API and the types cannot drift apart.

Usage

import { createRotator, TRANSFORM, COPY } from "@chimok/jpegtran-js";
import { readFileSync, writeFileSync } from "node:fs";

const rotator = await createRotator();
console.log(rotator.version()); // "10  25-Jan-2026"

const input = readFileSync("photo.jpg");

// rotate 90 degrees clockwise, keeping Exif and every other APPn marker
const { data, perfect } = rotator.rotate(input, 90, { copy: COPY.all });
writeFileSync("photo-rotated.jpg", data);
console.log(perfect); // false when the image is not MCU aligned

Lossless cropping, on its own or combined with a transform:

// crop to 200x150 at (100, 50)
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); // the dimensions actually produced

// the full jpegtran -crop syntax is available as a string
rotator.crop(input, "200x150+100+50");
rotator.crop(input, "200x150-10-10"); // measured from the right/bottom edge
rotator.crop(input, "200x150f+100+50"); // "f" forces the exact size

// crop and rotate in one pass: the crop applies to the rotated image
rotator.transformDetailed(input, { transform: TRANSFORM.rotate90, crop: "200x150+0+0", copy: COPY.all });

Lower-level entry points:

rotator.perfectPossible(input, TRANSFORM.rotate90); // is the transform MCU aligned?
rotator.transform(input, { transform: TRANSFORM.rotate270, copy: COPY.all, perfect: false });
rotator.transformDetailed(input, { transform: TRANSFORM.none, crop: "100x100" }); // { data, width, height }

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.ts

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 tests compare against the system jpegtran on the fixture corpus in test/fixtures:

node test.ts [fixture-dir]       # default: test/fixtures
node test/crop.ts [fixture-dir]  # crop, on the same corpus

The corpus is deliberately small and covers every shape the tests rely on: 4:2:0 / 4:2:2 / 4:4:4, baseline and progressive, MCU-aligned and not, with and without Exif, plus one file that only pretends to be a JPEG. Another corpus can be passed as the first argument or through JPEGTRAN_FIXTURES.

Current result on that corpus:

  • byte-identical output to jpegtran for every fixture and every rotation (21/21),
  • 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,
  • jpegtran -crop and crop extension match in all 164 crop checks.

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.

The package carries both licences at the same time, which is what the SPDX expression IJG AND MIT says — it is AND, not OR, because the package contains both kinds of file:

  • dist/jpegtran.wasm and dist/jpegtran.mjs are compiled from IJG libjpeg and its transupp.c and stay under the IJG licence.
  • everything else — the TypeScript sources, the compiled JavaScript, the CLI, the tests and the build script — is under the MIT licence.

Redistribution is unproblematic: the IJG licence is a free software licence and, per the FSF licence list, compatible with the GNU GPL. Its condition (2) only asks that the acknowledgement above ("This software is based in part on the work of the Independent JPEG Group.") ends up in your documentation if you ship the compiled module, and that the names of the IJG authors are not used to endorse your product. The same terms apply to libjpeg-turbo and mozjpeg, so this is not something specific to this package.