From 6417d6b90adbefb89b044d2a41e94222e7decbaf Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 04:49:49 -0500 Subject: [PATCH] Preserve shared format boundary behavior --- crates/affinity/src/model/read.rs | 6 ++- crates/image-codecs/src/allocation.rs | 66 +++++++++++++++++++++++---- crates/raw/src/cr3.rs | 7 +++ crates/raw/src/jxl.rs | 4 ++ crates/raw/src/lib.rs | 10 +++- crates/xml/src/lib.rs | 2 +- 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/crates/affinity/src/model/read.rs b/crates/affinity/src/model/read.rs index c0cf58e77..14b17ad99 100644 --- a/crates/affinity/src/model/read.rs +++ b/crates/affinity/src/model/read.rs @@ -679,7 +679,11 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> { } let class = s.class(id).unwrap_or(Tag::of(b"ShpN")); let (path, even_odd) = crate::geometry::outline(self, id, class, world).unwrap_or_else(|| { - self.warn("an artboard whose outline could not be read (clipped to its rectangle)"); + if class == Tag::of(b"PCrv") { + self.warn("a converted artboard outline could not be read (clipped to its rectangle)"); + } else { + self.warn("an artboard whose outline could not be read (clipped to its rectangle)"); + } (Path { subpaths: vec![crate::shapes::rectangle(b.x0, b.y0, b.x1, b.y1)] }, false) }); let (background, strokes) = paint::node_paint(self, id, world); diff --git a/crates/image-codecs/src/allocation.rs b/crates/image-codecs/src/allocation.rs index 5425a0c24..a82f43f8e 100644 --- a/crates/image-codecs/src/allocation.rs +++ b/crates/image-codecs/src/allocation.rs @@ -1,10 +1,7 @@ //! Predecode bounds for raster containers whose frames may exceed their declared canvas. -use crate::codecs::webp; use crate::error::CodecError; use crate::format::Format; -use crate::image::{ChannelLayout, SampleType}; -use crate::options::Limits; /// The largest width and height any decoded frame can declare, without allocating pixel data. /// Animated GIF and WebP frames are inspected individually, including frames larger than the @@ -12,12 +9,10 @@ use crate::options::Limits; pub fn probe_allocation_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> { let format = crate::detect(bytes).ok_or(CodecError::UnknownFormat)?; let (width, height) = match format { + Format::Png => png_bounds(bytes)?, + Format::Jpeg => jpeg_bounds(bytes)?, Format::Gif => gif_bounds(bytes)?, - Format::WebP => { - let (width, height, _) = webp::probe(bytes, &Limits::default())?; - let (frame_width, frame_height) = webp_bounds(bytes)?; - (width.max(frame_width), height.max(frame_height)) - } + Format::WebP => webp_bounds(bytes)?, _ => { if let Some(info) = crate::inspect_raster(bytes)? { (info.width, info.height) @@ -26,7 +21,9 @@ pub fn probe_allocation_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> { } } }; - Limits::default().check(width, height, ChannelLayout::Rgba, SampleType::U8)?; + // This is an allocation-size probe, not a decode policy. Callers need the coded extents even + // when they exceed this crate's default decode limits so they can report/refuse against their + // own budgets before passing the bytes to a decoder. Ok((width, height)) } @@ -34,6 +31,57 @@ fn malformed(format: Format, message: &'static str) -> CodecError { CodecError::malformed(format, message) } +fn png_bounds(bytes: &[u8]) -> Result<(u32, u32), CodecError> { + let bad = || malformed(Format::Png, "truncated or malformed PNG header"); + if bytes.get(..8) != Some(b"\x89PNG\r\n\x1a\n") || bytes.get(8..16) != Some(b"\0\0\0\rIHDR") { + return Err(bad()); + } + let width = u32::from_be_bytes(bytes.get(16..20).ok_or_else(bad)?.try_into().map_err(|_| bad())?); + let height = u32::from_be_bytes(bytes.get(20..24).ok_or_else(bad)?.try_into().map_err(|_| bad())?); + if width == 0 || height == 0 { + return Err(bad()); + } + Ok((width, height)) +} + +fn jpeg_bounds(bytes: &[u8]) -> Result<(u32, u32), CodecError> { + let bad = || malformed(Format::Jpeg, "truncated or malformed JPEG frame header"); + if bytes.get(..2) != Some(&[0xff, 0xd8]) { + return Err(bad()); + } + let mut at = 2usize; + while at < bytes.len() { + while bytes.get(at) == Some(&0xff) { + at += 1; + } + let marker = *bytes.get(at).ok_or_else(bad)?; + at += 1; + if matches!(marker, 0xd8 | 0xd9 | 0x01 | 0xd0..=0xd7) { + continue; + } + let length = + usize::from(u16::from_be_bytes(bytes.get(at..at.checked_add(2).ok_or_else(bad)?).ok_or_else(bad)?.try_into().map_err(|_| bad())?)); + if length < 2 { + return Err(bad()); + } + let end = at.checked_add(length).ok_or_else(bad)?; + let segment = bytes.get(at..end).ok_or_else(bad)?; + if matches!(marker, 0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf) { + if segment.len() < 7 { + return Err(bad()); + } + let height = u32::from(u16::from_be_bytes([segment[3], segment[4]])); + let width = u32::from(u16::from_be_bytes([segment[5], segment[6]])); + if width == 0 || height == 0 { + return Err(bad()); + } + return Ok((width, height)); + } + at = end; + } + Err(bad()) +} + fn u16_le(bytes: &[u8], at: usize) -> Option { Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?)) } diff --git a/crates/raw/src/cr3.rs b/crates/raw/src/cr3.rs index 60fe292c8..956446c6e 100644 --- a/crates/raw/src/cr3.rs +++ b/crates/raw/src/cr3.rs @@ -184,6 +184,13 @@ fn black_at_active(levels: [f32; 4], pattern: u8, active: Rect) -> BlackLevel { BlackLevel { repeat_rows: 2, repeat_cols: 2, values, delta_h: vec![], delta_v: vec![] } } +/// Orientation metadata usable for embedded-preview fallback even when the raw track is unsupported. +pub(crate) fn orientation(bytes: &[u8]) -> Option { + let container = parse_cr3(bytes)?; + let cmt1 = container.cmt.first().and_then(|b| *b).and_then(|b| Tiff::parse(b).ok())?; + Some(cmt1.ifds.first()?.u16(craft_tiff::tags::ORIENTATION).unwrap_or(1)) +} + pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result { let container = parse_cr3(bytes).ok_or(RawError::NotRaw)?; let track = main_track(&container.tracks).ok_or_else(|| RawError::Unsupported("CR3 without a Bayer raw track".into()))?; diff --git a/crates/raw/src/jxl.rs b/crates/raw/src/jxl.rs index 128f4eeea..e0458fb68 100644 --- a/crates/raw/src/jxl.rs +++ b/crates/raw/src/jxl.rs @@ -238,6 +238,10 @@ fn open(src: &[u8], cw: usize, ch: usize, vw: usize, vh: usize, cpp: usize, bits } let max = match header.metadata.bit_depth { BitDepth::IntegerSample { bits_per_sample } if !float && (1..=16).contains(&bits_per_sample) => ((1u32 << bits_per_sample) - 1) as f32, + // Some DNG writers label LinearRaw samples as floating point while storing a lossless + // integer JPEG XL codestream. JXL exposes decoded pixels as normalized f32 in either + // case, so retain them as floats instead of rejecting the otherwise valid IFD/tile pair. + BitDepth::IntegerSample { bits_per_sample: 1..=16 } if float => 0.0, BitDepth::FloatSample { .. } if float => 0.0, d => return Err(RawError::Corrupt(format!("JPEG XL tile sample type {d:?} does not match the IFD"))), }; diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index c9c072122..dbcf6f953 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -638,7 +638,15 @@ pub fn probe_info(bytes: &[u8]) -> Result { /// Orientation of a supported raw, read through the same format parser as [`probe_info`]. pub fn orientation(bytes: &[u8]) -> Option { - probe_info(bytes).ok().map(|info| info.orientation.to_exif()) + if let Ok(info) = probe_info(bytes) { + return Some(info.orientation.to_exif()); + } + // A damaged/unsupported pixel IFD must not hide usable orientation metadata from preview + // fallback. CR3 keeps IFD0 in its CMT1 box; TIFF-based raws keep it at the file start. + if probe(bytes) == Some(RawFormat::Cr3) { + return cr3::orientation(bytes); + } + craft_tiff::Tiff::parse(bytes).ok()?.ifds.first()?.u16(craft_tiff::tags::ORIENTATION).or(Some(1)) } /// [`probe_info`] within `limits`: a file that declares more than they allow fails as [`decode_with`] would. diff --git a/crates/xml/src/lib.rs b/crates/xml/src/lib.rs index b953690bf..87057b387 100644 --- a/crates/xml/src/lib.rs +++ b/crates/xml/src/lib.rs @@ -44,7 +44,7 @@ pub enum Error { #[error("the XML is larger than {0} bytes")] TooLarge(usize), /// Elements nest deeper than [`Limits::max_depth`]. - #[error("XML elements nest deeper than {0} levels")] + #[error("XML elements are nested deeper than {0} levels")] TooDeep(usize), /// The tree would hold more than [`Limits::max_nodes`] nodes. #[error("the XML holds more than {0} nodes")]