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
jp2ksavepath. 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. |
Distroless static-link contract¶
The distroless image is the spec-load-bearing artifact. Its invariants:
- Zero
.soruntime dependencies.iiirisdis 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=...indeploy/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-pdfso 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/amd64andlinux/arm64ship per release viadocker buildxwith QEMU emulation. The manifest list is at$IMAGE:vX.Y.Zand:latest. - Runs as
nonroot(UID 65532 from the upstream distroless base). No shell, no apt, no package manager.kubectl execis 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, andLIBVIPS-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.
Relink contract (LGPL §6(d))¶
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.mdis updated in lockstep. Don't change the build without updating the pointer.
Test coverage¶
- HTJ2K functional smoke.
testdata/sample.jphis a bundled fixture (generated once by OpenJPH'sojph_compress); the integration testGET /iiif/3/sample.jph/full/max/0/default.jpgasserts 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-validatejob 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-smokejob 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 (
jp2ksavehandles 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
jp2kbackend 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 isgcr.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.