Fix #2033: an EPS whose layers can't be used keeps its layer tree

When the editing data's art is rejected (another page size, art the page
doesn't draw, too many text objects…), the file still comes in as its page,
but now on a layer above the editing data's layers. They come in empty, in
their order, with their names, visibility, lock, print option and sublayers.
The page's layer takes a free name when the file's layers have its Layer 1.
This commit is contained in:
Ridzzz0Alam
2026-10-29 20:36:11 +07:00
committed by bflatastic
parent 7afba9bde4
commit 6661f2419b
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();