mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
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:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user