Skip to main content

j2k_native/j2c/
recode.rs

1//! Coefficient-domain JPEG 2000 family recode helpers.
2
3use alloc::vec::Vec;
4
5use super::build::{self, Decomposition, SubBand};
6use super::codestream::{ComponentInfo, Header, QuantizationStyle, WaveletTransform};
7use super::decode::{
8    decode_component_tile_bit_planes_budgeted, DecodeAllocationBudget, DecoderContext,
9    DecompositionStorage,
10};
11use super::encode::allocation::{checked_add_bytes, checked_element_bytes};
12use super::segment;
13use super::tile::{self, Tile};
14use crate::error::{bail, DecodingError, Result, TileError, ValidationError};
15use crate::reader::BitReader;
16use crate::{
17    try_reserve_decode_elements, EncodeOptions, EncodeResult, J2kForwardDwt53Level,
18    J2kForwardDwt53Output, PrecomputedHtj2k53Component, PrecomputedHtj2k53Image,
19};
20
21#[cfg(test)]
22mod tests;
23
24/// Reversible 5/3 source coefficients ready for HTJ2K code-block recoding.
25#[derive(Debug)]
26pub struct Reversible53CoefficientImage {
27    /// Precomputed wavelet coefficients in the native HTJ2K encoder shape.
28    pub image: PrecomputedHtj2k53Image,
29    /// Source COD multi-component transform flag to preserve in output.
30    pub use_mct: bool,
31    /// Source code-block width exponent minus two.
32    pub code_block_width_exp: u8,
33    /// Source code-block height exponent minus two.
34    pub code_block_height_exp: u8,
35    /// Source quantization guard-bit count.
36    pub guard_bits: u8,
37}
38
39impl Reversible53CoefficientImage {
40    /// Encode this complete coefficient owner as HTJ2K under one retained-input budget.
41    ///
42    /// # Errors
43    ///
44    /// Returns a typed error when retained capacity arithmetic, encode
45    /// planning, allocation, packetization, or codestream assembly fails.
46    #[doc(hidden)]
47    pub fn encode_htj2k(&self, options: &EncodeOptions) -> EncodeResult<Vec<u8>> {
48        let retained_bytes = self.checked_retained_capacity_bytes()?;
49        super::encode::encode_precomputed_htj2k_53_with_mct_and_retained_owner(
50            &self.image,
51            options,
52            self.use_mct,
53            self,
54            retained_bytes,
55        )
56    }
57
58    /// Return allocator-capacity bytes retained while this coefficient image is encoded.
59    fn checked_retained_capacity_bytes(&self) -> EncodeResult<usize> {
60        let mut bytes = checked_element_bytes::<PrecomputedHtj2k53Component>(
61            self.image.components.capacity(),
62            "reversible coefficient component capacity",
63        )?;
64        for component in &self.image.components {
65            bytes = checked_add_bytes(
66                bytes,
67                checked_element_bytes::<f32>(
68                    component.dwt.ll.capacity(),
69                    "reversible coefficient LL capacity",
70                )?,
71                "reversible coefficient image capacity",
72            )?;
73            bytes = checked_add_bytes(
74                bytes,
75                checked_element_bytes::<J2kForwardDwt53Level>(
76                    component.dwt.levels.capacity(),
77                    "reversible coefficient level capacity",
78                )?,
79                "reversible coefficient image capacity",
80            )?;
81            for level in &component.dwt.levels {
82                for band in [&level.hl, &level.lh, &level.hh] {
83                    bytes = checked_add_bytes(
84                        bytes,
85                        checked_element_bytes::<f32>(
86                            band.capacity(),
87                            "reversible coefficient detail-band capacity",
88                        )?,
89                        "reversible coefficient image capacity",
90                    )?;
91                }
92            }
93        }
94        Ok(bytes)
95    }
96}
97
98pub(crate) fn extract_reversible_53_coefficients<'a>(
99    data: &'a [u8],
100    header: &Header<'a>,
101    retained_image_bytes: usize,
102    ctx: &mut DecoderContext<'a>,
103) -> Result<Reversible53CoefficientImage> {
104    ctx.release_reusable_allocations();
105    let result = extract_reversible_53_coefficients_inner(data, header, retained_image_bytes, ctx);
106    ctx.release_reusable_allocations();
107    result
108}
109
110fn extract_reversible_53_coefficients_inner<'a>(
111    data: &'a [u8],
112    header: &Header<'a>,
113    retained_image_bytes: usize,
114    ctx: &mut DecoderContext<'a>,
115) -> Result<Reversible53CoefficientImage> {
116    validate_header_for_reversible_53_recode(header)?;
117
118    let mut reader = BitReader::new(data);
119    let tiles = tile::parse(&mut reader, header, retained_image_bytes)?;
120    if tiles.len() != 1 {
121        bail!(DecodingError::UnsupportedFeature(
122            "coefficient-domain 5/3 recode currently supports single-tile codestreams"
123        ));
124    }
125
126    let tile = &tiles[0];
127    validate_tile_for_reversible_53_recode(tile)?;
128
129    ctx.tile_decode_context.channel_data.clear();
130    ctx.storage.reset_for_next_tile();
131
132    build::build(
133        tile,
134        &mut ctx.storage,
135        tiles.structural_workspace_bytes(),
136        false,
137        build::BuildWorkspace::CoefficientsOnly,
138        None,
139    )?;
140    segment::parse(tile, header, &mut ctx.storage)?;
141
142    let mut no_ht_decoder = None;
143    let cpu_decode_parallelism = ctx.cpu_decode_parallelism();
144    decode_component_tile_bit_planes_budgeted(
145        tile,
146        0..tile.component_infos.len(),
147        &mut ctx.tile_decode_context,
148        &mut ctx.storage,
149        header,
150        &mut no_ht_decoder,
151        cpu_decode_parallelism,
152        false,
153    )?;
154
155    let output_plan = RecodeOutputPlan::for_storage(tile, &ctx.storage)?;
156    let mut budget = DecodeAllocationBudget::for_storage(&ctx.storage)?;
157    output_plan.include_in(&mut budget)?;
158    let image =
159        precomputed_image_from_storage(header, tile, &ctx.storage, &output_plan, &mut budget)?;
160    let first = tile.component_infos.first().ok_or(TileError::Invalid)?;
161    let params = &first.coding_style.parameters;
162    Ok(Reversible53CoefficientImage {
163        image,
164        use_mct: tile.mct,
165        code_block_width_exp: params.code_block_width.saturating_sub(2),
166        code_block_height_exp: params.code_block_height.saturating_sub(2),
167        guard_bits: first.quantization_info.guard_bits,
168    })
169}
170
171struct RecodeOutputPlan {
172    components: usize,
173    levels: usize,
174    coefficients: usize,
175}
176
177impl RecodeOutputPlan {
178    fn for_storage(tile: &Tile<'_>, storage: &DecompositionStorage<'_>) -> Result<Self> {
179        let level_count =
180            storage
181                .tile_decompositions
182                .iter()
183                .try_fold(0usize, |count, decomposition| {
184                    count
185                        .checked_add(decomposition.decompositions.len())
186                        .ok_or(ValidationError::ImageTooLarge)
187                })?;
188        Ok(Self {
189            components: tile.component_infos.len(),
190            levels: level_count,
191            coefficients: storage.coefficients.len(),
192        })
193    }
194
195    fn include_in(&self, budget: &mut DecodeAllocationBudget) -> Result<()> {
196        budget.include_elements::<PrecomputedHtj2k53Component>(self.components)?;
197        budget.include_elements::<J2kForwardDwt53Level>(self.levels)?;
198        budget.include_elements::<f32>(self.coefficients)
199    }
200}
201
202fn validate_header_for_reversible_53_recode(header: &Header<'_>) -> Result<()> {
203    if header.skipped_resolution_levels != 0 {
204        bail!(DecodingError::UnsupportedFeature(
205            "coefficient-domain 5/3 recode requires full-resolution decode settings"
206        ));
207    }
208    if header.size_data.num_tiles() != 1 {
209        bail!(DecodingError::UnsupportedFeature(
210            "coefficient-domain 5/3 recode currently supports single-tile codestreams"
211        ));
212    }
213    if header.size_data.image_area_x_offset != 0
214        || header.size_data.image_area_y_offset != 0
215        || header.size_data.tile_x_offset != 0
216        || header.size_data.tile_y_offset != 0
217    {
218        bail!(DecodingError::UnsupportedFeature(
219            "coefficient-domain 5/3 recode currently requires zero image and tile origins"
220        ));
221    }
222    Ok(())
223}
224
225fn validate_tile_for_reversible_53_recode(tile: &Tile<'_>) -> Result<()> {
226    if !matches!(tile.component_infos.len(), 1 | 3) {
227        bail!(DecodingError::UnsupportedFeature(
228            "coefficient-domain 5/3 recode supports only grayscale or RGB codestreams"
229        ));
230    }
231    if tile.mct && tile.component_infos.len() != 3 {
232        bail!(DecodingError::UnsupportedFeature(
233            "reversible color transform requires three components"
234        ));
235    }
236
237    let first = tile.component_infos.first().ok_or(TileError::Invalid)?;
238    let first_params = &first.coding_style.parameters;
239    let first_bit_depth = first.size_info.precision;
240    let first_guard_bits = first.quantization_info.guard_bits;
241
242    for component in &tile.component_infos {
243        validate_component_for_reversible_53_recode(component)?;
244        if component.size_info.precision != first_bit_depth {
245            bail!(DecodingError::UnsupportedFeature(
246                "coefficient-domain 5/3 recode requires equal component bit depths"
247            ));
248        }
249        if component.quantization_info.guard_bits != first_guard_bits {
250            bail!(DecodingError::UnsupportedFeature(
251                "coefficient-domain 5/3 recode requires equal component guard bits"
252            ));
253        }
254        let params = &component.coding_style.parameters;
255        if params.num_decomposition_levels != first_params.num_decomposition_levels
256            || params.code_block_width != first_params.code_block_width
257            || params.code_block_height != first_params.code_block_height
258        {
259            bail!(DecodingError::UnsupportedFeature(
260                "coefficient-domain 5/3 recode requires matching component coding geometry"
261            ));
262        }
263    }
264
265    Ok(())
266}
267
268fn validate_component_for_reversible_53_recode(component: &ComponentInfo) -> Result<()> {
269    if component.wavelet_transform() != WaveletTransform::Reversible53 {
270        bail!(DecodingError::UnsupportedFeature(
271            "coefficient-domain lossless recode currently supports only reversible 5/3 sources"
272        ));
273    }
274    if component.num_decomposition_levels() == 0 {
275        bail!(DecodingError::UnsupportedFeature(
276            "coefficient-domain 5/3 recode requires at least one decomposition level"
277        ));
278    }
279    if component.quantization_info.quantization_style != QuantizationStyle::NoQuantization {
280        bail!(DecodingError::UnsupportedFeature(
281            "coefficient-domain 5/3 recode requires no-quantization QCD/QCC"
282        ));
283    }
284    if component
285        .coding_style
286        .parameters
287        .code_block_style
288        .uses_high_throughput_block_coding()
289    {
290        bail!(DecodingError::UnsupportedFeature(
291            "source already uses HT block coding"
292        ));
293    }
294    Ok(())
295}
296
297fn precomputed_image_from_storage(
298    header: &Header<'_>,
299    tile: &Tile<'_>,
300    storage: &DecompositionStorage<'_>,
301    output_plan: &RecodeOutputPlan,
302    budget: &mut DecodeAllocationBudget,
303) -> Result<PrecomputedHtj2k53Image> {
304    let mut components = Vec::new();
305    try_reserve_decode_elements(&mut components, output_plan.components)?;
306    budget.include_capacity_overage::<PrecomputedHtj2k53Component>(
307        output_plan.components,
308        components.capacity(),
309    )?;
310    for (component_index, component_info) in tile.component_infos.iter().enumerate() {
311        let tile_decomposition = storage
312            .tile_decompositions
313            .get(component_index)
314            .ok_or(TileError::Invalid)?;
315        components.push(PrecomputedHtj2k53Component {
316            x_rsiz: component_info.size_info.horizontal_resolution,
317            y_rsiz: component_info.size_info.vertical_resolution,
318            dwt: component_dwt_from_storage(tile_decomposition, storage, budget)?,
319        });
320    }
321
322    let first = tile.component_infos.first().ok_or(TileError::Invalid)?;
323    Ok(PrecomputedHtj2k53Image {
324        width: header.size_data.image_width(),
325        height: header.size_data.image_height(),
326        bit_depth: first.size_info.precision,
327        signed: false,
328        components,
329    })
330}
331
332fn component_dwt_from_storage(
333    tile_decomposition: &super::decode::TileDecompositions,
334    storage: &DecompositionStorage<'_>,
335    budget: &mut DecodeAllocationBudget,
336) -> Result<J2kForwardDwt53Output> {
337    let ll = storage
338        .sub_bands
339        .get(tile_decomposition.first_ll_sub_band)
340        .ok_or(TileError::Invalid)?;
341
342    let level_count = tile_decomposition.decompositions.len();
343    let mut levels = Vec::new();
344    try_reserve_decode_elements(&mut levels, level_count)?;
345    budget.include_capacity_overage::<J2kForwardDwt53Level>(level_count, levels.capacity())?;
346    for idx in tile_decomposition.decompositions.clone() {
347        let decomposition = storage.decompositions.get(idx).ok_or(TileError::Invalid)?;
348        levels.push(level_from_decomposition(decomposition, storage, budget)?);
349    }
350
351    Ok(J2kForwardDwt53Output {
352        ll: subband_coefficients(ll, storage, budget)?,
353        ll_width: ll.rect.width(),
354        ll_height: ll.rect.height(),
355        levels,
356    })
357}
358
359fn level_from_decomposition(
360    decomposition: &Decomposition,
361    storage: &DecompositionStorage<'_>,
362    budget: &mut DecodeAllocationBudget,
363) -> Result<J2kForwardDwt53Level> {
364    let hl = storage
365        .sub_bands
366        .get(decomposition.sub_bands[0])
367        .ok_or(TileError::Invalid)?;
368    let lh = storage
369        .sub_bands
370        .get(decomposition.sub_bands[1])
371        .ok_or(TileError::Invalid)?;
372    let hh = storage
373        .sub_bands
374        .get(decomposition.sub_bands[2])
375        .ok_or(TileError::Invalid)?;
376    Ok(J2kForwardDwt53Level {
377        hl: subband_coefficients(hl, storage, budget)?,
378        lh: subband_coefficients(lh, storage, budget)?,
379        hh: subband_coefficients(hh, storage, budget)?,
380        width: decomposition.rect.width(),
381        height: decomposition.rect.height(),
382        low_width: lh.rect.width(),
383        low_height: hl.rect.height(),
384        high_width: hl.rect.width(),
385        high_height: lh.rect.height(),
386    })
387}
388
389fn subband_coefficients(
390    subband: &SubBand,
391    storage: &DecompositionStorage<'_>,
392    budget: &mut DecodeAllocationBudget,
393) -> Result<Vec<f32>> {
394    let coefficients = storage
395        .coefficients
396        .get(subband.coefficients.clone())
397        .ok_or(TileError::Invalid)?;
398    let mut copied = Vec::new();
399    try_reserve_decode_elements(&mut copied, coefficients.len())?;
400    budget.include_capacity_overage::<f32>(coefficients.len(), copied.capacity())?;
401    copied.extend_from_slice(coefficients);
402    Ok(copied)
403}