mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
refactor(eps): move legacy_ai.rs items to legacy_ai/{encoding,prolog,writer}.rs
This commit is contained in:
+11
-1249
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
use super::writer::Writer;
|
||||
use super::*;
|
||||
|
||||
/// A gradient definition: `%AI5_BeginGradient: (name)`, `(name) type stops Bd`, `[`, the stops
|
||||
/// (`colorSpec colorStyle midPoint rampPoint %_Bs`, each with its midpoint to the next stop), `BD`,
|
||||
/// `%AI5_EndGradient`. A linear gradient's stops are listed from the last to the first (§1.4.3). A
|
||||
/// radial gradient's stops are listed from the first to the last, as in §3.5.2's radial
|
||||
/// definition (10, then 100).
|
||||
pub(super) fn gradient_def(s: &mut String, w: &Writer, name: &str, g: &Gradient) {
|
||||
let name = latin_string(name);
|
||||
let radial = g.kind == GradientKind::Radial;
|
||||
let kind = if radial { 1 } else { 0 };
|
||||
let _ = writeln!(s, "%AI5_BeginGradient: {name}\n{name} {kind} {} Bd\n[", g.stops.len());
|
||||
let mut stops: Vec<_> = g.stops.iter().collect();
|
||||
if !radial {
|
||||
stops.reverse();
|
||||
}
|
||||
for stop in stops {
|
||||
let paint = Paint::Solid { color: stop.color, swatch: stop.swatch.clone(), tint: stop.tint };
|
||||
match w.ink(&paint) {
|
||||
Ink::Gray(v) => {
|
||||
ps::push_num(s, f64::from(v));
|
||||
s.push_str(" 0 ");
|
||||
}
|
||||
Ink::Cmyk(c) => {
|
||||
ps::push_nums(s, &c.map(f64::from));
|
||||
s.push_str("1 ");
|
||||
}
|
||||
Ink::Rgb(c, rgb) => {
|
||||
ps::push_nums(s, &c.map(f64::from));
|
||||
ps::push_nums(s, &rgb.map(f64::from));
|
||||
s.push_str("2 ");
|
||||
}
|
||||
Ink::Spot { cmyk, name, tint } => {
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
let _ = write!(s, "{} ", latin_string(w.spot_name(&name)));
|
||||
ps::push_num(s, f64::from(1.0 - tint));
|
||||
s.push_str(" 3 ");
|
||||
}
|
||||
}
|
||||
ps::push_nums(s, &[f64::from(stop.midpoint) * 100.0, f64::from(stop.offset) * 100.0]);
|
||||
s.push_str("%_Bs\n");
|
||||
}
|
||||
s.push_str("BD\n%AI5_EndGradient\n");
|
||||
}
|
||||
|
||||
/// A colour as the format writes it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum Ink {
|
||||
/// Lightness (1 is white).
|
||||
Gray(f32),
|
||||
Cmyk([f32; 4]),
|
||||
/// RGB, with its CMYK equivalent.
|
||||
Rgb([f32; 4], [f32; 3]),
|
||||
/// A spot colour's full CMYK equivalent, name and tint (1: full).
|
||||
Spot {
|
||||
cmyk: [f32; 4],
|
||||
name: String,
|
||||
tint: f32,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn unit(v: f32) -> f32 {
|
||||
if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Can `a` be drawn through (a finite, non-flat map)?
|
||||
pub(super) fn invertible(a: Affine) -> bool {
|
||||
let det = a.determinant();
|
||||
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).
|
||||
pub(super) fn latin_string(name: &str) -> String {
|
||||
let bytes: Vec<u8> = name.chars().filter_map(latin::encode).collect();
|
||||
ps::bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Can type `t` be written as text: horizontal, without ruby (which a text object can't hold), its
|
||||
/// runs plain characters (no inline graphics) that 8-bit text can hold, filled and stroked with
|
||||
/// colours?
|
||||
pub(super) fn writable(t: &TextObject) -> bool {
|
||||
let solid = |p: &Paint| matches!(p, Paint::Solid { .. } | Paint::None);
|
||||
!t.vertical
|
||||
&& t.ruby.is_empty()
|
||||
&& t.runs.iter().any(|r| !r.text.is_empty())
|
||||
&& t.runs.iter().all(|r| {
|
||||
r.inline.is_none()
|
||||
&& solid(&r.style.fill)
|
||||
&& solid(&r.style.stroke)
|
||||
&& (if r.style.all_caps { r.text.to_uppercase() } else { r.text.clone() }).chars().all(|c| latin::encode(c).is_some())
|
||||
})
|
||||
}
|
||||
|
||||
/// Has type `t` attributes a legacy `.ai` text object can't hold (lost when it's written as text)?
|
||||
pub(super) fn lost_attributes(t: &TextObject) -> bool {
|
||||
let run_lost = |s: &CharStyle| {
|
||||
s.underline
|
||||
|| s.strikethrough
|
||||
|| s.small_caps.is_some()
|
||||
|| s.position != CharPosition::Normal
|
||||
|| s.rotation != 0.0
|
||||
|| !s.features.is_empty()
|
||||
|| !s.font_axes.is_empty()
|
||||
};
|
||||
let para_lost = |p: &ParaStyle| p.space_after != 0.0 || !p.tabs.is_empty() || p.list.is_some();
|
||||
t.runs.iter().any(|r| run_lost(&r.style))
|
||||
|| t.paragraph_styles().iter().any(para_lost)
|
||||
|| (matches!(t.kind, TextKind::Area { .. }) && t.area != AreaOptions::default())
|
||||
|| (matches!(t.kind, TextKind::OnPath { .. }) && (t.path_effect != PathEffect::default() || t.path_spacing != 0.0))
|
||||
}
|
||||
|
||||
/// The font type in style `st` is set in: its PostScript name, whether it's TrueType, and its
|
||||
/// ascent and descent (ems). A font that isn't installed is named after its family and style.
|
||||
pub(super) fn font_of(st: &CharStyle) -> (String, bool, f64, f64) {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
match db.face_version(&st.font_family, &st.font_style, st.font_version.as_deref()) {
|
||||
Some(face) => {
|
||||
let (ascent, descent) = face.vertical_metrics();
|
||||
let em = face.units_per_em().max(1.0);
|
||||
let name = face.postscript_name();
|
||||
(name, face.is_truetype(), ascent / em, descent / em)
|
||||
}
|
||||
None => {
|
||||
let squeeze = |s: &str| -> String { s.chars().filter(|c| c.is_ascii_alphanumeric()).collect() };
|
||||
let style = squeeze(&st.font_style);
|
||||
let name =
|
||||
if style.is_empty() || style == "Regular" { squeeze(&st.font_family) } else { format!("{}-{style}", squeeze(&st.font_family)) };
|
||||
(if name.is_empty() { "Helvetica".into() } else { name }, false, 0.75, 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `v`, or `default` when it isn't finite.
|
||||
pub(super) fn finite(v: f64, default: f64) -> f64 {
|
||||
if v.is_finite() { v } else { default }
|
||||
}
|
||||
|
||||
/// Where a fraction of `path`'s length is, as the start of type on a path gives it (§15.5.2): the
|
||||
/// index of the segment it's on, plus how far along that segment (its curve parameter).
|
||||
pub(super) fn segment_point(path: &BezPath, fraction: f64) -> f64 {
|
||||
use kurbo::ParamCurveArclen;
|
||||
const ACCURACY: f64 = 1e-3;
|
||||
let segments: Vec<kurbo::PathSeg> = path.segments().collect();
|
||||
let lengths: Vec<f64> = segments.iter().map(|s| s.arclen(ACCURACY)).collect();
|
||||
let total: f64 = lengths.iter().sum();
|
||||
if !(total.is_finite() && total > 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
let mut left = fraction.clamp(0.0, 1.0) * total;
|
||||
for (i, (seg, len)) in segments.iter().zip(&lengths).enumerate() {
|
||||
if left <= *len || i + 1 == segments.len() {
|
||||
let t = if *len > 0.0 { seg.inv_arclen(left.min(*len), ACCURACY) } else { 0.0 };
|
||||
return i as f64 + t.clamp(0.0, 0.9999);
|
||||
}
|
||||
left -= len;
|
||||
}
|
||||
0.0
|
||||
}
|
||||
|
||||
/// Can `p` paint a path object's fill directly (a colour or a linear or radial gradient)?
|
||||
pub(super) fn direct_fill(p: &Paint) -> bool {
|
||||
match p {
|
||||
Paint::Solid { .. } => true,
|
||||
Paint::Gradient(g) => g.gradient.kind != GradientKind::Freeform && !g.gradient.stops.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Can stroke `s` be written as the stroke of a path object (a plain line of a colour)?
|
||||
pub(super) fn direct_stroke(s: &StrokeLayer) -> bool {
|
||||
matches!(s.paint, Paint::Solid { .. }) && stroke::is_plain(s)
|
||||
}
|
||||
|
||||
/// The average colour of a gradient's stops (in RGB).
|
||||
pub(super) fn average(g: &Gradient) -> Color {
|
||||
let n = g.stops.len().max(1) as f32;
|
||||
let sum = g.stops.iter().map(|s| s.color.to_rgb()).fold([0.0f32; 3], |a, c| [a[0] + c[0], a[1] + c[1], a[2] + c[2]]);
|
||||
Color::rgb(unit(sum[0] / n), unit(sum[1] / n), unit(sum[2] / n))
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
|
||||
/// The setup's fonts (§3.1, §3.3): `%%IncludeFont` for each, the encoding strings are in (`TE`),
|
||||
/// and each font re-encoded to it as `_name` (`TZ`; a TrueType font takes the encoding as its own,
|
||||
/// not the standard one, as the specification says of fonts that aren't Type 1).
|
||||
pub(super) fn fonts(s: &mut String, fonts: &[(String, bool)]) {
|
||||
if fonts.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (name, _) in fonts {
|
||||
let _ = writeln!(s, "%%IncludeFont: {name}");
|
||||
}
|
||||
let te = latin::te();
|
||||
s.push_str(&te);
|
||||
// The encoding pairs, without `[` and `TE`.
|
||||
let pairs = te.trim_start_matches('[').trim_end().trim_end_matches("TE").trim_end();
|
||||
for (name, truetype) in fonts {
|
||||
let _ = writeln!(s, "%AI3_BeginEncoding: _{name} {name}");
|
||||
if *truetype {
|
||||
let _ = writeln!(s, "[{pairs}\n/_{name}/{name} 0 0 0 TZ\n%AI3_EndEncoding TrueType");
|
||||
} else {
|
||||
let _ = writeln!(s, "[/_{name}/{name} 0 0 1 TZ\n%AI3_EndEncoding AdobeType");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The prolog's procedure set, as `%%DocumentSuppliedResources` names it: a file that has it is
|
||||
/// read from its program when its layers are asked for ([`crate::import_with`]).
|
||||
pub(crate) const PROCSET: &str = "VectorCraft_Legacy 1 0";
|
||||
|
||||
/// The prolog: a procedure set of this writer's own (`VectorCraft_Legacy 1 0`), in a dictionary
|
||||
/// (`VCLegacy`) the setup begins and the trailer ends (§1.2: the prolog holds definitions only,
|
||||
/// the setup activates them, the trailer deactivates them). It gives each operator the script
|
||||
/// uses its meaning in PostScript, so that the file prints and previews as a PostScript program,
|
||||
/// written from the PostScript Language Reference and this format's specification (no other
|
||||
/// application's procedure set): paths and their painting with the fill and stroke colours,
|
||||
/// compound paths painted as one path, clipping groups, guides that don't print, layers that are
|
||||
/// hidden or don't print marking nothing, and images drawn from the samples on the comment lines
|
||||
/// that follow `XI`. Gradients fill with the colour set before them (the writer sets the first
|
||||
/// stop's), and type marks nothing: apps that read the format draw both. The page trailer's
|
||||
/// `annotatepage` (§3, Script Setup: `gsave annotatepage grestore showpage`) marks nothing.
|
||||
pub(super) const PROLOG: &str = "%%BeginResource: procset VectorCraft_Legacy 1 0
|
||||
/VCLegacy 96 dict def
|
||||
VCLegacy begin
|
||||
/bd {bind def} bind def
|
||||
% State: the fill and stroke colours (procedures that set them), their overprints, the fill rule,
|
||||
% a clip to make at the next painting operator, a compound path being built, a guide being
|
||||
% drawn, whether the layer prints, and what the compound path is painted with.
|
||||
/_fc {0 setgray} def /_sc {0 setgray} def
|
||||
/_fo false def /_so false def /_eo false def /_w false def /_cp false def /_g false def
|
||||
/_on true def /_rf false def /_rs false def
|
||||
% Painting: a fill and a stroke that keep the path; the path painted as _rf and _rs say (after
|
||||
% the clip W asked for), then ended.
|
||||
/_fill {gsave _fc _fo setoverprint _eo {eofill} {fill} ifelse grestore} bd
|
||||
/_stroke {gsave _sc _so setoverprint stroke grestore} bd
|
||||
/_draw {_w {_eo {eoclip} {clip} ifelse /_w false def} if
|
||||
_on _g not and {_rf {_fill} if _rs {_stroke} if} if newpath} bd
|
||||
% close? fill? stroke? _r: paints now, or at the end of the compound path being built.
|
||||
/_r {/_rs exch def /_rf exch def {closepath} if _cp not {_draw} if} bd
|
||||
% Path construction (section 5.3.2) and painting (5.3.3).
|
||||
/m {moveto} bd /l {lineto} bd /L {lineto} bd /c {curveto} bd /C {curveto} bd
|
||||
/N {false false false _r} bd /n {true false false _r} bd
|
||||
/F {false true false _r} bd /f {true true false _r} bd
|
||||
/S {false false true _r} bd /s {true false true _r} bd
|
||||
/B {false true true _r} bd /b {true true true _r} bd
|
||||
/XR {1 eq /_eo exch def} bd
|
||||
% Compound paths (5.3.5): one path, painted at its end.
|
||||
/*u {/_cp true def} bd /*U {/_cp false def _draw} bd
|
||||
% Groups, clipping groups and masks (5.6), locking (5.1), guides (6.1).
|
||||
/u {} bd /U {} bd /q {gsave} bd /Q {grestore} bd
|
||||
/H {} bd /h {closepath} bd /W {/_w true def} bd
|
||||
/A {pop} bd
|
||||
/* {pop _g {newpath /_g false def} {/_g true def} ifelse} bd
|
||||
% Colours (5.4): fill and stroke colours, overprints; a custom colour as its CMYK equivalent at
|
||||
% its tint (tint 0 is the full colour).
|
||||
/g {/setgray load 2 array astore cvx /_fc exch def} bd
|
||||
/G {/setgray load 2 array astore cvx /_sc exch def} bd
|
||||
/k {/setcmykcolor load 5 array astore cvx /_fc exch def} bd
|
||||
/K {/setcmykcolor load 5 array astore cvx /_sc exch def} bd
|
||||
/Xa {/setrgbcolor load 4 array astore cvx /_fc exch def} bd
|
||||
/XA {/setrgbcolor load 4 array astore cvx /_sc exch def} bd
|
||||
/_tint {1 exch sub /_t exch def pop 4 {_t mul 4 1 roll} repeat} bd
|
||||
/x {_tint k} bd /X {_tint K} bd
|
||||
/O {1 eq /_fo exch def} bd /R {1 eq /_so exch def} bd
|
||||
/w {setlinewidth} bd /J {setlinecap} bd /j {setlinejoin} bd /M {setmiterlimit} bd /d {setdash} bd
|
||||
% Gradients (3.5): definitions are skipped; an instance fills with the colour set before it
|
||||
% (the writer sets its first stop's), and BB strokes the path as its flag says.
|
||||
/Bn {pop} bd /Bd {pop pop pop} bd /BD {cleartomark} bd
|
||||
/Bb {gsave} bd /Bg {12 {pop} repeat} bd /Bh {pop pop pop pop} bd
|
||||
/BB {grestore _cp {0 gt {/_rs true def} if}
|
||||
{dup 2 eq {closepath} if 0 gt _on and {_stroke} if newpath} ifelse} bd
|
||||
% Layers (9): a layer that is hidden or doesn't print marks nothing.
|
||||
/Lb {6 {pop} repeat exch pop exch pop 1 eq exch 1 eq and /_on exch def} bd
|
||||
/Ln {pop} bd /LB {/_on true def} bd
|
||||
% Images (8): the samples, in hex on the comment lines that follow XI, read a line at a time.
|
||||
/_l 256 string def /_s 128 string def
|
||||
/_row {{currentfile _l readline not {exit} if dup length 1 gt {exit} if pop} loop
|
||||
dup length 1 gt {dup length 1 sub 1 exch getinterval /ASCIIHexDecode filter _s readstring pop} {pop ()} ifelse} bd
|
||||
/XI {pop pop pop pop /_ic exch def /_ib exch def /_ih exch def /_iw exch def pop pop pop pop
|
||||
[1 0 0 -1 0 0] exch matrix concatmatrix
|
||||
_on {gsave concat _iw _ih _ib [1 0 0 1 0 0] /_row load false _ic 1 eq {1} {_ic} ifelse colorimage grestore} {pop} ifelse} bd
|
||||
% Type (15) is set by the app that reads the file: its operators take their operands and mark
|
||||
% nothing; encodings (3.3) are skipped.
|
||||
/TE {cleartomark} bd /TZ {cleartomark} bd
|
||||
/To {pop} bd /Tp {7 {pop} repeat} bd /TP {} bd /TO {} bd
|
||||
/Tr {pop} bd /Tf {pop pop pop pop} bd /Ts {pop} bd /Tz {pop pop} bd /Tt {pop} bd /TA {pop} bd
|
||||
/Tl {pop pop} bd /Ta {pop} bd /Ti {pop pop pop} bd /Tx {pop} bd
|
||||
% The page trailer (3): annotatepage marks nothing.
|
||||
/annotatepage {} bd
|
||||
end
|
||||
%%EndResource
|
||||
";
|
||||
|
||||
/// The first line of a PostScript file (the DSC version comment).
|
||||
pub(super) const PS_HEADER: &str = "%!PS-Adobe-3.0"; // brand-ok: the PostScript header comment
|
||||
@@ -0,0 +1,960 @@
|
||||
use super::encoding::{
|
||||
Ink, average, direct_fill, direct_stroke, finite, font_of, invertible, latin_string, lost_attributes, segment_point, unit, writable,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) struct Writer<'a> {
|
||||
doc: &'a Document,
|
||||
/// The document → art space.
|
||||
to_art: Affine,
|
||||
cms: Arc<Cms>,
|
||||
pub(super) body: String,
|
||||
/// Gradient definitions, by the name objects use.
|
||||
pub(super) gradients: Vec<(String, Gradient)>,
|
||||
/// Spot colors used: the name written and full CMYK equivalent.
|
||||
pub(super) 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)>,
|
||||
pub(super) warnings: Vec<String>,
|
||||
/// The lock state the last `A` set.
|
||||
locked: bool,
|
||||
/// Symbols and live objects being written inside one another.
|
||||
nest: u32,
|
||||
/// What to do with hidden objects.
|
||||
hidden: HiddenObjects,
|
||||
/// Type as outlines.
|
||||
outline_text: bool,
|
||||
/// The layer being written's hidden objects for its hidden layer, and whether each is locked.
|
||||
hidden_art: Vec<(Node, bool)>,
|
||||
/// Writing a layer that is hidden (or the hidden layer of one's hidden objects): hidden objects
|
||||
/// are written where they are.
|
||||
in_hidden: bool,
|
||||
/// The fonts type is set in: PostScript name, and whether it's TrueType.
|
||||
pub(super) fonts: Vec<(String, bool)>,
|
||||
}
|
||||
|
||||
impl<'a> Writer<'a> {
|
||||
pub(super) fn new(doc: &'a Document, to_art: Affine, o: &LegacyAiOptions) -> Self {
|
||||
Self {
|
||||
doc,
|
||||
to_art,
|
||||
cms: vectorcraft_color::cms::active(),
|
||||
body: String::new(),
|
||||
gradients: vec![],
|
||||
spots: vec![],
|
||||
spot_names: vec![],
|
||||
warnings: vec![],
|
||||
locked: false,
|
||||
nest: 0,
|
||||
hidden: o.hidden,
|
||||
outline_text: o.outline_text,
|
||||
hidden_art: vec![],
|
||||
in_hidden: false,
|
||||
fonts: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(&mut self, w: &str) {
|
||||
if !self.warnings.iter().any(|x| x == w) {
|
||||
self.warnings.push(w.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn line(&mut self, s: &str) {
|
||||
self.body.push_str(s);
|
||||
self.body.push('\n');
|
||||
}
|
||||
|
||||
// ---------- layers and objects ----------
|
||||
|
||||
/// `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.
|
||||
pub(super) fn latin_name(&mut self, name: &str, warning: &str) -> String {
|
||||
let mut lost = false;
|
||||
let kept = name
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if latin::encode(c).is_some() {
|
||||
c
|
||||
} else {
|
||||
lost = true;
|
||||
'?'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if lost {
|
||||
self.warn(warning);
|
||||
}
|
||||
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.
|
||||
pub(super) 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.
|
||||
pub(super) fn layer(&mut self, n: &Node) -> bool {
|
||||
let NodeKind::Layer { color, template, printable, children, clip, preview, dim_images } = &n.kind else { return false };
|
||||
let [r, g, b] = match color {
|
||||
LayerColor::Preset(i) => LAYER_COLORS.get(usize::from(*i)).map_or([79, 128, 255], |c| c.1),
|
||||
LayerColor::Custom(rgb) => *rgb,
|
||||
};
|
||||
let index = match color {
|
||||
LayerColor::Preset(i) if usize::from(*i) < LAYER_COLORS.len() => i64::from(*i),
|
||||
_ => -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,
|
||||
"%AI5_BeginLayer\n{} {} {} {} {} 0 {index} {r} {g} {b} Lb\n{} Ln",
|
||||
flag(n.visible),
|
||||
flag(*preview),
|
||||
flag(!n.locked),
|
||||
flag(*printable && !*template),
|
||||
flag(dim_images.is_some() || *template),
|
||||
name,
|
||||
);
|
||||
self.locked = false;
|
||||
self.line("0 A");
|
||||
self.in_hidden = !n.visible;
|
||||
self.contents(children, *clip, false);
|
||||
self.in_hidden = false;
|
||||
self.line("LB\n%AI5_EndLayer--");
|
||||
true
|
||||
}
|
||||
|
||||
/// The hidden layer of top-level layer `n`'s hidden objects, just above it (named after it,
|
||||
/// with its colour, lock and printing). False when it has none.
|
||||
pub(super) fn hidden_layer(&mut self, n: &Node) -> bool {
|
||||
if self.hidden_art.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let art = std::mem::take(&mut self.hidden_art);
|
||||
let NodeKind::Layer { color, template, printable, .. } = &n.kind else { return false };
|
||||
let [r, g, b] = match color {
|
||||
LayerColor::Preset(i) => LAYER_COLORS.get(usize::from(*i)).map_or([79, 128, 255], |c| c.1),
|
||||
LayerColor::Custom(rgb) => *rgb,
|
||||
};
|
||||
let index = match color {
|
||||
LayerColor::Preset(i) if usize::from(*i) < LAYER_COLORS.len() => i64::from(*i),
|
||||
_ => -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),
|
||||
name,
|
||||
);
|
||||
self.locked = false;
|
||||
self.line("0 A");
|
||||
self.in_hidden = true;
|
||||
for (node, locked) in &art {
|
||||
self.node(node, *locked);
|
||||
}
|
||||
self.in_hidden = false;
|
||||
self.line("LB\n%AI5_EndLayer--");
|
||||
true
|
||||
}
|
||||
|
||||
/// A container's members, the first clipping the others when `clip`; `locked`: the container is.
|
||||
fn contents(&mut self, children: &[Arc<Node>], clip: bool, locked: bool) {
|
||||
if clip {
|
||||
self.clip_group(children, locked);
|
||||
} else {
|
||||
for c in children {
|
||||
self.node(c, locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lock state of the objects that follow.
|
||||
fn lock(&mut self, locked: bool) {
|
||||
if locked != self.locked {
|
||||
self.locked = locked;
|
||||
self.line(if locked { "1 A" } else { "0 A" });
|
||||
}
|
||||
}
|
||||
|
||||
/// One object (inside a container locked when `locked`).
|
||||
fn node(&mut self, n: &Node, locked: bool) {
|
||||
if !n.visible && !self.in_hidden {
|
||||
match self.hidden {
|
||||
HiddenObjects::Omit => {
|
||||
self.warn(HIDDEN);
|
||||
return;
|
||||
}
|
||||
HiddenObjects::Layer => {
|
||||
self.warn(HIDDEN_LAYER);
|
||||
self.hidden_art.push((n.clone(), locked || n.locked));
|
||||
return;
|
||||
}
|
||||
HiddenObjects::Visible => self.warn(HIDDEN_SHOWN),
|
||||
}
|
||||
}
|
||||
if n.opacity < 1.0 || n.blend != BlendMode::Normal || n.extras().mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
self.warn(TRANSPARENCY);
|
||||
}
|
||||
if n.name.as_deref().is_some_and(|s| !s.is_empty()) && !n.is_layer() {
|
||||
self.warn(NAMES);
|
||||
}
|
||||
let locked = locked || n.locked;
|
||||
match &n.kind {
|
||||
NodeKind::Layer { children, clip, .. } => {
|
||||
self.warn(SUBLAYERS);
|
||||
self.group(children, *clip, locked);
|
||||
}
|
||||
NodeKind::Group { children, clip } => self.group(children, *clip, locked),
|
||||
NodeKind::Path { path, rule, guide, .. } => {
|
||||
self.lock(locked);
|
||||
if *guide {
|
||||
self.guide(path);
|
||||
} else {
|
||||
self.shape(n, path, *rule);
|
||||
}
|
||||
}
|
||||
NodeKind::Compound { children, rule } => {
|
||||
self.lock(locked);
|
||||
let mut subpaths = vec![];
|
||||
for p in children.iter().filter_map(|c| c.path_data()) {
|
||||
subpaths.extend(p.subpaths.iter().cloned());
|
||||
}
|
||||
self.shape(n, &PathData::new(subpaths), *rule);
|
||||
}
|
||||
NodeKind::Text(t) => {
|
||||
self.lock(locked);
|
||||
self.text(t);
|
||||
}
|
||||
NodeKind::Image(im) => {
|
||||
self.lock(locked);
|
||||
self.image(self.doc.image_blob(im), im.width, im.height, im.xf);
|
||||
}
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
self.warn(LIVE);
|
||||
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
{
|
||||
let mut art = (*sym.art).clone();
|
||||
art.transform(*xf, false);
|
||||
art.visible = true;
|
||||
self.nest += 1;
|
||||
self.node(&art, locked);
|
||||
self.nest -= 1;
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
self.warn(LIVE);
|
||||
if self.nest < MAX_NEST {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
self.nest += 1;
|
||||
if let Some(children) = g.children() {
|
||||
self.group(children, false, locked);
|
||||
}
|
||||
self.nest -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A group (`u` … `U`), or a clipping group (`q` … `Q`).
|
||||
fn group(&mut self, children: &[Arc<Node>], clip: bool, locked: bool) {
|
||||
if children.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.lock(locked);
|
||||
if clip {
|
||||
self.clip_group(children, locked);
|
||||
} else {
|
||||
self.line("u");
|
||||
self.contents(children, false, locked);
|
||||
self.lock(locked);
|
||||
self.line("U");
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipping group: its first member's outline clips the others (`h W n`). That member's fills
|
||||
/// paint behind the art, inside the clip, and its strokes over it, outside.
|
||||
fn clip_group(&mut self, children: &[Arc<Node>], locked: bool) {
|
||||
let Some((clip, rest)) = children.split_first() else { return };
|
||||
// Nothing to clip by hides the clipped art.
|
||||
let Some((outline, rule)) = vectorcraft_effects::clip_outline(clip) else { return };
|
||||
let paint = clip.clip_paint();
|
||||
self.lock(locked);
|
||||
self.line("q");
|
||||
let path = PathData::from_bezpath(&outline);
|
||||
self.fill_rule(rule);
|
||||
let several = path.subpaths.len() > 1;
|
||||
if several {
|
||||
self.line("*u");
|
||||
}
|
||||
for sp in path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()) {
|
||||
self.mask(sp);
|
||||
}
|
||||
if several {
|
||||
self.line("*U");
|
||||
}
|
||||
if let Some(fill) = &paint.fill {
|
||||
self.node(fill, locked);
|
||||
}
|
||||
for c in rest {
|
||||
self.node(c, locked);
|
||||
}
|
||||
self.lock(locked);
|
||||
self.line("Q");
|
||||
if let Some(stroke) = &paint.stroke {
|
||||
self.node(stroke, locked);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- paths ----------
|
||||
|
||||
/// A clipping path (§5.6.2): its points, `h` (closed) or `H`, `W`, then painted with nothing.
|
||||
fn mask(&mut self, sp: &SubPath) {
|
||||
self.subpath(sp);
|
||||
self.line(if sp.closed { "h\nW\nn" } else { "H\nW\nN" });
|
||||
}
|
||||
|
||||
/// A guide (§6): `(N) *`, its path, `(N) *`.
|
||||
fn guide(&mut self, path: &PathData) {
|
||||
for sp in path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()) {
|
||||
self.line("(N) *");
|
||||
self.subpath(sp);
|
||||
self.line("(N) *");
|
||||
}
|
||||
}
|
||||
|
||||
/// A path's fills and strokes: one object with one fill and a plain stroke over it where it
|
||||
/// can be, else a group of an object for each (in paint order).
|
||||
fn shape(&mut self, n: &Node, path: &PathData, rule: FillRule) {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
let items: Vec<&AppearanceItem> = n
|
||||
.appearance
|
||||
.items
|
||||
.iter()
|
||||
.filter(|i| match i {
|
||||
AppearanceItem::Fill(f) => f.visible && !f.paint.is_none(),
|
||||
AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && s.width.is_finite(),
|
||||
})
|
||||
.collect();
|
||||
for i in &items {
|
||||
if i.opacity() < 1.0 || i.blend() != BlendMode::Normal {
|
||||
self.warn(TRANSPARENCY);
|
||||
}
|
||||
}
|
||||
match items.as_slice() {
|
||||
[] => self.path_object(path, rule, None, None),
|
||||
[AppearanceItem::Fill(f)] if direct_fill(&f.paint) => self.path_object(path, rule, Some(f), None),
|
||||
[AppearanceItem::Stroke(s)] if direct_stroke(s) => self.path_object(path, rule, None, Some(s)),
|
||||
[AppearanceItem::Fill(f), AppearanceItem::Stroke(s)] if direct_fill(&f.paint) && direct_stroke(s) => {
|
||||
self.path_object(path, rule, Some(f), Some(s))
|
||||
}
|
||||
[one] => self.item(path, rule, one),
|
||||
items => {
|
||||
self.line("u");
|
||||
for i in items {
|
||||
self.item(path, rule, i);
|
||||
}
|
||||
self.line("U");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One fill or stroke of a path as an object of its own.
|
||||
fn item(&mut self, path: &PathData, rule: FillRule, item: &AppearanceItem) {
|
||||
match item {
|
||||
AppearanceItem::Fill(f) => self.fill_object(path, rule, f),
|
||||
AppearanceItem::Stroke(s) if direct_stroke(s) => self.path_object(path, rule, None, Some(s)),
|
||||
AppearanceItem::Stroke(s) => {
|
||||
self.warn(STROKES);
|
||||
let outline = stroke::outline_region(path, rule, s);
|
||||
let fill = FillLayer { overprint: s.overprint, ..FillLayer::new(s.paint.clone()) };
|
||||
self.fill_object(&outline, FillRule::NonZero, &fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A fill as an object of its own: patterns as their tiles clipped to the path.
|
||||
fn fill_object(&mut self, path: &PathData, rule: FillRule, f: &FillLayer) {
|
||||
if direct_fill(&f.paint) {
|
||||
return self.path_object(path, rule, Some(f), None);
|
||||
}
|
||||
let Paint::Pattern { pattern, xf } = &f.paint else {
|
||||
// A freeform gradient: its average colour.
|
||||
self.warn(FREEFORM);
|
||||
let color = match &f.paint {
|
||||
Paint::Gradient(g) => average(&g.gradient),
|
||||
_ => Color::gray(0.5),
|
||||
};
|
||||
let flat = FillLayer { paint: Paint::solid(color), ..f.clone() };
|
||||
return self.path_object(path, rule, Some(&flat), None);
|
||||
};
|
||||
self.warn(PATTERNS);
|
||||
let doc = self.doc;
|
||||
let Some(def) = doc.pattern(pattern) else {
|
||||
let flat = FillLayer { paint: Paint::solid(Color::gray(0.5)), ..f.clone() };
|
||||
return self.path_object(path, rule, Some(&flat), None);
|
||||
};
|
||||
let area = path.to_bezpath().bounding_box();
|
||||
let tiles = def.instances_in(*xf, area);
|
||||
self.line("q");
|
||||
self.fill_rule(rule);
|
||||
let several = path.subpaths.len() > 1;
|
||||
if several {
|
||||
self.line("*u");
|
||||
}
|
||||
for sp in path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()) {
|
||||
self.mask(sp);
|
||||
}
|
||||
if several {
|
||||
self.line("*U");
|
||||
}
|
||||
let locked = self.locked;
|
||||
for t in &tiles {
|
||||
self.node(t, locked);
|
||||
}
|
||||
self.lock(locked);
|
||||
self.line("Q");
|
||||
}
|
||||
|
||||
/// A path object painted with `fill` (a colour or a linear or radial gradient) and `stroke`
|
||||
/// (a plain line of a colour) over it; several subpaths make a compound path.
|
||||
fn path_object(&mut self, path: &PathData, rule: FillRule, fill: Option<&FillLayer>, stroke: Option<&StrokeLayer>) {
|
||||
let subpaths: Vec<&SubPath> = path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).collect();
|
||||
if subpaths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let gradient = match fill.map(|f| &f.paint) {
|
||||
Some(Paint::Gradient(g)) => Some(self.gradient(g, path.to_bezpath().bounding_box())),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(f) = fill {
|
||||
let paint = match &f.paint {
|
||||
// The colour under the gradient: its first stop's.
|
||||
Paint::Gradient(g) => g.gradient.stops.first().map_or(Paint::None, |s| Paint::solid(s.color)),
|
||||
p => p.clone(),
|
||||
};
|
||||
self.color(&paint, false);
|
||||
self.line(if f.overprint { "1 O" } else { "0 O" });
|
||||
}
|
||||
if let Some(s) = stroke {
|
||||
self.color(&s.paint, true);
|
||||
self.line(if s.overprint { "1 R" } else { "0 R" });
|
||||
self.line_style(s);
|
||||
}
|
||||
self.fill_rule(rule);
|
||||
let op = match (fill.is_some(), stroke.is_some()) {
|
||||
(true, true) => 'b',
|
||||
(true, false) => 'f',
|
||||
(false, true) => 's',
|
||||
(false, false) => 'n',
|
||||
};
|
||||
let several = subpaths.len() > 1;
|
||||
if several {
|
||||
self.line("*u");
|
||||
}
|
||||
for sp in subpaths {
|
||||
self.subpath(sp);
|
||||
if let Some(g) = &gradient {
|
||||
// The gradient fills between `Bb` and `BB`, whose flag strokes the path too (§3.5.3).
|
||||
self.body.push_str(g);
|
||||
self.line(if sp.closed { "f" } else { "F" });
|
||||
self.line(match (stroke.is_some(), sp.closed) {
|
||||
(false, _) => "0 BB",
|
||||
(true, false) => "1 BB",
|
||||
(true, true) => "2 BB",
|
||||
});
|
||||
} else {
|
||||
self.line(&if sp.closed { op.to_string() } else { op.to_ascii_uppercase().to_string() });
|
||||
}
|
||||
}
|
||||
if several {
|
||||
self.line("*U");
|
||||
}
|
||||
}
|
||||
|
||||
/// `rule` as `XR` (1: even-odd).
|
||||
fn fill_rule(&mut self, rule: FillRule) {
|
||||
self.line(if rule == FillRule::EvenOdd { "1 XR" } else { "0 XR" });
|
||||
}
|
||||
|
||||
/// One subpath's points, in art space: `m`, then a line or curve to each point (lower case: the
|
||||
/// point is smooth, upper case: a corner, §5.3.2), and for a closed one the segment back to the
|
||||
/// start, which says whether the start is smooth or a corner (as the specification's examples
|
||||
/// close paths: the start point again, then the painting operator).
|
||||
fn subpath(&mut self, sp: &SubPath) {
|
||||
let Some(first) = sp.anchors.first() else { return };
|
||||
let xf = self.to_art;
|
||||
let s = &mut self.body;
|
||||
let pt = |s: &mut String, p: Point| {
|
||||
let p = xf * p;
|
||||
ps::push_nums(s, &[p.x, p.y]);
|
||||
};
|
||||
pt(s, first.p);
|
||||
s.push_str("m\n");
|
||||
let segment = |s: &mut String, a: &vectorcraft_geom::Anchor, b: &vectorcraft_geom::Anchor| {
|
||||
let straight = a.h_out == a.p && b.h_in == b.p;
|
||||
let smooth = b.kind == AnchorKind::Smooth;
|
||||
if straight {
|
||||
pt(s, b.p);
|
||||
s.push_str(if smooth { "l\n" } else { "L\n" });
|
||||
} else {
|
||||
pt(s, a.h_out);
|
||||
pt(s, b.h_in);
|
||||
pt(s, b.p);
|
||||
s.push_str(if smooth { "c\n" } else { "C\n" });
|
||||
}
|
||||
};
|
||||
for pair in sp.anchors.windows(2) {
|
||||
if let [a, b] = pair {
|
||||
segment(s, a, b);
|
||||
}
|
||||
}
|
||||
if sp.closed
|
||||
&& sp.anchors.len() > 1
|
||||
&& let Some(last) = sp.anchors.last()
|
||||
{
|
||||
segment(s, last, first);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke's width, cap, join, miter limit and dashes: `w J j M d`.
|
||||
fn line_style(&mut self, st: &StrokeLayer) {
|
||||
let cap = match st.cap {
|
||||
LineCap::Butt => 0,
|
||||
LineCap::Round => 1,
|
||||
LineCap::Square => 2,
|
||||
};
|
||||
let join = match st.join {
|
||||
LineJoin::Miter => 0,
|
||||
LineJoin::Round => 1,
|
||||
LineJoin::Bevel => 2,
|
||||
};
|
||||
let s = &mut self.body;
|
||||
ps::push_num(s, st.width);
|
||||
let _ = write!(s, " w {cap} J {join} j ");
|
||||
ps::push_num(s, if st.miter_limit.is_finite() { st.miter_limit.max(1.0) } else { 10.0 });
|
||||
s.push_str(" M [");
|
||||
let dash = st.dash.as_ref().filter(|d| d.is_dashed());
|
||||
if let Some(d) = dash {
|
||||
ps::push_nums(s, &d.pattern);
|
||||
s.pop();
|
||||
}
|
||||
s.push_str("] ");
|
||||
ps::push_num(s, dash.map_or(0.0, |d| d.offset));
|
||||
s.push_str(" d\n");
|
||||
}
|
||||
|
||||
// ---------- colour ----------
|
||||
|
||||
/// How `p` (a solid paint) is written.
|
||||
pub(super) fn ink(&self, p: &Paint) -> Ink {
|
||||
let Paint::Solid { color, swatch, tint } = p else { return Ink::Gray(0.0) };
|
||||
let doc = self.doc;
|
||||
let intent = self.cms.settings().intent;
|
||||
if let Some(name) = swatch
|
||||
&& let Some(sw) = doc.swatch(name).filter(|s| s.spot)
|
||||
&& let Some(full) = sw.paint.color()
|
||||
{
|
||||
let full = doc.linked_color(full, true);
|
||||
return Ink::Spot { cmyk: self.cms.to_cmyk(&full, intent).map(unit), name: name.clone(), tint: unit(*tint) };
|
||||
}
|
||||
match *color {
|
||||
// Grey is ink coverage; the format's grey is lightness.
|
||||
Color::Gray { k } => Ink::Gray(unit(1.0 - k)),
|
||||
Color::Cmyk { c, m, y, k } => Ink::Cmyk([c, m, y, k].map(unit)),
|
||||
Color::Rgb { r, g, b } => Ink::Rgb(self.cms.to_cmyk(color, intent).map(unit), [r, g, b].map(unit)),
|
||||
Color::Lab { .. } => {
|
||||
let rgb = color.to_rgb().map(unit);
|
||||
Ink::Rgb(self.cms.to_cmyk(color, intent).map(unit), rgb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fill (or with `stroke`, the stroke) colour to solid paint `p`.
|
||||
fn color(&mut self, p: &Paint, stroke: bool) {
|
||||
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 {
|
||||
Ink::Gray(v) => {
|
||||
ps::push_num(s, f64::from(v));
|
||||
let _ = writeln!(s, " {}", op("g", "G"));
|
||||
}
|
||||
Ink::Cmyk(c) => {
|
||||
ps::push_nums(s, &c.map(f64::from));
|
||||
let _ = writeln!(s, "{}", op("k", "K"));
|
||||
}
|
||||
Ink::Rgb(_, rgb) => {
|
||||
ps::push_nums(s, &rgb.map(f64::from));
|
||||
let _ = writeln!(s, "{}", op("Xa", "XA"));
|
||||
}
|
||||
Ink::Spot { cmyk, name, tint } => {
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- gradients ----------
|
||||
|
||||
/// The definition gradient `g` uses (defined once for each distinct gradient), and its
|
||||
/// instance on a path of bounds `bounds`: `Bb`, a radial's highlight `x y angle fraction Bh`
|
||||
/// (its distance a fraction of the radius, at most 1, §3.5.5; `0 0 0 0 Bh` at the center,
|
||||
/// §3.5.6), `1 (name) x y angle length 1 0 0 1 0 0 Bg`.
|
||||
fn gradient(&mut self, g: &GradientPaint, bounds: Rect) -> String {
|
||||
let name = self.define(&g.gradient, g.swatch.as_deref());
|
||||
for stop in &g.gradient.stops {
|
||||
if stop.opacity < 1.0 {
|
||||
self.warn(TRANSPARENCY);
|
||||
}
|
||||
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.spot(&name, cmyk);
|
||||
}
|
||||
}
|
||||
}
|
||||
let geom = g.resolve(bounds);
|
||||
let (start, end) = (self.to_art * geom.start, self.to_art * geom.end);
|
||||
let d = end - start;
|
||||
let (angle, length) = (d.y.atan2(d.x).to_degrees(), d.hypot());
|
||||
// The matrix is relative to the origin of a canvas whose size the specification doesn't
|
||||
// give (§3.5.4), so it stays the identity: an elliptical radial is written circular.
|
||||
if g.gradient.kind == GradientKind::Radial && (geom.aspect - 1.0).abs() > 1e-6 {
|
||||
self.warn(ELLIPTICAL);
|
||||
}
|
||||
let mut s = String::from("Bb\n");
|
||||
if g.gradient.kind == GradientKind::Radial {
|
||||
// Every radial instance has a highlight (§3.5.5), `0 0 0 0 Bh` at the center (§3.5.6).
|
||||
// It lies within the gradient's circle: one past the radius is written on it.
|
||||
let off: Vec2 = geom.focal.map_or(Vec2::ZERO, |f| self.to_art * f - start);
|
||||
let fraction = if off.hypot() > 1e-9 && length > 1e-9 { off.hypot() / length } else { 0.0 };
|
||||
let (off, angle) = if fraction > 0.0 { (off / fraction.max(1.0), off.y.atan2(off.x).to_degrees()) } else { (Vec2::ZERO, 0.0) };
|
||||
ps::push_nums(&mut s, &[off.x, off.y, angle, fraction.min(1.0)]);
|
||||
s.push_str("Bh\n");
|
||||
}
|
||||
s.push_str("1 ");
|
||||
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, 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 = 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) {
|
||||
i += 1;
|
||||
name = format!("{base} {i}");
|
||||
}
|
||||
self.gradients.push((name.clone(), g.clone()));
|
||||
name
|
||||
}
|
||||
|
||||
// ---------- type ----------
|
||||
|
||||
/// Type: a text object when the format can hold it as text, else its outlines.
|
||||
fn text(&mut self, t: &TextObject) {
|
||||
if !invertible(t.xf) {
|
||||
return;
|
||||
}
|
||||
if !self.outline_text {
|
||||
if writable(t) {
|
||||
return self.text_object(t);
|
||||
}
|
||||
self.warn(TYPE);
|
||||
}
|
||||
self.outlines(t);
|
||||
}
|
||||
|
||||
/// A text object (§15): `type To`, the text path (`a b c d tx ty startPt Tp`, an area's frame
|
||||
/// or the path type runs along, `TP`), then each run's style and characters, `TO`. The matrix
|
||||
/// takes text space (y up, the first baseline's start for point type) to art space; the frame
|
||||
/// and path are in art space, as the specification's examples have them (§15.5).
|
||||
fn text_object(&mut self, t: &TextObject) {
|
||||
if lost_attributes(t) {
|
||||
self.warn(TYPE_ATTRIBUTES);
|
||||
}
|
||||
// Area type that Shrink Text to Fit scales down is written in the styles it draws in.
|
||||
let fitted = matches!((&t.kind, t.area.fit), (TextKind::Area { .. }, vectorcraft_doc::AreaFit::ShrinkText { .. }))
|
||||
.then(|| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).fit_scale)
|
||||
.filter(|f| *f != 1.0)
|
||||
.map(|f| vectorcraft_text::scaled(t, f));
|
||||
let t = fitted.as_ref().unwrap_or(t);
|
||||
let m = self.to_art * t.xf * Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
let (kind, path, start) = match &t.kind {
|
||||
TextKind::Point => (0, None, 0.0),
|
||||
TextKind::Area { frame } => (1, frame.subpaths.iter().find(|sp| !sp.anchors.is_empty()).cloned(), 0.0),
|
||||
TextKind::OnPath { path, .. } => {
|
||||
let sp = path.subpaths.iter().find(|sp| !sp.anchors.is_empty()).cloned();
|
||||
let fraction = t.kind.path_span().map_or(0.0, |(s, _)| s);
|
||||
let start = sp
|
||||
.as_ref()
|
||||
.map_or(0.0, |sp| segment_point(&PathData::new(vec![sp.clone()]).transformed(self.to_art * t.xf).to_bezpath(), fraction));
|
||||
(2, sp, start)
|
||||
}
|
||||
};
|
||||
let _ = writeln!(self.body, "{kind} To");
|
||||
let [a, b, c, d, e, f] = m.as_coeffs();
|
||||
ps::push_nums(&mut self.body, &[a, b, c, d, e, f, start]);
|
||||
self.line("Tp");
|
||||
if let Some(sp) = path {
|
||||
let placed = SubPath::new(sp.anchors.iter().map(|a| a.transform(t.xf)).collect(), sp.closed);
|
||||
self.subpath(&placed);
|
||||
self.line(if sp.closed { "n" } else { "N" });
|
||||
}
|
||||
self.line("TP");
|
||||
let mut para = 0usize;
|
||||
let mut para_start = true;
|
||||
for run in &t.runs {
|
||||
let text = if run.style.all_caps { run.text.to_uppercase() } else { run.text.clone() };
|
||||
let mut pieces = text.split('\n').peekable();
|
||||
while let Some(piece) = pieces.next() {
|
||||
let ends_paragraph = pieces.peek().is_some();
|
||||
if piece.is_empty() && !ends_paragraph {
|
||||
continue;
|
||||
}
|
||||
if para_start {
|
||||
self.paragraph(t.para_at(para));
|
||||
para_start = false;
|
||||
}
|
||||
self.char_style(&run.style, t.para_at(para));
|
||||
let mut bytes: Vec<u8> = piece.chars().filter_map(latin::encode).collect();
|
||||
if ends_paragraph {
|
||||
bytes.push(b'\r');
|
||||
para += 1;
|
||||
para_start = true;
|
||||
}
|
||||
self.body.push_str(&ps::bytes(&bytes));
|
||||
self.line(" Tx");
|
||||
}
|
||||
}
|
||||
self.line("TO");
|
||||
}
|
||||
|
||||
/// A paragraph's attributes: alignment `Ta` and indents `Ti` (first line's, left, right).
|
||||
fn paragraph(&mut self, p: &ParaStyle) {
|
||||
let align = match p.justify {
|
||||
Justify::Auto if p.direction == Some(ParaDirection::RightToLeft) => 2,
|
||||
Justify::Auto | Justify::Left => 0,
|
||||
Justify::Center => 1,
|
||||
Justify::Right => 2,
|
||||
Justify::JustifyLeft | Justify::JustifyCenter | Justify::JustifyRight => 3,
|
||||
Justify::JustifyAll => 4,
|
||||
};
|
||||
let _ = writeln!(self.body, "{align} Ta");
|
||||
ps::push_nums(&mut self.body, &[p.first_line_indent, p.left_indent, p.right_indent]);
|
||||
self.line("Ti");
|
||||
}
|
||||
|
||||
/// A run's character attributes and paint style: `Tr`, its fill and stroke, `Tf`, `Ts`, `Tz`,
|
||||
/// `Tt`, `TA`, and the leading with the space before its paragraph, `Tl`.
|
||||
fn char_style(&mut self, st: &CharStyle, p: &ParaStyle) {
|
||||
let fill = !st.fill.is_none();
|
||||
let stroke = st.has_stroke();
|
||||
let render = match (fill, stroke) {
|
||||
(true, false) => 0,
|
||||
(false, true) => 1,
|
||||
(true, true) => 2,
|
||||
(false, false) => 3,
|
||||
};
|
||||
let _ = writeln!(self.body, "{render} Tr");
|
||||
if fill {
|
||||
self.color(&st.fill, false);
|
||||
self.line(if st.overprint_fill { "1 O" } else { "0 O" });
|
||||
}
|
||||
if stroke {
|
||||
self.color(&st.stroke, true);
|
||||
self.line(if st.overprint_stroke { "1 R" } else { "0 R" });
|
||||
self.line_style(&st.stroke_layer());
|
||||
}
|
||||
let (name, truetype, ascent, descent) = font_of(st);
|
||||
if !self.fonts.iter().any(|(n, _)| *n == name) {
|
||||
self.fonts.push((name.clone(), truetype));
|
||||
}
|
||||
let size = finite(st.size, 12.0);
|
||||
let _ = write!(self.body, "/_{name} ");
|
||||
ps::push_nums(&mut self.body, &[size, ascent * size, descent * size]);
|
||||
self.line("Tf");
|
||||
ps::push_num(&mut self.body, finite(st.baseline_shift, 0.0));
|
||||
self.line(" Ts");
|
||||
ps::push_nums(&mut self.body, &[finite(st.h_scale, 100.0), finite(st.v_scale, 100.0)]);
|
||||
self.line("Tz");
|
||||
ps::push_num(&mut self.body, finite(st.tracking, 0.0));
|
||||
self.line(" Tt");
|
||||
self.line(if st.kerning.is_none() { "1 TA" } else { "0 TA" });
|
||||
ps::push_nums(&mut self.body, &[finite(st.effective_leading(), size * 1.2), finite(p.space_before, 0.0)]);
|
||||
self.line("Tl");
|
||||
}
|
||||
|
||||
/// Type as its glyphs' outlines: a group of a compound path for each run, with the run's fill
|
||||
/// and stroke.
|
||||
fn outlines(&mut self, t: &TextObject) {
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
let scale = t.xf.determinant().abs().sqrt();
|
||||
self.line("u");
|
||||
for (i, run) in t.runs.iter().enumerate() {
|
||||
let mut bp = BezPath::new();
|
||||
// Ruby (furigana) glyphs in their base text's run's paint, as the canvas draws them.
|
||||
for g in layout.glyphs.iter().chain(&layout.ruby).filter(|g| g.run == i) {
|
||||
bp.extend(g.outline.iter());
|
||||
}
|
||||
if bp.elements().is_empty() {
|
||||
continue;
|
||||
}
|
||||
bp.apply_affine(t.xf);
|
||||
let path = PathData::from_bezpath(&bp);
|
||||
let fill =
|
||||
(!run.style.fill.is_none()).then(|| FillLayer { overprint: run.style.overprint_fill, ..FillLayer::new(run.style.fill.clone()) });
|
||||
let stroke = run.style.has_stroke().then(|| {
|
||||
let mut s = run.style.stroke_layer();
|
||||
s.width *= scale;
|
||||
s
|
||||
});
|
||||
let items: Vec<AppearanceItem> = fill.map(AppearanceItem::Fill).into_iter().chain(stroke.map(AppearanceItem::Stroke)).collect();
|
||||
let mut n = Node::path(vectorcraft_doc::NodeId(0), path.clone(), vectorcraft_doc::Appearance::default());
|
||||
n.appearance = vectorcraft_doc::Appearance { items, ..vectorcraft_doc::Appearance::default() }.into();
|
||||
self.shape(&n, &path, FillRule::NonZero);
|
||||
}
|
||||
// Colour bitmap glyphs (emoji): their images, which the runs' paint doesn't paint.
|
||||
for g in &layout.glyphs {
|
||||
if let Some((image, to_text)) = g.placed_image() {
|
||||
self.image(Some(&image.blob), image.width, image.height, t.xf * to_text);
|
||||
}
|
||||
// COLR glyphs: their layers, as art.
|
||||
if let Some(art) = g.placed_colour(t.xf) {
|
||||
self.node(&art, false);
|
||||
}
|
||||
}
|
||||
self.line("U");
|
||||
}
|
||||
|
||||
// ---------- images ----------
|
||||
|
||||
/// An image (§8): `%AI5_File:`, `%AI5_BeginRaster`, its matrix (an art-space matrix applied to
|
||||
/// the pixels with their rows running downwards, `(u, -v)`) as `[ a b c d tx ty ]` and its
|
||||
/// operands with `XI`, its RGB samples in hex on comment lines of [`HEX_LINE`] digits,
|
||||
/// `%AI5_EndRaster`, then `F`. §8.4's examples have no `F`; without it, an app reading the file
|
||||
/// can leave out an image in a clipping group and the art after it. No path is open there, so
|
||||
/// the `F` paints nothing. `blob` holds the pixels, placed as a grid of `width` × `height`
|
||||
/// by `xf`.
|
||||
fn image(&mut self, blob: Option<&ImageBlob>, width: u32, height: u32, xf: Affine) {
|
||||
let flip = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
let m = self.to_art * xf * flip;
|
||||
if !invertible(m) {
|
||||
return;
|
||||
}
|
||||
if blob.is_some_and(|b| b.bytes.is_empty()) {
|
||||
self.warn(LARGE_IMAGE);
|
||||
return;
|
||||
}
|
||||
let Some(img) = blob.and_then(|b| image::load_from_memory(&b.bytes).ok()) else {
|
||||
self.warn(UNDECODED);
|
||||
return;
|
||||
};
|
||||
let (w, h) = (img.width(), img.height());
|
||||
if w == 0 || h == 0 || u64::from(w) * u64::from(h) > MAX_IMAGE_PIXELS {
|
||||
self.warn(LARGE_IMAGE);
|
||||
return;
|
||||
}
|
||||
// The object places the image's own pixel grid (its width × height) wherever the file's is.
|
||||
let fit = Affine::scale_non_uniform(f64::from(width.max(1)) / f64::from(w), f64::from(height.max(1)) / f64::from(h));
|
||||
let m = self.to_art * xf * fit * flip;
|
||||
let rgba = img.to_rgba8();
|
||||
let mut hex = String::with_capacity(rgba.len() / 4 * 7);
|
||||
hex.push('%');
|
||||
let mut on_line = 0;
|
||||
let mut see_through = false;
|
||||
for px in rgba.pixels() {
|
||||
let [r, g, b, a] = px.0;
|
||||
if a < 255 {
|
||||
see_through = true;
|
||||
}
|
||||
// Over white.
|
||||
let over = |c: u8| ((u32::from(c) * u32::from(a) + 255 * (255 - u32::from(a)) + 127) / 255) as u8;
|
||||
for c in [over(r), over(g), over(b)] {
|
||||
let _ = write!(hex, "{c:02X}");
|
||||
on_line += 2;
|
||||
if on_line >= HEX_LINE {
|
||||
hex.push_str("\n%");
|
||||
on_line = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if hex.ends_with('%') {
|
||||
hex.pop();
|
||||
} else {
|
||||
hex.push('\n');
|
||||
}
|
||||
if see_through {
|
||||
self.warn(IMAGE_ALPHA);
|
||||
}
|
||||
let [a, b, c, d, e, f] = m.as_coeffs();
|
||||
let s = &mut self.body;
|
||||
s.push_str("%AI5_File:\n%AI5_BeginRaster\n[ ");
|
||||
ps::push_nums(s, &[a, b, c, d, e, f]);
|
||||
let _ = writeln!(s, "] 0 0 {w} {h} {w} {h} 8 3 0 0 0 0 XI");
|
||||
s.push_str(&hex);
|
||||
s.push_str("%AI5_EndRaster\nF\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user