Skip to main content

j2k_native/j2c/encode/
options.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! JPEG 2000 encode option and request types.
4
5use alloc::vec::Vec;
6use j2k_types::encode_geometry::{
7    code_block_dimension, code_block_dimensions, CodeBlockGeometryError, EncodeCodeBlockDimensions,
8};
9
10use super::super::quantize;
11use crate::IrreversibleQuantizationSubbandScales;
12
13/// Encoding options for JPEG 2000.
14#[derive(Debug, Clone)]
15#[expect(
16    clippy::struct_excessive_bools,
17    reason = "the public options expose independent JPEG 2000 coding and marker switches"
18)]
19pub struct EncodeOptions {
20    /// Number of decomposition levels (default: 5).
21    pub num_decomposition_levels: u8,
22    /// Use reversible (lossless) transform (default: true).
23    pub reversible: bool,
24    /// Code-block width exponent minus 2 (default: 4, meaning 2^6=64).
25    pub code_block_width_exp: u8,
26    /// Code-block height exponent minus 2 (default: 4, meaning 2^6=64).
27    pub code_block_height_exp: u8,
28    /// Number of guard bits (default: 1 for reversible, 2 for irreversible).
29    pub guard_bits: u8,
30    /// Encode using HT block coding (HTJ2K / Part 15) instead of classic EBCOT.
31    pub use_ht_block_coding: bool,
32    /// Packet progression order to write in COD and use for packetization.
33    pub progression_order: EncodeProgressionOrder,
34    /// Write a TLM marker segment for the single tile-part.
35    pub write_tlm: bool,
36    /// Write PLT packet-length marker segments in the tile-part header.
37    pub write_plt: bool,
38    /// Write PLM packet-length marker segments in the main header.
39    pub write_plm: bool,
40    /// Write PPM packed packet-header marker segments in the main header.
41    pub write_ppm: bool,
42    /// Write PPT packed packet-header marker segments in tile-part headers.
43    pub write_ppt: bool,
44    /// Write SOP marker segments before packets.
45    pub write_sop: bool,
46    /// Write EPH markers after packet headers.
47    pub write_eph: bool,
48    /// Apply the JPEG 2000 multi-component color transform for 3+ component inputs.
49    pub use_mct: bool,
50    /// Number of cumulative quality layers to emit.
51    pub num_layers: u8,
52    /// Optional cumulative packet-body byte targets for each quality layer.
53    pub quality_layer_byte_targets: Vec<u64>,
54    /// Decode and verify HTJ2K codestreams inside the native encoder.
55    pub validate_high_throughput_codestream: bool,
56    /// Multiplier applied to irreversible 9/7 scalar quantization step sizes.
57    ///
58    /// `1.0` preserves the near-lossless default step sizes. Larger values
59    /// produce smaller codestreams by coarsening quantization.
60    pub irreversible_quantization_scale: f32,
61    /// Per-subband multipliers applied on top of
62    /// `irreversible_quantization_scale`.
63    pub irreversible_quantization_subband_scales: IrreversibleQuantizationSubbandScales,
64    /// Optional component-specific irreversible multipliers in QCD/QCC order:
65    /// final LL, then HL/LH/HH from coarsest to finest resolution.
66    /// Each component needs exactly `1 + 3 * actual_decomposition_levels` entries.
67    /// An active profile multiplies the unrounded nominal step
68    /// `2^-guard_bits * global_scale * orientation_scale * norm_weight`, then
69    /// rounds once to the stored tuple. `norm_weight` is the band's 9/7
70    /// synthesis-norm weight: 1 for the finest HL/LH bands, below 1 for coarser
71    /// bands. Every resulting tuple must be representable; active
72    /// profiles never silently saturate. An exact all-ones profile disables
73    /// weighting and is byte-identical to no profile, including global saturation.
74    /// For example, at precision 30 and two guard bits the finest HL band's
75    /// nominal 0.25 step clips to 0.5 without a profile. Active weights 0.99 or
76    /// 1.01 are rejected for that band; weight 2 is representable. Weights are
77    /// not based on the clipped 0.5 step.
78    /// Raw-pixel irreversible encoding supports this profile; precomputed
79    /// coefficient paths reject it until their metadata contract is extended.
80    pub irreversible_quantization_profile: Option<Vec<Vec<f32>>>,
81    /// Optional per-component SIZ sampling factors (`XRsiz`, `YRsiz`).
82    ///
83    /// `None` means every component is stored at the reference-grid
84    /// resolution. This is experimental and primarily intended for precomputed
85    /// coefficient encoders that preserve JPEG-native chroma subsampling.
86    pub component_sampling: Option<Vec<(u8, u8)>>,
87    /// Optional per-component whole-component ROI maxshift values.
88    ///
89    /// Non-zero entries emit RGN markers and encode every coefficient in that
90    /// component with the requested maxshift. Rectangular ROI authoring is not
91    /// represented by this field.
92    pub roi_component_shifts: Vec<u8>,
93    /// Optional tile width and height for multi-tile codestream output.
94    ///
95    /// Tile dimensions need not align with the wavelet, code-block, or precinct
96    /// grids. Each tile is transformed and partitioned at its absolute origin.
97    /// The effective decomposition depth is capped by the smallest tile-component.
98    pub tile_size: Option<(u32, u32)>,
99    /// Optional maximum number of complete packets to place in each tile-part.
100    pub tile_part_packet_limit: Option<u16>,
101    /// Optional precinct exponents in COD order, one per resolution level.
102    pub precinct_exponents: Vec<(u8, u8)>,
103}
104
105/// Borrowed component-plane samples for reversible 5/3 component-plane encode.
106#[derive(Debug, Clone, Copy)]
107pub struct EncodeComponentPlane<'a> {
108    /// Row-major little-endian component samples at this component's own grid.
109    pub data: &'a [u8],
110    /// Horizontal SIZ sampling factor (`XRsiz`).
111    pub x_rsiz: u8,
112    /// Vertical SIZ sampling factor (`YRsiz`).
113    pub y_rsiz: u8,
114}
115
116/// Borrowed component-plane samples with per-component precision metadata.
117#[derive(Debug, Clone, Copy)]
118pub struct EncodeTypedComponentPlane<'a> {
119    /// Row-major little-endian component samples at this component's own grid.
120    pub data: &'a [u8],
121    /// Horizontal SIZ sampling factor (`XRsiz`).
122    pub x_rsiz: u8,
123    /// Vertical SIZ sampling factor (`YRsiz`).
124    pub y_rsiz: u8,
125    /// Significant bits per sample for this component.
126    pub bit_depth: u8,
127    /// Whether samples in this component are signed.
128    pub signed: bool,
129}
130
131/// Rectangular region-of-interest request for JPEG 2000 maxshift encoding.
132///
133/// The rectangle is expressed in full-resolution reference-grid pixels. For
134/// sampled components, the encoder maps the rectangle to that component's SIZ
135/// grid before selecting wavelet coefficients. All regions for the same
136/// component must use the same non-zero `shift`, because JPEG 2000 RGN stores
137/// one maxshift value per component.
138#[derive(Debug, Clone, Copy)]
139pub struct EncodeRoiRegion {
140    /// Component index to which the ROI applies.
141    pub component: u16,
142    /// Left edge in reference-grid pixels.
143    pub x: u32,
144    /// Top edge in reference-grid pixels.
145    pub y: u32,
146    /// Width in reference-grid pixels.
147    pub width: u32,
148    /// Height in reference-grid pixels.
149    pub height: u32,
150    /// Maxshift value to write in the component's RGN marker.
151    pub shift: u8,
152}
153
154impl Default for EncodeOptions {
155    fn default() -> Self {
156        Self {
157            num_decomposition_levels: 5,
158            reversible: true,
159            code_block_width_exp: 4,
160            code_block_height_exp: 4,
161            guard_bits: 1,
162            use_ht_block_coding: false,
163            progression_order: EncodeProgressionOrder::Lrcp,
164            write_tlm: false,
165            write_plt: false,
166            write_plm: false,
167            write_ppm: false,
168            write_ppt: false,
169            write_sop: false,
170            write_eph: false,
171            use_mct: true,
172            num_layers: 1,
173            quality_layer_byte_targets: Vec::new(),
174            validate_high_throughput_codestream: true,
175            irreversible_quantization_scale: 1.0,
176            irreversible_quantization_subband_scales:
177                IrreversibleQuantizationSubbandScales::default(),
178            irreversible_quantization_profile: None,
179            component_sampling: None,
180            roi_component_shifts: Vec::new(),
181            tile_size: None,
182            tile_part_packet_limit: None,
183            precinct_exponents: Vec::new(),
184        }
185    }
186}
187
188/// JPEG 2000 packet progression orders supported by the encoder.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
190pub enum EncodeProgressionOrder {
191    /// Layer-resolution-component-position progression.
192    #[default]
193    Lrcp,
194    /// Resolution-layer-component-position progression.
195    Rlcp,
196    /// Resolution-position-component-layer progression.
197    Rpcl,
198    /// Position-component-resolution-layer progression.
199    Pcrl,
200    /// Component-position-resolution-layer progression.
201    Cprl,
202}
203
204impl EncodeProgressionOrder {
205    pub(crate) const fn packetization_order(self) -> crate::J2kPacketizationProgressionOrder {
206        match self {
207            Self::Lrcp => crate::J2kPacketizationProgressionOrder::Lrcp,
208            Self::Rlcp => crate::J2kPacketizationProgressionOrder::Rlcp,
209            Self::Rpcl => crate::J2kPacketizationProgressionOrder::Rpcl,
210            Self::Pcrl => crate::J2kPacketizationProgressionOrder::Pcrl,
211            Self::Cprl => crate::J2kPacketizationProgressionOrder::Cprl,
212        }
213    }
214}
215
216fn validate_irreversible_quantization_scale(scale: f32) -> Result<(), &'static str> {
217    if scale.is_finite() && scale > 0.0 {
218        Ok(())
219    } else {
220        Err("irreversible quantization scale must be finite and greater than zero")
221    }
222}
223
224pub(super) fn validate_irreversible_quantization_profile(
225    options: &EncodeOptions,
226) -> Result<(), &'static str> {
227    validate_irreversible_quantization_scale(options.irreversible_quantization_scale)?;
228    if quantize::subband_scales_all_valid(options.irreversible_quantization_subband_scales) {
229        Ok(())
230    } else {
231        Err("irreversible quantization subband scales must be finite and greater than zero")
232    }
233}
234
235/// Validated Part 1 code-block dimensions derived from COD's stored
236/// exponent-minus-two fields.
237pub(super) type CodeBlockGeometry = EncodeCodeBlockDimensions;
238
239/// Validate the JPEG 2000 Part 1 code-block exponent and area constraints
240/// without allocating or shifting by an unchecked public value.
241pub(super) fn validate_code_block_geometry(
242    options: &EncodeOptions,
243) -> Result<CodeBlockGeometry, &'static str> {
244    code_block_dimension(options.code_block_width_exp)
245        .map_err(|_| "code-block width exponent exceeds supported range")?;
246    code_block_dimension(options.code_block_height_exp)
247        .map_err(|_| "code-block height exponent exceeds supported range")?;
248    code_block_dimensions(options.code_block_width_exp, options.code_block_height_exp).map_err(
249        |error| match error {
250            CodeBlockGeometryError::AreaTooLarge => {
251                "code-block dimensions exceed JPEG 2000 Part 1 area limit"
252            }
253            CodeBlockGeometryError::DimensionTooSmall
254            | CodeBlockGeometryError::DimensionNotPowerOfTwo
255            | CodeBlockGeometryError::StoredExponentTooLarge => {
256                "code-block exponent exceeds supported range"
257            }
258        },
259    )
260}
261
262pub(super) fn validate_precinct_exponents_for_options(
263    options: &EncodeOptions,
264    num_decomposition_levels: u8,
265) -> Result<(), &'static str> {
266    validate_code_block_geometry(options)?;
267    if options.precinct_exponents.is_empty() {
268        return Ok(());
269    }
270
271    let expected = usize::from(num_decomposition_levels) + 1;
272    if options.precinct_exponents.len() != expected {
273        return Err("precinct exponent count must match resolution level count");
274    }
275    if options
276        .precinct_exponents
277        .iter()
278        .any(|&(horizontal_exponent, vertical_exponent)| {
279            horizontal_exponent > 15 || vertical_exponent > 15
280        })
281    {
282        return Err("precinct exponents must fit in COD marker nybbles");
283    }
284    let code_block_horizontal_exponent = options
285        .code_block_width_exp
286        .checked_add(2)
287        .ok_or("code-block width exponent exceeds supported range")?;
288    let code_block_vertical_exponent = options
289        .code_block_height_exp
290        .checked_add(2)
291        .ok_or("code-block height exponent exceeds supported range")?;
292    for (resolution, &(horizontal_exponent, vertical_exponent)) in
293        options.precinct_exponents.iter().enumerate()
294    {
295        let minimum_horizontal_exponent = if resolution == 0 {
296            code_block_horizontal_exponent
297        } else {
298            code_block_horizontal_exponent
299                .checked_add(1)
300                .ok_or("code-block width exponent exceeds supported range")?
301        };
302        let minimum_vertical_exponent = if resolution == 0 {
303            code_block_vertical_exponent
304        } else {
305            code_block_vertical_exponent
306                .checked_add(1)
307                .ok_or("code-block height exponent exceeds supported range")?
308        };
309        if horizontal_exponent < minimum_horizontal_exponent
310            || vertical_exponent < minimum_vertical_exponent
311        {
312            return Err("precinct exponents must not reduce encoder code-block dimensions");
313        }
314    }
315    Ok(())
316}
317
318impl EncodeOptions {
319    pub(super) fn custom_component_step_count(&self) -> usize {
320        self.irreversible_quantization_profile
321            .as_ref()
322            .filter(|profile| !quantize::profile_is_unity(profile))
323            .map_or(0, Vec::len)
324    }
325
326    pub(super) fn validate_component_profile(
327        &self,
328        components: usize,
329        levels: u8,
330    ) -> Result<(), &'static str> {
331        let Some(profile) = &self.irreversible_quantization_profile else {
332            return Ok(());
333        };
334        if self.reversible {
335            return Err("component quantization profile requires irreversible encoding");
336        }
337        if profile.len() != components {
338            return Err("quantization profile component count does not match input");
339        }
340        let expected = 1 + 3 * usize::from(levels);
341        if profile.iter().any(|row| row.len() != expected) {
342            return Err("quantization profile band count must match actual decomposition levels");
343        }
344        if profile
345            .iter()
346            .flatten()
347            .any(|x| !x.is_finite() || *x <= 0.0)
348        {
349            return Err("quantization profile multipliers must be finite and positive");
350        }
351        Ok(())
352    }
353}