mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:10:09 -06:00
M1: a stream with no endstream after it is the bytes its /Length gives (hayro-syntax patch 36)
ISO 32000-2 7.3.8.2: Length "indicates how many bytes of the PDF file are used for the stream's data". cos and hayro-syntax used /Length only when `endstream` followed it, and otherwise searched for the keyword; with none left in the file the stream was an error and its object null. A producer that writes `endstrea` after every stream lost every font and every page's content, and its pages drew blank with no warning. With no `endstream` left, a /Length the file holds is now the stream's extent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
419b267ee5
commit
bfaa20db23
@@ -502,14 +502,19 @@ fn parse_indirect_in(
|
||||
let mut after = Lexer::new(data, end);
|
||||
after.eat_keyword(b"endstream")
|
||||
};
|
||||
// A /Length that fits in the file, for a stream whose `endstream` is missing or misspelt.
|
||||
let fits = declared.and_then(|l| usize::try_from(l).ok()).filter(|l| start.checked_add(*l).is_some_and(|end| end <= data.len()));
|
||||
let len = match declared {
|
||||
Some(l) if valid(l) => l as usize,
|
||||
// Wrong or missing /Length: search for "endstream" (§7.3.8.1 recovery).
|
||||
_ => {
|
||||
find_endstream(data, start, endstreams)
|
||||
.ok_or_else(|| CosError::Syntax { offset: start, detail: "stream without endstream".into() })?
|
||||
- start
|
||||
}
|
||||
// Wrong or missing /Length: search for "endstream" (§7.3.8.1 recovery). With none
|
||||
// left in the file, a /Length that fits is the stream's extent, "how many bytes of the
|
||||
// PDF file are used for the stream's data" (ISO 32000-2 §7.3.8.2), as other readers
|
||||
// take it: a producer that wrote `endstrea` after every stream lost every font and
|
||||
// every page's content.
|
||||
_ => match find_endstream(data, start, endstreams) {
|
||||
Some(end) => end - start,
|
||||
None => fits.ok_or_else(|| CosError::Syntax { offset: start, detail: "stream without endstream".into() })?,
|
||||
},
|
||||
};
|
||||
let range = start..start + len;
|
||||
let raw = match shared {
|
||||
@@ -660,6 +665,24 @@ mod tests {
|
||||
assert_eq!(s.dict.int(b"Length"), Some(5));
|
||||
}
|
||||
|
||||
/// With no `endstream` left in the file, as a producer that writes `endstrea` leaves every
|
||||
/// stream, a stream is the bytes its /Length gives, "how many bytes of the PDF file are used
|
||||
/// for the stream's data" (ISO 32000-2 §7.3.8.2): it was an error, and the object null, so
|
||||
/// such a file lost every font and page content. A /Length past the file, or none, is still
|
||||
/// no stream.
|
||||
#[test]
|
||||
fn indirect_stream_without_endstream_is_its_length() {
|
||||
for end in [&b"\r\nendstrea\rendobj\r\n"[..], b"\nendobj\n", b""] {
|
||||
let mut data = b"7 0 obj\n<< /Length 5 >>\nstream\r\nHELLO".to_vec();
|
||||
data.extend_from_slice(end);
|
||||
let (_, o) = parse_indirect(&data, 0, &|_| None).unwrap();
|
||||
let Object::Stream(s) = o else { panic!("stream expected") };
|
||||
assert_eq!(&s.raw[..], b"HELLO", "{end:?}");
|
||||
}
|
||||
assert!(parse_indirect(b"7 0 obj\n<< /Length 6 >>\nstream\nHELLO", 0, &|_| None).is_err(), "past the file");
|
||||
assert!(parse_indirect(b"7 0 obj\n<< >>\nstream\nHELLO\nendstrea\nendobj\n", 0, &|_| None).is_err(), "no /Length");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indirect_stream_with_a_space_before_the_line_break() {
|
||||
// `stream \r\n` as Microsoft Reporting Services writes it: the data starts after the
|
||||
|
||||
@@ -9622,3 +9622,39 @@ fn a_stream_with_a_crypt_filter_of_its_own_is_drawn() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream with no `endstream` after it, as a producer that writes `endstrea` after every stream
|
||||
/// leaves it, is the bytes its `/Length` gives: "how many bytes of the PDF file are used for the
|
||||
/// stream's data" (ISO 32000-2 7.3.8.2). Nothing found its end, so the page's content was lost and
|
||||
/// the page drew blank, while other readers draw it. Vendored hayro-syntax patch 36.
|
||||
#[test]
|
||||
fn a_content_stream_without_endstream_is_drawn_to_its_length() {
|
||||
use super::*;
|
||||
let content = "1 0 0 rg 0 0 200 200 re f";
|
||||
let centre = |end: &str| -> [u8; 4] {
|
||||
let page = format!(
|
||||
"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\n{end}\nendobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
);
|
||||
let page = PageRenderer::new(Arc::new(page.into_bytes()), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
assert_eq!(centre("endstream"), [255, 0, 0, 255]);
|
||||
for end in ["endstrea", "", "end"] {
|
||||
assert_eq!(centre(end), [255, 0, 0, 255], "{end:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+21
@@ -322,6 +322,10 @@ impl<'a> Readable<'a> for Stream<'a> {
|
||||
r.jump(offset);
|
||||
parse_fallback(r, &dict)
|
||||
})
|
||||
.or_else(|| {
|
||||
r.jump(offset);
|
||||
parse_declared(r, &dict)
|
||||
})
|
||||
.error_none("was unable to manually parse the stream")
|
||||
}
|
||||
}
|
||||
@@ -431,6 +435,23 @@ fn parse_fallback<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option<Stream<'a>>
|
||||
Some(Stream::new(data, dict.clone()))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (36): a stream with no `endstream` after it, as a producer that writes
|
||||
/// `endstrea` leaves every stream, is the bytes its `/Length` gives, "how many bytes of the PDF
|
||||
/// file are used for the stream's data" (ISO 32000-2 7.3.8.2), as other readers take it. Neither
|
||||
/// parse above found it, so every font and page content of such a file was lost and its pages
|
||||
/// drew blank. Called only after [`parse_fallback`] found no `endstream` in the rest of the data,
|
||||
/// so it doesn't search again, and only a `/Length` the data holds is read.
|
||||
fn parse_declared<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option<Stream<'a>> {
|
||||
let length = dict.get::<u32>(LENGTH)?;
|
||||
|
||||
r.skip_white_spaces_and_comments();
|
||||
r.forward_tag(b"stream")?;
|
||||
forward_stream_eol(r)?;
|
||||
let data = r.read_bytes(length as usize)?;
|
||||
|
||||
Some(Stream::new(data, dict.clone()))
|
||||
}
|
||||
|
||||
fn trim_trailing_ascii_whitespace(data: &[u8]) -> usize {
|
||||
let mut end = data.len();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user