EPS import: CMap mappings to base fonts' codes and glyph names draw their glyphs

bfchar and bfrange mappings are kept in the CMap and carried over by
usecmap. A code mapped to a character code draws that code in the base font
usefont named; a code mapped to a glyph name draws that glyph.
This commit is contained in:
Don Omondi
2026-11-09 03:48:35 -05:00
parent 6854529b83
commit dc00c31a24
4 changed files with 207 additions and 28 deletions
+85 -14
View File
@@ -33,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<Shared<Obj>>; 3], Rc<CMap>)>;
/// 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<Shared<Obj>>; 4], Rc<CMap>)>;
/// A CMap's mappings, read from its dictionary.
pub(super) struct CMap {
@@ -46,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<str>),
}
/// A character of an `FMapType` 9 font: its code's descendant font and CID, and the notdef
@@ -54,6 +67,9 @@ pub(super) struct CidChar {
pub font: usize,
pub cid: u32,
pub notdef: Option<u32>,
/// The base-font code or glyph name a `bfchar` or `bfrange` mapping gives it, instead of a
/// CID.
pub base: Option<Base>,
}
impl CMap {
@@ -87,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
@@ -115,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;
@@ -139,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<Base> {
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<Base> {
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))
@@ -208,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()?;
@@ -221,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.
@@ -449,7 +520,7 @@ impl Interp<'_> {
/// The mappings of CMap dictionary `d` (read once while it stays the same).
pub(super) fn cmap(&mut self, d: &DictRef) -> Res<Rc<CMap>> {
let array = |k: &str| d.borrow().get(&Key::name(k)).and_then(|a| a.items().cloned());
let key = [array(CIDS), array(CODESPACE), array(NOTDEFS)];
let key = [array(CIDS), array(CODESPACE), array(NOTDEFS), array(BASES)];
let same = |a: &Option<Shared<Obj>>, b: &Option<Shared<Obj>>| match (a, b) {
(Some(a), Some(b)) => a.same(b),
(None, None) => true,
@@ -459,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);
}
+77
View File
@@ -333,6 +333,70 @@ fn damaged_cff_fontsets_never_panic() {
}
}
/// 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> <ffff> 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]
@@ -486,6 +550,19 @@ 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> <ff> 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.
+42 -12
View File
@@ -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<Rc<str>>,
cid: Option<(u32, Option<u32>)>,
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<Option<(vectorcraft_geom::BezPath, f64)>> {
/// 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<Option<(vectorcraft_geom::BezPath, f64)>> {
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<u8> {
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<str> {
f.get(&Key::name("Encoding")).and_then(|e| e.items()?.get(usize::from(code))).and_then(|o| o.text()).unwrap_or_else(|| Rc::from(".notdef"))
+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 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)
> **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) | 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) |
| 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 `<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: 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). |