Skip to main content

signinum_core/
passthrough.rs

1// SPDX-License-Identifier: 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}
58
59/// A borrowed compressed frame/tile that may be copied unchanged.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct PassthroughCandidate<'a> {
62    bytes: &'a [u8],
63    transfer_syntax: CompressedTransferSyntax,
64    payload_kind: CompressedPayloadKind,
65    info: Info,
66}
67
68impl<'a> PassthroughCandidate<'a> {
69    /// Construct a candidate from already-inspected compressed bytes.
70    #[must_use]
71    pub const fn new(
72        bytes: &'a [u8],
73        transfer_syntax: CompressedTransferSyntax,
74        payload_kind: CompressedPayloadKind,
75        info: Info,
76    ) -> Self {
77        Self {
78            bytes,
79            transfer_syntax,
80            payload_kind,
81            info,
82        }
83    }
84
85    /// Original compressed bytes. A successful passthrough decision returns
86    /// this exact slice.
87    #[must_use]
88    pub const fn bytes(&self) -> &'a [u8] {
89        self.bytes
90    }
91
92    /// Source compressed syntax.
93    #[must_use]
94    pub const fn transfer_syntax(&self) -> CompressedTransferSyntax {
95        self.transfer_syntax
96    }
97
98    /// Source payload/container shape.
99    #[must_use]
100    pub const fn payload_kind(&self) -> CompressedPayloadKind {
101        self.payload_kind
102    }
103
104    /// Header metadata inspected from the compressed payload.
105    #[must_use]
106    pub const fn info(&self) -> &Info {
107        &self.info
108    }
109
110    /// Evaluate whether this candidate can be copied unchanged into a
111    /// destination with the supplied requirements.
112    #[must_use]
113    pub fn evaluate(&self, requirements: &PassthroughRequirements) -> PassthroughDecision<'a> {
114        match self.copy_bytes_if_eligible(requirements) {
115            Ok(bytes) => PassthroughDecision::Copy { bytes },
116            Err(reason) => PassthroughDecision::Transcode { reason },
117        }
118    }
119
120    /// Return the original compressed bytes only when passthrough is legal.
121    pub fn copy_bytes_if_eligible(
122        &self,
123        requirements: &PassthroughRequirements,
124    ) -> Result<&'a [u8], PassthroughRejectReason> {
125        if self.bytes.is_empty() {
126            return Err(PassthroughRejectReason::EmptyPayload);
127        }
128        if self.transfer_syntax != requirements.transfer_syntax {
129            return Err(PassthroughRejectReason::TransferSyntaxMismatch {
130                source: self.transfer_syntax,
131                destination: requirements.transfer_syntax,
132            });
133        }
134        if self.payload_kind != requirements.payload_kind {
135            return Err(PassthroughRejectReason::PayloadKindMismatch {
136                source: self.payload_kind,
137                destination: requirements.payload_kind,
138            });
139        }
140        if let Some(destination) = requirements.dimensions {
141            if self.info.dimensions != destination {
142                return Err(PassthroughRejectReason::DimensionsMismatch {
143                    source: self.info.dimensions,
144                    destination,
145                });
146            }
147        }
148        if let Some(destination) = requirements.components {
149            if self.info.components != destination {
150                return Err(PassthroughRejectReason::ComponentsMismatch {
151                    source: self.info.components,
152                    destination,
153                });
154            }
155        }
156        if let Some(destination) = requirements.bit_depth {
157            if self.info.bit_depth != destination {
158                return Err(PassthroughRejectReason::BitDepthMismatch {
159                    source: self.info.bit_depth,
160                    destination,
161                });
162            }
163        }
164        if let Some(destination) = requirements.colorspace {
165            if self.info.colorspace != destination {
166                return Err(PassthroughRejectReason::ColorspaceMismatch {
167                    source: self.info.colorspace,
168                    destination,
169                });
170            }
171        }
172        if let Some(destination) = requirements.tile_layout {
173            if self.info.tile_layout != Some(destination) {
174                return Err(PassthroughRejectReason::TileLayoutMismatch {
175                    source: self.info.tile_layout,
176                    destination,
177                });
178            }
179        }
180
181        Ok(self.bytes)
182    }
183}
184
185/// Destination requirements for copying compressed bytes unchanged.
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub struct PassthroughRequirements {
188    /// Required destination compressed syntax.
189    pub transfer_syntax: CompressedTransferSyntax,
190    /// Required destination payload/container shape.
191    pub payload_kind: CompressedPayloadKind,
192    /// Optional exact output dimensions.
193    pub dimensions: Option<(u32, u32)>,
194    /// Optional exact component count.
195    pub components: Option<u8>,
196    /// Optional exact bit depth.
197    pub bit_depth: Option<u8>,
198    /// Optional exact colorspace.
199    pub colorspace: Option<Colorspace>,
200    /// Optional exact tile layout.
201    pub tile_layout: Option<TileLayout>,
202}
203
204impl PassthroughRequirements {
205    /// Start a requirements set with the mandatory syntax and payload shape.
206    #[must_use]
207    pub const fn new(
208        transfer_syntax: CompressedTransferSyntax,
209        payload_kind: CompressedPayloadKind,
210    ) -> Self {
211        Self {
212            transfer_syntax,
213            payload_kind,
214            dimensions: None,
215            components: None,
216            bit_depth: None,
217            colorspace: None,
218            tile_layout: None,
219        }
220    }
221
222    /// Require exact frame/tile dimensions.
223    #[must_use]
224    pub const fn with_dimensions(mut self, dimensions: (u32, u32)) -> Self {
225        self.dimensions = Some(dimensions);
226        self
227    }
228
229    /// Require an exact component count.
230    #[must_use]
231    pub const fn with_components(mut self, components: u8) -> Self {
232        self.components = Some(components);
233        self
234    }
235
236    /// Require an exact bit depth.
237    #[must_use]
238    pub const fn with_bit_depth(mut self, bit_depth: u8) -> Self {
239        self.bit_depth = Some(bit_depth);
240        self
241    }
242
243    /// Require an exact colorspace.
244    #[must_use]
245    pub const fn with_colorspace(mut self, colorspace: Colorspace) -> Self {
246        self.colorspace = Some(colorspace);
247        self
248    }
249
250    /// Require an exact tile layout.
251    #[must_use]
252    pub const fn with_tile_layout(mut self, tile_layout: TileLayout) -> Self {
253        self.tile_layout = Some(tile_layout);
254        self
255    }
256}
257
258/// Result of a passthrough eligibility check.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum PassthroughDecision<'a> {
261    /// Copy these compressed bytes unchanged.
262    Copy {
263        /// Borrowed source bytes to copy unchanged.
264        bytes: &'a [u8],
265    },
266    /// Decode/transcode instead, for the stated reason.
267    Transcode {
268        /// Reason byte-preserving passthrough was rejected.
269        reason: PassthroughRejectReason,
270    },
271}
272
273/// First reason a compressed payload was rejected for byte-preserving copy.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum PassthroughRejectReason {
277    /// The source compressed payload is empty.
278    EmptyPayload,
279    /// Source and destination compressed syntaxes differ.
280    TransferSyntaxMismatch {
281        /// Source syntax found in the candidate.
282        source: CompressedTransferSyntax,
283        /// Required destination syntax.
284        destination: CompressedTransferSyntax,
285    },
286    /// Source and destination payload/container shapes differ.
287    PayloadKindMismatch {
288        /// Source payload shape found in the candidate.
289        source: CompressedPayloadKind,
290        /// Required destination payload shape.
291        destination: CompressedPayloadKind,
292    },
293    /// Source and destination dimensions differ.
294    DimensionsMismatch {
295        /// Source dimensions found in the candidate.
296        source: (u32, u32),
297        /// Required destination dimensions.
298        destination: (u32, u32),
299    },
300    /// Source and destination component counts differ.
301    ComponentsMismatch {
302        /// Source component count found in the candidate.
303        source: u8,
304        /// Required destination component count.
305        destination: u8,
306    },
307    /// Source and destination bit depths differ.
308    BitDepthMismatch {
309        /// Source bit depth found in the candidate.
310        source: u8,
311        /// Required destination bit depth.
312        destination: u8,
313    },
314    /// Source and destination colorspaces differ.
315    ColorspaceMismatch {
316        /// Source colorspace found in the candidate.
317        source: Colorspace,
318        /// Required destination colorspace.
319        destination: Colorspace,
320    },
321    /// Source and destination tile layouts differ.
322    TileLayoutMismatch {
323        /// Source tile layout found in the candidate.
324        source: Option<TileLayout>,
325        /// Required destination tile layout.
326        destination: TileLayout,
327    },
328}