diff --git a/crates/eps/src/legacy_ai.rs b/crates/eps/src/legacy_ai.rs index 43ef9c06b..e25871000 100644 --- a/crates/eps/src/legacy_ai.rs +++ b/crates/eps/src/legacy_ai.rs @@ -85,6 +85,7 @@ pub const HIDDEN: &str = "hidden objects are left out (hidden layers are kept, h pub const HIDDEN_LAYER: &str = "hidden objects are moved to a hidden layer above their own (legacy .ai files can hide only layers)"; pub const HIDDEN_SHOWN: &str = "hidden objects are written visible (legacy .ai files can hide only layers)"; pub const TRANSPARENCY: &str = "transparency (opacity, blending modes, opacity masks) is written opaque: legacy .ai files have none"; +pub const LAYER_NAME: &str = "characters outside Windows Latin-1 in layer names are written as ? (legacy .ai files can't hold them)"; pub const NAMES: &str = "object names are left out (legacy .ai files name only layers)"; const PATTERNS: &str = "pattern fills are written as their tiles, clipped to the shape"; const FREEFORM: &str = "freeform gradients are written as their average colour"; @@ -452,6 +453,25 @@ impl<'a> Writer<'a> { // ---------- layers and objects ---------- + /// A layer's name as a PostScript string in Windows-1252, like the type; a character outside it + /// is written as `?`, with a warning. + fn layer_name(&mut self, name: &str) -> String { + let mut lost = false; + let bytes: Vec = name + .chars() + .map(|c| { + latin::encode(c).unwrap_or_else(|| { + lost = true; + b'?' + }) + }) + .collect(); + if lost { + self.warn(LAYER_NAME); + } + ps::bytes(&bytes) + } + /// A top-level layer. False when it isn't a layer. fn layer(&mut self, n: &Node) -> bool { let NodeKind::Layer { color, template, printable, children, clip, preview, dim_images } = &n.kind else { return false }; @@ -464,6 +484,7 @@ impl<'a> Writer<'a> { _ => -1, }; let flag = |b: bool| u8::from(b); + let name = self.layer_name(n.name.as_deref().unwrap_or("Layer")); // A template layer doesn't print and shows dimmed. let _ = writeln!( self.body, @@ -473,7 +494,7 @@ impl<'a> Writer<'a> { flag(!n.locked), flag(*printable && !*template), flag(dim_images.is_some() || *template), - ps::string(n.name.as_deref().unwrap_or("Layer")), + name, ); self.locked = false; self.line("0 A"); @@ -501,13 +522,14 @@ impl<'a> Writer<'a> { _ => -1, }; let name = format!("{} (hidden)", n.name.as_deref().unwrap_or("Layer")); + let name = self.layer_name(&name); let _ = writeln!( self.body, "%AI5_BeginLayer\n0 1 {} {} {} 0 {index} {r} {g} {b} Lb\n{} Ln", u8::from(!n.locked), u8::from(*printable && !*template), u8::from(*template), - ps::string(&name), + name, ); self.locked = false; self.line("0 A"); diff --git a/crates/eps/src/tests_legacy_ai.rs b/crates/eps/src/tests_legacy_ai.rs index 78ee7ddbd..89517654a 100644 --- a/crates/eps/src/tests_legacy_ai.rs +++ b/crates/eps/src/tests_legacy_ai.rs @@ -642,3 +642,26 @@ fn hidden_objects_can_be_left_out_or_shown() { assert_eq!(pair.len(), 2); assert!(pair.iter().all(|n| n.visible)); } + +#[test] +fn layer_names_outside_latin_1_are_replaced_and_warned() { + let layered = |name: &str| { + let mut doc = Document::new(100.0, 100.0); + doc.layers.clear(); + let layer = doc.add_layer(Some(name)); + let n = shape(&mut doc, shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 40.0)), red(), Paint::None); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + // A hidden object, so the layer also gets a hidden layer named after it. + let mut hidden = shape(&mut doc, shapes::rectangle(Rect::new(50.0, 10.0, 80.0, 40.0)), red(), Paint::None); + hidden.visible = false; + doc.insert(Some(layer), usize::MAX, hidden).unwrap(); + write(&doc) + }; + let out = layered("\u{b808}\u{c774}\u{c5b4} 1 Caf\u{e9}"); + let text = String::from_utf8_lossy(&out.bytes); + assert!(text.contains("(??? 1 Caf\\351) Ln"), "{text}"); + assert!(text.contains("(??? 1 Caf\\351 \\(hidden\\)) Ln"), "{text}"); + assert!(out.warnings.iter().any(|w| w == LAYER_NAME), "{:?}", out.warnings); + // A name Latin-1 can hold is written as it is, with no warning. + assert!(!layered("Caf\u{e9}").warnings.iter().any(|w| w == LAYER_NAME)); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index f4df80736..2712ac9b8 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -66,7 +66,8 @@ private editing data. VectorCraft: graphics and gradient or pattern fills make that type outlines, and Text: Outlines (`outlineText: true`) writes all type as outlines. The format can hide only layers: hidden objects go to a hidden layer of their own above their layer, are omitted, or are made visible (Hidden Objects, `hiddenObjects: "layer"|"omit"|"visible"`). - Sublayers become groups; appearances, effects and brushes are expanded; object names are left out. + Sublayers become groups; appearances, effects and brushes are expanded; object names are left out; layer + names are written in Windows-1252, with `?` for characters outside it. The file is also a PostScript program that runs: its prolog, a procedure set of VectorCraft's own written from the PostScript Language Reference, gives the format's operators their meaning, so it prints and previews (gradients filled with their first stop's colour, type left to the apps that read the format). VectorCraft