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    ///
124    /// # Errors
125    ///
126    /// Returns a [`PassthroughRejectReason`] describing the first destination
127    /// requirement that the source payload does not satisfy.
128    pub fn copy_bytes_if_eligible(
129        &self,
130        requirements: &PassthroughRequirements,
131    ) -> Result<&'a [u8], PassthroughRejectReason> {
132        if self.bytes.is_empty() {
133            return Err(PassthroughRejectReason::EmptyPayload);
134        }
135        if self.transfer_syntax != requirements.transfer_syntax {
136            return Err(PassthroughRejectReason::TransferSyntaxMismatch {
137                source: self.transfer_syntax,
138                destination: requirements.transfer_syntax,
139            });
140        }
141        if self.payload_kind != requirements.payload_kind {
142            return Err(PassthroughRejectReason::PayloadKindMismatch {
143                source: self.payload_kind,
144                destination: requirements.payload_kind,
145            });
146        }
147        if let Some(destination) = requirements.dimensions {
148            if self.info.dimensions != destination {
149                return Err(PassthroughRejectReason::DimensionsMismatch {
150                    source: self.info.dimensions,
151                    destination,
152                });
153            }
154        }
155        if let Some(destination) = requirements.components {
156            if self.info.components != destination {
157                return Err(PassthroughRejectReason::ComponentsMismatch {
158                    source: self.info.components,
159                    destination,
160                });
161            }
162        }
163        if let Some(destination) = requirements.bit_depth {
164            if self.info.bit_depth != destination {
165                return Err(PassthroughRejectReason::BitDepthMismatch {
166                    source: self.info.bit_depth,
167                    destination,
168                });
169            }
170        }
171        if let Some(destination) = requirements.colorspace {
172            if self.info.colorspace != destination {
173                return Err(PassthroughRejectReason::ColorspaceMismatch {
174                    source: self.info.colorspace,
175                    destination,
176                });
177            }
178        }
179        if let Some(destination) = requirements.tile_layout {
180            if self.info.tile_layout != Some(destination) {
181                return Err(PassthroughRejectReason::TileLayoutMismatch {
182                    source: self.info.tile_layout,
183                    destination,
184                });
185            }
186        }
187
188        Ok(self.bytes)
189    }
190}
191
192/// Destination requirements for copying compressed bytes unchanged.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct PassthroughRequirements {
195    /// Required destination compressed syntax.
196    pub transfer_syntax: CompressedTransferSyntax,
197    /// Required destination payload/container shape.
198    pub payload_kind: CompressedPayloadKind,
199    /// Optional exact output dimensions.
200    pub dimensions: Option<(u32, u32)>,
201    /// Optional exact component count.
202    pub components: Option<u16>,
203    /// Optional exact bit depth.
204    pub bit_depth: Option<u8>,
205    /// Optional exact colorspace.
206    pub colorspace: Option<Colorspace>,
207    /// Optional exact tile layout.
208    pub tile_layout: Option<TileLayout>,
209}
210
211impl PassthroughRequirements {
212    /// Start a requirements set with the mandatory syntax and payload shape.
213    #[must_use]
214    pub const fn new(
215        transfer_syntax: CompressedTransferSyntax,
216        payload_kind: CompressedPayloadKind,
217    ) -> Self {
218        Self {
219            transfer_syntax,
220            payload_kind,
221            dimensions: None,
222            components: None,
223            bit_depth: None,
224            colorspace: None,
225            tile_layout: None,
226        }
227    }
228
229    /// Require exact frame/tile dimensions.
230    #[must_use]
231    pub const fn with_dimensions(mut self, dimensions: (u32, u32)) -> Self {
232        self.dimensions = Some(dimensions);
233        self
234    }
235
236    /// Require an exact component count.
237    #[must_use]
238    pub const fn with_components(mut self, components: u8) -> Self {
239        self.components = Some(components as u16);
240        self
241    }
242
243    /// Require an exact JPEG 2000 component count.
244    #[must_use]
245    pub const fn with_component_count(mut self, components: u16) -> Self {
246        self.components = Some(components);
247        self
248    }
249
250    /// Require an exact bit depth.
251    #[must_use]
252    pub const fn with_bit_depth(mut self, bit_depth: u8) -> Self {
253        self.bit_depth = Some(bit_depth);
254        self
255    }
256
257    /// Require an exact colorspace.
258    #[must_use]
259    pub const fn with_colorspace(mut self, colorspace: Colorspace) -> Self {
260        self.colorspace = Some(colorspace);
261        self
262    }
263
264    /// Require an exact tile layout.
265    #[must_use]
266    pub const fn with_tile_layout(mut self, tile_layout: TileLayout) -> Self {
267        self.tile_layout = Some(tile_layout);
268        self
269    }
270}
271
272/// Result of a passthrough eligibility check.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum PassthroughDecision<'a> {
275    /// Copy these compressed bytes unchanged.
276    Copy {
277        /// Borrowed source bytes to copy unchanged.
278        bytes: &'a [u8],
279    },
280    /// Decode/transcode instead, for the stated reason.
281    Transcode {
282        /// Reason byte-preserving passthrough was rejected.
283        reason: PassthroughRejectReason,
284    },
285}
286
287/// First reason a compressed payload was rejected for byte-preserving copy.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289#[non_exhaustive]
290pub enum PassthroughRejectReason {
291    /// The source compressed payload is empty.
292    EmptyPayload,
293    /// Source and destination compressed syntaxes differ.
294    TransferSyntaxMismatch {
295        /// Source syntax found in the candidate.
296        source: CompressedTransferSyntax,
297        /// Required destination syntax.
298        destination: CompressedTransferSyntax,
299    },
300    /// Source and destination payload/container shapes differ.
301    PayloadKindMismatch {
302        /// Source payload shape found in the candidate.
303        source: CompressedPayloadKind,
304        /// Required destination payload shape.
305        destination: CompressedPayloadKind,
306    },
307    /// Source and destination dimensions differ.
308    DimensionsMismatch {
309        /// Source dimensions found in the candidate.
310        source: (u32, u32),
311        /// Required destination dimensions.
312        destination: (u32, u32),
313    },
314    /// Source and destination component counts differ.
315    ComponentsMismatch {
316        /// Source component count found in the candidate.
317        source: u16,
318        /// Required destination component count.
319        destination: u16,
320    },
321    /// Source and destination bit depths differ.
322    BitDepthMismatch {
323        /// Source bit depth found in the candidate.
324        source: u8,
325        /// Required destination bit depth.
326        destination: u8,
327    },
328    /// Source and destination colorspaces differ.
329    ColorspaceMismatch {
330        /// Source colorspace found in the candidate.
331        source: Colorspace,
332        /// Required destination colorspace.
333        destination: Colorspace,
334    },
335    /// Source and destination tile layouts differ.
336    TileLayoutMismatch {
337        /// Source tile layout found in the candidate.
338        source: Option<TileLayout>,
339        /// Required destination tile layout.
340        destination: TileLayout,
341    },
342}