Merge pull request #2938 from dotCooCoo/fix/legacy-ai-names-windows-1252

Legacy .ai export: the title and spot color and gradient names are written in Windows-1252
This commit is contained in:
BFlat
2026-10-19 00:09:56 +07:00
committed by GitHub
3 changed files with 204 additions and 30 deletions
+81 -29
View File
@@ -50,7 +50,9 @@
//! their own above their layer, are left out or show, as [`HiddenObjects`] says (in a hidden
//! layer they stay where they are). Type that 8-bit text can't hold (characters outside
//! Windows-1252, vertical type, inline graphics, gradient or pattern fills) is written as its
//! outlines, and all type with [`LegacyAiOptions::outline_text`].
//! outlines, and all type with [`LegacyAiOptions::outline_text`]. Layer, spot color and gradient
//! names and the title are written in Windows-1252 too, and a character outside it is written as
//! `?`. Two spot colors or two gradients whose names become the same that way are numbered (`? 2`).
//!
//! The prolog is a procedure set of the writer's own ([`PROLOG`]) that gives the operators their
//! meaning in PostScript, so the file also prints and previews as a PostScript program (its
@@ -87,6 +89,8 @@ pub const HIDDEN_LAYER: &str = "hidden objects are moved to a hidden layer above
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 TITLE_AND_COLOR_NAMES: &str =
"characters outside Windows Latin-1 in the title and in spot color and gradient 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";
@@ -163,11 +167,12 @@ pub fn export_legacy_ai(doc: &Document, o: &LegacyAiOptions) -> Result<LegacyAiO
.filter(|b| b.is_finite())
.reduce(|a, b| a.union(b))
.unwrap_or(Rect::new(0.0, below, width, top));
let title = w.latin_name(&o.title, TITLE_AND_COLOR_NAMES);
let mut s = String::new();
s.push_str(PS_HEADER);
s.push('\n');
let _ = writeln!(s, "%%Creator: VectorCraft {}", env!("CARGO_PKG_VERSION"));
let _ = writeln!(s, "%%Title: {}", ps::string(&o.title));
let _ = writeln!(s, "%%Title: {}", latin_string(&title));
if let Some(t) = o.created {
let [y, mo, d, h, mi, _] = vectorcraft_doc::metadata::civil(t);
let _ = writeln!(s, "%%CreationDate: ({mo}/{d}/{y:04}) ({h:02}:{mi:02})");
@@ -179,12 +184,12 @@ pub fn export_legacy_ai(doc: &Document, o: &LegacyAiOptions) -> Result<LegacyAiO
s.push('\n');
s.push_str("%%DocumentProcessColors: Cyan Magenta Yellow Black\n");
for (i, (name, _)) in w.spots.iter().enumerate() {
let _ = writeln!(s, "{} {}", if i == 0 { "%%DocumentCustomColors:" } else { "%%+" }, ps::string(name));
let _ = writeln!(s, "{} {}", if i == 0 { "%%DocumentCustomColors:" } else { "%%+" }, latin_string(name));
}
for (i, (name, cmyk)) in w.spots.iter().enumerate() {
s.push_str(if i == 0 { "%%CMYKCustomColor: " } else { "%%+ " });
ps::push_nums(&mut s, &cmyk.map(f64::from));
let _ = writeln!(s, "{}", ps::string(name));
let _ = writeln!(s, "{}", latin_string(name));
}
for (i, (name, _)) in w.fonts.iter().enumerate() {
let _ = writeln!(s, "{} {name}", if i == 0 { "%%DocumentFonts:" } else { "%%+" });
@@ -335,7 +340,7 @@ const PS_HEADER: &str = "%!PS-Adobe-3.0"; // brand-ok: the PostScript header com
/// A gradient definition: `%AI5_BeginGradient: (name)`, `(name) type stops Bd`, `[`, the stops
/// last first (`colorSpec colorStyle midPoint rampPoint %_Bs`), `BD`, `%AI5_EndGradient`.
fn gradient_def(s: &mut String, w: &Writer, name: &str, g: &Gradient) {
let name = ps::string(name);
let name = latin_string(name);
let kind = if g.kind == GradientKind::Radial { 1 } else { 0 };
let _ = writeln!(s, "%AI5_BeginGradient: {name}\n{name} {kind} {} Bd\n[", g.stops.len());
for stop in g.stops.iter().rev() {
@@ -356,7 +361,7 @@ fn gradient_def(s: &mut String, w: &Writer, name: &str, g: &Gradient) {
}
Ink::Spot { cmyk, name, tint } => {
ps::push_nums(s, &cmyk.map(f64::from));
let _ = write!(s, "{} ", ps::string(&name));
let _ = write!(s, "{} ", latin_string(w.spot_name(&name)));
ps::push_num(s, f64::from(1.0 - tint));
s.push_str(" 3 ");
}
@@ -393,6 +398,13 @@ fn invertible(a: Affine) -> bool {
det.is_finite() && det.abs() > 1e-12
}
/// Name `name` as a PostScript string of its characters' Windows-1252 codes (a character outside
/// Windows-1252 is left out: [`Writer::latin_name`] replaces them first).
fn latin_string(name: &str) -> String {
let bytes: Vec<u8> = name.chars().filter_map(latin::encode).collect();
ps::bytes(&bytes)
}
struct Writer<'a> {
doc: &'a Document,
/// The document → art space.
@@ -401,8 +413,10 @@ struct Writer<'a> {
body: String,
/// Gradient definitions, by the name objects use.
gradients: Vec<(String, Gradient)>,
/// Spot colours used: name and full CMYK equivalent.
/// Spot colors used: the name written and full CMYK equivalent.
spots: Vec<(String, [f32; 4])>,
/// The name each spot color (by its swatch's name) is written with ([`Self::spot`]).
spot_names: Vec<(String, String)>,
warnings: Vec<String>,
/// The lock state the last `A` set.
locked: bool,
@@ -430,6 +444,7 @@ impl<'a> Writer<'a> {
body: String::new(),
gradients: vec![],
spots: vec![],
spot_names: vec![],
warnings: vec![],
locked: false,
nest: 0,
@@ -454,23 +469,62 @@ 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 {
/// `name` with each character outside Windows-1252 (the encoding type and names are written
/// in) replaced by `?`; adds the warning `warning` when it replaces one.
fn latin_name(&mut self, name: &str, warning: &str) -> String {
let mut lost = false;
let bytes: Vec<u8> = name
let kept = name
.chars()
.map(|c| {
latin::encode(c).unwrap_or_else(|| {
if latin::encode(c).is_some() {
c
} else {
lost = true;
b'?'
})
'?'
}
})
.collect();
if lost {
self.warn(LAYER_NAME);
self.warn(warning);
}
ps::bytes(&bytes)
kept
}
/// 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 {
latin_string(&self.latin_name(name, LAYER_NAME))
}
/// Note spot color `name` (its swatch's name), of full CMYK equivalent `cmyk`, as a custom
/// color of the document, and give the name it is written with. Its characters outside
/// Windows-1252 are written as `?` (with a warning), and it is numbered when another spot color
/// is already written with that name; the name is the same each time.
fn spot(&mut self, name: &str, cmyk: [f32; 4]) -> String {
let written = match self.spot_names.iter().find(|(n, _)| n == name) {
Some((_, w)) => w.clone(),
None => {
let base = self.latin_name(name, TITLE_AND_COLOR_NAMES);
let mut written = base.clone();
let mut i = 1;
while self.spot_names.iter().any(|(_, w)| *w == written) {
i += 1;
written = format!("{base} {i}");
}
self.spot_names.push((name.to_string(), written.clone()));
written
}
};
if !self.spots.iter().any(|(n, _)| *n == written) {
self.spots.push((written.clone(), cmyk));
}
written
}
/// The name spot color `name` (its swatch's name) is written with, once [`Self::spot`] has
/// noted it.
fn spot_name<'n>(&'n self, name: &'n str) -> &'n str {
self.spot_names.iter().find(|(n, _)| n == name).map_or(name, |(_, w)| w.as_str())
}
/// A top-level layer. False when it isn't a layer.
@@ -965,7 +1019,10 @@ impl<'a> Writer<'a> {
/// Set the fill (or with `stroke`, the stroke) colour to solid paint `p`.
fn color(&mut self, p: &Paint, stroke: bool) {
let ink = self.ink(p);
let ink = match self.ink(p) {
Ink::Spot { cmyk, name, tint } => Ink::Spot { name: self.spot(&name, cmyk), cmyk, tint },
ink => ink,
};
let op = |fill: &'static str, line: &'static str| if stroke { line } else { fill };
let s = &mut self.body;
match ink {
@@ -983,13 +1040,10 @@ impl<'a> Writer<'a> {
}
Ink::Spot { cmyk, name, tint } => {
ps::push_nums(s, &cmyk.map(f64::from));
let _ = write!(s, "{} ", ps::string(&name));
let _ = write!(s, "{} ", latin_string(&name));
// The format's tint is how much lighter than the full colour (0: full).
ps::push_num(s, f64::from(1.0 - tint));
let _ = writeln!(s, " {}", op("x", "X"));
if !self.spots.iter().any(|(n, _)| *n == name) {
self.spots.push((name, cmyk));
}
}
}
}
@@ -1009,10 +1063,8 @@ impl<'a> Writer<'a> {
if let Some(sw) = &stop.swatch {
// Spot colours in stops are custom colours too.
let p = Paint::Solid { color: stop.color, swatch: Some(sw.clone()), tint: stop.tint };
if let Ink::Spot { cmyk, name, .. } = self.ink(&p)
&& !self.spots.iter().any(|(n, _)| *n == name)
{
self.spots.push((name, cmyk));
if let Ink::Spot { cmyk, name, .. } = self.ink(&p) {
self.spot(&name, cmyk);
}
}
}
@@ -1036,21 +1088,21 @@ impl<'a> Writer<'a> {
s.push_str("Bh\n");
}
s.push_str("1 ");
s.push_str(&ps::string(&name));
s.push_str(&latin_string(&name));
s.push(' ');
ps::push_nums(&mut s, &[start.x, start.y, angle, length]);
s.push_str("1 0 0 1 0 0 Bg\n");
s
}
/// The name of the definition of `g` (its swatch's name when it has one), defined the first
/// time.
/// The name of the definition of `g` (its swatch's name when it has one, with each character
/// outside Windows-1252 written as `?` and a warning), defined the first time.
fn define(&mut self, g: &Gradient, swatch: Option<&str>) -> String {
if let Some((name, _)) = self.gradients.iter().find(|(_, d)| d == g) {
return name.clone();
}
let taken = |n: &str, defs: &[(String, Gradient)]| defs.iter().any(|(m, _)| m == n);
let base = swatch.filter(|s| !s.is_empty()).unwrap_or("Gradient").to_string();
let base = self.latin_name(swatch.filter(|s| !s.is_empty()).unwrap_or("Gradient"), TITLE_AND_COLOR_NAMES);
let mut name = base.clone();
let mut i = 1;
while taken(&name, &self.gradients) {
+121
View File
@@ -612,6 +612,127 @@ fn spot_colours_are_custom_colours() {
assert!((tint - 0.5).abs() < 1e-6);
}
/// A document with a spot color swatch for each of `spots`, a square filled with each at half
/// tint, and a square whose linear gradient fill (the first spot color to white) names the
/// gradient swatch `gradient` (the document holds no such swatch; the writer reads only the
/// fill's name).
fn spots_and_gradient(spots: &[&str], gradient: &str) -> Document {
let mut doc = Document::new(200.0, 100.0);
let layer = doc.layers[0].id;
for (i, name) in spots.iter().enumerate() {
let c = i as f32 * 0.5;
doc.swatches.push(vectorcraft_color::Swatch {
name: (*name).into(),
paint: Paint::solid(Color::cmyk(c, 0.5, 1.0, 0.0)),
global: true,
spot: true,
});
let fill = Paint::Solid { color: Color::cmyk(c / 2.0, 0.25, 0.5, 0.0), swatch: Some((*name).into()), tint: 0.5 };
let x = 10.0 + 40.0 * i as f64;
let n = shape(&mut doc, shapes::rectangle(Rect::new(x, 10.0, x + 30.0, 40.0)), fill, Paint::None);
doc.insert(Some(layer), usize::MAX, n).unwrap();
}
let mut first = GradientStop::new(0.0, Color::cmyk(0.0, 0.5, 1.0, 0.0));
first.swatch = Some(spots[0].into());
let g = Gradient { kind: GradientKind::Linear, stops: vec![first, GradientStop::new(1.0, Color::cmyk(0.0, 0.0, 0.0, 0.0))] };
let mut paint = GradientPaint::new(g);
paint.swatch = Some(gradient.into());
let n = shape(&mut doc, shapes::rectangle(Rect::new(10.0, 50.0, 50.0, 90.0)), Paint::Gradient(Box::new(paint)), Paint::None);
doc.insert(Some(layer), usize::MAX, n).unwrap();
doc
}
/// `doc` written with the title `title`, and run as a PostScript program ([`prints_as`]).
fn write_titled(doc: &Document, title: &str) -> LegacyAiOutput {
let o = LegacyAiOptions { title: title.into(), created: Some(0), ..LegacyAiOptions::default() };
let out = export_legacy_ai(doc, &o).unwrap();
prints_as(&out.bytes, doc, &o);
out
}
/// The spot colors and gradients of the objects of `doc`'s first layer, in paint order: each
/// solid fill's swatch, and each gradient fill's swatch with its first stop's.
fn inks(doc: &Document) -> Vec<Option<String>> {
let mut out = vec![];
for n in children(&doc.layers[0]) {
match n.appearance.fill_paint() {
Paint::Solid { swatch, .. } => out.push(swatch.clone()),
Paint::Gradient(g) => {
out.push(g.swatch.clone());
out.push(g.gradient.stops.first().and_then(|s| s.swatch.clone()));
}
_ => {}
}
}
out
}
/// The title, spot color names and gradient names are written in Windows-1252, as layer names
/// are: é is 351 (octal) in the header's custom colors, each `x`, the gradient's definition, its
/// stops and `Bg`. The spot color and gradient names read back as they were.
#[test]
fn spot_gradient_and_title_names_are_written_in_windows_1252() {
let doc = spots_and_gradient(&["Caf\u{e9}"], "D\u{e9}grad\u{e9}");
let out = write_titled(&doc, "R\u{e9}sum\u{e9}");
let text = String::from_utf8(out.bytes.clone()).unwrap();
for needle in [
"%%Title: (R\\351sum\\351)\n",
"%%DocumentCustomColors: (Caf\\351)\n",
"%%CMYKCustomColor: 0 0.5 1 0 (Caf\\351)\n",
"0 0.5 1 0 (Caf\\351) 0.5 x\n",
"%AI5_BeginGradient: (D\\351grad\\351)\n(D\\351grad\\351) 0 2 Bd\n",
"0 0.5 1 0 (Caf\\351) 0 3 50 0 %_Bs\n",
"1 (D\\351grad\\351) ",
] {
assert!(text.contains(needle), "missing {needle:?} in\n{text}");
}
assert!(!text.contains("\\303"), "{text}");
assert!(!out.warnings.iter().any(|w| w == TITLE_AND_COLOR_NAMES), "{:?}", out.warnings);
let read = crate::import(&out.bytes).unwrap().document;
let names = inks(&read);
let want = ["Caf\u{e9}", "D\u{e9}grad\u{e9}", "Caf\u{e9}"].map(|n| Some(n.to_string()));
assert_eq!(names, want);
}
/// A character Windows-1252 can't hold is written as `?`, with one warning for all of these
/// names. Names that become the same are numbered, so the header, each `x` and each `Bg` still
/// tell the colors and gradients apart.
#[test]
fn names_outside_windows_1252_are_replaced_and_kept_apart() {
// 赤 and 青 (spot colors), 渐变 and グラ (gradients), 日本 (the title).
let mut doc = spots_and_gradient(&["\u{8d64}", "\u{9752}"], "\u{6e10}\u{53d8}");
let g = Gradient {
kind: GradientKind::Linear,
stops: vec![GradientStop::new(0.0, Color::cmyk(1.0, 0.0, 0.0, 0.0)), GradientStop::new(1.0, Color::cmyk(0.0, 0.0, 0.0, 0.0))],
};
let mut paint = GradientPaint::new(g);
paint.swatch = Some("\u{30b0}\u{30e9}".into());
let n = shape(&mut doc, shapes::rectangle(Rect::new(60.0, 50.0, 100.0, 90.0)), Paint::Gradient(Box::new(paint)), Paint::None);
let layer = doc.layers[0].id;
doc.insert(Some(layer), usize::MAX, n).unwrap();
let out = write_titled(&doc, "\u{65e5}\u{672c}");
let text = String::from_utf8(out.bytes.clone()).unwrap();
for needle in [
"%%Title: (??)\n",
"%%DocumentCustomColors: (?)\n%%+ (? 2)\n",
"%%CMYKCustomColor: 0 0.5 1 0 (?)\n%%+ 0.5 0.5 1 0 (? 2)\n",
"0 0.5 1 0 (?) 0.5 x\n",
"0.5 0.5 1 0 (? 2) 0.5 x\n",
"(??) 0 2 Bd\n",
"(?? 2) 0 2 Bd\n",
"0 0.5 1 0 (?) 0 3 50 0 %_Bs\n",
"1 (??) ",
"1 (?? 2) ",
] {
assert!(text.contains(needle), "missing {needle:?} in\n{text}");
}
assert_eq!(out.warnings.iter().filter(|w| *w == TITLE_AND_COLOR_NAMES).count(), 1, "{:?}", out.warnings);
assert!(!out.warnings.iter().any(|w| w == LAYER_NAME), "{:?}", out.warnings);
let read = crate::import(&out.bytes).unwrap().document;
let want = [Some("?"), Some("? 2"), Some("??"), Some("?"), Some("?? 2"), None].map(|n| n.map(String::from));
assert_eq!(inks(&read), want);
}
#[test]
fn losses_are_warned() {
let mut doc = sample();
+2 -1
View File
@@ -67,7 +67,8 @@ private editing data. VectorCraft:
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; layer
names are written in Windows-1252, with `?` for characters outside it.
names, spot color and gradient names and the title are written in Windows-1252, with `?` for characters outside
it (two spot colors or two gradients whose names become the same are numbered).
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