From 6854529b83c12803f50e46e1b89c7df2efd04619 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:24:40 -0500 Subject: [PATCH] EPS import: CID-keyed fonts in a FontSet's CFF data draw their glyphs Each CID-keyed font in a FontSet's CFF data is defined as a CIDFont named by the CFF Name INDEX, with its glyphs found by CID through the charset and FDSelect, so a composefont over it draws its type. --- crates/eps/src/import/cid.rs | 113 +++++++++++++++++++++++-- crates/eps/src/import/interp.rs | 4 + crates/eps/src/import/tests_cid.rs | 128 +++++++++++++++++++++++++++++ docs/file-format-parity.md | 5 +- docs/mcp.md | 2 +- 5 files changed, 244 insertions(+), 8 deletions(-) diff --git a/crates/eps/src/import/cid.rs b/crates/eps/src/import/cid.rs index c05180de3..30cb9110f 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}; @@ -352,8 +354,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 +384,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,6 +420,32 @@ 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()); @@ -522,6 +551,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 +699,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..c68da65af 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -216,6 +216,123 @@ 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's mappings take font 0 until its own `usefont`, whatever font number an earlier CMap /// ended with (Adobe Technical Note #5014). #[test] @@ -369,6 +486,17 @@ 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")); } +/// 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/docs/file-format-parity.md b/docs/file-format-parity.md index 6fe9230e3..0306016ea 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: 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`) 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,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Date | Change | Summary | |---|---|---| +| 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 64d314ecb..6b6dcd219 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,