From 7d004de195253a0eb3d5900385fda5b484b26eb0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 09:20:46 -0800 Subject: [PATCH] .ai import: graphic style libraries open with their styles A .ai or EPS graphic style library holds graphic styles and their list in its editing data but no layers, so it came in as its PDF part or page with the default styles, and Other Library listed those. A file read as its PDF part or page, because it has no layers or its layers can't be used, now gets the styles of its list, in list order, as a file read from its layers does: File > Open shows them in the Graphic Styles panel and Other Library lists them in a library panel. Style stroke weights and effects are scaled by the Large Canvas factor of an EPS, and for a .ai whose editing data is in the units of its pages by the pages' UserUnit or the declared Large Canvas scale. Part of #3349 --- .../engine/src/cmd/fileio/tests_aiimport.rs | 32 ++++++ crates/engine/tests/import_fuzz.rs | 6 + crates/eps/src/import/ai.rs | 3 +- crates/eps/src/import/ai/read.rs | 2 +- crates/eps/src/import/ai/read/document.rs | 16 +++ crates/eps/src/import/native/layers.rs | 90 +++++++++++---- crates/eps/src/import/tests_native.rs | 108 ++++++++++++++++++ docs/mcp.md | 3 +- docs/target-app-parity.md | 1 + 9 files changed, 235 insertions(+), 26 deletions(-) diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 304734116..0aa1733d8 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -311,6 +311,38 @@ fn a_swatch_library_in_a_ai_file_without_layers_loads_its_palette() { assert!(matches!(sky, Some(vectorcraft_color::Paint::Gradient(_))), "{sky:?}"); } +/// Other Library… on a `.ai` graphic style library (art styles, their list and no layers) gives the +/// library the styles of its list, in the list's order, not the default styles; the file opens with +/// them too. +#[test] +fn a_graphic_style_library_in_a_ai_file_without_layers_loads_its_styles() { + let style = |name: &str, xa: &str| { + format!( + "/KnownStyle :\n({name}) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\ + /BasicFilter :\n(Fill Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n/FillStyle : 0 O\n{xa}0 1 Xd\n/Def ;\n (FillStyle) ,\n; /Dict ;\n /Part ,\n;\n" + ) + }; + let styles = format!( + "%AI9_BeginArtStyles\n{}{}{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n(Red)\n(Green)\n(Blue)\n%AI9_EndArtStyleList\n", + style("Blue", "0 0 0 0 0 0 1 Xa\n"), + style("Green", "0 0 0 0 0 1 0 Xa\n"), + style("Red", "0 0 0 0 1 0 0 Xa\n") + ); + let data = ai::editing_data(200.0, 100.0, &styles); + let file = ai::ai(&compressed(data.as_bytes()), "1 0 0 rg 10 50 30 40 re f"); + let mut s = Session::new(); + let p = json!({"dataBase64": vectorcraft_format::base64_encode(&file), "name": "fills.ai"}); + let r = s.execute("graphicStyle.loadLibrary", &p).unwrap(); + assert_eq!((&r["name"], &r["count"]), (&json!("fills"), &json!(3)), "{r}"); + let lib = s.execute("graphicStyle.library", &json!({"library": r["library"]})).unwrap(); + let names: Vec<&str> = lib["styles"].as_array().unwrap().iter().map(|g| g["name"].as_str().unwrap()).collect(); + assert_eq!(names, ["Red", "Green", "Blue"], "{lib}"); + let r = open(&mut s, "fills.ai", &file, json!({})).unwrap(); + let doc = &s.doc().unwrap().doc; + let styles: Vec<(&str, u32)> = doc.graphic_styles.iter().map(|g| (g.name.as_str(), g.id)).collect(); + assert_eq!(styles, [("Red", 1), ("Green", 2), ("Blue", 3)], "{r}"); +} + /// A 1-bit RGB image opens from the editing data in the colors its PDF part shows. #[test] fn a_one_bit_rgb_image_opens_from_the_editing_data_in_its_colors() { diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 8c2fec772..85c85dfa5 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3523,6 +3523,7 @@ proptest! { ], 0..12), ended in any::(), objects in prop::collection::vec((prop_oneof![arb_num(), prop::sample::select(vec!["1".to_string(), "6".into(), "9".into()])], prop::sample::select(vec!["A", "B", "", "Caf\u{e9}", "Missing"]), any::()), 0..6), + unit in prop::sample::select(vec![1.0, 2.0, 0.0, -1.0, f64::NAN, f64::INFINITY]), ) { use vectorcraft_testkit::ai; let style = |name: &str| { @@ -3551,6 +3552,11 @@ proptest! { survive("style lists and links", || Some(vectorcraft_eps::layered_ai(data.as_bytes(), Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?; let eps = ai::eps(data.as_bytes(), ai::page_ps()); survive("style lists and links in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?; + // A graphic style library: the styles and their list, no layers. + let library = ai::editing_data(200.0, 100.0, &header); + survive("style library", || Some(vectorcraft_eps::layered_ai(library.as_bytes(), Document::new(200.0, 100.0), vec![], false, false, unit).0))?; + let eps = ai::eps(library.as_bytes(), ai::page_ps()); + survive("style library in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?; } /// Objects on Path records of any shape: ids in any order, repeated or missing, any number of diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index bb225c397..21c6a62aa 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -38,7 +38,8 @@ pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; pub(crate) use frames::text_frames; pub(crate) use paint::MAX_STOPS as MAX_GRADIENT_STOPS; pub use read::{ - BindingRef, Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, bindings_note, brushes, palette, read, read_beside, variables, + BindingRef, Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, bindings_note, brushes, graphic_styles, palette, read, read_beside, + variables, }; pub(crate) use read::{join_and, slot_frame, slot_of}; diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 455c96777..567c659e7 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -124,7 +124,7 @@ mod types; pub(crate) use art::join_and; use art::preview_png; -pub use document::{BindingRef, Brushes, PaletteSwatches, VariableRead, artboards, bindings_note, brushes, palette, variables}; +pub use document::{BindingRef, Brushes, PaletteSwatches, VariableRead, artboards, bindings_note, brushes, graphic_styles, palette, variables}; pub(crate) use document::{slot_frame, slot_of}; pub use types::IMAGE_TOO_LARGE; use types::{BLEND_BUDGET, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster}; diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 56e3afff2..d615a09b5 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -361,6 +361,22 @@ pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option { Some(Brushes { brushes: lib, patterns: added, images, note: (unread > 0).then(|| brush::left_out_note(unread)) }) } +/// The graphic styles of the editing data `data`, for a file whose layers aren't read (a graphic +/// style library has none), as [`read`] makes them, with ids from 1, and the data's declared large +/// canvas scale (1 when it declares none). `large_canvas`: as [`read`] takes it. The styles are +/// those the list names before reading stops, and none when the data has no style list or lists no +/// style this reads. +pub fn graphic_styles(data: &[u8], large_canvas: bool) -> (Vec, f64) { + if crate::import::ai::find(data, b"BeginArtStyleList").is_none() { + return (vec![], 1.0); + } + let mut r = Reader::with_names(data); + r.large_canvas = large_canvas.then_some(1.0); + // The styles come before the art, so an error in the art leaves them read. + let _ = r.run(); + (r.graphic_styles().0, r.canvas_scale) +} + /// [`artboards`] as far as reading `data` from its start gets: those read before an error, and /// those of a `/Document` still open when it stopped (its art can be what stopped it). fn artboards_read(data: &[u8]) -> (Vec<(String, Rect)>, Option) { diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index ca5c956d8..f355093ce 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -7,8 +7,8 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect /// 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, with the swatches of the -/// editing copy's palette ([`use_palette`]) and its variable library ([`use_variables`]) when it -/// could be read, and with its layer tree, empty, +/// editing copy's palette ([`use_palette`]), its graphic styles ([`use_styles`]) and its variable +/// library ([`use_variables`]) when it could be read, and with its layer tree, empty, /// below the page's layer when that could be read (#2033). A program in the legacy format without /// layers (`illustrator`, see [`program_alone`]) is read with its art on one layer; when that /// can't be used, the file is `visible` as it was. @@ -40,6 +40,7 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus let have = v.document.patterns.len(); use_palette(data, &mut v.document); use_brushes(data, &mut v.document, have, &mut v.warnings); + use_styles(data, &mut v.document, true, 1.0, None); use_variables(data, &mut v.document, &mut v.warnings); } if tree.is_empty() { @@ -99,11 +100,11 @@ fn layer_tree(nodes: &[Arc]) -> Vec> { /// notes, or a note saying why the layers weren't read when the editing data can't be decompressed /// or its layers can't be read. The flag returned is `true` when the document is the one read from /// the layers; else the document is `visible`, its pages moved by `place_pages` when the editing -/// data was decompressed but its layers can't be read, and its swatches, brushes and variables those -/// of the editing data's palette ([`use_palette`]), brushes ([`use_brushes`]) and variable library -/// ([`use_variables`]) when it was -/// decompressed. `outlined`: the PDF part's type was read as -/// outlines (a file whose type shows then comes in as its PDF part). +/// data was decompressed but its layers can't be read, and its swatches, brushes, graphic styles and +/// variables those of the editing data's palette ([`use_palette`]), brushes ([`use_brushes`]), styles +/// ([`use_styles`]) and variable library ([`use_variables`]) when it was decompressed: a graphic +/// style library, which has no layers, opens with its styles. `outlined`: the PDF part's type was +/// read as outlines (a file whose type shows then comes in as its PDF part). /// `trust`: layers that draw differently from the PDF part are used all the same, with a warning. /// `unit`: the size in points of a unit of the PDF part's user space, its first page's `UserUnit`, /// by which its art came in scaled. Editing data whose artboard matches the page only in those @@ -136,6 +137,7 @@ pub fn layered_ai_from( let have = visible.patterns.len(); use_palette(&data, &mut visible); use_brushes(&data, &mut visible, have, &mut warnings); + use_styles(&data, &mut visible, false, unit, None); use_variables(&data, &mut visible, &mut warnings); return (visible, warnings, false); } @@ -147,6 +149,7 @@ pub fn layered_ai_from( let have = visible.patterns.len(); use_palette(&data, &mut visible); use_brushes(&data, &mut visible, have, &mut warnings); + use_styles(&data, &mut visible, false, unit, Some(&boards)); let mut all = unread(warnings, why); use_variables(&data, &mut visible, &mut all); (visible, all, false) @@ -276,6 +279,62 @@ fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec< warnings.extend(note.filter(|n| !warnings.contains(n))); } +/// `doc` (a file read as its page or PDF part, in place of its layers) with the graphic styles of +/// its editing data `data` in place of the default ones, as [`build`] gives a document read from +/// the layers ([`ai::graphic_styles`], `large_canvas` as it takes it). Unless `large_canvas` is set, +/// their strokes and effects are drawn `unit` times as large when the editing data's artboards are +/// the size of `doc`'s pages in units of `unit` points, else as many times as large as the data's +/// declared large canvas scale when they are the pages' size at that scale ([`board_scale`]). +/// `boards`: the editing data's artboards when they were already read. Left as it is when the +/// editing data has no style this reads. +fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, boards: Option<&[(String, Rect)]>) { + let (mut styles, canvas) = ai::graphic_styles(data, large_canvas); + if styles.is_empty() { + return; + } + if !large_canvas && (unit != 1.0 || canvas != 1.0) { + let read; + let boards = match boards { + Some(b) => b, + None => { + read = ai::artboards(data).0; + &read + } + }; + let k = board_scale(boards, doc, unit).or_else(|| board_scale(boards, doc, canvas)); + if let Some(k) = k.filter(|k| *k != 1.0) { + for g in &mut styles { + g.appearance.scale_strokes(k); + g.appearance.scale_effects(k, vectorcraft_effects::scale_effect); + } + } + } + doc.graphic_styles = styles.into(); +} + +/// How many points a unit of `boards` (the artboards of a file's editing data, in art space) is in +/// `doc` (the file read as its PDF part: a page per artboard): 1 when there are as many artboards as +/// pages, each the size of its page in points, or `unit` when `unit` is above 1 and each is the size +/// of its page in units of `unit` points. `None` otherwise. +fn board_scale(boards: &[(String, Rect)], doc: &Document, unit: f64) -> Option { + if boards.is_empty() || boards.len() != doc.artboards.len() { + return None; + } + let differ = |k: f64| { + boards + .iter() + .zip(&doc.artboards) + .any(|((_, b), a)| (b.width() - a.rect.width() / k).abs() > 0.5 || (b.height() - a.rect.height() / k).abs() > 0.5) + }; + if !differ(1.0) { + Some(1.0) + } else if unit.is_finite() && unit > 1.0 && !differ(unit) { + Some(unit) + } else { + None + } +} + /// The pages of `doc` (a `.ai` read as its PDF part: a page per artboard, in a row) named as /// `boards` (the artboards of its editing data, in art space) name them, and moved, with the art on /// each, to where they are, so the artboards keep their names and layout when the layers can't be @@ -284,22 +343,7 @@ fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec< /// or, when `unit` is above 1, each the size of its page in units of `unit` points: then the layout /// is drawn `unit` times as large. fn place_pages(boards: &[(String, Rect)], active: Option, doc: &mut Document, unit: f64) { - if boards.is_empty() || boards.len() != doc.artboards.len() { - return; - } - let differ = |k: f64| { - boards - .iter() - .zip(&doc.artboards) - .any(|((_, b), a)| (b.width() - a.rect.width() / k).abs() > 0.5 || (b.height() - a.rect.height() / k).abs() > 0.5) - }; - let k = if !differ(1.0) { - 1.0 - } else if unit.is_finite() && unit > 1.0 && !differ(unit) { - unit - } else { - return; - }; + let Some(k) = board_scale(boards, doc, unit) else { return }; for ((name, _), a) in boards.iter().zip(&mut doc.artboards) { a.name.clone_from(name); } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 18593c7a3..33b467bee 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1378,6 +1378,114 @@ fn an_ai_brush_library_without_layers_keeps_its_brushes() { assert!(d.pattern("Dots").is_some_and(|p| !p.art.is_empty()), "the pattern the brush's art paints with"); } +/// An art style that strokes with black at `width` units, after the effects `effects` (filters as +/// the format writes them), as the format writes it. +fn stroke_art_style(name: &str, width: f64, effects: &str) -> String { + format!( + "/KnownStyle :\n({name}) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n{effects}\ + /BasicFilter :\n(Stroke Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n\ + /StrokeStyle : 0 R\n0 0 0 0 0 0 1 XA\n0 J 0 j {width} w 10 M []0 d\n0 XR\n1 0 Xd\n/Def ;\n (StrokeStyle) ,\n; /Dict ;\n /Part ,\n;\n" + ) +} + +/// The art styles and the style list of a graphic style library: Thin, Medium and Thick, listed in +/// that order, 1, 2 and 3 units wide, Medium after an Offset Path of 4 units, and Unlisted, which +/// the list leaves out, with Thin listed twice. +fn style_list() -> String { + let offset = "/BasicFilter :\n(Adobe Offset Path) 1 0 /Filter ,\n1 /Visible ,\n(OffsetPath.aip) /PluginFileName ,\n(Offset Path) /Title ,\n\ + /Dictionary : /NotRecorded ,\n4 /Real (ofst) ,\n6 /Real (mlim) ,\n1 /Int (jntp) ,\n; /Dict ;\n /Part ,\n"; + format!( + "%AI9_BeginArtStyles\n{}{}{}{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n(Thin)\n(Medium)\n(Thick)\n(Thin)\n%AI9_EndArtStyleList\n", + stroke_art_style("Thick", 3.0, ""), + stroke_art_style("Unlisted", 4.0, ""), + stroke_art_style("Thin", 1.0, ""), + stroke_art_style("Medium", 2.0, offset) + ) +} + +/// The offset of each Offset Path in the graphic styles of `d`. +fn style_offsets(d: &Document) -> Vec { + let effects = d.graphic_styles.iter().flat_map(|g| &g.appearance.effects); + effects.filter(|e| e.id == "path.offsetPath").filter_map(|e| e.params["offset"].as_f64()).collect() +} + +/// A graphic style library (the art styles and list of [`style_list`], and no layers) with the +/// editing data's setup `setup` before them. +fn style_library(setup: &str) -> String { + let styles = style_list(); + format!("%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%BeginSetup\n{setup}{styles}%%EndSetup\n%%PageTrailer\n%%Trailer\n") +} + +/// A graphic style library (art styles and their list, no layers) comes in as its PDF part with the +/// library's graphic styles in place of the default ones, in the list's order: Other Library lists them. +#[test] +fn an_ai_graphic_style_library_without_layers_keeps_its_styles() { + let (d, warnings, read) = crate::layered_ai(&private(&style_library("")), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + let styles: Vec<(&str, u32, f64)> = d.graphic_styles.iter().map(|g| (g.name.as_str(), g.id, g.appearance.stroke_width())).collect(); + assert_eq!(styles, [("Thin", 1, 1.0), ("Medium", 2, 2.0), ("Thick", 3, 3.0)], "{warnings:?}"); + assert_eq!(style_offsets(&d), [4.0]); + assert!(warnings.is_empty(), "{warnings:?}"); + // Without a style list, the default styles stay. + let (d, warnings, _) = crate::layered_ai(&private(&native("")), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert_eq!(d.graphic_styles, Document::new(1.0, 1.0).graphic_styles, "{warnings:?}"); +} + +/// A graphic style library whose artboard is its page in the units of a `UserUnit` of 10 comes in +/// with its styles' stroke weights and effects ten times as large, as the page's art is; one whose +/// artboard is the page in points keeps them. Without a `UserUnit`, a Large Canvas library whose +/// artboard is the page at its declared scale of 10 comes in with them ten times as large too. +#[test] +fn an_ai_graphic_style_library_in_the_units_of_a_user_unit_page_comes_in_at_the_pages_size() { + let board = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\ + %_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\ + %_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n"; + let widths = |d: &Document| d.graphic_styles.iter().map(|g| g.appearance.stroke_width()).collect::>(); + let page = in_points(pdf_part(10.0, 86.0), 10.0); + let (d, warnings, read) = crate::layered_ai(&private(&style_library(board)), page, vec![], false, true, 10.0); + assert!(!read, "{warnings:?}"); + assert_eq!(widths(&d), [10.0, 20.0, 30.0], "{warnings:?}"); + assert_eq!(style_offsets(&d), [40.0]); + let points = board.replace("0 100 /Real", "0 1000 /Real").replace("100 0 /Real", "1000 0 /Real"); + let page = in_points(pdf_part(10.0, 86.0), 10.0); + let (d, warnings, _) = crate::layered_ai(&private(&style_library(&points)), page, vec![], false, true, 10.0); + assert_eq!(widths(&d), [1.0, 2.0, 3.0], "{warnings:?}"); + assert_eq!(style_offsets(&d), [4.0]); + let large = format!("%AI24_LargeCanvasScale: 10\n{board}"); + let page = in_points(pdf_part(10.0, 86.0), 10.0); + let (d, warnings, _) = crate::layered_ai(&private(&style_library(&large)), page, vec![], false, true, 1.0); + assert_eq!(widths(&d), [10.0, 20.0, 30.0], "{warnings:?}"); + assert_eq!(style_offsets(&d), [40.0]); +} + +/// An EPS whose editing copy has graphic styles and no layers comes in as its page with those +/// styles, at the page's size when the copy is a Large Canvas one. +#[test] +fn an_eps_without_layers_keeps_its_graphic_styles() { + let r = import(&eps(&page_square(50, 50), &style_library(""))).unwrap(); + let names: Vec<&str> = r.document.graphic_styles.iter().map(|g| g.name.as_str()).collect(); + assert_eq!(names, ["Thin", "Medium", "Thick"], "{:?}", r.warnings); + let r = import(&eps(&page_square(50, 50), &style_library("%AI24_LargeCanvasScale: 10\n"))).unwrap(); + let widths: Vec = r.document.graphic_styles.iter().map(|g| g.appearance.stroke_width()).collect(); + assert_eq!(widths, [10.0, 20.0, 30.0], "{:?}", r.warnings); + assert_eq!(style_offsets(&r.document), [40.0]); +} + +/// A `.ai` that comes in as its PDF part, and an EPS that comes in as its page, because their +/// layers can't be used (their editing data is for another page) get the graphic styles of their +/// editing data's style list in place of the default ones. +#[test] +fn a_file_whose_layers_cant_be_used_keeps_its_graphic_styles() { + let art = native(&format!("{}{}", style_list(), layer("Art", true, &square(10, 10)))).replace(" 0 0 100 100", " 0 0 600 400"); + let names = |d: &Document| d.graphic_styles.iter().map(|g| g.name.clone()).collect::>(); + let (d, warnings, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, false, 1.0); + assert!(!read && warnings.iter().any(|w| w.contains("600 × 400")), "{warnings:?}"); + assert_eq!(names(&d), ["Thin", "Medium", "Thick"], "{warnings:?}"); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + assert!(r.warnings.iter().any(|w| w.contains("600 × 400")), "{:?}", r.warnings); + assert_eq!(names(&r.document), ["Thin", "Medium", "Thick"], "{:?}", r.warnings); +} + /// A PDF part whose tiling has the name of a pattern its editing data defines (the PDF part names /// its tilings `Pattern 1`, `Pattern 2` and so on): the editing data's pattern comes in under a free /// variant of the name, and the palette's swatch and the brush's art paint with it, not with the diff --git a/docs/mcp.md b/docs/mcp.md index 3b3b7eb65..2dffb875f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2338,7 +2338,8 @@ its bounds. the styles unlinked from swatches, with their opacity, blend mode, isolate and knockout, and the patterns they paint with); without `path` it returns `{data}`, and `user: true` saves into the user library folder of the desktop app (category `user`, User Defined). `graphicStyle.loadLibrary {path? | data? | dataBase64?, name?}` loads a `.vcstyles` -file, or another document's graphic styles, as a library to add from; a file in the user library folder, or a file +file, or another document's graphic styles, as a library to add from (a `.ai` or EPS whose editing data holds graphic +styles and no layers gives the styles of its style list, in that order); a file in the user library folder, or a file with the same extension and bytes as one there, loads as that User Defined library. ## Symbol libraries diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8855b0e5a..321f737ef 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1192,6 +1192,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur SVG import keeps attributes in other applications' XML namespaces (`plotter:pen="2"`) on the objects and layers they belong to, as it keeps their `data-*` attributes: copies, duplicates and `.vectorcraft` files keep them, and SVG export writes them back with each namespace declared once on the root (the rest of #522; `crates/svg/tests/it/foreign_attributes.rs`, `tests/attributes.rs`). In a `.ai` or EPS opened from its editing data, type has its text object's opacity and blend mode, whether it is made from the file's text document or taken from the page. Where such type has transparency and comes in with other art (the rest of its look, or page type and strokes that go with it), a group of them has it. Where the page draws its type, or the strokes around it, with transparency that is neither its text object's nor its text object's combined with that of the objects around it, the page's type and strokes keep their own instead (`crates/eps/src/import/tests_native.rs`). Where the page draws type of no text object over text objects made from the text document, with their words in any case, that type is theirs as the page draws it and is left out; the page's other type of no text object goes only to text objects whose type the text document can't make, or else is left out with a warning. `.ai` editing-data import restores the document's listed graphic styles in panel order and links objects that use a listed style; unsupported appearances keep their drawn look (`crates/eps/src/import/ai/tests.rs`). + A `.ai` or EPS graphic style library, whose editing data holds the styles and their list but no layers, opens with those styles in list order. Other Library… loads another document's graphic styles ([Apply graphic styles](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/apply-graphic-styles.html)) and lists such a library's styles in a library panel. A `.ai` or EPS read as its PDF part or page because its layers can't be used also gets the styles of its list in place of the default ones (`crates/eps/src/import/tests_native.rs`, `cmd::fileio::tests_aiimport::a_graphic_style_library_in_a_ai_file_without_layers_loads_its_styles`). A `.ai` or EPS opened from its editing data whose text document makes the type of every text object that shows takes all its type from the text document: each text object stays in its own group, frame and layer, the page's type isn't matched with them, a file of more than 5,000 text objects still opens with its layers, and type the page draws differently (in another font) keeps the layers, with a warning. A line break inside a paragraph stays a line break in it (`crates/eps/src/import/tests_native.rs`, `crates/eps/src/import/ate.rs`). In SVG export without Fewer `` Elements, each character of live type that justification or Mojikumi moves (the letter gaps of a Justify All line without word spaces, the gaps between the characters of a justified Japanese line, closed-up punctuation, the space between Japanese and Latin letters) starts its own positioned ``, and type scaled horizontally on a justified line is stretched to its own width (`crates/svg/tests/it/text_layout.rs`). To find those characters, SVG export lays a type object out a second time only when the second layout can differ from the first: point type with Line-end Punctuation Half Width and no Japanese punctuation or letters, the setting new type gets, is laid out once. Exporting 5,000 such objects takes a median of 109.4 ms instead of 171.5 ms on Windows 11, with the same SVG (`crates/svg/src/export/text/tests.rs`).