fix(engine): list a page truncated for both forms and pictures once

This commit is contained in:
aiongg
2026-10-22 20:56:02 +00:00
parent 6394779b81
commit aa1c13f71c
2 changed files with 52 additions and 10 deletions
+8 -10
View File
@@ -184,18 +184,18 @@ impl crate::Document {
/// The pages as paragraphs and images (for Word, HTML and RTF export), including what
/// form XObjects draw.
pub fn export_pages(&self) -> Vec<pdfcraft_export::Page> {
self.export_pages_checked(true).0
self.export_pages_checked(true, MAX_EXPORT_IMAGE_BYTES).0
}
/// [`Self::export_pages`], and the pages whose forms were left partly unread or whose
/// pictures went past [`MAX_EXPORT_IMAGE_BYTES`] ([`OfficeExport::truncated_pages`]). Without
/// pictures went past `image_budget` bytes ([`OfficeExport::truncated_pages`]). Without
/// `images` no picture is prepared.
fn export_pages_checked(&self, images: bool) -> (Vec<pdfcraft_export::Page>, Vec<usize>) {
pub(crate) fn export_pages_checked(&self, images: bool, image_budget: usize) -> (Vec<pdfcraft_export::Page>, Vec<usize>) {
let Some(cos) = self.editor.as_ref().map(|e| &e.cos) else { return (Vec::new(), Vec::new()) };
let mut truncated = Vec::new();
// Each image object is encoded once and shared by every place that draws it.
let mut encoded: HashMap<ObjRef, PreparedImage> = HashMap::new();
let mut image_bytes = MAX_EXPORT_IMAGE_BYTES;
let mut image_bytes = image_budget;
let pages = self
.info
.pages
@@ -213,9 +213,7 @@ impl crate::Document {
let display_width = f64::from(if info.rotation % 180 == 0 { info.width } else { info.height });
let unit = if crop_width > 0.0 && crop_width.is_finite() && display_width.is_finite() { display_width / crop_width } else { 1.0 };
let reading = pdfcraft_edit::reading_blocks_checked(cos, i).unwrap_or_default();
if reading.truncated {
truncated.push(i);
}
let forms_unread = reading.truncated;
let blocks = reading
.blocks
.into_iter()
@@ -255,10 +253,10 @@ impl crate::Document {
})
})
.collect();
// The page colour (Edit a PDF ▸ Background, a producer's page fill) for formats that keep one.
if over_budget {
if forms_unread || over_budget {
truncated.push(i);
}
// The page colour (Edit a PDF ▸ Background, a producer's page fill) for formats that keep one.
let background = pdfcraft_edit::page_background(cos, i).ok().flatten();
pdfcraft_export::Page { width: info.width as f64, height: info.height as f64, blocks, images, background }
})
@@ -275,7 +273,7 @@ impl crate::Document {
/// ([`OfficeExport::truncated_pages`]).
pub fn export_office_checked(&self, format: OfficeFormat) -> OfficeExport {
// RTF leaves pictures out.
let (pages, truncated_pages) = self.export_pages_checked(format != OfficeFormat::Rtf);
let (pages, truncated_pages) = self.export_pages_checked(format != OfficeFormat::Rtf, MAX_EXPORT_IMAGE_BYTES);
let title = self.info.title.clone().unwrap_or_else(|| self.name.trim_end_matches(".pdf").to_string());
let bytes = match format {
OfficeFormat::Docx => pdfcraft_export::docx(&pages, &title),
+44
View File
@@ -7283,3 +7283,47 @@ fn exporting_office_keeps_dotted_leader_entries_as_paragraphs() {
let positions: Vec<_> = entries.iter().map(|entry| html.find(&format!("<p>{entry}</p>")).expect("one paragraph per entry")).collect();
assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "reading order: {html}");
}
/// A page whose forms go past the reading limit and whose picture goes past the image budget is
/// listed once among the truncated pages.
#[test]
fn a_page_truncated_for_forms_and_pictures_is_listed_once() {
let depth = compare::MAX_FORM_DEPTH + 1;
let mut objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>".into(),
String::new(),
"<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 4 >>\nstream\n\0\x40\u{80}\u{ff}\nendstream".into(),
"<< /Length 34 >>\nstream\nq 10 0 0 10 0 0 cm /Im Do Q /D1 Do\nendstream".into(),
];
// Forms D1..D{depth}, objects 6 onwards, each drawing the next.
let xobjects: String = (1..=depth).map(|i| format!("/D{i} {} 0 R ", 5 + i)).collect();
let resources = format!("<< /XObject << /Im 4 0 R {xobjects}>> >>");
for i in 1..=depth {
let body = if i == depth { "q Q".to_string() } else { format!("/D{} Do", i + 1) };
objs.push(format!(
"<< /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Resources {resources} /Length {} >>\nstream\n{body}\nendstream",
body.len()
));
}
objs[2] = format!("<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources {resources} >>");
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
bytes.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
let mut s = Session::new();
let id = s.open("both.pdf", None, Arc::new(bytes), None).unwrap();
let doc = s.get(id).unwrap();
assert_eq!(doc.export_pages_checked(false, 0).1, [0], "the forms alone truncate the page");
let (pages, truncated) = doc.export_pages_checked(true, 0);
assert!(pages[0].images.is_empty(), "the picture is past the budget");
assert_eq!(truncated, [0]);
}