mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
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:
+81
-29
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user