Files
vectorcraft/crates/eps/src/import/text.rs
T
bflatasticandClaude Opus 5.5 9f3b992ecc EPS import: other apps' EPS open as vectors, a preview that stands in says why
Discord feedback: "EPS files are always rasterized: they open as bitmaps, in
other programs as vectors". Every EPS saved by Illustrator 2026 fell back to
its TIFF preview (`gcheck` was unknown), and cairo/Inkscape files stopped at
their first image (`status`), dropped masked images (ImageType 3) and failed
on mesh gradients; the warning didn't say where the program failed.

Fallback never silent:
- The warning names the PostScript error, the operator that raised it and
  the named procedures it ran in, innermost first ("a PostScript error
  (typecheck in `add`), in `inner` in `outer`"); errors caught by `stopped`
  aren't reported. It reaches the status bar, document.open, app.open and
  file.open (which now return document.open's result, so MCP's open_file over
  the control channel gets it), and `vectorcraft-cli info` ("warnings").
- A file whose PostScript can't be read opens as its TIFF preview, else its
  Windows metafile preview (as vectors, scaled to the bounding box), else its
  EPSI bitmap. es @msg rows for the new messages.

Interpreter:
- Operators programs probe or use: gcheck/scheck, rootfont,
  currentcolorrendering, currenthsbcolor, status, token, read,
  bytesavailable, fileposition/setfileposition, resetfile, write*, pdfmark
  (marks dropped, as Ghostscript does), execform, nulldevice, strokepath,
  pathforall, user paths (ufill/ueofill/ustroke/uappend/upath/setbbox, the
  encoded form too), setcachedevice2, findcolorrendering, cache, device and
  VM queries, revision; (%stdout) (w) file gives a sink.
- `//name` is replaced by its value when read, inside procedures too.
- Tiling patterns become pattern swatches (their PaintProc drawn in pattern
  space, one cell; uncoloured ones in the colour under them; makepattern
  copies share one swatch); nested patterns stop at 8 levels.
- Type 3 fonts draw their glyphs (BuildGlyph/BuildChar, widths from
  setcachedevice/setcharwidth) as outlines grouped per string, spaced by
  ashow/widthshow/awidthshow/xshow/yshow/xyshow/kshow; glyphshow and
  stringwidth/cshow use them.
- Shadings of types 1 and 4-7 become gradient meshes (data from arrays,
  strings or files, packed or not, flags sharing edges), as fills through a
  shading pattern too; sampled functions (type 0) are evaluated;
  GradientMesh::coons/triangle are shared with PDF import.
- ImageType 3 masked images, interleaved by sample, by row or in separate
  sources; ReusableStreamDecode reads its data when made; Flate/LZW PNG and
  TIFF predictors.
- Bounded: DeviceN at 32 inks, sampled functions at 2^24 samples and 32
  outputs, 4096 patches a shading, strokepath ignores dashes finer than 1e5
  pieces, Type 3 glyphs nest 8 deep (a glyph that shows itself overflowed the
  stack before the limit), 64-bit index maths for 32-bit targets.

Tests: a synthetic program per generator (cairo/Inkscape, its Type 3 fonts,
matplotlib, CorelDRAW, Affinity, LibreOffice, Ghostscript eps2write,
Illustrator procsets), meshes and function shadings, masked images, fault
messages, probing operators, metafile/EPSI previews, `//name`, user paths,
hostile patterns/glyphs/shadings (run against main, 12 of the 14 generator
and feature tests fail; CorelDRAW- and LibreOffice-style programs already
read there). The import fuzz vocabulary covers the new operators. Checked
against real files made here with Inkscape 1.4 (cairo) and Illustrator 2026
(its own format and the CS-to-CC compatibility formats; all fell back or
stopped early on main and open as vectors now; its Illustrator 8 format
needs the next commit); those files aren't committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:23:36 +07:00

478 lines
21 KiB
Rust

//! Fonts and type: fonts are found by name (their programs aren't run) and type becomes point
//! type in the app's font of that name, sized and turned as the font matrix and the current
//! transform say.
use std::rc::Rc;
use std::sync::Arc;
use vectorcraft_color::Paint;
use vectorcraft_doc::{CharStyle, Node, NodeId, NodeKind, TextObject};
use vectorcraft_geom::{Affine, Point, Vec2};
use vectorcraft_text::TextLayout;
use super::graphics::MAX_APART;
use super::interp::{Interp, matrix_obj, matrix_of};
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, ps_err};
/// The font a program uses before it sets one.
const DEFAULT_FONT: &str = "Helvetica";
/// A font's family and style from its PostScript name: `Helvetica-BoldOblique` → (`Helvetica`,
/// `Bold Oblique`), `TimesNewRomanPS-BoldMT` → (`Times New Roman`, `Bold`), without a subset
/// prefix (`ABCDEF+`).
pub fn family_style(name: &str) -> (String, String) {
let name = match name.split_once('+') {
Some((prefix, rest)) if prefix.len() == 6 && prefix.bytes().all(|b| b.is_ascii_uppercase()) => rest,
_ => name,
};
let (family, style) = name.split_once('-').unwrap_or((name, ""));
let trim = |s: &str| -> String {
let s = ["PSMT", "MT", "PS"].iter().find_map(|suffix| s.strip_suffix(suffix)).unwrap_or(s);
// Words: a capital after a small letter starts one.
let mut out = String::with_capacity(s.len() + 4);
let mut prev_lower = false;
for ch in s.chars() {
if ch.is_ascii_uppercase() && prev_lower {
out.push(' ');
}
prev_lower = ch.is_ascii_lowercase();
out.push(ch);
}
out
};
let style = match trim(style).as_str() {
"" | "Roman" | "Regular" | "Book" | "Normal" => "Regular".to_string(),
s => s.to_string(),
};
(trim(family), style)
}
impl Interp<'_> {
pub fn text_op(&mut self, op: Op) -> Res {
use Op::*;
match op {
FindFont => {
let k = self.pop()?;
let font = self.find_font(&k)?;
self.push(Obj::Dict(font))?;
}
DefineFont => {
let font = self.pop_dict()?;
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "definefont".into()))?;
font.borrow_mut().entry(Key::name("FontName")).or_insert_with(|| k.obj());
self.fonts.borrow_mut().insert(k, Obj::Dict(font.clone()));
self.push(Obj::Dict(font))?;
}
UndefineFont => {
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "undefinefont".into()))?;
self.fonts.borrow_mut().remove(&k);
}
ScaleFont | MakeFont => {
let m = if op == ScaleFont {
let s = self.pop_num()?;
Affine::scale(s)
} else {
self.pop_matrix()?
};
let font = self.pop_dict()?;
self.push(Obj::Dict(transformed(&font, m)))?;
}
SetFont => {
let font = self.pop_dict()?;
self.g.font = Some(font);
}
SelectFont => {
let m = match self.pop()? {
Obj::Array { items, .. } => matrix_of(&items.borrow()).ok_or(PsError::Ps("typecheck", "selectfont".into()))?,
o => Affine::scale(o.as_num().ok_or(PsError::Ps("typecheck", "selectfont".into()))?),
};
let k = self.pop()?;
let font = self.find_font(&k)?;
self.g.font = Some(transformed(&font, m));
}
CurrentFont => {
let font = match self.g.font.clone() {
Some(f) => f,
None => self.find_font(&Obj::name(DEFAULT_FONT))?,
};
self.push(Obj::Dict(font))?;
}
FindResource => {
let category = self.pop()?;
let k = self.pop()?;
if category.text().as_deref() == Some("Font") {
let font = self.find_font(&k)?;
return self.push(Obj::Dict(font));
}
let key = k.key().ok_or(PsError::Ps("typecheck", "findresource".into()))?;
let v = self.category(&category)?.borrow().get(&key).cloned();
self.push(v.ok_or_else(|| PsError::Ps("undefinedresource", k.text().map(|t| t.to_string()).unwrap_or_default()))?)?;
}
DefineResource => {
let category = self.pop()?;
let v = self.pop()?;
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "defineresource".into()))?;
// A category is defined by its implementation dictionary.
if category.text().as_deref() == Some("Category") && !matches!(v, Obj::Dict(_)) {
return ps_err("typecheck", "defineresource");
}
self.instances(&category)?.borrow_mut().insert(k, v.clone());
self.push(v)?;
}
UndefineResource => {
let category = self.pop()?;
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "undefineresource".into()))?;
self.instances(&category)?.borrow_mut().remove(&k);
}
ResourceStatus => {
let category = self.pop()?;
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "resourcestatus".into()))?;
let known = self.instances(&category)?.borrow().contains_key(&k);
if known {
self.push(Obj::Int(1))?;
self.push(Obj::Int(0))?;
}
self.push(Obj::Bool(known))?;
}
ResourceForAll => self.resource_for_all()?,
Show | AShow | WidthShow | AWidthShow | XShow | YShow | XYShow | KShow => {
let (s, spacing) = self.show_operands(op)?;
match self.type3_font() {
Some(font) => self.show_type3(&font, &s, &spacing)?,
None => self.show(&s)?,
}
}
CShow => {
// The procedure shows or places each character: it gets its code and width.
let s = self.pop_str()?.to_vec();
let proc = self.pop_proc()?;
for b in s {
let v = self.width_of(&[b])?;
self.push(Obj::Int(i64::from(b)))?;
self.push_num(v.x)?;
self.push_num(v.y)?;
if !self.body(&proc)? {
break;
}
}
}
GlyphShow => {
let name = self.pop()?.text().unwrap_or_else(|| Rc::from(""));
match self.type3_font() {
Some(font) => {
let start = self.out.drawn.len();
let at = self.user_point()?;
let w = self.type3_glyph(&font, Glyph::Name(name.clone()), at, true)?;
self.g.cur = Some(self.xf() * (at + w));
self.group_glyphs(start, &name);
}
None => {
let ch = glyph_char(&name);
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
}
}
}
StringWidth => {
let s = self.pop_str()?.to_vec();
let v = self.width_of(&s)?;
self.push_num(v.x)?;
self.push_num(v.y)?;
}
CharPath => {
self.pop_bool()?;
let s = self.pop_str()?.to_vec();
self.char_path(&s)?;
}
// A glyph procedure gives its width (`w0x w0y` first).
SetCacheDevice => self.glyph_width = Some(self.nums::<6>()?[..2].try_into().unwrap_or_default()),
SetCharWidth => self.glyph_width = Some(self.nums::<2>()?),
SetCacheDevice2 => self.glyph_width = Some(self.nums::<10>()?[..2].try_into().unwrap_or_default()),
RootFont => {
let font = match self.g.font.clone() {
Some(f) => f,
None => self.find_font(&Obj::name(DEFAULT_FONT))?,
};
self.push(Obj::Dict(font))?;
}
_ => return ps_err("undefined", op.name()),
}
Ok(())
}
/// The string of a show operator and how it spaces its glyphs.
fn show_operands(&mut self, op: Op) -> Res<(Vec<u8>, Spacing)> {
use Op::*;
let num_vec = |[x, y]: [f64; 2]| Vec2::new(x, y);
if matches!(op, XShow | YShow | XYShow) {
// A number array (an encoded number string spaces glyphs by their widths).
let widths = match self.pop()? {
Obj::Array { items, .. } => items.borrow().iter().filter_map(Obj::as_num).collect(),
_ => vec![],
};
let s = self.pop_str()?.to_vec();
return Ok((s, Spacing::Widths(widths, op)));
}
let s = self.pop_str()?.to_vec();
let spacing = match op {
AShow => Spacing::Add(num_vec(self.nums()?), None),
WidthShow => {
let ch = self.pop_int()?;
Spacing::Add(Vec2::ZERO, Some((ch, num_vec(self.nums()?))))
}
AWidthShow => {
let a = num_vec(self.nums()?);
let ch = self.pop_int()?;
Spacing::Add(a, Some((ch, num_vec(self.nums()?))))
}
KShow => Spacing::Kern(self.pop_proc()?),
_ => Spacing::Plain,
};
Ok((s, spacing))
}
/// The current font when it is a Type 3 font: its glyphs are drawn by its own procedures.
fn type3_font(&self) -> Option<DictRef> {
let font = self.g.font.clone()?;
let f = font.borrow();
let builds = f.contains_key(&Key::name("BuildGlyph")) || f.contains_key(&Key::name("BuildChar"));
let type3 = f.get(&Key::name("FontType")).and_then(Obj::as_num) == Some(3.0) && builds;
drop(f);
type3.then_some(font)
}
/// How far `s` moves the current point (user space).
fn width_of(&mut self, s: &[u8]) -> Res<Vec2> {
let Some(font) = self.type3_font() else { return Ok(self.set_type(s, Point::ZERO)?.2) };
let mut w = Vec2::ZERO;
for &code in s {
w += self.type3_glyph(&font, Glyph::Code(code), Point::new(w.x, w.y), false)?;
}
Ok(w)
}
/// `s` shown in Type 3 font `font`, glyph by glyph, spaced as `spacing` says.
fn show_type3(&mut self, font: &DictRef, s: &[u8], spacing: &Spacing) -> Res {
let start = self.out.drawn.len();
for (i, &code) in s.iter().enumerate() {
let at = self.user_point()?;
let w = self.type3_glyph(font, Glyph::Code(code), at, true)?;
let d = match spacing {
Spacing::Plain | Spacing::Kern(_) => w,
Spacing::Add(a, c) => w + *a + c.filter(|(ch, _)| *ch == i64::from(code)).map_or(Vec2::ZERO, |(_, v)| v),
Spacing::Widths(v, op) => {
let n = |k: usize| v.get(k).copied();
match op {
Op::XShow => n(i).map(|x| Vec2::new(x, 0.0)),
Op::YShow => n(i).map(|y| Vec2::new(0.0, y)),
_ => n(2 * i).zip(n(2 * i + 1)).map(|(x, y)| Vec2::new(x, y)),
}
.unwrap_or(w)
}
};
self.g.cur = Some(self.xf() * (at + d));
if let (Spacing::Kern(p), Some(next)) = (spacing, s.get(i + 1)) {
self.push(Obj::Int(i64::from(code)))?;
self.push(Obj::Int(i64::from(*next)))?;
self.call(p.clone())?;
}
}
let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
self.group_glyphs(start, &text);
Ok(())
}
/// Glyph `glyph` of Type 3 font `font` drawn by its `BuildGlyph` (or `BuildChar`) procedure at
/// user point `at` (only measured unless `paint`) → its width (user space).
fn type3_glyph(&mut self, font: &DictRef, glyph: Glyph, at: Point, paint: bool) -> Res<Vec2> {
let (fm, build_glyph, build_char, encoding) = {
let f = font.borrow();
let get = |k: &str| f.get(&Key::name(k)).cloned();
let fm = get("FontMatrix").and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
(fm, get("BuildGlyph"), get("BuildChar"), get("Encoding"))
};
let code_of = |name: &str| -> Option<u8> {
let items = encoding.as_ref()?.items()?.to_vec();
items.iter().position(|o| o.text().as_deref() == Some(name)).and_then(|i| u8::try_from(i).ok())
};
let (name, code) = match glyph {
Glyph::Code(c) => {
let name = encoding.as_ref().and_then(|e| e.items()?.get(usize::from(c))).and_then(|o| o.text());
(name.unwrap_or_else(|| Rc::from(".notdef")), Some(c))
}
Glyph::Name(n) => {
let code = code_of(&n);
(n, code)
}
};
let arg = match (build_glyph, build_char, code) {
(Some(p), ..) => Some((p, Obj::Name(name))),
(None, Some(p), Some(c)) => Some((p, Obj::Int(i64::from(c)))),
_ => None,
};
let Some((proc, arg)) = arg else { return Ok(Vec2::ZERO) };
// A glyph that shows type in its own font would draw itself without end.
if self.out.apart >= MAX_APART {
return ps_err("limitcheck", "a Type 3 glyph");
}
self.gsave()?;
let depth = self.saved.len();
self.g.ctm = self.g.ctm * Affine::translate(at.to_vec2()) * fm;
self.take_path();
self.g.null |= !paint;
self.glyph_width = None;
self.out.apart += 1;
let r = self.push(Obj::Dict(font.clone())).and_then(|()| self.push(arg)).and_then(|()| self.call(proc));
self.out.apart -= 1;
let [wx, wy] = self.glyph_width.take().unwrap_or_default();
self.saved.truncate(depth);
self.grestore();
r?;
Ok(fm * Point::new(wx, wy) - fm * Point::ZERO)
}
/// The objects drawn since `start` (one show's glyphs) as one group named `text`, when they
/// are several under the same clips.
fn group_glyphs(&mut self, start: usize, text: &str) {
let here = self.out.chain(&self.g.clips);
let Some(drawn) = self.out.drawn.get(start..) else { return };
let same = drawn.iter().all(|(c, _)| c.iter().map(|c| c.id).eq(here.iter().map(|c| c.id)));
if drawn.len() < 2 || !same {
return;
}
let children: Vec<Arc<Node>> = self.out.drawn.drain(start..).map(|(_, n)| Arc::new(n)).collect();
let mut group = Node::group(NodeId(0), children);
group.name = Some(text.chars().take(64).collect());
let clips = self.g.clips.clone();
self.out.push(group, &clips);
}
/// The font named `k`: one the program defined, else a font of that name.
fn find_font(&mut self, k: &Obj) -> Res<DictRef> {
let key = k.key().ok_or(PsError::Ps("typecheck", "findfont".into()))?;
if let Some(Obj::Dict(d)) = self.fonts.borrow().get(&key) {
return Ok(d.clone());
}
let mut d = Dict::new();
d.insert(Key::name("FontName"), key.obj());
d.insert(Key::name("FontType"), Obj::Int(1));
d.insert(Key::name("FontMatrix"), matrix_obj(Affine::scale(0.001)));
d.insert(Key::name("FontBBox"), Obj::array(vec![Obj::Int(0), Obj::Int(-250), Obj::Int(1000), Obj::Int(1000)]));
d.insert(Key::name("Encoding"), Obj::array(vec![Obj::name(".notdef"); 256]));
let font = Rc::new(std::cell::RefCell::new(d));
self.fonts.borrow_mut().insert(key, Obj::Dict(font.clone()));
Ok(font)
}
/// The current font's name and its matrix for a font of one unit (user space).
fn font(&mut self) -> Res<(String, Affine)> {
let font = match self.g.font.clone() {
Some(f) => f,
None => self.find_font(&Obj::name(DEFAULT_FONT))?,
};
let f = font.borrow();
let name = f.get(&Key::name("FontName")).and_then(Obj::text).map_or_else(|| DEFAULT_FONT.to_string(), |n| n.to_string());
let m = f.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
Ok((name, m * Affine::scale(1000.0)))
}
/// `s` (read as Latin-1) set in the current font at user point `at`: point type placed there,
/// its layout in text space, and how far it moves the current point (user space).
fn set_type(&mut self, s: &[u8], at: Point) -> Res<(TextObject, TextLayout, Vec2)> {
// Laying out type costs about as much as 300 operations, and 64 more a character
// (measured), so `{ (a) stringwidth pop pop } loop` or `cshow` on a long string ends
// within the budget like any other loop instead of running for minutes.
self.spend((s.len() as u64).saturating_mul(64).saturating_add(300))?;
let (name, f1) = self.font()?;
// Text space (y down, `size` points an em) onto the document.
let m = self.xf() * Affine::translate(at.to_vec2()) * f1;
let size = m.determinant().abs().sqrt();
if !(size.is_finite() && size > 1e-3 && size < 1e5) {
return ps_err("undefinedresult", "a font size");
}
let (family, font_style) = family_style(&name);
// The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei".
let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
let fill = if self.g.paint.is_none() { Paint::solid(vectorcraft_color::Color::BLACK) } else { self.g.paint.clone() };
let style = CharStyle { font_family, font_style, size, fill, stroke: Paint::None, ..CharStyle::default() };
let mut t = TextObject::point(Point::ZERO, &text, style);
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
t.xf = m * Affine::scale_non_uniform(1.0 / size, -1.0 / size);
t.cached_bounds = Some(layout.bounds);
let adv: f64 = layout.glyphs.iter().map(|g| g.advance).sum();
Ok((t, layout, f1 * Point::new(adv / size, 0.0) - f1 * Point::ZERO))
}
/// `show`: point type at the current point, which moves past it.
fn show(&mut self, s: &[u8]) -> Res {
let at = self.user_point()?;
let (t, _, adv) = self.set_type(s, at)?;
if !self.g.paint.is_none() && !t.plain_text().trim().is_empty() {
let clips = self.g.clips.clone();
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
}
self.g.cur = Some(self.xf() * (at + adv));
Ok(())
}
/// `charpath`: the glyph outlines of `s` added to the path.
fn char_path(&mut self, s: &[u8]) -> Res {
let at = self.user_point()?;
let (t, layout, adv) = self.set_type(s, at)?;
for g in &layout.glyphs {
self.grow()?;
let mut o = g.outline.clone();
o.apply_affine(t.xf);
self.g.path.extend(o);
}
self.g.cur = Some(self.xf() * (at + adv));
Ok(())
}
}
/// How a show operator spaces its glyphs (Type 3 fonts; type in other fonts is laid out as point
/// type).
enum Spacing {
/// By their widths.
Plain,
/// `ashow`, `widthshow`, `awidthshow`: by their widths plus a displacement, and another for one
/// character code.
Add(Vec2, Option<(i64, Vec2)>),
/// `xshow`, `yshow`, `xyshow` (the operator): by the numbers given.
Widths(Vec<f64>, Op),
/// `kshow`: by their widths, the procedure run between each two.
Kern(Obj),
}
/// A glyph of a Type 3 font, by character code or by name (`glyphshow`).
enum Glyph {
Code(u8),
Name(Rc<str>),
}
/// A font scaled or transformed by `m` (`scalefont`, `makefont`).
fn transformed(font: &DictRef, m: Affine) -> DictRef {
let mut d = font.borrow().clone();
let fm = d.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
d.insert(Key::name("FontMatrix"), matrix_obj(m * fm));
Rc::new(std::cell::RefCell::new(d))
}
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
fn glyph_char(name: &str) -> char {
let mut chars = name.chars();
if let (Some(c), None) = (chars.next(), chars.next()) {
return c;
}
if let Some(hex) = name.strip_prefix("uni").or_else(|| name.strip_prefix('u'))
&& let Some(c) = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
{
return c;
}
match name {
"space" => ' ',
_ => '\u{2022}',
}
}