Skip to main content

j2k_jpeg/
info.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Image metadata and primitive value types. See spec Sections 2 and 4.
4//!
5//! `info.rs` intentionally has **no dependency on `error.rs`** — `error`
6//! depends on us (for `Rect` and `SofKind`), and the reverse would create a
7//! cycle. `DecodeOutcome`, which does need `Warning`, lives in `decoder.rs`
8//! and is added in M1b when the decode methods are introduced.
9
10use alloc::vec::Vec;
11
12/// Start-of-frame variant. Determines the decode pipeline.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SofKind {
15    /// SOF0: baseline sequential, 8-bit, Huffman.
16    Baseline8,
17    /// SOF1: extended sequential, 8-bit, Huffman.
18    Extended8,
19    /// SOF1: extended sequential, 12-bit, Huffman.
20    Extended12,
21    /// SOF2: progressive, 8-bit, Huffman.
22    Progressive8,
23    /// SOF2: progressive, 12-bit, Huffman.
24    Progressive12,
25    /// SOF3: lossless (Annex H predictor), Huffman.
26    Lossless,
27}
28
29/// Declared input color space after APP14 detection.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ColorSpace {
32    /// Single-component grayscale.
33    Grayscale,
34    /// Three-component luma/chroma JPEG data.
35    YCbCr,
36    /// Three-component RGB JPEG data.
37    Rgb,
38    /// Four-component CMYK JPEG data.
39    Cmyk,
40    /// Four-component YCCK JPEG data.
41    Ycck,
42}
43
44/// Per-component (H, V) sampling factors, stored in declaration order.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct SamplingFactors {
47    components: [(u8, u8); 4],
48    component_count: u8,
49    /// `max(H_i)` across components — MCU width in data units.
50    pub max_h: u8,
51    /// `max(V_i)` across components — MCU height in data units.
52    pub max_v: u8,
53}
54
55/// Error returned when constructing [`SamplingFactors`] from caller input.
56#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
57pub enum SamplingFactorsError {
58    /// At least one component is required.
59    #[error("sampling metadata must contain at least one component")]
60    Empty,
61    /// This crate stores at most four component sampling entries.
62    #[error("sampling metadata supports at most four components, got {count}")]
63    TooManyComponents {
64        /// Supplied component count.
65        count: usize,
66    },
67    /// Component sampling factors are outside the JPEG legal range.
68    #[error("invalid sampling ({h}x{v}) for component {component}")]
69    InvalidSampling {
70        /// Component index in declaration order.
71        component: usize,
72        /// Horizontal sampling factor.
73        h: u8,
74        /// Vertical sampling factor.
75        v: u8,
76    },
77}
78
79impl SamplingFactors {
80    /// Build sampling metadata from component `(H, V)` factors.
81    ///
82    /// # Errors
83    /// Returns [`SamplingFactorsError`] when no components are supplied, more
84    /// than four components are supplied, or any sampling factor is outside
85    /// the JPEG legal range `1..=4`.
86    pub fn from_components(components: &[(u8, u8)]) -> Result<Self, SamplingFactorsError> {
87        if components.is_empty() {
88            return Err(SamplingFactorsError::Empty);
89        }
90        if components.len() > 4 {
91            return Err(SamplingFactorsError::TooManyComponents {
92                count: components.len(),
93            });
94        }
95        for (idx, &(h, v)) in components.iter().enumerate() {
96            if !(1..=4).contains(&h) || !(1..=4).contains(&v) {
97                return Err(SamplingFactorsError::InvalidSampling {
98                    component: idx,
99                    h,
100                    v,
101                });
102            }
103        }
104        Ok(Self::from_validated_components(components))
105    }
106
107    pub(crate) fn from_validated_components(components: &[(u8, u8)]) -> Self {
108        debug_assert!(!components.is_empty());
109        debug_assert!(components.len() <= 4);
110        debug_assert!(components
111            .iter()
112            .all(|&(h, v)| (1..=4).contains(&h) && (1..=4).contains(&v)));
113        let mut packed = [(0u8, 0u8); 4];
114        let mut max_h = 0u8;
115        let mut max_v = 0u8;
116        for (idx, &(h, v)) in components.iter().enumerate() {
117            packed[idx] = (h, v);
118            max_h = max_h.max(h);
119            max_v = max_v.max(v);
120        }
121        Self {
122            components: packed,
123            component_count: components.len() as u8,
124            max_h,
125            max_v,
126        }
127    }
128
129    /// Number of declared components.
130    pub fn len(&self) -> usize {
131        self.component_count as usize
132    }
133
134    /// True when no components were declared.
135    pub fn is_empty(&self) -> bool {
136        self.component_count == 0
137    }
138
139    /// Sampling factors for a component by declaration index.
140    pub fn component(&self, index: usize) -> Option<(u8, u8)> {
141        self.components().get(index).copied()
142    }
143
144    /// Sampling factors in component declaration order.
145    pub fn components(&self) -> &[(u8, u8)] {
146        &self.components[..self.component_count as usize]
147    }
148
149    pub(crate) fn iter(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
150        self.components().iter().copied()
151    }
152}
153
154/// Minimum coded unit geometry derived from SOF sampling factors.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct McuGeometry {
157    /// MCU width in output pixels.
158    pub width: u32,
159    /// MCU height in output pixels.
160    pub height: u32,
161    /// Number of MCU columns covering the image.
162    pub columns: u32,
163    /// Number of MCU rows covering the image.
164    pub rows: u32,
165    /// Total MCU count in scan order.
166    pub count: u32,
167}
168
169/// Restart-marker index for a single-scan JPEG stream.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct RestartIndex {
172    /// Absolute byte offset of the first entropy byte after the SOS header.
173    pub scan_data_offset: usize,
174    /// Restart interval from DRI, in MCUs.
175    pub interval_mcus: u32,
176    /// Restart-addressable scan segments in MCU order.
177    pub segments: Vec<RestartSegment>,
178}
179
180/// One restart-addressable entropy segment in the original JPEG byte stream.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct RestartSegment {
183    /// First MCU index decoded from this segment.
184    pub start_mcu: u32,
185    /// Absolute byte offset of the first entropy byte for this segment.
186    pub entropy_offset: usize,
187    /// Absolute byte offset of the preceding RST marker's leading `0xff`.
188    pub marker_offset: Option<usize>,
189    /// Preceding marker byte (`0xd0..=0xd7`) for this segment.
190    pub marker: Option<u8>,
191}
192
193impl McuGeometry {
194    pub(crate) fn from_sampling(dimensions: (u32, u32), sampling: SamplingFactors) -> Self {
195        let width = u32::from(sampling.max_h) * 8;
196        let height = u32::from(sampling.max_v) * 8;
197        let columns = dimensions.0.div_ceil(width);
198        let rows = dimensions.1.div_ceil(height);
199        Self {
200            width,
201            height,
202            columns,
203            rows,
204            count: columns.saturating_mul(rows),
205        }
206    }
207}
208
209/// Inclusive axis-aligned rectangle in image coordinates.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct Rect {
212    /// Left coordinate in pixels.
213    pub x: u32,
214    /// Top coordinate in pixels.
215    pub y: u32,
216    /// Width in pixels.
217    pub w: u32,
218    /// Height in pixels.
219    pub h: u32,
220}
221
222impl Rect {
223    /// The full image rect for the given dimensions.
224    pub fn full(dims: (u32, u32)) -> Self {
225        Self {
226            x: 0,
227            y: 0,
228            w: dims.0,
229            h: dims.1,
230        }
231    }
232
233    /// True if the rect is fully inside the bounding box of size `dims`.
234    pub fn is_within(&self, dims: (u32, u32)) -> bool {
235        let (w, h) = dims;
236        self.x.checked_add(self.w).is_some_and(|r| r <= w)
237            && self.y.checked_add(self.h).is_some_and(|b| b <= h)
238    }
239}
240
241impl From<j2k_core::Rect> for Rect {
242    fn from(rect: j2k_core::Rect) -> Self {
243        Self {
244            x: rect.x,
245            y: rect.y,
246            w: rect.w,
247            h: rect.h,
248        }
249    }
250}
251
252impl From<Rect> for j2k_core::Rect {
253    fn from(rect: Rect) -> Self {
254        Self {
255            x: rect.x,
256            y: rect.y,
257            w: rect.w,
258            h: rect.h,
259        }
260    }
261}
262
263/// Internal JPEG-specific output format used behind the public core
264/// `PixelFormat` + `Downscale` API adapters.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub(crate) enum OutputFormat {
267    Rgb8,
268    Rgb8Scaled { factor: DownscaleFactor },
269    Rgba8 { alpha: u8 },
270    Rgba8Scaled { alpha: u8, factor: DownscaleFactor },
271    Gray8,
272    Gray8Scaled { factor: DownscaleFactor },
273    Gray16,
274    Gray16Scaled { factor: DownscaleFactor },
275    Rgb16,
276    Rgb16Scaled { factor: DownscaleFactor },
277    Rgba16 { alpha: u16 },
278    Rgba16Scaled { alpha: u16, factor: DownscaleFactor },
279}
280
281impl OutputFormat {
282    pub(crate) fn bytes_per_pixel(self) -> usize {
283        match self {
284            Self::Rgb8 | Self::Rgb8Scaled { .. } => 3,
285            Self::Rgba8 { .. } | Self::Rgba8Scaled { .. } => 4,
286            Self::Gray8 | Self::Gray8Scaled { .. } => 1,
287            Self::Gray16 | Self::Gray16Scaled { .. } => 2,
288            Self::Rgb16 | Self::Rgb16Scaled { .. } => 6,
289            Self::Rgba16 { .. } | Self::Rgba16Scaled { .. } => 8,
290        }
291    }
292
293    pub(crate) fn downscale(self) -> DownscaleFactor {
294        match self {
295            Self::Rgb8
296            | Self::Rgba8 { .. }
297            | Self::Gray8
298            | Self::Gray16
299            | Self::Rgb16
300            | Self::Rgba16 { .. } => DownscaleFactor::Full,
301            Self::Rgb8Scaled { factor }
302            | Self::Rgba8Scaled { factor, .. }
303            | Self::Gray8Scaled { factor }
304            | Self::Gray16Scaled { factor }
305            | Self::Rgb16Scaled { factor }
306            | Self::Rgba16Scaled { factor, .. } => factor,
307        }
308    }
309}
310
311/// IDCT-level downscale factor; applies only to DCT-based SOFs (see spec
312/// Section 4 matrix).
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub(crate) enum DownscaleFactor {
315    Full,
316    Half,
317    Quarter,
318    Eighth,
319}
320
321impl DownscaleFactor {
322    pub(crate) const fn denominator(self) -> u32 {
323        match self {
324            Self::Full => 1,
325            Self::Half => 2,
326            Self::Quarter => 4,
327            Self::Eighth => 8,
328        }
329    }
330
331    pub(crate) const fn output_block_size(self) -> u32 {
332        match self {
333            Self::Full => 8,
334            Self::Half => 4,
335            Self::Quarter => 2,
336            Self::Eighth => 1,
337        }
338    }
339}
340
341/// Override for APP14 color-transform detection.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum ColorTransform {
344    /// Detect the transform from APP14 metadata and component layout.
345    Auto,
346    /// Treat three-component data as RGB regardless of APP14 metadata.
347    ForceRgb,
348    /// Treat three-component data as YCbCr regardless of APP14 metadata.
349    ForceYCbCr,
350}
351
352/// Public decode options for JPEG reads.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct DecodeOptions {
355    color_transform: ColorTransform,
356}
357
358impl Default for DecodeOptions {
359    fn default() -> Self {
360        Self {
361            color_transform: ColorTransform::Auto,
362        }
363    }
364}
365
366impl DecodeOptions {
367    /// Override APP14 color-transform detection.
368    pub fn set_color_transform(&mut self, color_transform: ColorTransform) {
369        self.color_transform = color_transform;
370    }
371
372    /// Current color-transform override.
373    pub fn color_transform(&self) -> ColorTransform {
374        self.color_transform
375    }
376
377    /// Builder-style color-transform override.
378    #[must_use]
379    pub fn with_color_transform(mut self, color_transform: ColorTransform) -> Self {
380        self.set_color_transform(color_transform);
381        self
382    }
383
384    pub(crate) fn apply_to_info(self, info: &mut Info) {
385        match (self.color_transform, info.sampling.len()) {
386            (ColorTransform::Auto, _) => {}
387            (ColorTransform::ForceRgb, 3) => info.color_space = ColorSpace::Rgb,
388            (ColorTransform::ForceYCbCr, 3) => info.color_space = ColorSpace::YCbCr,
389            (ColorTransform::ForceRgb | ColorTransform::ForceYCbCr, _) => {}
390        }
391    }
392}
393
394/// Header-derived image metadata. Populated by `Decoder::inspect` and by
395/// `Decoder::new`. `scan_count` is the number of SOS markers observed in
396/// the input — for sequential this is always 1; for progressive it is the
397/// count of refinement passes.
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct Info {
400    /// Image dimensions as `(width, height)` in pixels.
401    pub dimensions: (u32, u32),
402    /// Header-derived color space after APP14 transform handling.
403    pub color_space: ColorSpace,
404    /// Per-component sampling factors from the SOF marker.
405    pub sampling: SamplingFactors,
406    /// Start-of-frame variant that selects the decode pipeline.
407    pub sof_kind: SofKind,
408    /// Sample precision in bits.
409    pub bit_depth: u8,
410    /// Restart interval in MCUs, if a DRI marker was present.
411    pub restart_interval: Option<u16>,
412    /// Derived MCU geometry for the image.
413    pub mcu_geometry: McuGeometry,
414    /// Number of SOS markers observed in the stream.
415    pub scan_count: u16,
416}
417
418impl Info {
419    /// Convert JPEG metadata into the codec-neutral core metadata type.
420    pub fn to_core_info(&self) -> j2k_core::Info {
421        j2k_core::Info {
422            dimensions: self.dimensions,
423            components: self.sampling.len() as u8,
424            colorspace: core_colorspace(self.color_space),
425            bit_depth: self.bit_depth,
426            tile_layout: None,
427            coded_unit_layout: Some(j2k_core::CodedUnitLayout {
428                unit_width: self.mcu_geometry.width,
429                unit_height: self.mcu_geometry.height,
430                units_x: self.mcu_geometry.columns,
431                units_y: self.mcu_geometry.rows,
432            }),
433            restart_interval: self.restart_interval.map(u32::from),
434            resolution_levels: 1,
435        }
436    }
437}
438
439fn core_colorspace(color_space: ColorSpace) -> j2k_core::Colorspace {
440    match color_space {
441        ColorSpace::Grayscale => j2k_core::Colorspace::Grayscale,
442        ColorSpace::YCbCr => j2k_core::Colorspace::YCbCr,
443        ColorSpace::Rgb => j2k_core::Colorspace::Rgb,
444        ColorSpace::Cmyk => j2k_core::Colorspace::Cmyk,
445        ColorSpace::Ycck => j2k_core::Colorspace::Ycck,
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn rect_full_matches_dimensions() {
455        let r = Rect::full((1024, 768));
456        assert_eq!(
457            r,
458            Rect {
459                x: 0,
460                y: 0,
461                w: 1024,
462                h: 768
463            }
464        );
465    }
466
467    #[test]
468    fn rect_is_within_accepts_contained_rect() {
469        assert!(Rect {
470            x: 0,
471            y: 0,
472            w: 100,
473            h: 100
474        }
475        .is_within((100, 100)));
476        assert!(Rect {
477            x: 10,
478            y: 20,
479            w: 30,
480            h: 40
481        }
482        .is_within((100, 100)));
483    }
484
485    #[test]
486    fn rect_is_within_rejects_overflowing_rect() {
487        assert!(!Rect {
488            x: 50,
489            y: 50,
490            w: 60,
491            h: 10
492        }
493        .is_within((100, 100)));
494        assert!(!Rect {
495            x: u32::MAX,
496            y: 0,
497            w: 1,
498            h: 1
499        }
500        .is_within((100, 100)));
501    }
502
503    #[test]
504    fn output_format_bytes_per_pixel_matches_spec() {
505        assert_eq!(OutputFormat::Rgb8.bytes_per_pixel(), 3);
506        assert_eq!(
507            OutputFormat::Rgb8Scaled {
508                factor: DownscaleFactor::Quarter
509            }
510            .bytes_per_pixel(),
511            3
512        );
513        assert_eq!(OutputFormat::Rgba8 { alpha: 255 }.bytes_per_pixel(), 4);
514        assert_eq!(
515            OutputFormat::Rgba8Scaled {
516                alpha: 255,
517                factor: DownscaleFactor::Half,
518            }
519            .bytes_per_pixel(),
520            4
521        );
522        assert_eq!(OutputFormat::Gray8.bytes_per_pixel(), 1);
523        assert_eq!(
524            OutputFormat::Gray8Scaled {
525                factor: DownscaleFactor::Half
526            }
527            .bytes_per_pixel(),
528            1
529        );
530        assert_eq!(OutputFormat::Gray16.bytes_per_pixel(), 2);
531        assert_eq!(
532            OutputFormat::Gray16Scaled {
533                factor: DownscaleFactor::Half
534            }
535            .bytes_per_pixel(),
536            2
537        );
538        assert_eq!(OutputFormat::Rgb16.bytes_per_pixel(), 6);
539        assert_eq!(
540            OutputFormat::Rgb16Scaled {
541                factor: DownscaleFactor::Half
542            }
543            .bytes_per_pixel(),
544            6
545        );
546        assert_eq!(
547            OutputFormat::Rgba16 { alpha: u16::MAX }.bytes_per_pixel(),
548            8
549        );
550        assert_eq!(
551            OutputFormat::Rgba16Scaled {
552                alpha: u16::MAX,
553                factor: DownscaleFactor::Half
554            }
555            .bytes_per_pixel(),
556            8
557        );
558    }
559
560    #[test]
561    fn sampling_factors_store_components_without_heap_state() {
562        let sampling =
563            SamplingFactors::from_components(&[(2, 2), (1, 1), (1, 1)]).expect("sampling");
564        assert_eq!(sampling.len(), 3);
565        assert_eq!(sampling.component(0), Some((2, 2)));
566        assert_eq!(sampling.component(1), Some((1, 1)));
567        assert_eq!(sampling.component(3), None);
568        assert_eq!(sampling.components(), &[(2, 2), (1, 1), (1, 1)]);
569        assert_eq!(sampling.max_h, 2);
570        assert_eq!(sampling.max_v, 2);
571    }
572
573    #[test]
574    fn sampling_factors_reject_empty_component_list() {
575        assert!(matches!(
576            SamplingFactors::from_components(&[]),
577            Err(SamplingFactorsError::Empty)
578        ));
579    }
580
581    #[test]
582    fn sampling_factors_accept_supported_component_counts() {
583        for components in [
584            &[(1, 1)][..],
585            &[(2, 2), (1, 1), (1, 1)][..],
586            &[(1, 1), (1, 1), (1, 1), (1, 1)][..],
587        ] {
588            let sampling = SamplingFactors::from_components(components).expect("sampling");
589            assert_eq!(sampling.len(), components.len());
590            assert_eq!(sampling.components(), components);
591        }
592    }
593
594    #[test]
595    fn sampling_factors_reject_invalid_factors() {
596        assert!(matches!(
597            SamplingFactors::from_components(&[(0, 1)]),
598            Err(SamplingFactorsError::InvalidSampling {
599                component: 0,
600                h: 0,
601                v: 1
602            })
603        ));
604        assert!(matches!(
605            SamplingFactors::from_components(&[(1, 5)]),
606            Err(SamplingFactorsError::InvalidSampling {
607                component: 0,
608                h: 1,
609                v: 5
610            })
611        ));
612    }
613
614    #[test]
615    fn sampling_factors_reject_more_than_four_components_without_panic() {
616        assert!(matches!(
617            SamplingFactors::from_components(&[(1, 1); 5]),
618            Err(SamplingFactorsError::TooManyComponents { count: 5 })
619        ));
620    }
621
622    #[test]
623    fn info_to_core_info_preserves_metadata_for_device_adapters() {
624        let info = Info {
625            dimensions: (32, 16),
626            color_space: ColorSpace::YCbCr,
627            sampling: SamplingFactors::from_components(&[(2, 2), (1, 1), (1, 1)])
628                .expect("sampling"),
629            sof_kind: SofKind::Baseline8,
630            bit_depth: 8,
631            restart_interval: Some(2),
632            mcu_geometry: McuGeometry {
633                width: 16,
634                height: 16,
635                columns: 2,
636                rows: 1,
637                count: 2,
638            },
639            scan_count: 1,
640        };
641
642        let core = info.to_core_info();
643
644        assert_eq!(core.dimensions, (32, 16));
645        assert_eq!(core.components, 3);
646        assert_eq!(core.colorspace, j2k_core::Colorspace::YCbCr);
647        assert_eq!(core.bit_depth, 8);
648        assert_eq!(core.tile_layout, None);
649        assert_eq!(
650            core.coded_unit_layout,
651            Some(j2k_core::CodedUnitLayout {
652                unit_width: 16,
653                unit_height: 16,
654                units_x: 2,
655                units_y: 1,
656            })
657        );
658        assert_eq!(core.restart_interval, Some(2));
659        assert_eq!(core.resolution_levels, 1);
660    }
661
662    #[test]
663    fn info_to_core_info_preserves_four_component_colorspaces() {
664        for (color_space, core_colorspace) in [
665            (ColorSpace::Cmyk, j2k_core::Colorspace::Cmyk),
666            (ColorSpace::Ycck, j2k_core::Colorspace::Ycck),
667        ] {
668            let info = Info {
669                dimensions: (64, 32),
670                color_space,
671                sampling: SamplingFactors::from_components(&[(1, 1), (1, 1), (1, 1), (1, 1)])
672                    .expect("sampling"),
673                sof_kind: SofKind::Baseline8,
674                bit_depth: 8,
675                restart_interval: None,
676                mcu_geometry: McuGeometry {
677                    width: 8,
678                    height: 8,
679                    columns: 8,
680                    rows: 4,
681                    count: 32,
682                },
683                scan_count: 1,
684            };
685
686            let core = info.to_core_info();
687
688            assert_eq!(core.components, 4);
689            assert_eq!(core.colorspace, core_colorspace);
690            assert_eq!(
691                core.coded_unit_layout,
692                Some(j2k_core::CodedUnitLayout {
693                    unit_width: 8,
694                    unit_height: 8,
695                    units_x: 8,
696                    units_y: 4,
697                })
698            );
699        }
700    }
701}