EPS import: embedded Type 1 fonts draw and space their type with their own glyphs

An embedded Type 1 font's eexec part is decrypted and its charstrings read,
so its glyphs and widths are used instead of the installed or fallback
font's. Codes are decoded through the font's Encoding, so fonts that place
glyphs at low codes no longer lose their type.
This commit is contained in:
Don Omondi
2026-11-09 03:48:35 -05:00
parent fde5a5fbef
commit 1b7c648da6
13 changed files with 926 additions and 56 deletions
+25
View File
@@ -2448,6 +2448,31 @@ proptest! {
);
survive("hostile CID-keyed fonts", || vectorcraft_eps::import(text.as_bytes()).ok().map(|r| r.document))?;
}
/// Embedded Type 1 fonts whose encrypted part is damaged or hostile (any bytes, binary or
/// hexadecimal), shown, measured and outlined: drawn, or left out.
#[test]
fn eps_type1_programs_never_panic(
part in prop::collection::vec(any::<u8>(), 0..400),
hex in any::<bool>(),
shown in prop::collection::vec(any::<u8>(), 0..12),
) {
let hexed = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
// Bytes that would end the part early are left out, as `cleartomark` ends it.
let part: Vec<u8> = part.into_iter().filter(|b| *b != b'c').collect();
let mut bytes = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\
12 dict begin /FontName /T1 def /FontType 1 def /FontMatrix [0.001 0 0 0.001 0 0] def /FontBBox [0 0 1000 1000] def\n\
/Encoding StandardEncoding def currentdict end currentfile eexec\n"
.to_vec();
if hex {
bytes.extend(hexed(&part).into_bytes());
} else {
bytes.extend(part);
}
bytes.extend(format!("\n{}\ncleartomark\n/T1 findfont 40 scalefont setfont 10 20 moveto <{}> show <{}> stringwidth pop pop\n\
10 60 moveto <{}> true charpath fill\nshowpage\n%%EOF\n", "0".repeat(512), hexed(&shown), hexed(&shown), hexed(&shown)).into_bytes());
survive("hostile Type 1 programs", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
}
}
/// The TrueType font [`type42_composite_eps`] carries.
+3
View File
@@ -45,6 +45,7 @@ mod obj;
mod preview;
mod shading;
mod text;
mod type1;
mod type42;
#[cfg(test)]
@@ -64,6 +65,8 @@ mod tests_images;
#[cfg(test)]
mod tests_native;
#[cfg(test)]
mod tests_type1;
#[cfg(test)]
mod tests_type42;
use std::collections::HashSet;
+10 -6
View File
@@ -9,7 +9,7 @@
use std::rc::Rc;
use skrifa::raw::ps::cs::{self, CharstringContext, CharstringKind, CommandSink};
use skrifa::raw::ps::cs::{CharstringContext, CharstringKind, CommandSink};
use skrifa::raw::ps::error::Error as CsError;
use skrifa::raw::types::Fixed;
use vectorcraft_geom::BezPath;
@@ -577,7 +577,10 @@ impl Interp<'_> {
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 (glyph, work) = type1_glyph(&charstring, &subrs);
// The subroutines it ran, which the evaluator limits for each glyph, not for the file.
self.spend(work)?;
let Some((outline, width)) = glyph 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;
@@ -679,12 +682,13 @@ pub(super) fn decrypt(data: &[u8], len_iv: f64) -> Vec<u8> {
}
/// 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)> {
/// subroutines `subrs`, or `None` when it can't be run; and the bytes of the charstrings it
/// entered (see [`super::type1::evaluate`]).
pub(super) fn type1_glyph(data: &[u8], subrs: &[Vec<u8>]) -> (Option<(BezPath, f64)>, u64) {
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()))))
let (w, work) = super::type1::evaluate(&ctx, data, &mut sink);
(w.ok().map(|w| (sink.0, w.map_or(0.0, |w| f64::from(w.to_f32())))), work)
}
/// What a Type 1 charstring runs with: its font's subroutines (no `seac` accents: a CIDFont's
+25 -7
View File
@@ -504,16 +504,18 @@ impl Interp<'_> {
Ok(out)
}
/// `file eexec`: an encrypted font program follows in `file`. It is skipped there, up to the
/// `cleartomark` that ends it (type is drawn in the app's fonts, by name): the font
/// dictionary it would complete is defined as it is.
/// `file eexec`: an encrypted font program follows in `file`. It isn't run: it is skipped
/// there, up to the `cleartomark` that ends it, and the font dictionary it would complete is
/// defined as it is, with the glyphs the Type 1 font parser reads from it (see `type1`).
fn eexec(&mut self) -> Res {
let src = self.pop()?;
let mut font = None;
if let Some(Obj::Dict(d)) = self.stack.last().cloned() {
let name = d.borrow().get(&Key::name("FontName")).cloned();
if let Some(k) = name.and_then(|n| n.key()) {
self.pop()?;
self.fonts.borrow_mut().insert(k, Obj::Dict(d));
self.fonts.borrow_mut().insert(k, Obj::Dict(d.clone()));
font = Some(d);
}
}
// `currentfile eexec`: the program's own file or the file being run. A string operand
@@ -524,15 +526,31 @@ impl Interp<'_> {
};
self.materialize(&f)?;
let missing = || PsError::Ps("ioerror", "eexec".into());
match &mut *f.borrow_mut() {
// The encrypted part, for the font's glyphs (a font's, of a size that can be read).
// Copied only when the font can still be kept (it is dropped otherwise).
let room = font.is_some() && self.type1_room();
let keep = |part: &[u8]| (room && part.len() <= super::super::type1::MAX_PROGRAM).then(|| part.to_vec());
let (encrypted, scanned) = match &mut *f.borrow_mut() {
Stream::Buf { data, pos, .. } => {
let at = find(data.get(*pos..).unwrap_or_default(), b"cleartomark").ok_or_else(missing)?;
let rest = data.get(*pos..).unwrap_or_default();
let at = find(rest, b"cleartomark").ok_or_else(missing)?;
let part = keep(rest.get(..at).unwrap_or_default());
*pos += at;
(part, at)
}
_ => {
let at = find(self.lex.data(), b"cleartomark").ok_or_else(missing)?;
let rest = self.lex.data();
let at = find(rest, b"cleartomark").ok_or_else(missing)?;
let part = keep(rest.get(..at).unwrap_or_default());
self.lex.advance(at);
(part, at)
}
};
// The search for `cleartomark` read the part: charged by its length (as `search` is), so `eexec` on a
// large reusable file in a loop runs out of the budget.
self.spend((scanned / 64) as u64)?;
if let (Some(d), Some(part)) = (font, encrypted) {
self.attach_type1(&d, &part)?;
}
self.push(Obj::Mark)
}
+19
View File
@@ -82,6 +82,9 @@ pub(crate) struct Interp<'a> {
/// 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 Type 1 fonts read from the encrypted part of their programs, by the `CharStrings`
/// dictionary given to their font dictionaries (see `type1`).
pub type1: Vec<(DictRef, Rc<super::type1::Font>)>,
/// 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.
@@ -222,6 +225,7 @@ impl<'a> Interp<'a> {
glyph_width: None,
type42: vec![],
cmaps: vec![],
type1: vec![],
cid_subrs: vec![],
cmap_font: 0,
type42_counted: HashSet::new(),
@@ -1461,6 +1465,21 @@ impl<'a> Interp<'a> {
})
}
/// Is `o` the standard encoding (`Some(false)`) or the ISO Latin-1 encoding (`Some(true)`)
/// of systemdict, whose names here are all `.notdef`?
pub fn standard_encoding(&self, o: &Obj) -> Option<bool> {
let items = o.items()?;
let s = self.system.borrow();
let is = |k: &str| s.get(&Key::name(k)).and_then(Obj::items).is_some_and(|e| e.same(items));
if is("StandardEncoding") {
Some(false)
} else if is("ISOLatin1Encoding") {
Some(true)
} else {
None
}
}
/// The instances of `category`: `FontDirectory` for fonts.
pub fn instances(&mut self, category: &Obj) -> Res<DictRef> {
if category.text().as_deref() == Some("Font") { Ok(self.fonts.clone()) } else { self.category(category) }
+19
View File
@@ -350,6 +350,25 @@ fn one_long_show_through_a_large_cmap_runs_out_of_the_budget() {
);
}
/// The evaluator limits each glyph, not the file: measuring a glyph whose subroutines (1 to 8
/// each calling the next four times) run a quarter of a million operations in a loop ends within
/// the operation budget.
#[test]
fn cid_glyphs_measured_in_a_loop_run_out_of_the_budget() {
let font = String::from_utf8(cid_font(true)).unwrap();
let at = font.find("/Subrs [<").unwrap() + "/Subrs [".len();
let end = font[at..].find(']').unwrap() + at;
let mut subrs = vec![encrypt(&cs(&[(&[1], 10), (&[0, 0], 21), (&[500, 0], 5), (&[-250, 400], 5), (&[], 9), (&[], 11)]))];
for k in 1..=8 {
subrs.push(encrypt(&cs(&[(&[k + 1], 10), (&[k + 1], 10), (&[k + 1], 10), (&[k + 1], 10), (&[], 11)])));
}
subrs.push(encrypt(&cs(&[(&[], 11)])));
let list: String = subrs.iter().map(|s| format!("<{}> ", hex(s))).collect();
let font = format!("{}{list}{}", &font[..at], &font[end..]);
check(&format!("{font}{CMAP} /TestFont 50 selectfont (B) stringwidth pop 25 sub abs 0.01 lt"));
super::tests::runs_out_of_budget(&format!("{font}{CMAP} /TestFont 50 selectfont {{ (B) stringwidth pop pop }} loop"));
}
/// `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.
+235
View File
@@ -0,0 +1,235 @@
//! Type 1 fonts embedded in the file: their encrypted part (`eexec`) is read for their glyphs
//! (Adobe Type 1 Font Format, chapters 6 and 7), so their type keeps the font's own widths, and
//! glyphs that stand for no character are drawn from their outlines. The font here is written
//! from scratch: two square and triangle glyphs in hand-made charstrings.
use vectorcraft_doc::{NodeKind, TextObject};
use super::tests::{all, bounds, check, read};
use crate::import::import;
/// A Type 1 charstring number (§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 (`hsbw` 13, `rmoveto` 21, `rlineto` 5, `closepath` 9, `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 key `r` after 4 leading bytes (§7.1: 55665 for the encrypted part,
/// 4330 for charstrings).
fn encrypt(plain: &[u8], mut r: u16) -> Vec<u8> {
[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()
}
/// Font `/TestOne`: A a square (100 to 500 across, 500 high) 600 wide, B a triangle (500
/// across, 400 high) 500 wide, and the triangle again under the name `g1`, which says nothing,
/// at code 67 (C). Its encoding is `encoding` (a PostScript expression); its encrypted part
/// written in hexadecimal when `hex`.
fn font(encoding: &str, hex: bool) -> Vec<u8> {
font_with(encoding, hex, &[], None)
}
/// [`font`] with subroutines `subrs` and, when given, charstring `a` for A.
fn font_with(encoding: &str, hex: bool, subrs: &[Vec<u8>], a: Option<Vec<u8>>) -> Vec<u8> {
let square = a.unwrap_or_else(|| cs(&[(&[0, 600], 13), (&[100, 0], 21), (&[400, 0], 5), (&[0, 500], 5), (&[-400, 0], 5), (&[], 9), (&[], 14)]));
let triangle = cs(&[(&[0, 500], 13), (&[0, 0], 21), (&[500, 0], 5), (&[-250, 400], 5), (&[], 9), (&[], 14)]);
let notdef = cs(&[(&[0, 250], 13), (&[], 14)]);
let mut private = b"dup /Private 8 dict dup begin\n/RD {string currentfile exch readstring pop} executeonly def\n\
/ND {noaccess def} executeonly def\n/NP {noaccess put} executeonly def\n/lenIV 4 def\n/password 5839 def\n\
/MinFeature {16 16} def\n"
.to_vec();
private.extend(format!("/Subrs {} array\n", subrs.len()).into_bytes());
for (i, subr) in subrs.iter().enumerate() {
let e = encrypt(subr, 4330);
private.extend(format!("dup {i} {} RD ", e.len()).into_bytes());
private.extend(e);
private.extend(b" NP\n");
}
private.extend(b"readonly def\n2 index /CharStrings 4 dict dup begin\n");
for (name, glyph) in [(".notdef", &notdef), ("A", &square), ("B", &triangle), ("g1", &triangle)] {
let e = encrypt(glyph, 4330);
private.extend(format!("/{name} {} RD ", e.len()).into_bytes());
private.extend(e);
private.extend(b" ND\n");
}
private.extend(b"end\nend\nreadonly put\nnoaccess put\ndup /FontName get exch definefont pop\nmark currentfile closefile\n");
let encrypted = encrypt(&private, 55665);
let mut out = format!(
"%!FontType1-1.0: TestOne 001\n12 dict begin\n/FontName /TestOne def /FontType 1 def /PaintType 0 def\n\
/FontMatrix [0.001 0 0 0.001 0 0] readonly def /FontBBox {{0 0 1000 1000}} readonly def\n\
/Encoding {encoding} def\ncurrentdict end\ncurrentfile eexec\n"
)
.into_bytes();
if hex {
for line in encrypted.chunks(32) {
out.extend(line.iter().map(|b| format!("{b:02x}")).collect::<String>().into_bytes());
out.push(b'\n');
}
} else {
out.extend(encrypted);
out.push(b'\n');
}
for _ in 0..8 {
out.extend(b"0".repeat(64));
out.push(b'\n');
}
out.extend(b"cleartomark\n");
out
}
/// The font's own encoding: A, B and `g1` at codes 65 to 67.
const OWN: &str = "256 array 0 1 255 {1 index exch /.notdef put} for dup 65 /A put dup 66 /B put dup 67 /g1 put readonly";
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()
}
fn program(encoding: &str, hex: bool, body: &str) -> crate::Imported {
let head = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n".to_vec();
import(&[head, font(encoding, hex), format!("{body}\nshowpage\n%%EOF\n").into_bytes()].concat()).unwrap()
}
#[test]
fn embedded_type1_type_keeps_the_fonts_widths() {
for hex in [true, false] {
let r = program(OWN, hex, "/TestOne findfont 50 scalefont setfont 10 20 moveto (AB) show");
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
let t = texts(&r);
assert_eq!(t.len(), 1, "{:?}", all(&r.document));
assert_eq!(t[0].plain_text(), "AB");
assert_eq!(t[0].runs[0].style.font_family, "Test One");
// B starts where A's 600 units end, whatever the font laid out.
let o: Vec<f64> = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t[0]).glyphs.iter().map(|g| g.origin.x).collect();
assert!((o[1] - o[0] - 30.0).abs() < 0.05, "{o:?}");
}
}
#[test]
fn embedded_type1_fonts_measure_and_outline_their_glyphs() {
let fonts = String::from_utf8(font(OWN, true)).unwrap();
check(&format!("{fonts} /TestOne findfont 50 scalefont setfont (AB) stringwidth pop 55 sub abs 0.01 lt"));
check(&format!(
"{fonts} /TestOne findfont 50 scalefont setfont newpath 10 20 moveto (A) true charpath pathbbox 45 sub abs 0.01 lt \
exch 35 sub abs 0.01 lt and exch 20 sub abs 0.01 lt and exch 15 sub abs 0.01 lt and"
));
check(&format!("{fonts} /TestOne findfont /CharStrings get /A known"));
}
/// A glyph whose name says no character (`g1`) is drawn from the font.
#[test]
fn embedded_type1_glyphs_without_a_character_are_drawn() {
let r = program(OWN, true, "/TestOne findfont 50 scalefont setfont 10 20 moveto (C) show");
assert!(texts(&r).is_empty());
let paths: Vec<_> = all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).map(|n| bounds(n)).collect();
assert_eq!(paths.len(), 1);
assert!((paths[0].x0 - 10.0).abs() < 0.01 && (paths[0].width() - 25.0).abs() < 0.01 && (paths[0].height() - 20.0).abs() < 0.01, "{paths:?}");
}
/// A font in the standard encoding takes the standard names of its codes (PLRM 3rd ed.,
/// Appendix E.5), so its glyphs and widths are found.
#[test]
fn standard_encoding_fonts_find_their_glyphs() {
let fonts = String::from_utf8(font("StandardEncoding", true)).unwrap();
check(&format!("{fonts} /TestOne findfont 50 scalefont setfont (AB) stringwidth pop 55 sub abs 0.01 lt"));
let r = read(&format!("{fonts} /TestOne findfont 50 scalefont setfont 10 20 moveto (AB) show"));
assert_eq!(texts(&r)[0].plain_text(), "AB");
}
/// A font found by name (its program not in the file), re-encoded with glyph names at other
/// codes, sets the characters its names say, not its codes read as Latin-1.
#[test]
fn named_fonts_set_the_characters_their_glyph_names_say() {
let r = read(
"/Helvetica findfont dup length dict begin { 1 index /FID ne { def } { pop pop } ifelse } forall\n\
/Encoding 256 array 0 1 255 { 1 index exch /.notdef put } for dup 1 /H put dup 2 /i put dup 3 /eacute put def\n\
currentdict end /Helv-Re exch definefont pop /Helv-Re 20 selectfont 10 20 moveto (\\001\\002\\003) show",
);
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<String>(), "Hié");
}
/// A font whose encoding is a copy of `StandardEncoding` (a different array, so not known as the
/// standard encoding) still finds each code's glyph by its character's name, not `.notdef`.
#[test]
fn copied_standard_encodings_find_their_glyphs() {
let fonts = String::from_utf8(font("StandardEncoding 256 array copy", true)).unwrap();
check(&format!("{fonts} /TestOne findfont 50 scalefont setfont (AB) stringwidth pop 55 sub abs 0.01 lt"));
let r = read(&format!("{fonts} /TestOne findfont 50 scalefont setfont 10 20 moveto (AB) show"));
assert_eq!(texts(&r)[0].plain_text(), "AB");
}
/// Subroutines 1 to 8 each calling the one below four times (`callsubr` 10, `return` 11), and
/// A a square drawn after calling subroutine 8: a few bytes that run some 260,000 operations.
fn heavy_font() -> String {
let mut subrs = vec![cs(&[(&[], 11)])];
for k in 1..=8 {
subrs.push(cs(&[(&[k - 1], 10), (&[k - 1], 10), (&[k - 1], 10), (&[k - 1], 10), (&[], 11)]));
}
let a = cs(&[(&[0, 600], 13), (&[8], 10), (&[100, 0], 21), (&[400, 0], 5), (&[0, 500], 5), (&[-400, 0], 5), (&[], 9), (&[], 14)]);
String::from_utf8(font_with(OWN, true, &subrs, Some(a))).unwrap()
}
/// Running a glyph's charstrings counts the bytes of every subroutine call, for the operation
/// budget; and the glyph still draws.
#[test]
fn running_a_type1_glyph_counts_its_subroutine_calls() {
let program = heavy_font();
let at = program.find("eexec\n").unwrap() + "eexec\n".len();
let font = super::type1::parse(&program.as_bytes()[at..]).unwrap();
let gid = font.font.glyph_names().find(|(_, n)| *n == "A").unwrap().0.to_u32();
let font = super::type1::Embedded::Type1(std::rc::Rc::new(font));
let (outline, w) = font.glyph(gid).unwrap();
assert!((w - 0.6).abs() < 1e-9 && !outline.elements().is_empty(), "{w}");
let work = font.take_work();
assert!(work > 4u64.pow(8), "{work}");
assert_eq!(font.take_work(), 0);
check(&format!("{program} /TestOne findfont 50 scalefont setfont (A) stringwidth pop 30 sub abs 0.01 lt"));
}
/// The evaluator limits each glyph, not the file: measuring a glyph whose subroutines run a
/// quarter of a million operations in a loop ends within the operation budget.
#[test]
fn type1_glyphs_measured_in_a_loop_run_out_of_the_budget() {
super::tests::runs_out_of_budget(&format!("{} /TestOne findfont 50 scalefont setfont {{ (A) stringwidth pop pop }} loop", heavy_font()));
}
/// One long string of a costly glyph pays for each glyph as it is measured: a million copies of
/// the glyph above stop at the operation budget after a few hundred, instead of all running first.
#[test]
fn a_long_string_of_type1_glyphs_runs_out_of_the_budget() {
super::tests::runs_out_of_budget(&format!(
"{} /TestOne findfont 50 scalefont setfont /s 1000000 string def 0 1 999999 {{ s exch 65 put }} for s stringwidth pop pop",
heavy_font()
));
}
/// `eexec`'s search for `cleartomark` reads the encrypted part: on a large reusable file in a
/// loop it runs out of the operation budget instead of reading megabytes a pass for free.
#[test]
fn eexec_on_a_large_file_in_a_loop_runs_out_of_the_budget() {
super::tests::runs_out_of_budget(
"/s 4000000 string def s 3999980 (cleartomark) putinterval /f s /ReusableStreamDecode filter def\n\
{ f resetfile f eexec cleartomark } loop",
);
}
+148 -39
View File
@@ -14,6 +14,7 @@ use vectorcraft_text::TextLayout;
use super::graphics::MAX_APART;
use super::interp::{Interp, matrix_obj, matrix_of};
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, ps_err};
use super::type1::Embedded;
/// The font a program uses before it sets one.
const DEFAULT_FONT: &str = "Helvetica";
@@ -430,10 +431,33 @@ impl Interp<'_> {
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();
let text: String = self.named_chars(s).into_iter().filter(|c| !c.is_control()).collect();
self.set_text(&text, at)
}
/// The character each code of `s` stands for in the current font (one found by name, whose
/// program isn't in the file): the one its glyph name says (the standard encodings by their
/// codes' names), else the code read as Latin-1.
fn named_chars(&self, s: &[u8]) -> Vec<char> {
let Some(font) = self.g.font.clone() else { return s.iter().map(|b| char::from(*b)).collect() };
let f = font.borrow();
let standard = f.get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
s.iter()
.map(|&code| {
let mut name = glyph_name(&f, code);
if &*name == ".notdef"
&& let Some(iso) = standard
{
name = Rc::from(standard_name(code, iso));
}
match &*name {
".notdef" => char::from(code),
n => named_char(code, n).unwrap_or(char::from(code)),
}
})
.collect()
}
/// [`Self::set_type`] for the characters `text`.
fn set_text(&mut self, text: &str, at: Point) -> Res<(TextObject, TextLayout, Vec2)> {
// Laying out type costs about as much as 300 operations, and 64 more a character
@@ -489,7 +513,7 @@ impl Interp<'_> {
// 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(),
None => self.named_chars(s).into_iter().map(Some).collect(),
}
.into_iter()
.map(|c| c.filter(|c| !c.is_control()))
@@ -602,7 +626,7 @@ impl Interp<'_> {
/// `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 {
fn show_type42_outlines(&mut self, data: &Embedded, 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());
@@ -617,7 +641,9 @@ impl Interp<'_> {
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) {
let glyph = data.glyph(g.gid);
self.spend(data.take_work())?;
if let Some(outline) = glyph.map(|o| o.0) {
self.type42_paint(outline, place, f1, Draw::Fill)?;
}
text.push(g.ch.unwrap_or('\u{2022}'));
@@ -660,7 +686,9 @@ impl Interp<'_> {
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) {
let glyph = data.glyph(g.gid);
self.spend(data.take_work())?;
if let Some((outline, _)) = glyph {
self.type42_paint(outline, pen, f1, Draw::Path)?;
}
pen += f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO;
@@ -874,7 +902,12 @@ impl Interp<'_> {
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);
// An em of the descendant's glyphs: a Type 42 font's matrix maps ems, a Type 1 font's
// its 1000-unit glyph space.
let fm = {
let f = g.font.borrow();
if units(&f) == 1.0 { g.outer * font_matrix(&f, 1.0) } else { g.outer * font_matrix(&f, 0.001) * Affine::scale(units(&f)) }
};
if let Some((outline, advance)) = self.type42_glyph(&g.font, g.code)? {
self.type42_paint(outline, at, fm, paint)?;
return Ok(fm * Point::new(advance, 0.0) - fm * Point::ZERO);
@@ -939,38 +972,80 @@ impl Interp<'_> {
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.
/// Glyph `code` of font `font` from the font program it carries (a Type 42 or Type 1 font):
/// its outline and width in ems, or `None` when it carries none.
fn type42_glyph(&mut self, font: &DictRef, code: u8) -> Res<Option<(vectorcraft_geom::BezPath, f64)>> {
let Some(data) = self.type42_data(font)? else { return Ok(None) };
let gid = {
let f = font.borrow();
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))
let Some(data) = self.embedded(font)? else { return Ok(None) };
let standard = font.borrow().get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
let (_, gid) = type42_code(&font.borrow(), &data, code, standard);
// Reading the font's character map (once) visits each of its mappings; running a Type 1
// glyph's charstrings is paid for by their bytes.
self.spend(data.take_work())?;
self.spend(64)?;
let glyph = data.glyph(gid);
self.spend(data.take_work())?;
Ok(glyph)
}
/// The codes of `s` in the current font when it is a 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>)>> {
/// The codes of `s` in the current font when it is a font that carries its font program (a
/// Type 42 font's TrueType font, a Type 1 font's encrypted part), not a descendant of a
/// composite font: the font program, and each code's glyph.
fn type42_plain(&mut self, s: &[u8]) -> Res<Option<(Embedded, Vec<Plain>)>> {
let Some(font) = self.g.font.clone() else { return Ok(None) };
let Some(data) = self.type42_data(&font)? else { return Ok(None) };
self.spend((s.len() as u64).saturating_mul(4))?;
let Some(data) = self.embedded(&font)? else { return Ok(None) };
self.spend((s.len() as u64).saturating_mul(16))?;
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();
let standard = f.get(&Key::name("Encoding")).and_then(|e| self.standard_encoding(e));
let mut glyphs = Vec::with_capacity(s.len());
for &code in s {
let (ch, gid) = type42_code(&f, &data, code, standard);
let width = data.advance(gid);
// Reading the font's character map (once) visits each of its mappings; measuring a
// Type 1 glyph runs its charstrings. Paid for code by code, so that a long string of
// a costly glyph stops at the budget instead of running every glyph first.
self.spend(data.take_work())?;
glyphs.push(Plain { ch, gid, width });
}
drop(f);
// Reading the font's character map (once) visits each of its mappings.
self.spend(data.take_char_map_work())?;
Ok(Some((data, glyphs)))
}
/// The font program font `font` carries: a Type 42 font's TrueType font, or the Type 1 font
/// read from its encrypted part (see `type1`).
fn embedded(&mut self, font: &DictRef) -> Res<Option<Embedded>> {
if let Some(d) = self.type42_data(font)? {
return Ok(Some(Embedded::TrueType(d)));
}
let strings = match font.borrow().get(&Key::name("CharStrings")) {
Some(Obj::Dict(d)) if font_type(&font.borrow()) == Some(1.0) => d.clone(),
_ => return Ok(None),
};
Ok(self.type1.iter().find(|(k, _)| k.same(&strings)).map(|(_, f)| Embedded::Type1(f.clone())))
}
/// Whether another embedded Type 1 font can still be kept (see [`Self::attach_type1`]).
pub(super) fn type1_room(&self) -> bool {
self.type1.len() < MAX_TYPE1
}
/// Give font dictionary `d` the glyphs of the Type 1 font whose encrypted part is
/// `encrypted`: a `CharStrings` dictionary naming each glyph's index, by which it is drawn.
pub(super) fn attach_type1(&mut self, d: &DictRef, encrypted: &[u8]) -> Res {
if self.type1.len() >= MAX_TYPE1 {
return Ok(());
}
self.alloc(encrypted.len())?;
self.spend((encrypted.len() / 256) as u64)?;
let Some(font) = super::type1::parse(encrypted) else { return Ok(()) };
// Its charstrings are kept a second time, to be run here.
self.alloc(font.size())?;
let strings: Dict = font.font.glyph_names().map(|(g, n)| (Key::name(n), Obj::Int(i64::from(g.to_u32())))).collect();
let strings = DictRef::new(strings);
d.borrow_mut().insert(Key::name("CharStrings"), Obj::Dict(strings.clone()));
self.type1.push((strings, Rc::new(font)));
Ok(())
}
/// The TrueType font Type 42 font `font` carries (read once for each `sfnts` array), or
/// `None` when it isn't a Type 42 font with one.
pub(super) fn type42_data(&mut self, font: &DictRef) -> Res<Option<Rc<super::type42::Font>>> {
@@ -1011,8 +1086,8 @@ impl Interp<'_> {
}
}
/// 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).
/// A character code of a font carrying its program: the character it stands for (`None` when
/// neither its glyph name nor the font says), its glyph and the glyph's width (ems).
struct Plain {
ch: Option<char>,
gid: u32,
@@ -1025,22 +1100,56 @@ fn char_string(f: &Dict, name: &str) -> Option<u32> {
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
/// The character and glyph of code `code` in font `f`, carrying font program `data`: by the
/// glyph name its `Encoding` gives it (a name such as `A`, `uni20AC` or `Euro` says the
/// character; else the glyph `CharStrings` gives it stands for one in the font's character map
/// or `post` names). 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);
/// or `post` names). The standard encodings (`standard`: whether ISO Latin-1's) have no names
/// here: the code's name in them is used (PLRM 3rd ed., Appendix E.5 and E.6). A code the
/// encoding leaves `.notdef` is read as Latin-1, drawn by that character's glyph.
fn type42_code(f: &Dict, data: &Embedded, code: u8, standard: Option<bool>) -> (Option<char>, u32) {
let mut name = glyph_name(f, code);
if &*name == ".notdef"
&& let Some(iso) = standard
{
name = Rc::from(standard_name(code, iso));
}
if &*name == ".notdef" {
let c = char::from(code);
return (Some(c), data.gid_of(c).or_else(|| char_string(f, ".notdef")).unwrap_or(0));
// A font without a character map (an embedded Type 1 font) has that character's glyph
// by its name. A copy of a standard encoding (`StandardEncoding 256 array copy`) comes
// here too: its names are the empty systemdict array's.
let latin = match code {
0x27 => "quotesingle",
0x2d => "hyphen",
0x60 => "grave",
_ => standard_name(code, true),
};
return (Some(c), data.gid_of(c).or_else(|| char_string(f, latin)).or_else(|| char_string(f, ".notdef")).unwrap_or(0));
}
let named = named_char(code, &name);
let gid = char_string(f, &name).or_else(|| named.and_then(|c| data.gid_of(c))).or_else(|| char_string(f, ".notdef")).unwrap_or(0);
(named.or_else(|| data.char_of(gid)), gid)
}
/// The name the standard encoding (or, when `iso`, the ISO Latin-1 encoding) gives code `code`:
/// the same names in the printable ASCII range but for ISO Latin-1's `minus`; ISO Latin-1's
/// upper half named as Windows-1252 names it, with its soft `hyphen`.
fn standard_name(code: u8, iso: bool) -> &'static str {
use skrifa::raw::ps::encoding::PredefinedEncoding;
match (iso, code) {
(false, _) => PredefinedEncoding::Standard.name(code),
(true, 0x2d) => "minus",
(true, 0x20..=0x7e) => PredefinedEncoding::Standard.name(code),
(true, 0xad) => "hyphen",
(true, 0xa0) => "space",
(true, 0xa1..=0xff) => crate::latin::high_name(code).unwrap_or(".notdef"),
_ => ".notdef",
}
}
/// Most Type 1 fonts read from their encrypted parts.
const MAX_TYPE1: usize = 256;
/// The warning for CID-keyed type in a CIDFont the file doesn't carry.
const MISSING_CID_FONT: &str = "CID-keyed type in a font the file doesn't carry was left out";
@@ -1064,7 +1173,7 @@ fn glyph_name(f: &Dict, code: u8) -> Rc<str> {
/// 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> {
pub(super) fn named_char(code: u8, name: &str) -> Option<char> {
match glyph_char(name) {
'\u{2022}' if name != "bullet" => {}
c => return Some(c),
+432
View File
@@ -0,0 +1,432 @@
//! Embedded fonts whose glyphs are drawn from the font program the file carries: Type 1 fonts,
//! whose encrypted part (`eexec`: Adobe Type 1 Font Format, chapter 7) holds their charstrings,
//! and Type 42 fonts (see `type42`). The interpreter doesn't run the encrypted part; it is read
//! by the Type 1 font parser, and the font dictionary gets the `CharStrings` dictionary the part
//! would have given it, each glyph name standing for its glyph's index, by which the glyph is
//! drawn. Its charstrings are run here, counting the bytes of the charstrings and subroutines
//! they enter, so that the work of drawing glyphs is paid for from the operation budget.
use std::cell::Cell;
use std::rc::Rc;
use skrifa::GlyphId;
use skrifa::outline::OutlinePen;
use skrifa::raw::ps::cs::{self, CharstringContext, CharstringKind, CommandSink, NopFilterSink, TransformSink};
use skrifa::raw::ps::error::Error as CsError;
use skrifa::raw::ps::type1::Type1Font;
use skrifa::raw::types::Fixed;
use vectorcraft_geom::BezPath;
use super::type42;
/// Most bytes of an encrypted font program read.
pub(super) const MAX_PROGRAM: usize = 16 << 20;
/// The font program a plain or descendant font carries.
#[derive(Clone)]
pub(super) enum Embedded {
TrueType(Rc<type42::Font>),
Type1(Rc<Font>),
}
impl Embedded {
/// Glyph `gid`: its outline and advance width, in ems (y up), or `None` when it can't be read.
pub fn glyph(&self, gid: u32) -> Option<(BezPath, f64)> {
match self {
Self::TrueType(f) => type42::glyph(&f.data, gid),
Self::Type1(f) => {
let mut pen = type42::Pen(BezPath::new(), 1.0 / UNITS);
// A glyph that can't be drawn draws nothing, and has no width.
let w = f.draw(gid, &mut pen).map_or(0.0, |w| w / UNITS);
Some((pen.0, if w.is_finite() { w } else { 0.0 }))
}
}
}
/// Glyph `gid`'s advance width, in ems.
pub fn advance(&self, gid: u32) -> f64 {
match self {
Self::TrueType(f) => type42::advance(&f.data, gid),
Self::Type1(_) => self.glyph(gid).map_or(0.0, |g| g.1),
}
}
/// The character glyph `gid` stands for, when the font says (a TrueType font's character
/// map or `post` names; a Type 1 glyph's name).
pub fn char_of(&self, gid: u32) -> Option<char> {
match self {
Self::TrueType(f) => f.char_of(gid),
Self::Type1(f) => f.font.glyph_name(GlyphId::new(gid)).and_then(|n| super::text::named_char(0, n)),
}
}
/// The work done since last asked, for the operation budget: a TrueType font's reading its
/// character map (see [`type42::Font::take_char_map_work`]), a Type 1 font's running its
/// charstrings (see [`Font::draw`]).
pub fn take_work(&self) -> u64 {
match self {
Self::TrueType(f) => f.take_char_map_work(),
Self::Type1(f) => f.work.take(),
}
}
/// The glyph a TrueType font's character map gives `c` (Type 1 fonts have none).
pub fn gid_of(&self, c: char) -> Option<u32> {
match self {
Self::TrueType(f) => f.gid_of(c),
Self::Type1(_) => None,
}
}
}
/// Type 1 glyph space: 1000 units an em, as the font matrix of the program made for parsing says.
const UNITS: f64 = 1000.0;
/// A Type 1 font read from its encrypted part: the font, and its charstrings as the font parser
/// reads them, run here so their work is counted.
pub(super) struct Font {
pub font: Type1Font,
/// Each glyph's name and decrypted charstring, by glyph index.
charstrings: Vec<(Vec<u8>, Vec<u8>)>,
/// The bytes of charstrings and subroutines run since last asked (see [`Embedded::take_work`]).
work: Cell<u64>,
}
impl Font {
/// The bytes the font keeps.
pub fn size(&self) -> usize {
self.charstrings.iter().fold(0usize, |n, (a, b)| n.saturating_add(a.len()).saturating_add(b.len()))
}
/// Draw glyph `gid` with `pen` (in 1000-unit glyph space, as the font parser draws it): its
/// advance width, or `None` when it can't be run. The bytes of each charstring it enters
/// (its own, its subroutines', `seac` accents') are added to the work done.
fn draw(&self, gid: u32, pen: &mut impl OutlinePen) -> Option<f64> {
let (name, charstring) = self.charstrings.get(gid as usize)?;
if self.font.glyph_name(GlyphId::new(gid)).map(str::as_bytes) != Some(name.as_slice()) {
return None;
}
let transform = self.font.transform(None);
let mut filter = NopFilterSink::new(pen);
let mut sink = TransformSink::new(&mut filter, transform);
let (w, work) = evaluate(&self.font, charstring, &mut sink);
self.work.set(self.work.get().saturating_add(work));
let w = w.ok()??;
Some(f64::from(transform.transform_h_metric(w).to_f32().max(0.0)))
}
}
/// Type 1 charstring `data` run in context `ctx` into `sink`: its width (when it gives one) and
/// the bytes of the charstrings it entered, at least one for each, which bound the operations it
/// ran. The evaluator stops a charstring after a million operations but starts again for every
/// glyph, so drawing many glyphs is paid for with these.
pub(super) fn evaluate(ctx: &impl CharstringContext, data: &[u8], sink: &mut impl CommandSink) -> (Result<Option<Fixed>, CsError>, u64) {
let counted = Counted { ctx, work: Cell::new(data.len() as u64 + 1) };
let w = cs::evaluate(&counted, None, data, sink);
(w, counted.work.get())
}
/// A charstring context counting the bytes of the charstrings it gives the evaluator.
struct Counted<'a, C> {
ctx: &'a C,
work: Cell<u64>,
}
impl<C> Counted<'_, C> {
fn add<'b>(&self, s: &'b [u8]) -> &'b [u8] {
self.work.set(self.work.get().saturating_add(s.len() as u64 + 1));
s
}
}
impl<C: CharstringContext> CharstringContext for Counted<'_, C> {
fn kind(&self) -> CharstringKind {
self.ctx.kind()
}
fn seac_components(&self, base: i32, accent: i32) -> Result<[&[u8]; 2], CsError> {
let [b, a] = self.ctx.seac_components(base, accent)?;
Ok([self.add(b), self.add(a)])
}
fn global_subr(&self, index: i32) -> Result<&[u8], CsError> {
self.ctx.global_subr(index).map(|s| self.add(s))
}
fn subr(&self, index: i32) -> Result<&[u8], CsError> {
self.ctx.subr(index).map(|s| self.add(s))
}
fn weight_vector(&self) -> &[Fixed] {
self.ctx.weight_vector()
}
}
/// The Type 1 font whose encrypted part is `encrypted` (from just after `eexec`, binary or
/// hexadecimal), or `None` when it can't be read. The part is read on its own, after a clear
/// part that gives only the font matrix of 1000-unit glyph space: the font's own clear part, run
/// by the interpreter, gives its matrix and encoding.
pub(super) fn parse(encrypted: &[u8]) -> Option<Font> {
if encrypted.len() > MAX_PROGRAM {
return None;
}
let mut program = b"%!FontType1-1.0: Embedded\n/FontMatrix [0.001 0 0 0.001 0 0] readonly def\ncurrentfile eexec\n".to_vec();
program.extend_from_slice(encrypted);
let font = Type1Font::new(&program).ok()?;
Some(Font { font, charstrings: charstrings(encrypted), work: Cell::new(0) })
}
/// The `.notdef` glyph of a font that has none: `0 333 hsbw endchar` (Adobe Type 1 Font Format,
/// §6), as the font parser gives it.
const NOTDEF: &[u8] = &[139, 247, 225, 13, 14];
/// The charstrings of encrypted part `encrypted`, with their names, in the order the font parser
/// numbers them: the first `CharStrings` dictionary's entries (`/name len RD bytes ND`) in the
/// order written, decrypted with the charstring key unless `lenIV` is negative (Adobe Type 1 Font
/// Format, §2.4 and §7), `.notdef` (made when missing) swapped to the front. Empty when the part
/// can't be read.
fn charstrings(encrypted: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
let skip = encrypted.iter().take_while(|c| matches!(c, b' ' | b'\t' | b'\r' | b'\n')).count();
let encrypted = encrypted.get(skip..).unwrap_or_default();
let hex = encrypted.len() > 3 && encrypted.iter().take(4).all(u8::is_ascii_hexdigit);
let bytes = if hex { unhex(encrypted) } else { encrypted.to_vec() };
let plain: Vec<u8> = decrypt(&bytes, 55665).into_iter().skip(4).collect();
let mut lex = Lexer { d: &plain, pos: 0 };
let mut len_iv = 4;
let mut out = Vec::new();
while let Some(t) = lex.next() {
match t {
Tok::Name(b"lenIV") => match lex.next() {
Some(Tok::Int(n)) => len_iv = n,
_ => return vec![],
},
Tok::Name(b"CharStrings") => {
let Some(Tok::Int(_)) = lex.next() else { return vec![] };
while let Some(t) = lex.next() {
let ends = lex.peek().is_some_and(special_or_space);
match t {
Tok::Other(b"end") if ends => break,
Tok::Other(b"def") if ends && !out.is_empty() => break,
Tok::Name(name) => {
let Some(Tok::Blob(data)) = lex.next() else { return vec![] };
let data = if len_iv >= 0 { decrypt(data, 4330).into_iter().skip(len_iv as usize).collect() } else { data.to_vec() };
out.push((name.to_vec(), data));
}
_ => {}
}
}
break;
}
_ => {}
}
}
if out.is_empty() {
return out;
}
let notdef = match out.iter().position(|(n, _)| n == b".notdef") {
Some(i) => i,
None => {
out.push((b".notdef".to_vec(), NOTDEF.to_vec()));
out.len() - 1
}
};
out.swap(0, notdef);
out
}
/// `data` decrypted with key `r` (Adobe Type 1 Font Format, §7.1).
fn decrypt(data: &[u8], mut r: u16) -> Vec<u8> {
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
})
.collect()
}
/// Hexadecimal `data` as bytes, white space skipped, up to the first other character (an odd
/// last digit is its byte's high half).
fn unhex(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len() / 2);
let mut high: Option<u8> = None;
for &c in data {
if space(c) {
continue;
}
let Some(v) = char::from(c).to_digit(16) else { break };
let v = v as u8;
match high.take() {
Some(h) => out.push(h << 4 | v),
None => high = Some(v),
}
}
out.extend(high.map(|h| h << 4));
out
}
fn space(c: u8) -> bool {
matches!(c, b' ' | b'\n' | b'\r' | b'\t' | b'\0' | 0x0C)
}
fn special_or_space(c: u8) -> bool {
space(c) || matches!(c, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%')
}
/// A token of a font's decrypted encrypted part, as the font parser splits them.
enum Tok<'a> {
Int(i64),
Name(&'a [u8]),
/// The bytes after `len RD ` (or `-|`).
Blob(&'a [u8]),
Other(&'a [u8]),
}
struct Lexer<'a> {
d: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
fn peek(&self) -> Option<u8> {
self.d.get(self.pos).copied()
}
fn byte(&mut self) -> Option<u8> {
let c = self.peek()?;
self.pos += 1;
Some(c)
}
fn skip_while(&mut self, f: impl Fn(u8) -> bool) {
while self.peek().is_some_and(&f) {
self.pos += 1;
}
}
fn skip_line(&mut self) {
while let Some(c) = self.byte() {
if c == b'\n' || c == b'\r' {
break;
}
}
}
/// The rest of a string after its `(`, nested parentheses and escapes included.
fn string(&mut self) -> Option<()> {
let mut depth = 1u32;
loop {
match self.byte()? {
b'(' => depth = depth.saturating_add(1),
b')' => {
depth -= 1;
if depth == 0 {
return Some(());
}
}
b'\\' => {
self.byte()?;
}
_ => {}
}
}
}
/// The rest of a procedure after its `{`, nested procedures, comments and strings included.
fn procedure(&mut self) -> Option<()> {
let mut depth = 1u32;
loop {
match self.byte()? {
b'{' => depth = depth.saturating_add(1),
b'}' => {
depth -= 1;
if depth == 0 {
return Some(());
}
}
b'%' => self.skip_line(),
b'(' => self.string()?,
_ => {}
}
}
}
fn next(&mut self) -> Option<Tok<'a>> {
loop {
self.skip_while(space);
let start = self.pos;
match self.byte()? {
b'%' => self.skip_line(),
b'{' => return self.procedure().map(|()| Tok::Other(b"{")),
b'(' => return self.string().map(|()| Tok::Other(b"(")),
b'<' if self.peek() == Some(b'<') => self.pos += 1,
b'<' => {
// Hexadecimal digits and white space, then `>`.
let end = loop {
let c = self.byte()?;
if !space(c) && !c.is_ascii_hexdigit() {
break c;
}
};
return (end == b'>').then_some(Tok::Other(b"<"));
}
b'>' => {
if self.byte()? != b'>' {
return None;
}
}
b'/' => {
let c = self.peek()?;
if special_or_space(c) {
if space(c) {
self.pos += 1;
}
return Some(Tok::Name(&[]));
}
self.skip_while(|c| !special_or_space(c));
return self.d.get(start + 1..self.pos).map(Tok::Name);
}
b'[' | b']' => return self.d.get(start..self.pos).map(Tok::Other),
c => {
self.skip_while(|c| !special_or_space(c));
let text = self.d.get(start..self.pos)?;
if (c.is_ascii_digit() || c == b'-')
&& !text.contains(&b'.')
&& let Some(n) = int(text)
{
return Some(self.blob(n).unwrap_or(Tok::Int(n)));
}
return Some(Tok::Other(text));
}
}
}
}
/// The `n` bytes after `RD ` or `-| `, when those come next.
fn blob(&mut self, n: i64) -> Option<Tok<'a>> {
let mut at = self.pos;
while self.d.get(at).copied().is_some_and(space) {
at += 1;
}
let start = at;
while self.d.get(at).copied().is_some_and(|c| !special_or_space(c)) {
at += 1;
}
if !matches!(self.d.get(start..at), Some(b"RD" | b"-|")) {
return None;
}
// One space, then the bytes; a count past the end ends the part.
let from = at + 1;
let to = usize::try_from(n).ok().and_then(|n| from.checked_add(n));
self.pos = to.filter(|to| *to <= self.d.len()).unwrap_or(self.d.len());
Some(Tok::Blob(to.and_then(|to| self.d.get(from..to)).unwrap_or_default()))
}
}
/// A PostScript integer (`12`, `-3`, `8#17`), or `None`.
fn int(text: &[u8]) -> Option<i64> {
let s = std::str::from_utf8(text).ok()?;
match s.find('#') {
Some(i @ (1 | 2)) => {
let radix = s.get(..i)?.parse::<u32>().ok().filter(|r| (2..=36).contains(r))?;
i64::from_str_radix(s.get(i + 1..)?, radix).ok()
}
_ => s.parse().ok(),
}
}
+1 -1
View File
@@ -157,7 +157,7 @@ pub(super) fn advance(data: &[u8], gid: u32) -> f64 {
}
/// A pen drawing an outline scaled from font units to ems.
struct Pen(BezPath, f64);
pub(super) struct Pen(pub BezPath, pub f64);
impl OutlinePen for Pen {
fn move_to(&mut self, x: f32, y: f32) {
+5
View File
@@ -172,6 +172,11 @@ const OTHER_NAMES: &[(&str, char)] = &[
("grave", '`'),
];
/// The Windows-1252 glyph name of code `code` (128–255), if it has one.
pub(crate) fn high_name(code: u8) -> Option<&'static str> {
code.checked_sub(0x80).and_then(|i| HIGH.get(usize::from(i))).copied().filter(|n| *n != ".notdef")
}
/// Character `b` of Windows-1252.
pub(crate) fn decode(b: u8) -> char {
match b {
+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: 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)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: embedded Type 1 fonts' glyphs and widths; 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 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) |
| 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); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; 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 along the baseline (`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 (spacing that moves characters off the baseline, as `yshow` and `xyshow` can, places each character as its own text object); 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: an embedded Type 1 font's encrypted part (`eexec`, [Adobe Type 1 Font Format](https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf), chapter 7) is no longer only skipped: its charstrings are read, and the font dictionary gets the `CharStrings` dictionary the part would have given it. Its type then takes its characters from its glyph names (the standard and ISO Latin-1 encodings by their codes' names, PLRM 3rd ed., Appendix E), its widths move the current point and space the live type as tracking (as Type 42 fonts do), `stringwidth` and `charpath` read its glyphs, glyphs whose names say no character are drawn from its outlines, and a composite font's Type 1 descendants draw their glyphs. A font found by name, re-encoded with glyph names at other codes (codes 1 to 12 for `T`, `r`, `a`… in pdftocairo's subsets), sets the characters its names say instead of its codes read as Latin-1 (such type was left out). |
| 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. |
+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; 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
point type in the font the file names (embedded Type 1 and Type 42 fonts' characters and widths come from the font program they carry, and the show operators' spacing along the baseline is kept as tracking, while spacing that moves characters off it (`yshow`, `xyshow`) makes a text object per character; CID-keyed fonts draw the glyphs of the Type 0 and Type 2 CIDFonts the file carries, except a CIDFont in a FontSet's Compact Font Format data, which isn't read). 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,