Skip to main content

j2k_jpeg/
info.rs

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