mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Merge pull request #6031 from mockydevs/fix/eps-cid-fonts
EPS import: CID-keyed fonts draw the glyphs of the CIDFonts the file carries
This commit is contained in:
@@ -2415,6 +2415,39 @@ proptest! {
|
||||
let text = type42_composite_eps(&font, gid);
|
||||
survive("damaged Type 42 glyphs", || vectorcraft_eps::import(text.as_bytes()).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// CID-keyed fonts with hostile CMaps (codespace ranges of any length, mappings past them)
|
||||
/// and Type 0 CIDFonts with damaged binary data (offset table, charstrings, subroutine table,
|
||||
/// any FDBytes and GDBytes), shown with any bytes: drawn, measured and outlined, or refused.
|
||||
#[test]
|
||||
fn eps_cid_fonts_never_panic(
|
||||
space in prop::collection::vec((prop::collection::vec(any::<u8>(), 0..6), prop::collection::vec(any::<u8>(), 0..6)), 0..4),
|
||||
cids in prop::collection::vec((prop::collection::vec(any::<u8>(), 1..5), any::<u16>()), 0..6),
|
||||
data in prop::collection::vec(any::<u8>(), 0..200),
|
||||
fd_bytes in 0u8..6,
|
||||
gd_bytes in 0u8..6,
|
||||
len_iv in -2i8..8,
|
||||
shown in prop::collection::vec(any::<u8>(), 0..24),
|
||||
) {
|
||||
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let ranges: String = space.iter().map(|(lo, hi)| format!("<{}> <{}>\n", hex(lo), hex(hi))).collect();
|
||||
let maps: String = cids.iter().map(|(c, cid)| format!("<{}> <{}> {cid}\n", hex(c), hex(c))).collect();
|
||||
let text = format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\
|
||||
/CIDInit /ProcSet findresource begin 20 dict begin /CIDFontName /F def /CIDFontType 0 def\n\
|
||||
/FDArray [4 dict begin /FontMatrix [0.001 0 0 0.001 0 0] def /Private 4 dict dup begin /lenIV {len_iv} def\n\
|
||||
/SubrMapOffset 3 def /SDBytes 2 def /SubrCount 4 def end def currentdict end] def\n\
|
||||
/CIDMapOffset 0 def /FDBytes {fd_bytes} def /GDBytes {gd_bytes} def /CIDCount 40 def\n\
|
||||
(Hex) {} StartData\n{}\n\
|
||||
/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /M def\n\
|
||||
{} begincodespacerange\n{ranges}endcodespacerange\n{} begincidrange\n{maps}endcidrange\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/C /M [/F] composefont 40 scalefont setfont 10 20 moveto <{}> show <{}> stringwidth pop pop\n\
|
||||
10 60 moveto <{}> true charpath fill\nshowpage\n%%EOF\n",
|
||||
data.len(), hex(&data), space.len(), cids.len(), hex(&shown), hex(&shown), hex(&shown)
|
||||
);
|
||||
survive("hostile CID-keyed fonts", || vectorcraft_eps::import(text.as_bytes()).ok().map(|r| r.document))?;
|
||||
}
|
||||
}
|
||||
|
||||
/// The TrueType font [`type42_composite_eps`] carries.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
mod ai;
|
||||
mod ate;
|
||||
mod cid;
|
||||
mod data;
|
||||
mod graphics;
|
||||
mod interp;
|
||||
@@ -49,6 +50,8 @@ mod type42;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_cid;
|
||||
#[cfg(test)]
|
||||
mod tests_composite;
|
||||
#[cfg(test)]
|
||||
mod tests_fontnames;
|
||||
@@ -60,6 +63,8 @@ mod tests_illustrator;
|
||||
mod tests_images;
|
||||
#[cfg(test)]
|
||||
mod tests_native;
|
||||
#[cfg(test)]
|
||||
mod tests_type42;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
//! CID-keyed fonts (PLRM 3rd ed., §5.11): CMaps built with the operators of the `CIDInit`
|
||||
//! procedure set (and the predefined `Identity-H` and `Identity-V`), `composefont`, Type 0
|
||||
//! fonts with `FMapType` 9 that map a show string's codes through their CMap to CIDs, and the
|
||||
//! glyphs of the CIDFonts they select: Type 0 CIDFonts (Type 1 charstrings in the binary data
|
||||
//! `StartData` reads, or in a `GlyphDirectory`: PLRM 3rd ed., §5.11.3, and Adobe Technical Note
|
||||
//! #5014, "Adobe CMap and CIDFont Files Specification") and Type 2 CIDFonts (TrueType glyphs in
|
||||
//! `sfnts`, picked through `CIDMap`). CIDs carry no characters: the glyphs are drawn from the
|
||||
//! font's outlines, as a composite font's are.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use skrifa::raw::ps::cs::{self, CharstringContext, CharstringKind, CommandSink};
|
||||
use skrifa::raw::ps::error::Error as CsError;
|
||||
use skrifa::raw::types::Fixed;
|
||||
use vectorcraft_geom::BezPath;
|
||||
|
||||
use super::interp::{Interp, matrix_obj};
|
||||
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
||||
|
||||
/// Most codespace ranges, character and notdef mappings a CMap may hold.
|
||||
const MAX_RANGES: usize = 1 << 16;
|
||||
/// Most bytes of glyph data a Type 0 CIDFont may carry.
|
||||
const MAX_GLYPH_DATA: usize = 64 << 20;
|
||||
/// Most CMaps whose mappings are kept read between shows.
|
||||
const MAX_CMAPS: usize = 8;
|
||||
/// Longest code a codespace range may give (PLRM 3rd ed., §5.11.5: the bytes of an integer).
|
||||
const MAX_CODE: usize = 4;
|
||||
|
||||
/// The CMap entries the CIDInit operators fill: codespace ranges (`[lo hi …]`, strings), and
|
||||
/// character and notdef mappings to CIDs (`[lo hi cid font …]`, the codes as strings).
|
||||
const CODESPACE: &str = "CodeSpaceRange";
|
||||
const CIDS: &str = "CIDRange";
|
||||
const NOTDEFS: &str = "NotDefRange";
|
||||
|
||||
/// The mappings of the CMaps shown with so far, by their character mapping, codespace and
|
||||
/// notdef mapping arrays, the most recent last.
|
||||
pub(super) type CMaps = Vec<([Option<Shared<Obj>>; 3], Rc<CMap>)>;
|
||||
|
||||
/// A CMap's mappings, read from its dictionary.
|
||||
pub(super) struct CMap {
|
||||
/// Codespace ranges: each byte's bounds.
|
||||
space: Vec<Vec<(u8, u8)>>,
|
||||
/// Character mappings (code length, first code, last code, first CID, font), latest first.
|
||||
cids: Vec<(usize, u32, u32, u32, usize)>,
|
||||
/// Notdef mappings, latest first.
|
||||
notdefs: Vec<(usize, u32, u32, u32, usize)>,
|
||||
}
|
||||
|
||||
/// A character of an `FMapType` 9 font: its code's descendant font and CID, and the notdef
|
||||
/// mapping's CID for when that font has no glyph for it.
|
||||
pub(super) struct CidChar {
|
||||
pub font: usize,
|
||||
pub cid: u32,
|
||||
pub notdef: Option<u32>,
|
||||
}
|
||||
|
||||
impl CMap {
|
||||
/// The mappings of CMap dictionary `d`.
|
||||
fn read(d: &Dict) -> Self {
|
||||
let items = |k: &str| d.get(&Key::name(k)).and_then(|a| a.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||
let bytes = |o: &Obj| match o {
|
||||
Obj::Str(s) => Some(s.to_vec()),
|
||||
_ => None,
|
||||
};
|
||||
let space = items(CODESPACE)
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|[lo, hi]| {
|
||||
let (lo, hi) = (bytes(lo)?, bytes(hi)?);
|
||||
(!lo.is_empty() && lo.len() == hi.len() && lo.len() <= MAX_CODE).then(|| lo.into_iter().zip(hi).collect())
|
||||
})
|
||||
.collect();
|
||||
let maps = |k: &str| -> Vec<(usize, u32, u32, u32, usize)> {
|
||||
items(k)
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|[lo, hi, cid, font]| {
|
||||
let (lo, hi) = (bytes(lo)?, bytes(hi)?);
|
||||
let cid = cid.as_num().filter(|c| (0.0..=f64::from(u32::MAX)).contains(c))? as u32;
|
||||
let font = font.as_num().filter(|f| (0.0..=255.0).contains(f))? as usize;
|
||||
(!lo.is_empty() && lo.len() == hi.len() && lo.len() <= MAX_CODE).then(|| (lo.len(), code(&lo), code(&hi), cid, font))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Self { space, cids: maps(CIDS), notdefs: maps(NOTDEFS) }
|
||||
}
|
||||
|
||||
/// The characters of show string `s` (PLRM 3rd ed., §5.11.5 "CMap Mapping" and "Handling
|
||||
/// Undefined Characters"), and how many ranges and mappings were compared to find them. It
|
||||
/// stops once that passes `limit` (the characters found so far, and the work done).
|
||||
pub fn decode(&self, s: &[u8], limit: u64) -> (Vec<CidChar>, u64) {
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
let mut work = 0u64;
|
||||
let mut i = 0;
|
||||
while let Some(rest) = s.get(i..).filter(|r| !r.is_empty() && work <= limit) {
|
||||
let matched = (1..=MAX_CODE.min(rest.len())).find(|&n| {
|
||||
work += self.space.len() as u64;
|
||||
self.space.iter().any(|r| r.len() == n && fits(r, rest))
|
||||
});
|
||||
let n = match matched {
|
||||
Some(n) => n,
|
||||
// An invalid code: as many bytes as the best partly matching range's codes.
|
||||
None => self.partial(rest),
|
||||
};
|
||||
let n = n.clamp(1, rest.len());
|
||||
let c = code(rest.get(..n).unwrap_or_default());
|
||||
let find = |maps: &[(usize, u32, u32, u32, usize)], work: &mut u64| {
|
||||
let at = maps.iter().position(|m| m.0 == n && (m.1..=m.2).contains(&c));
|
||||
*work += at.map_or(maps.len(), |k| k + 1) as u64;
|
||||
at.and_then(|k| maps.get(k)).map(|m| (m.4, m.3.saturating_add(c - m.1), m.3))
|
||||
};
|
||||
let notdef = find(&self.notdefs, &mut work).map(|(_, _, base)| base);
|
||||
out.push(match (matched.is_some(), find(&self.cids, &mut work)) {
|
||||
(true, Some((font, cid, _))) => CidChar { font, cid, notdef },
|
||||
// No character mapping: the notdef mapping's CID, else font 0's CID 0.
|
||||
_ => match find(&self.notdefs, &mut work) {
|
||||
Some((font, _, base)) if matched.is_some() => CidChar { font, cid: base, notdef: None },
|
||||
_ => CidChar { font: 0, cid: 0, notdef: None },
|
||||
},
|
||||
});
|
||||
i += n;
|
||||
}
|
||||
(out, work)
|
||||
}
|
||||
|
||||
/// How many bytes an invalid code at the start of `rest` takes: the length of the codes of
|
||||
/// the range matching the most of its first bytes (the shortest of those), or of the
|
||||
/// shortest range when none matches its first byte.
|
||||
fn partial(&self, rest: &[u8]) -> usize {
|
||||
let prefix = |r: &[(u8, u8)]| r.iter().zip(rest).take_while(|((lo, hi), b)| (lo..=hi).contains(b)).count();
|
||||
let best = self.space.iter().map(|r| prefix(r)).max().unwrap_or(0);
|
||||
let pick = self.space.iter().filter(|r| best == 0 || prefix(r) == best).map(Vec::len).min();
|
||||
pick.unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Does code `bytes` (as long as `range`, at least) start in codespace range `range`?
|
||||
fn fits(range: &[(u8, u8)], bytes: &[u8]) -> bool {
|
||||
range.len() <= bytes.len() && range.iter().zip(bytes).all(|((lo, hi), b)| (lo..=hi).contains(&b))
|
||||
}
|
||||
|
||||
/// The number a code's bytes make, the first byte highest.
|
||||
fn code(bytes: &[u8]) -> u32 {
|
||||
bytes.iter().take(MAX_CODE).fold(0u32, |c, b| c << 8 | u32::from(*b))
|
||||
}
|
||||
|
||||
/// The number `n` bytes at `at` of `data` make, the first byte highest.
|
||||
fn number(data: &[u8], at: usize, n: usize) -> Option<u64> {
|
||||
let bytes = data.get(at..at.checked_add(n)?)?;
|
||||
(n <= 8).then(|| bytes.iter().fold(0u64, |v, b| v << 8 | u64::from(*b)))
|
||||
}
|
||||
|
||||
/// A predefined Identity CMap: two-byte codes, each the CID of its own value; vertical for
|
||||
/// `Identity-V`.
|
||||
pub(super) fn identity(name: &str, vertical: bool) -> Obj {
|
||||
let full = || Obj::string(vec![0xff, 0xff]);
|
||||
let zero = || Obj::string(vec![0, 0]);
|
||||
let mut d = Dict::new();
|
||||
d.insert(Key::name("CMapName"), Obj::name(name));
|
||||
d.insert(Key::name("CMapType"), Obj::Int(1));
|
||||
d.insert(Key::name("WMode"), Obj::Int(i64::from(vertical)));
|
||||
d.insert(Key::name(CODESPACE), Obj::array(vec![zero(), full()]));
|
||||
d.insert(Key::name(CIDS), Obj::array(vec![zero(), full(), Obj::Int(0), Obj::Int(0)]));
|
||||
Obj::dict(d)
|
||||
}
|
||||
|
||||
impl Interp<'_> {
|
||||
/// `composefont` and the operators of the `CIDInit` and `FontSetInit` procedure sets.
|
||||
pub fn cid_op(&mut self, op: Op) -> Res {
|
||||
use Op::*;
|
||||
match op {
|
||||
// A CMap's mappings take font 0 until its own `usefont` (Adobe Technical Note #5014).
|
||||
BeginCMap => self.cmap_font = 0,
|
||||
EndCMap | EndRearrangedFont => {}
|
||||
BeginCodeSpaceRange | BeginBfChar | BeginBfRange | BeginCidChar | BeginCidRange | BeginNotDefChar | BeginNotDefRange => {
|
||||
self.pop_int()?;
|
||||
self.push(Obj::Mark)?;
|
||||
}
|
||||
EndCodeSpaceRange => {
|
||||
let items = self.pop_mark_items()?;
|
||||
if items.len() % 2 != 0 || !items.iter().all(|o| matches!(o, Obj::Str(_))) {
|
||||
return ps_err("typecheck", "endcodespacerange");
|
||||
}
|
||||
self.cmap_append(CODESPACE, items)?;
|
||||
}
|
||||
EndCidChar | EndCidRange | EndNotDefChar | EndNotDefRange => {
|
||||
let items = self.pop_mark_items()?;
|
||||
let per = if matches!(op, EndCidChar | EndNotDefChar) { 2 } else { 3 };
|
||||
if items.len() % per != 0 {
|
||||
return ps_err("rangecheck", op.name());
|
||||
}
|
||||
let mut out = Vec::with_capacity(items.len() / per * 4);
|
||||
for m in items.chunks_exact(per) {
|
||||
let (lo, hi, cid) = match m {
|
||||
[lo, cid] => (lo, lo, cid),
|
||||
[lo, hi, cid] => (lo, hi, cid),
|
||||
_ => return ps_err("rangecheck", op.name()),
|
||||
};
|
||||
if !(matches!(lo, Obj::Str(_)) && matches!(hi, Obj::Str(_)) && cid.as_num().is_some()) {
|
||||
return ps_err("typecheck", op.name());
|
||||
}
|
||||
out.extend([lo.clone(), hi.clone(), cid.clone(), Obj::Int(self.cmap_font)]);
|
||||
}
|
||||
self.cmap_append(if matches!(op, EndCidChar | EndCidRange) { CIDS } else { NOTDEFS }, out)?;
|
||||
}
|
||||
// Mappings to character codes or names in base fonts aren't read: their codes take
|
||||
// font 0's CID 0, as an unmapped code does.
|
||||
EndBfChar | EndBfRange => {
|
||||
self.pop_mark_items()?;
|
||||
}
|
||||
UseFont => {
|
||||
let n = self.pop_int()?;
|
||||
self.cmap_font = n.clamp(0, 255);
|
||||
}
|
||||
UseCMap => {
|
||||
let name = self.pop()?;
|
||||
let parent = self.find_cmap(&name)?;
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", "usecmap".into()))?;
|
||||
for k in [CODESPACE, CIDS, NOTDEFS] {
|
||||
// Both arrays are copied into a new one: charged by its size first, as
|
||||
// `cmap_append` charges its copy, so `usecmap` of a large CMap in a loop runs
|
||||
// out of the budgets instead of the memory.
|
||||
let len = |c: &DictRef| c.borrow().get(&Key::name(k)).and_then(|a| a.items().map(|i| i.len())).unwrap_or(0);
|
||||
let n = len(&parent).saturating_add(len(&d));
|
||||
if n > MAX_RANGES * 4 {
|
||||
return Err(PsError::Limit("a CMap has too many mappings"));
|
||||
}
|
||||
self.alloc(n.saturating_mul(std::mem::size_of::<Obj>()))?;
|
||||
self.spend_bytes(n.saturating_mul(std::mem::size_of::<Obj>()))?;
|
||||
let theirs = parent.borrow().get(&Key::name(k)).and_then(|a| a.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||
let ours = d.borrow().get(&Key::name(k)).and_then(|a| a.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||
// The parent's mappings first: the CMap's own come later and win.
|
||||
let all: Vec<Obj> = theirs.into_iter().chain(ours).collect();
|
||||
if all.len() > MAX_RANGES * 4 {
|
||||
return Err(PsError::Limit("a CMap has too many mappings"));
|
||||
}
|
||||
self.insert(&d, Key::name(k), Some(Obj::array(all)))?;
|
||||
}
|
||||
let w = parent.borrow().get(&Key::name("WMode")).cloned();
|
||||
if let Some(w) = w
|
||||
&& !d.borrow().contains_key(&Key::name("WMode"))
|
||||
{
|
||||
self.insert(&d, Key::name("WMode"), Some(w))?;
|
||||
}
|
||||
}
|
||||
BeginRearrangedFont => {
|
||||
self.pop()?;
|
||||
self.pop()?;
|
||||
}
|
||||
BeginUseMatrix => {
|
||||
self.pop_int()?;
|
||||
}
|
||||
EndUseMatrix => {
|
||||
self.pop()?;
|
||||
}
|
||||
StartData => self.start_data()?,
|
||||
ComposeFont => self.compose_font()?,
|
||||
_ => return ps_err("undefined", op.name()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The operands down to the mark that a `begin…` operator pushed.
|
||||
fn pop_mark_items(&mut self) -> Res<Vec<Obj>> {
|
||||
let at = self.stack.iter().rposition(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?;
|
||||
self.spend((self.stack.len() - at) as u64)?;
|
||||
let items = self.stack.split_off(at + 1);
|
||||
self.stack.pop();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// `items` added to the CMap being defined (the current dictionary) under `key`.
|
||||
fn cmap_append(&mut self, key: &str, items: Vec<Obj>) -> Res {
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", key.into()))?;
|
||||
let mut all = d.borrow().get(&Key::name(key)).and_then(|a| a.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||
if all.len().saturating_add(items.len()) > MAX_RANGES * 4 {
|
||||
return Err(PsError::Limit("a CMap has too many mappings"));
|
||||
}
|
||||
self.alloc(items.len() * std::mem::size_of::<Obj>())?;
|
||||
// The mappings so far are copied too: a CMap of many small blocks pays for each copy.
|
||||
self.spend_bytes(all.len().saturating_mul(std::mem::size_of::<Obj>()))?;
|
||||
all.extend(items);
|
||||
self.insert(&d, Key::name(key), Some(Obj::array(all)))
|
||||
}
|
||||
|
||||
/// CMap `name` (a name, or a CMap dictionary itself).
|
||||
pub(super) fn find_cmap(&mut self, name: &Obj) -> Res<DictRef> {
|
||||
if let Obj::Dict(d) = name {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let key = name.key().ok_or(PsError::Ps("typecheck", "CMap".into()))?;
|
||||
match self.instances(&Obj::name("CMap"))?.borrow().get(&key) {
|
||||
Some(Obj::Dict(d)) => Ok(d.clone()),
|
||||
_ => Err(PsError::Ps("undefinedresource", name.text().map(|t| t.to_string()).unwrap_or_default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// CIDFont `name` (a name, or a dictionary itself): one the program defined, in the CIDFont
|
||||
/// category or as a font, else a CIDFont of that name without glyphs (its glyphs show
|
||||
/// nothing).
|
||||
fn find_cid_font(&mut self, name: &Obj) -> Res<DictRef> {
|
||||
if let Obj::Dict(d) = name {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let key = name.key().ok_or(PsError::Ps("typecheck", "composefont".into()))?;
|
||||
if let Some(Obj::Dict(d)) = self.instances(&Obj::name("CIDFont"))?.borrow().get(&key) {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
if let Some(Obj::Dict(d)) = self.fonts.borrow().get(&key) {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let mut d = Dict::new();
|
||||
d.insert(Key::name("CIDFontName"), key.obj());
|
||||
d.insert(Key::name("CIDFontType"), Obj::Int(0));
|
||||
d.insert(Key::name("FontType"), Obj::Int(9));
|
||||
d.insert(Key::name("FontMatrix"), matrix_obj(vectorcraft_geom::Affine::scale(0.001)));
|
||||
Ok(DictRef::new(d))
|
||||
}
|
||||
|
||||
/// `key cmap [fonts] composefont font`: a Type 0 font with `FMapType` 9 over `cmap` and the
|
||||
/// CIDFonts (or fonts) named, defined as a font (PLRM 3rd ed., `composefont` in chapter 8).
|
||||
fn compose_font(&mut self) -> Res {
|
||||
let fonts = self.pop()?;
|
||||
let cmap = self.pop()?;
|
||||
let key = self.pop()?.key().ok_or(PsError::Ps("typecheck", "composefont".into()))?;
|
||||
let names = fonts.items().map(|i| i.to_vec()).ok_or(PsError::Ps("typecheck", "composefont".into()))?;
|
||||
let cmap = self.find_cmap(&cmap)?;
|
||||
let mut deps = Vec::with_capacity(names.len());
|
||||
for n in &names {
|
||||
deps.push(Obj::Dict(self.find_cid_font(n)?));
|
||||
}
|
||||
let mut d = Dict::new();
|
||||
d.insert(Key::name("FontName"), key.obj());
|
||||
d.insert(Key::name("FontType"), Obj::Int(0));
|
||||
d.insert(Key::name("FMapType"), Obj::Int(9));
|
||||
d.insert(Key::name("FontMatrix"), matrix_obj(vectorcraft_geom::Affine::IDENTITY));
|
||||
d.insert(Key::name("Encoding"), Obj::array((0..deps.len() as i64).map(Obj::Int).collect()));
|
||||
d.insert(Key::name("FDepVector"), Obj::array(deps));
|
||||
if let Some(w) = cmap.borrow().get(&Key::name("WMode")).cloned() {
|
||||
d.insert(Key::name("WMode"), w);
|
||||
}
|
||||
d.insert(Key::name("CMap"), Obj::Dict(cmap));
|
||||
let font = DictRef::new(d);
|
||||
self.fonts.borrow_mut().insert(key, Obj::Dict(font.clone()));
|
||||
self.push(Obj::Dict(font))
|
||||
}
|
||||
|
||||
/// `StartData`: `(Binary) n StartData` in a Type 0 CIDFont file reads its binary data section
|
||||
/// (`n` bytes after the space that ends the name, or `n` bytes in hexadecimal for `(Hex)`)
|
||||
/// into the CIDFont's `GlyphData`, defines the CIDFont (the current dictionary) as a resource
|
||||
/// and ends it and the CIDInit dictionary under it (Adobe Technical Note #5014, §4.3). A
|
||||
/// FontSet's `/name n StartData` (Compact Font Format data: PLRM 3rd ed., §5.8.1) is skipped:
|
||||
/// its fonts are set as type by name.
|
||||
fn start_data(&mut self) -> Res {
|
||||
// Glyph data is often larger than the most other operators take (`pop_count`).
|
||||
let n = self.pop_int()?;
|
||||
let kind = self.pop()?;
|
||||
let f = self.running.last().cloned().unwrap_or_else(|| Rc::new(std::cell::RefCell::new(super::data::Stream::Current)));
|
||||
if n < 0 {
|
||||
return ps_err("rangecheck", "StartData");
|
||||
}
|
||||
let n = usize::try_from(n).unwrap_or(usize::MAX);
|
||||
if n > MAX_GLYPH_DATA {
|
||||
return Err(PsError::Limit("a font's data is too large"));
|
||||
}
|
||||
self.alloc(n)?;
|
||||
self.spend((n / 256) as u64)?;
|
||||
let hex = match &kind {
|
||||
Obj::Str(s) => s.to_vec() == b"Hex",
|
||||
_ => false,
|
||||
};
|
||||
let data = if hex {
|
||||
let d = self.read_hex(&f, n)?;
|
||||
// The data can end with the `>` of a hexadecimal string.
|
||||
self.skip_hex_end(&f)?;
|
||||
d
|
||||
} else {
|
||||
self.read(&f, n)?
|
||||
};
|
||||
if !matches!(kind, Obj::Str(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", "StartData".into()))?;
|
||||
self.insert(&d, Key::name("GlyphData"), Some(Obj::string(data)))?;
|
||||
// A Type 0 CIDFont without a matrix of its own: the 1000-unit glyph space of Type 1
|
||||
// fonts, and each FDArray matrix scaled to it (PLRM 3rd ed., §5.11.3).
|
||||
// The changes go through the journal, so `restore` undoes them as it does the program's.
|
||||
if !d.borrow().contains_key(&Key::name("FontMatrix")) {
|
||||
self.insert(&d, Key::name("FontMatrix"), Some(matrix_obj(vectorcraft_geom::Affine::scale(0.001))))?;
|
||||
let fds = d.borrow().get(&Key::name("FDArray")).and_then(|a| a.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||
for fd in fds {
|
||||
if let Obj::Dict(fd) = fd {
|
||||
let m = fd.borrow().get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| super::interp::matrix_of(&i.borrow())));
|
||||
if let Some(m) = m {
|
||||
self.insert(&fd, Key::name("FontMatrix"), Some(matrix_obj(vectorcraft_geom::Affine::scale(1000.0) * m)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let name = d.borrow().get(&Key::name("CIDFontName")).and_then(Obj::key);
|
||||
if let Some(k) = name {
|
||||
self.instances(&Obj::name("CIDFont"))?.borrow_mut().insert(k, Obj::Dict(d.clone()));
|
||||
}
|
||||
if self.dicts.len() > 3 && self.dicts.last().is_some_and(|t| t.same(&d)) {
|
||||
self.dicts.pop();
|
||||
let cid_init = self.instances(&Obj::name("ProcSet"))?.borrow().get(&Key::name("CIDInit")).cloned();
|
||||
if let (Some(Obj::Dict(p)), Some(top)) = (cid_init, self.dicts.last())
|
||||
&& top.same(&p)
|
||||
&& self.dicts.len() > 3
|
||||
{
|
||||
self.dicts.pop();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The mappings of CMap dictionary `d` (read once while it stays the same).
|
||||
pub(super) fn cmap(&mut self, d: &DictRef) -> Res<Rc<CMap>> {
|
||||
let array = |k: &str| d.borrow().get(&Key::name(k)).and_then(|a| a.items().cloned());
|
||||
let key = [array(CIDS), array(CODESPACE), array(NOTDEFS)];
|
||||
let same = |a: &Option<Shared<Obj>>, b: &Option<Shared<Obj>>| match (a, b) {
|
||||
(Some(a), Some(b)) => a.same(b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if let Some((_, m)) = self.cmaps.iter().find(|(k, _)| k.iter().zip(&key).all(|(a, b)| same(a, b))) {
|
||||
return Ok(m.clone());
|
||||
}
|
||||
let m = Rc::new(CMap::read(&d.borrow()));
|
||||
self.spend((m.cids.len() + m.space.len() + m.notdefs.len()) as u64)?;
|
||||
if self.cmaps.len() >= MAX_CMAPS {
|
||||
self.cmaps.remove(0);
|
||||
}
|
||||
self.cmaps.push((key, m.clone()));
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// Glyph `cid` of CIDFont `font`: its outline in the CIDFont's glyph space and its width
|
||||
/// there, or `None` when the font has no glyph for it (or isn't a CIDFont with glyphs).
|
||||
pub(super) fn cid_glyph(&mut self, font: &DictRef, cid: u32) -> Res<Option<(BezPath, f64)>> {
|
||||
let (kind, count) = {
|
||||
let f = font.borrow();
|
||||
let num = |k: &str| f.get(&Key::name(k)).and_then(Obj::as_num);
|
||||
(num("CIDFontType"), num("CIDCount"))
|
||||
};
|
||||
if count.is_some_and(|c| f64::from(cid) >= c) {
|
||||
return Ok(None);
|
||||
}
|
||||
let glyph = match kind {
|
||||
Some(2.0) => self.cid_type2(font, cid)?,
|
||||
Some(0.0) => self.cid_type0(font, cid)?,
|
||||
_ => None,
|
||||
};
|
||||
// Widths from the font's `Metrics` override the glyph's own (PLRM 3rd ed., §5.9.2).
|
||||
Ok(glyph.map(|(outline, w)| {
|
||||
let m = font.borrow().get(&Key::name("Metrics")).cloned();
|
||||
let w = match m {
|
||||
Some(Obj::Dict(m)) => match m.borrow().get(&Key::Int(i64::from(cid))) {
|
||||
Some(o) => match o.as_num() {
|
||||
Some(v) => v,
|
||||
None => o
|
||||
.items()
|
||||
.and_then(|i| match i.len() {
|
||||
2 => i.get(1).and_then(|v| v.as_num()),
|
||||
4 => i.get(2).and_then(|v| v.as_num()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(w),
|
||||
},
|
||||
None => w,
|
||||
},
|
||||
_ => w,
|
||||
};
|
||||
(outline, if w.is_finite() { w } else { 0.0 })
|
||||
}))
|
||||
}
|
||||
|
||||
/// Glyph `cid` of a Type 2 CIDFont: the TrueType glyph its `CIDMap` gives it (a string, or
|
||||
/// strings, of `GDBytes`-byte glyph indices by CID; a dictionary; or an integer added to the
|
||||
/// CID: PLRM 3rd ed., §5.11.3), its width in ems.
|
||||
fn cid_type2(&mut self, font: &DictRef, cid: u32) -> Res<Option<(BezPath, f64)>> {
|
||||
let Some(data) = self.type42_data(font)? else { return Ok(None) };
|
||||
let gid = {
|
||||
let f = font.borrow();
|
||||
let gd = f.get(&Key::name("GDBytes")).and_then(Obj::as_num).filter(|n| (1.0..=4.0).contains(n)).map_or(2, |n| n as usize);
|
||||
match f.get(&Key::name("CIDMap")) {
|
||||
None => Some(u64::from(cid)),
|
||||
Some(Obj::Int(off)) => u64::try_from(i64::from(cid).saturating_add(*off)).ok(),
|
||||
Some(Obj::Dict(d)) => d.borrow().get(&Key::Int(i64::from(cid))).and_then(Obj::as_num).filter(|g| *g >= 0.0).map(|g| g as u64),
|
||||
Some(Obj::Str(s)) => (cid as usize).checked_mul(gd).and_then(|at| number(&s.borrow(), at, gd)),
|
||||
Some(Obj::Array { items, .. }) => {
|
||||
// The strings one after another, each a whole number of entries.
|
||||
let Some(mut at) = (cid as usize).checked_mul(gd) else { return Ok(None) };
|
||||
let mut found = None;
|
||||
for s in items.borrow().iter() {
|
||||
let Obj::Str(s) = s else { continue };
|
||||
let s = s.borrow();
|
||||
let len = s.len() - s.len() % gd;
|
||||
if at < len {
|
||||
found = number(&s, at, gd);
|
||||
break;
|
||||
}
|
||||
at -= len;
|
||||
}
|
||||
found
|
||||
}
|
||||
Some(_) => None,
|
||||
}
|
||||
};
|
||||
let Some(gid) = gid.and_then(|g| u32::try_from(g).ok()).filter(|g| *g <= u32::from(u16::MAX)) else { return Ok(None) };
|
||||
if gid == 0 && cid != 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(super::type42::glyph(&data.data, gid))
|
||||
}
|
||||
|
||||
/// Glyph `cid` of a Type 0 CIDFont: the Type 1 charstring its `GlyphDirectory` entry or
|
||||
/// its binary data section's offset table gives it, run with the subroutines of its
|
||||
/// FDArray dictionary's `Private` dictionary, in the FDArray dictionary's glyph space under
|
||||
/// the CIDFont's; its width in the CIDFont's glyph space.
|
||||
fn cid_type0(&mut self, font: &DictRef, cid: u32) -> Res<Option<(BezPath, f64)>> {
|
||||
let f = font.borrow();
|
||||
let num = |k: &str, default: f64| f.get(&Key::name(k)).and_then(Obj::as_num).unwrap_or(default);
|
||||
let fd_bytes = num("FDBytes", 0.0);
|
||||
let gd_bytes = num("GDBytes", 0.0);
|
||||
if !(0.0..=4.0).contains(&fd_bytes) || !(0.0..=4.0).contains(&gd_bytes) {
|
||||
return Ok(None);
|
||||
}
|
||||
let (fd_bytes, gd_bytes) = (fd_bytes as usize, gd_bytes as usize);
|
||||
let data: Option<Shared<u8>> = match f.get(&Key::name("GlyphData")) {
|
||||
Some(Obj::Str(s)) => Some(s.clone()),
|
||||
_ => None,
|
||||
};
|
||||
// The FDArray index and the charstring.
|
||||
let found: Option<(usize, Vec<u8>)> = match f.get(&Key::name("GlyphDirectory")) {
|
||||
Some(dir @ (Obj::Array { .. } | Obj::Dict(_))) => {
|
||||
let entry = match dir {
|
||||
Obj::Dict(d) => d.borrow().get(&Key::Int(i64::from(cid))).cloned(),
|
||||
_ => dir.items().and_then(|i| i.get(cid as usize)),
|
||||
};
|
||||
match entry {
|
||||
Some(Obj::Str(s)) => {
|
||||
let s = s.to_vec();
|
||||
let fd = number(&s, 0, fd_bytes).unwrap_or(0) as usize;
|
||||
Some((fd, s.get(fd_bytes..).unwrap_or_default().to_vec()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => data.as_ref().and_then(|data| {
|
||||
let data = data.borrow();
|
||||
let map = num("CIDMapOffset", 0.0);
|
||||
if !(0.0..=data.len() as f64).contains(&map) {
|
||||
return None;
|
||||
}
|
||||
let at = (map as usize).checked_add((cid as usize).checked_mul(fd_bytes + gd_bytes)?)?;
|
||||
let fd = if fd_bytes == 0 { 0 } else { number(&data, at, fd_bytes)? as usize };
|
||||
let start = number(&data, at + fd_bytes, gd_bytes)? as usize;
|
||||
let end = number(&data, at + 2 * fd_bytes + gd_bytes, gd_bytes)? as usize;
|
||||
Some((fd, data.get(start..end)?.to_vec()))
|
||||
}),
|
||||
};
|
||||
let Some((fd, charstring)) = found.filter(|(_, c)| !c.is_empty()) else { return Ok(None) };
|
||||
let Some(Obj::Dict(fd)) = f.get(&Key::name("FDArray")).and_then(|a| a.items().and_then(|i| i.get(fd))) else { return Ok(None) };
|
||||
drop(f);
|
||||
let fd = fd.borrow();
|
||||
let fdm = fd
|
||||
.get(&Key::name("FontMatrix"))
|
||||
.and_then(|o| o.items().and_then(|i| super::interp::matrix_of(&i.borrow())))
|
||||
.unwrap_or(vectorcraft_geom::Affine::IDENTITY);
|
||||
let Some(Obj::Dict(private)) = fd.get(&Key::name("Private")).cloned() else { return Ok(None) };
|
||||
drop(fd);
|
||||
let len_iv = private.borrow().get(&Key::name("lenIV")).and_then(Obj::as_num).unwrap_or(4.0);
|
||||
let subrs = self.cid_subrs(&private, data.as_ref(), len_iv)?;
|
||||
self.spend((charstring.len() as u64).saturating_add(64))?;
|
||||
let charstring = decrypt(&charstring, len_iv);
|
||||
let Some((outline, width)) = type1_glyph(&charstring, &subrs) else { return Ok(None) };
|
||||
let mut outline = outline;
|
||||
outline.apply_affine(fdm);
|
||||
let w = fdm * vectorcraft_geom::Point::new(width, 0.0) - fdm * vectorcraft_geom::Point::ZERO;
|
||||
Ok(Some((outline, w.x)))
|
||||
}
|
||||
|
||||
/// The subroutines of a Type 0 CIDFont's FDArray `Private` dictionary `private`, decrypted
|
||||
/// with `len_iv`: its `Subrs` array, else the subroutine table of the CIDFont's binary data
|
||||
/// `data` (Adobe Technical Note #5014). Read once while they stay the same, and counted
|
||||
/// against the budgets before they are copied.
|
||||
fn cid_subrs(&mut self, private: &DictRef, data: Option<&Shared<u8>>, len_iv: f64) -> Res<Rc<Vec<Vec<u8>>>> {
|
||||
let same_data = |d: &Option<Shared<u8>>| match (d, data) {
|
||||
(Some(a), Some(b)) => a.same(b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if let Some(i) = self.cid_subrs.iter().position(|(p, d, _)| p.same(private) && same_data(d)) {
|
||||
let hit = self.cid_subrs.remove(i);
|
||||
let subrs = hit.2.clone();
|
||||
self.cid_subrs.push(hit);
|
||||
return Ok(subrs);
|
||||
}
|
||||
let p = private.borrow();
|
||||
// Each subroutine's bytes: a string of `Subrs`, or a range of the binary data.
|
||||
let strings: Option<Vec<Option<Shared<u8>>>> = match p.get(&Key::name("Subrs")) {
|
||||
Some(Obj::Array { items, .. }) => Some(
|
||||
items
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Obj::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
let mut ranges: Vec<Option<(usize, usize)>> = Vec::new();
|
||||
if strings.is_none() {
|
||||
let num = |k: &str| p.get(&Key::name(k)).and_then(Obj::as_num).filter(|n| *n >= 0.0);
|
||||
if let (Some(map), Some(sd), Some(count), Some(data)) = (num("SubrMapOffset"), num("SDBytes"), num("SubrCount"), data) {
|
||||
let data = data.borrow();
|
||||
let (map, sd, count) = (map as usize, (sd as usize).min(4), (count as usize).min(MAX_RANGES));
|
||||
for i in 0..count {
|
||||
let at = map.saturating_add(i.saturating_mul(sd));
|
||||
let range = number(&data, at, sd).zip(number(&data, at.saturating_add(sd), sd));
|
||||
ranges.push(range.map(|(a, b)| (a as usize, b as usize)).filter(|(a, b)| a <= b && *b <= data.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(p);
|
||||
let total = match &strings {
|
||||
Some(strings) => strings.iter().flatten().fold(0usize, |t, s| t.saturating_add(s.len())),
|
||||
None => ranges.iter().flatten().fold(0usize, |t, (a, b)| t.saturating_add(b - a)),
|
||||
};
|
||||
if total > MAX_GLYPH_DATA {
|
||||
return Err(PsError::Limit("a font's subroutines are too large"));
|
||||
}
|
||||
self.alloc(total)?;
|
||||
self.spend_bytes(total)?;
|
||||
self.spend((strings.as_ref().map_or(ranges.len(), Vec::len) / 16) as u64)?;
|
||||
let subrs: Vec<Vec<u8>> = match (&strings, data) {
|
||||
(Some(strings), _) => strings.iter().map(|s| s.as_ref().map(|s| decrypt(&s.borrow(), len_iv)).unwrap_or_default()).collect(),
|
||||
(None, Some(data)) => {
|
||||
let data = data.borrow();
|
||||
ranges.iter().map(|r| r.and_then(|(a, b)| data.get(a..b)).map(|s| decrypt(s, len_iv)).unwrap_or_default()).collect()
|
||||
}
|
||||
(None, None) => vec![],
|
||||
};
|
||||
let subrs = Rc::new(subrs);
|
||||
if self.cid_subrs.len() >= MAX_SUBR_SETS {
|
||||
self.cid_subrs.remove(0);
|
||||
}
|
||||
self.cid_subrs.push((private.clone(), data.cloned(), subrs.clone()));
|
||||
Ok(subrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Most FDArray dictionaries whose subroutines are kept decrypted between glyphs.
|
||||
const MAX_SUBR_SETS: usize = 32;
|
||||
|
||||
/// The decrypted subroutines of FDArray `Private` dictionaries, by the dictionary and the
|
||||
/// CIDFont's binary data (see [`Interp::cid_subrs`]), the most recently used last.
|
||||
pub(super) type CidSubrs = Vec<(DictRef, Option<Shared<u8>>, Rc<Vec<Vec<u8>>>)>;
|
||||
|
||||
/// Type 1 charstring `data` decrypted (the charstring key 4330, the first `len_iv` bytes
|
||||
/// dropped: Adobe Type 1 Font Format, §7.2); as it is when `len_iv` is negative.
|
||||
pub(super) fn decrypt(data: &[u8], len_iv: f64) -> Vec<u8> {
|
||||
if len_iv < 0.0 {
|
||||
return data.to_vec();
|
||||
}
|
||||
let mut r: u16 = 4330;
|
||||
let plain = data.iter().map(|&c| {
|
||||
let p = c ^ (r >> 8) as u8;
|
||||
r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719);
|
||||
p
|
||||
});
|
||||
plain.skip(len_iv.min(64.0) as usize).collect()
|
||||
}
|
||||
|
||||
/// The outline (glyph space) and width of decrypted Type 1 charstring `data`, with
|
||||
/// subroutines `subrs`, or `None` when it can't be run.
|
||||
pub(super) fn type1_glyph(data: &[u8], subrs: &[Vec<u8>]) -> Option<(BezPath, f64)> {
|
||||
let ctx = Type1 { subrs };
|
||||
let mut sink = Sink(BezPath::new());
|
||||
let w = cs::evaluate(&ctx, None, data, &mut sink).ok()?;
|
||||
Some((sink.0, w.map_or(0.0, |w| f64::from(w.to_f32()))))
|
||||
}
|
||||
|
||||
/// What a Type 1 charstring runs with: its font's subroutines (no `seac` accents: a CIDFont's
|
||||
/// glyphs have no names to build them from).
|
||||
struct Type1<'a> {
|
||||
subrs: &'a [Vec<u8>],
|
||||
}
|
||||
|
||||
impl CharstringContext for Type1<'_> {
|
||||
fn kind(&self) -> CharstringKind {
|
||||
CharstringKind::Type1
|
||||
}
|
||||
fn seac_components(&self, base: i32, _accent: i32) -> Result<[&[u8]; 2], CsError> {
|
||||
Err(CsError::InvalidSeacCode(base))
|
||||
}
|
||||
fn global_subr(&self, _index: i32) -> Result<&[u8], CsError> {
|
||||
Err(CsError::MissingSubroutines)
|
||||
}
|
||||
fn subr(&self, index: i32) -> Result<&[u8], CsError> {
|
||||
usize::try_from(index).ok().and_then(|i| self.subrs.get(i)).map(Vec::as_slice).ok_or(CsError::MissingSubroutines)
|
||||
}
|
||||
}
|
||||
|
||||
/// A charstring's path, in glyph space.
|
||||
struct Sink(BezPath);
|
||||
|
||||
impl CommandSink for Sink {
|
||||
fn move_to(&mut self, x: Fixed, y: Fixed) {
|
||||
self.0.move_to((f64::from(x.to_f32()), f64::from(y.to_f32())));
|
||||
}
|
||||
fn line_to(&mut self, x: Fixed, y: Fixed) {
|
||||
self.0.line_to((f64::from(x.to_f32()), f64::from(y.to_f32())));
|
||||
}
|
||||
fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
|
||||
let p = |a: Fixed, b: Fixed| (f64::from(a.to_f32()), f64::from(b.to_f32()));
|
||||
self.0.curve_to(p(cx0, cy0), p(cx1, cy1), p(x, y));
|
||||
}
|
||||
fn close(&mut self) {
|
||||
self.0.close_path();
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ impl Interp<'_> {
|
||||
}
|
||||
|
||||
/// Up to `n` bytes of file `f`.
|
||||
fn read(&mut self, f: &Rc<RefCell<Stream>>, n: usize) -> Res<Vec<u8>> {
|
||||
pub(in super::super) fn read(&mut self, f: &Rc<RefCell<Stream>>, n: usize) -> Res<Vec<u8>> {
|
||||
self.materialize(f)?;
|
||||
Ok(match &mut *f.borrow_mut() {
|
||||
Stream::Current => {
|
||||
@@ -236,7 +236,7 @@ impl Interp<'_> {
|
||||
}
|
||||
|
||||
/// Up to `n` bytes read as hexadecimal digits from `f` (other characters skipped).
|
||||
fn read_hex(&mut self, f: &Rc<RefCell<Stream>>, n: usize) -> Res<Vec<u8>> {
|
||||
pub(in super::super) fn read_hex(&mut self, f: &Rc<RefCell<Stream>>, n: usize) -> Res<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(n);
|
||||
let mut high: Option<u8> = None;
|
||||
while out.len() < n {
|
||||
@@ -250,6 +250,31 @@ impl Interp<'_> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Skip the whitespace and the `>` that end hexadecimal data in `f`, if a `>` is next: the
|
||||
/// rest is left unread.
|
||||
pub(in super::super) fn skip_hex_end(&mut self, f: &Rc<RefCell<Stream>>) -> Res {
|
||||
self.materialize(f)?;
|
||||
let blank = |b: &u8| b.is_ascii_whitespace() || *b == 0;
|
||||
match &mut *f.borrow_mut() {
|
||||
Stream::Current => {
|
||||
let rest = self.lex.data();
|
||||
let n = rest.iter().take_while(|b| blank(b)).count();
|
||||
if rest.get(n) == Some(&b'>') {
|
||||
self.lex.advance(n + 1);
|
||||
}
|
||||
}
|
||||
Stream::Buf { data, pos, .. } => {
|
||||
let rest = data.get(*pos..).unwrap_or_default();
|
||||
let n = rest.iter().take_while(|b| blank(b)).count();
|
||||
if rest.get(n) == Some(&b'>') {
|
||||
*pos += n + 1;
|
||||
}
|
||||
}
|
||||
Stream::Pending { .. } => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read bytes `got` into string `s` and push the part of `s` they fill.
|
||||
fn fill(&mut self, s: Shared<u8>, got: &[u8]) -> Res {
|
||||
if !s.write(0, got) {
|
||||
|
||||
@@ -79,6 +79,13 @@ pub(crate) struct Interp<'a> {
|
||||
/// The `sfnts` arrays whose TrueType data has been counted against [`MAX_MEMORY`]: a font
|
||||
/// read again after it left `type42` doesn't count again.
|
||||
pub type42_counted: HashSet<Shared<Obj>>,
|
||||
/// The mappings of the CMaps shown with so far, by their range arrays (see `cid`), the most
|
||||
/// recent last.
|
||||
pub cmaps: super::cid::CMaps,
|
||||
/// The decrypted subroutines of Type 0 CIDFonts (see `cid`).
|
||||
pub cid_subrs: super::cid::CidSubrs,
|
||||
/// The font number `usefont` gave the CMap mappings that follow.
|
||||
pub cmap_font: i64,
|
||||
pub g: GState,
|
||||
pub saved: Vec<GState>,
|
||||
pub out: Out,
|
||||
@@ -101,7 +108,7 @@ pub(crate) struct Fault {
|
||||
}
|
||||
|
||||
/// The TrueType data of Type 42 fonts by their `sfnts` array (see [`Interp::type42`]).
|
||||
pub(crate) type Type42Cache = Vec<(Shared<Obj>, Option<Rc<Vec<u8>>>)>;
|
||||
pub(crate) type Type42Cache = Vec<(Shared<Obj>, Option<Rc<super::type42::Font>>)>;
|
||||
|
||||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
@@ -137,7 +144,7 @@ impl<'a> Interp<'a> {
|
||||
let encodings = new_dict();
|
||||
{
|
||||
let mut d = system.borrow_mut();
|
||||
for (op, name) in Op::ALL {
|
||||
for (op, name) in Op::ALL.iter().filter(|(op, _)| !Op::PROCSET.contains(op)) {
|
||||
d.insert(Key::name(name), Obj::Op(*op));
|
||||
}
|
||||
for name in ["StandardEncoding", "ISOLatin1Encoding"] {
|
||||
@@ -155,6 +162,21 @@ impl<'a> Interp<'a> {
|
||||
let resources = new_dict();
|
||||
resources.borrow_mut().insert(Key::name("Category"), Obj::dict(categories));
|
||||
resources.borrow_mut().insert(Key::name("Encoding"), Obj::Dict(encodings));
|
||||
// The procedure sets that build CMaps and CIDFonts (CIDInit) and font sets (FontSetInit),
|
||||
// and the predefined Identity CMaps.
|
||||
let cid_init: Dict = Op::PROCSET.iter().map(|op| (Key::name(op.name()), Obj::Op(*op))).collect();
|
||||
let font_set_init = Dict::from([(Key::name("StartData"), Obj::Op(Op::StartData))]);
|
||||
resources.borrow_mut().insert(
|
||||
Key::name("ProcSet"),
|
||||
Obj::dict(Dict::from([(Key::name("CIDInit"), Obj::dict(cid_init)), (Key::name("FontSetInit"), Obj::dict(font_set_init))])),
|
||||
);
|
||||
resources.borrow_mut().insert(
|
||||
Key::name("CMap"),
|
||||
Obj::dict(Dict::from([
|
||||
(Key::name("Identity-H"), super::cid::identity("Identity-H", false)),
|
||||
(Key::name("Identity-V"), super::cid::identity("Identity-V", true)),
|
||||
])),
|
||||
);
|
||||
let fonts = new_dict();
|
||||
system.borrow_mut().insert(Key::name("FontDirectory"), Obj::Dict(fonts.clone()));
|
||||
for name in ["GlobalFontDirectory", "SharedFontDirectory"] {
|
||||
@@ -199,6 +221,9 @@ impl<'a> Interp<'a> {
|
||||
fault: None,
|
||||
glyph_width: None,
|
||||
type42: vec![],
|
||||
cmaps: vec![],
|
||||
cid_subrs: vec![],
|
||||
cmap_font: 0,
|
||||
type42_counted: HashSet::new(),
|
||||
g,
|
||||
saved: vec![],
|
||||
@@ -383,6 +408,11 @@ impl<'a> Interp<'a> {
|
||||
self.spend((n / 256) as u64)
|
||||
}
|
||||
|
||||
/// The operations left in the budget (`max_ops`).
|
||||
pub fn ops_left(&self) -> u64 {
|
||||
self.max_ops.saturating_sub(self.ops)
|
||||
}
|
||||
|
||||
/// Count `ops` operations' worth of work against the budget (`max_ops`): what an operator that costs
|
||||
/// far more than one operation (laying out type) does in a loop has to end too.
|
||||
pub fn spend(&mut self, ops: u64) -> Res {
|
||||
@@ -1401,7 +1431,7 @@ impl<'a> Interp<'a> {
|
||||
|
||||
/// Put `v` under `k` in `d` (`None`: remove it), within [`MAX_ALLOC`] entries; while a `save`
|
||||
/// is open the old value is kept for `restore`.
|
||||
fn insert(&mut self, d: &DictRef, k: Key, v: Option<Obj>) -> Res {
|
||||
pub fn insert(&mut self, d: &DictRef, k: Key, v: Option<Obj>) -> Res {
|
||||
let old = {
|
||||
let mut dm = d.borrow_mut();
|
||||
if v.is_some() && dm.len() >= MAX_ALLOC && !dm.contains_key(&k) {
|
||||
|
||||
@@ -21,6 +21,11 @@ impl DictRef {
|
||||
note(Made::Dict(Rc::downgrade(&d)));
|
||||
Self(d)
|
||||
}
|
||||
|
||||
/// Is `o` this very dictionary?
|
||||
pub fn same(&self, o: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.0, &o.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// An array or a dictionary a program made.
|
||||
@@ -443,6 +448,16 @@ macro_rules! ops {
|
||||
/// Every operator with its name (`systemdict`).
|
||||
pub const ALL: &'static [(Op, &'static str)] = &[$((Op::$v, $n)),*];
|
||||
|
||||
/// The operators of the CIDInit procedure set (PLRM 3rd ed., §5.11.4), found through
|
||||
/// it rather than systemdict.
|
||||
pub const PROCSET: &'static [Op] = &[
|
||||
Op::BeginCMap, Op::EndCMap, Op::UseCMap, Op::BeginCodeSpaceRange, Op::EndCodeSpaceRange, Op::UseFont,
|
||||
Op::BeginBfChar, Op::EndBfChar, Op::BeginBfRange, Op::EndBfRange, Op::BeginCidChar, Op::EndCidChar,
|
||||
Op::BeginCidRange, Op::EndCidRange, Op::BeginNotDefChar, Op::EndNotDefChar, Op::BeginNotDefRange,
|
||||
Op::EndNotDefRange, Op::BeginRearrangedFont, Op::EndRearrangedFont, Op::BeginUseMatrix, Op::EndUseMatrix,
|
||||
Op::StartData,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self { $(Op::$v => $n),* }
|
||||
}
|
||||
@@ -542,6 +557,15 @@ ops! {
|
||||
SetVmThreshold = "setvmthreshold", VmReclaim = "vmreclaim", StartJob = "startjob", CurrentObjectFormat = "currentobjectformat",
|
||||
Revision = "revision", SerialNumber = "serialnumber", Read = "read", FilePosition = "fileposition",
|
||||
SetFilePosition = "setfileposition", DeleteFile = "deletefile", RenameFile = "renamefile", Run = "run",
|
||||
// CID-keyed fonts: `composefont`, and the operators of the CIDInit procedure set (CMaps and
|
||||
// `StartData`, which FontSetInit has too), which aren't in systemdict (see `Op::PROCSET`).
|
||||
ComposeFont = "composefont", BeginCMap = "begincmap", EndCMap = "endcmap", UseCMap = "usecmap",
|
||||
BeginCodeSpaceRange = "begincodespacerange", EndCodeSpaceRange = "endcodespacerange", UseFont = "usefont",
|
||||
BeginBfChar = "beginbfchar", EndBfChar = "endbfchar", BeginBfRange = "beginbfrange", EndBfRange = "endbfrange",
|
||||
BeginCidChar = "begincidchar", EndCidChar = "endcidchar", BeginCidRange = "begincidrange", EndCidRange = "endcidrange",
|
||||
BeginNotDefChar = "beginnotdefchar", EndNotDefChar = "endnotdefchar", BeginNotDefRange = "beginnotdefrange",
|
||||
EndNotDefRange = "endnotdefrange", BeginRearrangedFont = "beginrearrangedfont", EndRearrangedFont = "endrearrangedfont",
|
||||
BeginUseMatrix = "beginusematrix", EndUseMatrix = "endusematrix", StartData = "StartData",
|
||||
// User paths.
|
||||
UFill = "ufill", UEoFill = "ueofill", UStroke = "ustroke", UAppend = "uappend", UPath = "upath", SetBBox = "setbbox",
|
||||
}
|
||||
|
||||
@@ -983,8 +983,8 @@ fn intervals_share_their_arrays_and_strings() {
|
||||
fn resourceforall_enumerates_matching_names() {
|
||||
// #234.
|
||||
blue("(*) { pop } 128 string /Category resourceforall 0 0 1 setrgbcolor");
|
||||
let count = |template: &str| format!("/n 0 def ({template}) {{ pop /n n 1 add def }} 128 string /ProcSet resourceforall n");
|
||||
let define = "[/Foo /Fop /Bar /F*x] { 1 dict /ProcSet defineresource pop } forall";
|
||||
let count = |template: &str| format!("/n 0 def ({template}) {{ pop /n n 1 add def }} 128 string /Form resourceforall n");
|
||||
let define = "[/Foo /Fop /Bar /F*x] { 1 dict /Form defineresource pop } forall";
|
||||
check(&format!("{define} {} 4 eq", count("*")));
|
||||
check(&format!("{define} {} 2 eq", count("Fo?")));
|
||||
check(&format!("{define} {} 3 eq", count("F*")));
|
||||
@@ -992,10 +992,10 @@ fn resourceforall_enumerates_matching_names() {
|
||||
check(&format!("{define} {} 0 eq", count("Q*")));
|
||||
check(&format!("{define} {} 1 eq", count("*a*")));
|
||||
// Each name is copied into the scratch string; exit stops.
|
||||
check(&format!("{define} /last () def (B*) {{ dup length string copy /last exch def }} 8 string /ProcSet resourceforall last (Bar) eq"));
|
||||
check(&format!("{define} /n 0 def (*) {{ pop /n n 1 add def exit }} 8 string /ProcSet resourceforall n 1 eq"));
|
||||
check(&format!("{define} /last () def (B*) {{ dup length string copy /last exch def }} 8 string /Form resourceforall last (Bar) eq"));
|
||||
check(&format!("{define} /n 0 def (*) {{ pop /n n 1 add def exit }} 8 string /Form resourceforall n 1 eq"));
|
||||
// A scratch string too small for the names, as the prolog probes it.
|
||||
check(&format!("{define} {{ (*) {{ pop }} 2 string /ProcSet resourceforall }} stopped"));
|
||||
check(&format!("{define} {{ (*) {{ pop }} 2 string /Form resourceforall }} stopped"));
|
||||
check("/n 0 def (Gen*) { pop /n n 1 add def } 16 string /Category resourceforall n 1 eq");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
//! CID-keyed fonts (PLRM 3rd ed., §5.11): CMaps built with the CIDInit procedure set's
|
||||
//! operators, `composefont`, `FMapType` 9 fonts mapping codes to CIDs, Type 0 CIDFonts (Type 1
|
||||
//! charstrings in the binary data `StartData` reads: Adobe Technical Note #5014) and Type 2
|
||||
//! CIDFonts (TrueType glyphs through `CIDMap`). The CIDFonts are written from scratch: Type 0
|
||||
//! glyphs are hand-made charstrings (Adobe Type 1 Font Format, chapter 6), Type 2 glyphs come
|
||||
//! from the bundled Source Sans 3.
|
||||
|
||||
use vectorcraft_doc::NodeKind;
|
||||
|
||||
use super::tests::{all, bounds, check};
|
||||
use super::tests_composite::{glyph, sfnts};
|
||||
use crate::import::import;
|
||||
|
||||
/// A Type 1 charstring number (Adobe Type 1 Font Format, §6.2).
|
||||
fn num(v: i32) -> Vec<u8> {
|
||||
match v {
|
||||
-107..=107 => vec![(v + 139) as u8],
|
||||
108..=1131 => vec![((v - 108) / 256 + 247) as u8, ((v - 108) % 256) as u8],
|
||||
-1131..=-108 => vec![((-v - 108) / 256 + 251) as u8, ((-v - 108) % 256) as u8],
|
||||
_ => [vec![255], v.to_be_bytes().to_vec()].concat(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A charstring from numbers and commands (`hsbw` 13, `rmoveto` 21, `rlineto` 5, `closepath`
|
||||
/// 9, `callsubr` 10, `return` 11, `endchar` 14).
|
||||
fn cs(parts: &[(&[i32], u8)]) -> Vec<u8> {
|
||||
parts.iter().flat_map(|(args, op)| args.iter().flat_map(|a| num(*a)).chain([*op])).collect()
|
||||
}
|
||||
|
||||
/// `plain` encrypted with the charstring key and 4 leading bytes (Adobe Type 1 Font Format, §7).
|
||||
fn encrypt(plain: &[u8]) -> Vec<u8> {
|
||||
let mut r: u16 = 4330;
|
||||
[0u8; 4]
|
||||
.iter()
|
||||
.chain(plain)
|
||||
.map(|&p| {
|
||||
let c = p ^ (r >> 8) as u8;
|
||||
r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719);
|
||||
c
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
/// A Type 0 CIDFont `/TestCID` with three CIDs: 0 an empty glyph 500 wide; 1 a square (100 to
|
||||
/// 500 across, 500 high) 600 wide, its charstring plain (FDArray dictionary 0, `lenIV` -1); 2 a
|
||||
/// triangle (500 across, 400 high) 500 wide drawn by a subroutine, encrypted (dictionary 1,
|
||||
/// `lenIV` 4). The binary data section, written in hexadecimal when `hex_data`.
|
||||
fn cid_font(hex_data: bool) -> Vec<u8> {
|
||||
let empty = cs(&[(&[0, 500], 13), (&[], 14)]);
|
||||
let square = cs(&[(&[0, 600], 13), (&[100, 0], 21), (&[400, 0], 5), (&[0, 500], 5), (&[-400, 0], 5), (&[], 9), (&[], 14)]);
|
||||
let triangle = encrypt(&cs(&[(&[0, 500], 13), (&[0], 10), (&[], 14)]));
|
||||
let subr = encrypt(&cs(&[(&[0, 0], 21), (&[500, 0], 5), (&[-250, 400], 5), (&[], 9), (&[], 11)]));
|
||||
// The offset table: FDArray index (1 byte) and offset (2 bytes) of CIDs 0 to 2 and the end.
|
||||
let table = 4 * 3;
|
||||
let offsets = [table, table + empty.len(), table + empty.len() + square.len(), table + empty.len() + square.len() + triangle.len()];
|
||||
let mut data = vec![];
|
||||
for (fd, at) in [0u8, 0, 1, 0].iter().zip(offsets) {
|
||||
data.push(*fd);
|
||||
data.extend_from_slice(&(at as u16).to_be_bytes());
|
||||
}
|
||||
data.extend(empty);
|
||||
data.extend(square);
|
||||
data.extend(triangle);
|
||||
let mut out = format!(
|
||||
"/CIDInit /ProcSet findresource begin\n20 dict begin\n/CIDFontName /TestCID def /CIDFontType 0 def\n\
|
||||
/CIDSystemInfo 3 dict dup begin /Registry (Test) def /Ordering (Shapes) def /Supplement 0 def end def\n\
|
||||
/FontBBox [0 0 1000 1000] def\n/FDArray [\n\
|
||||
4 dict begin /FontMatrix [0.001 0 0 0.001 0 0] def /Private 2 dict dup begin /lenIV -1 def end def currentdict end\n\
|
||||
4 dict begin /FontMatrix [0.001 0 0 0.001 0 0] def /Private 2 dict dup begin /lenIV 4 def /Subrs [<{}>] def end def currentdict end\n\
|
||||
] def\n/CIDMapOffset 0 def /FDBytes 1 def /GDBytes 2 def /CIDCount 3 def\n",
|
||||
hex(&subr)
|
||||
)
|
||||
.into_bytes();
|
||||
if hex_data {
|
||||
out.extend(format!("(Hex) {} StartData\n{}>\n", data.len(), hex(&data)).into_bytes());
|
||||
} else {
|
||||
out.extend(format!("(Binary) {} StartData ", data.len()).into_bytes());
|
||||
out.extend(data);
|
||||
out.push(b'\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// CMap `/Test-H`: one-byte codes 00–7F and two-byte codes 8000–FFFF; A and B are CIDs 1 and 2,
|
||||
/// <8001> CID 2, and codes 00–1F without a character take CID 1 as their notdef.
|
||||
const CMAP: &str = "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
|
||||
/CMapName /Test-H def /CMapType 1 def /WMode 0 def\n\
|
||||
2 begincodespacerange <00> <7f> <8000> <ffff> endcodespacerange\n\
|
||||
1 begincidrange <41> <42> 1 endcidrange\n1 begincidchar <8001> 2 endcidchar\n\
|
||||
1 beginnotdefrange <00> <1f> 1 endnotdefrange\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/TestFont /Test-H [/TestCID] composefont pop\n";
|
||||
|
||||
/// `body` after the CIDFont and the CMap, as an EPS file.
|
||||
fn program(hex_data: bool, body: &str) -> Vec<u8> {
|
||||
let head = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n".to_vec();
|
||||
[head, cid_font(hex_data), CMAP.as_bytes().to_vec(), format!("{body}\nshowpage\n%%EOF\n").into_bytes()].concat()
|
||||
}
|
||||
|
||||
fn paths(r: &crate::Imported) -> Vec<vectorcraft_geom::Rect> {
|
||||
all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).map(|n| bounds(n)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type0_cidfonts_draw_their_charstrings_through_the_cmap() {
|
||||
for hex_data in [false, true] {
|
||||
let r = import(&program(hex_data, "/TestFont 50 selectfont 10 20 moveto (AB) show")).unwrap();
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
assert!(!all(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Text(_))));
|
||||
let p = paths(&r);
|
||||
assert_eq!(p.len(), 2, "{p:?}");
|
||||
// The square, 100 to 500 units across at 50 pt an em; the triangle 600 units on.
|
||||
let (sq, tri) = (p[0], p[1]);
|
||||
assert!((sq.x0 - 15.0).abs() < 0.01 && (sq.width() - 20.0).abs() < 0.01 && (sq.height() - 25.0).abs() < 0.01, "{sq:?}");
|
||||
assert!((sq.y1 - 80.0).abs() < 0.01, "{sq:?}");
|
||||
assert!((tri.x0 - 40.0).abs() < 0.01 && (tri.width() - 25.0).abs() < 0.01 && (tri.height() - 20.0).abs() < 0.01, "{tri:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Codes take as many bytes as their codespace range says; codes without a character take their
|
||||
/// notdef mapping's CID, else CID 0 (PLRM 3rd ed., §5.11.5).
|
||||
#[test]
|
||||
fn codes_map_by_their_codespace_with_notdef_fallbacks() {
|
||||
let fonts = String::from_utf8(cid_font(true)).unwrap() + CMAP;
|
||||
let width = |s: &str, w: f64| check(&format!("{fonts} /TestFont 50 selectfont {s} stringwidth pop {w} sub abs 0.01 lt"));
|
||||
width("(AB)", 55.0);
|
||||
// A two-byte code mapped by a character mapping.
|
||||
width("<8001>", 25.0);
|
||||
// A two-byte code (one character) without a mapping: CID 0.
|
||||
width("<9041>", 25.0);
|
||||
// A one-byte code with a notdef mapping: CID 1.
|
||||
width("<01>", 30.0);
|
||||
}
|
||||
|
||||
/// `usecmap` brings in another CMap's ranges and mappings; the CMap's own come after them.
|
||||
#[test]
|
||||
fn usecmap_takes_the_parents_mappings() {
|
||||
let r = import(&program(
|
||||
true,
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /Test-H usecmap /CMapName /Child def\n\
|
||||
1 begincidchar <43> 2 endcidchar endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/Child2 /Child [/TestCID] composefont 50 scalefont setfont 10 20 moveto (AC) show",
|
||||
))
|
||||
.unwrap();
|
||||
let p = paths(&r);
|
||||
assert_eq!(p.len(), 2, "{p:?}");
|
||||
assert!((p[1].x0 - 40.0).abs() < 0.01 && (p[1].height() - 20.0).abs() < 0.01, "{p:?}");
|
||||
}
|
||||
|
||||
/// A Type 2 CIDFont with Source Sans 3's glyphs: CIDs 1 and 2 are H and I.
|
||||
fn cid2_font(cid_map: &str) -> String {
|
||||
format!(
|
||||
"20 dict begin /CIDFontName /TestCID2 def /CIDFontType 2 def /FontType 11 def /FontMatrix [1 0 0 1 0 0] def\n\
|
||||
/CIDSystemInfo 3 dict dup begin /Registry (Test) def /Ordering (Latin) def /Supplement 0 def end def\n\
|
||||
/FontBBox [0 0 1 1] def /CIDCount 3 def /GDBytes 2 def /CIDMap {cid_map} def\n\
|
||||
/sfnts [\n{}] def CIDFontName currentdict end /CIDFont defineresource pop\n",
|
||||
sfnts()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type2_cidfonts_draw_their_truetype_glyphs_through_cidmap() {
|
||||
let (h, wh) = glyph('H');
|
||||
let (i, wi) = glyph('I');
|
||||
for cid_map in [format!("<0000{h:04x}{i:04x}>"), format!("[<0000{h:04x}> <{i:04x}>]")] {
|
||||
let body = format!("{}/T2 /Identity-H [/TestCID2] composefont 50 scalefont setfont 10 20 moveto <00010002> show", cid2_font(&cid_map));
|
||||
let r = super::tests::read(&body);
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
let p = paths(&r);
|
||||
assert_eq!(p.len(), 2, "{cid_map}: {p:?}");
|
||||
let (bh, bi) = (p[0], p[1]);
|
||||
assert!(bh.x0 > 10.0 && bh.x0 < 18.0 && bh.height() > 25.0 && bh.height() < 40.0, "{bh:?}");
|
||||
assert!(bi.x0 > 10.0 + 50.0 * wh - 1.0 && bi.x1 < 10.0 + 50.0 * (wh + wi), "{bi:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `Identity-V`: each glyph an em below the one before (writing mode 1).
|
||||
#[test]
|
||||
fn identity_v_sets_glyphs_downwards() {
|
||||
let (h, _) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
let body = format!(
|
||||
"{}/T2V /Identity-V [/TestCID2] composefont 40 scalefont setfont 50 90 moveto <00010002> show",
|
||||
cid2_font(&format!("<0000{h:04x}{i:04x}>"))
|
||||
);
|
||||
let p = paths(&super::tests::read(&body));
|
||||
assert_eq!(p.len(), 2, "{p:?}");
|
||||
// Document y runs down: the I is 40 pt (an em) below the H.
|
||||
assert!((p[1].y1 - p[0].y1 - 40.0).abs() < 0.5, "{p:?}");
|
||||
check(&format!(
|
||||
"{}/T2V /Identity-V [/TestCID2] composefont 40 scalefont setfont (\\000\\001\\000\\002) stringwidth -80 sub abs 0.01 lt exch abs 0.01 lt and",
|
||||
cid2_font("0")
|
||||
));
|
||||
}
|
||||
|
||||
/// A CIDFont the file doesn't carry draws nothing, an em a character, and says so.
|
||||
#[test]
|
||||
fn cidfonts_the_file_lacks_draw_nothing_with_a_warning() {
|
||||
let r = super::tests::read(
|
||||
"/X /Identity-H [/NotInFile] composefont 50 scalefont setfont 10 20 moveto <00410042> show 0 0 1 setrgbcolor currentpoint 5 5 rectfill",
|
||||
);
|
||||
let p = paths(&r);
|
||||
assert_eq!(p.len(), 1, "{p:?}");
|
||||
// The point moved two ems along.
|
||||
assert!((p[0].x0 - 110.0).abs() < 0.01, "{p:?}");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("CID-keyed")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// The CIDInit operators are found through the procedure set, not systemdict.
|
||||
#[test]
|
||||
fn cidinit_operators_are_not_in_systemdict() {
|
||||
check("systemdict /begincmap known not /CIDInit /ProcSet findresource /begincmap known and systemdict /composefont known and");
|
||||
}
|
||||
|
||||
/// A CMap's mappings take font 0 until its own `usefont`, whatever font number an earlier CMap
|
||||
/// ended with (Adobe Technical Note #5014).
|
||||
#[test]
|
||||
fn each_cmap_starts_at_font_0() {
|
||||
let r = import(&program(
|
||||
true,
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Two-H def\n\
|
||||
1 begincodespacerange <00> <ff> endcodespacerange 1 usefont 1 begincidchar <41> 1 endcidchar\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /One-H def\n\
|
||||
1 begincodespacerange <00> <ff> endcodespacerange 1 begincidrange <41> <42> 1 endcidrange\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/One /One-H [/TestCID] composefont 50 scalefont setfont 10 20 moveto (AB) show",
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(paths(&r).len(), 2, "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A Type 0 CIDFont with a matrix of its own (its FDArray matrices map 1000 units to an em):
|
||||
/// vertical type moves an em a character, not a thousand.
|
||||
#[test]
|
||||
fn cidfonts_with_their_own_matrix_move_an_em_a_character() {
|
||||
let font = String::from_utf8(cid_font(true)).unwrap().replace("/CIDFontType 0 def", "/CIDFontType 0 def /FontMatrix [1 0 0 1 0 0] def");
|
||||
check(&format!(
|
||||
"{font}{CMAP} /V /Identity-V [/TestCID] composefont 40 scalefont setfont (\\000\\001\\000\\002) stringwidth -80 sub abs 0.01 lt exch abs 0.01 lt and"
|
||||
));
|
||||
}
|
||||
|
||||
/// `restore` undoes the matrices `StartData` gives a CIDFont and its FDArray dictionaries, as
|
||||
/// it undoes the program's own changes.
|
||||
#[test]
|
||||
fn restore_undoes_the_matrices_start_data_sets() {
|
||||
let font = String::from_utf8(cid_font(true)).unwrap().replacen(
|
||||
"4 dict begin /FontMatrix [0.001 0 0 0.001 0 0] def /Private 2 dict dup begin /lenIV -1 def end def currentdict end",
|
||||
"fd0",
|
||||
1,
|
||||
);
|
||||
check(&format!(
|
||||
"/fd0 4 dict begin /FontMatrix [0.001 0 0 0.001 0 0] def /Private 2 dict dup begin /lenIV -1 def end def currentdict end def\n\
|
||||
/sv save def\n{font}\nsv restore fd0 /FontMatrix get 0 get 0.001 sub abs 1e-9 lt"
|
||||
));
|
||||
}
|
||||
|
||||
/// Notdef mappings added to a CMap that was already shown with take effect (its mappings are
|
||||
/// read again).
|
||||
#[test]
|
||||
fn notdef_mappings_added_later_are_read() {
|
||||
let fonts = String::from_utf8(cid_font(true)).unwrap() + CMAP;
|
||||
check(&format!(
|
||||
"{fonts} /TestFont 50 selectfont <21> stringwidth pop pop\n\
|
||||
/CIDInit /ProcSet findresource begin /Test-H /CMap findresource begin 1 beginnotdefrange <20> <2f> 1 endnotdefrange end end\n\
|
||||
<21> stringwidth pop 30 sub abs 0.01 lt"
|
||||
));
|
||||
}
|
||||
|
||||
/// A CMap built of many small blocks pays for copying its mappings at each block.
|
||||
#[test]
|
||||
fn many_small_cmap_blocks_run_out_of_the_budget() {
|
||||
super::tests::runs_out_of_budget("/CIDInit /ProcSet findresource begin 12 dict begin begincmap { 1 begincidchar <41> 1 endcidchar } loop");
|
||||
}
|
||||
|
||||
/// A CIDFont's subroutines are decrypted once, not again for every glyph: measuring a glyph of
|
||||
/// a font with 10 MB of them in a loop ends within the budget.
|
||||
#[test]
|
||||
fn cidfont_subroutines_are_read_once() {
|
||||
let font = String::from_utf8(cid_font(true)).unwrap();
|
||||
let at = font.find("/Subrs [<").unwrap() + "/Subrs [".len();
|
||||
let font = format!(
|
||||
"{}{} 0 1 9998 {{ pop 1000 string }} for{}",
|
||||
&font[..at],
|
||||
&font[at..font[at..].find(']').unwrap() + at],
|
||||
&font[font[at..].find(']').unwrap() + at..]
|
||||
);
|
||||
super::tests::runs_out_of_budget(&format!("{font}{CMAP} /TestFont 50 selectfont {{ (B) stringwidth pop pop }} loop"));
|
||||
}
|
||||
|
||||
/// Binary data larger than most operators' counts (4 MB) is read: CJK CIDFonts carry more.
|
||||
#[test]
|
||||
fn start_data_reads_more_than_4_mb() {
|
||||
let mut font = cid_font(false);
|
||||
let marker = b"(Binary) ";
|
||||
let at = font.windows(marker.len()).position(|w| w == marker).unwrap();
|
||||
let rest = font.split_off(at);
|
||||
let len_end = rest.iter().skip(marker.len()).position(|b| *b == b' ').unwrap() + marker.len();
|
||||
let n: usize = std::str::from_utf8(&rest[marker.len()..len_end]).unwrap().parse().unwrap();
|
||||
let data_at = len_end + " StartData ".len();
|
||||
let mut data = rest[data_at..data_at + n].to_vec();
|
||||
data.resize(5_000_000, 0);
|
||||
font.extend(format!("(Binary) {} StartData ", data.len()).into_bytes());
|
||||
font.extend(data);
|
||||
font.push(b'\n');
|
||||
let head = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n".to_vec();
|
||||
let r = import(&[head, font, CMAP.as_bytes().to_vec(), b"/TestFont 50 selectfont 10 20 moveto (AB) show\nshowpage\n%%EOF\n".to_vec()].concat())
|
||||
.unwrap();
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
assert_eq!(paths(&r).len(), 2);
|
||||
}
|
||||
|
||||
/// Decoding a string through a CMap with many mappings counts against the operation budget.
|
||||
#[test]
|
||||
fn decoding_through_a_large_cmap_runs_out_of_the_budget() {
|
||||
// A CIDFont the file doesn't carry: its glyphs cost nothing, so the decoding is what counts.
|
||||
super::tests::runs_out_of_budget(
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Big-H def\n\
|
||||
2 begincodespacerange <00> <7f> <8000> <ffff> endcodespacerange\n\
|
||||
16000 begincidrange 0 1 15999 { /i exch def 2 string dup 0 i 256 idiv 128 add put dup 1 i 256 mod put dup 1 } for endcidrange\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/Big /Big-H [/NotInFile] composefont 50 scalefont setfont /s 1000 string def { s stringwidth pop pop } loop",
|
||||
);
|
||||
}
|
||||
|
||||
/// A CID-keyed font's glyphs are grouped under the font's name: a CID stands for no character.
|
||||
#[test]
|
||||
fn cid_glyph_groups_take_the_fonts_name() {
|
||||
let r = import(&program(true, "/TestFont 50 selectfont 10 20 moveto (AB) show")).unwrap();
|
||||
let names: Vec<_> = all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Group { .. })).map(|n| n.name.clone()).collect();
|
||||
assert_eq!(names, [Some("TestFont".to_string())]);
|
||||
}
|
||||
|
||||
/// One show of a long string through a CMap with many mappings stops at the operation budget,
|
||||
/// not after decoding the whole string.
|
||||
#[test]
|
||||
fn one_long_show_through_a_large_cmap_runs_out_of_the_budget() {
|
||||
super::tests::runs_out_of_budget(
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Big-H def\n\
|
||||
2 begincodespacerange <00> <7f> <8000> <ffff> endcodespacerange\n\
|
||||
/mk { 0 1 15999 { /i exch def 2 string dup 0 i 256 idiv 128 add put dup 1 i 256 mod put dup 1 } for } def\n\
|
||||
16000 begincidrange mk endcidrange 16000 begincidrange mk endcidrange 16000 begincidrange mk endcidrange 16000 begincidrange mk endcidrange\n\
|
||||
16000 beginnotdefrange mk endnotdefrange 16000 beginnotdefrange mk endnotdefrange 16000 beginnotdefrange mk endnotdefrange 16000 beginnotdefrange mk endnotdefrange\n\
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end\n\
|
||||
/Big /Big-H [/NotInFile] composefont 50 scalefont setfont 4000000 string stringwidth pop pop",
|
||||
);
|
||||
}
|
||||
|
||||
/// `usecmap` copies the parent's mappings into new arrays: the copies count against the memory
|
||||
/// budget, so keeping the CMaps made from a large one in a loop stops with an error instead of
|
||||
/// growing without bound.
|
||||
#[test]
|
||||
fn usecmap_copies_count_against_the_memory_budget() {
|
||||
let r = import(&super::tests::eps(
|
||||
"0 0 10 10 rectfill /CIDInit /ProcSet findresource begin 12 dict begin begincmap\n\
|
||||
1 begincodespacerange <0000> <ffff> endcodespacerange\n\
|
||||
0 1 599 { pop 100 begincidrange 0 1 99 { pop <0000> <0001> 0 } for endcidrange } for\n\
|
||||
endcmap /Big currentdict /CMap defineresource pop end end\n\
|
||||
/CIDInit /ProcSet findresource begin { 12 dict begin /Big usecmap currentdict end } loop",
|
||||
))
|
||||
.unwrap();
|
||||
assert!(r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings);
|
||||
}
|
||||
@@ -11,10 +11,10 @@ use vectorcraft_doc::NodeKind;
|
||||
|
||||
use super::tests::{all, bounds, fill, read};
|
||||
|
||||
const FONT: &[u8] = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
pub(super) const FONT: &[u8] = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
|
||||
/// The glyph index of `c` in [`FONT`] and its advance width in ems.
|
||||
fn glyph(c: char) -> (u32, f64) {
|
||||
pub(super) fn glyph(c: char) -> (u32, f64) {
|
||||
let font = FontRef::new(FONT).unwrap();
|
||||
let gid = font.charmap().map(c).unwrap();
|
||||
let upem = f64::from(font.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
|
||||
@@ -24,7 +24,7 @@ fn glyph(c: char) -> (u32, f64) {
|
||||
|
||||
/// `sfnts` strings of [`FONT`]: the first of odd length, ending in a padding byte that isn't part
|
||||
/// of the font, the rest split at even lengths.
|
||||
fn sfnts() -> String {
|
||||
pub(super) fn sfnts() -> String {
|
||||
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let mut out = format!("<{}00>\n", hex(&FONT[..20000]));
|
||||
for chunk in FONT[20000..].chunks(40000) {
|
||||
@@ -251,3 +251,16 @@ fn unreadable_type42_data_read_again_runs_out_of_the_budget() {
|
||||
/Comp findfont 50 scalefont setfont {{ ({s}) stringwidth pop pop }} loop"
|
||||
));
|
||||
}
|
||||
|
||||
/// Characters that map to no glyph (an empty `FDepVector`) cost nothing to draw, but finding that
|
||||
/// out reads the whole string: a `loop` showing, measuring or outlining a long string of them runs
|
||||
/// out of the operation budget instead of stalling the import.
|
||||
#[test]
|
||||
fn unmapped_composite_characters_in_a_loop_run_out_of_the_budget() {
|
||||
let font = "16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def\n\
|
||||
/Encoding [0] def /FDepVector [] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont /s 100000 string def";
|
||||
for body in ["{ 0 0 moveto s show } loop", "{ s stringwidth pop pop } loop", "{ newpath 0 0 moveto s false charpath } loop"] {
|
||||
super::tests::runs_out_of_budget(&format!("{font} {body}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
//! Plain Type 42 fonts (TrueType fonts in PostScript, Adobe Technical Note #5012, "The Type 42
|
||||
//! Font Format Specification") that carry their TrueType font: each code's character from its
|
||||
//! glyph name, or from the glyph its `CharStrings` name gives when the name says nothing
|
||||
//! (`/g12`); its width from the font's own metrics; glyphs that stand for no character drawn
|
||||
//! from the font's outlines. Spacing from the show operators (PLRM 3rd ed., §5.1.4 and the
|
||||
//! `ashow`, `widthshow` and `xshow` entries of chapter 8) is kept on live type as tracking. The
|
||||
//! fonts are written from scratch around the bundled Source Sans 3.
|
||||
|
||||
use skrifa::{FontRef, MetadataProvider};
|
||||
use vectorcraft_doc::{NodeKind, TextObject};
|
||||
|
||||
use super::tests::{all, read};
|
||||
use super::tests_composite::{FONT, glyph, sfnts};
|
||||
|
||||
/// A program defining Type 42 font `/Plain42` whose `Encoding` gives codes 1 to `names.len()`
|
||||
/// glyph names `/g1`, `/g2`… mapped through `CharStrings` to the glyph of each of `glyphs`.
|
||||
fn font(glyphs: &[u32]) -> String {
|
||||
let mut enc = String::new();
|
||||
let mut strings = String::new();
|
||||
for (i, g) in glyphs.iter().enumerate() {
|
||||
enc.push_str(&format!("Encoding {} /g{} put ", i + 1, i + 1));
|
||||
strings.push_str(&format!("/g{} {g} def ", i + 1));
|
||||
}
|
||||
format!(
|
||||
"10 dict begin /FontName /Plain42 def /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /FontBBox [0 0 1 1] def /PaintType 0 def\n\
|
||||
/Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for {enc}\n\
|
||||
/CharStrings {} dict def CharStrings begin /.notdef 0 def {strings} end\n\
|
||||
/sfnts [\n{}] def FontName currentdict end definefont pop\n",
|
||||
glyphs.len() + 1,
|
||||
sfnts()
|
||||
)
|
||||
}
|
||||
|
||||
fn texts(r: &crate::Imported) -> Vec<TextObject> {
|
||||
all(&r.document)
|
||||
.iter()
|
||||
.filter_map(|n| match &n.kind {
|
||||
NodeKind::Text(t) => Some((**t).clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where each glyph of `t` starts along its baseline (text space).
|
||||
fn origins(t: &TextObject) -> Vec<f64> {
|
||||
vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).glyphs.iter().map(|g| g.origin.x).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_names_that_say_nothing_read_the_fonts_character_map() {
|
||||
let (h, _) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
let r = read(&format!("{}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002) show", font(&[h, i])));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.len(), 1, "{:?}", all(&r.document));
|
||||
assert_eq!(t[0].plain_text(), "HI");
|
||||
assert_eq!(t[0].runs[0].style.font_family, "Plain42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_glyph_name_says_its_character_whatever_its_code() {
|
||||
// The Euro by its name at code 128, where Latin-1 has a control code.
|
||||
// A standard encoding name says its character too (`hyphen`), not one the font's character
|
||||
// map also maps to its glyph (the soft hyphen).
|
||||
let (e, _) = glyph('€');
|
||||
let (h, _) = glyph('-');
|
||||
let program = font(&[e, h])
|
||||
.replace("Encoding 1 /g1 put", "Encoding 128 /Euro put")
|
||||
.replace("/g1 ", "/Euro ")
|
||||
.replace("Encoding 2 /g2 put", "Encoding 45 /hyphen put")
|
||||
.replace("/g2 ", "/hyphen ");
|
||||
let r = read(&format!("{program}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\200-) show"));
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<String>(), "€-");
|
||||
}
|
||||
|
||||
/// A glyph of the bundled font with an outline that the character map doesn't reach (an
|
||||
/// alternate form) and whose name says no character.
|
||||
fn alternate_glyph() -> u32 {
|
||||
let f = FontRef::new(FONT).unwrap();
|
||||
let mapped: std::collections::HashSet<u32> = f.charmap().mappings().map(|(_, g)| g.to_u32()).collect();
|
||||
let names = f.glyph_names();
|
||||
(1..f.glyph_names().num_glyphs())
|
||||
.find(|g| !mapped.contains(g) && super::text::glyph_char(names.get(skrifa::GlyphId::new(*g)).unwrap().as_str()) == '\u{2022}')
|
||||
.and_then(|g| {
|
||||
// One with an outline.
|
||||
let (o, _) = super::type42::glyph(FONT, g)?;
|
||||
(!o.elements().is_empty()).then_some(g)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyphs_that_stand_for_no_character_are_drawn_from_the_font() {
|
||||
let alt = alternate_glyph();
|
||||
let (h, _) = glyph('H');
|
||||
let r = read(&format!("{}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002) show", font(&[h, alt])));
|
||||
assert!(texts(&r).is_empty());
|
||||
let paths = all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count();
|
||||
assert_eq!(paths, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fonts_own_widths_space_its_type_and_move_the_point() {
|
||||
let (h, wh) = glyph('H');
|
||||
let (i, wi) = glyph('I');
|
||||
let f = font(&[h, i]);
|
||||
let check = |cond: String| super::tests::check(&format!("{f} /Plain42 findfont 50 scalefont setfont {cond}"));
|
||||
check(format!("(\\001\\002) stringwidth pop {} sub abs 0.01 lt", 50.0 * (wh + wi)));
|
||||
check(format!("gsave nulldevice 10 20 moveto (\\001\\002) show currentpoint grestore pop {} sub abs 0.01 lt", 10.0 + 50.0 * (wh + wi)));
|
||||
check("gsave nulldevice 10 20 moveto (\\001\\002) [30 7] xshow currentpoint grestore pop 47 sub abs 0.01 lt".into());
|
||||
check("newpath 10 20 moveto (\\001) true charpath pathbbox exch pop sub abs 25 gt exch 10 sub abs 8 lt and".into());
|
||||
}
|
||||
|
||||
/// `xshow` and `ashow` put each character where they say: the type's tracking makes up the
|
||||
/// difference from its font's advances.
|
||||
#[test]
|
||||
fn show_operators_space_live_type_with_tracking() {
|
||||
let (h, wh) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
let f = font(&[h, i, h]);
|
||||
let r = read(&format!("{f}/Plain42 findfont 50 scalefont setfont 10 20 moveto (\\001\\002\\003) [40 25 10] xshow"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.len(), 1);
|
||||
assert_eq!(t[0].plain_text(), "HIH");
|
||||
let o = origins(&t[0]);
|
||||
assert!((o[1] - o[0] - 40.0).abs() < 0.05 && (o[2] - o[1] - 25.0).abs() < 0.05, "{o:?}");
|
||||
let r = read(&format!("{f}/Plain42 findfont 50 scalefont setfont 10 20 moveto 6 0 (\\001\\002) ashow"));
|
||||
let o = origins(&texts(&r)[0]);
|
||||
assert!((o[1] - o[0] - (50.0 * wh + 6.0)).abs() < 0.05, "{o:?}");
|
||||
}
|
||||
|
||||
/// A font found by name (its program isn't in the file) keeps the installed font's spacing,
|
||||
/// plus what the show operator adds.
|
||||
#[test]
|
||||
fn named_fonts_take_the_show_operators_spacing() {
|
||||
let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto (AB) [30 0] xshow");
|
||||
let t = texts(&r);
|
||||
assert_eq!(t[0].plain_text(), "AB");
|
||||
let o = origins(&t[0]);
|
||||
assert!((o[1] - o[0] - 30.0).abs() < 0.05, "{o:?}");
|
||||
// Plain `show` keeps a single run with no tracking.
|
||||
let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto (AB) show");
|
||||
let t = texts(&r);
|
||||
assert!(t[0].runs.len() == 1 && t[0].runs[0].style.tracking == 0.0, "{:?}", t[0].runs);
|
||||
}
|
||||
|
||||
/// Characters the installed font would join in a ligature (`fi`) keep the places the file gives
|
||||
/// each of them: the type is set without ligatures and each character's tracking is its own.
|
||||
#[test]
|
||||
fn characters_a_ligature_would_join_keep_their_places() {
|
||||
// Each character at its own width in the bundled Source Sans 3, as a PDF converter writes them.
|
||||
let (_, wf) = glyph('f');
|
||||
let (_, wi) = glyph('i');
|
||||
let (f, i) = (50.0 * wf, 50.0 * wi);
|
||||
let r = read(&format!("/SourceSans3-Regular findfont 50 scalefont setfont 10 20 moveto (fit) [{f} {i} 0] xshow"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t[0].plain_text(), "fit");
|
||||
let o = origins(&t[0]);
|
||||
assert_eq!(o.len(), 3, "{o:?}");
|
||||
assert!((o[1] - o[0] - f).abs() < 0.05 && (o[2] - o[1] - i).abs() < 0.05, "{o:?} {f} {i}");
|
||||
}
|
||||
|
||||
/// A move that is finite in user space but not along the type's baseline (a font matrix
|
||||
/// squashed a thousandfold) is an `undefinedresult`, not a tracking that isn't a number.
|
||||
#[test]
|
||||
fn moves_too_large_for_the_baseline_are_refused() {
|
||||
let r = read("0 0 10 10 rectfill /Helvetica findfont [0.001 0 0 50 0 0] makefont setfont 10 20 moveto (A\\001) [1e307 -1e307] xshow");
|
||||
assert!(texts(&r).iter().all(|t| t.runs.iter().all(|r| r.style.tracking.is_finite())), "{:?}", texts(&r));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("undefinedresult")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Moves that overflow (two `1e308` numbers in `xshow`) in type drawn from the font's outlines
|
||||
/// are an `undefinedresult` before anything is painted, as for live type: no glyph lands at
|
||||
/// infinity.
|
||||
#[test]
|
||||
fn outline_moves_that_overflow_are_refused() {
|
||||
let alt = alternate_glyph();
|
||||
let (h, _) = glyph('H');
|
||||
let r = read(&format!(
|
||||
"0 0 10 10 rectfill {}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002\\001) [1e308 1e308 0] xshow",
|
||||
font(&[h, alt])
|
||||
));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("undefinedresult")), "{:?}", r.warnings);
|
||||
assert_eq!(all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count(), 1, "{:?}", all(&r.document));
|
||||
}
|
||||
|
||||
/// Reading a font's character map counts the mappings it visits, once, for the operation
|
||||
/// budget.
|
||||
#[test]
|
||||
fn reading_the_character_map_counts_its_mappings() {
|
||||
let f = super::type42::Font::new(FONT.to_vec());
|
||||
let (h, _) = glyph('H');
|
||||
assert_eq!(f.char_of(h), Some('H'));
|
||||
let n = f.take_char_map_work();
|
||||
assert!(n > 100, "{n}");
|
||||
f.char_of(h);
|
||||
assert_eq!(f.take_char_map_work(), 0);
|
||||
}
|
||||
|
||||
/// A glyph named for a symbol font's private-use code (`uniF041`) stands for no character, as
|
||||
/// when the character map maps only that code to it, so it's drawn from the font's outline;
|
||||
/// other names still say their character.
|
||||
#[test]
|
||||
fn private_use_glyph_names_stand_for_no_character() {
|
||||
use super::type42::post_name_char;
|
||||
assert_eq!(post_name_char("uniF041"), None);
|
||||
assert_eq!(post_name_char("uF0FF"), None);
|
||||
assert_eq!(post_name_char("uni00AD"), None);
|
||||
assert_eq!(post_name_char("uniF100"), Some('\u{F100}'));
|
||||
assert_eq!(post_name_char("uni20AC"), Some('€'));
|
||||
assert_eq!(post_name_char("A"), Some('A'));
|
||||
assert_eq!(post_name_char("bullet"), Some('\u{2022}'));
|
||||
assert_eq!(post_name_char("g12"), None);
|
||||
}
|
||||
|
||||
/// A glyph the character map doesn't reach is looked up by its glyph name once: the first lookup
|
||||
/// counts for the operation budget, a second one is remembered (a miss too).
|
||||
#[test]
|
||||
fn glyph_name_lookups_are_counted_once() {
|
||||
let f = super::type42::Font::new(FONT.to_vec());
|
||||
let alt = alternate_glyph();
|
||||
let first = f.char_of(alt);
|
||||
f.take_char_map_work();
|
||||
// The character map was read by the first call; the name lookup is remembered.
|
||||
let g = FontRef::new(FONT).unwrap().glyph_names().num_glyphs() + 5;
|
||||
assert_eq!(f.char_of(g), None);
|
||||
assert!(f.take_char_map_work() > 0);
|
||||
assert_eq!(f.char_of(g), None);
|
||||
assert_eq!(f.char_of(alt), first);
|
||||
assert_eq!(f.take_char_map_work(), 0);
|
||||
}
|
||||
|
||||
/// Control codes (a new string's zeros) set no type but are still read: showing or measuring a
|
||||
/// long string of them in a loop runs out of the operation budget.
|
||||
#[test]
|
||||
fn control_codes_in_a_loop_run_out_of_the_budget() {
|
||||
for body in ["{ s stringwidth pop pop } loop", "{ 0 0 moveto s show } loop"] {
|
||||
super::tests::runs_out_of_budget(&format!("/Helvetica findfont 10 scalefont setfont /s 1000000 string def {body}"));
|
||||
}
|
||||
}
|
||||
+429
-83
@@ -168,12 +168,12 @@ impl Interp<'_> {
|
||||
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) {
|
||||
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(&s)?,
|
||||
None => self.show_plain(&s, &spacing)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,7 @@ impl Interp<'_> {
|
||||
};
|
||||
self.push(Obj::Dict(font))?;
|
||||
}
|
||||
op if op == ComposeFont || Op::PROCSET.contains(&op) => self.cid_op(op)?,
|
||||
_ => return ps_err("undefined", op.name()),
|
||||
}
|
||||
Ok(())
|
||||
@@ -277,14 +278,20 @@ impl Interp<'_> {
|
||||
|
||||
/// 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) {
|
||||
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 { return Ok(self.set_type(s, Point::ZERO)?.2) };
|
||||
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)?;
|
||||
@@ -421,10 +428,18 @@ impl Interp<'_> {
|
||||
/// `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 = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
|
||||
self.set_text(&text, at)
|
||||
}
|
||||
|
||||
/// [`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((s.len() as u64).saturating_mul(64).saturating_add(300))?;
|
||||
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;
|
||||
@@ -435,10 +450,9 @@ impl Interp<'_> {
|
||||
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 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);
|
||||
@@ -448,19 +462,193 @@ impl Interp<'_> {
|
||||
|
||||
/// `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 (t, _, adv) = self.set_type(s, at)?;
|
||||
if !self.g.paint.is_none() && !t.plain_text().trim().is_empty() {
|
||||
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 => s.iter().map(|b| Some(char::from(*b))).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;
|
||||
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 d = spacing.advance(i, i64::from(code), em(own));
|
||||
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();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
||||
}
|
||||
self.g.cur = Some(self.xf() * (at + adv));
|
||||
self.g.cur = Some(self.xf() * (at + total));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `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: &super::type42::Font, glyphs: &[Plain], s: &[u8], spacing: &Spacing, at: Point, f1: Affine) -> Res {
|
||||
// Each glyph's place, 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 places = Vec::with_capacity(glyphs.len());
|
||||
let mut pen = at;
|
||||
for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() {
|
||||
places.push(pen);
|
||||
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();
|
||||
for (g, place) in glyphs.iter().zip(places) {
|
||||
if let Some(outline) = super::type42::glyph(&data.data, g.gid).map(|o| o.0) {
|
||||
self.type42_paint(outline, place, f1, Draw::Fill)?;
|
||||
}
|
||||
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) {
|
||||
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)?;
|
||||
@@ -468,6 +656,18 @@ impl Interp<'_> {
|
||||
}
|
||||
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 {
|
||||
if let Some((outline, _)) = super::type42::glyph(&data.data, g.gid) {
|
||||
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 {
|
||||
@@ -520,14 +720,17 @@ impl Spacing {
|
||||
}
|
||||
}
|
||||
|
||||
/// A character of a composite font: the descendant font that draws it, its code there, and the
|
||||
/// composite font's matrix (over the descendant's own).
|
||||
/// 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,
|
||||
cid: Option<(u32, Option<u32>)>,
|
||||
outer: Affine,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
/// What drawing a glyph does with it.
|
||||
@@ -561,15 +764,34 @@ fn units(f: &Dict) -> f64 {
|
||||
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"); `None` for other fonts and
|
||||
/// mappings, which are set as type in the font of the composite font's name.
|
||||
fn composite_glyphs(&self, s: &[u8]) -> Option<Vec<Mapped>> {
|
||||
let font = self.g.font.clone()?;
|
||||
let f = font.borrow();
|
||||
if font_type(&f) != Some(0.0) || f.get(&Key::name("FMapType")).and_then(Obj::as_num) != Some(2.0) {
|
||||
return None;
|
||||
/// 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);
|
||||
}
|
||||
let (encoding, fonts) = (f.get(&Key::name("Encoding"))?.items()?.clone(), f.get(&Key::name("FDepVector"))?.items()?.clone());
|
||||
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);
|
||||
@@ -579,10 +801,37 @@ impl Interp<'_> {
|
||||
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, outer });
|
||||
out.push(Mapped { font: d.clone(), pick, code, cid: None, outer, vertical: false });
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
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) {
|
||||
Some(Obj::Dict(d)) => {
|
||||
Some(Mapped { font: d.clone(), pick: 0, code: (c.cid & 0xff) as u8, 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.
|
||||
@@ -596,7 +845,8 @@ impl Interp<'_> {
|
||||
// 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));
|
||||
let c = code_char(&g.font.borrow(), g.code);
|
||||
// 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 { code_char(&g.font.borrow(), g.code) };
|
||||
if c != '\u{2022}' {
|
||||
text.push(c);
|
||||
}
|
||||
@@ -618,25 +868,15 @@ impl Interp<'_> {
|
||||
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 {
|
||||
return self.type3_glyph_in(&g.font, Glyph::Code(g.code), at, paint == Draw::Fill, g.outer);
|
||||
}
|
||||
let fm = g.outer * font_matrix(&g.font.borrow(), 1.0);
|
||||
if let Some((outline, advance)) = self.type42_glyph(&g.font, g.code)? {
|
||||
// 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 {
|
||||
let mut p = outline;
|
||||
p.apply_affine(self.xf() * Affine::translate(at.to_vec2()) * fm);
|
||||
if paint == Draw::Fill {
|
||||
self.fill_path(p, vectorcraft_geom::FillRule::NonZero)?;
|
||||
} else {
|
||||
if self.g.path.elements().len().saturating_add(p.elements().len()) > super::graphics::MAX_PATH {
|
||||
return Err(PsError::Limit("a path has too many points"));
|
||||
}
|
||||
self.g.path.extend(p);
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -662,62 +902,148 @@ impl Interp<'_> {
|
||||
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` of Type 42 font `font` from the TrueType font it carries: its outline and
|
||||
/// width in its glyph space (ems), or `None` when it isn't a Type 42 font with one.
|
||||
fn type42_glyph(&mut self, font: &DictRef, code: u8) -> Res<Option<(vectorcraft_geom::BezPath, f64)>> {
|
||||
let (sfnts, gid) = {
|
||||
let Some(data) = self.type42_data(font)? else { return Ok(None) };
|
||||
let gid = {
|
||||
let f = font.borrow();
|
||||
if font_type(&f) != Some(42.0) {
|
||||
let name = glyph_name(&f, code);
|
||||
char_string(&f, &name).or_else(|| char_string(&f, ".notdef")).unwrap_or(0)
|
||||
};
|
||||
Ok(super::type42::glyph(&data.data, gid))
|
||||
}
|
||||
|
||||
/// The codes of `s` in the current font when it is a Type 42 font that carries its TrueType
|
||||
/// font (not a descendant of a composite font): the font, and each code's glyph.
|
||||
fn type42_plain(&mut self, s: &[u8]) -> Res<Option<(Rc<super::type42::Font>, Vec<Plain>)>> {
|
||||
let Some(font) = self.g.font.clone() else { return Ok(None) };
|
||||
let Some(data) = self.type42_data(&font)? else { return Ok(None) };
|
||||
self.spend((s.len() as u64).saturating_mul(4))?;
|
||||
let f = font.borrow();
|
||||
let glyphs = s
|
||||
.iter()
|
||||
.map(|&code| {
|
||||
let (ch, gid) = type42_code(&f, &data, code);
|
||||
Plain { ch, gid, width: super::type42::advance(&data.data, gid) }
|
||||
})
|
||||
.collect();
|
||||
drop(f);
|
||||
// Reading the font's character map (once) visits each of its mappings.
|
||||
self.spend(data.take_char_map_work())?;
|
||||
Ok(Some((data, glyphs)))
|
||||
}
|
||||
|
||||
/// 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) };
|
||||
let name = glyph_name(&f, code);
|
||||
let strings = f.get(&Key::name("CharStrings")).and_then(|c| match c {
|
||||
Obj::Dict(d) => Some(d.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let gid = strings
|
||||
.and_then(|d| {
|
||||
let d = d.borrow();
|
||||
d.get(&Key::name(&name)).or_else(|| d.get(&Key::name(".notdef"))).and_then(Obj::as_num)
|
||||
})
|
||||
.filter(|g| (0.0..=f64::from(u16::MAX)).contains(g))
|
||||
.map_or(0, |g| g as u32);
|
||||
(sfnts, gid)
|
||||
sfnts
|
||||
};
|
||||
// The least recently used font goes first: a hit moves to the end.
|
||||
let data = match self.type42.iter().position(|(k, _)| k.same(&sfnts)) {
|
||||
Some(i) => {
|
||||
let hit = self.type42.remove(i);
|
||||
let data = hit.1.clone();
|
||||
self.type42.push(hit);
|
||||
data
|
||||
}
|
||||
None => {
|
||||
// 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(Rc::new);
|
||||
if let Some(d) = &data
|
||||
&& self.type42_counted.insert(sfnts.clone())
|
||||
{
|
||||
self.alloc(d.len())?;
|
||||
}
|
||||
if self.type42.len() >= MAX_TYPE42 {
|
||||
self.type42.remove(0);
|
||||
}
|
||||
self.type42.push((sfnts, data.clone()));
|
||||
data
|
||||
}
|
||||
};
|
||||
let Some(data) = data else { return Ok(None) };
|
||||
Ok(super::type42::glyph(&data, gid))
|
||||
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 Type 42 font: 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 Type 42 font `f`, carrying TrueType font `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). A code the encoding leaves `.notdef` (the standard encodings here have no
|
||||
/// names) is read as Latin-1, drawn by that character's glyph.
|
||||
fn type42_code(f: &Dict, data: &super::type42::Font, code: u8) -> (Option<char>, u32) {
|
||||
let name = glyph_name(f, code);
|
||||
if &*name == ".notdef" {
|
||||
let c = char::from(code);
|
||||
return (Some(c), data.gid_of(c).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 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;
|
||||
|
||||
@@ -735,6 +1061,26 @@ 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`.
|
||||
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();
|
||||
@@ -744,7 +1090,7 @@ fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
}
|
||||
|
||||
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
|
||||
fn glyph_char(name: &str) -> char {
|
||||
pub(super) fn glyph_char(name: &str) -> char {
|
||||
let mut chars = name.chars();
|
||||
if let (Some(c), None) = (chars.next(), chars.next()) {
|
||||
return c;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use skrifa::{FontRef, GlyphId, MetadataProvider};
|
||||
use vectorcraft_geom::BezPath;
|
||||
|
||||
@@ -45,6 +48,86 @@ pub(super) fn font_size(sfnts: &[Obj]) -> (usize, usize) {
|
||||
(sfnts.len(), bytes)
|
||||
}
|
||||
|
||||
/// The TrueType font a Type 42 font carries, with the character each glyph stands for (read
|
||||
/// when first asked).
|
||||
pub(super) struct Font {
|
||||
pub data: Vec<u8>,
|
||||
chars: OnceCell<HashMap<u32, char>>,
|
||||
/// The characters glyph names give glyphs the character map doesn't reach, misses too:
|
||||
/// a custom `post` name is found by scanning the names before it, so each glyph is looked
|
||||
/// up once.
|
||||
names: RefCell<HashMap<u32, Option<char>>>,
|
||||
/// The character-map mappings visited reading `chars`, not yet counted against the
|
||||
/// operation budget (see [`Self::take_char_map_work`]).
|
||||
work: Cell<u64>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(data: Vec<u8>) -> Self {
|
||||
Self { data, chars: OnceCell::new(), names: RefCell::new(HashMap::new()), work: Cell::new(0) }
|
||||
}
|
||||
|
||||
/// The work reading the character map did since last asked (mappings visited).
|
||||
pub fn take_char_map_work(&self) -> u64 {
|
||||
self.work.take()
|
||||
}
|
||||
|
||||
/// The character glyph `gid` stands for: the one the font's character map maps to it
|
||||
/// (the lowest, when it maps several), else the one its `post` table's glyph name says
|
||||
/// (`uni20AC`, a single character). `None` for a glyph neither names, and for one a symbol
|
||||
/// font maps only from, or names only as, its private-use codes (U+F000–U+F0FF), which
|
||||
/// aren't characters.
|
||||
pub fn char_of(&self, gid: u32) -> Option<char> {
|
||||
let chars = self.chars.get_or_init(|| {
|
||||
let mut m = HashMap::new();
|
||||
if let Ok(font) = FontRef::new(&self.data) {
|
||||
for (c, g) in font.charmap().mappings() {
|
||||
self.work.set(self.work.get().saturating_add(1));
|
||||
let Some(c) = char::from_u32(c).filter(|c| is_char(*c)) else {
|
||||
continue;
|
||||
};
|
||||
m.entry(g.to_u32()).and_modify(|e: &mut char| *e = (*e).min(c)).or_insert(c);
|
||||
}
|
||||
}
|
||||
m
|
||||
});
|
||||
if let Some(c) = chars.get(&gid) {
|
||||
return Some(*c);
|
||||
}
|
||||
if let Some(c) = self.names.borrow().get(&gid) {
|
||||
return *c;
|
||||
}
|
||||
// Finding a custom name reads the names before it: about one operation for every 16.
|
||||
self.work.set(self.work.get().saturating_add(u64::from(gid / 16) + 1));
|
||||
let c = FontRef::new(&self.data).ok().and_then(|font| {
|
||||
let name = font.glyph_names().get(GlyphId::new(gid))?;
|
||||
post_name_char(name.as_str())
|
||||
});
|
||||
self.names.borrow_mut().insert(gid, c);
|
||||
c
|
||||
}
|
||||
|
||||
/// The glyph the font's character map gives `c`.
|
||||
pub fn gid_of(&self, c: char) -> Option<u32> {
|
||||
let font = FontRef::new(&self.data).ok()?;
|
||||
font.charmap().map(c).map(|g| g.to_u32()).filter(|g| *g != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether character `c`, read from a font's character map or glyph names, is one the glyph
|
||||
/// stands for: not a symbol font's private-use codes, controls or the soft hyphen, which doesn't
|
||||
/// show (a hyphen glyph mapped from it is the hyphen's).
|
||||
fn is_char(c: char) -> bool {
|
||||
!('\u{F000}'..='\u{F0FF}').contains(&c) && !c.is_control() && c != '\u{AD}'
|
||||
}
|
||||
|
||||
/// The character `post` table glyph name `name` says (`uni20AC`, a single character), when it
|
||||
/// says one [`is_char`] accepts.
|
||||
pub(super) fn post_name_char(name: &str) -> Option<char> {
|
||||
let c = super::text::glyph_char(name);
|
||||
(c != '\u{2022}' || name == "bullet").then_some(c).filter(|c| is_char(*c))
|
||||
}
|
||||
|
||||
/// Glyph `gid` of TrueType font `data`: its outline and advance width, in ems (y up), or `None`
|
||||
/// when the font can't be read.
|
||||
pub(super) fn glyph(data: &[u8], gid: u32) -> Option<(BezPath, f64)> {
|
||||
@@ -65,6 +148,14 @@ pub(super) fn glyph(data: &[u8], gid: u32) -> Option<(BezPath, f64)> {
|
||||
Some((pen.0, if advance.is_finite() { advance } else { 0.0 }))
|
||||
}
|
||||
|
||||
/// The advance width of glyph `gid` of TrueType font `data`, in ems (0 when it can't be read).
|
||||
pub(super) fn advance(data: &[u8], gid: u32) -> f64 {
|
||||
let Ok(font) = FontRef::new(data) else { return 0.0 };
|
||||
let upem = f64::from(font.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
|
||||
let w = font.glyph_metrics(Size::unscaled(), LocationRef::default()).advance_width(GlyphId::new(gid)).map_or(0.0, f64::from) / upem;
|
||||
if w.is_finite() { w } else { 0.0 }
|
||||
}
|
||||
|
||||
/// A pen drawing an outline scaled from font units to ems.
|
||||
struct Pen(BezPath, f64);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -281,7 +281,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
| Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) |
|
||||
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure with template layers and ruler guides, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus |
|
||||
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings and CID-keyed fonts are still set as type in the font of their name); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed fonts in a FontSet (Compact Font Format) and CIDFonts the file doesn't carry not drawn; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths; composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry; a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`) |
|
||||
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). CNC preset and VCarve QA remain open |
|
||||
@@ -320,6 +320,8 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-07 | minor | EPS import: CID-keyed fonts (PLRM 3rd ed., §5.11; [Adobe Technical Note #5014](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5014.CIDFont_Spec.pdf)) draw from the font data the file carries instead of stopping the program at `composefont` or the CIDInit operators: CMaps built with `begincmap` … `endcmap` (codespace ranges, CID and notdef mappings, `usefont`, `usecmap`; mappings to base-font codes are not read) and the predefined `Identity-H` and `Identity-V`; `composefont` and `FMapType` 9 fonts, whose codes take as many bytes as their codespace range says, an invalid or unmapped code falling back as §5.11.5 says; Type 0 CIDFonts' Type 1 charstrings, read by `StartData` (binary or hexadecimal) or from a `GlyphDirectory`, with their FDArray dictionaries' matrices and subroutines (`Subrs` or the binary data's subroutine table); Type 2 CIDFonts' TrueType glyphs through `CIDMap` (strings, a dictionary or an offset); `Metrics` widths; vertical writing mode set downwards. A CIDFont the file doesn't carry draws nothing, with a warning; FontSet (Compact Font Format) data is skipped. |
|
||||
| 2026-11-07 | minor | EPS import: a plain Type 42 font that carries its TrueType font ([Adobe Technical Note #5012](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5012.Type42_Spec.pdf)) gives each code its character by its glyph name (single characters, `uniXXXX`, the standard encoding's and Windows-1252's names) or, when the name says nothing (`/g12`), by the glyph `CharStrings` gives it in the font's own character map or `post` names, instead of reading the code as Latin-1 (a Euro at code 128 was dropped); the font's own widths move the current point (`stringwidth`, `show`), `charpath` outlines its glyphs, and a string with a glyph that stands for no character is drawn from the font's outlines. Live type keeps the spacing the file gives it: where the show operator's numbers (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`: PLRM 3rd ed., chapter 8) or the embedded font's widths differ from the installed or fallback font's advances, each character's tracking makes up the difference, as PDF import does, so justified type in a font that isn't installed no longer drifts (pdftocairo and pdftops EPS of a justified PDF page set in a missing font: 10.2% and 9.0% of the page differing from Ghostscript's render, now 6.2% and 5.8%). Type under `nulldevice` no longer shows. |
|
||||
| 2026-11-06 | minor | EPS import: the buffers an image decoder makes for itself (its sample planes, its copies of the strings a procedure data source hands back, and the split buffers of an interleaved masked image) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept, and anything the program's own procedures allocate, still count. A user's Illustrator 25.2 EPS with 71 CMYK images (used locally, not committed) used to stop with "the program uses too much memory" and come in as its screen preview; it now opens with its art. The budget itself is unchanged. Scores and estimates unchanged. |
|
||||
| 2026-11-07 | minor | EPS import: a sampled function (type 0) of two or more inputs took the nearest sample, so a function-based shading (type 1) over one came in as flat blocks; it is now interpolated between its samples (multilinearly), and with `Order 3` by cubics through them (Catmull-Rom, kept in the function's range), as the PostScript Language Reference (3rd ed., §3.10.1, type 0 functions) defines. An axial or radial shading whose function is a linearly interpolated sampled function of one input has a stop at each sample, not 33 evenly spaced ones. Tests `sampled_functions_interpolate_linearly_or_by_cubics`, `mesh_and_function_shadings_become_gradient_meshes`; a 2 × 2 function-based shading is 1.2% from Ghostscript's render (it was 45.9%); scores unchanged |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document keeps its paragraphs' tab stops (positions and alignments; leaders aren't read), at most 1,024 a paragraph and 2^20 a story, past which the default stops apply with a warning. A user's `.ai` (used locally, not committed) whose price lists came in on the default stops now lays them out as its page has them (its type 0.8% from its page, from 1.5%; what differs is hyphenation in Hungarian and type in a font that isn't installed). Scores and estimates unchanged. |
|
||||
|
||||
+1
-1
@@ -5554,7 +5554,7 @@ dictionaries (the standard ones are values in `systemdict`; `dictstack`, `intern
|
||||
intervals share storage, resources (categories made from `Generic`, `resourceforall`), executable filters (`cvx exec`
|
||||
runs their data; `flushfile` skips it), axial and radial shadings and shading patterns (gradients), images and image
|
||||
masks (data in the file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as
|
||||
point type in the font the file names (embedded font programs are skipped). In a file in Illustrator's own format
|
||||
point type in the font the file names (embedded Type 1 font programs are skipped; a Type 42 font's characters and widths come from the TrueType font it carries, and the show operators' spacing is kept as tracking; CID-keyed fonts draw the glyphs of the Type 0 and Type 2 CIDFonts the file carries). In a file in Illustrator's own format
|
||||
(Illustrator 3–8 `.ai` and their EPS, written with the prolog that defines its operators) the groups it writes (`u` …
|
||||
`U`) come in as groups, nested as they were; clips and groups nest at most 128 deep. The artboard is the `%%HiResBoundingBox`
|
||||
(else `%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know,
|
||||
|
||||
Reference in New Issue
Block a user