Skip to main content

j2k_core/
passthrough.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use crate::{Colorspace, Info, TileLayout};
4
5/// Compressed syntax carried by a source frame or accepted by a destination.
6///
7/// The enum intentionally names codec profiles rather than container-specific
8/// UIDs. Container integrations can map these variants to their local transfer
9/// syntax identifiers and keep that policy outside the codec crates.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum CompressedTransferSyntax {
13    /// Baseline 8-bit JPEG interchange format.
14    JpegBaseline8,
15    /// Sequential JPEG beyond the baseline profile.
16    JpegExtendedSequential,
17    /// Classic JPEG 2000 codestream using reversible coding.
18    Jpeg2000Lossless,
19    /// Classic JPEG 2000 codestream using irreversible coding.
20    Jpeg2000Lossy,
21    /// High-throughput JPEG 2000 codestream using reversible coding.
22    HtJpeg2000Lossless,
23    /// High-throughput JPEG 2000 codestream using irreversible coding.
24    HtJpeg2000Lossy,
25}
26
27impl CompressedTransferSyntax {
28    /// True when the syntax profile is lossless.
29    #[must_use]
30    pub const fn is_lossless(self) -> bool {
31        matches!(self, Self::Jpeg2000Lossless | Self::HtJpeg2000Lossless)
32    }
33
34    /// True when the syntax belongs to the JPEG 2000 family.
35    #[must_use]
36    pub const fn is_jpeg2000_family(self) -> bool {
37        matches!(
38            self,
39            Self::Jpeg2000Lossless
40                | Self::Jpeg2000Lossy
41                | Self::HtJpeg2000Lossless
42                | Self::HtJpeg2000Lossy
43        )
44    }
45}
46
47/// Encapsulation shape of the compressed bytes.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[non_exhaustive]
50pub enum CompressedPayloadKind {
51    /// Complete JPEG interchange byte stream.
52    JpegInterchange,
53    /// Raw JPEG 2000 / HTJ2K codestream bytes.
54    Jpeg2000Codestream,
55    /// JP2 file-format wrapper around a JPEG 2000 codestream.
56    Jp2File,
57    /// JPH file-format wrapper around an HTJ2K codestream.
58    JphFile,
59}
60
61/// A borrowed compressed frame/tile that may be copied unchanged.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct PassthroughCandidate<'a> {
64    bytes: &'a [u8],
65    transfer_syntax: CompressedTransferSyntax,
66    payload_kind: CompressedPayloadKind,
67    info: Info,
68}
69
70impl<'a> PassthroughCandidate<'a> {
71    /// Construct a candidate from already-inspected compressed bytes.
72    #[must_use]
73    pub const fn new(
74        bytes: &'a [u8],
75        transfer_syntax: CompressedTransferSyntax,
76        payload_kind: CompressedPayloadKind,
77        info: Info,
78    ) -> Self {
79        Self {
80            bytes,
81            transfer_syntax,
82            payload_kind,
83            info,
84        }
85    }
86
87    /// Original compressed bytes. A successful passthrough decision returns
88    /// this exact slice.
89    #[must_use]
90    pub const fn bytes(&self) -> &'a [u8] {
91        self.bytes
92    }
93
94    /// Source compressed syntax.
95    #[must_use]
96    pub const fn transfer_syntax(&self) -> CompressedTransferSyntax {
97        self.transfer_syntax
98    }
99
100    /// Source payload/container shape.
101    #[must_use]
102    pub const fn payload_kind(&self) -> CompressedPayloadKind {
103        self.payload_kind
104    }
105
106    /// Header metadata inspected from the compressed payload.
107    #[must_use]
108    pub const fn info(&self) -> &Info {
109        &self.info
110    }
111
112    /// Evaluate whether this candidate can be copied unchanged into a
113    /// destination with the supplied requirements.
114    #[must_use]
115    pub fn evaluate(&self, requirements: &PassthroughRequirements) -> PassthroughDecision<'a> {
116        match self.copy_bytes_if_eligible(requirements) {
117            Ok(bytes) => PassthroughDecision::Copy { bytes },
118            Err(reason) => PassthroughDecision::Transcode { reason },
119        }
120    }
121
122    /// Return the original compressed bytes only when passthrough is legal.
123    pub fn copy_bytes_if_eligible(
124        &self,
125        requirements: &PassthroughRequirements,
126    ) -> Result<&'a [u8], PassthroughRejectReason> {
127        if self.bytes.is_empty() {
128            return Err(PassthroughRejectReason::EmptyPayload);
129        }
130        if self.transfer_syntax != requirements.transfer_syntax {
131            return Err(PassthroughRejectReason::TransferSyntaxMismatch {
132                source: self.transfer_syntax,
133                destination: requirements.transfer_syntax,
134            });
135        }
136        if self.payload_kind != requirements.payload_kind {
137            return Err(PassthroughRejectReason::PayloadKindMismatch {
138                source: self.payload_kind,
139                destination: requirements.payload_kind,
140            });
141        }
142        if let Some(destination) = requirements.dimensions {
143            if self.info.dimensions != destination {
144                return Err(PassthroughRejectReason::DimensionsMismatch {
145                    source: self.info.dimensions,
146                    destination,
147                });
148            }
149        }
150        if let Some(destination) = requirements.components {
151            if self.info.components != destination {
152                return Err(PassthroughRejectReason::ComponentsMismatch {
153                    source: self.info.components,
154                    destination,
155                });
156            }
157        }
158        if let Some(destination) = requirements.bit_depth {
159            if self.info.bit_depth != destination {
160                return Err(PassthroughRejectReason::BitDepthMismatch {
161                    source: self.info.bit_depth,
162                    destination,
163                });
164            }
165        }
166        if let Some(destination) = requirements.colorspace {
167            if self.info.colorspace != destination {
168                return Err(PassthroughRejectReason::ColorspaceMismatch {
169                    source: self.info.colorspace,
170                    destination,
171                });
172            }
173        }
174        if let Some(destination) = requirements.tile_layout {
175            if self.info.tile_layout != Some(destination) {
176                return Err(PassthroughRejectReason::TileLayoutMismatch {
177                    source: self.info.tile_layout,
178                    destination,
179                });
180            }
181        }
182
183        Ok(self.bytes)
184    }
185}
186
187/// Destination requirements for copying compressed bytes unchanged.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct PassthroughRequirements {
190    /// Required destination compressed syntax.
191    pub transfer_syntax: CompressedTransferSyntax,
192    /// Required destination payload/container shape.
193    pub payload_kind: CompressedPayloadKind,
194    /// Optional exact output dimensions.
195    pub dimensions: Option<(u32, u32)>,
196    /// Optional exact component count.
197    pub components: Option<u16>,
198    /// Optional exact bit depth.
199    pub bit_depth: Option<u8>,
200    /// Optional exact colorspace.
201    pub colorspace: Option<Colorspace>,
202    /// Optional exact tile layout.
203    pub tile_layout: Option<TileLayout>,
204}
205
206impl PassthroughRequirements {
207    /// Start a requirements set with the mandatory syntax and payload shape.
208    #[must_use]
209    pub const fn new(
210        transfer_syntax: CompressedTransferSyntax,
211        payload_kind: CompressedPayloadKind,
212    ) -> Self {
213        Self {
214            transfer_syntax,
215            payload_kind,
216            dimensions: None,
217            components: None,
218            bit_depth: None,
219            colorspace: None,
220            tile_layout: None,
221        }
222    }
223
224    /// Require exact frame/tile dimensions.
225    #[must_use]
226    pub const fn with_dimensions(mut self, dimensions: (u32, u32)) -> Self {
227        self.dimensions = Some(dimensions);
228        self
229    }
230
231    /// Require an exact component count.
232    #[must_use]
233    pub const fn with_components(mut self, components: u8) -> Self {
234        self.components = Some(components as u16);
235        self
236    }
237
238    /// Require an exact JPEG 2000 component count.
239    #[must_use]
240    pub const fn with_component_count(mut self, components: u16) -> Self {
241        self.components = Some(components);
242        self
243    }
244
245    /// Require an exact bit depth.
246    #[must_use]
247    pub const fn with_bit_depth(mut self, bit_depth: u8) -> Self {
248        self.bit_depth = Some(bit_depth);
249        self
250    }
251
252    /// Require an exact colorspace.
253    #[must_use]
254    pub const fn with_colorspace(mut self, colorspace: Colorspace) -> Self {
255        self.colorspace = Some(colorspace);
256        self
257    }
258
259    /// Require an exact tile layout.
260    #[must_use]
261    pub const fn with_tile_layout(mut self, tile_layout: TileLayout) -> Self {
262        self.tile_layout = Some(tile_layout);
263        self
264    }
265}
266
267/// Result of a passthrough eligibility check.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum PassthroughDecision<'a> {
270    /// Copy these compressed bytes unchanged.
271    Copy {
272        /// Borrowed source bytes to copy unchanged.
273        bytes: &'a [u8],
274    },
275    /// Decode/transcode instead, for the stated reason.
276    Transcode {
277        /// Reason byte-preserving passthrough was rejected.
278        reason: PassthroughRejectReason,
279    },
280}
281
282/// First reason a compressed payload was rejected for byte-preserving copy.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284#[non_exhaustive]
285pub enum PassthroughRejectReason {
286    /// The source compressed payload is empty.
287    EmptyPayload,
288    /// Source and destination compressed syntaxes differ.
289    TransferSyntaxMismatch {
290        /// Source syntax found in the candidate.
291        source: CompressedTransferSyntax,
292        /// Required destination syntax.
293        destination: CompressedTransferSyntax,
294    },
295    /// Source and destination payload/container shapes differ.
296    PayloadKindMismatch {
297        /// Source payload shape found in the candidate.
298        source: CompressedPayloadKind,
299        /// Required destination payload shape.
300        destination: CompressedPayloadKind,
301    },
302    /// Source and destination dimensions differ.
303    DimensionsMismatch {
304        /// Source dimensions found in the candidate.
305        source: (u32, u32),
306        /// Required destination dimensions.
307        destination: (u32, u32),
308    },
309    /// Source and destination component counts differ.
310    ComponentsMismatch {
311        /// Source component count found in the candidate.
312        source: u16,
313        /// Required destination component count.
314        destination: u16,
315    },
316    /// Source and destination bit depths differ.
317    BitDepthMismatch {
318        /// Source bit depth found in the candidate.
319        source: u8,
320        /// Required destination bit depth.
321        destination: u8,
322    },
323    /// Source and destination colorspaces differ.
324    ColorspaceMismatch {
325        /// Source colorspace found in the candidate.
326        source: Colorspace,
327        /// Required destination colorspace.
328        destination: Colorspace,
329    },
330    /// Source and destination tile layouts differ.
331    TileLayoutMismatch {
332        /// Source tile layout found in the candidate.
333        source: Option<TileLayout>,
334        /// Required destination tile layout.
335        destination: TileLayout,
336    },
337}