Merge pull request #4589 from Ridzzz0Alam/fix/2033-eps-fallback-layers

Fix #2033: An EPS whose layers can't be used keeps its layer tree
This commit is contained in:
BFlat
2026-10-29 21:00:45 +07:00
committed by GitHub
3 changed files with 97 additions and 15 deletions
+14 -2
View File
@@ -44,8 +44,20 @@ pub(super) fn build(
) -> Result<(Document, Vec<String>), 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>), 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));
+46 -5
View File
@@ -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<Arc<Node>> = tree.iter().map(|l| Arc::new(doc.reid(l))).collect();
let taken: Vec<Option<String>> = 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<Node>]) -> Vec<Arc<Node>> {
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
+37 -8
View File
@@ -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();