mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 08:51:00 -06:00
bfchar and bfrange mappings are kept in the CMap and carried over by usecmap. A code mapped to a character code draws that code in the base font usefont named; a code mapped to a glyph name draws that glyph.
1285 lines
60 KiB
Rust
1285 lines
60 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::cid::Base;
|
|
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};
|
|
use super::type1::Embedded;
|
|
|
|
/// The font a program uses before it sets one.
|
|
const DEFAULT_FONT: &str = "Helvetica";
|
|
|
|
/// A word of a style as PostScript names abbreviate it (`AkzidenzGroteskPro-BoldCnIt`), in full.
|
|
fn style_word(w: &str) -> String {
|
|
match w {
|
|
"Lt" => "Light",
|
|
"Md" | "Med" => "Medium",
|
|
"Bd" => "Bold",
|
|
"XBd" => "Extra Bold",
|
|
"Blk" => "Black",
|
|
"Hv" => "Heavy",
|
|
"Th" => "Thin",
|
|
"Rg" => "Regular",
|
|
"Sb" | "Smbd" => "Semibold",
|
|
"Cn" | "Cond" => "Condensed",
|
|
"Ext" | "Ex" => "Extended",
|
|
"It" => "Italic",
|
|
"Obl" => "Oblique",
|
|
w => w,
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
/// A font's family and style from its PostScript name, without a subset prefix (`ABCDEF+`) or the
|
|
/// `*1` of a re-encoded copy (`ABCDEF+RollerBabyBV*1`): an installed or bundled face of that
|
|
/// exact name gives its own (a family can hold hyphens: `rounded-x-mplus-1c-black` is Rounded-X
|
|
/// M+ 1c, black); else the name is split, `Helvetica-BoldOblique` → (`Helvetica`, `Bold
|
|
/// Oblique`), `TimesNewRomanPS-BoldMT` → (`Times New Roman`, `Bold`).
|
|
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 name = match name.rsplit_once('*') {
|
|
Some((base, n)) if !base.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => base,
|
|
_ => name,
|
|
};
|
|
if let Some(face) = vectorcraft_text::FontDb::global().find_postscript(name) {
|
|
return (face.family.clone(), face.style.clone());
|
|
}
|
|
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.split(' ').map(style_word).collect::<Vec<_>>().join(" "),
|
|
};
|
|
(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)?;
|
|
if let Some(glyphs) = self.composite_glyphs(&s)? {
|
|
self.show_composite(&glyphs, &spacing)?;
|
|
} else {
|
|
match self.type3_font() {
|
|
Some(font) => self.show_type3(&font, &s, &spacing)?,
|
|
None => self.show_plain(&s, &spacing)?,
|
|
}
|
|
}
|
|
}
|
|
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))?;
|
|
}
|
|
op if op == ComposeFont || Op::PROCSET.contains(&op) => self.cid_op(op)?,
|
|
_ => 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> {
|
|
if let Some(glyphs) = self.composite_glyphs(s)? {
|
|
let mut w = Vec2::ZERO;
|
|
for g in &glyphs {
|
|
w += self.composite_glyph(g, Point::new(w.x, w.y), Draw::No)?;
|
|
}
|
|
return Ok(w);
|
|
}
|
|
let Some(font) = self.type3_font() else {
|
|
if let Some((_, glyphs)) = self.type42_plain(s)? {
|
|
let (_, f1) = self.font()?;
|
|
return Ok(glyphs.iter().map(|g| f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO).sum());
|
|
}
|
|
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 = spacing.advance(i, i64::from(code), w);
|
|
self.g.cur = Some(self.xf() * (at + d));
|
|
self.kern(spacing, s, i, at + d)?;
|
|
}
|
|
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> {
|
|
self.type3_glyph_in(font, glyph, at, paint, Affine::IDENTITY)
|
|
}
|
|
|
|
/// [`Self::type3_glyph`] for a descendant of a composite font whose matrix is `outer`.
|
|
fn type3_glyph_in(&mut self, font: &DictRef, glyph: Glyph, at: Point, paint: bool, outer: Affine) -> 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 = outer * font_matrix(&f, 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 = DictRef::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());
|
|
// A composite font's glyphs are its descendants', in their glyph space under its matrix
|
|
// (PLRM 3rd ed., §5.10 "Composite Fonts"): the first descendant stands for them all.
|
|
if font_type(&f) == Some(0.0) {
|
|
let first = f.get(&Key::name("FDepVector")).and_then(|v| v.items()?.get(0));
|
|
if let Some(Obj::Dict(d)) = first {
|
|
let d = d.borrow();
|
|
return Ok((name, font_matrix(&f, 1.0) * font_matrix(&d, 0.001) * Affine::scale(units(&d))));
|
|
}
|
|
}
|
|
Ok((name, font_matrix(&f, 0.001) * Affine::scale(units(&f))))
|
|
}
|
|
|
|
/// `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)> {
|
|
// Every byte is read, also the control codes left out of the type (a new string's zeros).
|
|
self.spend(s.len() as u64)?;
|
|
let text: String = self.named_chars(s).into_iter().filter(|c| !c.is_control()).collect();
|
|
self.set_text(&text, at)
|
|
}
|
|
|
|
/// The character each code of `s` stands for in the current font (one found by name, whose
|
|
/// program isn't in the file): the one its glyph name says (the standard encodings by their
|
|
/// codes' names), else the code read as Latin-1.
|
|
fn named_chars(&self, s: &[u8]) -> Vec<char> {
|
|
let Some(font) = self.g.font.clone() else { return s.iter().map(|b| char::from(*b)).collect() };
|
|
let f = font.borrow();
|
|
let standard = f.get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
|
|
s.iter()
|
|
.map(|&code| {
|
|
let mut name = glyph_name(&f, code);
|
|
if &*name == ".notdef"
|
|
&& let Some(iso) = standard
|
|
{
|
|
name = Rc::from(standard_name(code, iso));
|
|
}
|
|
match &*name {
|
|
".notdef" => char::from(code),
|
|
n => named_char(code, n).unwrap_or(char::from(code)),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// [`Self::set_type`] for the characters `text`.
|
|
fn set_text(&mut self, text: &str, 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((text.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 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 {
|
|
self.show_plain(s, &Spacing::Plain)
|
|
}
|
|
|
|
/// `s` shown in a font that isn't a Type 3 or composite font, spaced as `spacing` says: point
|
|
/// type at the current point, which moves past it as far as the file says, each character
|
|
/// where the file puts it. A Type 42 font that carries its TrueType font gives each code its
|
|
/// character (by its glyph name, else the character its glyph stands for in the font's own
|
|
/// character map) and its glyph's width; type in a font found by name is spaced by the
|
|
/// installed font's advances. Where the file's spacing differs from the advances of the
|
|
/// installed (or fallback) font, each character's tracking makes up the difference, so type
|
|
/// in a font that isn't installed keeps the file's line lengths and justified lines don't
|
|
/// drift, as PDF import does. Glyphs that stand for no character are drawn from the font's
|
|
/// outlines (the whole string, as one group).
|
|
fn show_plain(&mut self, s: &[u8], spacing: &Spacing) -> Res {
|
|
// Every code gets a move, also the control codes left out of the type.
|
|
self.spend(s.len() as u64)?;
|
|
let at = self.user_point()?;
|
|
let (_, f1) = self.font()?;
|
|
let plain = self.type42_plain(s)?;
|
|
if let Some((data, glyphs)) = &plain
|
|
&& glyphs.iter().any(|g| g.ch.is_none() && g.gid != 0)
|
|
{
|
|
return self.show_type42_outlines(data, glyphs, s, spacing, at, f1);
|
|
}
|
|
// Each code's character, if it has one that shows (control codes don't).
|
|
let chars: Vec<Option<char>> = match &plain {
|
|
Some((_, glyphs)) => glyphs.iter().map(|g| g.ch).collect::<Vec<_>>(),
|
|
None => self.named_chars(s).into_iter().map(Some).collect(),
|
|
}
|
|
.into_iter()
|
|
.map(|c| c.filter(|c| !c.is_control()))
|
|
.collect();
|
|
let text: String = chars.iter().flatten().collect();
|
|
let (mut t, mut layout, _) = self.set_text(&text, at)?;
|
|
let size = t.runs.first().map_or(1.0, |r| r.style.size);
|
|
let starts: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
|
|
// The file places each character's glyph: where the layout joins characters in a
|
|
// ligature, the type is set without ligatures, so each character has its own advance.
|
|
let glyph_starts: std::collections::HashSet<usize> = layout.glyphs.iter().map(|g| g.byte).collect();
|
|
if starts.iter().any(|b| !glyph_starts.contains(b)) {
|
|
for run in &mut t.runs {
|
|
run.style.features.push("-liga".into());
|
|
}
|
|
layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
|
t.cached_bounds = Some(layout.bounds);
|
|
}
|
|
// Each character's advance in the layout (text space).
|
|
let mut natural = vec![0.0; starts.len()];
|
|
for g in &layout.glyphs {
|
|
if let Some(n) = starts.binary_search(&g.byte).ok().and_then(|k| natural.get_mut(k)) {
|
|
*n += g.advance;
|
|
}
|
|
}
|
|
// Each code's move (user space) and that move in text space, along the baseline and across.
|
|
let em = |x: f64| f1 * Point::new(x, 0.0) - f1 * Point::ZERO;
|
|
let inv = f1.inverse();
|
|
let mut moves = Vec::with_capacity(s.len());
|
|
let mut k = 0;
|
|
// What `kshow`'s procedure draws goes after the type.
|
|
let start = self.out.drawn.len();
|
|
let mut pen = at;
|
|
for (i, (&code, c)) in s.iter().zip(&chars).enumerate() {
|
|
let own = match &plain {
|
|
Some((_, glyphs)) => glyphs.get(i).map_or(0.0, |g| g.width),
|
|
None if c.is_some() => natural.get(k).copied().unwrap_or(0.0) / size,
|
|
None => 0.0,
|
|
};
|
|
k += usize::from(c.is_some());
|
|
let mut d = spacing.advance(i, i64::from(code), em(own));
|
|
d += self.kern(spacing, s, i, pen + d)?;
|
|
pen += d;
|
|
let e = inv * Point::new(d.x, d.y) - inv * Point::ZERO;
|
|
if ![d.x, d.y, e.x * size, e.y * size].iter().all(|v| v.is_finite()) {
|
|
return ps_err("undefinedresult", "a show");
|
|
}
|
|
moves.push((d, e.x * size, e.y * size));
|
|
}
|
|
let total: Vec2 = moves.iter().map(|m| m.0).sum();
|
|
if !(total.x.is_finite() && total.y.is_finite()) {
|
|
return ps_err("undefinedresult", "a show");
|
|
}
|
|
let across = moves.iter().any(|m| m.2.abs() > size * 1e-3);
|
|
// Nothing shows on the null device (`nulldevice`, a Type 3 glyph only measured).
|
|
let painted = !self.g.paint.is_none() && !self.g.null;
|
|
if across {
|
|
// Moves across the baseline (`yshow`, `xyshow`): each character where the file puts it.
|
|
let mut pen = at;
|
|
for ((d, ..), c) in moves.iter().zip(&chars) {
|
|
if let Some(c) = c
|
|
&& painted
|
|
&& !c.is_whitespace()
|
|
{
|
|
let (t, ..) = self.set_text(c.encode_utf8(&mut [0; 4]), pen)?;
|
|
let clips = self.g.clips.clone();
|
|
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
|
}
|
|
pen += *d;
|
|
}
|
|
} else if painted && !text.trim().is_empty() {
|
|
// The moves of codes before the first character move the type; those of codes after
|
|
// a character, before the next, are that character's.
|
|
let mut lead = Vec2::ZERO;
|
|
let mut target: Vec<f64> = Vec::with_capacity(natural.len());
|
|
for ((d, along, _), c) in moves.iter().zip(&chars) {
|
|
match (c, target.last_mut()) {
|
|
(Some(_), _) => target.push(*along),
|
|
(None, Some(last)) => *last += along,
|
|
(None, None) => lead += *d,
|
|
}
|
|
}
|
|
let tracking: Vec<f64> = target
|
|
.iter()
|
|
.zip(&natural)
|
|
// Hundredths of a thousandth of an em: no drift along a line, no noise in the panels.
|
|
.map(|(want, has)| (((want - has) / size * 1000.0).clamp(-1000.0, 10000.0) * 100.0).round() / 100.0)
|
|
.collect();
|
|
if tracking.iter().any(|t| *t != 0.0)
|
|
&& let Some(run) = t.runs.first().cloned()
|
|
{
|
|
let mut runs: Vec<vectorcraft_doc::TextRun> = Vec::new();
|
|
for (c, tr) in text.chars().zip(&tracking) {
|
|
match runs.last_mut() {
|
|
Some(last) if last.style.tracking == *tr => last.text.push(c),
|
|
_ => runs.push(vectorcraft_doc::TextRun {
|
|
text: c.to_string(),
|
|
style: CharStyle { tracking: *tr, ..run.style.clone() },
|
|
inline: run.inline.clone(),
|
|
}),
|
|
}
|
|
}
|
|
t.runs = runs;
|
|
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
|
|
}
|
|
if lead != Vec2::ZERO {
|
|
t.xf = Affine::translate(self.xf() * Point::new(lead.x, lead.y) - self.xf() * Point::ZERO) * t.xf;
|
|
}
|
|
let clips = self.g.clips.clone();
|
|
if self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips).is_some()
|
|
&& let Some(after) = self.out.drawn.get_mut(start..)
|
|
{
|
|
after.rotate_right(1);
|
|
}
|
|
}
|
|
self.g.cur = Some(self.xf() * (at + total));
|
|
Ok(())
|
|
}
|
|
|
|
/// `kshow` between character `i` of `s` and the next: its procedure run with the two codes
|
|
/// and the current point at user point `pen` (PLRM 3rd ed., `kshow`: the procedure may
|
|
/// change the current point, as kerning does) → how far it moved the point (user space).
|
|
/// Nothing for other show operators or after the last character.
|
|
fn kern(&mut self, spacing: &Spacing, s: &[u8], i: usize, pen: Point) -> Res<Vec2> {
|
|
let (Spacing::Kern(p), Some(&code), Some(&next)) = (spacing, s.get(i), s.get(i + 1)) else { return Ok(Vec2::ZERO) };
|
|
self.g.cur = Some(self.xf() * pen);
|
|
self.push(Obj::Int(i64::from(code)))?;
|
|
self.push(Obj::Int(i64::from(next)))?;
|
|
self.call(p.clone())?;
|
|
Ok(self.user_point()? - pen)
|
|
}
|
|
|
|
/// `s` in a Type 42 font, some of whose glyphs stand for no character, drawn from the font's
|
|
/// outlines at the current point (one group), spaced as `spacing` says.
|
|
fn show_type42_outlines(&mut self, data: &Embedded, glyphs: &[Plain], s: &[u8], spacing: &Spacing, at: Point, f1: Affine) -> Res {
|
|
// The moves, checked before anything is painted: moves that overflow (two `1e308`
|
|
// numbers in `xshow`) are an error, as in `show_plain`, not glyphs at infinity.
|
|
let mut pen = at;
|
|
for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() {
|
|
pen += spacing.advance(i, i64::from(code), f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO);
|
|
if !(pen.x.is_finite() && pen.y.is_finite()) {
|
|
return ps_err("undefinedresult", "a show");
|
|
}
|
|
}
|
|
let start = self.out.drawn.len();
|
|
let mut text = String::new();
|
|
let mut pen = at;
|
|
for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() {
|
|
let glyph = data.glyph(g.gid);
|
|
self.spend(data.take_work())?;
|
|
if let Some(outline) = glyph.map(|o| o.0) {
|
|
self.type42_paint(outline, pen, f1, Draw::Fill)?;
|
|
}
|
|
let d = spacing.advance(i, i64::from(code), f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO);
|
|
// `kshow`'s procedure moves the point as it runs, so its moves are checked as they come.
|
|
let d = d + self.kern(spacing, s, i, pen + d)?;
|
|
pen += d;
|
|
if !(pen.x.is_finite() && pen.y.is_finite()) {
|
|
return ps_err("undefinedresult", "a show");
|
|
}
|
|
text.push(g.ch.unwrap_or('\u{2022}'));
|
|
}
|
|
self.g.cur = Some(self.xf() * pen);
|
|
self.group_glyphs(start, &text);
|
|
Ok(())
|
|
}
|
|
|
|
/// Glyph outline `outline` (glyph space) under font matrix `fm` at user point `at`: filled, or
|
|
/// added to the current path (`charpath`).
|
|
fn type42_paint(&mut self, mut outline: vectorcraft_geom::BezPath, at: Point, fm: Affine, paint: Draw) -> Res {
|
|
// Measuring a glyph reads its outline too, so `stringwidth` in a loop pays for it.
|
|
self.spend((outline.elements().len() as u64).saturating_add(16))?;
|
|
if paint == Draw::No {
|
|
return Ok(());
|
|
}
|
|
outline.apply_affine(self.xf() * Affine::translate(at.to_vec2()) * fm);
|
|
if paint == Draw::Fill {
|
|
return self.fill_path(outline, vectorcraft_geom::FillRule::NonZero);
|
|
}
|
|
if self.g.path.elements().len().saturating_add(outline.elements().len()) > super::graphics::MAX_PATH {
|
|
return Err(PsError::Limit("a path has too many points"));
|
|
}
|
|
self.g.path.extend(outline);
|
|
Ok(())
|
|
}
|
|
|
|
/// `charpath`: the glyph outlines of `s` added to the path.
|
|
fn char_path(&mut self, s: &[u8]) -> Res {
|
|
if let Some(glyphs) = self.composite_glyphs(s)? {
|
|
for g in &glyphs {
|
|
let at = self.user_point()?;
|
|
let w = self.composite_glyph(g, at, Draw::Path)?;
|
|
self.g.cur = Some(self.xf() * (at + w));
|
|
}
|
|
return Ok(());
|
|
}
|
|
if let Some((data, glyphs)) = self.type42_plain(s)? {
|
|
let (_, f1) = self.font()?;
|
|
let mut pen = self.user_point()?;
|
|
for g in &glyphs {
|
|
let glyph = data.glyph(g.gid);
|
|
self.spend(data.take_work())?;
|
|
if let Some((outline, _)) = glyph {
|
|
self.type42_paint(outline, pen, f1, Draw::Path)?;
|
|
}
|
|
pen += f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO;
|
|
}
|
|
self.g.cur = Some(self.xf() * pen);
|
|
return Ok(());
|
|
}
|
|
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>),
|
|
}
|
|
|
|
impl Spacing {
|
|
/// How far glyph `i` (character code `code`, width `w`) moves the current point.
|
|
fn advance(&self, i: usize, code: i64, w: Vec2) -> Vec2 {
|
|
match self {
|
|
Self::Plain | Self::Kern(_) => w,
|
|
Self::Add(a, c) => w + *a + c.filter(|(ch, _)| *ch == code).map_or(Vec2::ZERO, |(_, v)| v),
|
|
Self::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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A character of a composite font: the descendant font that draws it, its code there (its last
|
|
/// byte for a CID), its CID and the notdef mapping's (`FMapType` 9), the composite font's matrix
|
|
/// (over the descendant's own) and whether it is set vertically (`WMode` 1).
|
|
struct Mapped {
|
|
font: DictRef,
|
|
/// The byte that picked the descendant (the font number).
|
|
pick: u8,
|
|
code: u8,
|
|
/// The glyph name a CMap maps the character to, when the font's encoding has no code for it.
|
|
name: Option<Rc<str>>,
|
|
cid: Option<(u32, Option<u32>)>,
|
|
outer: Affine,
|
|
vertical: bool,
|
|
}
|
|
|
|
/// What drawing a glyph does with it.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Draw {
|
|
/// Fills it (`show`).
|
|
Fill,
|
|
/// Adds its outline to the current path (`charpath`).
|
|
Path,
|
|
/// Only measures it (`stringwidth`).
|
|
No,
|
|
}
|
|
|
|
/// Font `f`'s `FontType`.
|
|
fn font_type(f: &Dict) -> Option<f64> {
|
|
f.get(&Key::name("FontType")).and_then(Obj::as_num)
|
|
}
|
|
|
|
/// Font `f`'s matrix, `default` times the identity when it has none.
|
|
fn font_matrix(f: &Dict, default: f64) -> Affine {
|
|
f.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(default))
|
|
}
|
|
|
|
/// Font `f`'s glyph units in its matrix's space: Type 1 glyphs are in 1000-unit space; a Type 42
|
|
/// font's glyph space is one unit an em (its FontMatrix, usually the identity, is already the
|
|
/// em's).
|
|
fn units(f: &Dict) -> f64 {
|
|
if font_type(f) == Some(42.0) { 1.0 } else { 1000.0 }
|
|
}
|
|
|
|
impl Interp<'_> {
|
|
/// The characters of `s` in the current font when it is a composite font with the 8/8 mapping
|
|
/// (`FMapType` 2: a byte picking the descendant through `Encoding` and `FDepVector`, then the
|
|
/// code in it: PLRM 3rd ed., §5.10.1 "Type 0 Font Dictionaries") or a CMap (`FMapType` 9:
|
|
/// codes mapped to a descendant and a CID, §5.11.5); `None` for other fonts and mappings,
|
|
/// which are set as type in the font of the composite font's name.
|
|
fn composite_glyphs(&mut self, s: &[u8]) -> Res<Option<Vec<Mapped>>> {
|
|
let Some(font) = self.g.font.clone() else { return Ok(None) };
|
|
let map = {
|
|
let f = font.borrow();
|
|
if font_type(&f) != Some(0.0) {
|
|
return Ok(None);
|
|
}
|
|
f.get(&Key::name("FMapType")).and_then(Obj::as_num)
|
|
};
|
|
if map == Some(9.0) {
|
|
return self.cid_chars(&font, s).map(Some);
|
|
}
|
|
if map != Some(2.0) {
|
|
return Ok(None);
|
|
}
|
|
// The scan visits every byte pair, also those that map to no glyph and so cost nothing
|
|
// further: charged by the string's length first, so a long string of unmapped characters
|
|
// shown in a loop runs out of the budget.
|
|
self.spend(s.len() as u64)?;
|
|
let f = font.borrow();
|
|
let (Some(encoding), Some(fonts)) =
|
|
(f.get(&Key::name("Encoding")).and_then(|e| e.items().cloned()), f.get(&Key::name("FDepVector")).and_then(|e| e.items().cloned()))
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let (encoding, fonts) = (encoding.borrow(), fonts.borrow());
|
|
let outer = font_matrix(&f, 1.0);
|
|
let mut out = Vec::with_capacity(s.len() / 2);
|
|
for &[pick, code] in s.as_chunks::<2>().0 {
|
|
let index = encoding.get(usize::from(pick)).and_then(Obj::as_num).filter(|i| *i >= 0.0 && i.fract() == 0.0);
|
|
// A character whose descendant is missing (or not a base font) shows nothing.
|
|
if let Some(Obj::Dict(d)) = index.and_then(|i| fonts.get(i as usize))
|
|
&& font_type(&d.borrow()) != Some(0.0)
|
|
{
|
|
out.push(Mapped { font: d.clone(), pick, code, name: None, cid: None, outer, vertical: false });
|
|
}
|
|
}
|
|
Ok(Some(out))
|
|
}
|
|
|
|
/// The characters of `s` in `FMapType` 9 font `font`: its CMap's codes, each a CID in the
|
|
/// descendant `FDepVector` gives its font number (PLRM 3rd ed., §5.11.5).
|
|
fn cid_chars(&mut self, font: &DictRef, s: &[u8]) -> Res<Vec<Mapped>> {
|
|
let (cmap, fonts, outer, wmode) = {
|
|
let f = font.borrow();
|
|
let fonts = f.get(&Key::name("FDepVector")).and_then(|e| e.items().map(|i| i.to_vec())).unwrap_or_default();
|
|
let wmode = f.get(&Key::name("WMode")).and_then(Obj::as_num);
|
|
(f.get(&Key::name("CMap")).cloned(), fonts, font_matrix(&f, 1.0), wmode)
|
|
};
|
|
let Some(Obj::Dict(cmap)) = cmap else { return Ok(vec![]) };
|
|
let vertical = wmode.or_else(|| cmap.borrow().get(&Key::name("WMode")).and_then(Obj::as_num)) == Some(1.0);
|
|
let map = self.cmap(&cmap)?;
|
|
// Decoding stops when it would pass the budget left, which `spend` then reports.
|
|
let (chars, work) = map.decode(s, self.ops_left().saturating_mul(64));
|
|
// Each code is compared with the CMap's ranges and mappings until one matches.
|
|
self.spend((s.len() as u64).saturating_add(work / 64))?;
|
|
Ok(chars
|
|
.into_iter()
|
|
.filter_map(|c| match (fonts.get(c.font), c.base) {
|
|
// A base font's code, or its glyph name (at the code its encoding gives it, if any).
|
|
(Some(Obj::Dict(d)), Some(Base::Code(code))) => {
|
|
Some(Mapped { font: d.clone(), pick: 0, code, name: None, cid: None, outer, vertical })
|
|
}
|
|
(Some(Obj::Dict(d)), Some(Base::Name(name))) => {
|
|
let code = encoded(&d.borrow(), &name);
|
|
let name = code.is_none().then_some(name);
|
|
Some(Mapped { font: d.clone(), pick: 0, code: code.unwrap_or(0), name, cid: None, outer, vertical })
|
|
}
|
|
// A code without a mapping in a base font (not a CIDFont): its code 0, the
|
|
// `.notdef` glyph CID 0 stands for in a CIDFont.
|
|
(Some(Obj::Dict(d)), None) if !d.borrow().contains_key(&Key::name("CIDFontType")) => {
|
|
Some(Mapped { font: d.clone(), pick: 0, code: 0, name: None, cid: None, outer, vertical })
|
|
}
|
|
(Some(Obj::Dict(d)), None) => {
|
|
Some(Mapped { font: d.clone(), pick: 0, code: (c.cid & 0xff) as u8, name: None, cid: Some((c.cid, c.notdef)), outer, vertical })
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// The characters of a composite font shown at the current point, spaced as `spacing` says.
|
|
fn show_composite(&mut self, glyphs: &[Mapped], spacing: &Spacing) -> Res {
|
|
let start = self.out.drawn.len();
|
|
let mut text = String::new();
|
|
for (i, g) in glyphs.iter().enumerate() {
|
|
let at = self.user_point()?;
|
|
let w = self.composite_glyph(g, at, Draw::Fill)?;
|
|
// `widthshow` and `awidthshow` match a composite font's character by its font number
|
|
// times 256 plus its code (PLRM 3rd ed., `widthshow` in chapter 8).
|
|
let d = spacing.advance(i, i64::from(g.pick) * 256 + i64::from(g.code), w);
|
|
self.g.cur = Some(self.xf() * (at + d));
|
|
// A CID stands for no character (its code is only the CID's low byte).
|
|
let c = if g.cid.is_some() { '\u{2022}' } else { g.name.as_deref().map_or_else(|| code_char(&g.font.borrow(), g.code), glyph_char) };
|
|
if c != '\u{2022}' {
|
|
text.push(c);
|
|
}
|
|
}
|
|
if text.trim().is_empty()
|
|
&& let Some(name) = self.g.font.as_ref().and_then(|f| f.borrow().get(&Key::name("FontName")).and_then(Obj::text))
|
|
{
|
|
text = name.to_string();
|
|
}
|
|
self.group_glyphs(start, &text);
|
|
Ok(())
|
|
}
|
|
|
|
/// Character `g` of a composite font at user point `at`, drawn by its descendant: a Type 3
|
|
/// font's procedure, a Type 42 font's TrueType outline, else as type in the font of the
|
|
/// descendant's name (the character its glyph name stands for) → its width (user space).
|
|
fn composite_glyph(&mut self, g: &Mapped, at: Point, paint: Draw) -> Res<Vec2> {
|
|
let builds = {
|
|
let f = g.font.borrow();
|
|
font_type(&f) == Some(3.0) && (f.contains_key(&Key::name("BuildGlyph")) || f.contains_key(&Key::name("BuildChar")))
|
|
};
|
|
if let Some((cid, notdef)) = g.cid {
|
|
return self.cid_glyph_at(g, cid, notdef, at, paint);
|
|
}
|
|
if builds {
|
|
let glyph = g.name.clone().map_or(Glyph::Code(g.code), Glyph::Name);
|
|
return self.type3_glyph_in(&g.font, glyph, at, paint == Draw::Fill, g.outer);
|
|
}
|
|
// An em of the descendant's glyphs: a Type 42 font's matrix maps ems, a Type 1 font's
|
|
// its 1000-unit glyph space.
|
|
let fm = {
|
|
let f = g.font.borrow();
|
|
if units(&f) == 1.0 { g.outer * font_matrix(&f, 1.0) } else { g.outer * font_matrix(&f, 0.001) * Affine::scale(units(&f)) }
|
|
};
|
|
if let Some((outline, advance)) = self.type42_glyph(&g.font, g.code, g.name.as_deref())? {
|
|
self.type42_paint(outline, at, fm, paint)?;
|
|
return Ok(fm * Point::new(advance, 0.0) - fm * Point::ZERO);
|
|
}
|
|
// Set as type in the descendant's font, under the composite font's matrix.
|
|
let c = g.name.as_deref().map_or_else(|| code_char(&g.font.borrow(), g.code), |n| named_char(0, n).unwrap_or('\u{2022}'));
|
|
let Ok(byte) = u8::try_from(u32::from(c)) else { return Ok(Vec2::ZERO) };
|
|
// Scaling the descendant copies its dictionary for every character: charged by its size,
|
|
// so a huge font dictionary shown in a loop runs out of the budget.
|
|
let entries = g.font.borrow().len();
|
|
self.spend(entries as u64)?;
|
|
let saved = self.g.font.replace(transformed(&g.font, g.outer));
|
|
let r = match paint {
|
|
Draw::Fill => {
|
|
self.g.cur = Some(self.xf() * at);
|
|
self.show(&[byte]).and_then(|()| self.user_point()).map(|p| p - at)
|
|
}
|
|
Draw::Path => {
|
|
self.g.cur = Some(self.xf() * at);
|
|
self.char_path(&[byte]).and_then(|()| self.user_point()).map(|p| p - at)
|
|
}
|
|
Draw::No => self.set_type(&[byte], at).map(|t| t.2),
|
|
};
|
|
self.g.font = saved;
|
|
r
|
|
}
|
|
|
|
/// Glyph `cid` of CIDFont `g.font` at user point `at` (else the notdef mapping's glyph, else
|
|
/// CID 0's) → its width (user space), or its vertical displacement in writing mode 1 (the
|
|
/// glyph's origin then half its width left of the point and 0.88 em above it, and the next
|
|
/// one an em below it, the defaults ISO 32000-1 §9.7.4.3 gives CIDFonts without vertical
|
|
/// metrics). A CIDFont the file doesn't carry draws nothing, an em wide.
|
|
fn cid_glyph_at(&mut self, g: &Mapped, cid: u32, notdef: Option<u32>, at: Point, paint: Draw) -> Res<Vec2> {
|
|
let (em, fm) = {
|
|
let f = g.font.borrow();
|
|
// Without a matrix, Type 2 CIDFonts, like Type 42 fonts, are in ems; Type 0 in
|
|
// 1000-unit space. With one, an em is the glyph units it makes one unit of text space.
|
|
let default = if f.get(&Key::name("CIDFontType")).and_then(Obj::as_num) == Some(2.0) { 1.0 } else { 0.001 };
|
|
let m = font_matrix(&f, default);
|
|
let scale = m.determinant().abs().sqrt();
|
|
let em = if scale.is_finite() && scale > 1e-9 { 1.0 / scale } else { 1.0 / default };
|
|
(em, g.outer * m)
|
|
};
|
|
let mut glyph = self.cid_glyph(&g.font, cid)?;
|
|
for other in [notdef, Some(0)].into_iter().flatten() {
|
|
if glyph.is_none() {
|
|
glyph = self.cid_glyph(&g.font, other)?;
|
|
}
|
|
}
|
|
let (outline, w) = match glyph {
|
|
Some(g) => g,
|
|
None => {
|
|
self.out.warn(MISSING_CID_FONT);
|
|
(vectorcraft_geom::BezPath::new(), em)
|
|
}
|
|
};
|
|
if g.vertical {
|
|
self.type42_paint(outline, at, fm * Affine::translate((-w / 2.0, -0.88 * em)), paint)?;
|
|
return Ok(fm * Point::new(0.0, -em) - fm * Point::ZERO);
|
|
}
|
|
self.type42_paint(outline, at, fm, paint)?;
|
|
Ok(fm * Point::new(w, 0.0) - fm * Point::ZERO)
|
|
}
|
|
|
|
/// Glyph `code` (or the glyph named `name`) of font `font` from the font program it carries
|
|
/// (a Type 42 or Type 1 font): its outline and width in ems, or `None` when it carries none.
|
|
fn type42_glyph(&mut self, font: &DictRef, code: u8, name: Option<&str>) -> Res<Option<(vectorcraft_geom::BezPath, f64)>> {
|
|
let Some(data) = self.embedded(font)? else { return Ok(None) };
|
|
let standard = font.borrow().get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
|
|
let gid = match name {
|
|
Some(n) => {
|
|
let f = font.borrow();
|
|
char_string(&f, n).or_else(|| named_char(0, n).and_then(|c| data.gid_of(c))).or_else(|| char_string(&f, ".notdef")).unwrap_or(0)
|
|
}
|
|
None => type42_code(&font.borrow(), &data, code, standard).1,
|
|
};
|
|
// Reading the font's character map (once) visits each of its mappings; running a Type 1
|
|
// glyph's charstrings is paid for by their bytes.
|
|
self.spend(data.take_work())?;
|
|
self.spend(64)?;
|
|
let glyph = data.glyph(gid);
|
|
self.spend(data.take_work())?;
|
|
Ok(glyph)
|
|
}
|
|
|
|
/// The codes of `s` in the current font when it is a font that carries its font program (a
|
|
/// Type 42 font's TrueType font, a Type 1 font's encrypted part), not a descendant of a
|
|
/// composite font: the font program, and each code's glyph.
|
|
fn type42_plain(&mut self, s: &[u8]) -> Res<Option<(Embedded, Vec<Plain>)>> {
|
|
let Some(font) = self.g.font.clone() else { return Ok(None) };
|
|
let Some(data) = self.embedded(&font)? else { return Ok(None) };
|
|
self.spend((s.len() as u64).saturating_mul(16))?;
|
|
let f = font.borrow();
|
|
let standard = f.get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
|
|
let mut glyphs = Vec::with_capacity(s.len());
|
|
for &code in s {
|
|
let (ch, gid) = type42_code(&f, &data, code, standard);
|
|
let width = data.advance(gid);
|
|
// Reading the font's character map (once) visits each of its mappings; measuring a
|
|
// Type 1 glyph runs its charstrings. Paid for code by code, so that a long string of
|
|
// a costly glyph stops at the budget instead of running every glyph first.
|
|
self.spend(data.take_work())?;
|
|
glyphs.push(Plain { ch, gid, width });
|
|
}
|
|
drop(f);
|
|
Ok(Some((data, glyphs)))
|
|
}
|
|
|
|
/// The font program font `font` carries: a Type 42 font's TrueType font, or the Type 1 font
|
|
/// read from its encrypted part (see `type1`).
|
|
fn embedded(&mut self, font: &DictRef) -> Res<Option<Embedded>> {
|
|
if let Some(d) = self.type42_data(font)? {
|
|
return Ok(Some(Embedded::TrueType(d)));
|
|
}
|
|
let strings = match font.borrow().get(&Key::name("CharStrings")) {
|
|
Some(Obj::Dict(d)) if font_type(&font.borrow()) == Some(1.0) => d.clone(),
|
|
_ => return Ok(None),
|
|
};
|
|
Ok(self.type1.iter().find(|(k, _)| k.same(&strings)).map(|(_, f)| Embedded::Type1(f.clone())))
|
|
}
|
|
|
|
/// Whether another embedded Type 1 font can still be kept (see [`Self::attach_type1`]).
|
|
pub(super) fn type1_room(&self) -> bool {
|
|
self.type1.len() < MAX_TYPE1
|
|
}
|
|
|
|
/// Give font dictionary `d` the glyphs of the Type 1 font whose encrypted part is
|
|
/// `encrypted`: a `CharStrings` dictionary naming each glyph's index, by which it is drawn.
|
|
pub(super) fn attach_type1(&mut self, d: &DictRef, encrypted: &[u8]) -> Res {
|
|
if self.type1.len() >= MAX_TYPE1 {
|
|
return Ok(());
|
|
}
|
|
self.alloc(encrypted.len())?;
|
|
self.spend((encrypted.len() / 256) as u64)?;
|
|
let Some(font) = super::type1::parse(encrypted) else { return Ok(()) };
|
|
// Its charstrings are kept a second time, to be run here.
|
|
self.alloc(font.size())?;
|
|
let strings: Dict = font.font.glyph_names().map(|(g, n)| (Key::name(n), Obj::Int(i64::from(g.to_u32())))).collect();
|
|
let strings = DictRef::new(strings);
|
|
d.borrow_mut().insert(Key::name("CharStrings"), Obj::Dict(strings.clone()));
|
|
self.type1.push((strings, Rc::new(font)));
|
|
Ok(())
|
|
}
|
|
|
|
/// The TrueType font Type 42 font `font` carries (read once for each `sfnts` array), or
|
|
/// `None` when it isn't a Type 42 font with one.
|
|
pub(super) fn type42_data(&mut self, font: &DictRef) -> Res<Option<Rc<super::type42::Font>>> {
|
|
let sfnts = {
|
|
let f = font.borrow();
|
|
// Type 42 fonts and Type 2 CIDFonts (FontType 11) carry TrueType fonts.
|
|
let cid2 = f.get(&Key::name("CIDFontType")).and_then(Obj::as_num) == Some(2.0);
|
|
if font_type(&f) != Some(42.0) && !cid2 {
|
|
return Ok(None);
|
|
}
|
|
let Some(Obj::Array { items: sfnts, .. }) = f.get(&Key::name("sfnts")).cloned() else { return Ok(None) };
|
|
sfnts
|
|
};
|
|
// The least recently used font goes first: a hit moves to the end.
|
|
if let Some(i) = self.type42.iter().position(|(k, _)| k.same(&sfnts)) {
|
|
let hit = self.type42.remove(i);
|
|
let data = hit.1.clone();
|
|
self.type42.push(hit);
|
|
return Ok(data);
|
|
}
|
|
// Joining the strings visits each element and copies its bytes, whether or not the font
|
|
// can then be read: charged first, as other bulk copies are.
|
|
let (elements, bytes) = super::type42::font_size(&sfnts.borrow());
|
|
self.spend_bytes(bytes)?;
|
|
self.spend((elements / 16) as u64)?;
|
|
// Fonts whose data can't be read are kept too, so they are copied only once.
|
|
let data = super::type42::font_data(&sfnts.borrow()).map(|d| Rc::new(super::type42::Font::new(d)));
|
|
if let Some(d) = &data
|
|
&& self.type42_counted.insert(sfnts.clone())
|
|
{
|
|
self.alloc(d.data.len())?;
|
|
}
|
|
if self.type42.len() >= MAX_TYPE42 {
|
|
self.type42.remove(0);
|
|
}
|
|
self.type42.push((sfnts, data.clone()));
|
|
Ok(data)
|
|
}
|
|
}
|
|
|
|
/// A character code of a font carrying its program: the character it stands for (`None` when
|
|
/// neither its glyph name nor the font says), its glyph and the glyph's width (ems).
|
|
struct Plain {
|
|
ch: Option<char>,
|
|
gid: u32,
|
|
width: f64,
|
|
}
|
|
|
|
/// The glyph index `CharStrings` gives glyph name `name` in Type 42 font `f`.
|
|
fn char_string(f: &Dict, name: &str) -> Option<u32> {
|
|
let Some(Obj::Dict(d)) = f.get(&Key::name("CharStrings")) else { return None };
|
|
d.borrow().get(&Key::name(name)).and_then(Obj::as_num).filter(|g| (0.0..=f64::from(u16::MAX)).contains(g)).map(|g| g as u32)
|
|
}
|
|
|
|
/// The character and glyph of code `code` in font `f`, carrying font program `data`: by the
|
|
/// glyph name its `Encoding` gives it (a name such as `A`, `uni20AC` or `Euro` says the
|
|
/// character; else the glyph `CharStrings` gives it stands for one in the font's character map
|
|
/// or `post` names). The standard encodings (`standard`: whether ISO Latin-1's) have no names
|
|
/// here: the code's name in them is used (PLRM 3rd ed., Appendix E.5 and E.6). A code the
|
|
/// encoding leaves `.notdef` is read as Latin-1, drawn by that character's glyph.
|
|
fn type42_code(f: &Dict, data: &Embedded, code: u8, standard: Option<bool>) -> (Option<char>, u32) {
|
|
let mut name = glyph_name(f, code);
|
|
if &*name == ".notdef"
|
|
&& let Some(iso) = standard
|
|
{
|
|
name = Rc::from(standard_name(code, iso));
|
|
}
|
|
if &*name == ".notdef" {
|
|
let c = char::from(code);
|
|
// A font without a character map (an embedded Type 1 font) has that character's glyph
|
|
// by its name. A copy of a standard encoding (`StandardEncoding 256 array copy`) comes
|
|
// here too: its names are the empty systemdict array's.
|
|
let latin = match code {
|
|
0x27 => "quotesingle",
|
|
0x2d => "hyphen",
|
|
0x60 => "grave",
|
|
_ => standard_name(code, true),
|
|
};
|
|
return (Some(c), data.gid_of(c).or_else(|| char_string(f, latin)).or_else(|| char_string(f, ".notdef")).unwrap_or(0));
|
|
}
|
|
let named = named_char(code, &name);
|
|
let gid = char_string(f, &name).or_else(|| named.and_then(|c| data.gid_of(c))).or_else(|| char_string(f, ".notdef")).unwrap_or(0);
|
|
(named.or_else(|| data.char_of(gid)), gid)
|
|
}
|
|
|
|
/// The name the standard encoding (or, when `iso`, the ISO Latin-1 encoding) gives code `code`:
|
|
/// the same names in the printable ASCII range but for ISO Latin-1's `minus`; ISO Latin-1's
|
|
/// upper half named as Windows-1252 names it, with its soft `hyphen`.
|
|
fn standard_name(code: u8, iso: bool) -> &'static str {
|
|
use skrifa::raw::ps::encoding::PredefinedEncoding;
|
|
match (iso, code) {
|
|
(false, _) => PredefinedEncoding::Standard.name(code),
|
|
(true, 0x2d) => "minus",
|
|
(true, 0x20..=0x7e) => PredefinedEncoding::Standard.name(code),
|
|
(true, 0xad) => "hyphen",
|
|
(true, 0xa0) => "space",
|
|
(true, 0xa1..=0xff) => crate::latin::high_name(code).unwrap_or(".notdef"),
|
|
_ => ".notdef",
|
|
}
|
|
}
|
|
|
|
/// Most Type 1 fonts read from their encrypted parts.
|
|
const MAX_TYPE1: usize = 256;
|
|
|
|
/// The warning for CID-keyed type in a CIDFont the file doesn't carry.
|
|
const MISSING_CID_FONT: &str = "CID-keyed type in a font the file doesn't carry was left out";
|
|
|
|
/// Most Type 42 fonts whose TrueType data is kept between glyphs.
|
|
const MAX_TYPE42: usize = 16;
|
|
|
|
/// The character code `code` of font `f` stands for: its glyph name's, or the code read as
|
|
/// Latin-1 when the font names no glyph for it (a font found by name, whose program isn't run).
|
|
fn code_char(f: &Dict, code: u8) -> char {
|
|
match &*glyph_name(f, code) {
|
|
".notdef" => char::from(code),
|
|
name => glyph_char(name),
|
|
}
|
|
}
|
|
|
|
/// The code font `f`'s `Encoding` gives glyph name `name`, if any.
|
|
fn encoded(f: &Dict, name: &str) -> Option<u8> {
|
|
let items = f.get(&Key::name("Encoding"))?.items()?.to_vec();
|
|
items.iter().position(|o| o.text().as_deref() == Some(name)).and_then(|i| u8::try_from(i).ok())
|
|
}
|
|
|
|
/// The glyph name font `f`'s `Encoding` gives code `code` (`.notdef` without one).
|
|
fn glyph_name(f: &Dict, code: u8) -> Rc<str> {
|
|
f.get(&Key::name("Encoding")).and_then(|e| e.items()?.get(usize::from(code))).and_then(|o| o.text()).unwrap_or_else(|| Rc::from(".notdef"))
|
|
}
|
|
|
|
/// The character glyph name `name` (at code `code`) says: a single character or `uniXXXX`; a
|
|
/// name of the standard encoding's printable ASCII range (`hyphen`, `quotedbl`: PLRM 3rd ed.,
|
|
/// Appendix E.6); one of the Windows-1252 names; else `None`.
|
|
pub(super) fn named_char(code: u8, name: &str) -> Option<char> {
|
|
match glyph_char(name) {
|
|
'\u{2022}' if name != "bullet" => {}
|
|
c => return Some(c),
|
|
}
|
|
use skrifa::raw::ps::encoding::PredefinedEncoding;
|
|
if let Some(c) = (0x20u8..0x7F).find(|c| PredefinedEncoding::Standard.name(*c) == name) {
|
|
// The standard encoding has curly quotes where ASCII has the straight quote and the grave.
|
|
return Some(match c {
|
|
0x27 => '\u{2019}',
|
|
0x60 => '\u{2018}',
|
|
c => char::from(c),
|
|
});
|
|
}
|
|
crate::latin::glyph(code, name)
|
|
}
|
|
|
|
/// 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));
|
|
DictRef::new(d)
|
|
}
|
|
|
|
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
|
|
pub(super) 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'))
|
|
&& hex.chars().all(|c| c.is_ascii_hexdigit())
|
|
&& let Some(c) = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
|
|
{
|
|
return c;
|
|
}
|
|
match name {
|
|
"space" => ' ',
|
|
_ => '\u{2022}',
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::glyph_char;
|
|
|
|
#[test]
|
|
fn glyph_char_rejects_plus_in_hex() {
|
|
assert_eq!(glyph_char("uni20AC"), '€');
|
|
assert_eq!(glyph_char("u1F600"), '\u{1F600}');
|
|
assert_eq!(glyph_char("uni+041"), '\u{2022}');
|
|
assert_eq!(glyph_char("u+41"), '\u{2022}');
|
|
}
|
|
}
|