From 9aac391e045c3c5894c1112e537d8ea2da382521 Mon Sep 17 00:00:00 2001 From: samhyun Date: Thu, 29 Oct 2026 10:29:56 +0900 Subject: [PATCH 1/2] M8: a progressive JPEG sampling a component finer than its first is refused, not decoded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zune-jpeg 0.5.15, the decoder behind `image`, asserts in its upsampler on a progressive frame whose later component is sampled more finely than its first (etemesi254/zune-image#236; an 8 × 8 Y 1×1, Cb 2×2 file is enough). The panic reached everywhere such an image is decoded: the CLI died writing a thumbnail, the desktop canvas's guard caught it on every render of the page, and in the browser it aborted the tab. The fix in zune-jpeg (36007420) is in 0.5.16 pre-releases only, which `image`'s requirement doesn't take. Such a JPEG is now refused from its frame header before the decoder sees it, as an image past the byte budget is. The decoder refuses the larger ones of this shape itself, so nothing that decoded is lost; baseline frames of the same shape decode and are left to it. The check can go once zune-jpeg 0.5.16 ships. Co-Authored-By: Claude Fable 5.1 --- crates/render/src/images.rs | 48 +++++++++++ crates/render/src/jpeg_sampling_tests.rs | 104 +++++++++++++++++++++++ crates/render/src/lib.rs | 2 + 3 files changed, 154 insertions(+) create mode 100644 crates/render/src/jpeg_sampling_tests.rs diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 586c2dac..380b6712 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -100,6 +100,9 @@ fn decode_in(bytes: &[u8], target: ColorProfile) -> Option<(Pixmap, bool)> { /// tall. pub(crate) fn decode_within(bytes: &[u8], target: ColorProfile, max_side: u32, max_bytes: u64) -> Option<(Pixmap, bool)> { use image::ImageDecoder; + if jpeg_finer_than_first(bytes) { + return None; + } let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?; let mut limits = image::Limits::default(); limits.max_alloc = Some(max_bytes); @@ -364,6 +367,51 @@ pub fn apply_filters(p: &mut Pixmap, f: &ImageFilters) { } } +/// Whether `bytes` are a progressive JPEG whose frame samples some component more finely (across +/// or down) than its first. zune-jpeg 0.5.15, the decoder behind `image`, asserts in its upsampler +/// on such a frame (etemesi254/zune-image#236; fixed there by 36007420, unreleased as of +/// 0.5.16-rc3), and in the browser a panic aborts the page. Until the fix ships, such a JPEG is +/// refused from its header: the decoder refuses the larger ones itself, so nothing that decoded is +/// lost. Baseline frames of the same shape decode and are left to it. +pub(crate) fn jpeg_finer_than_first(bytes: &[u8]) -> bool { + let Some((marker, frame)) = jpeg_frame(bytes) else { return false }; + if !matches!(marker, 0xC2 | 0xC6 | 0xCA | 0xCE) { + return false; + } + // Length (2), precision (1), height (2), width (2), components (1), then id, sampling and + // table for each component. + let n = usize::from(frame.get(7).copied().unwrap_or(0)); + let sampling = |k: usize| frame.get(9 + 3 * k).map(|b| (b >> 4, b & 0x0F)); + let Some((h0, v0)) = sampling(0) else { return false }; + (1..n).any(|k| sampling(k).is_some_and(|(h, v)| h > h0 || v > v0)) +} + +/// A JPEG's start-of-frame marker and the segment after it (from its length bytes), found as the +/// decoders find it: fill bytes are skipped and markers without a length stepped over. `None` for +/// anything but a JPEG, or one whose scan, end or bytes come before a frame. +fn jpeg_frame(bytes: &[u8]) -> Option<(u8, &[u8])> { + if !bytes.starts_with(&[0xFF, 0xD8]) { + return None; + } + let mut at = 2; + loop { + if *bytes.get(at)? != 0xFF { + return None; + } + let marker = *bytes.get(at + 1)?; + match marker { + 0xFF => at += 1, + 0x01 | 0xD0..=0xD8 => at += 2, + 0xD9 | 0xDA => return None, + 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return Some((marker, bytes.get(at + 2..)?)), + _ => { + let len = usize::from(u16::from_be_bytes([*bytes.get(at + 2)?, *bytes.get(at + 3)?])); + at = at.checked_add(2)?.checked_add(len)?; + } + } + } +} + /// Read an image's pixel size without keeping the decode. pub fn dimensions(bytes: &[u8]) -> Option<(u32, u32)> { let reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?; diff --git a/crates/render/src/jpeg_sampling_tests.rs b/crates/render/src/jpeg_sampling_tests.rs new file mode 100644 index 00000000..11e21853 --- /dev/null +++ b/crates/render/src/jpeg_sampling_tests.rs @@ -0,0 +1,104 @@ +//! A progressive JPEG whose frame samples a component more finely than its first is refused from +//! its header, not handed to the decoder: zune-jpeg 0.5.15, the decoder behind `image`, asserts +//! in its upsampler on such a frame (etemesi254/zune-image#236), and in the browser a panic aborts +//! the page. Baseline frames of the same shape are decoded (or refused) by the decoder itself. + +use std::io::Cursor; + +use super::*; + +/// An 8 × 8 progressive RGB JPEG with every component sampled 1 × 1, as jpeg-encoder 0.6 writes it +/// at quality 50 (JFIF header, the standard tables, three scans). Its frame header (SOF2) starts at +/// byte 20; the second component's sampling byte is [`SECOND_SAMPLING`], the third's follows 3 on. +const TINY_PROGRESSIVE: &[u8] = &[ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xC2, 0x00, 0x11, + 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x00, 0x11, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x01, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, + 0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, + 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, + 0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11, 0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, + 0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, + 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, + 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, + 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, + 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, + 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xFF, 0xDA, 0x00, 0x08, + 0x01, 0x01, 0x11, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00, + 0x00, 0x01, 0x14, 0x00, 0x4D, 0xB0, 0x97, 0x8E, 0x45, 0x01, 0x08, 0x03, 0x70, 0xAF, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x01, 0x14, 0x00, + 0xAD, 0x17, 0xA5, 0x4A, 0x6C, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x01, 0x14, 0x00, 0x8A, 0x75, 0xA1, 0xCB, 0x6D, 0x62, 0x7F, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x00, 0x00, 0x15, 0x29, 0x00, 0xE0, 0x75, 0x1C, 0x8C, 0x74, 0xAF, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x15, 0x29, 0x00, + 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x15, 0x29, 0x00, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00, 0x00, 0x2A, 0x3F, 0x00, 0xC8, 0x3C, + 0x7E, 0x1F, 0x79, 0x07, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x2A, 0x3F, 0x00, 0xFE, 0x96, 0x8E, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, + 0x11, 0x2A, 0x3F, 0x00, 0xF9, 0x36, 0xFF, 0x00, 0x99, 0xFB, 0xDF, 0xFF, 0xD9, +]; +const SECOND_SAMPLING: usize = 34; + +fn with_second_sampled(hv: u8) -> Vec { + let mut b = TINY_PROGRESSIVE.to_vec(); + b[SECOND_SAMPLING] = hv; + b +} + +#[test] +fn the_jpeg_as_written_is_not_finer_than_its_first_and_decodes() { + assert_eq!(TINY_PROGRESSIVE[SECOND_SAMPLING], 0x11); + assert!(!images::jpeg_finer_than_first(TINY_PROGRESSIVE)); + let p = images::decode(TINY_PROGRESSIVE).expect("decoded"); + assert_eq!((p.width(), p.height()), (8, 8)); + // Chroma sampled coarser than luma (the usual way round) isn't finer either. + assert!(!images::jpeg_finer_than_first(&{ + let mut b = TINY_PROGRESSIVE.to_vec(); + b[SECOND_SAMPLING - 3] = 0x22; + b + })); +} + +/// Without the check this test panics in the decoder (an assertion in zune-jpeg's upsampler). +#[test] +fn a_progressive_jpeg_sampling_a_component_finer_than_its_first_is_refused_from_its_header() { + for hv in [0x22, 0x21, 0x12] { + let b = with_second_sampled(hv); + assert!(images::jpeg_finer_than_first(&b), "second component {hv:#x}"); + assert!(images::decode(&b).is_none(), "second component {hv:#x}"); + } + let mut third = TINY_PROGRESSIVE.to_vec(); + third[SECOND_SAMPLING + 3] = 0x22; + assert!(images::jpeg_finer_than_first(&third)); + assert!(images::decode(&third).is_none()); +} + +#[test] +fn baseline_frames_and_other_files_are_left_to_the_decoder() { + // A baseline JPEG (as `image` writes them) with its second component made finer: the decoder + // takes baseline frames of this shape, so it isn't refused from its header. + let mut out = Vec::new(); + image::RgbImage::from_fn(8, 8, |x, y| image::Rgb([x as u8 * 30, y as u8 * 30, 100])) + .write_to(&mut Cursor::new(&mut out), image::ImageFormat::Jpeg) + .unwrap(); + let sof = out.windows(2).position(|m| m == [0xFF, 0xC0]).expect("a baseline frame"); + out[sof + 14] = 0x22; + assert!(!images::jpeg_finer_than_first(&out)); + let _ = images::decode(&out); + // Not JPEGs, or JPEGs that end before their frame. + let mut png = Vec::new(); + image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255])).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + assert!(!images::jpeg_finer_than_first(&png)); + assert!(!images::jpeg_finer_than_first(&TINY_PROGRESSIVE[..19])); + assert!(!images::jpeg_finer_than_first(&[0xFF, 0xD8])); + assert!(!images::jpeg_finer_than_first(&[])); + assert!(images::decode(&TINY_PROGRESSIVE[..19]).is_none()); +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 24a5e57a..858dc27a 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -492,6 +492,8 @@ mod image_budget_tests; #[cfg(test)] mod image_profile_tests; #[cfg(test)] +mod jpeg_sampling_tests; +#[cfg(test)] mod lod_tests; #[cfg(test)] mod magnified_image_tests; From bdddf9a2baaa95f99cd941b19b1675de3f46a01a Mon Sep 17 00:00:00 2001 From: samhyun Date: Thu, 29 Oct 2026 11:16:17 +0900 Subject: [PATCH 2/2] M8: the baseline fixture of the JPEG sampling test is a well-formed file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the review of #1491: patching a component's sampling in the header of a baseline JPEG `image` wrote leaves its entropy data for 1 × 1 components, so decoding it could only fail and the test proved nothing past the frame not being refused. The fixture is now a baseline CMYK JPEG as jpeg-encoder writes it with 2 × 2 sampling (its black sampled finer than its cyan), and the test asserts it is left to the decoder and decodes. Co-Authored-By: Claude Fable 5.1 --- crates/render/src/jpeg_sampling_tests.rs | 55 +++++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/crates/render/src/jpeg_sampling_tests.rs b/crates/render/src/jpeg_sampling_tests.rs index 11e21853..e001e702 100644 --- a/crates/render/src/jpeg_sampling_tests.rs +++ b/crates/render/src/jpeg_sampling_tests.rs @@ -1,7 +1,7 @@ //! A progressive JPEG whose frame samples a component more finely than its first is refused from //! its header, not handed to the decoder: zune-jpeg 0.5.15, the decoder behind `image`, asserts //! in its upsampler on such a frame (etemesi254/zune-image#236), and in the browser a panic aborts -//! the page. Baseline frames of the same shape are decoded (or refused) by the decoder itself. +//! the page. Baseline frames of the same shape are decoded by the decoder itself and left to it. use std::io::Cursor; @@ -47,6 +47,44 @@ const TINY_PROGRESSIVE: &[u8] = &[ ]; const SECOND_SAMPLING: usize = 34; +/// An 8 × 8 baseline CMYK JPEG as jpeg-encoder 0.6 writes it at quality 50 with 2 × 2 sampling: +/// its black is sampled 2 × 2 against 1 × 1 for the others (SOF0 at byte 36), a well-formed file +/// of the shape the check is about, in a baseline frame. +const TINY_BASELINE_CMYK: &[u8] = &[ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xEE, 0x00, 0x0E, + 0x41, 0x64, 0x6F, 0x62, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x14, 0x08, 0x00, 0x08, 0x00, 0x08, 0x04, 0x00, 0x11, + 0x01, 0x01, 0x11, 0x01, 0x02, 0x11, 0x01, 0x03, 0x22, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 0x0E, 0x0D, + 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, + 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D, 0x71, 0x79, + 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11, 0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, 0x1A, 0x1A, 0x2F, 0x63, 0x42, + 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, + 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, + 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, + 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, + 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, + 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, + 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, + 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, + 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, + 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, + 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, + 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, + 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x0E, 0x04, 0x00, 0x11, 0x01, 0x11, 0x02, 0x11, 0x03, 0x00, 0x00, 0x3F, 0x00, 0xEE, 0xA6, 0xAA, 0x2A, + 0xB3, 0x83, 0x95, 0xED, 0x6D, 0x75, 0xBB, 0xF5, 0xB4, 0x97, 0x6D, 0xB6, 0x5A, 0xD9, 0x2B, 0x8D, 0x39, 0x3A, 0xB3, 0x8F, 0x54, 0xFB, 0xCB, 0xAF, + 0xFD, 0xBD, 0x7E, 0x9D, 0xDA, 0xEC, 0x96, 0xB7, 0xE2, 0x95, 0x47, 0x2A, 0x8E, 0x0B, 0x46, 0xB7, 0x77, 0x7A, 0xDD, 0xBE, 0xCD, 0x3F, 0xBD, 0xBF, + 0x2B, 0x6C, 0x0E, 0xC3, 0x6E, 0xE9, 0xB6, 0xCA, 0xE0, 0x07, 0x1B, 0xC9, 0x21, 0x95, 0x4E, 0x72, 0x54, 0x0C, 0x8E, 0x00, 0x62, 0x00, 0x5C, 0x28, + 0x45, 0xEF, 0x51, 0xFF, 0x00, 0x6B, 0xDA, 0xFF, 0x00, 0x76, 0xC7, 0xFF, 0x00, 0x01, 0x27, 0xA1, 0xFF, 0x00, 0xE3, 0xFB, 0xFE, 0xDD, 0x65, 0xFF, + 0x00, 0xD1, 0x31, 0xD6, 0x35, 0x7F, 0xFF, 0xD9, +]; + fn with_second_sampled(hv: u8) -> Vec { let mut b = TINY_PROGRESSIVE.to_vec(); b[SECOND_SAMPLING] = hv; @@ -83,16 +121,11 @@ fn a_progressive_jpeg_sampling_a_component_finer_than_its_first_is_refused_from_ #[test] fn baseline_frames_and_other_files_are_left_to_the_decoder() { - // A baseline JPEG (as `image` writes them) with its second component made finer: the decoder - // takes baseline frames of this shape, so it isn't refused from its header. - let mut out = Vec::new(); - image::RgbImage::from_fn(8, 8, |x, y| image::Rgb([x as u8 * 30, y as u8 * 30, 100])) - .write_to(&mut Cursor::new(&mut out), image::ImageFormat::Jpeg) - .unwrap(); - let sof = out.windows(2).position(|m| m == [0xFF, 0xC0]).expect("a baseline frame"); - out[sof + 14] = 0x22; - assert!(!images::jpeg_finer_than_first(&out)); - let _ = images::decode(&out); + // A well-formed baseline frame with its black sampled finer than its cyan: the decoder takes + // it, so it isn't refused from its header. + assert!(!images::jpeg_finer_than_first(TINY_BASELINE_CMYK)); + let p = images::decode(TINY_BASELINE_CMYK).expect("decoded"); + assert_eq!((p.width(), p.height()), (8, 8)); // Not JPEGs, or JPEGs that end before their frame. let mut png = Vec::new(); image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255])).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap();