diff --git a/crates/render/src/raster/tests.rs b/crates/render/src/raster/tests.rs
index 4c22065196..5677328deb 100644
--- a/crates/render/src/raster/tests.rs
+++ b/crates/render/src/raster/tests.rs
@@ -9175,19 +9175,37 @@ fn a_complete_update_reads_as_before() {
assert_eq!((count, size, model), (2, (300, 150), 2));
}
-/// A file with data after `%%EOF` that holds no object (a web page glued to a served file) is read
-/// through its table as before, by the renderer and the model alike: the file has a stale copy of
-/// its page that only a rebuild would show.
+/// A file with data after `%%EOF` that holds no object header (a web page glued to a served file)
+/// is read through its table by the renderer, as before: the file has a stale copy of its page that
+/// only a rebuild would show. The model, finding no `startxref` in its footer window, rebuilds from
+/// object headers and takes that stale 999 x 999 copy, so here the two still differ, as they did
+/// before this change.
#[test]
fn trailing_data_without_objects_reads_as_before() {
let mut junk = one_page_file_with(true);
- junk.extend_from_slice(b"
1 0 objection: 12 0 endobj, 3 0obj, 4 0 obj_name
");
+ junk.extend_from_slice(b"1 0 objection: 12 0 endobj, 3 0obj, 4 0 obj_name, 4294967296 0 obj
");
junk.extend(std::iter::repeat_n(b' ', 5000));
junk.extend_from_slice(b"\n");
+ let doc = pdfcraft_cos::Document::open(Arc::new(junk.clone())).unwrap();
+ let catalog = doc.get(doc.root().unwrap());
+ let pages = doc.resolve(catalog.as_dict().unwrap().get(b"Pages").unwrap());
+ let kid = doc.resolve(pages.as_dict().unwrap().get(b"Kids").unwrap().as_array().unwrap().first().unwrap());
+ let media = kid.as_dict().unwrap().get(b"MediaBox").unwrap().as_array().unwrap().iter().filter_map(|x| x.as_f64()).collect::>();
+ assert_eq!(media, [0.0, 0.0, 999.0, 999.0], "the model's rebuilt page");
let ((count, size), model) = renderer_and_model_pages(junk);
assert_eq!((count, size, model), (1, (200, 100), 1));
}
+/// A cut-off update whose object headers run straight into their dictionaries (`4 0 obj<<`) is
+/// still found: a delimiter ends the keyword (ISO 32000-2 §7.2.2), and the renderer shows the
+/// update's new first page.
+#[test]
+fn a_cut_off_update_with_obj_followed_by_a_delimiter_is_rebuilt() {
+ let file = String::from_utf8(with_update(&one_page_file(), 5000, false)).unwrap().replace(" obj\n<<", " obj<<").into_bytes();
+ let ((count, size), model) = renderer_and_model_pages(file);
+ assert_eq!((count, size, model), (2, (300, 150), 2));
+}
+
/// A cut-off update that ends within the footer window leaves the older `startxref` near the end,
/// which the model reads too: both keep the older revision there.
#[test]
diff --git a/vendor/hayro-syntax/src/xref.rs b/vendor/hayro-syntax/src/xref.rs
index 92d69ca5d3..c70cc554b4 100644
--- a/vendor/hayro-syntax/src/xref.rs
+++ b/vendor/hayro-syntax/src/xref.rs
@@ -918,8 +918,9 @@ pub(crate) fn cut_off_update_follows(data: &[u8]) -> bool {
/// PdfCraft patch: (hayro-syntax (43)) whether `data` holds an `N G obj` header: two runs of
/// digits and the keyword, separated by white space, at the start of the data or after white
/// space or `>`, the keyword ended by white space, a delimiter or the end of the data (ISO
-/// 32000-2 §7.2.2), as the document model's repair scan reads it: `1 0 obj_name` is no header.
-/// One linear pass, no allocation.
+/// 32000-2 §7.2.2), as the document model's repair scan reads it: `1 0 obj_name` is no header,
+/// and neither is one whose object number doesn't fit 32 bits (`4294967296 0 obj`), which that
+/// scan skips too. One linear pass, no allocation.
fn has_object_header(data: &[u8]) -> bool {
let digits = |from: usize| {
data.get(from..).map_or(0, |rest| {
@@ -951,7 +952,13 @@ fn has_object_header(data: &[u8]) -> bool {
at += g;
let s2 = spaces(at);
at += s2;
- if s1 > 0
+ // The object number as the model reads it, a 32-bit unsigned integer.
+ let number_fits = data
+ .get(i..i + n)
+ .and_then(|d| core::str::from_utf8(d).ok())
+ .is_some_and(|d| d.parse::().is_ok());
+ if number_fits
+ && s1 > 0
&& g > 0
&& s2 > 0
&& data.get(at..).is_some_and(|rest| rest.starts_with(b"obj"))