New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@chimok/jpegtran-js

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@chimok/jpegtran-js

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

latest
Source
npmnpm
Version
0.9.0
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

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

pathwhat
src/jpegtran_wrapper.cthe exported C API: transform, perfect query, error reporting
src/index.tsthe wrapper source: createRotator() and the transform/copy/status constants
src/cli.tsthe CLI source
dist/index.jscompiled from src/index.ts, the entry point of the package
dist/index.d.tscompiled from src/index.ts, the type declarations of the package
dist/cli.jscompiled from src/cli.ts, the jpegtran-js binary
dist/jpegtran.mjs + dist/jpegtran.wasmthe WebAssembly build, produced by build.sh
tsconfig.jsontype checking (tsc, no emit)
tsconfig.build.jsonemits dist/index.js, dist/index.d.ts and dist/cli.js
build.shbuilds dist/jpegtran.mjs + dist/jpegtran.wasm from the libjpeg sources
test.tsdifferential test against the system jpegtran/djpeg
test/simple.tssmall smoke test on the bundled sample image
test/crop.tsdifferential test against jpegtran -crop
test/fixtures/the fixture corpus (see Test)
LICENSEthe 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:

  • 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        # 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.

Keywords

jpeg

FAQs

Package last updated on 16 Oct 2026

Related posts