diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index fda4ad890..2e7d90aa6 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3427,6 +3427,44 @@ proptest! { survive("hostile Objects on Path", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0).0))?; } } + + /// Brush objects of every kind with any parameters, the brush manager's order with any names, + /// and brush art sections nested or cut short: read with the layers, or as a library without + /// them, the brushes kept within their ranges. + #[test] + fn ai_brushes_never_panic( + kind in 0usize..7, + params in prop::collection::vec(prop_oneof![arb_num(), Just("/".to_string()), Just(" / ".to_string()), Just("nan".to_string()), Just("inf".to_string()), "[A-Za-z ]{0,6}"], 0..40), + order in "[ -~]{0,80}", + nest in 0usize..6, + cut in any::(), + layers in any::(), + ) { + use vectorcraft_testkit::ai; + let kinds = ["Adobe Calligraphic Brush Tool", "Adobe Scatter Brush Tool", "Adobe ArtOnPath Brush Tool", "Adobe PatternOnPath Brush Tool", "Adobe dBrush Brush Tool", "Adobe Brush Manager Order", "Other"]; + let object = |k: &str, name: &str, p: &str| { + let chars: Vec = p.chars().collect(); + let lines: String = chars.chunks(12).map(|c| format!("({}) -\n", c.iter().collect::())).collect(); + format!("%AI8_BeginPluginObject\n({k})\n({name})\n{lines}() .\n%AI8_EndPluginObject\n") + }; + let p = params.join(" "); + let mut art = String::new(); + for _ in 0..nest { + art.push_str("%AI8_BeginBrushPattern\n(A)\n"); + } + art.push_str("0 g\n0 0 m\n4 0 L\n4 4 L\nf\n"); + for _ in 0..nest.saturating_sub(usize::from(cut)) { + art.push_str("%AI8_EndBrushPattern\n"); + } + let records = format!("{art}{}{}", object(kinds[kind], "B", &p), object(kinds[5], kinds[5], &order.replace(['(', ')', '\\'], ""))); + let body = if layers { format!("{records}{}", ai::layer("L", "0 g\n10 10 m\n20 20 L\nS\n")) } else { records }; + let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&ai::editing_data(100.0, 100.0, &body))].concat(); + survive("hostile .ai brushes", || { + let d = vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0; + let _ = vectorcraft_brush::library(&d); + Some(d) + })?; + } } /// The data [`ai_objects_on_path_never_panics`] damages, as it is, opens with its Objects on Path group. diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index ea437a4d1..75a9debf2 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -18,6 +18,7 @@ //! did before (the printed page, or the PDF), with a warning saying why. mod blend; +mod brush; mod envelope; mod frames; mod lex; @@ -35,7 +36,7 @@ use crate::ps; pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; pub(crate) use frames::text_frames; -pub use read::{IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, palette, read, read_beside, variables}; +pub use read::{Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, brushes, palette, read, read_beside, variables}; pub(crate) use read::{join_and, slot_frame, slot_of}; pub(crate) use read::is_look_slot; diff --git a/crates/eps/src/import/ai/brush.rs b/crates/eps/src/import/ai/brush.rs new file mode 100644 index 000000000..f7e1f8893 --- /dev/null +++ b/crates/eps/src/import/ai/brush.rs @@ -0,0 +1,341 @@ +//! Brush definitions (not publicly documented): plug-in objects in the setup, as written in files +//! that users own and sent us, brush library files among them (not presets installed with any +//! other app). +//! +//! - Each object is a `%AI8_BeginPluginObject` section of strings: the plug-in's name, the brush's +//! name, then its parameters, split into strings that `-` continues and `.` ends. +//! - One object, the brush manager's order, has the order of the Brushes panel: its parameters +//! are the plug-in name and brush name of each brush, each one after a `/` and a space. +//! - A Calligraphic brush's parameters are 11 numbers: the first is 1; the diameter, roundness and +//! angle as (low, high) pairs; how the diameter, roundness and angle vary (0 Fixed, 1 Random, 2 +//! Pressure, other codes the pen's other channels); a last number. +//! - A Scatter, Art or Pattern brush's parameters are fields split by `/`: a number, the art's +//! section names (one for Scatter, five for Art and Pattern, empty where there is none, each +//! after a space for Art and Pattern), more fields, and last 15 numbers. +//! - Scatter: the size (2, 3), spacing (5, 6) and scatter (8, 9) as (low, high) factors of the +//! art, the rotation in degrees (11, 12), how each varies (4, 7, 10, 13) and whether the +//! rotation follows the path (14). +//! - Pattern: its scale as a factor (3). +//! - A Bristle brush's parameters start with 8 numbers: two codes, then the stiffness, bristle +//! thickness, density, paint opacity and bristle length as fractions and the size in +//! millimeters. +//! - The art is a `%AI8_BeginBrushPattern` section: its name as a string, then the art (read by +//! [`super::read`]). +//! +//! What isn't read keeps its default: the Art brush's options, the pattern brush's tiles (which +//! slot is which tile isn't known) and every brush's colorization, which is left at None so the +//! art keeps its own colors. + +use std::collections::HashMap; + +use vectorcraft_brush::{ArtBrush, Bristle, Brush, BrushKind, Calligraphic, Colorization, Scatter, Variation}; +use vectorcraft_doc::Node; + +use super::lex::{Lexer, Tok}; +use super::obj; + +/// The brush manager's order: the plug-in name of its object. +pub(super) const MANAGER: &str = "Adobe Brush Manager Order"; // brand-ok: the plug-in name the files use + +/// The plug-in names of the brush kinds. +const TOOLS: [(&str, Tool); 5] = [ + ("Adobe Calligraphic Brush Tool", Tool::Calligraphic), // brand-ok: the plug-in name the files use + ("Adobe Scatter Brush Tool", Tool::Scatter), // brand-ok: the plug-in name the files use + ("Adobe ArtOnPath Brush Tool", Tool::Art), // brand-ok: the plug-in name the files use + ("Adobe PatternOnPath Brush Tool", Tool::Pattern), // brand-ok: the plug-in name the files use + ("Adobe dBrush Brush Tool", Tool::Bristle), // brand-ok: the plug-in name the files use +]; + +/// Most brushes read from one file. +pub(super) const MAX_BRUSHES: usize = 4096; +/// Most brush art sections read from one file (a pattern brush has up to five). +pub(super) const MAX_BRUSH_ART: usize = 5 * MAX_BRUSHES; +/// Most [`art_weight`] copied into the brushes of one file, each brush counting its own copy. +const MAX_BRUSH_ART_NODES: usize = 250_000; + +/// Most bytes of a plug-in object's parameters read (the brush manager's order of [`MAX_BRUSHES`] +/// brushes with long names fits): the rest is left out. +const MAX_PARAMS: usize = 1 << 20; + +/// Millimeters → points. +const MM: f64 = 72.0 / 25.4; + +/// A brush's kind, as its plug-in name gives it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Tool { + Calligraphic, + Scatter, + Art, + Pattern, + Bristle, +} + +/// The kind of a brush whose object's plug-in name is `name`. +pub(super) fn tool(name: &str) -> Option { + TOOLS.iter().find(|(n, _)| *n == name).map(|(_, t)| *t) +} + +/// A brush as its object gives it: its definition, and the names of the art sections it needs. +#[derive(Clone, Debug, PartialEq)] +pub(super) struct BrushDraft { + pub name: String, + pub kind: DraftKind, +} + +#[derive(Clone, Debug, PartialEq)] +pub(super) enum DraftKind { + Calligraphic(Calligraphic), + /// The art is the section named `art`. + Scatter { + brush: Scatter, + art: String, + }, + /// The art is the section named `art`. + Art { + brush: ArtBrush, + art: String, + }, + /// The five art sections the brush names, in its order (which is which tile isn't known), and + /// its scale (%). + Pattern { + tiles: [Option; 5], + scale: f64, + }, + Bristle(Bristle), +} + +impl DraftKind { + pub(super) fn tool(&self) -> Tool { + match self { + DraftKind::Calligraphic(_) => Tool::Calligraphic, + DraftKind::Scatter { .. } => Tool::Scatter, + DraftKind::Art { .. } => Tool::Art, + DraftKind::Pattern { .. } => Tool::Pattern, + DraftKind::Bristle(_) => Tool::Bristle, + } + } +} + +/// Why a brush's object isn't read. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum BrushError { + /// Fewer values than the kind has, or one that isn't a finite number. + Values, + /// No art section named. + NoArt, +} + +/// The strings of a plug-in object's section (`section`: what follows `%AI8_BeginPluginObject`): +/// its plug-in name, its name and its parameters, joined. `None` with fewer than two strings. +pub(super) fn plugin_strings(section: &[u8]) -> Option<(String, String, String)> { + let mut lex = Lexer::new(section); + let mut strings = vec![]; + let mut params = vec![]; + while let Some(t) = lex.next_token() { + if let Tok::Str(s) | Tok::Hex(s) = t.tok { + if strings.len() < 2 { + strings.push(obj::text(&s)); + } else { + // Joined before they are text: a string may end inside a character. + let room = MAX_PARAMS.saturating_sub(params.len()); + params.extend_from_slice(s.get(..room.min(s.len())).unwrap_or_default()); + } + } + } + let [kind, name]: [String; 2] = strings.try_into().ok()?; + Some((kind, name, obj::text(¶ms))) +} + +/// The brush manager's order (`params`: its parameters): the kind and name of each brush. Pairs +/// whose plug-in name isn't a brush kind's are left out; `None` when there are none. +pub(super) fn parse_brush_manager(params: &str) -> Option> { + let fields: Vec<&str> = params.split('/').take(2 * MAX_BRUSHES).map(|f| f.strip_prefix(' ').unwrap_or(f)).collect(); + let mut out = vec![]; + for [kind, name] in fields.as_chunks::<2>().0 { + if let Some(t) = tool(kind) + && out.len() < MAX_BRUSHES + { + out.push((t, (*name).to_string())); + } + } + (!out.is_empty()).then_some(out) +} + +/// The numbers of `s` (split by white space): `None` when one isn't a finite number. +fn numbers(s: &str) -> Option> { + s.split_whitespace().map(|w| w.parse::().ok().filter(|v| v.is_finite())).collect() +} + +/// How a value varies, by its code: `None` for a code that isn't Fixed, Random or Pressure. +fn variation(code: f64) -> Option { + match code { + 0.0 => Some(Variation::Fixed), + 1.0 => Some(Variation::Random), + 2.0 => Some(Variation::Pressure), + _ => None, + } +} + +/// [`variation`], or for another code, Random when the value varies and Fixed when it doesn't. +fn mode(code: f64, varies: bool) -> Variation { + variation(code).unwrap_or(if varies { Variation::Random } else { Variation::Fixed }) +} + +/// The brush named `name` of kind `kind` from its parameters `params`. +pub(super) fn parse_tool_object(kind: Tool, name: &str, params: &str) -> Result { + let kind = match kind { + Tool::Calligraphic => { + let n = numbers(params).ok_or(BrushError::Values)?; + let [_, d0, d1, r0, r1, a0, a1, dm, rm, am, ..] = n.as_slice() else { return Err(BrushError::Values) }; + let mid = |a: f64, b: f64| (a + b) / 2.0; + let half = |a: f64, b: f64| (b - a).abs() / 2.0; + let variation = [half(*a0, *a1), half(*r0, *r1), half(*d0, *d1)]; + let modes = [mode(*am, variation[0] != 0.0), mode(*rm, variation[1] != 0.0), mode(*dm, variation[2] != 0.0)]; + DraftKind::Calligraphic(Calligraphic { + angle: mid(*a0, *a1), + roundness: mid(*r0, *r1), + size: mid(*d0, *d1), + variation, + modes: Some(modes), + }) + } + Tool::Scatter | Tool::Art | Tool::Pattern => { + let fields: Vec<&str> = params.split('/').collect(); + let slots = if kind == Tool::Scatter { 1 } else { 5 }; + if fields.len() < slots + 2 { + return Err(BrushError::Values); + } + let tiles: Vec> = fields + .iter() + .skip(1) + .take(slots) + .map(|f| { + let f = f.strip_prefix(' ').unwrap_or(f); + (!f.is_empty()).then(|| f.to_string()) + }) + .collect(); + let n = fields.last().and_then(|f| numbers(f)).ok_or(BrushError::Values)?; + let [_, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, _] = n.as_slice() else { return Err(BrushError::Values) }; + let art = || tiles.first().cloned().flatten().ok_or(BrushError::NoArt); + match kind { + Tool::Scatter => { + let pct = |a: f64, b: f64| (a * 100.0, b * 100.0); + let values = [pct(*p2, *p3), pct(*p5, *p6), pct(*p8, *p9), (*p11, *p12)]; + let codes = [*p4, *p7, *p10, *p13]; + let modes = [0, 1, 2, 3].map(|i| mode(codes[i], values[i].0 != values[i].1)); + let [size, spacing, scatter, rotation] = values; + let brush = Scatter { + size, + spacing, + scatter, + rotation, + rotation_relative_to_path: *p14 == 1.0, + colorization: Colorization::None, + modes: Some(modes), + ..Scatter::default() + }; + DraftKind::Scatter { brush, art: art()? } + } + Tool::Art => DraftKind::Art { brush: ArtBrush { colorization: Colorization::None, ..ArtBrush::default() }, art: art()? }, + _ => { + let tiles: [Option; 5] = tiles.try_into().map_err(|_| BrushError::Values)?; + DraftKind::Pattern { tiles, scale: p3 * 100.0 } + } + } + } + Tool::Bristle => { + let words: Vec<&str> = params.split_whitespace().take(8).collect(); + let n = numbers(&words.join(" ")).ok_or(BrushError::Values)?; + let [_, _, stiffness, thickness, density, opacity, length, size] = n.as_slice() else { return Err(BrushError::Values) }; + DraftKind::Bristle(Bristle { + stiffness: stiffness * 100.0, + thickness: thickness * 100.0, + density: density * 100.0, + opacity: opacity * 100.0, + length: length * 100.0, + size: size * MM, + ..Bristle::default() + }) + } + }; + Ok(BrushDraft { name: name.to_string(), kind }) +} + +/// The brushes of `drafts`, their art from the sections `art` (by name), in the brush manager's +/// `order` (brushes it doesn't list after those it does, in the order of their objects), each +/// kept within its ranges → the brushes and how many were left out: those whose art isn't read, +/// pattern brushes (which slot holds which tile isn't known), a second brush of the same name, and +/// brushes whose copies of art go past [`MAX_BRUSH_ART_NODES`]. +pub(super) fn finish_brushes(drafts: Vec, art: &HashMap, order: Option<&[(Tool, String)]>) -> (Vec, usize) { + finish_brushes_within(drafts, art, order, MAX_BRUSH_ART_NODES) +} + +/// What copying `art` costs: its nodes, the art of their opacity masks, and the anchors of their paths. +pub(super) fn art_weight(art: &Node) -> usize { + let mut weight = 0usize; + art.walk(&mut |n| { + weight = weight.saturating_add(1 + n.path_data().map_or(0, |p| p.anchor_count())); + if let Some(m) = &n.extras().mask { + weight = weight.saturating_add(art_weight(&m.art)); + } + }); + weight +} + +/// [`finish_brushes`] with at most `budget` of [`art_weight`] copied into the brushes (each brush +/// that uses a section holds its own copy): a brush whose art would go past it is left out. +pub(super) fn finish_brushes_within( + drafts: Vec, + art: &HashMap, + order: Option<&[(Tool, String)]>, + budget: usize, +) -> (Vec, usize) { + let mut left_out = 0; + let mut copied = 0usize; + let mut weights: HashMap<&str, usize> = HashMap::new(); + let mut copy = |name: &str| -> Option { + let (key, a) = art.get_key_value(name)?; + let weight = *weights.entry(key.as_str()).or_insert_with(|| art_weight(a)); + let total = copied.saturating_add(weight); + (total <= budget).then(|| { + copied = total; + a.clone() + }) + }; + let mut made: Vec> = vec![]; + for d in drafts { + let tool = d.kind.tool(); + let repeated = made.iter().flatten().any(|(_, b)| b.name == d.name); + let kind = match d.kind { + _ if repeated => None, + DraftKind::Calligraphic(c) => Some(BrushKind::Calligraphic(c)), + DraftKind::Scatter { brush, art: name } => copy(&name).map(|a| BrushKind::Scatter(Scatter { art: a, ..brush })), + DraftKind::Art { brush, art: name } => copy(&name).map(|a| BrushKind::Art(ArtBrush { art: a, ..brush })), + DraftKind::Pattern { .. } => None, + DraftKind::Bristle(b) => Some(BrushKind::Bristle(b)), + }; + match kind { + Some(kind) => { + let mut b = Brush { name: d.name, kind }; + b.sanitize(); + made.push(Some((tool, b))); + } + _ => left_out += 1, + } + } + let mut out = Vec::with_capacity(made.len()); + for (tool, name) in order.unwrap_or_default() { + if let Some(slot) = made.iter_mut().find(|m| m.as_ref().is_some_and(|(t, b)| t == tool && &b.name == name)) + && let Some((_, b)) = slot.take() + { + out.push(b); + } + } + out.extend(made.into_iter().flatten().map(|(_, b)| b)); + (out, left_out) +} + +/// The note for brushes [`finish_brushes`] left out. +pub(super) fn left_out_note(n: usize) -> String { + format!("brushes VectorCraft can't read yet were left out of the Brushes panel ({n})") +} diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 53555528c..885cc6a7c 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -70,6 +70,9 @@ //! - Swatches: the palette (`%AI5_BeginPalette`), read by [`super::palette`], replaces the //! default swatches. //! +//! - Brushes: the brush plug-in objects and their art sections (`%AI8_BeginBrushPattern`), read by +//! [`super::brush`], replace the default brushes when the file has any. +//! //! - Graphic styles: the style list (`%AI9_BeginArtStyleList`), one `(name)` per style in the //! panel's order, each the name of an art style. The listed styles whose appearance reads //! ([`appearance_from_art_style`](ops::appearance_from_art_style)) replace the default graphic @@ -77,7 +80,7 @@ //! style's appearance and transparency is linked to it. A `1` naming a style the list doesn't //! have is the object's own appearance, not a link. //! -//! Sections the reader doesn't need (brushes, symbols, styles, filters) are skipped; +//! Sections the reader doesn't need (symbols, styles, filters) are skipped; //! type, symbols, fills with a pattern it can't read, placed files without a preview and operators //! it doesn't know are errors. So is data that ends inside a layer, a group or a compound path. @@ -114,7 +117,7 @@ mod types; pub(crate) use art::join_and; use art::preview_png; -pub use document::{PaletteSwatches, VariableRead, artboards, palette, variables}; +pub use document::{Brushes, PaletteSwatches, VariableRead, artboards, brushes, 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}; @@ -341,6 +344,18 @@ struct Reader<'a> { /// The legacy text object being read (`To` … `TO`), and the modal type attributes. text: Option, type_state: legacy_text::TypeState, + /// The file has brush objects (the brush manager's order or a brush). + brush_objects: bool, + /// The brush manager's order. + brush_order: Option>, + /// The brushes read, in the order of their objects. + brush_drafts: Vec, + /// The art of the brushes (`%AI8_BeginBrushPattern`), by name. + brush_art: HashMap, + /// The brush art sections read so far, repeated names included. + brush_art_sections: usize, + /// Brush objects that weren't read. + brushes_unread: usize, /// The graph being read (see [`Reader::graph_record`]). graph: Option, /// The multi-layer mask being read (`Mb` … `MB`), which goes on past a layer's end. @@ -451,6 +466,12 @@ impl<'a> Reader<'a> { v7_patterns: HashSet::new(), text: None, type_state: legacy_text::TypeState::default(), + brush_objects: false, + brush_order: None, + brush_drafts: Vec::new(), + brush_art: HashMap::new(), + brush_art_sections: 0, + brushes_unread: 0, graph: None, multi_mask: None, } diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index cf047363a..e2c9df048 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -1,9 +1,10 @@ +use super::super::brush; use super::art::{four, sane, two}; use super::types::{ FLIP, GState, Kind, MAX_HEX, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED, }; -use super::{Arc, Node, PatternDef, Reader, Rect, Tok, V, marker}; +use super::{Arc, Lexer, Node, PatternDef, Reader, Rect, Tok, V, marker, obj}; impl<'a> Reader<'a> { pub(super) fn push(&mut self, v: V) -> Result<(), String> { @@ -98,6 +99,17 @@ impl<'a> Reader<'a> { self.symbol_bounds(section); return Ok(()); } + // Brushes and their art (those in a pattern's or a brush's art aren't read). + if m == "BeginPluginObject" && self.pattern.is_none() { + let section = self.section("EndPluginObject")?; + self.plugin_object(section); + return Ok(()); + } + if m == "BeginBrushPattern" && self.pattern.is_none() { + let section = self.section("EndBrushPattern")?; + self.define_brush_art(section); + return Ok(()); + } if m == "BeginArtStyles" { let section = self.section("EndArtStyles")?; self.styles = crate::import::ai::style::art_styles(section); @@ -505,6 +517,71 @@ impl<'a> Reader<'a> { } } + /// A plug-in object (`section`: what follows `%AI8_BeginPluginObject`): the brush manager's + /// order or a brush ([`brush`]); other objects are left alone. + fn plugin_object(&mut self, section: &[u8]) { + let Some((kind, name, params)) = brush::plugin_strings(section) else { return }; + if kind == brush::MANAGER { + self.brush_objects = true; + self.brush_order = brush::parse_brush_manager(¶ms); + return; + } + let Some(tool) = brush::tool(&kind) else { return }; + self.brush_objects = true; + if self.brush_drafts.len() >= brush::MAX_BRUSHES { + self.brushes_unread += 1; + return; + } + match brush::parse_tool_object(tool, &name, ¶ms) { + Ok(d) => self.brush_drafts.push(d), + Err(_) => self.brushes_unread += 1, + } + } + + /// A brush's art (`section`: what follows `%AI8_BeginBrushPattern`, its name as a string, then + /// the art), read by a reader of its own as a pattern's is, in a space with y down. Art it can't + /// read whole isn't kept: the brushes that use it aren't read. + fn define_brush_art(&mut self, section: &[u8]) { + if self.brush_art_sections >= brush::MAX_BRUSH_ART || self.subreader_depth >= MAX_SUBREADERS { + return; + } + self.brush_art_sections += 1; + let mut lex = Lexer::new(section); + let Some(Tok::Str(name) | Tok::Hex(name)) = lex.next_token().map(|t| t.tok) else { return }; + let name = obj::text(&name); + let mut sub = Reader::new(lex.slice(lex.offset(), section.len())); + // In a space of its own (y down), with no head to take. + sub.pattern = Some(PatternHead { taken: true, ..PatternHead::default() }); + sub.subreader_depth = self.subreader_depth + 1; + // The gradients and patterns defined so far, lent. + sub.gradients = std::mem::take(&mut self.gradients); + sub.pattern_tiles = std::mem::take(&mut self.pattern_tiles); + sub.nodes = self.nodes; + sub.template = self.template; + let read = sub.open(Kind::Group, false).and_then(|()| sub.run()); + self.nodes = sub.nodes; + self.gradients = std::mem::take(&mut sub.gradients); + self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles); + // Images aren't kept: the document's images don't count a brush's art among their users. Art + // read with a note (something left out of it) isn't kept either. + let whole = read.is_ok() + && sub.unsupported.is_empty() + && sub.unknown.is_empty() + && sub.unread.is_empty() + && sub.hidden_unread.is_empty() + && sub.warnings.is_empty() + && sub.frames.len() == 1 + && sub.doc.images.is_empty(); + let Some(frame) = sub.frames.pop().filter(|f| whole && !f.children.is_empty()) else { return }; + let mut art: Vec> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect(); + let node = match art.len() { + 1 => art.pop().map(Arc::unwrap_or_clone), + _ => Some(Node::group(self.doc.alloc_id(), art)), + }; + let Some(node) = node else { return }; + self.brush_art.insert(name, node); + } + /// In a pattern's definition, the operands of its first operator start with its head, /// `(name) llx lly urx ury`: taken off them. pub(super) fn take_head(&mut self, mut vals: Vec) -> Vec { @@ -717,4 +794,64 @@ mod tests { reader.symbol_bounds(definition.as_bytes()); assert!(reader.symbol_bounds.is_empty()); } + + #[test] + fn brush_art_stops_at_subreader_limit() { + let art = b"(A)\n0 g\n0 0 m\n4 0 L\n4 4 L\nf\n"; + let mut reader = Reader::new(b""); + reader.define_brush_art(art); + assert_eq!(reader.brush_art.len(), 1); + let mut reader = Reader::new(b""); + reader.subreader_depth = MAX_SUBREADERS; + reader.define_brush_art(art); + assert!(reader.brush_art.is_empty()); + } + + #[test] + fn brush_art_sections_stop_at_their_limit_with_repeated_names() { + let mut reader = Reader::new(b""); + reader.brush_art_sections = brush::MAX_BRUSH_ART - 1; + reader.define_brush_art(b"(A)\n0 g\n0 0 m\n4 0 L\n4 4 L\nf\n"); + let first = reader.brush_art.get("A").map(|n| n.id); + assert!(first.is_some()); + // The same name again is past the limit: the first art stays. + reader.define_brush_art(b"(A)\n0 g\n0 0 m\n8 0 L\n8 8 L\nf\n"); + assert_eq!(reader.brush_art.get("A").map(|n| n.id), first); + } + + #[test] + fn brushes_sharing_art_stop_at_the_budget_of_copied_nodes() { + let mut reader = Reader::new(b""); + reader.define_brush_art(b"(A)\n0 g\n0 0 m\n4 0 L\n4 4 L\nf\n"); + let nodes = reader.brush_art.get("A").map(brush::art_weight).unwrap_or_default(); + assert!(nodes > 0); + let draft = |i: usize| brush::BrushDraft { + name: format!("Brush {i}"), + kind: brush::DraftKind::Art { brush: vectorcraft_brush::ArtBrush::default(), art: "A".to_string() }, + }; + let drafts: Vec<_> = (0..5).map(draft).collect(); + let (kept, left_out) = brush::finish_brushes_within(drafts.clone(), &reader.brush_art, None, 3 * nodes); + assert_eq!((kept.len(), left_out), (3, 2)); + let (kept, left_out) = brush::finish_brushes_within(drafts, &reader.brush_art, None, 5 * nodes); + assert_eq!((kept.len(), left_out), (5, 0)); + // A second brush of the same name is left out without using the budget. + let mut repeated: Vec<_> = (0..2).map(draft).collect(); + repeated.insert(1, draft(0)); + let (kept, left_out) = brush::finish_brushes_within(repeated, &reader.brush_art, None, 2 * nodes); + assert_eq!((kept.len(), left_out), (2, 1)); + } + + #[test] + fn brush_art_with_an_unread_object_is_not_kept() { + // A placed file the data keeps no preview of, shown and hidden. + let placed = + "%AI5_BeginPlace\n%%FileType: 1\n[5 0 0 -5 0 200] 10 20 14 22 4 4 0\n(missing.png)`\n%%IncludeFile:missing.png\n~\n%AI5_EndPlace\n"; + let hidden: String = placed.lines().map(|l| format!("%_{l}\n")).collect(); + for placed in [placed, hidden.as_str()] { + let art = format!("(A)\n0 g\n0 0 m\n4 0 L\n4 4 L\nf\n{placed}"); + let mut reader = Reader::new(b""); + reader.define_brush_art(art.as_bytes()); + assert!(reader.brush_art.is_empty(), "{placed:?}"); + } + } } diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 63dc18968..c5a05b915 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -1,3 +1,4 @@ +use super::super::brush; use super::art::{element_name, join_and, sane}; use super::containers::drop_commented_copies; use super::ops::appearance_from_art_style; @@ -171,29 +172,85 @@ fn artboard_array(data: &[u8]) -> Option<&[u8]> { pub struct PaletteSwatches { pub swatches: Vec, pub groups: Vec, - /// The patterns the editing data defines that the given patterns lack and a swatch paints with. + /// The patterns the editing data defines that a swatch paints with, named apart from the given + /// patterns ([`beside`]). pub patterns: Vec, /// The image blobs the art of `patterns` shows, by key. pub images: std::collections::BTreeMap, } +/// The editing data's patterns `defined`, for a document that has the patterns `have` (those of a +/// file's PDF part or page): each keeps its name, except one whose name a pattern of `have` already +/// has (the PDF part names its tilings `Pattern 1`, `Pattern 2` and so on), which takes a free +/// variant (`Pattern 1 2`). The art of each pattern paints with the names they got. Also the names +/// changed, old to new. The same `defined` and `have` always give the same names. +fn beside(mut defined: Vec, have: &[PatternDef]) -> (Vec, std::collections::BTreeMap) { + let mut renamed = std::collections::BTreeMap::new(); + let mut taken: std::collections::BTreeSet = have.iter().chain(&defined).map(|p| p.name.clone()).collect(); + let tries = taken.len() + 2; + for d in &mut defined { + if !have.iter().any(|p| p.name == d.name) { + continue; + } + let Some(name) = (2..=tries).map(|i| format!("{} {i}", d.name)).find(|n| !taken.contains(n)) else { continue }; + taken.insert(name.clone()); + renamed.insert(std::mem::replace(&mut d.name, name.clone()), name); + } + if !renamed.is_empty() { + for d in &mut defined { + for a in &mut d.art { + repoint(Arc::make_mut(a), &renamed); + } + } + } + (defined, renamed) +} + +/// `n` and what it holds painting with the patterns `renamed` renames (old to new) by their new names. +fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap) { + for p in vectorcraft_doc::swatches::node_paints_mut(n) { + if let Paint::Pattern { pattern, .. } = p + && let Some(new) = renamed.get(pattern) + { + pattern.clone_from(new); + } + } + if let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_mut()) { + repoint(Arc::make_mut(&mut mask.art), renamed); + } + if let Some(children) = n.children_mut() { + for c in children { + repoint(Arc::make_mut(c), renamed); + } + } +} + /// The swatches and color groups of the palette of the editing data `data`, for a file whose /// layers aren't read: the swatches its PDF part has are the defaults (#4036). They come as /// [`read`] makes them, with pattern swatches only for the patterns in `patterns` and those the -/// editing data defines; `None` when the palette is empty or reading stops before it. +/// editing data defines, which are named apart from `patterns` ([`beside`]); `None` when the +/// palette is empty or reading stops before it. pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option { let mut r = Reader::new(data); // The palette, the gradients and the patterns it names come before the art, so an error in the // art leaves them read. let _ = r.run(); + let (defined, renamed) = beside(std::mem::take(&mut r.doc.patterns), patterns); let mut known = patterns.to_vec(); - let defined = std::mem::take(&mut r.doc.patterns); - known.extend(defined.iter().filter(|d| !patterns.iter().any(|p| p.name == d.name)).cloned()); - let (swatches, groups) = r.palette_swatches(&known)?; + known.extend(defined.iter().cloned()); + let (mut swatches, mut groups) = r.palette_swatches(&known)?; + // A swatch of a pattern the editing data defines paints with that pattern, under the name it got. + for s in swatches.iter_mut().chain(groups.iter_mut().flat_map(|g| &mut g.swatches)) { + if let Paint::Pattern { pattern, .. } = &mut s.paint + && let Some(new) = renamed.get(pattern) + { + pattern.clone_from(new); + } + } let painted = |name: &str| { swatches.iter().chain(groups.iter().flat_map(|g| &g.swatches)).any(|s| matches!(&s.paint, Paint::Pattern { pattern, .. } if pattern == name)) }; - let added: Vec = defined.into_iter().filter(|d| !patterns.iter().any(|p| p.name == d.name) && painted(&d.name)).collect(); + let added: Vec = defined.into_iter().filter(|d| painted(&d.name)).collect(); let mut shown = Document::new(1.0, 1.0); shown.patterns.clone_from(&added); let mut images = std::collections::BTreeMap::new(); @@ -205,6 +262,61 @@ pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option Some(PaletteSwatches { swatches, groups, patterns: added, images }) } +/// The brushes of a `.ai` file's editing data, as [`brushes`] reads them. +pub struct Brushes { + pub brushes: Vec, + /// The patterns the editing data defines that the brushes' art paints with, named apart from + /// the given patterns ([`beside`]). + pub patterns: Vec, + /// The image blobs the art of `patterns` shows, by key. + pub images: std::collections::BTreeMap, + /// The note for the brushes left out. + pub note: Option, +} + +/// The brushes of the editing data `data`, for a file whose layers aren't read (a brush library +/// has none), as [`read`] makes them, with the patterns of the editing data their art paints with, +/// named apart from `patterns` ([`beside`]) as [`palette`] names them. `None` when the file has no +/// brush objects or reading stops before them. +pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option { + // Brush objects are plug-in objects: data without one isn't read again. + memchr::memmem::find(data, b"BeginPluginObject")?; + let mut r = Reader::new(data); + // The brushes come before the art, so an error in the art leaves them read. + let _ = r.run(); + let (mut lib, unread) = r.brush_library()?; + let (defined, renamed) = beside(std::mem::take(&mut r.doc.patterns), patterns); + if !renamed.is_empty() { + for a in lib.iter_mut().flat_map(|b| b.art_mut()) { + repoint(a, &renamed); + } + } + // The patterns the brushes' art paints with, and those their art paints with, and so on. + let mut stack: Vec<&vectorcraft_doc::Node> = lib.iter().flat_map(|b| b.art()).collect(); + let mut used = std::collections::BTreeSet::new(); + while let Some(n) = stack.pop() { + stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| &**c)); + for p in vectorcraft_doc::swatches::node_paints(n) { + if let Paint::Pattern { pattern, .. } = p + && used.insert(pattern.clone()) + && let Some(d) = defined.iter().find(|d| &d.name == pattern) + { + stack.extend(d.art.iter().map(|a| &**a)); + } + } + } + let added: Vec = defined.into_iter().filter(|d| used.contains(&d.name)).collect(); + let mut shown = Document::new(1.0, 1.0); + shown.patterns.clone_from(&added); + let mut images = std::collections::BTreeMap::new(); + shown.visit_images(|_, im| { + if let Some(blob) = r.doc.images.get(&im.key) { + images.insert(im.key.clone(), blob.clone()); + } + }); + Some(Brushes { brushes: lib, patterns: added, images, note: (unread > 0).then(|| brush::left_out_note(unread)) }) +} + /// [`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)> { @@ -538,6 +650,17 @@ impl<'a> Reader<'a> { Some((swatches, groups)) } + /// The brushes read ([`brush::finish_brushes`]) and how many weren't; `None` when the file has + /// no brush objects (the document keeps the default brushes). + fn brush_library(&mut self) -> Option<(Vec, usize)> { + if !self.brush_objects { + return None; + } + let drafts = std::mem::take(&mut self.brush_drafts); + let (lib, left_out) = brush::finish_brushes(drafts, &self.brush_art, self.brush_order.as_deref()); + Some((lib, left_out + self.brushes_unread)) + } + /// The graphic styles of the style list, in its order: each listed name the art styles define with /// an appearance [`appearance_from_art_style`] reads, once (a name listed again, empty or not /// defined is left out), with ids from 1. Also the id of each kept name. @@ -691,6 +814,7 @@ impl<'a> Reader<'a> { doc.swatches.push(Swatch { paint: Paint::Pattern { pattern: name.clone(), xf: Affine::IDENTITY }, name, global: false, spot: false }); } } + let brushes = self.brush_library(); // The file's graphic styles in place of the defaults, and the objects linked to them. let (styles, ids) = self.graphic_styles(); let styles_read = !styles.is_empty(); @@ -702,6 +826,12 @@ impl<'a> Reader<'a> { doc.graphic_styles = styles.into(); } let mut warnings = self.warnings; + if let Some((lib, unread)) = brushes { + vectorcraft_brush::store(&mut doc, &lib); + if unread > 0 { + warnings.push(brush::left_out_note(unread)); + } + } if self.drawn_looks > 0 { warnings.push(format!( "objects with several fills or strokes, effects or brushes came in as their drawn look ({}): their appearance isn't editable yet", diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 152f6afa8..cbe7966e7 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -4959,6 +4959,186 @@ fn an_objects_on_path_group_in_versioned_content_opens_once() { assert!(kids[0].extras().objects_on_path.is_some()); } +// ---- brushes ------------------------------------------------------------------------------ + +const CALLIGRAPHIC: &str = "Adobe Calligraphic Brush Tool"; +const SCATTER: &str = "Adobe Scatter Brush Tool"; +const ART_BRUSH: &str = "Adobe ArtOnPath Brush Tool"; +const PATTERN_BRUSH: &str = "Adobe PatternOnPath Brush Tool"; +const BRISTLE: &str = "Adobe dBrush Brush Tool"; + +/// A plug-in object of plug-in `kind` named `name`, its parameters `params` split into strings of +/// 16 characters at most (`-` after each but the last, `.` after that). +fn plugin_object(kind: &str, name: &str, params: &str) -> String { + let chars: Vec = params.chars().collect(); + let chunks: Vec = chars.chunks(16).map(|c| c.iter().collect()).collect(); + let n = chunks.len(); + let lines: String = chunks.iter().enumerate().map(|(i, c)| format!("({c}) {}\n", if i + 1 == n { "." } else { "-" })).collect(); + format!("%AI5_Begin_NonPrinting\nNp\n%AI8_BeginPluginObject\n({kind})\n({name})\n{lines}%AI8_EndPluginObject\n%AI5_End_NonPrinting--\n") +} + +/// The brush manager's order of the brushes `brushes` (plug-in, name). +fn brush_order(brushes: &[(&str, &str)]) -> String { + let params: String = brushes.iter().map(|(k, n)| format!(" {k}/ {n}/")).collect(); + plugin_object(brush::MANAGER, brush::MANAGER, ¶ms) +} + +/// A brush's art named `name`: a filled square of side `side`. +fn brush_art(name: &str, side: f64) -> String { + format!("%AI8_BeginBrushPattern\n({name})\n0 A\n0 g\n{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, side, side)) +} + +/// A Calligraphic brush with diameter 2 to 6, roundness 40 to 60 and angle -30 to 10, the diameter +/// varying at random, the roundness fixed and the angle with the pen's pressure. +const NIB: &str = "1 2 6 40 60 -30 10 1 0 2 0"; +/// A Scatter brush of the art `Dot art`: size 50% to 150% at random, spacing 120% to 200% fixed, +/// scatter -25% to 25% with the pressure, rotation -45° to 30° at random and following the path. +const SPRAY: &str = "2 /Dot art/ 5 1 0 0 / 0 0.5 1.5 1 1.2 2 0 -0.25 0.25 2 -45 30 1 1 0"; +/// An Art brush of the art `Ribbon art`. +const RIBBON: &str = "2 / Ribbon art/ / / / / 0 0/ 5 0 0 0 / 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1"; +/// A Pattern brush of four tiles (the fourth slot empty) at 80%. +const FENCE: &str = "2 / Tile one/ Tile two/ Tile three/ / Tile five/ 0 0/ 0 1 / 1 1 0.8 0 4 0 0 0 0.8 0.8 0 0 0 0 0"; +/// A Bristle brush: stiffness 60%, thickness 40%, density 30%, opacity 80%, length 150%, size 2 mm. +const MOP: &str = "0 4 0.6 0.4 0.3 0.8 1.5 2 library shape"; + +/// The brushes of the five kinds, their art and the manager's order (the Bristle brush first). +fn five_brushes() -> String { + let order = brush_order(&[(BRISTLE, "Mop"), (CALLIGRAPHIC, "Nib"), (SCATTER, "Spray"), (ART_BRUSH, "Ribbon"), (PATTERN_BRUSH, "Fence")]); + let art: String = ["Dot art", "Ribbon art", "Tile one", "Tile two", "Tile three", "Tile five"].iter().map(|n| brush_art(n, 4.0)).collect(); + let objects = [ + (CALLIGRAPHIC, "Nib", NIB), + (SCATTER, "Spray", SPRAY), + (ART_BRUSH, "Ribbon", RIBBON), + (PATTERN_BRUSH, "Fence", FENCE), + (BRISTLE, "Mop", MOP), + ]; + let objects: String = objects.iter().map(|(k, n, p)| plugin_object(k, n, p)).collect(); + format!("{art}{order}{objects}") +} + +fn brush_named<'a>(lib: &'a [vectorcraft_brush::Brush], name: &str) -> &'a vectorcraft_brush::BrushKind { + match lib.iter().find(|b| b.name == name) { + Some(b) => &b.kind, + None => panic!("no brush {name} in {:?}", lib.iter().map(|b| &b.name).collect::>()), + } +} + +/// The brush manager's order gives the kind and name of each brush, in the panel's order. +#[test] +fn the_brush_managers_order_gives_each_brushs_kind_in_order() { + let params = format!(" {BRISTLE}/ Mop/ {CALLIGRAPHIC}/ Nib/ {SCATTER}/ Spray/ {ART_BRUSH}/ Ribbon/ {PATTERN_BRUSH}/ Fence /"); + let order = brush::parse_brush_manager(¶ms).unwrap(); + use brush::Tool; + let want = [(Tool::Bristle, "Mop"), (Tool::Calligraphic, "Nib"), (Tool::Scatter, "Spray"), (Tool::Art, "Ribbon"), (Tool::Pattern, "Fence ")]; + assert_eq!(order, want.map(|(t, n)| (t, n.to_string())), "a name keeps its trailing spaces"); + assert_eq!(brush::parse_brush_manager(" Some Other Plugin/ Thing/"), None); +} + +/// A file's brushes replace the default ones, in the manager's order, with their values. A pattern +/// brush is left out (which slot holds which tile isn't known), with a note. +#[test] +fn a_files_brushes_come_in_with_their_values_in_the_panels_order() { + let r = read_ok(&format!("{}{}", five_brushes(), layer(&(rect_path(0.0, 0.0, 10.0, 10.0) + "f\n")))); + let lib = vectorcraft_brush::library(&r.doc); + let names: Vec<&str> = lib.iter().map(|b| b.name.as_str()).collect(); + assert_eq!(names, ["Mop", "Nib", "Spray", "Ribbon"], "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w == "brushes VectorCraft can't read yet were left out of the Brushes panel (1)"), "{:?}", r.warnings); + use vectorcraft_brush::{BrushKind, Variation}; + let BrushKind::Calligraphic(c) = brush_named(&lib, "Nib") else { panic!("calligraphic") }; + assert_eq!((c.angle, c.roundness, c.size), (-10.0, 50.0, 4.0)); + assert_eq!(c.variation, [20.0, 10.0, 2.0]); + assert_eq!(c.modes, Some([Variation::Pressure, Variation::Fixed, Variation::Random])); + let BrushKind::Scatter(s) = brush_named(&lib, "Spray") else { panic!("scatter") }; + assert_eq!((s.size, s.spacing, s.scatter, s.rotation), ((50.0, 150.0), (120.0, 200.0), (-25.0, 25.0), (-45.0, 30.0))); + assert_eq!(s.modes, Some([Variation::Random, Variation::Fixed, Variation::Pressure, Variation::Random])); + assert!(s.rotation_relative_to_path); + assert_eq!(s.art.geometric_bounds().map(|b| (b.width(), b.height())), Some((4.0, 4.0)), "the art of its section"); + let BrushKind::Art(a) = brush_named(&lib, "Ribbon") else { panic!("art") }; + assert!(a.art.geometric_bounds().is_some(), "the art of its section"); + assert_eq!(a.colorization, vectorcraft_brush::Colorization::None, "the art keeps its colors"); + let BrushKind::Bristle(b) = brush_named(&lib, "Mop") else { panic!("bristle") }; + let got = [b.stiffness, b.thickness, b.density, b.opacity, b.length, b.size]; + let want = [60.0, 40.0, 30.0, 80.0, 150.0, 2.0 * 72.0 / 25.4]; + assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-9), "{got:?}"); +} + +/// A Pattern brush's five slots name its art sections, in its order, an empty slot naming none; +/// its scale comes in as a percentage. +#[test] +fn a_pattern_brushs_five_slots_are_kept() { + let d = brush::parse_tool_object(brush::Tool::Pattern, "Fence", FENCE).unwrap(); + let brush::DraftKind::Pattern { tiles, scale } = d.kind else { panic!("{d:?}") }; + let s = |n: &str| Some(n.to_string()); + assert_eq!(tiles, [s("Tile one"), s("Tile two"), s("Tile three"), None, s("Tile five")]); + assert!((scale - 80.0).abs() < 1e-9); + let d = brush::parse_tool_object(brush::Tool::Scatter, "Spray", SPRAY).unwrap(); + assert!(matches!(d.kind, brush::DraftKind::Scatter { ref art, .. } if art == "Dot art"), "{d:?}"); + let d = brush::parse_tool_object(brush::Tool::Art, "Ribbon", RIBBON).unwrap(); + assert!(matches!(d.kind, brush::DraftKind::Art { ref art, .. } if art == "Ribbon art"), "{d:?}"); +} + +/// A brush's art filled with a pattern the file defines before it is read, with that pattern. +#[test] +fn a_brushs_art_takes_the_patterns_defined_before_it() { + let dots = "%AI3_BeginPattern: (Dots)\n(Dots) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n"; + let art_ = format!( + "%AI8_BeginBrushPattern\n(Dot art)\n0 A\n(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n%AI8_EndBrushPattern\n", + rect_path(0.0, 0.0, 4.0, 4.0) + ); + let r = read_ok(&format!("{dots}{art_}{}{}", plugin_object(SCATTER, "Spray", SPRAY), layer(""))); + let lib = vectorcraft_brush::library(&r.doc); + let vectorcraft_brush::BrushKind::Scatter(s) = brush_named(&lib, "Spray") else { panic!("scatter") }; + assert!(matches!(fill(&s.art), Paint::Pattern { ref pattern, .. } if pattern == "Dots"), "{:?}", r.warnings); +} + +/// A file without brush objects keeps the default brushes. +#[test] +fn a_file_without_brushes_keeps_the_default_ones() { + let r = read_ok(&layer(&(rect_path(0.0, 0.0, 10.0, 10.0) + "f\n"))); + assert!(!r.doc.unknown.contains_key(vectorcraft_brush::DOC_KEY)); +} + +/// A brush whose object can't be read, or whose art is missing or can't be read, is left out with a +/// note; the layers and the other brushes still come in. +#[test] +fn a_brush_that_cant_be_read_leaves_the_layers_and_other_brushes() { + let bad_art = "%AI8_BeginBrushPattern\n(Odd art)\n0 g\n0 0 m\n1 1 L\nnotanoperator\nf\n%AI8_EndBrushPattern\n"; + let objects = [ + plugin_object(CALLIGRAPHIC, "Nib", NIB), + plugin_object(CALLIGRAPHIC, "Short", "1 2 3"), + plugin_object(CALLIGRAPHIC, "Huge", "1 2 inf 40 60 -30 10 1 0 2 0"), + plugin_object(SCATTER, "Odd", &SPRAY.replace("Dot art", "Odd art")), + plugin_object(SCATTER, "Lost", &SPRAY.replace("Dot art", "Nowhere")), + plugin_object(ART_BRUSH, "Bare", "2 / / / / / / 0 0/ 5 0 0 0 / 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1"), + plugin_object(BRISTLE, "Nib", MOP), + ] + .concat(); + let r = read_ok(&format!("{bad_art}{objects}{}", layer(&(rect_path(0.0, 0.0, 10.0, 10.0) + "f\n")))); + assert_eq!(art(&r.doc).len(), 1, "the layer's art"); + let lib = vectorcraft_brush::library(&r.doc); + assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::>(), ["Nib"], "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.ends_with("Brushes panel (6)")), "{:?}", r.warnings); +} + +/// Files with brush objects and no brushes VectorCraft reads get an empty list, not the defaults. +#[test] +fn a_file_whose_brushes_are_all_left_out_has_none() { + let r = read_ok(&format!("{}{}", plugin_object(PATTERN_BRUSH, "Fence", FENCE), layer(""))); + assert!(vectorcraft_brush::library(&r.doc).is_empty()); +} + +/// A gradient's `Br` (a stop's ramp) isn't a brush: the gradient keeps its stops. +#[test] +fn a_gradients_br_is_not_a_brush() { + let gradient = + "%AI5_BeginGradient: (G)\n(G) 0 2 Bd\n[\n0 0 0 0 1 1 1 2 1 6 50 0 Bs\n0 0 0 1 0 0 0 2 0.5 6 50 100 Bs\n0 Br\nBD\n%AI5_EndGradient\n"; + let art_ = format!("{}Bb\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n10 0 0 10 100 50 Xm\nf\n0 BB\n", rect_path(0.0, 0.0, 20.0, 20.0)); + let r = read_ok(&format!("{gradient}{}", layer(&art_))); + assert!(!r.doc.unknown.contains_key(vectorcraft_brush::DOC_KEY)); + let Paint::Gradient(g) = fill(&art(&r.doc)[0]) else { panic!("a gradient: {:?}", r.warnings) }; + assert_eq!(g.gradient.stops.len(), 2); +} + use read::{GRAPH_PLAIN, GRAPH_UNENDED}; /// Editing data in the legacy format (version 7 specification, chapter 2): `%AI5_FileFormat 2.0`, diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index a8bbd0837..f5f769380 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -37,7 +37,9 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus Err(why) => { let mut v = visible; if let Ok(data) = &data { + let have = v.document.patterns.len(); use_palette(data, &mut v.document); + use_brushes(data, &mut v.document, have, &mut v.warnings); use_variables(data, &mut v.document, &mut v.warnings); } if tree.is_empty() { @@ -97,8 +99,9 @@ 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 and variables those of the -/// editing data's palette ([`use_palette`]) and variable library ([`use_variables`]) when it was +/// 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). /// `trust`: layers that draw differently from the PDF part are used all the same, with a warning. @@ -117,15 +120,19 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec, outl }; if !has_layers(&data) { let (mut visible, mut warnings) = (visible, warnings); + let have = visible.patterns.len(); use_palette(&data, &mut visible); + use_brushes(&data, &mut visible, have, &mut warnings); use_variables(&data, &mut visible, &mut warnings); return (visible, warnings, false); } // The PDF part, its pages placed and named as the editing data's artboards are, when the // layers can't be used (`why`). - let from_pdf_part = |mut visible: Document, warnings: Vec, why: &str| { + let from_pdf_part = |mut visible: Document, mut warnings: Vec, why: &str| { place_pages(&ai::artboards(&data), &mut visible, unit); + let have = visible.patterns.len(); use_palette(&data, &mut visible); + use_brushes(&data, &mut visible, have, &mut warnings); let mut all = unread(warnings, why); use_variables(&data, &mut visible, &mut all); (visible, all, false) @@ -161,8 +168,9 @@ fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec) { /// `doc` (a `.ai` read as its PDF part, in place of its layers) with the swatches and color groups /// of the palette of its editing data `data` in place of the default ones, as [`build`] gives a /// document read from the layers. The spot and pattern swatches of the PDF part's art that the -/// palette doesn't have follow the palette's swatches. The patterns that the editing data defines and -/// the PDF part lacks come in for the palette's pattern swatches, with the images they show. Left as +/// palette doesn't have follow the palette's swatches. The patterns that the editing data defines come +/// in for the palette's pattern swatches, with the images they show; one whose name a pattern of +/// the PDF part already has takes a free variant of it ([`ai::palette`]). Left as /// it is when the palette is empty. fn use_palette(data: &[u8], doc: &mut Document) { let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns) else { return }; @@ -181,6 +189,29 @@ fn use_palette(data: &[u8], doc: &mut Document) { (doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into()); } +/// `doc` (a file read as its page or PDF part, in place of its layers) with the brushes of its +/// editing data `data` in place of the default ones, as [`build`] gives a document read from the +/// layers, the patterns of the editing data their art paints with (named apart from the first +/// `have` patterns of `doc`, those of the page or PDF part, as [`use_palette`] names them, which +/// may have added some of them already), and the note for the brushes left out added to +/// `warnings`. Left as it is when the editing data has no brushes. +fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec) { + let page = doc.patterns.get(..have).unwrap_or(&doc.patterns); + let Some(ai::Brushes { brushes, patterns, images, note }) = ai::brushes(data, page) else { return }; + for (key, blob) in images { + doc.images.entry(key).or_insert(blob); + } + for mut p in patterns { + if doc.pattern(&p.name).is_some() { + continue; + } + p.art = p.art.iter().map(|n| Arc::new(doc.reid(n))).collect(); + doc.patterns.push(p); + } + vectorcraft_brush::store(doc, &brushes); + warnings.extend(note.filter(|n| !warnings.contains(n))); +} + /// 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 diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index dd1848266..e553712e9 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1344,6 +1344,100 @@ fn an_ai_file_without_layers_keeps_the_patterns_of_its_palette() { assert!(keys.iter().all(|k| d.images.contains_key(k)), "{keys:?}"); } +/// A brush library (brush objects and their art, no layers) comes in as its PDF part with the +/// library's brushes in place of the default ones: Other Library lists them. +#[test] +fn an_ai_brush_library_without_layers_keeps_its_brushes() { + let order = "%AI8_BeginPluginObject\n(Adobe Brush Manager Order)\n(Adobe Brush Manager Order)\n\ + ( Adobe Scatter Brush Tool/ Spray/ Adobe Calli) -\n(graphic Brush Tool/ Nib/) .\n%AI8_EndPluginObject\n"; + let nib = "%AI8_BeginPluginObject\n(Adobe Calligraphic Brush Tool)\n(Nib)\n(1 2 6 40 60 -30 10 1 0 2 0) .\n%AI8_EndPluginObject\n"; + let spray = "%AI8_BeginPluginObject\n(Adobe Scatter Brush Tool)\n(Spray)\n(2 /Dot art/ 5 1 0 0 / 0 0.5 1.5 1 ) -\n\ + (1.2 2 0 -0.25 0.25 2 -45 30 1 1 0) .\n%AI8_EndPluginObject\n"; + // The art is filled with a pattern no swatch paints with: it comes in with the brush. + let dots = "%AI3_BeginPattern: (Dots)\n(Dots) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n"; + let dot = "%AI8_BeginBrushPattern\n(Dot art)\n(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n0 0 m\n4 0 L\n4 4 L\nf\n%AI8_EndBrushPattern\n"; + let library = + format!("%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 0 0\n%%BeginSetup\n{dots}{dot}{order}{nib}{spray}%%EndSetup\n%%PageTrailer\n%%Trailer\n"); + let (d, warnings, read) = crate::layered_ai(&private(&library), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + let lib = vectorcraft_brush::library(&d); + assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::>(), ["Spray", "Nib"], "{warnings:?}"); + assert!(warnings.is_empty(), "{warnings:?}"); + assert!(d.pattern("Dots").is_some_and(|p| !p.art.is_empty()), "the pattern the brush's art paints with"); +} + +/// 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 +/// tiling. +#[test] +fn an_ai_file_without_layers_keeps_its_patterns_apart_from_the_pdf_parts_tilings() { + let dots = "%AI3_BeginPattern: (Pattern 1)\n(Pattern 1) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n"; + let palette = "%AI5_BeginPalette\n0 0 Pb\n(Pattern 1) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Dots)\nPc\nPB\n%AI5_EndPalette\n"; + let order = "%AI8_BeginPluginObject\n(Adobe Brush Manager Order)\n(Adobe Brush Manager Order)\n( Adobe Scatter Brush Tool/ Spray/) .\n%AI8_EndPluginObject\n"; + let spray = "%AI8_BeginPluginObject\n(Adobe Scatter Brush Tool)\n(Spray)\n(2 /Dot art/ 5 1 0 0 / 0 0.5 1.5 1 ) -\n\ + (1.2 2 0 -0.25 0.25 2 -45 30 1 1 0) .\n%AI8_EndPluginObject\n"; + let dot = "%AI8_BeginBrushPattern\n(Dot art)\n(Pattern 1) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n0 0 m\n4 0 L\n4 4 L\nf\n%AI8_EndBrushPattern\n"; + let part = || { + let mut part = pdf_part(10.0, 86.0); + let id = part.alloc_id(); + let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 9.0, 9.0)); + let tiling = std::sync::Arc::new(Node::path(id, square, vectorcraft_doc::Appearance::default())); + let tile = vectorcraft_geom::Rect::new(0.0, 0.0, 9.0, 9.0); + part.patterns.push(vectorcraft_doc::PatternDef { tile, ..vectorcraft_doc::PatternDef::new("Pattern 1", vec![tiling]) }); + part + }; + let width = |d: &Document, name: &str| d.pattern(name).map(|p| p.tile.width()); + let swatch_pattern = |d: &Document| match d.swatch("Dots").map(|s| &s.paint) { + Some(vectorcraft_color::Paint::Pattern { pattern, .. }) => Some(pattern.clone()), + _ => None, + }; + fn painted(n: &Node, out: &mut Vec) { + for p in vectorcraft_doc::swatches::node_paints(n) { + if let vectorcraft_color::Paint::Pattern { pattern, .. } = p { + out.push(pattern.clone()); + } + } + for c in n.children().into_iter().flatten() { + painted(c, out); + } + } + let brushed = |d: &Document| { + let mut out = vec![]; + for b in vectorcraft_brush::library(d) { + for a in b.art() { + painted(a, &mut out); + } + } + out + }; + + // The palette alone. + let art = native(&format!("{dots}{palette}")); + let (d, warnings, read) = crate::layered_ai(&private(&art), part(), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + assert_eq!(width(&d, "Pattern 1"), Some(9.0), "the PDF part's tiling stays"); + assert_eq!(swatch_pattern(&d).as_deref(), Some("Pattern 1 2"), "{warnings:?}"); + assert_eq!(width(&d, "Pattern 1 2"), Some(4.0)); + + // A brush library: the brush's art paints with the editing data's pattern. + let library = format!("%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 0 0\n%%BeginSetup\n{dots}{dot}{order}{spray}%%EndSetup\n%%PageTrailer\n%%Trailer\n"); + let (d, warnings, read) = crate::layered_ai(&private(&library), part(), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + assert_eq!(brushed(&d), ["Pattern 1 2"], "{warnings:?}"); + assert_eq!(width(&d, "Pattern 1"), Some(9.0)); + assert_eq!(width(&d, "Pattern 1 2"), Some(4.0)); + + // Both: the palette's swatch and the brush paint with the one pattern, added once. + let both = + format!("%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 0 0\n%%BeginSetup\n{dots}{palette}{dot}{order}{spray}%%EndSetup\n%%PageTrailer\n%%Trailer\n"); + let (d, warnings, _) = crate::layered_ai(&private(&both), part(), vec![], false, true, 1.0); + assert_eq!(swatch_pattern(&d).as_deref(), Some("Pattern 1 2"), "{warnings:?}"); + assert_eq!(brushed(&d), ["Pattern 1 2"], "{warnings:?}"); + let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect(); + assert_eq!(names, ["Pattern 1", "Pattern 1 2"]); +} + /// A `.ai` that comes in as its PDF part, whose layers can't be used or that has none, gets the /// variables, data sets, active data set and lock of its editing data's variable library, and so /// does an EPS that comes in as its page. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a69bd4f0d..fd683d3ce 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4500,6 +4500,7 @@ axis Both И двете @msg it has {_1}, which VectorCraft doesn't read from it yet той съдържа {_1}, който VectorCraft все още не може да разчете @msg its objects are nested too deeply обектите му са вложени твърде дълбоко @msg its section ending with `%{end}` doesn't end разделът му, завършващ на `%{end}`, няма край +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) четките, които VectorCraft все още не може да прочете, бяха пропуснати от панела „Четки“ ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet обектите с множество запълвания или щрихове, ефекти или четки се появиха във вид на изрисувания си изглед ({_1}): тяхната поява все още не подлежи на редактиране @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиентът „{name}“ не е дефиниран във файла: заменен е с обикновен цвят @msg an image VectorCraft can't read was left out изображение, което VectorCraft не може да разчете, беше пропуснато diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 212e28f2b..c785d37dd 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4899,6 +4899,7 @@ imageTrace Outline Obris @msg it has {_1}, which VectorCraft doesn't read from it yet sadrži {_1}, što VectorCraft još ne čita iz nje @msg its objects are nested too deeply njeni objekti su previše duboko ugniježđeni @msg its section ending with `%{end}` doesn't end njen odjeljak koji završava s `%{end}` ne završava +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) četkice koje VectorCraft još ne može pročitati izostavljene su iz panela Četkice ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekti s više ispuna ili poteza, efekata ili četkica stigli su kao njihov iscrtani izgled ({_1}): njihov izgled se još ne može uređivati @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradijent „{name}” nije definisan u datoteci: umjesto njega koristi se obična boja @msg an image VectorCraft can't read was left out izostavljena je slika koju VectorCraft ne može pročitati diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 552b0b8ac..5a591d27f 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5351,6 +5351,7 @@ digits Malayalam Malaiàlam @msg object {id} is not a selected top-level object l’objecte {id} no és un objecte de nivell superior seleccionat @msg objects can't be nested more than 32 deep els objectes no es poden niar més de 32 nivells @msg objects inside a blend, envelope, repeat or Intertwine group can't be attached to a path els objectes dins d’una fusió, un embolcall, una repetició o un grup Intertwine no es poden adjuntar a un traçat +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) els pinzells que VectorCraft encara no pot llegir s'han deixat fora del tauler Pinzells ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet els objectes amb diversos farciments o traços, efectes o pinzells han arribat amb el seu aspecte dibuixat ({_1}): la seva aparença encara no és editable @msg Offset Path: select paths Desplaça el traçat: selecciona traçats @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Desplaça el traçat: el desplaçament no deixa res dels traçats seleccionats (una retracció més gran que les formes) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index d96985198..1472c0747 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4954,6 +4954,7 @@ imageTrace Outline Obrys @msg it has {_1}, which VectorCraft doesn't read from it yet obsahuje {_1}, což z něj VectorCraft zatím nečte @msg its objects are nested too deeply jeho objekty jsou vnořené příliš hluboko @msg its section ending with `%{end}` doesn't end jeho oddíl končící `%{end}` nemá konec +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) štětce, které VectorCraft zatím neumí načíst, byly vynechány z panelu Štětce ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekty s více výplněmi nebo tahy, efekty či štětci byly načteny jako vykreslený vzhled ({_1}): jejich vzhled zatím nelze upravovat @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it přechod „{name}“ není v souboru definován: zastupuje ho plná barva @msg an image VectorCraft can't read was left out obrázek, který VectorCraft neumí přečíst, byl vynechán diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b050dbb63..73256a62b 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4587,6 +4587,7 @@ imageTrace Outline Kontur @msg it has {_1}, which VectorCraft doesn't read from it yet den har {_1}, hvilket VectorCraft endnu ikke læser fra den @msg its objects are nested too deeply dens objekter er indlejret for dybt @msg its section ending with `%{end}` doesn't end dens sektion, der ender med `%{end}`, ender ikke +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pensler, som VectorCraft endnu ikke kan læse, blev udeladt af panelet Pensler ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekter med flere fyld eller streger, effekter eller pensler kom ind, som de er tegnet ({_1}): deres udseende er endnu ikke redigerbart @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten »{name}« er ikke defineret i filen: der bruges en flad farve i stedet @msg an image VectorCraft can't read was left out et billede, som VectorCraft ikke kan læse, blev udeladt diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a26cc0c1a..c380e4a07 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4564,6 +4564,7 @@ theme Appearance Erscheinungsbild @msg it has {_1}, which VectorCraft doesn't read from it yet es enthält {_1}, was VectorCraft daraus noch nicht liest @msg its objects are nested too deeply seine Objekte sind zu tief verschachtelt @msg its section ending with `%{end}` doesn't end sein Abschnitt, der mit `%{end}` enden soll, endet nicht +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) Pinsel, die VectorCraft noch nicht lesen kann, wurden im Bedienfeld „Pinsel“ ausgelassen ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet Objekte mit mehreren Flächen oder Konturen, Effekten oder Pinseln wurden so übernommen, wie sie gezeichnet aussehen ({_1}): Ihr Aussehen ist noch nicht bearbeitbar @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it der Verlauf „{name}“ ist in der Datei nicht definiert: Eine einfache Farbe steht an seiner Stelle @msg an image VectorCraft can't read was left out ein Bild, das VectorCraft nicht lesen kann, wurde ausgelassen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 78bc64257..c763a01f2 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4597,6 +4597,7 @@ digits Malayalam Μαλαγιάλαμ @msg it has {_1}, which VectorCraft doesn't read from it yet περιέχει {_1}, τα οποία το VectorCraft δεν μπορεί ακόμη να διαβάσει @msg its objects are nested too deeply τα αντικείμενά του έχουν υπερβολικά βαθιά ένθεση @msg its section ending with `%{end}` doesn't end η ενότητά του που τελειώνει σε `%{end}` δεν τερματίζεται +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) τα πινέλα που το VectorCraft δεν μπορεί ακόμη να διαβάσει παραλείφθηκαν από τον πίνακα Πινέλα ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet τα αντικείμενα με πολλαπλά γεμίσματα, περιγράμματα, εφέ ή πινέλα εισήχθησαν ως απεικονίσεις ({_1}): η εμφάνισή τους δεν είναι ακόμη επεξεργάσιμη @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it το ντεγκραντέ «{name}» δεν ορίζεται στο αρχείο: χρησιμοποιείται στη θέση του ένα συμπαγές χρώμα @msg an image VectorCraft can't read was left out παραλείφθηκε εικόνα που δεν μπορεί να διαβάσει το VectorCraft diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4c49048ae..d71d23b18 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4648,6 +4648,7 @@ axis Both Ambos @msg it has {_1}, which VectorCraft doesn't read from it yet tienen {_1}, que VectorCraft aún no lee de ellos @msg its objects are nested too deeply sus objetos están anidados a demasiada profundidad @msg its section ending with `%{end}` doesn't end su sección que termina con `%{end}` no termina +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) los pinceles que VectorCraft aún no puede leer se omitieron del panel Pinceles ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet los objetos con varios rellenos o trazos, efectos o pinceles se importaron con su aspecto dibujado ({_1}): su aspecto aún no se puede editar @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it el degradado “{name}” no está definido en el archivo: lo sustituye un color sólido @msg an image VectorCraft can't read was left out se omitió una imagen que VectorCraft no puede leer diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 363c388db..102ef7c3b 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4856,6 +4856,7 @@ imageTrace Outline Ääriviiva @msg it has {_1}, which VectorCraft doesn't read from it yet siinä on {_1}, jota VectorCraft ei vielä lue siitä @msg its objects are nested too deeply sen objektit ovat liian syvällä sisäkkäin @msg its section ending with `%{end}` doesn't end sen `%{end}`-merkintään päättyvä osio ei pääty +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) siveltimet, joita VectorCraft ei vielä osaa lukea, jätettiin pois Siveltimet-paneelista ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objektit, joilla on useita täyttöjä tai viivoja, tehosteita tai siveltimiä, tuotiin piirretyn näköisinä ({_1}): niiden ulkoasua ei voi vielä muokata @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it liukuväriä “{name}” ei ole määritetty tiedostossa: sen tilalla on tasaväri @msg an image VectorCraft can't read was left out kuva, jota VectorCraft ei voi lukea, jätettiin pois diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 2fc442dab..b1ddc5f4f 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4636,6 +4636,7 @@ axis Both Les deux @msg it has {_1}, which VectorCraft doesn't read from it yet elles contiennent {_1}, que VectorCraft n’y lit pas encore @msg its objects are nested too deeply leurs objets sont imbriqués trop profondément @msg its section ending with `%{end}` doesn't end leur section qui se termine par `%{end}` ne se termine pas +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) les formes que VectorCraft ne sait pas encore lire ont été omises du panneau Formes ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet les objets à plusieurs fonds ou contours, effets ou formes ont été importés tels qu’ils sont dessinés ({_1}) : leur aspect n’est pas encore modifiable @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it le dégradé « {name} » n’est pas défini dans le fichier : une couleur unie le remplace @msg an image VectorCraft can't read was left out une image que VectorCraft ne peut pas lire a été ignorée diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f5be6e413..7da6af288 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5035,6 +5035,7 @@ imageTrace Outline Körvonal @msg object {0} cannot have children ennek az objektumnak nem lehetnek gyermekei: {0} @msg object {id} is not a path az objektum nem útvonal: {id} @msg object {id} is not a selected top-level object az objektum nem kijelölt legfelső szintű objektum: {id} +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) a VectorCraft által még nem olvasható ecsetek kimaradtak az Ecsetek panelről ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet a több kitöltésű vagy körvonalú, effektusos vagy ecsetes objektumok a megrajzolt kinézetükkel kerültek be ({_1}): a megjelenésük még nem szerkeszthető @msg Offset Path: select paths Útvonal eltolása: jelölj ki útvonalakat @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Útvonal eltolása: az eltolás után semmi sem marad a kijelölt útvonalakból (a befelé eltolás nagyobb az alakzatoknál) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 0c4be65a6..4e32a3d8a 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4587,6 +4587,7 @@ imageTrace Outline Kontur @msg it has {_1}, which VectorCraft doesn't read from it yet memiliki {_1}, yang belum dibaca VectorCraft dari berkas tersebut @msg its objects are nested too deeply objeknya bersarang terlalu dalam @msg its section ending with `%{end}` doesn't end bagian yang berakhir dengan `%{end}` tidak berakhir +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) kuas yang belum dapat dibaca VectorCraft tidak dimasukkan ke panel Kuas ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objek dengan beberapa isian atau goresan, efek, atau kuas masuk sebagai tampilan gambarnya ({_1}): penampilannya belum dapat diedit @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradien “{name}” tidak didefinisikan dalam berkas: warna polos dipakai sebagai pengganti @msg an image VectorCraft can't read was left out gambar yang tidak dapat dibaca VectorCraft tidak disertakan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 12da08947..56235fef1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4690,6 +4690,7 @@ axis Both Entrambi @msg it has {_1}, which VectorCraft doesn't read from it yet contengono {_1}, che VectorCraft non vi legge ancora @msg its objects are nested too deeply i loro oggetti sono annidati troppo in profondità @msg its section ending with `%{end}` doesn't end la loro sezione che termina con `%{end}` non termina +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) i pennelli che VectorCraft non sa ancora leggere sono stati esclusi dal pannello Pennelli ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet gli oggetti con più riempimenti o tratti, effetti o pennelli sono stati importati come appaiono disegnati ({_1}): il loro aspetto non è ancora modificabile @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it la sfumatura “{name}” non è definita nel file: la sostituisce un colore pieno @msg an image VectorCraft can't read was left out un'immagine che VectorCraft non può leggere è stata omessa diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f68f17fc5..cc48dc8bb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4386,6 +4386,7 @@ swatch Bright Violet ビビッドバイオレット @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel リンク画像は、元のファイルが見つかるまで、ファイルに保存された低解像度のプレビューで表示されます:リンクパネルで再リンクしてください @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise 表示に使われるフォントの 1 行に対して低すぎるフレーム内のエリア内文字は、フレームを高くしました ({_1}): そうしないと文字が表示されません @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it グラデーション「{name}」はファイル内で定義されていません: 代わりに単色を使用しています +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft がまだ読み込めないブラシ {_1} 個はブラシパネルに追加されませんでした @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 塗りや線が複数ある、または効果やブラシを適用したオブジェクト {_1} 個は見た目のまま読み込まれました: アピアランスはまだ編集できません @msg patterns nested too deeply are filled with mid-grey 入れ子が深すぎるパターンは中間のグレーで塗りつぶされています @msg shadings of an unknown type were left out 種類が不明なシェーディングは除外されました diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index f329a90f8..02470e035 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4385,6 +4385,7 @@ font-technology Variable ცვლადი @msg it has {_1}, which VectorCraft doesn't read from it yet მას აქვს {_1}, რასაც VectorCraft მისგან ჯერ ვერ კითხულობს @msg its objects are nested too deeply მისი ობიექტები ზედმეტად ღრმადაა ჩადგმული @msg its section ending with `%{end}` doesn't end მისი სექცია, რომელიც `%{end}`-ით მთავრდება, არ სრულდება +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) ფუნჯები, რომლებსაც VectorCraft ჯერ ვერ კითხულობს, ფუნჯების პანელში არ დაემატა ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet რამდენიმე შევსების ან კონტურის, ეფექტის ან ფუნჯის მქონე ობიექტები შემოვიდა დახატული სახით ({_1}): მათი იერსახე ჯერ არ რედაქტირდება @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it გრადიენტი „{name}“ ფაილში განსაზღვრული არ არის: მის ნაცვლად ერთგვაროვანი ფერია @msg an image VectorCraft can't read was left out გამოსახულება, რომელსაც VectorCraft ვერ კითხულობს, გამოტოვებულია diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index abdcb7cf6..fc30c96d3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4313,6 +4313,7 @@ theme Appearance 외관 @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel 연결된 그림은 해당 파일을 찾을 때까지 파일에 저장된 저해상도 미리 보기로 표시됩니다: 링크 패널에서 다시 연결하세요 @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise 표시되는 글꼴의 한 줄보다 낮은 프레임의 영역 문자는 프레임을 더 높였습니다({_1}). 그렇지 않으면 문자가 표시되지 않습니다 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 그레이디언트 “{name}”이(가) 파일에 정의되어 있지 않아 단색으로 대체했습니다 +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft에서 아직 읽을 수 없는 브러시는 브러시 패널에서 제외되었습니다({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 칠이나 선이 여러 개인 개체, 효과, 브러시가 있는 개체는 그려진 모양({_1})으로 가져왔습니다: 모양새는 아직 편집할 수 없습니다 @msg patterns nested too deeply are filled with mid-grey 너무 깊게 중첩된 패턴은 중간 회색으로 채웁니다 @msg shadings of an unknown type were left out 알 수 없는 종류의 셰이딩은 제외했습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index fb2ebeb99..cd7a559d2 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4599,6 +4599,7 @@ imageTrace Outline Contour @msg it has {_1}, which VectorCraft doesn't read from it yet het bevat {_1}, wat VectorCraft er nog niet uit leest @msg its objects are nested too deeply de objecten ervan zijn te diep genest @msg its section ending with `%{end}` doesn't end zijn sectie die op `%{end}` eindigt, eindigt niet +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penselen die VectorCraft nog niet kan lezen, zijn weggelaten uit het deelvenster Penselen ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objecten met meerdere vullingen of lijnen, effecten of penselen zijn als hun getekende uiterlijk binnengekomen ({_1}): hun vormgeving is nog niet bewerkbaar @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it verloop „{name}” is niet gedefinieerd in het bestand: er is een effen kleur voor gebruikt @msg an image VectorCraft can't read was left out een afbeelding die VectorCraft niet kan lezen, is weggelaten diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f329a539a..d61832b34 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4780,6 +4780,7 @@ imageTrace Outline Kontur @msg it has {_1}, which VectorCraft doesn't read from it yet zawiera {_1}, czego VectorCraft jeszcze z niego nie odczytuje @msg its objects are nested too deeply jego obiekty są zagnieżdżone zbyt głęboko @msg its section ending with `%{end}` doesn't end jego sekcja kończąca się na `%{end}` nie ma końca +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pędzle, których VectorCraft jeszcze nie odczytuje, pominięto w panelu Pędzle ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet obiekty z kilkoma wypełnieniami lub obrysami, efektami albo pędzlami wczytano jako ich narysowany wygląd ({_1}): ich wyglądu nie można jeszcze edytować @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradient „{name}” nie jest zdefiniowany w pliku: zastąpiono go jednolitym kolorem @msg an image VectorCraft can't read was left out pominięto obraz, którego VectorCraft nie potrafi odczytać diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 726c16d44..31b2c5b93 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4230,6 +4230,7 @@ axis Both Ambos @msg it has {_1}, which VectorCraft doesn't read from it yet tem {_1}, que o VectorCraft ainda não lê desse arquivo @msg its objects are nested too deeply seus objetos estão aninhados com profundidade excessiva @msg its section ending with `%{end}` doesn't end sua seção que termina com `%{end}` não termina +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) os pincéis que o VectorCraft ainda não consegue ler ficaram fora do painel Pincéis ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objetos com vários preenchimentos ou traços, efeitos ou pincéis entraram com a aparência desenhada ({_1}): sua aparência ainda não é editável @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it o gradiente “{name}” não está definido no arquivo: uma cor sólida o substitui @msg an image VectorCraft can't read was left out uma imagem que o VectorCraft não consegue ler foi omitida diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ba7a94689..7a77d759b 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4685,6 +4685,7 @@ axis Both Оба @msg it has {_1}, which VectorCraft doesn't read from it yet в них есть {_1}, которые VectorCraft пока не читает @msg its objects are nested too deeply объекты в них вложены слишком глубоко @msg its section ending with `%{end}` doesn't end раздел, заканчивающийся на `%{end}`, не заканчивается +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) кисти, которые VectorCraft пока не умеет читать, не добавлены в панель «Кисти» ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet объекты с несколькими заливками или обводками, эффектами или кистями импортированы в нарисованном виде ({_1}): их оформление пока нельзя редактировать @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиент «{name}» не определён в файле: вместо него используется сплошной цвет @msg an image VectorCraft can't read was left out изображение, которое VectorCraft не может прочитать, пропущено diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 81942ced5..85e2cbb64 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4547,6 +4547,7 @@ axis Both Båda @msg it has {_1}, which VectorCraft doesn't read from it yet den innehåller {_1}, som VectorCraft inte kan läsa från den än @msg its objects are nested too deeply dess objekt är för djupt kapslade @msg its section ending with `%{end}` doesn't end dess avsnitt som slutar med `%{end}` avslutas aldrig +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penslar som VectorCraft ännu inte kan läsa utelämnades från panelen Penslar ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekt med flera fyllningar eller konturer, effekter eller penslar importerades som sitt ritade utseende ({_1}): deras utseende går inte att redigera än @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten ”{name}” är inte definierad i filen: en enfärgad fyllning används i stället @msg an image VectorCraft can't read was left out en bild som VectorCraft inte kan läsa utelämnades diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index af1a87f89..1e3574678 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4384,6 +4384,7 @@ axis Both Her İkisi @msg it has {_1}, which VectorCraft doesn't read from it yet VectorCraft'ın henüz okumadığı {_1} var @msg its objects are nested too deeply nesneleri çok derin bir şekilde iç içe geçmiş @msg its section ending with `%{end}` doesn't end `%{end}` ile biten bölümü bitmiyor +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft'ın henüz okuyamadığı fırçalar Fırçalar panelinin dışında bırakıldı ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet çeşitli dolgulara veya konturlara, efektlere veya fırçalara sahip nesneler çizilmiş görünümleri olarak geldi ({_1}): görünümleri henüz düzenlenemez @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it “{name}” degradesi dosyada tanımlı değil: onun yerine düz bir renk geliyor @msg an image VectorCraft can't read was left out VectorCraft'ın okuyamadığı bir resim dışarıda bırakıldı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 07e5d8db9..f0ba143cc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4674,6 +4674,7 @@ axis Both Обидві @msg it has {_1}, which VectorCraft doesn't read from it yet у них є {_1}, які VectorCraft поки не читає @msg its objects are nested too deeply об’єкти в них вкладені надто глибоко @msg its section ending with `%{end}` doesn't end розділ, що закінчується на `%{end}`, не закінчується +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) пензлі, які VectorCraft поки не вміє читати, не додано до панелі «Пензлі» ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet об’єкти з кількома заливками або обведеннями, ефектами або пензлями імпортовані в намальованому вигляді ({_1}): їх оформлення поки що не можна редагувати @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градієнт «{name}» не визначено у файлі: замість нього використовується суцільний колір @msg an image VectorCraft can't read was left out зображення, яке VectorCraft не може прочитати, пропущено diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e0844fb25..436fe20d0 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4689,6 +4689,7 @@ theme Appearance Giao diện @msg object {0} cannot have children đối tượng {0} không thể có phần tử con @msg object {id} is not a path đối tượng {id} không phải là đường dẫn @msg object {id} is not a selected top-level object đối tượng {id} không phải là đối tượng cấp cao nhất đã chọn +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) các cọ mà VectorCraft chưa đọc được đã bị bỏ khỏi bảng Cọ ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet đối tượng có nhiều tô hoặc nét, hiệu ứng hoặc cọ đã vào dưới dạng bề ngoài ({_1}): diện mạo chưa thể chỉnh sửa @msg Offset Path: select paths Đường dẫn độ lệch: hãy chọn đường dẫn @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Đường dẫn độ lệch: độ lệch không để lại gì từ các đường dẫn đã chọn (độ lệch vào trong lớn hơn hình) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 1ea470392..2131de982 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4108,6 +4108,7 @@ imageTrace Outline 轮廓 @msg graphs come in as plain art, without their data or settings 图表作为普通图稿导入,不保留其数据和设置 @msg an image whose data is cut short was left out 已略去数据不完整的图像 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 文件中未定义渐变“{name}”:已改用纯色代替 +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚无法读取的画笔未加入“画笔”面板 ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 带有多个填色或描边、效果或画笔的对象已按其绘制外观导入 ({_1}):其外观尚不可编辑 @msg patterns nested too deeply are filled with mid-grey 嵌套过深的图案以中灰色填充 @msg shadings of an unknown type were left out 已略去未知类型的着色 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 8d7d0ff58..d2ce0501e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2003,6 +2003,7 @@ file-filter Templates 範本 @msg graphs come in as plain art, without their data or settings 圖表以一般圖稿匯入,不保留其資料與設定 @msg an image whose data is cut short was left out 已略過資料不完整的影像 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 檔案中未定義漸層「{name}」:已改用單色替代 +@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚無法讀取的筆刷未加入「筆刷」面板 ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 具有多個填色或筆畫、效果或筆刷的物件已依其繪製外觀匯入 ({_1}):其外觀目前尚無法編輯 @msg patterns nested too deeply are filled with mid-grey 巢狀層級過深的圖樣以中灰色填滿 @msg shadings of an unknown type were left out 已略過類型未知的著色 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 7a90a0d53..65e6de0bf 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -118,7 +118,7 @@ private editing data. VectorCraft: ### Illustrator's private editing data -the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke or Convert to Shape comes in as itself with them and their saved settings; one with more, other effects, an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), a brush or dashes aligned to corners comes in as its drawn look (a group named after it). Type keeps its art style as a live appearance whatever its stack, so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols, pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068). A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. +the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke or Convert to Shape comes in as itself with them and their saved settings; one with more, other effects, an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), a brush or dashes aligned to corners comes in as its drawn look (a group named after it). The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. Type keeps its art style as a live appearance whatever its stack, so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols, pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068). A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. ## Format by format diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6d10c3994..200dc5c63 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1150,6 +1150,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai` or PDF whose editing data holds a readable variable library opens with its variable definitions, data-set rows, text, visibility and linked-file values, active row and lock in the Variables panel, whether its layers are read or it comes in as its PDF part; graph values and malformed entries are skipped with a note, and objects aren't bound yet (`crates/eps/src/import/ai/tests.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data keeps each artboard's center mark, cross hairs, video safe areas and video ruler pixel aspect ratio, and the file's Document Raster Effects Settings; the author comes from the header's `%%For` (the editing data's, else an EPS's own) when the file's File Info gives none, the header's custom colors that no swatch has come in as spot swatches, a named color the file defines takes the place of a default swatch of that name when the file has no palette, and an older file's `%%TemplateBox` places its artboard. A value of another type or out of range keeps its default (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` opened from its editing data opens its Objects on Path groups live, with their path, their objects in order along it, Pivot and Rotate, and their art where the file draws it; a group whose spacing has been dragged, or with options an Objects on Path group doesn't hold, opens as a plain group (`crates/eps/src/import/ai/tests.rs`). + A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art. A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. Pattern brushes and brushes that can't be read are left out with a note; brushed paths still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482). When the PDF content of a `.ai` has a page for each artboard, the layers are compared with every page on its artboard (until the pages compared take 8,388,608 pixels at the comparison's scale, about 67 letter-size pages, or number 128): art that only the layers or only a later page draws is handled as art that differs on the first page is, and a text object that a later page doesn't draw is left out unless its layer doesn't print (`crates/eps/src/import/tests_native.rs`).