Skip to main content

j2k/
recode.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! JPEG 2000-family coefficient-domain recoding APIs.
4//!
5//! The direct 5/3 path decodes classic JPEG 2000 Tier-1 code-blocks into
6//! reversible wavelet coefficients and re-encodes those coefficients with
7//! HTJ2K block coding. It does not run inverse DWT, forward DWT, or a
8//! pixel-domain lossless encode. The output is coefficient-preserving for the
9//! supported reversible 5/3 profile, not byte-preserving unless passthrough is
10//! reported.
11
12use alloc::vec::Vec;
13
14use j2k_core::{
15    Colorspace, CompressedPayloadKind, CompressedTransferSyntax, PassthroughRequirements,
16    Unsupported,
17};
18use j2k_native::{DecodeSettings, EncodeOptions, Image};
19
20use crate::{
21    encode::{native_progression_order, J2kEncodeValidation, J2kProgressionOrder},
22    parse::{parse_image_info, ParsedImageInfo},
23    J2kError, J2kView,
24};
25
26/// Options for classic JPEG 2000 reversible 5/3 to HTJ2K lossless recoding.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub struct J2kToHtj2kOptions {
30    /// Requested output payload shape.
31    ///
32    /// DICOM encapsulated WSI frames use raw JPEG 2000-family codestreams, so
33    /// the default is [`CompressedPayloadKind::Jpeg2000Codestream`]. JP2 output
34    /// is not produced by the coefficient-domain recoder yet.
35    pub output_payload_kind: CompressedPayloadKind,
36    /// Output packet progression order.
37    pub progression: J2kProgressionOrder,
38    /// Optional decoded-pixel validation of the produced codestream.
39    pub validation: J2kEncodeValidation,
40}
41
42impl Default for J2kToHtj2kOptions {
43    fn default() -> Self {
44        Self {
45            output_payload_kind: CompressedPayloadKind::Jpeg2000Codestream,
46            progression: J2kProgressionOrder::Lrcp,
47            validation: J2kEncodeValidation::CpuRoundTrip,
48        }
49    }
50}
51
52impl J2kToHtj2kOptions {
53    /// Create J2K/JP2 to HTJ2K recode options.
54    pub const fn new(
55        output_payload_kind: CompressedPayloadKind,
56        progression: J2kProgressionOrder,
57        validation: J2kEncodeValidation,
58    ) -> Self {
59        Self {
60            output_payload_kind,
61            progression,
62            validation,
63        }
64    }
65}
66
67/// Recode path used for a J2K/JP2 to HTJ2K request.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum J2kToHtj2kMode {
70    /// Input bytes already matched the requested HTJ2K transfer syntax and
71    /// payload kind, so bytes were copied unchanged.
72    Passthrough,
73    /// Classic reversible 5/3 code-blocks were entropy-decoded to quantized
74    /// wavelet coefficients and re-encoded with HT block coding.
75    CoefficientPreserving,
76    /// Reserved for an explicit decode-pixels/re-encode fallback.
77    PixelPreserving,
78}
79
80/// Metadata describing a J2K/JP2 to HTJ2K recode.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct J2kToHtj2kReport {
83    /// Recode path used for this output.
84    pub mode: J2kToHtj2kMode,
85    /// Classified input transfer syntax.
86    pub input_transfer_syntax: CompressedTransferSyntax,
87    /// Output transfer syntax.
88    pub output_transfer_syntax: CompressedTransferSyntax,
89    /// Classified input payload/container kind.
90    pub input_payload_kind: CompressedPayloadKind,
91    /// Output payload/container kind.
92    pub output_payload_kind: CompressedPayloadKind,
93    /// Image width in pixels.
94    pub width: u32,
95    /// Image height in pixels.
96    pub height: u32,
97    /// Component count.
98    pub components: u8,
99    /// Significant bits per component.
100    pub bit_depth: u8,
101}
102
103/// HTJ2K codestream bytes and recode metadata.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct ReencodedHtj2k {
106    /// Encoded HTJ2K bytes.
107    pub bytes: Vec<u8>,
108    /// Recode metadata and selected path.
109    pub report: J2kToHtj2kReport,
110}
111
112/// Recode a classic JPEG 2000 reversible 5/3 J2K/JP2 input to lossless HTJ2K.
113///
114/// This is a JPEG 2000-family coefficient-domain recode. For supported classic
115/// lossless 5/3 sources it preserves decoded quantized wavelet coefficients and
116/// changes only the block coding and packetized codestream representation. It
117/// is not a DCT JPEG transcode and does not claim byte preservation except when
118/// [`J2kToHtj2kMode::Passthrough`] is reported.
119pub fn recode_j2k_to_htj2k_lossless(
120    bytes: &[u8],
121    options: J2kToHtj2kOptions,
122) -> Result<ReencodedHtj2k, J2kError> {
123    let view = J2kView::parse(bytes)?;
124    let parsed = parse_image_info(bytes)?;
125    let info = view.info().clone();
126    let output_transfer_syntax = CompressedTransferSyntax::HtJpeg2000Lossless;
127
128    if let Some(candidate) = view.passthrough_candidate() {
129        let requirements =
130            PassthroughRequirements::new(output_transfer_syntax, options.output_payload_kind);
131        if let Ok(copy) = candidate.copy_bytes_if_eligible(&requirements) {
132            return Ok(ReencodedHtj2k {
133                bytes: copy.to_vec(),
134                report: J2kToHtj2kReport {
135                    mode: J2kToHtj2kMode::Passthrough,
136                    input_transfer_syntax: candidate.transfer_syntax(),
137                    output_transfer_syntax,
138                    input_payload_kind: candidate.payload_kind(),
139                    output_payload_kind: options.output_payload_kind,
140                    width: info.dimensions.0,
141                    height: info.dimensions.1,
142                    components: info.components,
143                    bit_depth: info.bit_depth,
144                },
145            });
146        }
147    }
148
149    validate_recode_request(&parsed, options)?;
150
151    let source = Image::new(bytes, &DecodeSettings::default())
152        .map_err(|err| map_native_decode_error(err, "source JPEG 2000 parse failed"))?;
153    let coefficients = source
154        .decode_reversible_53_coefficients()
155        .map_err(|err| map_native_decode_error(err, "source coefficient extraction failed"))?;
156
157    let encode_options = native_encode_options(options, &coefficients);
158    let codestream = j2k_native::encode_precomputed_htj2k_53_with_mct(
159        &coefficients.image,
160        &encode_options,
161        coefficients.use_mct,
162    )
163    .map_err(|err| J2kError::Backend(format!("HTJ2K coefficient recode failed: {err}")))?;
164
165    if options.validation == J2kEncodeValidation::CpuRoundTrip {
166        validate_recode_roundtrip(bytes, &codestream)?;
167    }
168
169    Ok(ReencodedHtj2k {
170        bytes: codestream,
171        report: J2kToHtj2kReport {
172            mode: J2kToHtj2kMode::CoefficientPreserving,
173            input_transfer_syntax: parsed.transfer_syntax,
174            output_transfer_syntax,
175            input_payload_kind: parsed.payload_kind,
176            output_payload_kind: options.output_payload_kind,
177            width: parsed.info.dimensions.0,
178            height: parsed.info.dimensions.1,
179            components: parsed.info.components,
180            bit_depth: parsed.info.bit_depth,
181        },
182    })
183}
184
185fn validate_recode_request(
186    parsed: &ParsedImageInfo,
187    options: J2kToHtj2kOptions,
188) -> Result<(), J2kError> {
189    if options.output_payload_kind != CompressedPayloadKind::Jpeg2000Codestream {
190        return Err(Unsupported {
191            what: "coefficient-domain J2K to HTJ2K recode currently emits only raw codestreams",
192        }
193        .into());
194    }
195    if parsed.transfer_syntax != CompressedTransferSyntax::Jpeg2000Lossless {
196        return Err(Unsupported {
197            what: "coefficient-domain lossless recode currently supports only classic lossless J2K",
198        }
199        .into());
200    }
201    if !matches!(parsed.info.components, 1 | 3) {
202        return Err(Unsupported {
203            what: "coefficient-domain lossless recode supports only grayscale or RGB component counts",
204        }
205        .into());
206    }
207    if !matches!(parsed.info.bit_depth, 8 | 16) {
208        return Err(Unsupported {
209            what: "coefficient-domain lossless recode supports only 8-bit or 16-bit sources",
210        }
211        .into());
212    }
213    if !matches!(
214        parsed.info.colorspace,
215        Colorspace::Grayscale
216            | Colorspace::SGray
217            | Colorspace::Rgb
218            | Colorspace::SRgb
219            | Colorspace::Rct
220    ) {
221        return Err(Unsupported {
222            what: "coefficient-domain lossless recode supports only Gray/RGB/RCT colorspaces",
223        }
224        .into());
225    }
226    if parsed.components.iter().any(|component| component.signed) {
227        return Err(Unsupported {
228            what: "signed JPEG 2000 sources are not supported for coefficient-domain recode yet",
229        }
230        .into());
231    }
232    if parsed
233        .components
234        .iter()
235        .any(|component| component.bit_depth != parsed.info.bit_depth)
236    {
237        return Err(Unsupported {
238            what: "mixed component bit depths are not supported for coefficient-domain recode",
239        }
240        .into());
241    }
242    if parsed
243        .components
244        .iter()
245        .any(|component| component.x_rsiz != 1 || component.y_rsiz != 1)
246    {
247        return Err(Unsupported {
248            what: "component subsampling is not supported for coefficient-domain recode yet",
249        }
250        .into());
251    }
252    Ok(())
253}
254
255fn native_encode_options(
256    options: J2kToHtj2kOptions,
257    coefficients: &j2k_native::Reversible53CoefficientImage,
258) -> EncodeOptions {
259    EncodeOptions {
260        reversible: true,
261        use_ht_block_coding: true,
262        use_mct: coefficients.use_mct,
263        code_block_width_exp: coefficients.code_block_width_exp,
264        code_block_height_exp: coefficients.code_block_height_exp,
265        guard_bits: coefficients.guard_bits,
266        progression_order: native_progression_order(options.progression),
267        write_tlm: options.progression == J2kProgressionOrder::Rpcl,
268        validate_high_throughput_codestream: false,
269        ..EncodeOptions::default()
270    }
271}
272
273fn validate_recode_roundtrip(source: &[u8], encoded: &[u8]) -> Result<(), J2kError> {
274    let source = Image::new(source, &DecodeSettings::default())
275        .map_err(|err| map_native_decode_error(err, "source JPEG 2000 validation parse failed"))?
276        .decode_native()
277        .map_err(|err| map_native_decode_error(err, "source JPEG 2000 validation decode failed"))?;
278    let encoded = Image::new(encoded, &DecodeSettings::default())
279        .map_err(|err| map_native_decode_error(err, "HTJ2K validation parse failed"))?
280        .decode_native()
281        .map_err(|err| map_native_decode_error(err, "HTJ2K validation decode failed"))?;
282
283    if source.width != encoded.width
284        || source.height != encoded.height
285        || source.bit_depth != encoded.bit_depth
286        || source.num_components != encoded.num_components
287        || source.data != encoded.data
288    {
289        return Err(J2kError::Backend(
290            "HTJ2K coefficient recode failed pixel validation".to_string(),
291        ));
292    }
293    Ok(())
294}
295
296fn map_native_decode_error(err: j2k_native::DecodeError, context: &'static str) -> J2kError {
297    match err {
298        j2k_native::DecodeError::Decoding(j2k_native::DecodingError::UnsupportedFeature(what)) => {
299            J2kError::Unsupported(Unsupported { what })
300        }
301        _ => J2kError::Backend(format!("{context}: {err}")),
302    }
303}