diff --git a/lib/interface/encode.c b/lib/interface/encode.c index edbd3c9..8443112 100644 --- a/lib/interface/encode.c +++ b/lib/interface/encode.c @@ -37,7 +37,9 @@ extern int EncodeArray( int use_mct, PyObject *compression_ratios, PyObject *signal_noise_ratios, - int codec_format + int codec_format, + int add_tlm, + int add_plt ) { /* Encode a numpy ndarray using JPEG 2000. @@ -64,6 +66,10 @@ extern int EncodeArray( The format of the encoded JPEG 2000 data, one of: * ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream * ``1`` - OPJ_CODEC_JP2 : JP2 file format + add_tlm : int + Add tile-part data length markers (TLM). Supported values 0-1. + add_plt : int + Add packet length tile-part header markers (PLT). Supported values 0-1. Returns ------- @@ -411,6 +417,24 @@ extern int EncodeArray( goto failure; } + const char* extra_options[3] = { NULL, NULL, NULL }; + int extra_option_index = 0; + if (add_plt) { + extra_options[extra_option_index] = "PLT=YES"; + extra_option_index += 1; + } + if (add_tlm) { + extra_options[extra_option_index] = "TLM=YES"; + extra_option_index += 1; + } + if (extra_option_index > 0) { + if (! opj_encoder_set_extra_options(codec, extra_options)) { + py_error("Failed to set extra options on the encoder"); + return_code = 28; + goto failure; + } + } + /* Send info, warning, error message to Python logging */ opj_set_info_handler(codec, info_callback, NULL); opj_set_warning_handler(codec, warning_callback, NULL); diff --git a/openjpeg/_openjpeg.pyx b/openjpeg/_openjpeg.pyx index 2f5fc33..b6fd045 100644 --- a/openjpeg/_openjpeg.pyx +++ b/openjpeg/_openjpeg.pyx @@ -32,6 +32,8 @@ cdef extern int EncodeArray( PyObject* compression_ratios, PyObject* signal_noise_ratios, int codec_format, + bint add_tlm, + bint add_plt, ) cdef extern int EncodeBuffer( PyObject* src, @@ -213,6 +215,8 @@ def encode_array( List[float] compression_ratios, List[float] signal_noise_ratios, int codec_format, + bint add_tlm, + bint add_plt, ) -> Tuple[int, bytes]: """Return the JPEG 2000 compressed `arr`. @@ -239,6 +243,10 @@ def encode_array( * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) * ``1``: A boxed JPEG 2000 codestream (JP2 format) + add_tlm : bool + If ``True`` then add tile-part length markers (TLM) to the codestream. + add_plt : bool + If ``True`` then add packet length tile-part header markers (PLT) to the codestream. Returns ------- @@ -318,6 +326,8 @@ def encode_array( compression_ratios, signal_noise_ratios, codec_format, + add_tlm, + add_plt, ) return return_code, dst.getvalue() diff --git a/openjpeg/tests/test_encode.py b/openjpeg/tests/test_encode.py index 6afd39a..5c364c3 100644 --- a/openjpeg/tests/test_encode.py +++ b/openjpeg/tests/test_encode.py @@ -70,6 +70,57 @@ def parse_j2k(buffer): return param +def parse_codestream_markers(buffer): + offset = 0 + markers = [] + while offset < len(buffer): + symbol_code_bytes = buffer[offset: offset + 2] + marker = unpack(">H", symbol_code_bytes)[0] + offset += 2 + if marker == 0xFF4F: + markers.append("SOC") + elif marker == 0xFF51: + markers.append("SIZ") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF52: + markers.append("COD") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF55: + markers.append("TLM") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF58: + markers.append("PLT") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF5C: + markers.append("QCD") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF64: + markers.append("COM") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF90: + markers.append("SOT") + length_bytes = buffer[offset: offset + 2] + length = unpack(">H", length_bytes)[0] + offset += length + elif marker == 0xFF93: + markers.append("SOD") + # If we get to here, we have the marker info we need + break + else: + raise Exception(f"unexpected marker: 0x{marker:04X}") + return markers class TestEncode: """Tests for encode_array()""" @@ -700,7 +751,84 @@ def test_jp2(self): buffer = encode_array(arr, codec_format=1) assert buffer.startswith(b"\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a") + + def test_no_tlm_or_plt_explicit(self): + """Test encoding with no TLM or PLT, explicitly disabled""" + rows = 123 + cols = 234 + bit_depth = 8 + maximum = 2**bit_depth - 1 + dtype = f"u{math.ceil(bit_depth / 8)}" + arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype) + buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_tlm=False, add_plt=False) + out = decode(buffer) + markers = parse_codestream_markers(buffer) + assert "TLM" not in markers + assert "PLT" not in markers + assert np.allclose(arr, out, atol=5) + def test_no_tlm_or_plt_default(self): + """Test encoding with no TLM or PLT, default options""" + rows = 123 + cols = 234 + bit_depth = 8 + maximum = 2**bit_depth - 1 + dtype = f"u{math.ceil(bit_depth / 8)}" + arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype) + buffer = encode_array(arr, compression_ratios=[4, 2, 1]) + out = decode(buffer) + markers = parse_codestream_markers(buffer) + assert "TLM" not in markers + assert "PLT" not in markers + assert np.allclose(arr, out, atol=5) + + + def test_tlm(self): + """Test encoding with TLM""" + rows = 123 + cols = 234 + bit_depth = 8 + maximum = 2**bit_depth - 1 + dtype = f"u{math.ceil(bit_depth / 8)}" + arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype) + buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_tlm=True) + out = decode(buffer) + markers = parse_codestream_markers(buffer) + assert "TLM" in markers + assert "PLT" not in markers + assert np.allclose(arr, out, atol=5) + + def test_plt(self): + """Test encoding with PLT""" + rows = 123 + cols = 234 + bit_depth = 8 + maximum = 2**bit_depth - 1 + dtype = f"u{math.ceil(bit_depth / 8)}" + arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype) + buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_plt=True) + out = decode(buffer) + markers = parse_codestream_markers(buffer) + assert "TLM" not in markers + assert "PLT" in markers + assert np.allclose(arr, out, atol=5) + + def test_tlm_and_plt(self): + """Test encoding with both TLM and PLT""" + rows = 123 + cols = 234 + bit_depth = 8 + maximum = 2**bit_depth - 1 + dtype = f"u{math.ceil(bit_depth / 8)}" + arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype) + buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_plt=True, add_tlm=True) + out = decode(buffer) + param = parse_j2k(buffer) + assert param["precision"] == bit_depth + markers = parse_codestream_markers(buffer) + assert "TLM" in markers + assert "PLT" in markers + assert np.allclose(arr, out, atol=5) class TestEncodeBuffer: """Tests for _openjpeg.encode_buffer""" diff --git a/openjpeg/utils.py b/openjpeg/utils.py index 704f533..3f6b973 100644 --- a/openjpeg/utils.py +++ b/openjpeg/utils.py @@ -434,6 +434,8 @@ def encode_array( compression_ratios: Union[List[float], None] = None, signal_noise_ratios: Union[List[float], None] = None, codec_format: int = 0, + add_tlm: bool = False, + add_plt: bool = False, **kwargs: Any, ) -> bytes: """Return the JPEG 2000 compressed `arr`. @@ -523,6 +525,13 @@ def encode_array( * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) * ``1``: A boxed JPEG 2000 codestream (JP2 format) + add_tlm : bool, optional + Add tile-part length markers (TLM) to the codestream. This can help + to speed up decoding of parts of very large images for some decoders. + add_plt : bool, optional + Add packet length, tile-part length markers (PLT) to the codestream. This + can help to speed up decoding of parts of very large images for some + decoders. Returns ------- @@ -553,6 +562,8 @@ def encode_array( compression_ratios, signal_noise_ratios, codec_format, + add_tlm, + add_plt, ) if return_code != 0: