Preserve shared format boundary behavior

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 04:49:49 -05:00
1 parent 65a8069d32
commit 6417d6b90a
6 files changed
+83 -12

No files matched your search

+5 -1
View File
@@ -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);
+57 -9
View File
@@ -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<u16> {
Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?))
}
+7
View File
@@ -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<u16> {
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<RawImage> {
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()))?;
+4
View File
@@ -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"))),
};
+9 -1
View File
@@ -638,7 +638,15 @@ pub fn probe_info(bytes: &[u8]) -> Result<RawInfo> {
/// Orientation of a supported raw, read through the same format parser as [`probe_info`].
pub fn orientation(bytes: &[u8]) -> Option<u16> {
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.
+1 -1
View File
@@ -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")]