third_party/tinyexr/jph.md
This note summarizes OpenEXR's High-Throughput JPEG 2000 support, usually named
HTJ2K or JPH, and outlines an implementation plan for TinyEXR v3 C. The local
reference inspected was /mnt/nvme02/work/openexr, especially:
src/lib/OpenEXRCore/internal_ht.cppsrc/lib/OpenEXRCore/internal_ht_common.cppsrc/lib/OpenEXRCore/internal_ht_common.hsrc/lib/OpenEXRCore/compression.ccmake/OpenEXRSetup.cmakeexternal/OpenJPHOpenEXR supports two HTJ2K compression modes:
HTJ2K256_COMPRESSION, enum value 10, using 256 scanlines per chunk.HTJ2K32_COMPRESSION, enum value 11, using 32 scanlines per chunk.The file-layout documentation describes both as lossless JPEG 2000 coding using the High-Throughput blocker. In OpenEXRCore these appear as:
EXR_COMPRESSION_HTJ2K256 = 10
EXR_COMPRESSION_HTJ2K32 = 11
Compression chunk height is selected in exr_compression_lines_per_chunk():
HTJ2K32: 32 scanlines.HTJ2K256: 256 scanlines.The generic compression dispatcher routes both modes to:
internal_exr_apply_ht()internal_exr_undo_ht()The actual JPEG 2000 codec is not implemented inside OpenEXR. OpenEXR uses OpenJPH, a separate BSD-2-Clause C++ HTJ2K implementation.
OpenEXR does not store a bare OpenJPH codestream directly. It prepends a small OpenEXR-specific HTJ2K header, then stores the JPEG 2000 codestream.
All integer fields are big-endian:
uint16_t magic = 0x4854 // 'H', 'T'
uint32_t payload_length
uint16_t channel_count
uint16_t cs_to_file_channel[channel_count]
optional opaque extension bytes up to payload_length
uint8_t jpeg2000_codestream[]
The channel map translates JPEG 2000 component index to OpenEXR file channel index. This is required because the encoder may reorder RGB channels so OpenJPH can apply JPEG 2000's reversible color transform.
OpenEXR's RGB detection is heuristic:
R/G/B or Red/Green/Blue, case-insensitive.main.R, main.G, main.B.R, G, B; remaining channels follow in file order.Encoding flow in internal_ht.cpp:
ojph::codestream.(0, 0) and image extent to the chunk width/height.128 x 325Decoding flow:
x_samples > 1 or y_samples > 1.OpenEXR has recent security fixes in this area. Its current decoder performs important validation for codestream/channel width mismatches and integer overflow before writing decoded pixels. TinyEXR should keep these checks from the start.
OpenEXR CMake finds OpenJPH >= 0.21.0 through CMake package config or
pkg-config. If not found, it uses the vendored external/OpenJPH copy. The
vendored OpenJPH is configured as a static dependency with executables disabled.
Licenses:
The OpenJPH license is permissive and compatible with TinyEXR's permissive licensing goals, but copied source files must retain their original copyright and license text.
.h/.cA true zstd-style extraction is not straightforward.
The zstd integration was practical because zstd is already C and can be reduced to a small C-facing subset. OpenJPH is different:
ojph::codestream,
ojph::mem_infile, and ojph::outfile_base.Therefore a minimal single tinyexr_jph.h + tinyexr_jph.c C11 codec is a
medium-to-large port, not a simple file extraction. It is possible, but it
should be treated as a separate codec-porting project with its own fuzzing and
conformance work.
The pragmatic first implementation should use OpenJPH through a tiny adapter. After behavior is correct and tested, a pure C11 port can be considered if the project still requires a no-C++ dependency.
Add TinyEXR constants for OpenEXR-compatible enum values:
EXR_COMPRESSION_HTJ2K256 = 10
EXR_COMPRESSION_HTJ2K32 = 11
Add line-per-chunk handling:
HTJ2K256: 256 linesHTJ2K32: 32 linesAdd parser/writer code for the small OpenEXR HT chunk header:
0x4854.channel_count.cs_to_file_channel[].payload_length.This part can be plain C11 and small.
Use a small TinyEXR adapter layer to isolate the C++ dependency:
src/exr_jph.c or src/exr_htj2k.c
deps/openjph/tinyexr_openjph_adapter.h
deps/openjph/tinyexr_openjph_adapter.cpp
extern "C".The adapter API should avoid exposing C++ types:
int tinyexr_jph_encode(
const struct tinyexr_jph_encode_desc *desc,
const void *packed_pixels,
size_t packed_size,
void *compressed,
size_t compressed_capacity,
size_t *compressed_size);
int tinyexr_jph_decode(
const struct tinyexr_jph_decode_desc *desc,
const void *compressed,
size_t compressed_size,
void *packed_pixels,
size_t packed_capacity);
This keeps TinyEXR's public API C-compatible while allowing the first backend to use upstream OpenJPH.
Build options:
TINYEXR_ENABLE_HTJ2K=OFF by default initially.TINYEXR_USE_SYSTEM_OPENJPH=ON/OFF.Add tests with OpenEXR-generated HTJ2K files:
HTJ2K32 and HTJ2K256.beauty.R/G/B.Round-trip tests should compare:
Fuzz the decoder entry point because JPEG 2000 codestream parsing is complex and OpenEXR has already had HTJ2K-related bug fixes.
Only start this if avoiding C++ is a hard requirement after the adapter backend works.
A realistic C11 port would need to extract and rewrite these OpenJPH areas:
Suggested constraints for a first C11 port:
128 x 32.5.Expected output shape if this phase succeeds:
deps/jph/tinyexr_jph.hdeps/jph/tinyexr_jph.cdeps/jph/LICENSE.OpenJPHThis port must preserve OpenJPH BSD-2-Clause copyright/license notices for any derived code and should document which OpenJPH revision was used.
Implement the OpenEXR-compatible HT header and TinyEXR codec dispatch in C11, then bind to OpenJPH behind a private C adapter. This is the shortest path to correct OpenEXR interoperability.
A single .h/.c pure C11 HTJ2K codec is possible in principle, but it is not a
small extraction like zstd. It should be planned as a second-stage port after
OpenJPH-backed behavior, compatibility tests, and fuzz targets are in place.