diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 0b9435c71..e99d0a4e8 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1884,6 +1884,11 @@ impl<'a> Ex<'a> { .attr("ImportPolicy", "NoAutoImport") .attr("ExportPolicy", "NoAutoExport"), ); + if is_pdf && asset.page > 0 { + // PDF Import Options: the page is 1-based; page 1 (the default) needs no element, as + // InDesign omits it too. + el.push(El::new("PDFAttribute").attr("PageNumber", asset.page.saturating_add(1).to_string())); + } el } diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 51dc66d84..c72237964 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -194,8 +194,9 @@ struct Importer<'r> { page_index: HashMap, item_ids: HashMap, assets: BTreeMap>, - /// Assets read from a link path, so later placements of that path reuse them unread. - linked_assets: HashMap, + /// Assets read from a link path at a page, so later placements of that path and page reuse + /// them unread. + linked_assets: HashMap<(String, u32), AssetId>, ctx: ItemCtx, sections: Vec
, footnote_options: designcraft_doc::FootnoteOptions, @@ -2377,17 +2378,28 @@ impl<'r> Importer<'r> { }) }; let mut data = g.prop_el("Contents").map(|c| base64_decode(&c.text_content())).unwrap_or_default(); + // PDF Import Options: `PDFAttribute/@PageNumber` is 1-based; `Asset.page` is 0-based. A + // missing or malformed value means page 1, and an out-of-range page later fails to render + // rather than panicking. + let page = g + .find("PDFAttribute") + .or_else(|| g.prop_el("PDFAttribute")) + .and_then(|a| a.get("PageNumber")) + .and_then(|v| v.trim().parse::().ok()) + .and_then(|n| u32::try_from(n.saturating_sub(1)).ok()) + .unwrap_or(0); let mut read_from = None; if data.is_empty() && let Some(p) = &uri { - // Every placement of a linked file shares one asset; read the file once. - if let Some(id) = self.linked_assets.get(p) { + // Every placement of a linked file at one page shares one asset; read the file once. + let key = (p.clone(), page); + if let Some(id) = self.linked_assets.get(&key) { return graphic(*id); } if let Some(d) = (self.read_link)(p) { data = d; - read_from = Some(p.clone()); + read_from = Some(key); } } let (mime, px) = sniff_image(&data); @@ -2407,9 +2419,9 @@ impl<'r> Importer<'r> { let fresh = AssetId(self.alloc()); let link_path = uri.filter(|p| p.contains('/') || p.contains('\\')); let id = - designcraft_doc::insert_asset(&mut self.assets, Asset { page: 0, id: fresh, name, mime, link: link_path, data: Arc::new(data), pixels }); - if let Some(p) = read_from { - self.linked_assets.insert(p, id); + designcraft_doc::insert_asset(&mut self.assets, Asset { page, id: fresh, name, mime, link: link_path, data: Arc::new(data), pixels }); + if let Some(key) = read_from { + self.linked_assets.insert(key, id); } graphic(id) } diff --git a/crates/idml/src/tests/assets.rs b/crates/idml/src/tests/assets.rs index dd2761442..d4d1501f2 100644 --- a/crates/idml/src/tests/assets.rs +++ b/crates/idml/src/tests/assets.rs @@ -31,6 +31,22 @@ fn image(n: usize, uri: &str, contents: Option<&[u8]>) -> String { ) } +/// A graphic frame showing `pdf` (a `` element) with the page `PageNumber` (1-based). +fn pdf_frame(n: usize, x: f64, page: u32) -> String { + format!( + r#" + + + + + + + + + "# + ) +} + fn package(frames: &[String]) -> Vec { let spread = format!( r#" @@ -108,3 +124,27 @@ fn one_file_placed_several_times_imports_as_one_asset() { } } } + +/// `PDFAttribute/@PageNumber` (1-based) is read into `Asset.page` (0-based), and the same file at +/// different pages stays apart (#905). +#[test] +fn a_pdf_placement_reads_its_page_number() { + let bytes = b"%PDF-1.7\n% fixture".to_vec(); + let reader = |p: &str| p.ends_with("insert.pdf").then(|| bytes.clone()); + let frames = vec![pdf_frame(1, 0.0, 2), pdf_frame(2, 100.0, 1), pdf_frame(3, 200.0, 2)]; + let d = import_idml_with(&package(&frames), &reader).unwrap(); + let p = placements(&d); + let page = |id| d.assets.get(&id).map(|a| a.page); + assert_eq!(page(p[0].0), Some(1), "PageNumber=\"2\" is the second page, stored 0-based"); + assert_eq!(page(p[1].0), Some(0), "PageNumber=\"1\" is the first page"); + assert_eq!(p[0].0, p[2].0, "the same file at the same page shares one asset"); + assert_ne!(p[0].0, p[1].0, "the same file at another page is another asset"); + + // Exported (linked or embedded), the page survives the round trip. + for embed_images in [false, true] { + let out = export_idml_with(&d, &ExportOptions { embed_images }); + let back = import_idml_with(&out, &reader).unwrap(); + let pages: Vec<_> = placements(&back).iter().map(|(id, _, _)| back.assets.get(id).map(|a| a.page)).collect(); + assert_eq!(pages, vec![Some(1), Some(0), Some(1)], "embed_images: {embed_images}"); + } +}