EPS import: CID-keyed fonts draw the glyphs of the CIDFonts the file carries

Adds composefont, the CIDInit and FontSetInit procedure sets and the CMap
operators, so EPS with CID-keyed fonts (as pdftops -level3 writes) opens.
Type 0 fonts with FMapType 9 decode their codes through the CMap's codespace
ranges and CID mappings, and the Identity CMaps are predefined.
This commit is contained in:
Don Omondi
2026-11-09 03:48:35 -05:00
parent 7a25b86a75
commit fde5a5fbef
13 changed files with 1360 additions and 36 deletions
+33
View File
@@ -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.
+3
View File
@@ -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;
+728
View File
@@ -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();
}
}
+27 -2
View File
@@ -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) {
+32 -2
View File
@@ -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,
@@ -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) {
+24
View File
@@ -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",
}
+5 -5
View File
@@ -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");
}
+367
View File
@@ -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);
}
+16
View File
@@ -197,6 +197,22 @@ fn reading_the_character_map_counts_its_mappings() {
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]
+103 -18
View File
@@ -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(())
@@ -719,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.
@@ -760,28 +764,36 @@ 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.
/// 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) || f.get(&Key::name("FMapType")).and_then(Obj::as_num) != Some(2.0) {
if font_type(&f) != Some(0.0) {
return Ok(None);
}
f.get(&Key::name("FMapType")).and_then(Obj::as_num)
};
if map == Some(9.0) {
return self.cid_chars(&font, s).map(Some);
}
if map != Some(2.0) {
return Ok(None);
}
// The scan visits every byte pair, also those that map to no glyph and so cost nothing
// further: charged by the string's length first, so a long string of unmapped characters
// shown in a loop runs out of the budget.
self.spend(s.len() as u64)?;
Ok(Self::map_composite(&font.borrow(), s))
}
/// The characters of `s` in composite font `f` (8/8 mapping), as [`Self::composite_glyphs`].
fn map_composite(f: &Dict, s: &[u8]) -> Option<Vec<Mapped>> {
let (encoding, fonts) = (f.get(&Key::name("Encoding"))?.items()?.clone(), f.get(&Key::name("FDepVector"))?.items()?.clone());
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 outer = font_matrix(&f, 1.0);
let mut out = Vec::with_capacity(s.len() / 2);
for &[pick, code] in s.as_chunks::<2>().0 {
let index = encoding.get(usize::from(pick)).and_then(Obj::as_num).filter(|i| *i >= 0.0 && i.fract() == 0.0);
@@ -789,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.
@@ -806,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);
}
@@ -828,6 +868,9 @@ 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);
}
@@ -859,6 +902,43 @@ 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)>> {
@@ -893,10 +973,12 @@ impl Interp<'_> {
/// 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.
fn type42_data(&mut self, font: &DictRef) -> Res<Option<Rc<super::type42::Font>>> {
pub(super) fn type42_data(&mut self, font: &DictRef) -> Res<Option<Rc<super::type42::Font>>> {
let sfnts = {
let f = font.borrow();
if font_type(&f) != Some(42.0) {
// 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) };
@@ -959,6 +1041,9 @@ fn type42_code(f: &Dict, data: &super::type42::Font, code: u8) -> (Option<char>,
(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;
+18 -6
View File
@@ -75,16 +75,15 @@ impl Font {
/// 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 its private-use codes (U+F000–U+F0FF), which aren't characters.
/// 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));
// 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).
let Some(c) = char::from_u32(c).filter(|c| !('\u{F000}'..='\u{F0FF}').contains(c) && !c.is_control() && *c != '\u{AD}') else {
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);
@@ -102,8 +101,7 @@ impl Font {
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))?;
let c = super::text::glyph_char(name.as_str());
(c != '\u{2022}' || name.as_str() == "bullet").then_some(c)
post_name_char(name.as_str())
});
self.names.borrow_mut().insert(gid, c);
c
@@ -116,6 +114,20 @@ impl Font {
}
}
/// 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)> {
+3 -2
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (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)
> **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); 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 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,7 @@ 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 |
+1 -1
View File
@@ -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 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). 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,