Files
vectorcraft/crates/eps/src/print.rs
T
dotCooCoo 766e291e3c Save and Export: notes of what the file loses come before notes on options
Each writer marks its notes as a loss (something of the document that the
file leaves out or that reaches it changed) or as a note on the options
(an option it didn't apply or applied another way while the art reaches
the file as it looks, or something an option turns off). The PDF, SVG,
EPS, legacy .ai, DXF and HP-GL writers return their notes on the options
in a list of their own (`setting_notes`) beside `warnings`, and the
engine's `Encoded` does the same.

The replies of the save and export commands, and of
`clipboard.exportPdf` and `clipboard.exportPng`, list the losses first,
each kind in the order the writers gave them, across every file that
Export for Screens writes. The status bar names the first note, so an
export to lossy WebP of type in a font that isn't installed now names the
fallback-font note instead of the lossy WebP note. A print to a file
keeps the order its notes are given in.
2026-11-12 08:33:57 -08:00

189 lines
8.1 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.
//! Print to a PostScript file: the sheets a print job laid out ([`PrintPage`]), each drawn by the
//! document walk the EPS writer uses, in one DSC-conforming file: a page per sheet with its paper
//! size, the ink and halftone screen of a separation, the flatness, and film negatives.
use std::fmt::Write;
use kurbo::{Shape, Vec2};
use vectorcraft_doc::{Document, Node};
use vectorcraft_geom::{Affine, Rect};
use crate::scene::{Page, Scene};
use crate::{EpsOptions, EpsOutput, Level, comments, ps};
/// One sheet of a print job (see the PDF writer's print plan for how the spaces relate).
#[derive(Clone, Debug)]
pub struct PrintPage<'a> {
/// The document the sheet draws: the job's, or a separation's plate.
pub doc: &'a Document,
/// The page size in points.
pub size: (f64, f64),
/// Drawing space → page space (y down from the top-left corner).
pub view: Affine,
/// The part of the drawing space that shows (a tile): art and marks are clipped to it.
pub window: Option<Rect>,
/// Document space → drawing space.
pub place: Affine,
/// Copies on the sheet: each one's move in drawing space from where [`Self::place`] puts the
/// art. The art and the marks are drawn once per copy. Empty: one copy, not moved.
pub copies: &'a [Vec2],
/// The art drawn, in document space: what lies outside it is left out.
pub area: Rect,
/// Printer's marks, in drawing space.
pub marks: Option<&'a Node>,
/// A film negative: paper black, ink clear.
pub negative: bool,
/// A separation's ink: its name, screen ruling (lpi) and angle (degrees).
pub ink: Option<(&'a str, f64, f64)>,
}
/// The settings of a whole job.
#[derive(Clone, Debug, Default)]
pub struct PrintJob {
pub level: Level,
/// `%%Title`.
pub title: String,
/// `%%CreationDate` as Unix seconds (UTC).
pub created: Option<i64>,
/// The flatness curves are drawn with (device pixels); `None`: the device's own.
pub flatness: Option<f64>,
}
/// Every number of `a` finite?
fn finite(a: Affine) -> bool {
a.as_coeffs().iter().all(|v| v.is_finite())
}
/// The program that draws sheet `p` (its page setup and content, without the DSC page comment),
/// and what its walks found.
fn sheet(p: &PrintPage, job: &PrintJob) -> Result<(String, Vec<Page>), String> {
let (w, h) = p.size;
let r = p.area;
if !(w.is_finite() && h.is_finite() && w >= 1.0 && h >= 1.0 && [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite())) {
return Err(format!("page size {w} × {h}"));
}
// `moves` holds each copy's move in drawing space; without copies, it holds one zero move.
let moves = if p.copies.is_empty() { &[Vec2::ZERO][..] } else { p.copies };
let place_at = |m: Vec2| if m == Vec2::ZERO { p.place } else { Affine::translate(m) * p.place };
if !finite(p.view) || !moves.iter().all(|m| finite(place_at(*m))) {
return Err("a page's placement is not finite".into());
}
let opts = |region: Rect| EpsOptions { level: job.level, region, ..EpsOptions::default() };
let art_opts = opts(r);
let art = Scene::new(p.doc, &art_opts).run();
// The marks' area: the drawing space the page shows (within the tile).
let paper = Rect::new(0.0, 0.0, w, h);
let shown = if p.view.determinant().abs() > 1e-12 { p.view.inverse().transform_rect_bbox(paper) } else { paper };
let shown = p.window.map_or(shown, |t| t.intersect(shown));
// The marks body is drawn for every copy, so it is generated for the union of the regions
// the copies show.
let shown = moves.iter().map(|m| shown - *m).reduce(|a, b| a.union(b)).unwrap_or(shown);
let marks_opts = opts(shown);
let marks = p.marks.map(|m| Scene::new(p.doc, &marks_opts).run_node(m));
let mut s = String::with_capacity(art.body.len() + art.setup.len() + 1024);
let (wi, hi) = (w.ceil() as i64, h.ceil() as i64);
let _ = writeln!(s, "%%PageBoundingBox: 0 0 {wi} {hi}");
if let Some((name, _, _)) = p.ink {
// A line break would end the comment and the rest would run as PostScript.
let _ = writeln!(s, "%%PlateColor: {}", name.replace(['\r', '\n'], " "));
}
s.push_str("%%BeginPageSetup\n");
let _ = writeln!(s, "/setpagedevice where {{pop << /PageSize [{} {}] >> setpagedevice}} if", ps::num(w), ps::num(h));
s.push_str("%%EndPageSetup\nVCdict begin\nq\n");
if p.negative {
// Ink becomes clear and paper black: the inverted transfer paints the paper's white black.
let _ = writeln!(s, "{{1 exch sub}} settransfer 1 g 0 0 {} {} rectfill", ps::num(w), ps::num(h));
}
if let Some((_, lpi, angle)) = p.ink {
// A round dot.
let _ = writeln!(s, "{} {} {{dup mul exch dup mul add 1 exch sub}} setscreen", ps::num(lpi), ps::num(angle));
}
if let Some(f) = job.flatness {
let _ = writeln!(s, "{} setflat", ps::num(f));
}
// Page space (y down) onto the paper (y up), then the drawing space.
let _ = writeln!(s, "[1 0 0 -1 0 {}] cm", ps::num(h));
let _ = writeln!(s, "{} cm", ps::matrix(p.view));
if let Some(t) = p.window {
ps::push_path(&mut s, &t.to_path(0.1));
s.push_str("W\n");
}
for (i, m) in moves.iter().enumerate() {
s.push_str("q\n");
let _ = writeln!(s, "{} cm", ps::matrix(place_at(*m)));
ps::push_path(&mut s, &r.to_path(0.1));
s.push_str("W\n");
// The setup defines the spot color spaces once; each copy draws the same body.
if i == 0 {
s.push_str(&art.setup);
}
s.push_str(&art.body);
s.push_str("Q\n");
}
let mut found = vec![art];
if let Some(marks) = marks {
// The marks' spot colour spaces are defined after the art is drawn: names may be reused.
s.push_str(&marks.setup);
for m in moves {
let moved = *m != Vec2::ZERO;
if moved {
let _ = writeln!(s, "q\n{} cm", ps::matrix(Affine::translate(*m)));
}
s.push_str(&marks.body);
if moved {
s.push_str("Q\n");
}
}
found.push(marks);
}
s.push_str("Q\nend\nshowpage\n%%PageTrailer\n");
Ok((s, found))
}
/// Write the sheets `pages` in `order` (indices into `pages`; copies repeat them) as one
/// PostScript file of `order.len()` pages. Each sheet is drawn once and repeated for its copies.
pub fn print(pages: &[PrintPage], order: &[usize], job: &PrintJob) -> Result<EpsOutput, String> {
if order.is_empty() {
return Err("nothing to print".into());
}
let mut drawn = Vec::with_capacity(pages.len());
let mut custom: Vec<(String, [f32; 4])> = vec![];
let mut warnings: Vec<String> = vec![];
for p in pages {
let (s, found) = sheet(p, job)?;
for page in found {
for c in page.custom {
if !custom.iter().any(|(n, _)| *n == c.0) {
custom.push(c);
}
}
for w in page.warnings {
if !warnings.contains(&w) {
warnings.push(w);
}
}
}
drawn.push(s);
}
let (w, h) = pages.iter().fold((0.0f64, 0.0f64), |(w, h), p| (w.max(p.size.0), h.max(p.size.1)));
let size: usize = order.iter().filter_map(|i| drawn.get(*i)).map(String::len).sum();
let mut s = String::with_capacity(size + 4096);
s.push_str("%!PS-Adobe-3.0\n"); // brand-ok: the DSC version line every conforming file starts with
comments(&mut s, &job.title, job.created, job.level, &custom);
let _ = writeln!(s, "%%BoundingBox: 0 0 {} {}", w.ceil() as i64, h.ceil() as i64);
let _ = writeln!(s, "%%Pages: {}\n%%PageOrder: Ascend\n%%EndComments", order.len());
s.push_str("%%BeginProlog\n");
s.push_str(ps::PROLOG);
s.push_str("%%EndProlog\n%%BeginSetup\n%%EndSetup\n");
for (n, i) in order.iter().enumerate() {
let page = drawn.get(*i).ok_or("a page out of range")?;
let _ = writeln!(s, "%%Page: {0} {0}", n + 1);
s.push_str(page);
}
s.push_str("%%Trailer\n%%EOF\n");
Ok(EpsOutput { bytes: s.into_bytes(), warnings, setting_notes: vec![] })
}
#[cfg(test)]
mod tests;