Skip to main content

j2k_native/j2c/
capabilities.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! JPEG 2000 CAP and CPF marker parsing shared by decode and inspection.
4
5use alloc::vec::Vec;
6
7mod magnitude;
8pub(crate) use magnitude::encode_magnitude_bound;
9pub use magnitude::required_magnitude_bound;
10
11const HTJ2K_PCAP_MASK: u32 = 1 << 17;
12pub(crate) const HTJ2K_RSIZ_MASK: u16 = 1 << 14;
13const CCAP15_RESERVED_MASK: u16 = 0x07C0;
14
15/// Part 15 code-block set declared by `Ccap15`.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum Htj2kCapabilityMode {
18    /// Every code-block uses HT coding.
19    HtOnly,
20    /// HT coding is declared, but the codestream may use classic coding.
21    HtDeclared,
22    /// Classic and HT code-block coding may be mixed.
23    Mixed,
24}
25
26/// Corresponding-profile words parsed from one CPF marker segment.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct J2kCorrespondingProfile {
29    words: Vec<u16>,
30}
31
32impl J2kCorrespondingProfile {
33    /// Return the `Pcpf_i` words in codestream order.
34    #[must_use]
35    pub fn words(&self) -> &[u16] {
36        &self.words
37    }
38
39    /// Return `CPFnum` when its little-word-endian representation fits `u64`.
40    #[must_use]
41    pub fn number_u64(&self) -> Option<u64> {
42        profile_number_u64(self.words.len(), self.words.iter().copied())
43    }
44}
45
46/// Parsed Part 15 CAP/CPF facts plus the default COD HT style bits.
47#[derive(Clone, Debug, Eq, PartialEq)]
48#[expect(
49    clippy::struct_excessive_bools,
50    reason = "the booleans expose independent Part 15 CAP and COD capability facts"
51)]
52pub struct Htj2kCapabilities {
53    pcap: u32,
54    ccap15: u16,
55    mode: Htj2kCapabilityMode,
56    multiple_ht_sets: bool,
57    roi: bool,
58    heterogeneous: bool,
59    ht_irreversible: bool,
60    magnitude_bound: u8,
61    quality_layers: u8,
62    default_ht_block_coding: bool,
63    default_mixed_block_coding: bool,
64    corresponding_profile: Option<J2kCorrespondingProfile>,
65}
66
67impl Htj2kCapabilities {
68    /// Raw `Pcap` word from CAP.
69    #[must_use]
70    pub const fn pcap(&self) -> u32 {
71        self.pcap
72    }
73
74    /// Raw Part 15 capability word.
75    #[must_use]
76    pub const fn ccap15(&self) -> u16 {
77        self.ccap15
78    }
79
80    /// Declared HT code-block set.
81    #[must_use]
82    pub const fn mode(&self) -> Htj2kCapabilityMode {
83        self.mode
84    }
85
86    /// Whether `Ccap15` advertises multiple HT sets.
87    #[must_use]
88    pub const fn multiple_ht_sets(&self) -> bool {
89        self.multiple_ht_sets
90    }
91
92    /// Whether `Ccap15` advertises RGN use.
93    #[must_use]
94    pub const fn roi(&self) -> bool {
95        self.roi
96    }
97
98    /// Whether `Ccap15` advertises heterogeneous HT sets.
99    #[must_use]
100    pub const fn heterogeneous(&self) -> bool {
101        self.heterogeneous
102    }
103
104    /// Whether `Ccap15` advertises irreversible HT coding.
105    #[must_use]
106    pub const fn ht_irreversible(&self) -> bool {
107        self.ht_irreversible
108    }
109
110    /// Decoded `BMAGB` magnitude bound in the range 8 through 74.
111    #[must_use]
112    pub const fn magnitude_bound(&self) -> u8 {
113        self.magnitude_bound
114    }
115
116    /// Number of quality layers declared by the main COD marker.
117    #[must_use]
118    pub const fn quality_layers(&self) -> u8 {
119        self.quality_layers
120    }
121
122    /// Whether the main COD marker selects HT block coding by default.
123    #[must_use]
124    pub const fn default_ht_block_coding(&self) -> bool {
125        self.default_ht_block_coding
126    }
127
128    /// Whether the main COD marker permits mixed classic/HT block coding.
129    #[must_use]
130    pub const fn default_mixed_block_coding(&self) -> bool {
131        self.default_mixed_block_coding
132    }
133
134    /// Corresponding profile advertised by CPF, when present.
135    #[must_use]
136    pub const fn corresponding_profile(&self) -> Option<&J2kCorrespondingProfile> {
137        self.corresponding_profile.as_ref()
138    }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub(crate) enum CapabilityMarkerError {
143    Cap(&'static str),
144    Cpf(&'static str),
145    Allocation { bytes: usize },
146}
147
148impl CapabilityMarkerError {
149    pub(crate) const fn marker_label(self) -> &'static str {
150        match self {
151            Self::Cap(what) | Self::Cpf(what) => what,
152            Self::Allocation { .. } => "CPF profile allocation",
153        }
154    }
155}
156
157#[derive(Clone, Copy, Debug)]
158#[expect(
159    clippy::struct_excessive_bools,
160    reason = "the booleans preserve independent validated Ccap15 flag bits"
161)]
162struct Htj2kCapabilityCore {
163    pcap: u32,
164    ccap15: u16,
165    mode: Htj2kCapabilityMode,
166    multiple_ht_sets: bool,
167    roi: bool,
168    heterogeneous: bool,
169    ht_irreversible: bool,
170    magnitude_bound: u8,
171}
172
173#[derive(Default)]
174pub(crate) struct CapabilityMarkerState<'a> {
175    saw_cap: bool,
176    htj2k: Option<Htj2kCapabilityCore>,
177    cpf_payload: Option<&'a [u8]>,
178}
179
180impl<'a> CapabilityMarkerState<'a> {
181    pub(crate) fn record_cap(&mut self, payload: &'a [u8]) -> Result<(), CapabilityMarkerError> {
182        if self.saw_cap {
183            return Err(CapabilityMarkerError::Cap("duplicate CAP"));
184        }
185        self.saw_cap = true;
186        self.htj2k = parse_cap(payload)?;
187        Ok(())
188    }
189
190    pub(crate) fn record_cpf(&mut self, payload: &'a [u8]) -> Result<(), CapabilityMarkerError> {
191        if self.cpf_payload.is_some() {
192            return Err(CapabilityMarkerError::Cpf("duplicate CPF"));
193        }
194        validate_cpf(payload)?;
195        self.cpf_payload = Some(payload);
196        Ok(())
197    }
198
199    pub(crate) fn validate_rsiz(&self, rsiz: u16) -> Result<(), CapabilityMarkerError> {
200        match (rsiz & HTJ2K_RSIZ_MASK != 0, self.htj2k.is_some()) {
201            (true, false) => Err(CapabilityMarkerError::Cap(
202                "SIZ advertises Part 15 without Pcap15",
203            )),
204            (false, true) => Err(CapabilityMarkerError::Cap(
205                "Pcap15 is present without the Part 15 SIZ capability",
206            )),
207            _ => Ok(()),
208        }?;
209        if self.cpf_payload.is_some() && self.htj2k.is_none() {
210            return Err(CapabilityMarkerError::Cpf(
211                "CPF is present without Part 15 capabilities",
212            ));
213        }
214        Ok(())
215    }
216
217    pub(crate) fn high_throughput(&self) -> bool {
218        self.htj2k.is_some()
219    }
220
221    pub(crate) fn to_public(
222        &self,
223        quality_layers: u8,
224        default_ht_block_coding: bool,
225        default_mixed_block_coding: bool,
226    ) -> Result<Option<Htj2kCapabilities>, CapabilityMarkerError> {
227        let Some(core) = self.htj2k else {
228            return Ok(None);
229        };
230        let corresponding_profile = self
231            .cpf_payload
232            .map(J2kCorrespondingProfile::from_payload)
233            .transpose()?;
234        Ok(Some(Htj2kCapabilities {
235            pcap: core.pcap,
236            ccap15: core.ccap15,
237            mode: core.mode,
238            multiple_ht_sets: core.multiple_ht_sets,
239            roi: core.roi,
240            heterogeneous: core.heterogeneous,
241            ht_irreversible: core.ht_irreversible,
242            magnitude_bound: core.magnitude_bound,
243            quality_layers,
244            default_ht_block_coding,
245            default_mixed_block_coding,
246            corresponding_profile,
247        }))
248    }
249}
250
251impl J2kCorrespondingProfile {
252    fn from_payload(payload: &[u8]) -> Result<Self, CapabilityMarkerError> {
253        let word_count = payload.len() / 2;
254        let bytes = word_count
255            .checked_mul(core::mem::size_of::<u16>())
256            .ok_or(CapabilityMarkerError::Allocation { bytes: usize::MAX })?;
257        let mut words = Vec::new();
258        words
259            .try_reserve_exact(word_count)
260            .map_err(|_| CapabilityMarkerError::Allocation { bytes })?;
261        words.extend(
262            payload
263                .chunks_exact(2)
264                .map(|word| u16::from_be_bytes([word[0], word[1]])),
265        );
266        Ok(Self { words })
267    }
268}
269
270fn parse_cap(payload: &[u8]) -> Result<Option<Htj2kCapabilityCore>, CapabilityMarkerError> {
271    if payload.len() < 4 {
272        return Err(CapabilityMarkerError::Cap(
273            "CAP payload is shorter than Pcap",
274        ));
275    }
276    let pcap = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
277    let expected_len = 4usize
278        .checked_add(pcap.count_ones() as usize * 2)
279        .ok_or(CapabilityMarkerError::Cap("CAP payload length overflows"))?;
280    if payload.len() != expected_len {
281        return Err(CapabilityMarkerError::Cap(
282            "CAP payload does not match the Pcap capability count",
283        ));
284    }
285    if pcap & HTJ2K_PCAP_MASK == 0 {
286        return Ok(None);
287    }
288    let preceding_capabilities = (pcap >> 18).count_ones() as usize;
289    let offset = 4 + preceding_capabilities * 2;
290    let ccap15 = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
291    if ccap15 & CCAP15_RESERVED_MASK != 0 {
292        return Err(CapabilityMarkerError::Cap("CAP reserved Ccap15 bits"));
293    }
294    let mode = match ccap15 >> 14 {
295        0 => Htj2kCapabilityMode::HtOnly,
296        2 => Htj2kCapabilityMode::HtDeclared,
297        3 => Htj2kCapabilityMode::Mixed,
298        _ => return Err(CapabilityMarkerError::Cap("CAP reserved HT mode")),
299    };
300    Ok(Some(Htj2kCapabilityCore {
301        pcap,
302        ccap15,
303        mode,
304        multiple_ht_sets: ccap15 & (1 << 13) != 0,
305        roi: ccap15 & (1 << 12) != 0,
306        heterogeneous: ccap15 & (1 << 11) != 0,
307        ht_irreversible: ccap15 & (1 << 5) != 0,
308        magnitude_bound: decode_magnitude_bound((ccap15 & 0x1F) as u8),
309    }))
310}
311
312fn validate_cpf(payload: &[u8]) -> Result<(), CapabilityMarkerError> {
313    if payload.is_empty() || !payload.len().is_multiple_of(2) {
314        return Err(CapabilityMarkerError::Cpf(
315            "CPF payload must contain complete profile words",
316        ));
317    }
318    if payload[payload.len() - 2..] == [0, 0] {
319        return Err(CapabilityMarkerError::Cpf(
320            "CPF final profile word must be non-zero",
321        ));
322    }
323    Ok(())
324}
325
326fn profile_number_u64(word_count: usize, words: impl Iterator<Item = u16>) -> Option<u64> {
327    if word_count > 4 {
328        return None;
329    }
330    let mut encoded = 0u64;
331    for (index, word) in words.enumerate() {
332        encoded = encoded.checked_add(u64::from(word) << (index * 16))?;
333    }
334    encoded.checked_sub(1)
335}
336
337const fn decode_magnitude_bound(p: u8) -> u8 {
338    match p {
339        0 => 8,
340        1..=19 => p + 8,
341        20..=30 => 4 * (p - 19) + 27,
342        _ => 74,
343    }
344}