Write legacy .ai layer names in Windows Latin-1 and warn when a character is replaced (#2693)

This commit is contained in:
MarekSpodBiedry
2026-10-18 01:11:38 +07:00
committed by bflatastic
parent 56df1a1663
commit b5164825d6
3 changed files with 49 additions and 3 deletions
+24 -2
View File
@@ -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<u8> = 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");
+23
View File
@@ -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));
}
+2 -1
View File
@@ -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