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 .wasm module (~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 whatever dart2js/dart2wasm itself adds.
  • No background-isolate offloading. On native platforms, decodeJ2k/ encodeJ2k are safe to call from a background isolate to keep the UI thread free. The web has no Isolate.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 dart2js and dart2wasm (dart test -p chrome and dart 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 via dart: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 .wasm is built with Emscripten's STANDALONE_WASM mode and loaded with a small, hand-written set of WASI shims in lib/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 (see tool/build_wasm.sh, which reports the exact missing import).
  • Rebuilding the .wasm requires the Emscripten SDK, which is not needed for ordinary native-platform use of this package. See the comment at the top of tool/build_wasm.sh for 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 by hook/build.dart on every platform — see OPENJPEG_LICENSE in that directory (BSD-2-Clause). opj_config.h/opj_config_private.h are 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 .c file via package:native_toolchain_c's CBuilder, 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.dart is ffigen-generated from src/openjpeg_ffi.h — regenerate via dart run ffigen --config ffigen.yaml if the shim header changes.
  • lib/src/types.dart — the shared Jp2kImage/exception types, used by both implementations below.
  • lib/src/decoder_io.dart — the native (dart:ffi) implementation, used wherever dart:io is available.
  • lib/src/decoder_web.dart — the web implementation: loads the embedded WebAssembly module via dart:js_interop, used wherever dart:js_interop is available (i.e. web).
  • lib/src/openjpeg_wasm_bytes.g.dart — generated by tool/build_wasm.sh; the compiled .wasm module (see web/openjpeg.wasm), base64-encoded so decoder_web.dart needs no separate asset fetch.
  • tool/build_wasm.sh — rebuilds web/openjpeg.wasm and lib/src/openjpeg_wasm_bytes.g.dart from 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