libjpeg-turbo-dart

A pure-Dart port of libjpeg-turbo: JPEG decode, encode, and lossless transforms with output verified byte-for-byte against the C library. No native dependencies.

The port tracks the libjpeg-turbo 3.2.0 sources and transliterates the libjpeg API, so anyone familiar with jpeg_read_header / jpeg_start_decompress will feel at home — the same call sequences, the same knobs, the same behavior, down to the bit.

Why this exists

Every platform binding of libjpeg-turbo drags a native build along. This package is the other trade: a single-threaded, pure-Dart implementation that produces identical output — not "visually identical", but cmp-identical against djpeg, cjpeg, and jpegtran across a battery of several hundred cross-validation cases.

Features

All of the following are implemented and cross-validated against the native tools (or, where the CLI tools can't reach a code path, against C reference drivers compiled from the libjpeg-turbo sources):

Decompression

  • Baseline, progressive, and arithmetic-coded (sequential + progressive) JPEG, at 8-bit and 12-bit data precision
  • Lossless JPEG, all 7 predictors, point transform, 2–16-bit precision
  • Restart markers in all coding modes; best-effort decoding of truncated and corrupt streams (identical to djpeg, including block smoothing / DC interpolation for partial progressive files)
  • Scaled decoding, all factors N/8 for N = 1..16
  • Fancy and merged (-nosmooth) upsampling, all common subsamplings
  • Color quantization: 1-pass and 2-pass, Floyd-Steinberg / ordered / no dithering, plus external colormaps (djpeg -map parity)
  • Output color spaces: grayscale, RGB, CMYK/YCCK, RGB565 (with ordered dithering), and all extended pixel formats (extRgb, extBgra, extArgb, …) with alpha fill
  • Partial decompression (jpegSkipScanlines / jpegCropScanline), buffered-image mode, and a push-style suspending streaming source (jpegStreamSrc) for chunk-fed input
  • ICC profile extraction (multi-chunk reassembly)

Compression

  • Baseline, progressive, arithmetic, and lossless encoding; 8-bit and 12-bit lossy, 2–16-bit lossless
  • Optimized Huffman tables, custom scan scripts (cjpeg -scans parity), restart intervals, input smoothing, -baseline quant clamping
  • Input color spaces: grayscale, RGB, YCbCr, CMYK/YCCK, and all extended pixel formats
  • ICC profile embedding; growing in-memory destination

Lossless transforms (jpegtran parity)

  • Rotate/flip/transpose/transverse with -trim and -perfect, crop (including non-iMCU-aligned), wipe, drop, roll, grayscale reduction, marker copying, re-coding to progressive/arithmetic/optimized

Usage

Decode a JPEG from memory:

import 'dart:io';
import 'dart:typed_data';
import 'package:libjpeg_turbo_dart/libjpeg_turbo_dart.dart';

void main() {
  final jpeg = File('input.jpg').readAsBytesSync();

  final cinfo = JpegDecompress();
  cinfo.err = jpegStdError();
  jpegCreateDecompress(cinfo, jpegLibVersion, 0);
  jpegMemSrc(cinfo, jpeg);
  jpegReadHeader(cinfo, true);
  jpegStartDecompress(cinfo);

  final w = cinfo.outputWidth, h = cinfo.outputHeight;
  final nc = cinfo.outputComponents; // 3 for an RGB decode
  final pixels = Uint8List(w * h * nc);
  // Uint8List rows take a fast path in the color converters.
  final row = Uint8List(w * nc);
  while (cinfo.outputScanline < h) {
    final y = cinfo.outputScanline;
    jpegReadScanlines(cinfo, [row], 1);
    pixels.setRange(y * w * nc, (y + 1) * w * nc, row);
  }
  jpegFinishDecompress(cinfo);
  jpegDestroyDecompress(cinfo);
  // pixels now holds interleaved RGB scanlines.
}

Encode RGB pixels to a JPEG in memory:

final cinfo = JpegCompress();
cinfo.err = jpegStdError();
jpegCreateCompress(cinfo);
jpegMemDest(cinfo, 1 << 20); // initial buffer size; grows as needed

cinfo.imageWidth = w;
cinfo.imageHeight = h;
cinfo.inputComponents = 3;
cinfo.inColorSpace = JColorSpace.rgb;
jpegSetDefaults(cinfo);
jpegSetQuality(cinfo, 85, false);

jpegStartCompress(cinfo, true);
final row = Uint8List(w * 3);
while (cinfo.nextScanline < cinfo.imageHeight) {
  // ... fill row with interleaved RGB for scanline cinfo.nextScanline ...
  jpegWriteScanlines(cinfo, [row], 1);
}
jpegFinishCompress(cinfo);
final Uint8List bytes = jpegMemDestGetBuffer(cinfo);
jpegDestroyCompress(cinfo);

Errors surface as catchable JpegError exceptions (the port's equivalent of the C library's error_exit longjmp); recoverable corruption produces warnings and best-effort output, exactly as in C.

The tool/ directory contains cjpeg/djpeg/jpegtran-equivalent drivers (enc.dart, dec.dart, tran.dart, and friends) that double as usage references and as the cross-validation harness.

Verification

Correctness here means byte-for-byte identity with libjpeg-turbo, not approximate equivalence:

  • The test suite (dart test, ~290 tests) synthesizes a deterministic corpus with cjpeg at setup, then compares decodes bit-exactly against djpeg (including truncated/corrupt streams, scaling, quantization), encodes byte-identically against cjpeg across the option matrix, and transforms byte-identically against jpegtran. The tests skip cleanly when the native tools are not on PATH.
  • Paths the CLI tools cannot exercise (CMYK/YCCK in both directions, buffered-image output passes, extended pixel formats, dithered RGB565) are verified against small C reference drivers (tool/*.c) built from the no-SIMD libjpeg-turbo 3.2.0 sources, byte-comparing raw dumps and JPEG output in both directions.

Documented deviations:

  • -dct float at 12-bit precision differs from C by ≤1 LSB on a handful of samples (the port computes in float64, C in float32; the C sources themselves disclaim cross-machine float reproducibility). 8-bit float decode is byte-identical.
  • RGB565 output is supported at 8-bit precision only (as in the TurboJPEG API).
  • The port matches libjpeg-turbo 3.2.0 behavior; djpeg 3.1.x renders one specific mid-scan progressive truncation differently.

Performance

Single-threaded, AOT-compiled on Apple Silicon, decoding to Uint8List rows:

Case time
decode 768×1024 baseline 4:2:0 q90 ~13 ms
decode 1632×1224 progressive 4:2:0 ~46 ms
encode 768×1024 RGB → q85 4:2:0 ~22 ms

That is at parity with a scalar (no-SIMD) C build of libjpeg-turbo. The remaining ~3× gap to the stock SIMD-enabled library is hand-written NEON/SSE vector code, which no current Dart backend can express — this is close to the ceiling for a pure-Dart implementation.

Limitations

  • No SIMD (see above) and single-threaded, like the C library.
  • Developed and verified on the native Dart VM (JIT and AOT); dart2js/dart2wasm are untested.
  • The TurboJPEG-style wrapper (turbojpeg.dart) is a convenience layer that has not been through the same cross-validation as the libjpeg API; prefer the core API for now.

Architecture

The code follows the original libjpeg-turbo architecture with these key modules:

Module Files Description
Core types jpeglib.dart, jmorecfg.dart, jconfig.dart Data structures, type definitions
Error handling jerror.dart Error codes, message table, error manager
Memory manager jmemmgr.dart Pool-based allocation (simplified for Dart GC)
DCT/IDCT jfdct_*.dart, jidct_*.dart, jdct.dart Forward/inverse DCT algorithms
Huffman coding jchuff.dart, jdhuff.dart, jstdhuff.dart Huffman encode/decode
Arithmetic coding jcarith.dart, jdarith.dart, jaricom.dart Arithmetic encode/decode
Color conversion jccolor.dart, jdcolor.dart RGB↔YCbCr, CMYK↔YCCK, RGB565, extended formats
Sampling jcsample.dart, jdsample.dart, jdmerge.dart Chroma up/downsampling, merged upsampling
Compression jcapimin.dart, jcparam.dart, jcmaster.dart, … Full compression pipeline
Decompression jdapimin.dart, jdmaster.dart, jdmarker.dart, … Full decompression pipeline
Progressive jcphuff.dart, jdphuff.dart Progressive Huffman
Lossless jclhuff.dart, jdlhuff.dart, jcdiffct.dart, jddiffct.dart Lossless JPEG
Transforms transupp.dart jpegtran-style lossless transformations

License

This software is covered by two compatible BSD-style open source licenses, identical to the original libjpeg-turbo:

  • The IJG (Independent JPEG Group) License — applies to the libjpeg API library and associated code
  • The Modified (3-clause) BSD License — applies to the TurboJPEG API library and associated programs

See LICENSE and README.ijg for full license text. The zlib License section of libjpeg-turbo covers only its SIMD extensions, which are not part of this port.

Provenance

This package is a derivative work: a translation of the libjpeg-turbo 3.2.0 C sources (themselves based on the Independent JPEG Group's libjpeg) into Dart. Per the IJG license conditions, the original copyright notices are retained unaltered in every ported source file, the accompanying README.ijg file is included unaltered, and this section documents that the sources have been modified (translated from C to Dart, with Dart-specific restructuring and optimizations). The Dart port itself is

  • Copyright (C) 2026, Fredrik Smedberg

and that notice is layered on top of the original ones in each ported file, the same way libjpeg-turbo layers its notices over the IJG's. The port is distributed under the same licenses as the code it derives from.

This project is not affiliated with or endorsed by the libjpeg-turbo project or the Independent JPEG Group.

Acknowledgment

This software is based in part on the work of the Independent JPEG Group.

Libraries

libjpeg_turbo_dart