.ai import: hidden ruler guides open as ruler guides with their own state

A straight `(N) *` guide path that runs across the canvas or an artboard
opens as a ruler guide, but a hidden one (`1 Xw`) stayed a guide path.
It now opens as a ruler guide too, and every ruler guide made from such a
path keeps the path's hide, lock and name, so its Layers row shows them.
Before, a locked guide opened unlocked and a named one lost its name.

Hidden or locked guide paths that are short, slanted, grouped or `(S) *`
stay guide paths, and commented copies (`%_`) are still left out.

Docs: docs/target-app-parity.md, docs/file-format-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 17:55:31 -08:00
parent 71cef8ac8d
commit cfa76d674f
6 changed files with 43 additions and 7 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
//! next layers: a clipping group in each layer, read by [`masks`].
//! - Ruler guides: a straight `(N) *` guide path, directly on a layer, that runs across the whole
//! canvas (16,383 points around the template box's centre) or from edge to edge of one artboard
//! is a ruler guide on that layer ([`Document::guides`]); other guide paths stay paths.
//! is a ruler guide on that layer ([`Document::guides`]), shown or hidden, with the path's name,
//! lock and visibility; other guide paths stay paths.
//! - Groups `u` … `U`, compound paths `*u` … `*U`, clipping groups `q` … `Q` (the path painted
//! with `W` clips); a group's transparency and name follow its end. A wraparound group `*w` …
//! `*W` (version 7 specification §15.4.2) is a group whose objects other than its type wrap
+1 -2
View File
@@ -234,11 +234,10 @@ impl<'a> Reader<'a> {
}
let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide };
let mut node = self.new_node(kind, &gs);
// A ruler guide is written as a straight `(N) *` guide path on its layer.
// A ruler guide is written as a straight `(N) *` guide path on its layer, shown or hidden.
if guide
&& op == "N"
&& !hidden
&& !gs.hidden
&& !clip
&& self.pattern.is_none()
&& matches!(self.frames.last(), Some(Frame { kind: Kind::Layer(_), .. }))
+7 -2
View File
@@ -1443,7 +1443,8 @@ fn ruler_guide(a: Point, b: Point, centre: Option<(f64, f64)>, boards: &[Rect])
}
/// Makes the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
/// ruler guide nodes with the same ids, moved to the top of their layer in their order.
/// ruler guide nodes with the same ids, names, locks and visibility, moved to the top of their layer
/// in their order.
fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Guide>, depth: usize) {
if depth > MAX_DEPTH {
return;
@@ -1456,7 +1457,11 @@ fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Gu
let mut guides = vec![];
kids.retain(|k| match pick(k.id) {
Some(g) => {
guides.push(Arc::new(Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard })));
let mut n = Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard });
n.name = k.name.clone();
n.locked = k.locked;
n.visible = k.visible;
guides.push(Arc::new(n));
false
}
None => true,
+31 -1
View File
@@ -8786,7 +8786,8 @@ fn other_guide_paths_stay_guide_paths() {
guide_line(0.0, 30.0, 200.0, 30.0, "S"),
guide_line(-7885.0, -7796.0, 8498.0, 8587.0, "N"),
format!("u\n{}U\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
format!("1 Xw\n{}0 Xw\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
format!("1 Xw\n{}0 Xw\n", guide_line(20.0, 40.0, 80.0, 40.0, "N")),
format!("1 A\n{}0 A\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "S")),
"-7885 40 m\n306 40 L\n8498 40 L\n(N) *\n".to_string(),
];
for body in cases {
@@ -8796,6 +8797,35 @@ fn other_guide_paths_stay_guide_paths() {
}
}
/// A hidden (`1 Xw`), locked (`1 A`) or named ruler guide path is a ruler guide that keeps its own
/// visibility, lock and name, across the canvas or an artboard, on a shown layer.
#[test]
fn hidden_locked_and_named_guide_paths_are_ruler_guides() {
let body = format!(
"1 Xw\n{}0 Xw\n1 A\n{}0 A\n{}%_/ArtDictionary :\n%_(Mark) /String (AIArtName) ,\n%_;\n%_\n1 Xw\n{}0 Xw\n",
guide_line(-7885.0, 40.0, 8498.0, 40.0, "N"),
guide_line(25.0, 8587.0, 25.0, -7796.0, "N"),
guide_line(-7885.0, 90.0, 8498.0, 90.0, "N"),
guide_line(0.0, 30.0, 200.0, 30.0, "N"),
);
let doc = read(canvas_stream(&layer(&body)).as_bytes()).unwrap().doc;
let top = &doc.layers[0];
assert!(top.visible);
let kids = top.children().unwrap();
assert_eq!(kids.len(), 4);
assert!(kids.iter().all(|c| c.is_ruler_guide()), "no guide path is left");
let got: Vec<_> = doc.ruler_guides().into_iter().map(|g| (g.vertical, g.pos, g.artboard, g.layer, g.hidden, g.locked, g.name)).collect();
let want = [
(false, 60.0, None, Some(top.id), true, false, None),
(true, 25.0, None, Some(top.id), false, true, None),
(false, 10.0, None, Some(top.id), false, false, Some("Mark".to_string())),
(false, 70.0, Some(1), Some(top.id), true, false, None),
];
assert_eq!(got, want);
// The nodes themselves carry the state, so the Layers rows show it.
assert_eq!(kids.iter().map(|k| (k.visible, k.locked)).collect::<Vec<_>>(), [(false, false), (true, true), (true, false), (false, false)]);
}
/// The layer whose `Lb` has `ops` as its options (the first one in the file).
fn layer_with(ops: &str) -> Node {
let doc = read_ok(&format!("%AI5_BeginLayer\n{ops} Lb\n(L) Ln\nLB\n%AI5_EndLayer--\n")).doc;
+1 -1
View File
@@ -190,7 +190,7 @@ private editing data. VectorCraft:
editing data is read whole (#758), live effects that are not read or have settings that are not read, and
brushes come in as their drawn look (#637; pattern fills are read since #1025, and symbols and their instances
open live, as do the symbols of a symbol library without layers); legacy `.ai` without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868; guides
open as ruler guides and guide paths since #5057 (#779).
open as ruler guides and guide paths since #5057 (#779), hidden, locked and named ruler guides with their state.
Type under a Warp effect is compared with the art saved with it: type that draws as that art stays editable with
the effect live, and type that draws differently, or that the file's limit of checks has no check left for, opens
as that art, with one note that counts them. Type stays live without a comparison when the saved art paints
+1
View File
@@ -1677,6 +1677,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
In a `.ai` or EPS opened from its editing data, and in a legacy `.ai`, each anchor is smooth or a corner as the case of the segment operator ending on it says (`l`, `c`, `v`, `y` smooth; `L`, `C`, `V`, `Y` corner), and a closed path's first anchor takes the case of the segment back to it; points and handles stay where they are. Paths, compound paths, clipping paths, guide paths, area type frames and type-on-a-path paths all keep it (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/legacy_ai/tests.rs`).
In `.ai` and EPS editing-data import, a clipping group, layer or opacity mask's art with several clipping paths (`W`) clips to their intersection: each clipping path after the first clips a clipping group nested inside the one before it, and each keeps its own stroke and transparency. A compound path of clipping paths (`*u` … `*U`) is still one clipping path, and clipping paths that would nest the art past the document's 32 levels stop the editing data from being read (`crates/eps/src/import/ai/tests.rs`).
`.ai` and EPS editing-data import keeps template layers, and opens canvas and artboard ruler guides as ruler guides on their layers; guide objects stay editable guide paths (#779; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
A hidden (`1 Xw`) ruler guide path in `.ai` and EPS editing data opens as a hidden ruler guide rather than a guide path, and every ruler guide keeps its path's hide, lock and name, so its Layers row shows them (`hidden_locked_and_named_guide_paths_are_ruler_guides`).
`.ai` and EPS editing-data import keeps each path's Attributes panel note (`%AI3_Note:`, `%%Note:`, more lines in `%AI5_NoteMore:`, up to 1,024 characters) and Show Center (`1 Ap`); a note or Show Center applies to every path after it until the next one, and a note before a group or compound path is not theirs (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_illustrator.rs`).
`.ai` and EPS editing-data import reads object tags (`/identifier (string) XT`) on the object before them, whatever kind of object it is: `AdobeURL` becomes the object's URL, and every other tag is kept in the object's data in order, up to 64 per object; a tag longer than 64 KiB, and tags in a pattern's or brush's art, are ignored. A tag on art that shows no longer stops the editing data from being read, and brush art that holds tags is read (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_illustrator.rs`).
A `.ai` saved without PDF compatibility (its PDF part only a placeholder page) is placed and pasted from its editing data, as it opens: File › Place places the art of the layers that show on the first artboard (`file.place`'s `page` places another one), and Paste of it as PDF data (`clipboard.importPdf`) takes every object. `document.open` with `pages` naming every page of such a file opens it the same way (`crates/engine/src/cmd/fileio/tests_aiimport.rs`).