diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 995e8e8d3..24001e8f0 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -44,8 +44,20 @@ pub(super) fn build( ) -> Result<(Document, Vec), String> { // An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art // space, as its PDF part may (#2031). - let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, restyled, warnings } = - ai::read(data, matches!(page, Page::Art))?; + build_from(ai::read(data, matches!(page, Page::Art))?, data, visible, page, outlined, trust, unit) +} + +/// [`build`] from `structure`, the editing copy `data` already read. +pub(super) fn build_from( + structure: Structure, + data: &[u8], + visible: Option<&Document>, + page: Page, + outlined: bool, + trust: bool, + unit: f64, +) -> Result<(Document, Vec), String> { + let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, restyled, warnings } = structure; // Editing data in the units of the page's user space ([`in_units`]) is compared with the page // scaled down to those units, and the document read from it is scaled up by `unit`. let board = artboard.map(|a| to_doc.transform_rect_bbox(a)); diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index c4403cdcf..6be56c200 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -1,4 +1,4 @@ -use super::build::{Page, build}; +use super::build::{Page, build, build_from}; use super::slots::LAYERS_UNREAD; use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai}; @@ -6,11 +6,20 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect /// its program in the legacy format, see [`editing_copy`]): its layers (the /// hidden ones too), groups, compound paths, clipping groups and artboards, with the text and the /// strokes around it of the page. When the file has no editing copy, `visible` as it was; when it -/// has one that can't be used, `visible` with a warning that says why, and with the swatches of -/// the editing copy's palette ([`use_palette`]) when it could be read. +/// has one that can't be used, `visible` with a warning that says why, with the swatches of the +/// editing copy's palette ([`use_palette`]) when it could be read, and with its layer tree, empty, +/// below the page's layer when that could be read (#2033). pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool) -> Imported { let Some(data) = editing_copy(ps) else { return visible }; - let result = data.as_deref().map_err(Clone::clone).and_then(|data| build(data, Some(&visible.document), Page::Art, false, trust, 1.0)); + // The layer tree is kept from the structure read, for the page to come in on when the layers + // can't be used. + let (tree, result) = match data.as_deref() { + Ok(d) => match ai::read(d, true) { + Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)), + Err(why) => (vec![], Err(why)), + }, + Err(why) => (vec![], Err(why.clone())), + }; match result { Ok((document, mut warnings)) => { for w in &visible.warnings { @@ -25,12 +34,44 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool) -> Im if let Ok(data) = &data { use_palette(data, &mut v.document); } - v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, in one layer")); + if tree.is_empty() { + v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, in one layer")); + return v; + } + let doc = &mut v.document; + let mut layers: Vec> = tree.iter().map(|l| Arc::new(doc.reid(l))).collect(); + let taken: Vec> = layers.iter().map(|l| l.name.clone()).collect(); + layers.append(&mut doc.layers); + doc.layers = layers; + // The page's layer takes a name of its own: the file's layers usually have its `Layer 1`. + for i in taken.len()..doc.layers.len() { + if doc.layers.get(i).is_some_and(|l| taken.contains(&l.name)) { + let name = doc.next_layer_name(); + if let Some(l) = doc.layers.get_mut(i) { + Arc::make_mut(l).name = Some(name); + } + } + } + v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, on a layer above its layers, which come in empty")); v } } } +/// The layers in `nodes` with only their sublayers in them: names, order and options without the art. +fn layer_tree(nodes: &[Arc]) -> Vec> { + let layers = nodes.iter().filter(|n| matches!(n.kind, NodeKind::Layer { .. })); + layers + .map(|l| { + let mut l = Node::clone(l); + if let Some(children) = l.children_mut() { + *children = layer_tree(children); + } + Arc::new(l) + }) + .collect() +} + /// The `.ai` `visible` (read as its PDF part), or a PDF saved with the editing data, read through /// its editing copy (`private`, the joined `AIPrivateData` or `AIPDFPrivateData` streams) instead, /// as [`layered`] reads an EPS: with the layers and objects of the file, its artboards (with the diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index c0fa58c22..4e1848f4f 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -395,8 +395,8 @@ fn editing_data_for_a_page_of_another_size_names_both_sizes() { let art = native(&layer("Jig", true, &square(10, 10))).replace(" 0 0 100 100", " 0 0 600 400"); let file = String::from_utf8(eps(&page_square(10, 10), &art)).unwrap().replace("%%BoundingBox: 0 0 100 100", "%%BoundingBox: 0 0 612 792"); let r = import(file.as_bytes()).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); - let note = "the file's layers weren't read (its editing data is for a 600 × 400 pt page, not the file's 612 × 792 pt page): it comes in as its page, in one layer"; + assert_eq!(names(&r.document), ["Jig", "Layer 1"], "{:?}", r.warnings); + let note = "the file's layers weren't read (its editing data is for a 600 × 400 pt page, not the file's 612 × 792 pt page): it comes in as its page, on a layer above its layers, which come in empty"; assert!(r.warnings.iter().any(|w| w == note), "{:?}", r.warnings); // A `.ai` whose PDF part has an A4 page. let (_, w, read) = crate::layered_ai(&private(&art), Document::new(595.2756, 841.8898), vec![], false, false, 1.0); @@ -404,6 +404,35 @@ fn editing_data_for_a_page_of_another_size_names_both_sizes() { assert!(!read && w.iter().any(|w| w == note), "{w:?}"); } +/// #2033: an EPS whose layers can't be used (here its editing data is for another page) still +/// keeps its layer tree: the layers come in empty, in their order and with their names and +/// options (sublayers too), and the page comes in on a layer above them, under a name none of +/// them has. +#[test] +fn an_eps_whose_layers_cant_be_used_keeps_its_layer_tree() { + // Unlocked 0 (locked), printing 0. + let locked = layer("Locked", true, &layer("Sub", true, &square(20, 20))).replacen("1 1 1 1 0 0 1 0", "1 1 0 0 0 0 1 0", 1); + let layers = [layer("Layer 1", true, ""), locked, layer("Hidden", false, &square(50, 50))].concat(); + let art = native(&layers).replace(" 0 0 100 100", " 0 0 600 400"); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let d = &r.document; + assert_eq!(names(d), ["Layer 1", "Locked", "Hidden", "Layer 6"], "{:?}", r.warnings); + let [back, locked, hidden, page] = &d.layers[..] else { panic!("{:?}", names(d)) }; + let options = |l: &Node| (l.visible, l.locked, matches!(l.kind, NodeKind::Layer { printable: true, .. })); + assert_eq!( + [options(back), options(locked), options(hidden), options(page)], + [(true, false, true), (true, true, false), (false, false, true), (true, false, true)] + ); + assert!(back.children().unwrap().is_empty() && hidden.children().unwrap().is_empty()); + let [sub] = &locked.children().unwrap()[..] else { panic!("{locked:?}") }; + assert!(sub.name.as_deref() == Some("Sub") && sub.children().unwrap().is_empty()); + assert_eq!(page.children().unwrap().len(), 1, "the page's square"); + let mut ids = std::collections::BTreeSet::new(); + d.walk(|n| assert!(ids.insert(n.id), "{:?} twice", n.id)); + let note = "the file's layers weren't read (its editing data is for a 600 × 400 pt page, not the file's 100 × 100 pt page): it comes in as its page, on a layer above its layers, which come in empty"; + assert!(r.warnings.iter().any(|w| w == note), "{:?}", r.warnings); +} + /// #2034: editing data whose page is the file's at another scale (a Large Canvas file without its /// scale, for one) gives that scale, the editing data's page to the file's. #[test] @@ -411,14 +440,14 @@ fn editing_data_for_the_page_at_another_scale_names_the_scale() { let art = native(&layer("Jig", true, &square(10, 10))); let note = |file: &str| { let r = import(file.as_bytes()).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Jig", "Layer 1"], "{:?}", r.warnings); r.warnings.into_iter().find(|w| w.contains("its editing data is for")).unwrap_or_default() }; let file = String::from_utf8(eps(&page_square(10, 10), &art)).unwrap(); let ten = file.replace("%%BoundingBox: 0 0 100 100", "%%BoundingBox: 0 0 1000 1000"); assert_eq!( note(&ten), - "the file's layers weren't read (its editing data is for a 100 × 100 pt page, the file's 1000 × 1000 pt page at 1:10): it comes in as its page, in one layer" + "the file's layers weren't read (its editing data is for a 100 × 100 pt page, the file's 1000 × 1000 pt page at 1:10): it comes in as its page, on a layer above its layers, which come in empty" ); let wide = file.replace("%%BoundingBox: 0 0 100 100", "%%BoundingBox: 0 0 250 250"); assert!(note(&wide).contains("for a 100 × 100 pt page, the file's 250 × 250 pt page at 1:2.5)")); @@ -466,7 +495,7 @@ fn a_large_canvas_eps_keeps_its_layers_at_the_pages_size() { // A declared scale the art doesn't fit still makes it another page's. let art = native_large_canvas(20.0, 10.0, &layer("Art", true, &square(10, 10))); let r = import(&eps(&page_square(10, 10), &art)).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Art", "Layer 1"], "{:?}", r.warnings); assert!(r.warnings.iter().any(|w| w.contains("for a 200 × 200 pt page, the file's 100 × 100 pt page at 2:1")), "{:?}", r.warnings); // A `.ai` keeps its art space: its PDF part is read at the size it declares. let art = native_large_canvas(100.0, 10.0, &layer("Art", true, &square(10, 10))); @@ -1044,7 +1073,7 @@ fn more_text_objects_than_are_matched_leave_the_page() { let doc = text_document(&[("Hi\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &many)), &doc); let r = import(&eps(&page_square(10, 10), &art)).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Back", "Spare", "Layer 1"], "{:?}", r.warnings); assert!(r.warnings.iter().any(|w| w.contains("too many text objects") && crate::is_loss(w)), "{:?}", r.warnings); } @@ -1156,7 +1185,7 @@ fn an_object_the_page_doesnt_draw_makes_the_file_come_in_as_its_page() { let extra = "0 0 1 0 k\n50 50 m\n70 50 L\n70 70 L\n50 70 L\nf"; let art = native(&layer("Back", true, &(square(10, 10) + "\n" + extra))); let r = import(&eps(&page_square(10, 10), &art)).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Back", "Layer 1"], "{:?}", r.warnings); assert!(r.warnings.iter().any(|w| w.contains("layers weren't read") && w.contains("differs from the page's")), "{:?}", r.warnings); } @@ -1208,7 +1237,7 @@ fn art_the_layers_print_outside_an_eps_page_of_its_art_makes_it_come_in_as_its_p let file = |far: &str| eps_of_art([10, 10, 14, 14], &page_square(10, 10), &(layer("Art", true, &square(10, 10)) + far)); // A square the layers print far from it: the page would have it. let r = import(&file(&layer("Far", true, &square(80, 80)))).unwrap(); - assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Art", "Far", "Layer 1"], "{:?}", r.warnings); assert!(r.warnings.iter().any(|w| w.contains("layers weren't read")), "{:?}", r.warnings); // Hidden, or on a layer that doesn't print, the page doesn't have it. let r = import(&file(&layer("Far", false, &square(80, 80)))).unwrap();