Skip to main content

j2k_jpeg/decoder/
view.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use crate::error::JpegError;
4use crate::info::{ColorSpace, DecodeOptions, Info, RestartIndex, SofKind};
5use crate::parse::header::{parse_header, parse_header_with_external_live, ParsedHeader};
6use j2k_core::{CompressedPayloadKind, PassthroughCandidate};
7
8use super::core_traits::jpeg_passthrough_syntax;
9use super::{find_component_index, restart_index_for_stream};
10
11/// A parsed borrowed view of a JPEG stream.
12#[derive(Debug)]
13pub struct JpegView<'a> {
14    pub(super) bytes: &'a [u8],
15    pub(super) header: ParsedHeader,
16    pub(super) info: Info,
17    pub(super) options: DecodeOptions,
18}
19
20impl<'a> JpegView<'a> {
21    /// Parse the stream into a borrowed view that can later build a decoder.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error when the JPEG header is malformed or unsupported.
26    pub fn parse(input: &'a [u8]) -> Result<Self, JpegError> {
27        Self::parse_with_options(input, DecodeOptions::default())
28    }
29
30    /// Parse while charging an already-live owner baseline to parser metadata.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error when the aggregate host budget is exceeded or the JPEG
35    /// header is malformed or unsupported.
36    pub(crate) fn parse_with_external_live(
37        input: &'a [u8],
38        external_live_bytes: usize,
39    ) -> Result<Self, JpegError> {
40        Self::parse_with_options_and_external_live(
41            input,
42            DecodeOptions::default(),
43            external_live_bytes,
44        )
45    }
46
47    /// Parse the stream with explicit decode options.
48    ///
49    /// # Errors
50    ///
51    /// Returns an error when the JPEG header is malformed or unsupported.
52    pub fn parse_with_options(input: &'a [u8], options: DecodeOptions) -> Result<Self, JpegError> {
53        let header = parse_header(input)?;
54        Ok(Self::from_header(input, header, options))
55    }
56
57    fn parse_with_options_and_external_live(
58        input: &'a [u8],
59        options: DecodeOptions,
60        external_live_bytes: usize,
61    ) -> Result<Self, JpegError> {
62        let header = parse_header_with_external_live(input, external_live_bytes)?;
63        Ok(Self::from_header(input, header, options))
64    }
65
66    fn from_header(input: &'a [u8], header: ParsedHeader, options: DecodeOptions) -> Self {
67        let mut info = header.info();
68        options.apply_to_info(&mut info);
69        Self {
70            bytes: input,
71            header,
72            info,
73            options,
74        }
75    }
76
77    /// Header-derived metadata for the parsed stream.
78    #[must_use]
79    pub fn info(&self) -> &Info {
80        &self.info
81    }
82
83    /// Original compressed bytes backing this view.
84    #[must_use]
85    pub fn bytes(&self) -> &'a [u8] {
86        self.bytes
87    }
88
89    pub(crate) fn parsed_header(&self) -> &ParsedHeader {
90        &self.header
91    }
92
93    /// Return a byte-preserving passthrough candidate for active DICOM/WSI
94    /// transfer syntaxes.
95    ///
96    /// Progressive JPEG is intentionally not exposed here because the active
97    /// conversion path should transcode it rather than introduce a retired or
98    /// unsupported destination syntax.
99    #[must_use]
100    pub fn passthrough_candidate(&self) -> Option<PassthroughCandidate<'a>> {
101        jpeg_passthrough_syntax(&self.info).map(|transfer_syntax| {
102            PassthroughCandidate::new(
103                self.bytes,
104                transfer_syntax,
105                CompressedPayloadKind::JpegInterchange,
106                self.info.to_core_info(),
107            )
108        })
109    }
110
111    /// Build a restart-marker byte-offset index for the first scan.
112    ///
113    /// Offsets are absolute byte positions in the original JPEG byte slice.
114    /// Returns `Ok(None)` when the stream has no non-zero DRI marker.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error when restart-marker syntax is malformed.
119    pub fn restart_index(&self) -> Result<Option<RestartIndex>, JpegError> {
120        restart_index_for_stream(
121            self.bytes,
122            self.header.sos_offset,
123            &self.info,
124            self.info.restart_interval,
125        )
126    }
127
128    pub(crate) fn has_lossless_subsampled_color_capability_shape(&self) -> bool {
129        if self.info.sof_kind != SofKind::Lossless
130            || !matches!(self.info.color_space, ColorSpace::Rgb | ColorSpace::YCbCr)
131            || !matches!(self.info.bit_depth, 8 | 16)
132            || self.info.sampling.len() != 3
133            || !self
134                .info
135                .sampling
136                .components()
137                .iter()
138                .any(|&(h, v)| h != 1 || v != 1)
139            || self.header.scan_count != 1
140        {
141            return false;
142        }
143
144        let Some(scan) = self.header.scan.as_ref() else {
145            return false;
146        };
147        if !(1..=7).contains(&scan.ss)
148            || scan.se != 0
149            || scan.ah != 0
150            || scan.al != 0
151            || scan.components.len() != 3
152        {
153            return false;
154        }
155
156        scan.components.iter().all(|scan_component| {
157            find_component_index(&self.header.component_ids, scan_component.id).is_some()
158                && self.header.huffman_tables.dc[scan_component.dc_table as usize].is_some()
159        })
160    }
161}