Skip to content

HTJ2K decode + distroless image

The contract iiiris maintains for JPEG 2000 / HTJ2K source images and the static-linked distroless container image that ships them. This spec replaces docs/briefs/HTJ2K.md now that the work has shipped and stabilised.

Codec scope

  • HTJ2K (High-Throughput JPEG 2000 — ISO/IEC 15444-15) decode is delivered via OpenJPEG ≥ 2.5.0 (decoder production-ready since May 2022). Same codec library iiiris uses for classic Part 1 JPEG 2000; no separate JP2 backend is maintained.
  • Decode only. OpenJPEG has no HTJ2K encoder (upstream issue #1487); iiiris accepts HTJ2K sources but cannot produce HTJ2K output. Classic Part 1 JP2 output is supported via the same jp2ksave path. Encode targets in IIIF are typically JPEG / PNG / WebP / classic JP2, so the encoder gap is rarely visible.

Accepted source forms

iiiris's image.Pipeline decodes all three through libvips's jp2kload:

  • Boxed .jp2 — classic Part 1 codestream wrapped in JP2 boxes.
  • Boxed .jph — HTJ2K codestream wrapped in JP2 boxes.
  • Raw J2K codestreams — bare codestreams without box framing (the default output of OpenJPH's ojph_compress).

No file-extension check is performed; the libvips loader sniffs the content. Operators can serve HTJ2K with any filename. MIME on the admin browse view stays image/jp2 for both classic JP2 and HTJ2K because libvips reports it that way; not a contract iiiris needs to adjust.

Container image tiers

Tagged releases produce three container artifacts (plus the Linux binary, Windows zip, and Windows MSI — see scaffold.md for the full release matrix):

Tag Base Linkage Audience
$IMAGE:vX.Y.Z / $IMAGE:latest gcr.io/distroless/static-debian12:nonroot Static libvips + OpenJPEG + transitive image-format libs Default. Smallest practical runtime; production hardening.
$IMAGE:vX.Y.Z-debian debian:bookworm-slim Dynamic via apt-installed libvips Operators who need a shell, apt, or a distroless-rollback path.
iiiris-ci:latest (local only) debian:bookworm-slim + Go + libvips + golangci-lint + IIIF validator harness Dynamic CI builder image used by every Linux job. Not published.

The distroless image is the spec-load-bearing artifact. Its invariants:

  • Zero .so runtime dependencies. iiirisd is statically linked against musl libc + libstdc++ + every transitive image-library dependency.
  • Pinned upstream versions. Each of OpenJPEG, libvips, libjpeg- turbo, libpng, libtiff, libwebp, glib, libxml2, fribidi, expat, zlib has a --build-arg <NAME>_VERSION=... in deploy/distroless/Dockerfile. Pinned defaults are documented in ../../LIBVIPS-SOURCE.md; operators can override a version to relink without source edits.
  • Trimmed libvips foreigners. Built with --without-heif --without-jxl --without-rsvg --without-poppler --without-magick --without-openslide --without-pdf so the static link surface stays compact and the riskiest CGO paths stay out of the binary. Kept: jpeg-turbo, png, tiff, webp, openjp2, zlib, glib2, expat, libxml2.
  • Multi-arch. Both linux/amd64 and linux/arm64 ship per release via docker buildx with QEMU emulation. The manifest list is at $IMAGE:vX.Y.Z and :latest.
  • Runs as nonroot (UID 65532 from the upstream distroless base). No shell, no apt, no package manager. kubectl exec is not supported on this image; use the debian-slim variant or an ephemeral debug pod.
  • Compliance bundle baked in at /usr/share/doc/iiiris/: LICENSE, NOTICE, THIRD_PARTY_LICENSES.md, and LIBVIPS-SOURCE.md. The last is the LGPL-2.1 §6(d) "info-to-relink" pointer that makes the static link legally redistributable. Don't remove or rename it — redistributing the static image without the relink pointer would break compliance.

Because the distroless image statically links libvips (LGPL-2.1-or-later), iiiris commits to the §6(d) "give the user enough to relink" requirement. The promise:

  • LIBVIPS-SOURCE.md (bundled in every distroless image and preserved in the repo root) lists every pinned upstream library with version, source URL, license, and build flags.
  • The Dockerfile's --build-arg <NAME>_VERSION=... mechanism makes relinking against a different version a one-command rebuild — no source patching required.
  • If the static-link recipe changes (different toolchain, different library set, different flags), LIBVIPS-SOURCE.md is updated in lockstep. Don't change the build without updating the pointer.

Test coverage

  • HTJ2K functional smoke. testdata/sample.jph is a bundled fixture (generated once by OpenJPH's ojph_compress); the integration test GET /iiif/3/sample.jph/full/max/0/default.jpg asserts 200 + valid JPEG body. OpenJPH itself is not in the build chain — only the resulting fixture is.
  • Validator coverage. The IIIF Image API validators (iiif-validate job in CI) exercise the v2 + v3 surfaces against the live build; HTJ2K decode rides through transparently because libvips treats it as a JP2 source.
  • Performance benchmarks vs classic JP2 are not part of the contract. The bench-smoke job covers the smoke-test path; deeper perf comparisons are operator-side concerns.

Out of scope

  • Encoding HTJ2K. OpenJPEG doesn't implement an HTJ2K encoder; iiiris doesn't either. If this changes upstream, the encoder path lands without an API change (jp2ksave handles output).
  • Alternative JP2 backends (Grok, openhtj2k as a separate library). One codec to maintain. Grok was explicitly rejected during design — its AGPL-3 license would propagate to the distributed binary and libvips can't use Grok as a jp2k backend without a patched fork.
  • Removing libvips. A "Grok rewrite of internal/image" was considered and dropped during HTJ2K design. libvips stays as the pipeline backbone.
  • Static-linking on glibc. Distroless uses static-debian12:nonroot; the C/C++ runtime is musl + libstdc++. If musl fights us, the documented fallback is gcr.io/distroless/cc-debian12 (glibc + libgcc) with libvips dynamically linked — still a meaningful improvement over the debian-slim image but no longer fully static. The fallback has never been needed in practice.