diff --git a/crates/eps/src/import/cid.rs b/crates/eps/src/import/cid.rs index c05180de3..febfa20f9 100644 --- a/crates/eps/src/import/cid.rs +++ b/crates/eps/src/import/cid.rs @@ -9,9 +9,11 @@ use std::rc::Rc; -use skrifa::raw::ps::cs::{CharstringContext, CharstringKind, CommandSink}; +use skrifa::raw::ps::cff::CffFontRef; +use skrifa::raw::ps::cff::index::Index; +use skrifa::raw::ps::cs::{CharstringContext, CharstringKind, CommandSink, NopFilterSink, TransformSink}; use skrifa::raw::ps::error::Error as CsError; -use skrifa::raw::types::Fixed; +use skrifa::raw::types::{Fixed, GlyphId}; use vectorcraft_geom::BezPath; use super::interp::{Interp, matrix_obj}; @@ -31,10 +33,13 @@ const MAX_CODE: usize = 4; const CODESPACE: &str = "CodeSpaceRange"; const CIDS: &str = "CIDRange"; const NOTDEFS: &str = "NotDefRange"; +/// Mappings to a base font's character codes or glyph names (`[lo hi dst font …]`: `dst` a +/// string, a name, or an array of them, one a code). +const BASES: &str = "BFRange"; -/// 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>; 3], Rc)>; +/// The mappings of the CMaps shown with so far, by their character mapping, codespace, notdef +/// mapping and base-font mapping arrays, the most recent last. +pub(super) type CMaps = Vec<([Option>; 4], Rc)>; /// A CMap's mappings, read from its dictionary. pub(super) struct CMap { @@ -44,6 +49,16 @@ pub(super) struct CMap { cids: Vec<(usize, u32, u32, u32, usize)>, /// Notdef mappings, latest first. notdefs: Vec<(usize, u32, u32, u32, usize)>, + /// Mappings to base-font codes or glyph names (code length, first code, last code, what + /// the first code maps to, font), latest first. + bases: Vec<(usize, u32, u32, Obj, usize)>, +} + +/// What a code maps to in a base font (`beginbfchar`, `beginbfrange`). +#[derive(Clone)] +pub(super) enum Base { + Code(u8), + Name(Rc), } /// A character of an `FMapType` 9 font: its code's descendant font and CID, and the notdef @@ -52,6 +67,9 @@ pub(super) struct CidChar { pub font: usize, pub cid: u32, pub notdef: Option, + /// The base-font code or glyph name a `bfchar` or `bfrange` mapping gives it, instead of a + /// CID. + pub base: Option, } impl CMap { @@ -85,7 +103,18 @@ impl CMap { }) .collect() }; - Self { space, cids: maps(CIDS), notdefs: maps(NOTDEFS) } + let bases = items(BASES) + .as_chunks::<4>() + .0 + .iter() + .rev() + .filter_map(|[lo, hi, dst, font]| { + let (lo, hi) = (bytes(lo)?, bytes(hi)?); + 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), dst.clone(), font)) + }) + .collect(); + Self { space, cids: maps(CIDS), notdefs: maps(NOTDEFS), bases } } /// The characters of show string `s` (PLRM 3rd ed., §5.11.5 "CMap Mapping" and "Handling @@ -113,12 +142,16 @@ impl CMap { 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 }, + let at = self.bases.iter().position(|m| m.0 == n && (m.1..=m.2).contains(&c)); + work += at.map_or(self.bases.len(), |k| k + 1) as u64; + let base = at.and_then(|k| self.bases.get(k)).and_then(|m| Some((m.4, base_of(&m.3, c - m.1)?))); + out.push(match (matched.is_some(), find(&self.cids, &mut work), base) { + (true, Some((font, cid, _)), _) => CidChar { font, cid, notdef, base: None }, + (true, None, Some((font, base))) => CidChar { font, cid: 0, notdef: None, base: Some(base) }, // 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 }, + Some((font, _, base)) if matched.is_some() => CidChar { font, cid: base, notdef: None, base: None }, + _ => CidChar { font: 0, cid: 0, notdef: None, base: None }, }, }); i += n; @@ -137,6 +170,30 @@ impl CMap { } } +/// What the code `k` codes after the first of a base-font mapping to `dst` maps to: a string's +/// code with its last byte that much higher, the `k`th of an array of codes and names, or a +/// name (TN #5099, `beginbfrange`). +fn base_of(dst: &Obj, k: u32) -> Option { + match dst { + // An array's elements are read as codes and names only: an array may hold itself, so + // reading its arrays in turn could recurse without end. + Obj::Array { items, .. } => base_of_leaf(items.borrow().get(usize::try_from(k).ok()?)?, 0), + _ => base_of_leaf(dst, k), + } +} + +/// [`base_of`] for a string or a name. +fn base_of_leaf(dst: &Obj, k: u32) -> Option { + match dst { + Obj::Str(s) => { + let last = u32::from(s.len().checked_sub(1).and_then(|i| s.get(i))?).checked_add(k)?; + Some(Base::Code(u8::try_from(last).ok()?)) + } + Obj::Name(n) if k == 0 => Some(Base::Name(n.clone())), + _ => None, + } +} + /// 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)) @@ -206,10 +263,26 @@ impl Interp<'_> { } 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. + // Mappings to character codes or names in base fonts (TN #5099, §§3.3 and 4). EndBfChar | EndBfRange => { - self.pop_mark_items()?; + let items = self.pop_mark_items()?; + let per = if op == EndBfChar { 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, dst) = match m { + [lo, dst] => (lo, lo, dst), + [lo, hi, dst] => (lo, hi, dst), + _ => return ps_err("rangecheck", op.name()), + }; + if !(matches!(lo, Obj::Str(_)) && matches!(hi, Obj::Str(_)) && matches!(dst, Obj::Str(_) | Obj::Name(_) | Obj::Array { .. })) { + return ps_err("typecheck", op.name()); + } + out.extend([lo.clone(), hi.clone(), dst.clone(), Obj::Int(self.cmap_font)]); + } + self.cmap_append(BASES, out)?; } UseFont => { let n = self.pop_int()?; @@ -219,7 +292,7 @@ impl Interp<'_> { 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] { + for k in [CODESPACE, CIDS, NOTDEFS, BASES] { // 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. @@ -352,8 +425,9 @@ impl Interp<'_> { /// (`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. + /// FontSet's `/name n StartData` (Compact Font Format data: PLRM 3rd ed., §5.8.1) defines its + /// CID-keyed fonts as CIDFonts (see [`Self::font_set`]); its other 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()?; @@ -381,7 +455,7 @@ impl Interp<'_> { self.read(&f, n)? }; if !matches!(kind, Obj::Str(_)) { - return Ok(()); + return self.font_set(data); } let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", "StartData".into()))?; self.insert(&d, Key::name("GlyphData"), Some(Obj::string(data)))?; @@ -417,10 +491,36 @@ impl Interp<'_> { Ok(()) } + /// The CID-keyed fonts of FontSet data `data` (Compact Font Format: Adobe Technical Note + /// #5176, a top DICT with `ROS`), each defined as a Type 0 CIDFont of its name in the CFF's + /// Name INDEX, in glyph space of its units an em, whose glyphs are its Type 2 charstrings + /// picked by the CIDs its charset gives them. + fn font_set(&mut self, data: Vec) -> Res { + let data: Rc<[u8]> = data.into(); + for index in 0..MAX_FONT_SET { + let Ok(font) = CffFontRef::new_cff(&data, index, None) else { break }; + let Some(name) = font.metadata().and_then(|m| m.name().map(str::to_string)) else { continue }; + if !font.is_cid() || self.cff.len() >= MAX_CFF { + continue; + } + let upem = f64::from(font.upem()); + let upem = if upem.is_finite() && upem >= 1.0 { upem } else { 1000.0 }; + let mut d = Dict::new(); + d.insert(Key::name("CIDFontName"), Obj::name(&name)); + 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(1.0 / upem))); + let d = DictRef::new(d); + self.instances(&Obj::name("CIDFont"))?.borrow_mut().insert(Key::name(&name), Obj::Dict(d.clone())); + self.cff.push((d, Rc::new(Cff::new(data.clone(), index)))); + } + Ok(()) + } + /// The mappings of CMap dictionary `d` (read once while it stays the same). pub(super) fn cmap(&mut self, d: &DictRef) -> Res> { 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 key = [array(CIDS), array(CODESPACE), array(NOTDEFS), array(BASES)]; let same = |a: &Option>, b: &Option>| match (a, b) { (Some(a), Some(b)) => a.same(b), (None, None) => true, @@ -430,7 +530,7 @@ impl Interp<'_> { 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)?; + self.spend((m.cids.len() + m.space.len() + m.notdefs.len() + m.bases.len()) as u64)?; if self.cmaps.len() >= MAX_CMAPS { self.cmaps.remove(0); } @@ -522,6 +622,17 @@ impl Interp<'_> { /// 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> { + if let Some(cff) = self.cff.iter().find(|(k, _)| k.same(font)).map(|(_, c)| c.clone()) { + self.spend(64)?; + let read = cff.gids_read().is_some(); + let glyph = cff.glyph(cid); + self.spend(cff.take_work())?; + // Reading the charset visits each of its glyphs. + if !read { + self.spend(cff.gids_read().unwrap_or(0) as u64)?; + } + return Ok(glyph); + } 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); @@ -659,6 +770,69 @@ impl Interp<'_> { } } +/// Most fonts read from one FontSet. +const MAX_FONT_SET: u32 = 64; +/// Most CIDFonts read from FontSets. +const MAX_CFF: usize = 256; + +/// A CID-keyed font in Compact Font Format data: the data and the font's index in it, and the +/// glyph its charset gives each CID (read when first asked). +pub(super) struct Cff { + data: Rc<[u8]>, + index: u32, + gids: std::cell::OnceCell>, + /// The charstring bytes glyphs entered, not yet charged. + work: std::cell::Cell, +} + +impl Cff { + pub(super) fn new(data: Rc<[u8]>, index: u32) -> Self { + Self { data, index, gids: std::cell::OnceCell::new(), work: std::cell::Cell::new(0) } + } + + /// Whether the CID-to-glyph map has been read, and the glyphs it holds. + fn gids_read(&self) -> Option { + self.gids.get().map(std::collections::HashMap::len) + } + + /// Glyph `cid`: the glyph the charset gives that CID, drawn from its Type 2 charstring with + /// the subroutines and widths of its FDSelect subfont, in font units; `None` when the font + /// has no glyph for it. + pub(super) fn glyph(&self, cid: u32) -> Option<(BezPath, f64)> { + let font = CffFontRef::new_cff(&self.data, self.index, None).ok()?; + // The charset read once into a map, instead of searched for each glyph. + let gids = self.gids.get_or_init(|| font.charset().map(|c| c.iter().map(|(g, sid)| (sid.to_u16(), g)).collect()).unwrap_or_default()); + let gid = *gids.get(&u16::try_from(cid).ok()?)?; + if gid.to_u32() == 0 && cid != 0 { + return None; + } + let sub = font.subfont(font.subfont_index(gid)?, &[]).ok()?; + // Run as `CffFontRef::draw` does, through the counting context, so the subroutines a + // glyph enters (up to a million operations each time) are paid for. + let subrs = match sub.subrs_offset() { + 0 => Index::Empty, + at => Index::new(self.data.get(at as usize..)?, false).ok()?, + }; + let charstrings = font.charstrings().clone(); + let data = charstrings.get(gid.to_u32() as usize)?; + let ctx = (&self.data[..], &charstrings, font.global_subrs(), &subrs); + let mut pen = super::type42::Pen(BezPath::new(), 1.0); + let transform = font.transform(&sub, None); + let mut filter = NopFilterSink::new(&mut pen); + let mut sink = TransformSink::new(&mut filter, transform); + let (w, work) = super::type1::evaluate(&ctx, data, &mut sink); + self.work.set(self.work.get().saturating_add(work)); + let w = w.ok()?.map(|w| w + sub.nominal_width()).or(sub.default_width()); + let w = w.map_or(0.0, |w| f64::from(transform.transform_h_metric(w).to_f32().max(0.0))); + Some((pen.0, if w.is_finite() { w } else { 0.0 })) + } + + /// The charstring bytes entered since the last call (see [`super::type1::evaluate`]). + pub(super) fn take_work(&self) -> u64 { + self.work.take() + } +} + /// Most FDArray dictionaries whose subroutines are kept decrypted between glyphs. const MAX_SUBR_SETS: usize = 32; diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 6408f4fa7..4920b1d66 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -85,6 +85,9 @@ pub(crate) struct Interp<'a> { /// 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)>, + /// The CIDFonts a FontSet's Compact Font Format data defined, by their dictionaries: the + /// data and the font's index in it (see `cid`). + pub cff: Vec<(DictRef, Rc)>, /// 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. @@ -226,6 +229,7 @@ impl<'a> Interp<'a> { type42: vec![], cmaps: vec![], type1: vec![], + cff: vec![], cid_subrs: vec![], cmap_font: 0, type42_counted: HashSet::new(), diff --git a/crates/eps/src/import/tests_cid.rs b/crates/eps/src/import/tests_cid.rs index f930f94db..0aed6bba2 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -216,6 +216,187 @@ fn cidinit_operators_are_not_in_systemdict() { check("systemdict /begincmap known not /CIDInit /ProcSet findresource /begincmap known and systemdict /composefont known and"); } +/// A CFF INDEX (Adobe Technical Note #5176, §5) of `items`, with 2-byte offsets. +fn cff_index(items: &[Vec]) -> Vec { + let mut out = (items.len() as u16).to_be_bytes().to_vec(); + if items.is_empty() { + return out; + } + out.push(2); + let mut at = 1u16; + out.extend(at.to_be_bytes()); + for i in items { + at += i.len() as u16; + out.extend(at.to_be_bytes()); + } + out.extend(items.iter().flatten()); + out +} + +/// A CFF DICT operand as a 5-byte integer (TN #5176, Table 3), so offsets don't change its size. +fn cff_int(v: i32) -> Vec { + [vec![29], v.to_be_bytes().to_vec()].concat() +} + +/// A Type 2 charstring (Adobe Technical Note #5177) from numbers, each a 3-byte integer +/// (`28`), and operators (`rmoveto` 21, `rlineto` 5, `endchar` 14). +fn cs2(parts: &[(&[i16], u8)]) -> Vec { + parts.iter().flat_map(|(args, op)| args.iter().flat_map(|a| [vec![28], a.to_be_bytes().to_vec()].concat()).chain([*op])).collect() +} + +/// A CID-keyed CFF font `/CffCID` (TN #5176, §18): glyph 0 `.notdef`, glyph 1 CID 1 a square +/// (100 to 500 across, 500 high) 600 wide, glyph 2 CID 7 a triangle (500 across, 400 high) 500 +/// wide (the first number of a charstring its width, `nominalWidthX` 0). +fn cid_cff() -> Vec { + let glyphs = vec![ + cs2(&[(&[0], 14)]), + cs2(&[(&[600, 100, 0], 21), (&[400, 0, 0, 500, -400, 0], 5), (&[], 14)]), + cs2(&[(&[500, 0, 0], 21), (&[500, 0, -250, 400], 5), (&[], 14)]), + ]; + let header = vec![1, 0, 4, 2]; + let names = cff_index(&[b"CffCID".to_vec()]); + let strings = cff_index(&[b"Test".to_vec(), b"Shapes".to_vec()]); + let global = cff_index(&[]); + // The top DICT: ROS (12 30), CIDCount (12 34), charset (15), CharStrings (17), FDArray + // (12 36), FDSelect (12 37), each offset a 5-byte integer. + let top = |charset: i32, strings_at: i32, fd_array: i32, fd_select: i32| -> Vec { + [ + cff_int(391), + cff_int(392), + cff_int(0), + vec![12, 30], + cff_int(8), + vec![12, 34], + cff_int(charset), + vec![15], + cff_int(strings_at), + vec![17], + cff_int(fd_array), + vec![12, 36], + cff_int(fd_select), + vec![12, 37], + ] + .concat() + }; + let top_len = cff_index(&[top(0, 0, 0, 0)]).len(); + let charset_at = header.len() + names.len() + top_len + strings.len() + global.len(); + // Format 0: the CIDs of glyphs 1 and 2. + let charset = vec![0, 0, 1, 0, 7]; + let fd_select_at = charset_at + charset.len(); + let fd_select = vec![0, 0, 0, 0]; + let strings_at = fd_select_at + fd_select.len(); + let char_strings = cff_index(&glyphs); + let private_at = strings_at + char_strings.len(); + // defaultWidthX (20) and nominalWidthX (21) 0. + let private = [cff_int(0), vec![20], cff_int(0), vec![21]].concat(); + let fd_array_at = private_at + private.len(); + let fd = [cff_int(private.len() as i32), cff_int(private_at as i32), vec![18]].concat(); + let fd_array = cff_index(&[fd]); + let top = cff_index(&[top(charset_at as i32, strings_at as i32, fd_array_at as i32, fd_select_at as i32)]); + [header, names, top, strings, global, charset, fd_select, char_strings, private, fd_array].concat() +} + +/// A FontSet holding CID-keyed Compact Font Format data (PLRM 3rd ed., §5.8.1, `StartData`; +/// TN #5176) defines its CIDFont, whose glyphs are drawn by their CIDs. +#[test] +fn cff_fontsets_draw_their_cidfonts_glyphs() { + let data = cid_cff(); + let head = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n".to_vec(); + let set = + [format!("/FontSetInit /ProcSet findresource begin\n/CffCID {} StartData ", data.len()).into_bytes(), data, b"\nend\n".to_vec()].concat(); + let body = "/CffFont /Identity-H [/CffCID] composefont 50 scalefont setfont 10 20 moveto <00010007> show\n\ + (\\000\\001\\000\\007) stringwidth pop 55 sub abs 0.01 lt { 0 0 5 5 rectfill } if\nshowpage\n%%EOF\n"; + let r = import(&[head, set, body.as_bytes().to_vec()].concat()).unwrap(); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let p = paths(&r); + assert_eq!(p.len(), 3, "{p:?}"); + 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!((tri.x0 - 40.0).abs() < 0.01 && (tri.width() - 25.0).abs() < 0.01 && (tri.height() - 20.0).abs() < 0.01, "{tri:?}"); +} + +/// Damaged FontSet data (each byte of the CFF data changed, in turn) is read as fonts or as +/// nothing, never a crash. +#[test] +fn damaged_cff_fontsets_never_panic() { + let data = cid_cff(); + let head = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n".to_vec(); + let body = b"/CffFont /Identity-H [/CffCID] composefont 50 scalefont setfont 10 20 moveto <000100070002> show showpage\n%%EOF\n".to_vec(); + for at in 0..data.len() { + for v in [0u8, 0x7f, 0xff, data[at].wrapping_add(1)] { + let mut d = data.clone(); + d[at] = v; + let set = + [format!("/FontSetInit /ProcSet findresource begin\n/CffCID {} StartData ", d.len()).into_bytes(), d, b"\nend\n".to_vec()].concat(); + let _ = import(&[head.clone(), set, body.clone()].concat()); + } + } +} + +/// A CMap mapping two-byte codes to base fonts' codes and glyph names (`beginbfchar`, +/// `beginbfrange`, `usefont`: Adobe Technical Note #5099, "Building CMap Files for CID-Keyed +/// Fonts"): font 0 a Type 42 font that carries Source Sans 3, font 1 a font found by name. +const BF_CMAP: &str = "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\ + /CMapName /TestBF-H def /CMapType 1 def /WMode 0 def\n\ + 1 begincodespacerange <0000> endcodespacerange\n\ + 0 usefont 1 beginbfchar <0001> <01> endbfchar 1 beginbfrange <0002> <0003> [/g2 /g1] endbfrange\n\ + 1 usefont 1 beginbfrange <0010> <0012> <41> endbfrange\n\ + endcmap CMapName currentdict /CMap defineresource pop end end\n"; + +#[test] +fn cmaps_map_codes_to_base_fonts_codes_and_glyph_names() { + let (h, wh) = glyph('H'); + let (i, wi) = glyph('I'); + // `/Plain42`: codes 1 and 2 are glyphs g1 (H) and g2 (I); g2 is also named outside the encoding. + let plain = format!( + "10 dict begin /FontName /Plain42 def /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /FontBBox [0 0 1 1] def /PaintType 0 def\n\ + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 1 /g1 put\n\ + /CharStrings 3 dict def CharStrings begin /.notdef 0 def /g1 {h} def /g2 {i} def end\n\ + /sfnts [\n{}] def FontName currentdict end definefont pop\n", + sfnts() + ); + let body = format!("{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 50 scalefont setfont 10 20 moveto <000100020003> show"); + let r = super::tests::read(&body); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let p = paths(&r); + assert_eq!(p.len(), 3, "{p:?}"); + // H, I, H, each its own width along. + assert!(p[0].x0 > 10.0 && p[0].x0 < 18.0, "{p:?}"); + assert!(p[1].x0 > 10.0 + 50.0 * wh - 1.0 && p[1].x1 < 10.0 + 50.0 * (wh + wi), "{p:?}"); + assert!(p[2].x0 > 10.0 + 50.0 * (wh + wi) && (p[2].width() - p[0].width()).abs() < 0.01, "{p:?}"); + // Codes to a font found by name set its characters as type. + let body = format!("{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 20 scalefont setfont 10 20 moveto <001000110012> show"); + let r = super::tests::read(&body); + let text: String = all(&r.document) + .iter() + .filter_map(|n| match &n.kind { + NodeKind::Text(t) => Some(t.plain_text()), + _ => None, + }) + .collect(); + assert_eq!(text, "ABC"); +} + +/// A code a base-font CMap doesn't map, in a base font, is that font's code 0 (`.notdef`): it +/// moves the point by that glyph's width, not by a thousand ems, and the font isn't missing. +#[test] +fn unmapped_codes_in_base_fonts_take_their_notdef() { + let plain = format!( + "10 dict begin /FontName /Plain42 def /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /FontBBox [0 0 1 1] def /PaintType 0 def\n\ + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for\n\ + /CharStrings 1 dict def CharStrings begin /.notdef 0 def end\n\ + /sfnts [\n{}] def FontName currentdict end definefont pop\n", + sfnts() + ); + check(&format!( + "{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 50 scalefont setfont <0009> stringwidth pop dup 0 gt exch 100 lt and" + )); + let r = super::tests::read(&format!( + "{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 50 scalefont setfont 10 20 moveto <0009> show 0 0 10 10 rectfill" + )); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); +} + /// A CMap's mappings take font 0 until its own `usefont`, whatever font number an earlier CMap /// ended with (Adobe Technical Note #5014). #[test] @@ -369,6 +550,30 @@ fn cid_glyphs_measured_in_a_loop_run_out_of_the_budget() { super::tests::runs_out_of_budget(&format!("{font}{CMAP} /TestFont 50 selectfont {{ (B) stringwidth pop pop }} loop")); } +/// A `beginbfrange` destination array that holds itself is read without recursing into it (an +/// array element that is an array maps nothing), so showing its code is no crash. +#[test] +fn a_bfrange_array_holding_itself_is_no_crash() { + let body = "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Self-H def\n\ + 1 begincodespacerange <00> endcodespacerange\n\ + /a 1 array def a 0 a put 1 beginbfrange <00> <00> a endbfrange\n\ + endcmap CMapName currentdict /CMap defineresource pop end end\n\ + /S /Self-H [/Helvetica] composefont 20 scalefont setfont 10 20 moveto <00> show 0 0 10 10 rectfill"; + let r = super::tests::read(body); + assert_eq!(paths(&r).len(), 1, "{:?}", r.warnings); +} + +/// Drawing a glyph of a FontSet's CIDFont counts the charstring bytes it enters, as Type 1 +/// glyphs do, so glyphs that run long (nested subroutines) are paid for by the `show` that +/// draws them, not a flat amount. +#[test] +fn cff_glyphs_count_the_charstrings_they_run() { + let cff = super::cid::Cff::new(std::rc::Rc::from(cid_cff()), 0); + assert!(cff.glyph(1).is_some()); + assert!(cff.take_work() > 0); + assert_eq!(cff.take_work(), 0); +} + /// `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. diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index dd1d77878..3fb2aec5e 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -11,6 +11,7 @@ use vectorcraft_doc::{CharStyle, Node, NodeId, NodeKind, TextObject}; use vectorcraft_geom::{Affine, Point, Vec2}; use vectorcraft_text::TextLayout; +use super::cid::Base; use super::graphics::MAX_APART; use super::interp::{Interp, matrix_obj, matrix_of}; use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, ps_err}; @@ -780,6 +781,8 @@ struct Mapped { /// The byte that picked the descendant (the font number). pick: u8, code: u8, + /// The glyph name a CMap maps the character to, when the font's encoding has no code for it. + name: Option>, cid: Option<(u32, Option)>, outer: Affine, vertical: bool, @@ -853,7 +856,7 @@ 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, cid: None, outer, vertical: false }); + out.push(Mapped { font: d.clone(), pick, code, name: None, cid: None, outer, vertical: false }); } } Ok(Some(out)) @@ -877,9 +880,23 @@ impl Interp<'_> { 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 }) + .filter_map(|c| match (fonts.get(c.font), c.base) { + // A base font's code, or its glyph name (at the code its encoding gives it, if any). + (Some(Obj::Dict(d)), Some(Base::Code(code))) => { + Some(Mapped { font: d.clone(), pick: 0, code, name: None, cid: None, outer, vertical }) + } + (Some(Obj::Dict(d)), Some(Base::Name(name))) => { + let code = encoded(&d.borrow(), &name); + let name = code.is_none().then_some(name); + Some(Mapped { font: d.clone(), pick: 0, code: code.unwrap_or(0), name, cid: None, outer, vertical }) + } + // A code without a mapping in a base font (not a CIDFont): its code 0, the + // `.notdef` glyph CID 0 stands for in a CIDFont. + (Some(Obj::Dict(d)), None) if !d.borrow().contains_key(&Key::name("CIDFontType")) => { + Some(Mapped { font: d.clone(), pick: 0, code: 0, name: None, cid: None, outer, vertical }) + } + (Some(Obj::Dict(d)), None) => { + Some(Mapped { font: d.clone(), pick: 0, code: (c.cid & 0xff) as u8, name: None, cid: Some((c.cid, c.notdef)), outer, vertical }) } _ => None, }) @@ -898,7 +915,7 @@ impl Interp<'_> { let d = spacing.advance(i, i64::from(g.pick) * 256 + i64::from(g.code), w); self.g.cur = Some(self.xf() * (at + d)); // A CID stands for no character (its code is only the CID's low byte). - let c = if g.cid.is_some() { '\u{2022}' } else { code_char(&g.font.borrow(), g.code) }; + let c = if g.cid.is_some() { '\u{2022}' } else { g.name.as_deref().map_or_else(|| code_char(&g.font.borrow(), g.code), glyph_char) }; if c != '\u{2022}' { text.push(c); } @@ -924,7 +941,8 @@ impl Interp<'_> { return self.cid_glyph_at(g, cid, notdef, at, paint); } if builds { - return self.type3_glyph_in(&g.font, Glyph::Code(g.code), at, paint == Draw::Fill, g.outer); + let glyph = g.name.clone().map_or(Glyph::Code(g.code), Glyph::Name); + return self.type3_glyph_in(&g.font, glyph, at, paint == Draw::Fill, g.outer); } // An em of the descendant's glyphs: a Type 42 font's matrix maps ems, a Type 1 font's // its 1000-unit glyph space. @@ -932,12 +950,12 @@ impl Interp<'_> { 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)? { + if let Some((outline, advance)) = self.type42_glyph(&g.font, g.code, g.name.as_deref())? { self.type42_paint(outline, at, fm, paint)?; return Ok(fm * Point::new(advance, 0.0) - fm * Point::ZERO); } // Set as type in the descendant's font, under the composite font's matrix. - let c = code_char(&g.font.borrow(), g.code); + let c = g.name.as_deref().map_or_else(|| code_char(&g.font.borrow(), g.code), |n| named_char(0, n).unwrap_or('\u{2022}')); let Ok(byte) = u8::try_from(u32::from(c)) else { return Ok(Vec2::ZERO) }; // Scaling the descendant copies its dictionary for every character: charged by its size, // so a huge font dictionary shown in a loop runs out of the budget. @@ -996,12 +1014,18 @@ impl Interp<'_> { Ok(fm * Point::new(w, 0.0) - fm * Point::ZERO) } - /// 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> { + /// Glyph `code` (or the glyph named `name`) of font `font` from the font program it carries + /// (a Type 42 or Type 1 font): its outline and width in ems, or `None` when it carries none. + fn type42_glyph(&mut self, font: &DictRef, code: u8, name: Option<&str>) -> Res> { 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); + let gid = match name { + Some(n) => { + let f = font.borrow(); + char_string(&f, n).or_else(|| named_char(0, n).and_then(|c| data.gid_of(c))).or_else(|| char_string(&f, ".notdef")).unwrap_or(0) + } + None => type42_code(&font.borrow(), &data, code, standard).1, + }; // Reading the font's character map (once) visits each of its mappings; running a Type 1 // glyph's charstrings is paid for by their bytes. self.spend(data.take_work())?; @@ -1189,6 +1213,12 @@ fn code_char(f: &Dict, code: u8) -> char { } } +/// The code font `f`'s `Encoding` gives glyph name `name`, if any. +fn encoded(f: &Dict, name: &str) -> Option { + let items = f.get(&Key::name("Encoding"))?.items()?.to_vec(); + items.iter().position(|o| o.text().as_deref() == Some(name)).and_then(|i| u8::try_from(i).ok()) +} + /// The glyph name font `f`'s `Encoding` gives code `code` (`.notdef` without one). fn glyph_name(f: &Dict, code: u8) -> Rc { f.get(&Key::name("Encoding")).and_then(|e| e.items()?.get(usize::from(code))).and_then(|o| o.text()).unwrap_or_else(|| Rc::from(".notdef")) diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 6fe9230e3..85ff57417 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: `kshow` procedures kern live type; 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) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; 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); 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`, and the moves of `kshow`'s procedure, run between each two characters) 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`, `xyshow` or a `kshow` procedure 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) | +| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CIDFonts the file doesn't carry not drawn; name-keyed fonts in a FontSet set as type by name; #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`, and the moves of `kshow`'s procedure, run between each two characters) 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`, `xyshow` or a `kshow` procedure 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`; `bfchar` and `bfrange` mappings give a base-font descendant its codes or glyph names) 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, and the CID-keyed fonts of a FontSet's Compact Font Format data (Type 2 charstrings); 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 ``, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`) | | DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). CNC preset and VCarve QA remain open | @@ -320,6 +320,8 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Date | Change | Summary | |---|---|---| +| 2026-11-07 | minor | EPS import: CMap mappings to base fonts (`beginbfchar` … `endbfchar`, `beginbfrange` … `endbfrange` after `usefont`: [Adobe Technical Note #5099](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5099.CMapResources.pdf); PLRM 3rd ed., §5.11.4) are read instead of dropped: a code mapped to a character code (a range's codes counting up from its last byte) or to a glyph name (one, or an array of them) draws that glyph of the base font it names, a Type 3, Type 42 or embedded Type 1 font's own glyph or, in a font found by name, its character as type. Such codes took font 0's CID 0 and drew nothing. | +| 2026-11-07 | minor | EPS import: a FontSet's Compact Font Format data (`/name n StartData` in the FontSetInit procedure set: PLRM 3rd ed., §5.8.1; [Adobe Technical Note #5176](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf)) is read instead of skipped: each CID-keyed font in it (a top DICT with `ROS`) is defined as a CIDFont of its name, and type in a CID-keyed font over it draws its glyphs, picked by CID through the charset and drawn from their Type 2 charstrings with their FDSelect subfont's subroutines and widths. Name-keyed fonts in a FontSet are still set as type by name. | | 2026-11-07 | minor | EPS import: `kshow` (PLRM 3rd ed., chapter 8) runs its procedure between each two characters of type in a base font (not a Type 3 or composite font), with the two character codes, instead of only between a Type 3 font's glyphs: where it moves the current point (kerning), the next character follows, kept on the live type as tracking, and what it draws is drawn over the type. | | 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. | diff --git a/docs/mcp.md b/docs/mcp.md index e4ff7cd53..3221f299f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 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 +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, CIDFonts in a FontSet's Compact Font Format data included). 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,