From 49ec643a78334a82bf492163ddefbcf96e8314fe Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Sun, 8 Nov 2026 14:03:32 +0800 Subject: [PATCH] M2: a page's own /MediaBox wins and a nearest box without area gives Letter, tested in the renderer and model (view.render-pages) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA --- crates/model/src/lib.rs | 29 +++++++++++++++++++++ crates/render/src/raster/tests.rs | 43 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 037edb0b30..94da24ea1c 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -303,6 +303,35 @@ mod tests { assert_ne!(p.view_matrix(&d), view_matrix_for(0, [0.0, 0.0, 300.0, 400.0]), "the origin is kept"); } + /// The nearest ancestor's `/MediaBox` wins over the root's, origin and all: the crop and the view matrix come from + /// the intermediate `/Pages` node's box. + #[test] + fn a_page_takes_its_nearest_ancestors_media_box_and_origin() { + let d = doc_from(&[ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>".to_string(), + "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 1 /MediaBox [100 200 400 600] >>".to_string(), + "<< /Type /Page /Parent 3 0 R >>".to_string(), + ]); + let p = &pages(&d)[0]; + assert_eq!(p.crop(&d), [100.0, 200.0, 400.0, 600.0]); + assert_eq!(p.view_matrix(&d), view_matrix_for(0, [100.0, 200.0, 400.0, 600.0])); + } + + /// An intermediate `/Pages` node's `/MediaBox` without area is the inherited one, so the page is US Letter, not + /// its grandparent's box. + #[test] + fn a_nearest_media_box_without_area_gives_letter_not_a_further_ancestors() { + let d = doc_from(&[ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>".to_string(), + "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 1 /MediaBox [0 0 0 0] >>".to_string(), + "<< /Type /Page /Parent 3 0 R >>".to_string(), + ]); + let p = &pages(&d)[0]; + assert_eq!(p.crop(&d), [0.0, 0.0, 612.0, 792.0]); + } + /// A `/MediaBox` without area (`[0 0 0 0]`) is US Letter too, as the renderer draws it. #[test] fn a_page_whose_media_box_has_no_area_is_letter() { diff --git a/crates/render/src/raster/tests.rs b/crates/render/src/raster/tests.rs index f36c5523e2..ec7f0ce91b 100644 --- a/crates/render/src/raster/tests.rs +++ b/crates/render/src/raster/tests.rs @@ -5602,6 +5602,49 @@ trailer << /Root 1 0 R >> assert_eq!((info.pages[0].width, info.pages[0].height), (300.0, 400.0), "inspected size"); } +/// A page's own `/MediaBox` wins over its parent's: rendered and inspected at the page's 200 × 100, the content placed by +/// the page box's origin. +#[test] +fn a_pages_own_media_box_wins_over_its_parents() { + let pdf = b"%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [50 50 250 150] /Contents 4 0 R >> endobj +4 0 obj << /Length 25 >> stream +0 0 1 rg 60 60 50 50 re f +endstream endobj +trailer << /Root 1 0 R >> +%%EOF" + .to_vec(); + let page = render_first_page(pdf.clone()); + assert_eq!((page.width, page.height), (200, 100), "the page's box, not its parent's"); + // The square sits 10 pt in from the page box's lower-left corner, at (50, 50). + assert_eq!(rgb_at(&page.rgba, page.width as usize, 30, 100 - 30), [0, 0, 255], "placed by the page box's origin"); + let info = crate::inspect(Arc::new(pdf), None).expect("inspect"); + assert_eq!((info.pages[0].width, info.pages[0].height), (200.0, 100.0), "inspected size"); +} + +/// The nearest ancestor's `/MediaBox` is the inherited one even when it has no area: an intermediate `/Pages` node's +/// `[0 0 0 0]` gives US Letter, not the grandparent's 300 × 400 (ISO 32000-2 §7.7.3.4). +#[test] +fn a_nearest_media_box_without_area_gives_letter_not_a_further_ancestors() { + let pdf = b"%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [5 0 R] /Count 1 /MediaBox [0 0 300 400] >> endobj +5 0 obj << /Type /Pages /Parent 2 0 R /Kids [3 0 R] /Count 1 /MediaBox [0 0 0 0] >> endobj +3 0 obj << /Type /Page /Parent 5 0 R /Contents 4 0 R >> endobj +4 0 obj << /Length 25 >> stream +0 0 1 rg 10 10 50 50 re f +endstream endobj +trailer << /Root 1 0 R >> +%%EOF" + .to_vec(); + let page = render_first_page(pdf.clone()); + assert_eq!((page.width, page.height), (612, 792), "rendered size"); + let info = crate::inspect(Arc::new(pdf), None).expect("inspect"); + assert_eq!((info.pages[0].width, info.pages[0].height), (612.0, 792.0), "inspected size"); +} + /// A page whose content expands far beyond its compressed stream stops at the expansion budget, /// which is 64 times the compressed size and at least `MAX_DECODED_STREAM`, and the page says so. #[test]