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