nigig-org/crates/apps/pdf/pdf-document/tests/page_ops_roundtrip.rs
andodeki e0aab74452
Some checks failed
email.yml / feat(pdf): page operations — insert, reorder, duplicate, delete, import (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
feat(pdf): page operations — insert, reorder, duplicate, delete, import
Phase 5's page management, matching dart-pdf's `page_ops_test.dart`,
`page_index_map_test.dart` and `import_source_test.dart`.

A reader sees "page 3"; the file holds a tree of /Pages nodes with /Kids
and /Count and /Parent back-pointers, any of which can be left stale. So
the writer **flattens to a single level**: a one-level /Pages node with
every page as a direct kid is valid, is what most producers emit, and
removes the entire class of bug where an intermediate node's /Count no
longer matches what is under it. Preserving an arbitrary tree shape
through arbitrary reordering is far more code for nothing a reader can
see.

`PagePlan` accumulates operations and applies them together, so
intermediate states never have to be valid — delete page 0 and insert a
new one at 0 without the document momentarily having no first page.
`PageIndexMap` reports where every page went, which is the only way to
fix an outline entry, named destination or link annotation afterwards.

What each operation carries matters and differs:

- Reorder and delete rewrite only the kid array, so page objects and
  their resources are untouched.
- Duplicate writes a new page dictionary that **shares** the original's
  resource references. Two pages naming one font object is normal;
  deep-copying would double the file and change nothing visible.
- Import must deep-copy the page and everything it reaches, renumbered,
  because source object numbers mean nothing in the destination. /Parent
  is deliberately not followed — it leads back to the source's page tree
  and from there to every other page in that file.

Inheritable attributes are resolved *before* a page is imported.
/Resources, /MediaBox, /CropBox and /Rotate may live on an ancestor
(Table 30) that is not coming with it, so a page imported without them
renders at the wrong size with no fonts, and nothing reports an error.

**Round-trip tested through the saved file**, which is Phase 5's exit
criterion: 20 tests that save, re-parse, and assert on what a reader
actually gets. Pages are identified by /MediaBox width rather than object
number, because object numbers are exactly what a page-tree bug
scrambles.

Mutation testing changed two things. Seven defects injected:

  /Count left stale                 1 fail
  /Count omitted entirely           1 fail
  imported /Parent not rewritten    1 fail
  inherited attributes not resolved 1 fail
  import does not deep-copy         3 fail
  duplicate loses /Contents         1 fail
  re-parenting skipped              1 fail

The last two only fail because of tests the mutations forced:

- **A stale /Count passed everything.** Our own parser walks /Kids and
  never reads /Count, so it cannot see the disagreement — but other
  readers trust /Count, and a document where the two differ opens with a
  different page count in different viewers. The test now reads the raw
  page-tree node instead of asking the document.
- **Re-parenting could be deleted with every test still green**, because
  the flat fixture's pages already parent to the root. Added a nested
  fixture with an intermediate /Pages node supplying an inherited
  /MediaBox — the case where leaving /Parent stale means a page keeps
  inheriting from a node it is no longer under.

Externally verified: a generated sample with pages swapped and duplicated
passes `qpdf --check` with no warnings, and poppler reads 4 pages with
the reordering visible in extracted text.

Engine suite 1039 -> 1075.

Remaining in Phase 5: flatten, object compaction, redaction, and outline
and struct-tree editing.
2026-08-17 10:38:55 +00:00

643 lines
23 KiB
Rust

//! Page operations, read back from the saved file.
//!
//! Phase 5's exit criterion is "edit-anything round-trip tests (modify →
//! save → re-parse → verify)". The unit tests in `page_ops.rs` check the
//! *plan*; these check the **file**, which is the only thing a reader ever
//! sees.
//!
//! The distinction matters more here than almost anywhere else in the
//! stack. A page-tree edit that is subtly wrong produces a document that
//! opens: `/Count` says five, the reader finds four, and which of those it
//! believes depends on the reader. Nothing errors. So every assertion here
//! goes through `PdfDocument::parse` on the saved bytes and checks what a
//! reader would actually get.
use nigig_pdf_cos::object::{PdfDict, PdfObj};
use nigig_pdf_document::page_ops::{apply_plan, import_pages, PageOpError, PagePlan};
use nigig_pdf_document::PdfDocument;
/// Build a document whose pages are individually identifiable.
///
/// Each page's `/MediaBox` width encodes its index — page `i` is
/// `100 + i` wide — so a reordering can be verified by reading sizes back
/// rather than by trusting object numbers. Object numbers are exactly what
/// a page-tree bug scrambles, so they cannot be the identity used to check
/// one.
fn document_with_pages(count: usize) -> Vec<u8> {
let mut out = String::from("%PDF-1.7\n");
let mut offsets = Vec::new();
let page_obj = |i: usize| 3 + i;
let content_obj = |i: usize, count: usize| 3 + count + i;
offsets.push(out.len());
out.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
offsets.push(out.len());
let kids: Vec<String> = (0..count).map(|i| format!("{} 0 R", page_obj(i))).collect();
out.push_str(&format!(
"2 0 obj\n<< /Type /Pages /Count {count} /Kids [{}] >>\nendobj\n",
kids.join(" ")
));
for i in 0..count {
offsets.push(out.len());
out.push_str(&format!(
"{} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} 200] \
/Contents {} 0 R >>\nendobj\n",
page_obj(i),
100 + i,
content_obj(i, count)
));
}
for i in 0..count {
offsets.push(out.len());
let content = format!("BT (page {i}) Tj ET");
out.push_str(&format!(
"{} 0 obj\n<< /Length {} >>\nstream\n{content}\nendstream\nendobj\n",
content_obj(i, count),
content.len()
));
}
let startxref = out.len();
let total = 1 + 2 + count * 2;
out.push_str(&format!("xref\n0 {total}\n0000000000 65535 f \n"));
for off in &offsets {
out.push_str(&format!("{off:010} 00000 n \n"));
}
out.push_str(&format!(
"trailer\n<< /Size {total} /Root 1 0 R >>\nstartxref\n{startxref}\n%%EOF\n"
));
out.into_bytes()
}
/// The `/MediaBox` widths of every page, in order — the page identities.
fn page_widths(bytes: &[u8]) -> Vec<i64> {
let mut doc = PdfDocument::parse(bytes).expect("the saved file must parse");
(0..doc.page_count())
.map(|i| {
let page = doc.page(i).unwrap_or_else(|e| panic!("page {i}: {e}"));
page.media_box[2] as i64
})
.collect()
}
fn parse(bytes: &[u8]) -> PdfDocument<'_> {
PdfDocument::parse(bytes).expect("parses")
}
#[test]
fn the_fixture_itself_reads_back_correctly() {
// If this fails, every other test in the file is meaningless.
let bytes = document_with_pages(4);
assert_eq!(page_widths(&bytes), vec![100, 101, 102, 103]);
}
#[test]
fn reordering_pages_survives_a_save_and_reparse() {
let bytes = document_with_pages(4);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.set_order(&[3, 1, 0, 2]).expect("reorders");
let (saved, report) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(report.final_page_count, 4);
assert_eq!(
page_widths(&saved),
vec![103, 101, 100, 102],
"the saved page order does not match the plan"
);
}
#[test]
fn moving_one_page_survives_a_save_and_reparse() {
let bytes = document_with_pages(3);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.move_page(0, 2).expect("moves");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(page_widths(&saved), vec![101, 102, 100]);
}
#[test]
fn deleting_a_page_survives_a_save_and_reparse() {
let bytes = document_with_pages(4);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.delete(1).expect("deletes");
let (saved, report) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(report.final_page_count, 3);
assert_eq!(page_widths(&saved), vec![100, 102, 103]);
// The reader's own count must agree with the kid array. A stale
// /Count is the classic page-tree bug: readers disagree about how many
// pages the document has and none of them errors.
let saved_doc = parse(&saved);
assert_eq!(saved_doc.page_count(), 3, "/Count disagrees with /Kids");
}
#[test]
fn duplicating_a_page_produces_two_readable_copies() {
let bytes = document_with_pages(2);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.duplicate(0).expect("duplicates");
let (saved, report) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(report.final_page_count, 3);
assert_eq!(
page_widths(&saved),
vec![100, 100, 101],
"the copy must sit directly after the original"
);
// Both copies must reach their content. A duplicate that shares the
// original's /Contents reference is correct; one that lost it renders
// blank, and blank is a valid page.
let mut saved_doc = parse(&saved);
for i in 0..2 {
let page = saved_doc.page(i).expect("page");
assert!(
!page.content_data.is_empty(),
"copy {i} has no content stream"
);
}
}
#[test]
fn inserting_a_new_page_survives_a_save_and_reparse() {
let bytes = document_with_pages(2);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
let mut page = PdfDict::new();
page.set(
"MediaBox",
PdfObj::Array(vec![
PdfObj::Int(0),
PdfObj::Int(0),
PdfObj::Int(555),
PdfObj::Int(200),
]),
);
plan.insert_new(1, page).expect("inserts");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(page_widths(&saved), vec![100, 555, 101]);
}
/// Every page must reach its own content after a reorder — not the
/// content of whichever page used to be at that index.
///
/// This is the assertion that catches a page tree rebuilt with the right
/// *shape* and the wrong *references*. Page order alone cannot see it:
/// the sizes come from the page dictionaries, which move together with
/// them; the content is a separate object and can be left behind.
#[test]
fn each_page_keeps_its_own_content_after_a_reorder() {
let bytes = document_with_pages(4);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.set_order(&[2, 0, 3, 1]).expect("reorders");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
let mut saved_doc = parse(&saved);
let expected = ["page 2", "page 0", "page 3", "page 1"];
for (i, want) in expected.iter().enumerate() {
let page = saved_doc.page(i).expect("page");
let text = String::from_utf8_lossy(&page.content_data).to_string();
assert!(
text.contains(want),
"page {i} should show {want:?} but its content is {text:?}"
);
}
}
/// Every page's `/Parent` must point at the rebuilt page tree.
///
/// A page whose `/Parent` still names an old intermediate node inherits
/// that node's attributes — a `/MediaBox` or `/Resources` that no longer
/// applies — and a reader following `/Parent` upward can leave the tree
/// entirely.
#[test]
fn every_page_parents_to_the_rebuilt_tree() {
let bytes = document_with_pages(3);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.set_order(&[2, 1, 0]).expect("reorders");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
let mut saved_doc = parse(&saved);
let root_ref = saved_doc
.trailer()
.get("Root")
.and_then(|o| o.as_ref().copied())
.expect("root");
let root = saved_doc.resolve_ref(root_ref).expect("root object");
let pages_ref = root
.as_dict()
.and_then(|d| d.get("Pages"))
.and_then(|o| o.as_ref().copied())
.expect("pages ref");
for i in 0..saved_doc.page_count() {
let page_ref = saved_doc.page_object_ref(i).expect("page ref");
let page = saved_doc.resolve_ref(page_ref).expect("page object");
let parent = page
.as_dict()
.and_then(|d| d.get("Parent"))
.and_then(|o| o.as_ref().copied())
.unwrap_or_else(|| panic!("page {i} has no /Parent"));
assert_eq!(parent, pages_ref, "page {i} parents outside the tree");
}
}
/// `/Count` must equal the number of kids.
///
/// Our own parser walks `/Kids` and never reads `/Count`, so it cannot see
/// a stale one — mutation-testing proved that: setting `/Count` to a
/// constant 5 passed every other test in this file. Other readers *do*
/// trust `/Count`, and a document where the two disagree opens with the
/// wrong number of pages in one viewer and the right number in another.
///
/// So this reads the raw page-tree node rather than asking the document
/// how many pages it thinks there are.
#[test]
fn the_page_count_matches_the_kid_array() {
for order in [vec![0usize, 1, 2, 3], vec![3, 2], vec![1, 1, 1]] {
let bytes = document_with_pages(4);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.set_order(&order).expect("reorders");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
let mut saved_doc = parse(&saved);
let root_ref = saved_doc
.trailer()
.get("Root")
.and_then(|o| o.as_ref().copied())
.expect("root");
let root = saved_doc.resolve_ref(root_ref).expect("root object");
let pages_ref = root
.as_dict()
.and_then(|d| d.get("Pages"))
.and_then(|o| o.as_ref().copied())
.expect("pages ref");
let pages = saved_doc.resolve_ref(pages_ref).expect("pages node");
let dict = pages.as_dict().expect("pages is a dictionary");
let count = dict.get_int("Count").expect("/Count must be present");
let kids = dict
.get_array("Kids")
.map(|k| k.len())
.expect("/Kids must be present");
assert_eq!(
count as usize, kids,
"order {order:?}: /Count says {count} but there are {kids} kids"
);
assert_eq!(
kids,
order.len(),
"order {order:?}: the kid array is the wrong length"
);
}
}
/// Every kid in the rebuilt tree must be a distinct, resolvable page.
///
/// A rebuilt array that repeated a reference, or kept a dangling one from
/// a deleted page, still has the right *length*, so the count check above
/// cannot see it.
#[test]
fn every_kid_resolves_to_a_page_object() {
let bytes = document_with_pages(4);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.delete(2).expect("deletes");
plan.move_page(0, 2).expect("moves");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
let mut saved_doc = parse(&saved);
let root_ref = saved_doc
.trailer()
.get("Root")
.and_then(|o| o.as_ref().copied())
.expect("root");
let root = saved_doc.resolve_ref(root_ref).expect("root object");
let pages_ref = root
.as_dict()
.and_then(|d| d.get("Pages"))
.and_then(|o| o.as_ref().copied())
.expect("pages ref");
let pages = saved_doc.resolve_ref(pages_ref).expect("pages node");
let kids: Vec<PdfObj> = pages
.as_dict()
.and_then(|d| d.get_array("Kids"))
.expect("kids")
.to_vec();
for (i, kid) in kids.iter().enumerate() {
let r = kid
.as_ref()
.copied()
.unwrap_or_else(|| panic!("kid {i} is not an indirect reference"));
let obj = saved_doc
.resolve_ref(r)
.unwrap_or_else(|e| panic!("kid {i} does not resolve: {e}"));
assert_eq!(
obj.as_dict().and_then(|d| d.get_name("Type")),
Some("Page"),
"kid {i} is not a /Page"
);
}
}
/// A page moved out of a *nested* tree must be re-parented.
///
/// The flat fixture above cannot test this: its pages already parent to
/// the root node, so the re-parenting branch never runs and mutation
/// testing showed it could be deleted with every other test still green.
/// A real document with an intermediate `/Pages` node is the case that
/// matters — leave the old `/Parent` in place and the page keeps
/// inheriting attributes from a node it is no longer under.
#[test]
fn a_page_from_a_nested_tree_is_reparented() {
// Root -> [intermediate -> [page A, page B]], with the intermediate
// node supplying a /MediaBox its children inherit.
let bytes = {
let mut out = String::from("%PDF-1.7\n");
let mut offsets = Vec::new();
offsets.push(out.len());
out.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
offsets.push(out.len());
out.push_str("2 0 obj\n<< /Type /Pages /Count 2 /Kids [3 0 R] >>\nendobj\n");
offsets.push(out.len());
out.push_str(
"3 0 obj\n<< /Type /Pages /Parent 2 0 R /Count 2 /Kids [4 0 R 5 0 R] \
/MediaBox [0 0 321 654] >>\nendobj\n",
);
offsets.push(out.len());
out.push_str("4 0 obj\n<< /Type /Page /Parent 3 0 R >>\nendobj\n");
offsets.push(out.len());
out.push_str("5 0 obj\n<< /Type /Page /Parent 3 0 R >>\nendobj\n");
let startxref = out.len();
out.push_str("xref\n0 6\n0000000000 65535 f \n");
for off in &offsets {
out.push_str(&format!("{off:010} 00000 n \n"));
}
out.push_str(&format!(
"trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{startxref}\n%%EOF\n"
));
out.into_bytes()
};
let mut doc = parse(&bytes);
assert_eq!(doc.page_count(), 2, "the nested fixture must parse");
let mut plan = PagePlan::new(doc.page_count());
plan.swap(0, 1).expect("swaps");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
let mut saved_doc = parse(&saved);
let root_ref = saved_doc
.trailer()
.get("Root")
.and_then(|o| o.as_ref().copied())
.expect("root");
let root = saved_doc.resolve_ref(root_ref).expect("root object");
let pages_ref = root
.as_dict()
.and_then(|d| d.get("Pages"))
.and_then(|o| o.as_ref().copied())
.expect("pages ref");
for i in 0..saved_doc.page_count() {
let page_ref = saved_doc.page_object_ref(i).expect("page ref");
let page = saved_doc.resolve_ref(page_ref).expect("page object");
let parent = page
.as_dict()
.and_then(|d| d.get("Parent"))
.and_then(|o| o.as_ref().copied())
.unwrap_or_else(|| panic!("page {i} has no /Parent"));
assert_eq!(
parent, pages_ref,
"page {i} still parents to the old intermediate node, so it \
inherits from a node it is no longer under"
);
}
}
/// The original bytes must remain intact: an incremental save appends.
///
/// A signed or already-incrementally-updated document depends on its
/// earlier revisions being byte-identical. Rewriting them in place
/// invalidates every signature over them.
#[test]
fn the_original_revision_is_left_untouched() {
let bytes = document_with_pages(3);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
plan.delete(1).expect("deletes");
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert!(
saved.len() > bytes.len(),
"an incremental save must grow the file"
);
assert_eq!(
&saved[..bytes.len()],
&bytes[..],
"the original revision was modified rather than appended to"
);
}
#[test]
fn an_empty_plan_is_refused() {
let bytes = document_with_pages(2);
let mut doc = parse(&bytes);
let mut plan = PagePlan::new(doc.page_count());
// Reaching an empty plan requires bypassing delete()'s own guard.
plan.delete(0).expect("first delete is allowed");
assert_eq!(
plan.delete(0),
Err(PageOpError::WouldEmptyDocument),
"the last page cannot be deleted"
);
let (saved, _) = apply_plan(&mut doc, &bytes, &plan).expect("applies");
assert_eq!(page_widths(&saved), vec![101]);
}
// ------------------------------------------------------------ importing
#[test]
fn importing_a_page_appends_it_and_it_reads_back() {
let dest_bytes = document_with_pages(2);
let src_bytes = document_with_pages(3);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let (saved, report) = import_pages(&mut dest, &dest_bytes, &mut src, &[2]).expect("imports");
assert_eq!(report.final_page_count, 3);
assert!(
report.objects_copied > 0,
"importing must copy the page and what it reaches"
);
// The imported page is source page 2, which is 102 wide.
assert_eq!(page_widths(&saved), vec![100, 101, 102]);
}
/// The imported page must bring its content with it.
///
/// This is the assertion that catches the central import bug: copying the
/// page dictionary but not the objects it references. The result is a page
/// whose `/Contents` names an object number that means something else
/// entirely in the destination — usually another page's content, always
/// silently.
#[test]
fn an_imported_page_brings_its_content_stream() {
let dest_bytes = document_with_pages(1);
let src_bytes = document_with_pages(3);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let (saved, _) = import_pages(&mut dest, &dest_bytes, &mut src, &[1]).expect("imports");
let mut saved_doc = parse(&saved);
assert_eq!(saved_doc.page_count(), 2);
let imported = saved_doc.page(1).expect("the imported page");
let text = String::from_utf8_lossy(&imported.content_data).to_string();
assert!(
text.contains("page 1"),
"the imported page shows {text:?}, not its own source content"
);
// And the destination's own page must be undisturbed.
let original = saved_doc.page(0).expect("page 0");
let original_text = String::from_utf8_lossy(&original.content_data).to_string();
assert!(
original_text.contains("page 0"),
"importing overwrote the destination's own content: {original_text:?}"
);
}
#[test]
fn importing_several_pages_keeps_their_order() {
let dest_bytes = document_with_pages(1);
let src_bytes = document_with_pages(4);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let (saved, report) = import_pages(&mut dest, &dest_bytes, &mut src, &[3, 1]).expect("imports");
assert_eq!(report.final_page_count, 3);
assert_eq!(page_widths(&saved), vec![100, 103, 101]);
}
#[test]
fn importing_no_pages_changes_nothing() {
let dest_bytes = document_with_pages(2);
let src_bytes = document_with_pages(2);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let (saved, report) = import_pages(&mut dest, &dest_bytes, &mut src, &[]).expect("no-op");
assert_eq!(report.final_page_count, 2);
assert_eq!(saved, dest_bytes, "a no-op import must not touch the file");
}
#[test]
fn importing_a_page_that_does_not_exist_is_refused() {
let dest_bytes = document_with_pages(1);
let src_bytes = document_with_pages(2);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let err = import_pages(&mut dest, &dest_bytes, &mut src, &[9]).expect_err("no page 9");
assert!(matches!(err, PageOpError::NoSuchPage { index: 9, .. }));
}
/// An imported page must not carry a `/Parent` pointing into the source.
///
/// Following it would lead a reader out of this document's page tree and
/// into object numbers that belong to something else.
#[test]
fn an_imported_page_parents_into_the_destination() {
let dest_bytes = document_with_pages(1);
let src_bytes = document_with_pages(2);
let mut dest = parse(&dest_bytes);
let mut src = parse(&src_bytes);
let (saved, _) = import_pages(&mut dest, &dest_bytes, &mut src, &[0]).expect("imports");
let mut saved_doc = parse(&saved);
let root_ref = saved_doc
.trailer()
.get("Root")
.and_then(|o| o.as_ref().copied())
.expect("root");
let root = saved_doc.resolve_ref(root_ref).expect("root object");
let pages_ref = root
.as_dict()
.and_then(|d| d.get("Pages"))
.and_then(|o| o.as_ref().copied())
.expect("pages ref");
let imported_ref = saved_doc.page_object_ref(1).expect("imported page ref");
let imported = saved_doc.resolve_ref(imported_ref).expect("imported page");
let parent = imported
.as_dict()
.and_then(|d| d.get("Parent"))
.and_then(|o| o.as_ref().copied())
.expect("the imported page must have a /Parent");
assert_eq!(
parent, pages_ref,
"the imported page parents into the source"
);
}
/// A page inheriting `/MediaBox` from its parent must keep that size when
/// imported — the ancestor it inherited from is not coming with it.
#[test]
fn an_imported_page_keeps_an_inherited_media_box() {
// A source whose page carries no /MediaBox of its own: it inherits
// one from the /Pages node, which is exactly the shape that breaks on
// import if the attribute is not resolved first.
let src = {
let mut out = String::from("%PDF-1.7\n");
let mut offsets = Vec::new();
offsets.push(out.len());
out.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
offsets.push(out.len());
out.push_str(
"2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] \
/MediaBox [0 0 333 444] >>\nendobj\n",
);
offsets.push(out.len());
out.push_str("3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n");
let startxref = out.len();
out.push_str("xref\n0 4\n0000000000 65535 f \n");
for off in &offsets {
out.push_str(&format!("{off:010} 00000 n \n"));
}
out.push_str(&format!(
"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n{startxref}\n%%EOF\n"
));
out.into_bytes()
};
let dest_bytes = document_with_pages(1);
let mut dest = parse(&dest_bytes);
let mut source = parse(&src);
assert_eq!(source.page_count(), 1, "the source fixture must parse");
let (saved, _) = import_pages(&mut dest, &dest_bytes, &mut source, &[0]).expect("imports");
let mut saved_doc = parse(&saved);
let imported = saved_doc.page(1).expect("the imported page");
assert_eq!(
imported.media_box[2] as i64, 333,
"the inherited /MediaBox was lost, so the page changed size"
);
}