libjpeg_turbo_dart 1.1.0
libjpeg_turbo_dart: ^1.1.0 copied to clipboard
Pure-Dart port of libjpeg-turbo: JPEG decode, encode, and lossless transforms with output verified byte-for-byte against the C library. On web, requires WebAssembly.
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.
Platform support #
Android, iOS, Windows, macOS, Linux — and web via WebAssembly only.
The library needs 64-bit integers, which the Dart VM and dart2wasm have
and dart2js does not. On the web that means:
- Build with
flutter build web --wasm. A plainflutter build webwill not work. - Your users need a browser with WebAssembly garbage collection:
Chrome 119+, Firefox 120+, or Safari 18.2+. Older browsers load the
JavaScript fallback bundle that
--wasmalso emits, where the library throwsJpegUnsupportedRuntimeErrorrather than producing wrong pixels.
Under WebAssembly the output is byte-identical to native. Details and the graceful-degradation hook are in Flutter web and WebAssembly.
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
-mapparity) - 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
-scansparity), restart intervals, input smoothing,-baselinequant 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
-trimand-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.
Flutter web and WebAssembly #
The package runs on the web, but only when compiled to WebAssembly:
flutter build web --wasm
Under dart2wasm the output is byte-identical to the native VM — same
encoded JPEGs, same decoded pixels — because dart2wasm has true 64-bit
integers.
It does not work under dart2js, and this is not a small gap. When
Dart is compiled to JavaScript an int is a double, and the bitwise
operators are defined on unsigned 32-bit values: -1 >> 3 evaluates to
4294967295, 1 << 50 evaluates to 0, and Int64List is not
implemented at all. The fixed-point DCT and color-conversion tables, and
the reciprocal quantizer, all depend on the 64-bit behavior.
Calling jpegCreateCompress or jpegCreateDecompress from a JavaScript
build throws JpegUnsupportedRuntimeError immediately, rather than
producing corrupt pixels somewhere deeper in the pipeline.
One sharp edge worth knowing: flutter build web --wasm still emits a
JavaScript fallback bundle alongside the WebAssembly one, and a browser
without WebAssembly garbage collection will load that fallback. So a
correctly built app can still hit the unsupported path on an old browser.
In practice you need Chrome 119+, Firefox 120+, or Safari 18.2+. To branch
on this ahead of time — say, to hide a feature rather than let it throw —
check the exported constant:
if (isJavaScriptRuntime) {
// No JPEG support on this runtime; degrade gracefully.
}
Nothing in the library touches dart:io, so there is no separate web
entrypoint to import. Diagnostics from output_message go to stderr
natively and to the browser console on the web; assign
cinfo.err.outputMessage to redirect them.
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 withcjpegat setup, then compares decodes bit-exactly againstdjpeg(including truncated/corrupt streams, scaling, quantization), encodes byte-identically againstcjpegacross the option matrix, and transforms byte-identically againstjpegtran. The tests skip cleanly when the native tools are not onPATH. - 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 floatat 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.
- On the web, WebAssembly only — not JavaScript. See Flutter web and WebAssembly.
- 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.