mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Keep filtered inline images whole when their data contains EI
Fixes #3579 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
6fb6b7cf6f
commit
07f3c97491
@@ -225,7 +225,7 @@ fn inline_image(data: &[u8], pos: usize) -> Result<(Dict, Vec<u8>, usize), usize
|
||||
}
|
||||
// One whitespace byte separates ID from the data.
|
||||
let begin = (lx.pos + 1).min(data.len());
|
||||
let end = unfiltered_image_end(data, begin, &dict).or_else(|| end_of_image(data, begin));
|
||||
let end = unfiltered_image_end(data, begin, &dict).or_else(|| declared_image_end(data, begin, &dict)).or_else(|| image_data_end(data, begin));
|
||||
Ok(match end {
|
||||
Some((end_data, end)) => (dict, data[begin..end_data].to_vec(), end),
|
||||
None => (dict, data[begin..].to_vec(), data.len()),
|
||||
@@ -302,6 +302,42 @@ fn unfiltered_image_end(data: &[u8], begin: usize, dict: &Dict) -> Option<(usize
|
||||
if data.get(ei..end) == Some(b"EI") && data.get(end).is_none_or(|b| is_ws(*b) || is_delim(*b)) { Some((end_data, end)) } else { None }
|
||||
}
|
||||
|
||||
/// The length declared by `/L` (or `/Length`, ISO 32000-2 §8.9.7), used when `EI` follows the
|
||||
/// declared bytes after optional whitespace. A declared length without that `EI` is ignored.
|
||||
fn declared_image_end(data: &[u8], begin: usize, dict: &Dict) -> Option<(usize, usize)> {
|
||||
let length = dict.get(b"L").or_else(|| dict.get(b"Length"))?.as_int()?;
|
||||
let end_data = begin.checked_add(usize::try_from(length).ok()?)?;
|
||||
let after = data.get(end_data..)?;
|
||||
let ei = end_data.checked_add(after.iter().position(|b| !is_ws(*b))?)?;
|
||||
let end = ei.checked_add(2)?;
|
||||
if data.get(ei..end) == Some(b"EI") && data.get(end).is_none_or(|b| is_ws(*b) || is_delim(*b)) { Some((end_data, end)) } else { None }
|
||||
}
|
||||
|
||||
/// Where inline image data of unknown length ends. A candidate `EI` followed by binary bytes before
|
||||
/// the next `EI` lies inside the (compressed) data, unless another inline image starts first.
|
||||
fn image_data_end(data: &[u8], begin: usize) -> Option<(usize, usize)> {
|
||||
let mut candidate = end_of_image(data, begin)?;
|
||||
loop {
|
||||
let (end_data, end) = candidate;
|
||||
let Some(next) = end_of_image(data, end) else {
|
||||
return Some(candidate);
|
||||
};
|
||||
let between = data.get(end..next.0).unwrap_or_default();
|
||||
if !between.iter().any(|b| !b.is_ascii()) || starts_inline_image(between) {
|
||||
return Some((end_data, end));
|
||||
}
|
||||
candidate = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `bytes` hold a whitespace-delimited `BI` operator.
|
||||
fn starts_inline_image(bytes: &[u8]) -> bool {
|
||||
bytes
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.any(|(i, pair)| pair == b"BI" && (i == 0 || is_ws(bytes[i - 1])) && bytes.get(i + 2).is_none_or(|b| is_ws(*b) || is_delim(*b)))
|
||||
}
|
||||
|
||||
/// Where inline image data starting at `begin` ends: (end of the data, position after `EI`). The
|
||||
/// data ends at whitespace + `EI` + (whitespace, a delimiter or the end).
|
||||
fn end_of_image(data: &[u8], begin: usize) -> Option<(usize, usize)> {
|
||||
|
||||
@@ -151,6 +151,32 @@ fn filtered_unknown_and_damaged_inline_images_keep_the_existing_fallback() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A zlib stream (one stored block) for 64 DeviceGray samples whose bytes hold " EI\n" at offset 41,
|
||||
/// the way a stored deflate block copies incompressible samples verbatim (#3579).
|
||||
const FLATE_WITH_EI: &str = "789c014000bfffed746159d50788f7ea604984cd08e00b4c8983207f56fe82c77ef8dfb8b9a3a586382045490a3120\
|
||||
31206c2025630aaa4353e6813e233b96539c18bc3a2c5009e9d51b81";
|
||||
|
||||
#[test]
|
||||
fn flate_inline_image_is_not_cut_at_an_ei_inside_its_compressed_bytes() {
|
||||
let flate: Vec<u8> = (0..FLATE_WITH_EI.len()).step_by(2).map(|i| u8::from_str_radix(&FLATE_WITH_EI[i..i + 2], 16).unwrap()).collect();
|
||||
assert_eq!(flate.len(), 75);
|
||||
assert_eq!(&flate[41..45], b" EI\n");
|
||||
for parameters in [b"/W 64 /H 1 /BPC 8 /CS /G /F /Fl".as_slice(), b"/W 64 /H 1 /BPC 8 /CS /G /F /Fl /L 75"] {
|
||||
let mut src = b"q 200 0 0 200 100 300 cm BI ".to_vec();
|
||||
src.extend_from_slice(parameters);
|
||||
src.extend_from_slice(b" ID ");
|
||||
src.extend_from_slice(&flate);
|
||||
src.extend_from_slice(b"\nEI Q BT /F1 24 Tf 72 200 Td (AFTER) Tj ET");
|
||||
let parsed = parse(&src);
|
||||
let label = String::from_utf8_lossy(parameters);
|
||||
assert_eq!(parsed.ops[2].inline.as_ref().unwrap().1.len(), 75, "{label}");
|
||||
let names: Vec<&[u8]> = parsed.ops.iter().map(|o| o.op.as_slice()).collect();
|
||||
assert_eq!(names, [&b"q"[..], b"cm", b"BI", b"Q", b"BT", b"Tf", b"Td", b"Tj", b"ET"], "{label}");
|
||||
assert_eq!(parsed.skipped, 0, "{label}");
|
||||
assert_eq!(parse(&serialize_ops(&parsed.ops)).ops[2].inline, parsed.ops[2].inline, "{label}");
|
||||
}
|
||||
}
|
||||
|
||||
fn inline_pdf(content: &[u8], object_streams: bool) -> Vec<u8> {
|
||||
use pdfcraft_cos::{Document, SaveOptions, Stream, write_full};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user