openjpeg_ffi
Pure-Dart-package-shaped bindings to OpenJPEG
for encoding and decoding raw JPEG2000 (.j2k) codestreams. Native platforms
(macOS/iOS/Android/Windows/Linux) go through dart:ffi and Dart's
native-assets build hooks; web goes through a precompiled WebAssembly
module — either way, no manual native/web setup required by consumers.
General-purpose: it has no knowledge of any particular container format (TIFF, whole-slide-imaging pyramids, etc.) — it just decodes a raw J2K codestream to raw pixels, so any caller that can hand it codestream bytes can use it.
Usage
import 'package:openjpeg_ffi/openjpeg_ffi.dart';
final image = decodeJ2k(bytes); // Uint8List of a raw J2K codestream (starts FF4F)
print('${image.width}x${image.height}, ${image.numComponents} components');
decodeJ2k is synchronous and blocking (like any FFI call), but — unlike
dart:ui's image codecs — has no main-isolate restriction, so it's safe to
call from a background isolate to keep decode work off the UI thread.
Throws Jp2kDecodeException if the codestream is malformed or otherwise fails to decode.
Encoding a raw raster back to a J2K codestream works the same way:
final bytes = encodeJ2k(
pixels, // Uint8List, interleaved width*height*numComponents
width: width,
height: height,
numComponents: numComponents,
compressionRatio: 0, // 0 = lossless; e.g. 20 for ~20:1 lossy
);
compressionRatio: 0 (the default) is mathematically lossless (reversible
5-3 wavelet); any value > 0 selects lossy compression (irreversible 9-7
wavelet) targeting roughly that size-reduction ratio. numComponents must
be 1-4. Throws Jp2kEncodeException on failure.
Web support
On web, decodeJ2k/encodeJ2k run against OpenJPEG compiled to
WebAssembly instead of the native library. Because instantiating a
WebAssembly module is inherently asynchronous, call and await
initOpenJpegWasm() once — e.g. during app startup — before the first
decodeJ2k/encodeJ2k call:
import 'package:openjpeg_ffi/openjpeg_ffi.dart';
await initOpenJpegWasm(); // no-op on native platforms — safe to call unconditionally
final image = decodeJ2k(bytes);
Calling decodeJ2k/encodeJ2k on web before initOpenJpegWasm() has
resolved throws a StateError. initOpenJpegWasm() is idempotent and safe
to call multiple times/concurrently.
Things to be aware of before relying on this in production:
- No manual asset setup, at a size cost. The
.wasmmodule (~360 KB) is embedded directly in the package as a base64 string, so it needs no separate file to be copied or served by the consuming app — but it does add roughly that much (before gzip) to your compiled web output, on top of whateverdart2js/dart2wasmitself adds. - No background-isolate offloading. On native platforms,
decodeJ2k/encodeJ2kare safe to call from a background isolate to keep the UI thread free. The web has noIsolate.spawn— every call runs on whichever thread called it (the main thread, unless you've wired up your own Web Worker), so a large decode/encode can visibly block the page. There's no worker-offloading helper built into this package yet. - Verified in Chrome via both
dart2jsanddart2wasm(dart test -p chromeanddart test -p chrome -c dart2wasm), including a lossless/lossy encode → decode round trip and error handling on garbage input. It has not been verified against Firefox, Safari, or the whole-slide-imaging oracle fixture used by the native test suite (that test reads a file from disk viadart:io, which isn't available on web) — if precise pixel-for-pixel parity with the native decoder matters for your use case, verify it yourself against your own fixtures first. - Standalone module, not the usual Emscripten runtime. The
.wasmis built with Emscripten'sSTANDALONE_WASMmode and loaded with a small, hand-written set of WASI shims inlib/src/decoder_web.dart(fd_write,fd_close,fd_seek,environ_*) rather than Emscripten's usual generated JS glue — this keeps the bundle small and dependency-free, but means any future vendored-source change that needs a WASI import beyond those six will fail to instantiate until the shim list is extended (seetool/build_wasm.sh, which reports the exact missing import). - Rebuilding the
.wasmrequires the Emscripten SDK, which is not needed for ordinary native-platform use of this package. See the comment at the top oftool/build_wasm.shfor setup; it's only needed if you're modifying the vendored OpenJPEG sources or the C shim, not for consuming the package.
Only raw J2K codestreams
This decodes the raw codestream format (FF4F SOC marker), not the
box-structured .jp2 file format — some containers (e.g. TIFF-based
whole-slide-imaging tile storage) embed raw codestreams directly, which is
what this package targets. Feeding it a .jp2 file will fail to decode.
Project structure
src/openjpeg_ffi.h/.c— the shim: a small, deliberately narrow C API (jp2k_decode/jp2k_free_result,jp2k_encode/jp2k_free_encode_result) wrapping OpenJPEG's own much larger surface, so only a handful of functions ever need Dart bindings.src/vendor/openjp2/— OpenJPEG 2.5.4's core decode library (src/lib/openjp2/upstream), vendored as source and compiled fresh byhook/build.darton every platform — seeOPENJPEG_LICENSEin that directory (BSD-2-Clause).opj_config.h/opj_config_private.hare hand-resolved static headers (see their own comments) rather than CMake-generated, since this package bypasses OpenJPEG's own CMake build entirely.hook/build.dart— compiles the shim plus every vendored.cfile viapackage:native_toolchain_c'sCBuilder, targeting whatever platform is building (macOS/iOS/Android/Windows/Linux — no per-platform code needed).lib/openjpeg_ffi.dart— the public Dart API (decodeJ2k,encodeJ2k,initOpenJpegWasm,Jp2kImage,Jp2kDecodeException,Jp2kEncodeException), conditionally exporting the native or web implementation.lib/openjpeg_ffi_bindings_generated.dartisffigen-generated fromsrc/openjpeg_ffi.h— regenerate viadart run ffigen --config ffigen.yamlif the shim header changes.lib/src/types.dart— the sharedJp2kImage/exception types, used by both implementations below.lib/src/decoder_io.dart— the native (dart:ffi) implementation, used whereverdart:iois available.lib/src/decoder_web.dart— the web implementation: loads the embedded WebAssembly module viadart:js_interop, used whereverdart:js_interopis available (i.e. web).lib/src/openjpeg_wasm_bytes.g.dart— generated bytool/build_wasm.sh; the compiled.wasmmodule (seeweb/openjpeg.wasm), base64-encoded sodecoder_web.dartneeds no separate asset fetch.tool/build_wasm.sh— rebuildsweb/openjpeg.wasmandlib/src/openjpeg_wasm_bytes.g.dartfrom the vendored C sources via Emscripten. Only needed when changing the vendored sources or the C shim — see the "Web support" section above for its caveats.test/fixtures/— a real J2K tile extracted from a whole-slide-imaging sample file, plus its Pillow-computed expected decode, for a fast pixel-exact regression test that needs no large sample file (native test suite only — see the "Web support" section for why the web test suite doesn't use it).
Libraries
- openjpeg_ffi
- FFI/WebAssembly bindings to OpenJPEG for encoding and decoding raw JPEG2000 codestreams — see decodeJ2k, encodeJ2k, and initOpenJpegWasm.
- openjpeg_ffi_bindings_generated