vectorcraft/crates/eps/src/scene.rs

952 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The document walk: each visible object is written into the page in paint order, in document
//! space (the page's first operator maps it onto PostScript's y-up space). Mirrors the PDF
//! writer's walk, without transparency.
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::Arc;
use kurbo::Shape;
use vectorcraft_color::{BlendMode, Color, GradientKind, GradientPaint, Paint};
use vectorcraft_doc::{
AppearanceItem, ColorMode, Document, Effect, ImageObject, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, TextObject,
};
use vectorcraft_effects::stroke::{self, WrittenShape};
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2};
use crate::ps::{self, clip_op, fill_op, push_nums, push_path};
use crate::{EpsOptions, Level, Overprint};
/// How deep symbols and brush art may nest in one another (deeper art is left out).
const MAX_NEST: u32 = 8;
/// Most pixels along a side of a freeform gradient's image (its colour field is smooth).
const MAX_FIELD_PX: f64 = 512.0;
/// Most bands a stepped gradient is drawn with.
const MAX_STEPS: usize = 1024;
/// Thinnest band of a stepped gradient (points).
const MIN_BAND: f64 = 0.25;
/// Largest image written (pixels).
const MAX_IMAGE_PIXELS: u64 = 1 << 26;
const TRANSPARENCY: &str =
"transparency (opacity, blending modes, opacity masks, see-through gradients) is written opaque: EPS has none, flatten it first";
const RASTER_FX: &str = "raster effects (shadows, glows, blurs, feathering) are left out: EPS has no transparency, flatten them first";
const IMAGE_ALPHA_L2: &str = "transparent image pixels are written white at PostScript Level 2 (Level 3 masks them out)";
const IMAGE_PARTLY: &str = "partly transparent image pixels are written over white";
const SPOT_GRADIENTS: &str = "gradients of spot colours are written in process colours";
const MISSING_PATTERN: &str = "missing patterns are written as mid-grey";
/// What the walk wrote: definitions for the setup, the page, the spot colours it uses (name and
/// CMYK equivalent) and the warnings.
pub(crate) struct Page {
pub setup: String,
pub body: String,
pub custom: Vec<(String, [f32; 4])>,
pub warnings: Vec<String>,
}
/// The colour spaces process colours are written in.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Space {
Gray,
Rgb,
Cmyk,
}
impl Space {
fn name(self) -> &'static str {
match self {
Space::Gray => "/DeviceGray",
Space::Rgb => "/DeviceRGB",
Space::Cmyk => "/DeviceCMYK",
}
}
fn n(self) -> usize {
match self {
Space::Gray => 1,
Space::Rgb => 3,
Space::Cmyk => 4,
}
}
}
/// A colour as written: in a process space (its first [`Space::n`] components) or a tint of a
/// spot colour space defined in the setup.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Ink {
Process(Space, [f32; 4]),
Spot { space: usize, tint: f32 },
}
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)?
fn invertible(a: Affine) -> bool {
let det = a.determinant();
det.is_finite() && det.abs() > 1e-12
}
fn overlaps(a: Rect, b: Rect) -> bool {
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
}
pub(crate) struct Scene<'a> {
doc: &'a Document,
o: &'a EpsOptions,
/// RGB and Lab colours are written as CMYK.
cmyk: bool,
cms: Arc<vectorcraft_color::cms::Cms>,
body: String,
setup: String,
/// Spot colour spaces defined in the setup (`VCsp<i>`): swatch name and CMYK equivalent.
spots: Vec<(String, [f32; 4])>,
warnings: Vec<String>,
brushes: Option<Vec<vectorcraft_brush::Brush>>,
/// Image keys → what draws them (`None`: left out).
images: HashMap<String, Option<Arc<String>>>,
/// Symbols and brush art being written inside one another.
nest: u32,
}
impl<'a> Scene<'a> {
pub fn new(doc: &'a Document, o: &'a EpsOptions) -> Self {
Self {
doc,
o,
cmyk: o.cmyk || doc.color_mode == ColorMode::Cmyk,
cms: vectorcraft_color::cms::active(),
body: String::new(),
setup: String::new(),
spots: vec![],
warnings: vec![],
brushes: None,
images: HashMap::new(),
nest: 0,
}
}
pub fn run(mut self) -> Page {
let doc = self.doc;
for l in &doc.layers {
self.node(l, false);
}
self.page()
}
/// Write one object (art outside the document: a print job's marks) instead of the layers.
pub fn run_node(mut self, n: &Node) -> Page {
self.node(n, false);
self.page()
}
fn page(self) -> Page {
let custom = self.spots.iter().filter(|(n, _)| n != vectorcraft_color::swatch::REGISTRATION).cloned().collect();
Page { setup: self.setup, body: self.body, custom, warnings: self.warnings }
}
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');
}
// ---------- colour ----------
/// A process colour in the space it is written in.
fn process(&self, c: &Color) -> Ink {
match *c {
Color::Cmyk { c, m, y, k } => Ink::Process(Space::Cmyk, [c, m, y, k].map(unit)),
// Grey is ink coverage; PostScript's grey is lightness.
Color::Gray { k } => Ink::Process(Space::Gray, [unit(1.0 - k), 0.0, 0.0, 0.0]),
_ if self.cmyk => Ink::Process(Space::Cmyk, self.cms.to_cmyk(c, self.cms.settings().intent).map(unit)),
Color::Rgb { r, g, b } => Ink::Process(Space::Rgb, [r, g, b, 0.0].map(unit)),
Color::Lab { .. } => {
let [r, g, b] = c.to_rgb();
Ink::Process(Space::Rgb, [r, g, b, 0.0].map(unit))
}
}
}
/// A solid paint: a tint of its spot colour (or of Registration, every plate) when it is linked
/// to one, else a process colour.
fn solid(&mut self, c: &Color, link: Option<&str>, tint: f32) -> Ink {
match link.and_then(|n| self.spot_space(n)) {
Some(space) => Ink::Spot { space, tint: unit(tint) },
None => self.process(c),
}
}
/// The index of the Separation colour space of spot swatch (or Registration) `name`, defined
/// in the setup the first time; `None` when `name` is neither.
fn spot_space(&mut self, name: &str) -> Option<usize> {
if let Some(i) = self.spots.iter().position(|(n, _)| n == name) {
return Some(i);
}
let registration = name == vectorcraft_color::swatch::REGISTRATION;
let cmyk = if registration {
[1.0; 4]
} else {
let doc = self.doc;
let sw = doc.swatch(name).filter(|s| s.spot)?;
let color = doc.linked_color(sw.paint.color()?, true);
self.cms.to_cmyk(&color, self.cms.settings().intent).map(unit)
};
let i = self.spots.len();
let _ = write!(self.setup, "/VCsp{i} [/Separation ");
if registration {
self.setup.push_str("/All /DeviceCMYK {dup dup dup}");
} else {
let [c, m, y, k] = cmyk.map(|v| ps::num(f64::from(v)));
let _ = write!(self.setup, "{} cvn /DeviceCMYK {{dup {c} mul exch dup {m} mul exch dup {y} mul exch {k} mul}}", ps::string(name));
}
self.setup.push_str("] def\n");
self.spots.push((name.to_string(), cmyk));
Some(i)
}
fn set_ink(&mut self, ink: Ink) {
match ink {
Ink::Process(space, v) => {
for x in v.iter().take(space.n()) {
ps::push_num(&mut self.body, f64::from(*x));
self.body.push(' ');
}
self.line(match space {
Space::Gray => "g",
Space::Rgb => "rg",
Space::Cmyk => "k",
});
}
Ink::Spot { space, tint } => {
let _ = writeln!(self.body, "VCsp{space} setcolorspace {} setcolor", ps::num(f64::from(tint)));
}
}
}
/// Turn overprinting on for one paint (when overprints are preserved); `false` when it stays off.
fn overprint_on(&mut self, overprint: bool) -> bool {
let on = overprint && self.o.overprint == Overprint::Preserve;
if on {
self.line("true op");
}
on
}
fn overprint_off(&mut self, on: bool) {
if on {
self.line("false op");
}
}
// ---------- objects ----------
fn node(&mut self, n: &Node, force: bool) {
if !force && !n.visible {
return;
}
if let NodeKind::Layer { template: true, .. } = n.kind {
return;
}
match n.visual_bounds() {
Some(b) if !force && !overlaps(b.inflate(1.0, 1.0), self.o.region) => return,
None if !n.is_container() => return,
_ => {}
}
self.note_losses(n);
match &n.kind {
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => {
children.iter().for_each(|c| self.node(c, false));
}
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => self.clip_group(children),
NodeKind::Path { path, rule, guide, .. } => {
if !*guide {
self.shape(n, &path.to_bezpath(), *rule);
}
}
NodeKind::Compound { children, rule } => {
let mut bp = BezPath::new();
for p in children.iter().filter_map(|c| c.path_data()) {
bp.extend(p.to_bezpath());
}
self.shape(n, &bp, *rule);
}
NodeKind::Text(t) => self.text(n, t),
NodeKind::Image(im) => self.image(im),
NodeKind::SymbolInstance { symbol, xf } => {
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);
self.nest += 1;
self.node(&art, true);
self.nest -= 1;
}
}
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
let g = vectorcraft_doc::live::expand_deep(n, None);
for c in g.children().into_iter().flatten() {
self.node(c, false);
}
}
}
}
/// A clipping group: the art inside its first child's outline. The clipping path's fills
/// paint behind the art (clipped) and its strokes over it (not clipped).
fn clip_group(&mut self, children: &[Arc<Node>]) {
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.line("q");
push_path(&mut self.body, &outline);
self.body.push_str(clip_op(rule));
if let Some(fill) = &paint.fill {
self.node(fill, false);
}
rest.iter().for_each(|c| self.node(c, false));
self.line("Q");
if let Some(stroke) = &paint.stroke {
self.node(stroke, false);
}
}
/// Warn about what EPS can't hold in `n`'s look.
fn note_losses(&mut self, n: &Node) {
if n.opacity < 1.0 || n.blend != BlendMode::Normal || n.mask.as_ref().is_some_and(|m| !m.disabled) {
self.warn(TRANSPARENCY);
}
let raster = |fx: &[Effect]| fx.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id));
if raster(&n.appearance.effects) || n.appearance.items.iter().any(|i| raster(i.effects())) {
self.warn(RASTER_FX);
}
}
/// Warn when a fill's or stroke's own opacity or blending mode can't be written.
fn note_item(&mut self, opacity: f32, blend: BlendMode) {
if opacity < 1.0 || blend != BlendMode::Normal {
self.warn(TRANSPARENCY);
}
}
/// A path's fills and strokes, in paint order.
fn shape(&mut self, n: &Node, bp: &BezPath, rule: FillRule) {
if bp.elements().is_empty() {
return;
}
let bounds = bp.bounding_box();
for item in &n.appearance.items {
match item {
AppearanceItem::Fill(fl) if fl.visible && !fl.paint.is_none() => {
self.note_item(fl.opacity, fl.blend);
self.fill(bp, rule, &fl.paint, bounds, fl.overprint);
}
AppearanceItem::Stroke(st) if st.visible && !st.paint.is_none() && st.width > 0.0 && st.width.is_finite() => {
self.stroke(bp, rule, st, bounds);
}
_ => {}
}
}
}
/// Fill `bp` (by `rule`) with paint `p`; `bounds` places unplaced gradients and patterns.
fn fill(&mut self, bp: &BezPath, rule: FillRule, p: &Paint, bounds: Rect, overprint: bool) {
if p.is_none() {
return;
}
let on = self.overprint_on(overprint);
match p {
Paint::None => {}
Paint::Solid { color, swatch, tint } => {
let ink = self.solid(color, swatch.as_deref(), *tint);
self.set_ink(ink);
push_path(&mut self.body, bp);
self.body.push_str(fill_op(rule));
}
_ => {
self.line("q");
push_path(&mut self.body, bp);
self.body.push_str(clip_op(rule));
self.paint_area(p, bounds, bp.bounding_box());
self.line("Q");
}
}
self.overprint_off(on);
}
/// Paint all of `area` (inside the current clip) with gradient or pattern `p`, placed on
/// `bounds`.
fn paint_area(&mut self, p: &Paint, bounds: Rect, area: Rect) {
let doc = self.doc;
match p {
Paint::Gradient(g) if g.gradient.kind == GradientKind::Freeform => self.field_image(g, bounds, area),
Paint::Gradient(g) => self.gradient(g, bounds, area),
Paint::Pattern { pattern, xf } => match doc.pattern(pattern) {
Some(def) => {
for inst in def.instances_in(*xf, area) {
self.node(&inst, true);
}
}
None => {
self.warn(MISSING_PATTERN);
self.set_ink(Ink::Process(Space::Gray, [0.5, 0.0, 0.0, 0.0]));
self.rect_fill(area);
}
},
Paint::Solid { color, swatch, tint } => {
let ink = self.solid(color, swatch.as_deref(), *tint);
self.set_ink(ink);
self.rect_fill(area);
}
Paint::None => {}
}
}
/// Fill rectangle `r` (a little larger, so its edges fall outside any clip it fills).
fn rect_fill(&mut self, r: Rect) {
let r = r.abs().inflate(1.0, 1.0);
push_nums(&mut self.body, &[r.x0, r.y0, r.width(), r.height()]);
self.line("rectfill");
}
/// Paint stroke `st` of the shape `bp` (filled by `rule`; geometric bounds `bounds`).
fn stroke(&mut self, bp: &BezPath, rule: FillRule, st: &StrokeLayer, bounds: Rect) {
if !stroke::is_plain(st) && self.brush_art(bp, st) {
return;
}
self.note_item(st.opacity, st.blend);
let paint_bounds = st.paint_bounds(bounds);
let area_paint = match &st.paint {
Paint::Pattern { .. } => true,
Paint::Gradient(g) => g.gradient.kind == GradientKind::Freeform,
_ => false,
};
if area_paint {
// The area the stroke paints, as Outline Stroke makes it, painted with the pattern's
// tiles or the freeform's image.
let region = stroke::outline_region(&PathData::from_bezpath(bp), rule, st).to_bezpath();
self.fill(&region, FillRule::NonZero, &st.paint, paint_bounds, st.overprint);
return;
}
let w = stroke::for_writer(bp, st);
let sided = match w.side {
Some(StrokeAlign::Inside) => {
self.line("q");
push_path(&mut self.body, bp);
self.body.push_str(clip_op(rule));
true
}
Some(StrokeAlign::Outside) => {
// Everything outside the path: a frame around all the stroke reaches plus the
// path, even-odd.
let mut outside = w.reach(st, bounds).inflate(1.0, 1.0).to_path(0.1);
outside.extend(bp.iter());
self.line("q");
push_path(&mut self.body, &outside);
self.body.push_str(clip_op(FillRule::EvenOdd));
true
}
_ => false,
};
match &w.shape {
WrittenShape::Stroke { width } => {
let on = self.overprint_on(st.overprint);
self.line_style(st, *width);
match &st.paint {
Paint::Solid { color, swatch, tint } => {
let ink = self.solid(color, swatch.as_deref(), *tint);
self.set_ink(ink);
push_path(&mut self.body, bp);
self.line("S");
}
p => {
// A gradient within the stroke: the stroke's outline as a clip.
self.line("q");
push_path(&mut self.body, bp);
self.line("strokepath W");
self.paint_area(p, paint_bounds, w.reach(st, bounds));
self.line("Q");
}
}
self.overprint_off(on);
}
WrittenShape::Fill(outlines) if st.path_gradient().is_some() => {
// A gradient along or across the stroke: slices clipped to its outlines.
if let Some(ws) = stroke::written_slices(bp, rule, st, outlines) {
self.line("q");
push_path(&mut self.body, &ws.clip);
self.body.push_str(clip_op(FillRule::NonZero));
for (shape, paint) in &ws.slices {
self.fill(shape, FillRule::NonZero, paint, shape.bounding_box(), st.overprint);
}
self.line("Q");
}
}
WrittenShape::Fill(outlines) => {
for o in outlines {
self.fill(o, FillRule::NonZero, &st.paint, paint_bounds, st.overprint);
}
}
}
if sided {
self.line("Q");
}
}
/// The line width, cap, join, miter limit and dashes of `st` at `width`.
fn line_style(&mut self, st: &StrokeLayer, width: f64) {
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, 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 {
push_nums(s, &d.pattern);
}
s.push_str("] ");
ps::push_num(s, dash.map_or(0.0, |d| d.offset));
s.push_str(" d\n");
}
/// Paint stroke `st` of `bp` with its brush art. False when it has no known brush.
fn brush_art(&mut self, bp: &BezPath, st: &StrokeLayer) -> bool {
if self.nest >= MAX_NEST {
return false;
}
let doc = self.doc;
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
let art = vectorcraft_brush::stroke_pieces(b, bp, st);
self.note_item(st.opacity, st.blend);
self.nest += 1;
for piece in &art {
self.node(piece, false);
}
self.nest -= 1;
true
}
// ---------- type ----------
/// Type as glyph outlines: the object's own fills and strokes below and above the
/// characters, each run's fill and stroke in text space.
fn text(&mut self, n: &Node, t: &TextObject) {
if !invertible(t.xf) {
return;
}
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
let tb = t.xf.transform_rect_bbox(layout.bounds);
let (below, above) = n.appearance.split_contents();
let all = (!n.appearance.items.is_empty()).then(|| {
let mut all = layout.to_bezpath();
all.apply_affine(t.xf);
all
});
if let Some(bp) = &all {
self.text_items(below, bp, tb);
}
self.line("q");
let _ = writeln!(self.body, "{} cm", ps::matrix(t.xf));
for (i, run) in t.runs.iter().enumerate() {
let mut bp = BezPath::new();
for g in layout.glyphs.iter().filter(|g| g.run == i) {
bp.extend(g.outline.iter());
}
if bp.elements().is_empty() {
continue;
}
self.fill(&bp, FillRule::NonZero, &run.style.fill, layout.bounds, run.style.overprint_fill);
if run.style.has_stroke() {
// Character strokes are drawn in text space, with their cap, join and dashes.
self.stroke(&bp, FillRule::NonZero, &run.style.stroke_layer(), layout.bounds);
}
}
self.line("Q");
if let Some(bp) = &all {
self.text_items(above, bp, tb);
}
}
/// Some of a type object's own fills and strokes on its glyph outlines `bp` (bounds `tb`).
fn text_items(&mut self, items: &[AppearanceItem], bp: &BezPath, tb: Rect) {
for item in items {
match item {
AppearanceItem::Fill(fl) if fl.visible && !fl.paint.is_none() => {
self.note_item(fl.opacity, fl.blend);
self.fill(bp, FillRule::NonZero, &fl.paint, tb, fl.overprint);
}
AppearanceItem::Stroke(st) if st.visible && !st.paint.is_none() && st.width > 0.0 && st.width.is_finite() => {
self.stroke(bp, FillRule::NonZero, st, tb);
}
_ => {}
}
}
}
// ---------- gradients ----------
/// The colour space a gradient's stops are written in, and its stops in it from offset 0 to 1
/// (midpoints as stops of their own).
fn gradient_stops(&mut self, g: &vectorcraft_color::Gradient) -> (Space, Vec<(f64, [f32; 4])>) {
if g.stops.iter().any(|s| s.opacity < 1.0) {
self.warn(TRANSPARENCY);
}
let doc = self.doc;
if g.stops.iter().any(|s| s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot))) {
self.warn(SPOT_GRADIENTS);
}
let stops = g.expanded_stops();
let space = if self.cmyk || stops.iter().any(|(_, c, _)| matches!(c, Color::Cmyk { .. })) {
Space::Cmyk
} else if stops.iter().all(|(_, c, _)| matches!(c, Color::Gray { .. })) {
Space::Gray
} else {
Space::Rgb
};
let comps = |c: &Color| -> [f32; 4] {
match (space, *c) {
(Space::Cmyk, _) => self.cms.to_cmyk(c, self.cms.settings().intent).map(unit),
(Space::Gray, Color::Gray { k }) => [unit(1.0 - k), 0.0, 0.0, 0.0],
(_, Color::Rgb { r, g, b }) => [r, g, b, 0.0].map(unit),
_ => {
let [r, g, b] = c.to_rgb();
[r, g, b, 0.0].map(unit)
}
}
};
let mut out: Vec<(f64, [f32; 4])> = vec![];
let mut last = 0.0f64;
for (o, c, _) in &stops {
let o = if o.is_finite() { f64::from(*o).clamp(last, 1.0) } else { last };
last = o;
out.push((o, comps(c)));
}
match (out.first().copied(), out.last().copied()) {
(Some(first), Some(end)) => {
if first.0 > 0.0 {
out.insert(0, (0.0, first.1));
}
if end.0 < 1.0 {
out.push((1.0, end.1));
}
}
_ => out = vec![(0.0, [0.0; 4]), (1.0, [0.0; 4])],
}
(space, out)
}
/// Paint all of `area` (inside the current clip) with linear or radial gradient `g`, placed on
/// `bounds`: a smooth shading at Level 3, else stepped fills.
fn gradient(&mut self, g: &GradientPaint, bounds: Rect, area: Rect) {
let geom = g.resolve(bounds);
let (space, stops) = self.gradient_stops(&g.gradient);
if !stops.windows(2).any(|w| w[1].0 > w[0].0) {
if let Some((_, c)) = stops.first() {
self.set_comps(space, *c);
self.rect_fill(area);
}
return;
}
let smooth = self.o.level == Level::Three && !self.o.compatible_gradients;
match g.gradient.kind {
GradientKind::Radial => {
let r = geom.length().max(1e-6);
let squash = geom.radial_squash();
let inv = squash.inverse();
let (f, c) = (inv * geom.focal_point(), geom.start);
self.line("q");
let _ = writeln!(self.body, "{} cm", ps::matrix(squash));
if smooth {
self.shading(3, space, &[f.x, f.y, 0.0, c.x, c.y, r], &stops);
} else {
self.radial_steps(f, c, r, inv.transform_rect_bbox(area), space, &stops);
}
self.line("Q");
}
_ => {
let (s, mut e) = (geom.start, geom.end);
if s.distance(e) < 1e-9 {
e = s + Vec2::new(1.0, 0.0);
}
if smooth {
self.shading(2, space, &[s.x, s.y, e.x, e.y], &stops);
} else {
self.linear_steps(s, e, area, space, &stops);
}
}
}
}
/// A Level 3 smooth shading of `kind` (2 axial, 3 radial) at `coords`, extended both ways.
fn shading(&mut self, kind: u8, space: Space, coords: &[f64], stops: &[(f64, [f32; 4])]) {
let n = space.n();
let comps = |c: &[f32; 4]| -> String {
let mut s = String::from("[");
push_nums(&mut s, &c.iter().take(n).map(|v| f64::from(*v)).collect::<Vec<_>>());
s.pop();
s.push(']');
s
};
let segments: Vec<_> = stops.windows(2).filter(|w| w[1].0 > w[0].0).collect();
let interp = |w: &&[(f64, [f32; 4])]| format!("<< /FunctionType 2 /Domain [0 1] /C0 {} /C1 {} /N 1 >>", comps(&w[0].1), comps(&w[1].1));
let function = match segments.as_slice() {
[one] => interp(one),
many => {
let mut f = String::from("<< /FunctionType 3 /Domain [0 1] /Functions [\n");
for w in many {
f.push_str(&interp(w));
f.push('\n');
}
f.push_str("] /Bounds [");
let bounds: Vec<f64> = many.iter().skip(1).map(|w| w[0].0).collect();
push_nums(&mut f, &bounds);
f.push_str("] /Encode [");
f.push_str(&"0 1 ".repeat(many.len()));
f.push_str("] >>");
f
}
};
let _ = write!(self.body, "<< /ShadingType {kind} /ColorSpace {} /Coords [", space.name());
push_nums(&mut self.body, coords);
let _ = writeln!(self.body, "] /Extend [true true]\n/Function {function} >> shfill");
}
/// The bands of each stretch between stops: how many, for colours `a` to `b` over `length`
/// points (one per colour level, none thinner than [`MIN_BAND`]).
fn bands(a: &[f32; 4], b: &[f32; 4], length: f64, segments: usize) -> usize {
let delta = a.iter().zip(b).map(|(x, y)| (x - y).abs()).fold(0.0f32, f32::max);
let by_color = (f64::from(delta) * 255.0).ceil().max(1.0) as usize;
let by_size = (length / MIN_BAND).ceil().max(1.0) as usize;
by_color.min(by_size).min(MAX_STEPS / segments.max(1)).max(1)
}
fn set_comps(&mut self, space: Space, c: [f32; 4]) {
self.set_ink(Ink::Process(space, c));
}
/// A linear gradient from `s` to `e` as bands across it, covering `area`: each band painted
/// from where it starts to the far end, so the next one leaves no seam.
fn linear_steps(&mut self, s: Point, e: Point, area: Rect, space: Space, stops: &[(f64, [f32; 4])]) {
let d = e - s;
let len = d.hypot();
// Gradient space: x along the gradient (0 to 1), y across it in points.
let frame = Affine::new([d.x, d.y, -d.y / len, d.x / len, s.x, s.y]);
let local = frame.inverse().transform_rect_bbox(area.inflate(1.0, 1.0));
let (t0, t1) = (local.x0.min(0.0), local.x1.max(1.0));
self.line("q");
let _ = writeln!(self.body, "{} cm", ps::matrix(frame));
let band = |sc: &mut Self, from: f64| {
push_nums(&mut sc.body, &[from, local.y0, t1 - from, local.height()]);
sc.line("rectfill");
};
if let Some((_, c)) = stops.first() {
self.set_comps(space, *c);
band(self, t0);
}
let segments: Vec<_> = stops.windows(2).filter(|w| w[1].0 > w[0].0).collect();
for w in &segments {
let ((oa, ca), (ob, cb)) = (w[0], w[1]);
let n = Self::bands(&ca, &cb, (ob - oa) * len, segments.len());
for k in 0..n {
let at = oa + (ob - oa) * k as f64 / n as f64;
self.set_comps(space, mix(&ca, &cb, (k as f32 + 0.5) / n as f32));
band(self, at);
}
}
if let Some((_, c)) = stops.last() {
self.set_comps(space, *c);
band(self, 1.0);
}
self.line("Q");
}
/// A radial gradient from focal point `f` to the circle of radius `r` around `c` as discs,
/// largest first, over the last colour across `area` (all in the gradient's own space).
fn radial_steps(&mut self, f: Point, c: Point, r: f64, area: Rect, space: Space, stops: &[(f64, [f32; 4])]) {
if let Some((_, last)) = stops.last() {
self.set_comps(space, *last);
self.rect_fill(area);
}
let segments: Vec<_> = stops.windows(2).filter(|w| w[1].0 > w[0].0).collect();
for w in segments.iter().rev() {
let ((oa, ca), (ob, cb)) = (w[0], w[1]);
let n = Self::bands(&ca, &cb, (ob - oa) * r, segments.len());
for k in (0..n).rev() {
let t = oa + (ob - oa) * (k + 1) as f64 / n as f64;
self.set_comps(space, mix(&ca, &cb, (k as f32 + 0.5) / n as f32));
let at = f + (c - f) * t;
push_nums(&mut self.body, &[at.x, at.y, r * t]);
self.line("0 360 newpath arc fill");
}
}
}
// ---------- images ----------
/// Paint all of `area` with an image of freeform gradient `g` placed on `bounds`, sampled at
/// the document's raster effects resolution (at most [`MAX_FIELD_PX`] a side).
fn field_image(&mut self, g: &GradientPaint, bounds: Rect, area: Rect) {
use vectorcraft_color::freeform::{painted_box, spread_scale};
let Some(b) = painted_box(bounds) else { return };
let area = area.abs();
let long = area.width().max(area.height());
if !(long > 1e-9 && long.is_finite()) {
return;
}
let k = (self.doc.raster_effects_ppi / 72.0).min(MAX_FIELD_PX / long);
let along = |len: f64| (len * k).ceil().clamp(1.0, MAX_FIELD_PX) as u16;
let (cols, rows) = (along(area.width()), along(area.height()));
let field = g.freeform_on(b).field_with(spread_scale(b), &|c| c.to_rgb());
let q = |v: f32| (unit(v) * 255.0).round() as u8;
let rgba: Vec<u8> = field.grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect();
let place = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width(), area.height());
if let Some(image) = self.encode_image(&rgba, u32::from(cols), u32::from(rows)) {
self.place_image(&image, place);
}
}
/// A placed image: its pixel grid stretched over its `width` × `height` (each image file
/// encoded once).
fn image(&mut self, im: &ImageObject) {
let place = im.xf * Affine::scale_non_uniform(f64::from(im.width.max(1)), f64::from(im.height.max(1)));
if !invertible(place) {
return;
}
let encoded = match self.images.get(&im.key) {
Some(e) => e.clone(),
None => {
let e = self.decode_image(&im.key).and_then(|img| self.encode_image(img.as_raw(), img.width(), img.height())).map(Arc::new);
self.images.insert(im.key.clone(), e.clone());
e
}
};
if let Some(e) = encoded {
self.place_image(&e, place);
}
}
/// Image `key` as RGBA pixels; `None` (with a warning) when it can't be written.
fn decode_image(&mut self, key: &str) -> Option<image::RgbaImage> {
let Some(img) = self.doc.images.get(key).and_then(|blob| image::load_from_memory(&blob.bytes).ok()) else {
self.warn(&format!("image '{key}' could not be decoded and was left out"));
return None;
};
if u64::from(img.width()) * u64::from(img.height()) > MAX_IMAGE_PIXELS {
self.warn(&format!("image '{key}' is too large for EPS and was left out"));
return None;
}
Some(img.to_rgba8())
}
/// Draw an image of [`Self::encode_image`] on the unit square mapped by `place`.
fn place_image(&mut self, encoded: &str, place: Affine) {
if !invertible(place) {
return;
}
self.line("q");
let _ = writeln!(self.body, "{} cm", ps::matrix(place));
self.body.push_str(encoded);
self.line("Q");
}
/// Straight RGBA pixels (`w` × `h`, rows from the top) as the operators and data that draw
/// them on the unit square: in the process colour space, transparent pixels masked out at
/// Level 3 (a key colour no opaque pixel has), white at Level 2; partly transparent ones over
/// white. `None` for an empty image.
fn encode_image(&mut self, rgba: &[u8], w: u32, h: u32) -> Option<String> {
if w == 0 || h == 0 {
return None;
}
let px = rgba.as_chunks::<4>().0;
let clear = px.iter().any(|p| p[3] == 0);
if px.iter().any(|p| p[3] > 0 && p[3] < 255) {
self.warn(IMAGE_PARTLY);
}
let keyed = clear && self.o.level == Level::Three;
if clear && !keyed {
self.warn(IMAGE_ALPHA_L2);
}
let space = if self.cmyk { Space::Cmyk } else { Space::Rgb };
let n = space.n();
// The key colour, which opaque pixels that happen to have it are nudged off.
let key: [u8; 4] = [255, 0, 255, 0];
// White: no ink, or full light.
let white: [u8; 4] = if space == Space::Cmyk { [0; 4] } else { [255; 4] };
let mut cache: HashMap<[u8; 3], [u8; 4]> = HashMap::new();
let cms = self.cms.clone();
let intent = cms.settings().intent;
let mut data = Vec::with_capacity(px.len() * n);
for p in px {
if p[3] == 0 {
data.extend(if keyed { &key } else { &white }.iter().take(n));
continue;
}
let al = u32::from(p[3]);
let rgb = [0, 1, 2].map(|i| ((u32::from(p[i]) * al + 255 * (255 - al) + 127) / 255) as u8);
let mut v = match space {
Space::Cmyk => *cache
.entry(rgb)
.or_insert_with(|| cms.srgb_to_cmyk(rgb.map(|x| f32::from(x) / 255.0), intent).map(|x| (unit(x) * 255.0).round() as u8)),
_ => [rgb[0], rgb[1], rgb[2], 0],
};
if keyed && v.iter().take(n).eq(key.iter().take(n)) {
v[1] = 1;
}
data.extend(v.iter().take(n));
}
let (filter, encoded) = match self.o.level {
Level::Three => ("/FlateDecode", ps::deflate(&data)),
Level::Two => {
let mut rl = Vec::with_capacity(data.len() / 2);
ps::packbits(&data, &mut rl);
// End of data.
rl.push(128);
("/RunLengthDecode", rl)
}
};
let mut out = String::with_capacity(encoded.len() * 5 / 4 + 400);
let _ = writeln!(out, "{} setcolorspace", space.name());
let decode = "0 1 ".repeat(n);
let _ = write!(
out,
"{{ /VCsrc currentfile /ASCII85Decode filter def\n<< /ImageType {} /Width {w} /Height {h} /BitsPerComponent 8 /Decode [{}] /ImageMatrix [{w} 0 0 {h} 0 0]",
if keyed { 4 } else { 1 },
decode.trim_end()
);
if keyed {
let mask: Vec<String> = key.iter().take(n).map(|v| format!("{v} {v}")).collect();
let _ = write!(out, " /MaskColor [{}]", mask.join(" "));
}
// The procedure is read whole before it runs: the image data follows it, and flushing the
// ASCII85 filter afterwards reads past its end marker.
let _ = writeln!(out, "\n/DataSource VCsrc {filter} filter >> image\nVCsrc flushfile }} exec");
out.push_str(&ps::ascii85(&encoded));
Some(out)
}
}
/// `a` to `b` at `t`.
fn mix(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
[0, 1, 2, 3].map(|i| a[i] + (b[i] - a[i]) * t)
}