Skip to main content

phasm_core/codec/jpeg/
frame.rs

1// Copyright (c) 2026 Christoph Gaffga
2// SPDX-License-Identifier: GPL-3.0-only
3// https://github.com/cgaffga/phasmcore
4
5//! JPEG frame header (SOF0) parsing.
6//!
7//! Extracts image dimensions, component information, and sampling factors
8//! from the Start of Frame marker segment.
9
10use super::error::{JpegError, Result};
11
12/// Information about one image component from SOF.
13#[derive(Debug, Clone)]
14pub struct Component {
15    /// Component ID (typically 1=Y, 2=Cb, 3=Cr).
16    pub id: u8,
17    /// Horizontal sampling factor (1–4).
18    pub h_sampling: u8,
19    /// Vertical sampling factor (1–4).
20    pub v_sampling: u8,
21    /// Quantization table ID (0–3).
22    pub quant_table_id: u8,
23}
24
25/// Frame information parsed from SOF0/SOF2 marker.
26#[derive(Debug, Clone)]
27pub struct FrameInfo {
28    /// Sample precision in bits (must be 8).
29    pub precision: u8,
30    /// Image height in pixels.
31    pub height: u16,
32    /// Image width in pixels.
33    pub width: u16,
34    /// Components in the frame.
35    pub components: Vec<Component>,
36    /// Maximum horizontal sampling factor across all components.
37    pub max_h_sampling: u8,
38    /// Maximum vertical sampling factor across all components.
39    pub max_v_sampling: u8,
40    /// MCU width in blocks (= max_h_sampling * 8 pixels).
41    pub mcu_width: u16,
42    /// MCU height in blocks (= max_v_sampling * 8 pixels).
43    pub mcu_height: u16,
44    /// Number of MCUs horizontally.
45    pub mcus_wide: u16,
46    /// Number of MCUs vertically.
47    pub mcus_tall: u16,
48    /// Whether this is a progressive JPEG (SOF2). False for baseline (SOF0).
49    pub is_progressive: bool,
50}
51
52impl FrameInfo {
53    /// Number of 8×8 blocks wide for a given component.
54    pub fn blocks_wide(&self, comp_idx: usize) -> usize {
55        let comp = &self.components[comp_idx];
56        (self.mcus_wide as usize) * (comp.h_sampling as usize)
57    }
58
59    /// Number of 8×8 blocks tall for a given component.
60    pub fn blocks_tall(&self, comp_idx: usize) -> usize {
61        let comp = &self.components[comp_idx];
62        (self.mcus_tall as usize) * (comp.v_sampling as usize)
63    }
64}
65
66/// Parse a SOF0/SOF2 marker segment body (after the 2-byte length).
67/// `progressive` should be true for SOF2 markers.
68pub fn parse_sof(data: &[u8]) -> Result<FrameInfo> {
69    parse_sof_ext(data, false)
70}
71
72/// Parse a SOF marker segment body with explicit progressive flag.
73pub fn parse_sof_ext(data: &[u8], progressive: bool) -> Result<FrameInfo> {
74    if data.len() < 6 {
75        return Err(JpegError::UnexpectedEof);
76    }
77
78    let precision = data[0];
79    if precision != 8 {
80        return Err(JpegError::UnsupportedPrecision(precision));
81    }
82
83    let height = u16::from_be_bytes([data[1], data[2]]);
84    let width = u16::from_be_bytes([data[3], data[4]]);
85    let num_components = data[5] as usize;
86
87    if width == 0 || height == 0 {
88        return Err(JpegError::InvalidDimensions);
89    }
90    if data.len() < 6 + num_components * 3 {
91        return Err(JpegError::UnexpectedEof);
92    }
93
94    let mut components = Vec::with_capacity(num_components);
95    let mut max_h = 0u8;
96    let mut max_v = 0u8;
97
98    for i in 0..num_components {
99        let offset = 6 + i * 3;
100        let id = data[offset];
101        let sampling = data[offset + 1];
102        let h_sampling = sampling >> 4;
103        let v_sampling = sampling & 0x0F;
104        let quant_table_id = data[offset + 2];
105
106        if h_sampling == 0 || v_sampling == 0 || h_sampling > 4 || v_sampling > 4 {
107            return Err(JpegError::InvalidDimensions);
108        }
109        if quant_table_id > 3 {
110            return Err(JpegError::InvalidQuantTableId(quant_table_id));
111        }
112
113        max_h = max_h.max(h_sampling);
114        max_v = max_v.max(v_sampling);
115
116        components.push(Component {
117            id,
118            h_sampling,
119            v_sampling,
120            quant_table_id,
121        });
122    }
123
124    let mcu_width = (max_h as u16) * 8;
125    let mcu_height = (max_v as u16) * 8;
126    let mcus_wide = width.div_ceil(mcu_width);
127    let mcus_tall = height.div_ceil(mcu_height);
128
129    Ok(FrameInfo {
130        precision,
131        height,
132        width,
133        components,
134        max_h_sampling: max_h,
135        max_v_sampling: max_v,
136        mcu_width,
137        mcu_height,
138        mcus_wide,
139        mcus_tall,
140        is_progressive: progressive,
141    })
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn parse_ycbcr_420() {
150        // SOF0 body: precision=8, height=480, width=640, 3 components
151        // Y: id=1, h=2, v=2, qt=0
152        // Cb: id=2, h=1, v=1, qt=1
153        // Cr: id=3, h=1, v=1, qt=1
154        let data = [
155            8, 1, 0xE0, 2, 0x80, 3, // precision, height=480, width=640, 3 comps
156            1, 0x22, 0, // Y: 2x2, qt=0
157            2, 0x11, 1, // Cb: 1x1, qt=1
158            3, 0x11, 1, // Cr: 1x1, qt=1
159        ];
160
161        let fi = parse_sof(&data).unwrap();
162        assert_eq!(fi.precision, 8);
163        assert_eq!(fi.height, 480);
164        assert_eq!(fi.width, 640);
165        assert_eq!(fi.components.len(), 3);
166        assert_eq!(fi.max_h_sampling, 2);
167        assert_eq!(fi.max_v_sampling, 2);
168        assert_eq!(fi.mcu_width, 16);
169        assert_eq!(fi.mcu_height, 16);
170        assert_eq!(fi.mcus_wide, 40);  // 640/16
171        assert_eq!(fi.mcus_tall, 30);  // 480/16
172
173        // Blocks for Y: 40*2=80 wide, 30*2=60 tall
174        assert_eq!(fi.blocks_wide(0), 80);
175        assert_eq!(fi.blocks_tall(0), 60);
176        // Blocks for Cb: 40*1=40 wide, 30*1=30 tall
177        assert_eq!(fi.blocks_wide(1), 40);
178        assert_eq!(fi.blocks_tall(1), 30);
179    }
180
181    #[test]
182    fn parse_grayscale() {
183        let data = [
184            8, 0, 64, 0, 64, 1, // 64x64, 1 component
185            1, 0x11, 0, // Y: 1x1, qt=0
186        ];
187        let fi = parse_sof(&data).unwrap();
188        assert_eq!(fi.components.len(), 1);
189        assert_eq!(fi.mcus_wide, 8);  // 64/8
190        assert_eq!(fi.mcus_tall, 8);
191    }
192
193    #[test]
194    fn parse_non_mcu_aligned() {
195        // 10x10 image with 1x1 sampling → 2x2 MCUs (ceil)
196        let data = [
197            8, 0, 10, 0, 10, 1,
198            1, 0x11, 0,
199        ];
200        let fi = parse_sof(&data).unwrap();
201        assert_eq!(fi.mcus_wide, 2); // ceil(10/8)
202        assert_eq!(fi.mcus_tall, 2);
203    }
204
205    #[test]
206    fn reject_12bit() {
207        let data = [12, 0, 8, 0, 8, 1, 1, 0x11, 0];
208        assert!(matches!(
209            parse_sof(&data),
210            Err(JpegError::UnsupportedPrecision(12))
211        ));
212    }
213}