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