From 7ff39bfda85fae5eca6eb6b6e8a0990611a26008 Mon Sep 17 00:00:00 2001 From: Christos Date: Mon, 9 Nov 2026 14:03:25 +0200 Subject: [PATCH 1/4] image-codecs: read a JPEG's frame from its markers (jpeg_info) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For storytold/photocraft#12473: PhotoCraft lifts its 2^30 px JPEG cap only after admitting the decode against free memory, and for that it needs the frame before decoding. jpeg_info reads it from the markers alone: the stored size, the layout decode returns, and whole_coefficients, set when decode hands the file to craft-jpeg (arithmetic-coded, lossless or 12-bit frames), whose working set is several times the decoded image. None without a frame of 1, 3 or 4 components. This was part of storytold/photocraft#12604 until storytold/photocraft#14236 moved the codecs here. Co-Authored-By: Νύξ (Nyx, AI) 🌑 --- crates/image-codecs/src/codecs/jpeg.rs | 27 ++++++++++++++++++++++++++ crates/image-codecs/src/lib.rs | 17 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/crates/image-codecs/src/codecs/jpeg.rs b/crates/image-codecs/src/codecs/jpeg.rs index 8e68451ef..2612a7f22 100644 --- a/crates/image-codecs/src/codecs/jpeg.rs +++ b/crates/image-codecs/src/codecs/jpeg.rs @@ -42,6 +42,18 @@ pub(crate) struct JpegMeta { pub frame: Option<(u32, u32, u8)>, } +/// The first frame of `b` as [`crate::JpegInfo`], from its markers alone (see [`crate::jpeg_info`]). +pub(crate) fn info(b: &[u8]) -> Option { + let (width, height, components) = scan_metadata(b).frame?; + let layout = match components { + 1 => ChannelLayout::Gray, + 3 => ChannelLayout::Rgb, + 4 => ChannelLayout::Cmyk, + _ => return None, + }; + Some(crate::JpegInfo { width, height, layout, whole_coefficients: uses_craft_jpeg(b) }) +} + /// Walk the marker segments up to SOS and collect metadata. pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { let mut m = JpegMeta::default(); @@ -573,6 +585,21 @@ mod tests { bytes.get(2..11) == Some(b"\xff\xe0\0\x10JFIF\0") } + /// `info` reads the frame from the markers, and which decoder the file goes to. + #[test] + fn info_reads_the_frame_and_the_decoder() { + let img = Image::from_raw(37, 21, ChannelLayout::Rgb, SampleType::U8, vec![128; 37 * 21 * 3]).unwrap(); + let rgb = crate::encode(&img, Format::Jpeg, &EncodeOptions::default()).unwrap(); + assert_eq!(info(&rgb), Some(crate::JpegInfo { width: 37, height: 21, layout: ChannelLayout::Rgb, whole_coefficients: false })); + // A 12-bit extended frame (SOF1) declaring 40,000 × 30,000 gray, markers only. + let mut twelve = vec![0xFF, 0xD8, 0xFF, 0xC1, 0, 11, 12]; + twelve.extend_from_slice(&30_000u16.to_be_bytes()); + twelve.extend_from_slice(&40_000u16.to_be_bytes()); + twelve.extend_from_slice(&[1, 1, 0x11, 0, 0xFF, 0xD9]); + assert_eq!(info(&twelve), Some(crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true })); + assert_eq!(info(b"not a jpeg"), None); + } + /// An APP1 Extended XMP fragment (GUID, total size, offset, data). fn extended_xmp(offset: u32) -> Vec { let mut seg = EXTENDED_XMP_HEADER.to_vec(); diff --git a/crates/image-codecs/src/lib.rs b/crates/image-codecs/src/lib.rs index d3ab272fa..3045cd090 100644 --- a/crates/image-codecs/src/lib.rs +++ b/crates/image-codecs/src/lib.rs @@ -212,6 +212,23 @@ pub fn probe_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> { Ok(if (5..=8).contains(&o) { (h, w) } else { (w, h) }) } +/// A JPEG's frame as its header declares it, read without decoding pixels: the stored size, the +/// layout [`decode`] returns, and whether [`decode`] hands the file to its whole-coefficient +/// decoder (arithmetic-coded, lossless or 12-bit frames), whose working set is several times the +/// decoded image. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct JpegInfo { + pub width: u32, + pub height: u32, + pub layout: ChannelLayout, + pub whole_coefficients: bool, +} + +/// [`JpegInfo`] of `bytes`; `None` without a readable frame of 1, 3 or 4 components. +pub fn jpeg_info(bytes: &[u8]) -> Option { + jpeg::info(bytes) +} + /// The colour table of a GIF's first frame (its Local Color Table, else the Global one) and the /// entry its Graphic Control Extension makes transparent, read without decoding pixels. pub fn gif_palette(bytes: &[u8]) -> Option<(Vec<[u8; 3]>, Option)> { From 44dfda20d7684195deeb68a3f4075e1ea26bad91 Mon Sep 17 00:00:00 2001 From: Christos Date: Mon, 9 Nov 2026 20:38:25 +0200 Subject: [PATCH 2/4] image-codecs: count the coefficients an 8-bit progressive decode keeps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zune-jpeg keeps every coefficient of an 8-bit progressive frame (SOF2) beside its output: 2 bytes per sample, per component at its sampling. That is 9.85 GB for a 48,000 x 34,200 4:4:4 file, more than the decoded image. JpegInfo::progressive_coefficients reports them from the frame header, so a caller can admit the decode against its real peak. Co-Authored-By: Νύξ (Nyx, AI) 🌑 --- crates/image-codecs/src/codecs/jpeg.rs | 57 ++++++++++++++++++++++++-- crates/image-codecs/src/lib.rs | 9 +++- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/crates/image-codecs/src/codecs/jpeg.rs b/crates/image-codecs/src/codecs/jpeg.rs index 2612a7f22..3664c2666 100644 --- a/crates/image-codecs/src/codecs/jpeg.rs +++ b/crates/image-codecs/src/codecs/jpeg.rs @@ -40,18 +40,37 @@ pub(crate) struct JpegMeta { pub adobe_transform: Option, /// (width, height, components) from the first SOFn. pub frame: Option<(u32, u32, u8)>, + /// [`crate::JpegInfo::progressive_coefficients`] of the first SOFn. + pub progressive_coefficients: u64, } /// The first frame of `b` as [`crate::JpegInfo`], from its markers alone (see [`crate::jpeg_info`]). pub(crate) fn info(b: &[u8]) -> Option { - let (width, height, components) = scan_metadata(b).frame?; + let m = scan_metadata(b); + let (width, height, components) = m.frame?; let layout = match components { 1 => ChannelLayout::Gray, 3 => ChannelLayout::Rgb, 4 => ChannelLayout::Cmyk, _ => return None, }; - Some(crate::JpegInfo { width, height, layout, whole_coefficients: uses_craft_jpeg(b) }) + let progressive_coefficients = m.progressive_coefficients; + Some(crate::JpegInfo { width, height, layout, whole_coefficients: uses_craft_jpeg(b), progressive_coefficients }) +} + +/// Bytes of the coefficients `zune-jpeg` keeps through an 8-bit progressive frame (SOF2), from +/// its header segment `seg`: per component, a 64-value block of `i16`s for each of its H × V +/// blocks in every MCU (A.2), the MCUs covering the frame at the largest sampling factors. 0 +/// for any other frame. +fn progressive_coefficients(marker: u8, seg: &[u8]) -> u64 { + let (0xC2, &[8, h0, h1, w0, w1, n, ref comps @ ..]) = (marker, seg) else { return 0 }; + let (h, w) = (u64::from(u16::from_be_bytes([h0, h1])), u64::from(u16::from_be_bytes([w0, w1]))); + let factors: Vec<(u64, u64)> = + comps.as_chunks::<3>().0.iter().take(usize::from(n)).map(|&[_, f, _]| (u64::from(f >> 4), u64::from(f & 15))).collect(); + let hmax = factors.iter().map(|f| f.0).max().unwrap_or(1).max(1); + let vmax = factors.iter().map(|f| f.1).max().unwrap_or(1).max(1); + let mcus = w.div_ceil(8 * hmax) * h.div_ceil(8 * vmax); + factors.iter().map(|&(fh, fv)| mcus * fh * fv * 64 * 2).sum() } /// Walk the marker segments up to SOS and collect metadata. @@ -112,6 +131,7 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { let h = u16::from_be_bytes([seg[1], seg[2]]) as u32; let w = u16::from_be_bytes([seg[3], seg[4]]) as u32; m.frame = Some((w, h, seg[5])); + m.progressive_coefficients = progressive_coefficients(marker, seg); } 0xEE if seg.len() >= 12 && seg.starts_with(b"Adobe") => { m.adobe_transform = Some(seg[11]); @@ -590,16 +610,45 @@ mod tests { fn info_reads_the_frame_and_the_decoder() { let img = Image::from_raw(37, 21, ChannelLayout::Rgb, SampleType::U8, vec![128; 37 * 21 * 3]).unwrap(); let rgb = crate::encode(&img, Format::Jpeg, &EncodeOptions::default()).unwrap(); - assert_eq!(info(&rgb), Some(crate::JpegInfo { width: 37, height: 21, layout: ChannelLayout::Rgb, whole_coefficients: false })); + let rgb_info = crate::JpegInfo { width: 37, height: 21, layout: ChannelLayout::Rgb, whole_coefficients: false, progressive_coefficients: 0 }; + assert_eq!(info(&rgb), Some(rgb_info)); // A 12-bit extended frame (SOF1) declaring 40,000 × 30,000 gray, markers only. let mut twelve = vec![0xFF, 0xD8, 0xFF, 0xC1, 0, 11, 12]; twelve.extend_from_slice(&30_000u16.to_be_bytes()); twelve.extend_from_slice(&40_000u16.to_be_bytes()); twelve.extend_from_slice(&[1, 1, 0x11, 0, 0xFF, 0xD9]); - assert_eq!(info(&twelve), Some(crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true })); + let twelve_info = + crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true, progressive_coefficients: 0 }; + assert_eq!(info(&twelve), Some(twelve_info)); assert_eq!(info(b"not a jpeg"), None); } + /// An 8-bit progressive frame (SOF2) reports every coefficient the decoder keeps through its + /// scans: a block of 64 `i16`s per component block of every MCU. + #[test] + fn info_counts_a_progressive_frames_coefficients() { + // Markers only: SOI, a frame of `marker` declaring 48,000 × 34,200 with `factors`, EOI. + let frame = |marker: u8, precision: u8, factors: &[u8]| { + let mut b = vec![0xFF, 0xD8, 0xFF, marker, 0, 8 + 3 * factors.len() as u8, precision]; + b.extend_from_slice(&34_200u16.to_be_bytes()); + b.extend_from_slice(&48_000u16.to_be_bytes()); + b.push(factors.len() as u8); + for (i, &f) in factors.iter().enumerate() { + b.extend_from_slice(&[i as u8 + 1, f, 0]); + } + b.extend_from_slice(&[0xFF, 0xD9]); + b + }; + let coefficients = |b: &[u8]| info(b).map(|i| i.progressive_coefficients); + // 4:4:4: 6,000 × 4,275 MCUs of one block per component. + assert_eq!(coefficients(&frame(0xC2, 8, &[0x11, 0x11, 0x11])), Some(6_000 * 4_275 * 3 * 128)); + // 4:2:0: 3,000 × 2,138 MCUs of 4 + 1 + 1 blocks. + assert_eq!(coefficients(&frame(0xC2, 8, &[0x22, 0x11, 0x11])), Some(3_000 * 2_138 * 6 * 128)); + // Baseline and 12-bit progressive frames (the latter decoded by craft-jpeg) hold none. + assert_eq!(coefficients(&frame(0xC0, 8, &[0x11, 0x11, 0x11])), Some(0)); + assert_eq!(coefficients(&frame(0xC2, 12, &[0x11, 0x11, 0x11])), Some(0)); + } + /// An APP1 Extended XMP fragment (GUID, total size, offset, data). fn extended_xmp(offset: u32) -> Vec { let mut seg = EXTENDED_XMP_HEADER.to_vec(); diff --git a/crates/image-codecs/src/lib.rs b/crates/image-codecs/src/lib.rs index 3045cd090..018890286 100644 --- a/crates/image-codecs/src/lib.rs +++ b/crates/image-codecs/src/lib.rs @@ -213,15 +213,20 @@ pub fn probe_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> { } /// A JPEG's frame as its header declares it, read without decoding pixels: the stored size, the -/// layout [`decode`] returns, and whether [`decode`] hands the file to its whole-coefficient +/// layout [`decode`] returns, whether [`decode`] hands the file to its whole-coefficient /// decoder (arithmetic-coded, lossless or 12-bit frames), whose working set is several times the -/// decoded image. +/// decoded image, and the coefficients it holds beside its output for an 8-bit progressive one. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct JpegInfo { pub width: u32, pub height: u32, pub layout: ChannelLayout, pub whole_coefficients: bool, + /// Bytes of DCT coefficients [`decode`] holds at once beside its output for an 8-bit + /// progressive frame (SOF2): every block of every component, at its own sampling, 64 + /// 16-bit values each, since the scans refine them in turn. 0 for the frames it decodes a + /// line of MCUs at a time. + pub progressive_coefficients: u64, } /// [`JpegInfo`] of `bytes`; `None` without a readable frame of 1, 3 or 4 components. From b3558f68213c47969e2a4350c0b897c01bd12335 Mon Sep 17 00:00:00 2001 From: Christos Date: Mon, 9 Nov 2026 21:15:27 +0200 Subject: [PATCH 3/4] image-codecs: test jpeg_info with four and two components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four components read as CMYK. Two, a layout decode doesn't return, give None. Co-Authored-By: Νύξ (Nyx, AI) 🌑 --- crates/image-codecs/src/codecs/jpeg.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/image-codecs/src/codecs/jpeg.rs b/crates/image-codecs/src/codecs/jpeg.rs index 3664c2666..b814a025b 100644 --- a/crates/image-codecs/src/codecs/jpeg.rs +++ b/crates/image-codecs/src/codecs/jpeg.rs @@ -621,6 +621,17 @@ mod tests { crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true, progressive_coefficients: 0 }; assert_eq!(info(&twelve), Some(twelve_info)); assert_eq!(info(b"not a jpeg"), None); + // Markers only, 200 × 100 with `n` components: four are CMYK, two no layout `decode` returns. + let sof0 = |n: u8| { + let mut b = vec![0xFF, 0xD8, 0xFF, 0xC0, 0, 8 + 3 * n, 8, 0, 100, 0, 200, n]; + for i in 1..=n { + b.extend_from_slice(&[i, 0x11, 0]); + } + b.extend_from_slice(&[0xFF, 0xD9]); + b + }; + assert_eq!(info(&sof0(4)).map(|i| (i.width, i.height, i.layout)), Some((200, 100, ChannelLayout::Cmyk))); + assert_eq!(info(&sof0(2)), None); } /// An 8-bit progressive frame (SOF2) reports every coefficient the decoder keeps through its From b270477c251c1096d9c9686ed32e0f21166a1ad5 Mon Sep 17 00:00:00 2001 From: Christos Date: Mon, 9 Nov 2026 21:47:08 +0200 Subject: [PATCH 4/4] image-codecs: count the coefficients of a frame whose first scan lacks components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zune-jpeg also holds every coefficient beside its output when an 8-bit sequential frame's first scan lacks some of its components: no row is finished until the scans that bring the others. jpeg_info now reads the first scan's component count and reports these as held_coefficients (was progressive_coefficients). The #12473 map rewritten with one component per scan was admitted at 12.6 GB without this, and OOM-killed in a 14 GB cgroup. Co-Authored-By: Νύξ (Nyx, AI) 🌑 --- crates/image-codecs/src/codecs/jpeg.rs | 79 ++++++++++++++++++-------- crates/image-codecs/src/lib.rs | 13 +++-- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/crates/image-codecs/src/codecs/jpeg.rs b/crates/image-codecs/src/codecs/jpeg.rs index b814a025b..8d613ce38 100644 --- a/crates/image-codecs/src/codecs/jpeg.rs +++ b/crates/image-codecs/src/codecs/jpeg.rs @@ -40,8 +40,8 @@ pub(crate) struct JpegMeta { pub adobe_transform: Option, /// (width, height, components) from the first SOFn. pub frame: Option<(u32, u32, u8)>, - /// [`crate::JpegInfo::progressive_coefficients`] of the first SOFn. - pub progressive_coefficients: u64, + /// [`crate::JpegInfo::held_coefficients`] of the first SOFn and the first scan. + pub held_coefficients: u64, } /// The first frame of `b` as [`crate::JpegInfo`], from its markers alone (see [`crate::jpeg_info`]). @@ -54,16 +54,27 @@ pub(crate) fn info(b: &[u8]) -> Option { 4 => ChannelLayout::Cmyk, _ => return None, }; - let progressive_coefficients = m.progressive_coefficients; - Some(crate::JpegInfo { width, height, layout, whole_coefficients: uses_craft_jpeg(b), progressive_coefficients }) + let held_coefficients = m.held_coefficients; + Some(crate::JpegInfo { width, height, layout, whole_coefficients: uses_craft_jpeg(b), held_coefficients }) } -/// Bytes of the coefficients `zune-jpeg` keeps through an 8-bit progressive frame (SOF2), from -/// its header segment `seg`: per component, a 64-value block of `i16`s for each of its H × V -/// blocks in every MCU (A.2), the MCUs covering the frame at the largest sampling factors. 0 -/// for any other frame. -fn progressive_coefficients(marker: u8, seg: &[u8]) -> u64 { - let (0xC2, &[8, h0, h1, w0, w1, n, ref comps @ ..]) = (marker, seg) else { return 0 }; +/// Bytes of the coefficients `zune-jpeg` holds beside its output for a frame it can't turn into +/// rows as it reads (see [`crate::JpegInfo::held_coefficients`]), from the frame's header +/// segment `seg` and the number of components in its first scan: per component, a 64-value +/// block of `i16`s for each of its H × V blocks in every MCU (A.2), the MCUs covering the frame +/// at the largest sampling factors. 0 for any other frame. +fn held_coefficients(marker: u8, seg: &[u8], scan_components: Option) -> u64 { + let &[8, h0, h1, w0, w1, n, ref comps @ ..] = seg else { return 0 }; + // Progressive scans refine every block in turn; a sequential first scan without every + // component leaves its rows unfinished until the scans that bring the others. + let held = match marker { + 0xC2 => true, + 0xC0 | 0xC1 => scan_components.is_some_and(|s| s < n), + _ => false, + }; + if !held { + return 0; + } let (h, w) = (u64::from(u16::from_be_bytes([h0, h1])), u64::from(u16::from_be_bytes([w0, w1]))); let factors: Vec<(u64, u64)> = comps.as_chunks::<3>().0.iter().take(usize::from(n)).map(|&[_, f, _]| (u64::from(f >> 4), u64::from(f & 15))).collect(); @@ -80,6 +91,9 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { // Photoshop splits a large resource block over consecutive APP13 segments. let mut photoshop: Vec = Vec::new(); let mut jfif_dpi = None; + // The first frame header and the number of components in the first scan. + let mut sof: Option<(u8, &[u8])> = None; + let mut scan_components = None; let mut i = 2; while i + 4 <= b.len() { if b[i] != 0xFF { @@ -95,6 +109,7 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { continue; } if marker == 0xDA || marker == 0xD9 { + scan_components = (marker == 0xDA).then(|| b.get(i + 4).copied()).flatten(); break; } let len = u16::from_be_bytes([b[i + 2], b[i + 3]]) as usize; @@ -131,7 +146,7 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { let h = u16::from_be_bytes([seg[1], seg[2]]) as u32; let w = u16::from_be_bytes([seg[3], seg[4]]) as u32; m.frame = Some((w, h, seg[5])); - m.progressive_coefficients = progressive_coefficients(marker, seg); + sof = Some((marker, seg)); } 0xEE if seg.len() >= 12 && seg.starts_with(b"Adobe") => { m.adobe_transform = Some(seg[11]); @@ -140,6 +155,7 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta { } i += 2 + len; } + m.held_coefficients = sof.map_or(0, |(marker, seg)| held_coefficients(marker, seg, scan_components)); m.dpi = photoshop_resolution(&photoshop) .or_else(|| m.xmp.as_deref().and_then(xmp_resolution)) .or_else(|| m.exif.as_deref().and_then(exif_resolution)) @@ -610,7 +626,7 @@ mod tests { fn info_reads_the_frame_and_the_decoder() { let img = Image::from_raw(37, 21, ChannelLayout::Rgb, SampleType::U8, vec![128; 37 * 21 * 3]).unwrap(); let rgb = crate::encode(&img, Format::Jpeg, &EncodeOptions::default()).unwrap(); - let rgb_info = crate::JpegInfo { width: 37, height: 21, layout: ChannelLayout::Rgb, whole_coefficients: false, progressive_coefficients: 0 }; + let rgb_info = crate::JpegInfo { width: 37, height: 21, layout: ChannelLayout::Rgb, whole_coefficients: false, held_coefficients: 0 }; assert_eq!(info(&rgb), Some(rgb_info)); // A 12-bit extended frame (SOF1) declaring 40,000 × 30,000 gray, markers only. let mut twelve = vec![0xFF, 0xD8, 0xFF, 0xC1, 0, 11, 12]; @@ -618,7 +634,7 @@ mod tests { twelve.extend_from_slice(&40_000u16.to_be_bytes()); twelve.extend_from_slice(&[1, 1, 0x11, 0, 0xFF, 0xD9]); let twelve_info = - crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true, progressive_coefficients: 0 }; + crate::JpegInfo { width: 40_000, height: 30_000, layout: ChannelLayout::Gray, whole_coefficients: true, held_coefficients: 0 }; assert_eq!(info(&twelve), Some(twelve_info)); assert_eq!(info(b"not a jpeg"), None); // Markers only, 200 × 100 with `n` components: four are CMYK, two no layout `decode` returns. @@ -634,12 +650,14 @@ mod tests { assert_eq!(info(&sof0(2)), None); } - /// An 8-bit progressive frame (SOF2) reports every coefficient the decoder keeps through its - /// scans: a block of 64 `i16`s per component block of every MCU. + /// A frame the decoder can't turn into rows as it reads reports every coefficient it holds: + /// a block of 64 `i16`s per component block of every MCU. That is an 8-bit progressive frame + /// (SOF2), or an 8-bit sequential one whose first scan lacks some of its components. #[test] - fn info_counts_a_progressive_frames_coefficients() { - // Markers only: SOI, a frame of `marker` declaring 48,000 × 34,200 with `factors`, EOI. - let frame = |marker: u8, precision: u8, factors: &[u8]| { + fn info_counts_the_coefficients_a_decode_holds() { + // Markers only: SOI, a frame of `marker` declaring 48,000 × 34,200 with `factors`, a first + // scan of the first `scan` components when given, EOI. + let frame = |marker: u8, precision: u8, factors: &[u8], scan: Option| { let mut b = vec![0xFF, 0xD8, 0xFF, marker, 0, 8 + 3 * factors.len() as u8, precision]; b.extend_from_slice(&34_200u16.to_be_bytes()); b.extend_from_slice(&48_000u16.to_be_bytes()); @@ -647,17 +665,30 @@ mod tests { for (i, &f) in factors.iter().enumerate() { b.extend_from_slice(&[i as u8 + 1, f, 0]); } + if let Some(n) = scan { + b.extend_from_slice(&[0xFF, 0xDA, 0, 6 + 2 * n, n]); + for c in 1..=n { + b.extend_from_slice(&[c, 0]); + } + b.extend_from_slice(&[0, 63, 0]); + } b.extend_from_slice(&[0xFF, 0xD9]); b }; - let coefficients = |b: &[u8]| info(b).map(|i| i.progressive_coefficients); + let held = |b: &[u8]| info(b).map(|i| i.held_coefficients); + let all_444 = 6_000 * 4_275 * 3 * 128; // 4:4:4: 6,000 × 4,275 MCUs of one block per component. - assert_eq!(coefficients(&frame(0xC2, 8, &[0x11, 0x11, 0x11])), Some(6_000 * 4_275 * 3 * 128)); + assert_eq!(held(&frame(0xC2, 8, &[0x11, 0x11, 0x11], Some(1))), Some(all_444)); // 4:2:0: 3,000 × 2,138 MCUs of 4 + 1 + 1 blocks. - assert_eq!(coefficients(&frame(0xC2, 8, &[0x22, 0x11, 0x11])), Some(3_000 * 2_138 * 6 * 128)); - // Baseline and 12-bit progressive frames (the latter decoded by craft-jpeg) hold none. - assert_eq!(coefficients(&frame(0xC0, 8, &[0x11, 0x11, 0x11])), Some(0)); - assert_eq!(coefficients(&frame(0xC2, 12, &[0x11, 0x11, 0x11])), Some(0)); + assert_eq!(held(&frame(0xC2, 8, &[0x22, 0x11, 0x11], None)), Some(3_000 * 2_138 * 6 * 128)); + // Sequential frames whose first scan lacks components hold them all too. + assert_eq!(held(&frame(0xC0, 8, &[0x11, 0x11, 0x11], Some(1))), Some(all_444)); + assert_eq!(held(&frame(0xC1, 8, &[0x11, 0x11, 0x11], Some(2))), Some(all_444)); + // A sequential first scan with every component is read a line of MCUs at a time, and a + // 12-bit progressive frame goes to craft-jpeg: they hold none. + assert_eq!(held(&frame(0xC0, 8, &[0x11, 0x11, 0x11], Some(3))), Some(0)); + assert_eq!(held(&frame(0xC0, 8, &[0x11, 0x11, 0x11], None)), Some(0)); + assert_eq!(held(&frame(0xC2, 12, &[0x11, 0x11, 0x11], None)), Some(0)); } /// An APP1 Extended XMP fragment (GUID, total size, offset, data). diff --git a/crates/image-codecs/src/lib.rs b/crates/image-codecs/src/lib.rs index 018890286..2c1fb1e40 100644 --- a/crates/image-codecs/src/lib.rs +++ b/crates/image-codecs/src/lib.rs @@ -215,18 +215,19 @@ pub fn probe_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> { /// A JPEG's frame as its header declares it, read without decoding pixels: the stored size, the /// layout [`decode`] returns, whether [`decode`] hands the file to its whole-coefficient /// decoder (arithmetic-coded, lossless or 12-bit frames), whose working set is several times the -/// decoded image, and the coefficients it holds beside its output for an 8-bit progressive one. +/// decoded image, and the coefficients it holds beside its output when it can't decode row by row. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct JpegInfo { pub width: u32, pub height: u32, pub layout: ChannelLayout, pub whole_coefficients: bool, - /// Bytes of DCT coefficients [`decode`] holds at once beside its output for an 8-bit - /// progressive frame (SOF2): every block of every component, at its own sampling, 64 - /// 16-bit values each, since the scans refine them in turn. 0 for the frames it decodes a - /// line of MCUs at a time. - pub progressive_coefficients: u64, + /// Bytes of DCT coefficients [`decode`] holds at once beside its output when it can't turn + /// the frame into rows as it reads: an 8-bit progressive frame (SOF2), whose scans refine + /// every block in turn, or an 8-bit Huffman one (SOF0, SOF1) whose first scan lacks some of + /// its components. Every block of every component at its own sampling, 64 16-bit values + /// each; 0 for the frames it decodes a line of MCUs at a time. + pub held_coefficients: u64, } /// [`JpegInfo`] of `bytes`; `None` without a readable frame of 1, 3 or 4 components.