From a096ece161de7bfac15b5ba7f7eb13b28fe5f1e4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:13:13 +0900 Subject: [PATCH] M4: drop the aggregate page-label cap from document inspection Document inspection refused every custom label once their total passed 4 MiB, so a valid document with long labels showed physical page numbers and a warning. Labels are now applied to every page. The per-label length limit, the number-tree work, depth and cycle limits and the prefix bounds are unchanged, and labels are still applied only after every range is accepted. MAX_LABEL_TOTAL_BYTES is removed. The inspector keeps one label per page (PageInfo.label), so its label memory grows with the page count times the label length. The page count is bounded by the objects in the file, and each label by 1,024 bytes. Co-Authored-By: Claude Haiku 5.5 --- crates/cos/src/page_labels.rs | 2 -- crates/render/src/inspect.rs | 21 ++++++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/crates/cos/src/page_labels.rs b/crates/cos/src/page_labels.rs index cf037bf1b..613292e44 100644 --- a/crates/cos/src/page_labels.rs +++ b/crates/cos/src/page_labels.rs @@ -5,8 +5,6 @@ pub const MAX_LABEL_BYTES: usize = 1024; /// Raw prefixes are bounded before decoding; this admits a full UTF-16BE ASCII label. pub const MAX_PREFIX_BYTES: usize = 2 * MAX_LABEL_BYTES + 2; -/// Sum of the UTF-8 bytes of custom labels for one document. -pub const MAX_LABEL_TOTAL_BYTES: usize = 4 << 20; /// The most number-tree entries (nodes, kids and range pairs) read from one `/PageLabels` /// tree. Scaled for real documents: one range per page on a ten-thousand-page PDF happens /// (prefix labels; #307's file has 9,156 pages), so the cap is twenty times that while still diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs index 995ec50cc..b7cbcc6f4 100644 --- a/crates/render/src/inspect.rs +++ b/crates/render/src/inspect.rs @@ -14,7 +14,7 @@ use hayro::hayro_syntax::DecryptionError; use hayro::hayro_syntax::LoadPdfError; use hayro::hayro_syntax::Pdf; use lopdf::{Dictionary, Document, LoadOptions, Object, ObjectId}; -use pdfcraft_cos::page_labels::{MAX_LABEL_BYTES, MAX_LABEL_TOTAL_BYTES, MAX_LABEL_TREE_DEPTH, MAX_LABEL_TREE_WORK, MAX_PREFIX_BYTES, alpha, roman}; +use pdfcraft_cos::page_labels::{MAX_LABEL_BYTES, MAX_LABEL_TREE_DEPTH, MAX_LABEL_TREE_WORK, MAX_PREFIX_BYTES, alpha, roman}; use crate::OpenError; use crate::structure::{LazyStructure, Structure}; @@ -761,7 +761,6 @@ impl<'a> Inspector<'a> { specs.insert(start, (first, prefix, style)); } let mut staged = Vec::new(); - let mut total = 0usize; for (i, _) in pages.iter().enumerate() { let Some((&start, (first, prefix, style))) = specs.range(..=i).next_back() else { continue }; let n = if matches!(*style, Some(b"D" | b"R" | b"r" | b"A" | b"a")) { @@ -797,10 +796,6 @@ impl<'a> Inspector<'a> { if label.is_empty() { continue; // Keep the existing physical number for an empty custom label. } - total = total.checked_add(label.len()).ok_or("the total label size overflows")?; - if total > MAX_LABEL_TOTAL_BYTES { - return Err("custom labels exceed 4 MiB in total"); - } staged.push((i, label)); } // A malformed later range must not leave a partially applied custom label sequence. @@ -1453,22 +1448,18 @@ mod tests { } #[test] - fn page_label_limits_bound_total_custom_output_atomically() { + fn page_label_limits_apply_labels_past_four_mib_in_total() { + // 4,097 labels of 1,024 bytes: each within the limit, together past 4 MiB. let doc = Document::with_version("1.7"); let mut tree = Dictionary::new(); tree.set("Nums", vec![Object::Integer(0), label_spec("", vec![b'x'; MAX_LABEL_BYTES], 1)]); let mut catalog = Dictionary::new(); catalog.set("PageLabels", Object::Dictionary(tree)); - let count = MAX_LABEL_TOTAL_BYTES / MAX_LABEL_BYTES + 1; - let mut pages: Vec<_> = (1..=count) + let mut pages: Vec<_> = (1..=4_097) .map(|n| PageInfo { width: 200.0, height: 300.0, crop: [0.0, 0.0, 200.0, 300.0], rotation: 0, label: n.to_string() }) .collect(); - let err = Inspector::new(&doc).page_labels(&catalog, &mut pages).unwrap_err(); - assert!(err.contains("4 MiB"), "{err}"); - assert!(pages.iter().enumerate().all(|(i, p)| p.label == (i + 1).to_string())); - // Exact aggregate boundary remains supported. - pages.pop(); - Inspector::new(&doc).page_labels(&catalog, &mut pages).unwrap(); + let notes = Inspector::new(&doc).page_labels(&catalog, &mut pages).unwrap(); + assert!(notes.is_empty(), "{notes:?}"); assert!(pages.iter().all(|p| p.label.len() == MAX_LABEL_BYTES)); }