mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
EPS import: Type 42 fonts take their characters and widths from the font they carry; show spacing kept as tracking
A plain Type 42 font now maps each code to its character through its glyph names, or through the glyph its CharStrings give it in the font's own character map or post names, instead of reading the code as Latin-1. The font's own widths move the current point, charpath outlines its glyphs, and the show operators' spacing and the embedded widths are kept on live type as tracking. Type shown under nulldevice no longer appears.
This commit is contained in:
@@ -60,6 +60,8 @@ mod tests_illustrator;
|
||||
mod tests_images;
|
||||
#[cfg(test)]
|
||||
mod tests_native;
|
||||
#[cfg(test)]
|
||||
mod tests_type42;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) struct Fault {
|
||||
}
|
||||
|
||||
/// The TrueType data of Type 42 fonts by their `sfnts` array (see [`Interp::type42`]).
|
||||
pub(crate) type Type42Cache = Vec<(Shared<Obj>, Option<Rc<Vec<u8>>>)>;
|
||||
pub(crate) type Type42Cache = Vec<(Shared<Obj>, Option<Rc<super::type42::Font>>)>;
|
||||
|
||||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
|
||||
@@ -11,10 +11,10 @@ use vectorcraft_doc::NodeKind;
|
||||
|
||||
use super::tests::{all, bounds, fill, read};
|
||||
|
||||
const FONT: &[u8] = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
pub(super) const FONT: &[u8] = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
|
||||
/// The glyph index of `c` in [`FONT`] and its advance width in ems.
|
||||
fn glyph(c: char) -> (u32, f64) {
|
||||
pub(super) fn glyph(c: char) -> (u32, f64) {
|
||||
let font = FontRef::new(FONT).unwrap();
|
||||
let gid = font.charmap().map(c).unwrap();
|
||||
let upem = f64::from(font.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
|
||||
@@ -24,7 +24,7 @@ fn glyph(c: char) -> (u32, f64) {
|
||||
|
||||
/// `sfnts` strings of [`FONT`]: the first of odd length, ending in a padding byte that isn't part
|
||||
/// of the font, the rest split at even lengths.
|
||||
fn sfnts() -> String {
|
||||
pub(super) fn sfnts() -> String {
|
||||
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let mut out = format!("<{}00>\n", hex(&FONT[..20000]));
|
||||
for chunk in FONT[20000..].chunks(40000) {
|
||||
@@ -251,3 +251,16 @@ fn unreadable_type42_data_read_again_runs_out_of_the_budget() {
|
||||
/Comp findfont 50 scalefont setfont {{ ({s}) stringwidth pop pop }} loop"
|
||||
));
|
||||
}
|
||||
|
||||
/// Characters that map to no glyph (an empty `FDepVector`) cost nothing to draw, but finding that
|
||||
/// out reads the whole string: a `loop` showing, measuring or outlining a long string of them runs
|
||||
/// out of the operation budget instead of stalling the import.
|
||||
#[test]
|
||||
fn unmapped_composite_characters_in_a_loop_run_out_of_the_budget() {
|
||||
let font = "16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def\n\
|
||||
/Encoding [0] def /FDepVector [] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont /s 100000 string def";
|
||||
for body in ["{ 0 0 moveto s show } loop", "{ s stringwidth pop pop } loop", "{ newpath 0 0 moveto s false charpath } loop"] {
|
||||
super::tests::runs_out_of_budget(&format!("{font} {body}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Plain Type 42 fonts (TrueType fonts in PostScript, Adobe Technical Note #5012, "The Type 42
|
||||
//! Font Format Specification") that carry their TrueType font: each code's character from its
|
||||
//! glyph name, or from the glyph its `CharStrings` name gives when the name says nothing
|
||||
//! (`/g12`); its width from the font's own metrics; glyphs that stand for no character drawn
|
||||
//! from the font's outlines. Spacing from the show operators (PLRM 3rd ed., §5.1.4 and the
|
||||
//! `ashow`, `widthshow` and `xshow` entries of chapter 8) is kept on live type as tracking. The
|
||||
//! fonts are written from scratch around the bundled Source Sans 3.
|
||||
|
||||
use skrifa::{FontRef, MetadataProvider};
|
||||
use vectorcraft_doc::{NodeKind, TextObject};
|
||||
|
||||
use super::tests::{all, read};
|
||||
use super::tests_composite::{FONT, glyph, sfnts};
|
||||
|
||||
/// A program defining Type 42 font `/Plain42` whose `Encoding` gives codes 1 to `names.len()`
|
||||
/// glyph names `/g1`, `/g2`… mapped through `CharStrings` to the glyph of each of `glyphs`.
|
||||
fn font(glyphs: &[u32]) -> String {
|
||||
let mut enc = String::new();
|
||||
let mut strings = String::new();
|
||||
for (i, g) in glyphs.iter().enumerate() {
|
||||
enc.push_str(&format!("Encoding {} /g{} put ", i + 1, i + 1));
|
||||
strings.push_str(&format!("/g{} {g} def ", i + 1));
|
||||
}
|
||||
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 {enc}\n\
|
||||
/CharStrings {} dict def CharStrings begin /.notdef 0 def {strings} end\n\
|
||||
/sfnts [\n{}] def FontName currentdict end definefont pop\n",
|
||||
glyphs.len() + 1,
|
||||
sfnts()
|
||||
)
|
||||
}
|
||||
|
||||
fn texts(r: &crate::Imported) -> Vec<TextObject> {
|
||||
all(&r.document)
|
||||
.iter()
|
||||
.filter_map(|n| match &n.kind {
|
||||
NodeKind::Text(t) => Some((**t).clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where each glyph of `t` starts along its baseline (text space).
|
||||
fn origins(t: &TextObject) -> Vec<f64> {
|
||||
vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).glyphs.iter().map(|g| g.origin.x).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_names_that_say_nothing_read_the_fonts_character_map() {
|
||||
let (h, _) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
let r = read(&format!("{}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002) show", font(&[h, i])));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.len(), 1, "{:?}", all(&r.document));
|
||||
assert_eq!(t[0].plain_text(), "HI");
|
||||
assert_eq!(t[0].runs[0].style.font_family, "Plain42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_glyph_name_says_its_character_whatever_its_code() {
|
||||
// The Euro by its name at code 128, where Latin-1 has a control code.
|
||||
// A standard encoding name says its character too (`hyphen`), not one the font's character
|
||||
// map also maps to its glyph (the soft hyphen).
|
||||
let (e, _) = glyph('€');
|
||||
let (h, _) = glyph('-');
|
||||
let program = font(&[e, h])
|
||||
.replace("Encoding 1 /g1 put", "Encoding 128 /Euro put")
|
||||
.replace("/g1 ", "/Euro ")
|
||||
.replace("Encoding 2 /g2 put", "Encoding 45 /hyphen put")
|
||||
.replace("/g2 ", "/hyphen ");
|
||||
let r = read(&format!("{program}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\200-) show"));
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<String>(), "€-");
|
||||
}
|
||||
|
||||
/// A glyph of the bundled font with an outline that the character map doesn't reach (an
|
||||
/// alternate form) and whose name says no character.
|
||||
fn alternate_glyph() -> u32 {
|
||||
let f = FontRef::new(FONT).unwrap();
|
||||
let mapped: std::collections::HashSet<u32> = f.charmap().mappings().map(|(_, g)| g.to_u32()).collect();
|
||||
let names = f.glyph_names();
|
||||
(1..f.glyph_names().num_glyphs())
|
||||
.find(|g| !mapped.contains(g) && super::text::glyph_char(names.get(skrifa::GlyphId::new(*g)).unwrap().as_str()) == '\u{2022}')
|
||||
.and_then(|g| {
|
||||
// One with an outline.
|
||||
let (o, _) = super::type42::glyph(FONT, g)?;
|
||||
(!o.elements().is_empty()).then_some(g)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyphs_that_stand_for_no_character_are_drawn_from_the_font() {
|
||||
let alt = alternate_glyph();
|
||||
let (h, _) = glyph('H');
|
||||
let r = read(&format!("{}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002) show", font(&[h, alt])));
|
||||
assert!(texts(&r).is_empty());
|
||||
let paths = all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count();
|
||||
assert_eq!(paths, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fonts_own_widths_space_its_type_and_move_the_point() {
|
||||
let (h, wh) = glyph('H');
|
||||
let (i, wi) = glyph('I');
|
||||
let f = font(&[h, i]);
|
||||
let check = |cond: String| super::tests::check(&format!("{f} /Plain42 findfont 50 scalefont setfont {cond}"));
|
||||
check(format!("(\\001\\002) stringwidth pop {} sub abs 0.01 lt", 50.0 * (wh + wi)));
|
||||
check(format!("gsave nulldevice 10 20 moveto (\\001\\002) show currentpoint grestore pop {} sub abs 0.01 lt", 10.0 + 50.0 * (wh + wi)));
|
||||
check("gsave nulldevice 10 20 moveto (\\001\\002) [30 7] xshow currentpoint grestore pop 47 sub abs 0.01 lt".into());
|
||||
check("newpath 10 20 moveto (\\001) true charpath pathbbox exch pop sub abs 25 gt exch 10 sub abs 8 lt and".into());
|
||||
}
|
||||
|
||||
/// `xshow` and `ashow` put each character where they say: the type's tracking makes up the
|
||||
/// difference from its font's advances.
|
||||
#[test]
|
||||
fn show_operators_space_live_type_with_tracking() {
|
||||
let (h, wh) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
let f = font(&[h, i, h]);
|
||||
let r = read(&format!("{f}/Plain42 findfont 50 scalefont setfont 10 20 moveto (\\001\\002\\003) [40 25 10] xshow"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.len(), 1);
|
||||
assert_eq!(t[0].plain_text(), "HIH");
|
||||
let o = origins(&t[0]);
|
||||
assert!((o[1] - o[0] - 40.0).abs() < 0.05 && (o[2] - o[1] - 25.0).abs() < 0.05, "{o:?}");
|
||||
let r = read(&format!("{f}/Plain42 findfont 50 scalefont setfont 10 20 moveto 6 0 (\\001\\002) ashow"));
|
||||
let o = origins(&texts(&r)[0]);
|
||||
assert!((o[1] - o[0] - (50.0 * wh + 6.0)).abs() < 0.05, "{o:?}");
|
||||
}
|
||||
|
||||
/// A font found by name (its program isn't in the file) keeps the installed font's spacing,
|
||||
/// plus what the show operator adds.
|
||||
#[test]
|
||||
fn named_fonts_take_the_show_operators_spacing() {
|
||||
let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto (AB) [30 0] xshow");
|
||||
let t = texts(&r);
|
||||
assert_eq!(t[0].plain_text(), "AB");
|
||||
let o = origins(&t[0]);
|
||||
assert!((o[1] - o[0] - 30.0).abs() < 0.05, "{o:?}");
|
||||
// Plain `show` keeps a single run with no tracking.
|
||||
let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto (AB) show");
|
||||
let t = texts(&r);
|
||||
assert!(t[0].runs.len() == 1 && t[0].runs[0].style.tracking == 0.0, "{:?}", t[0].runs);
|
||||
}
|
||||
|
||||
/// Characters the installed font would join in a ligature (`fi`) keep the places the file gives
|
||||
/// each of them: the type is set without ligatures and each character's tracking is its own.
|
||||
#[test]
|
||||
fn characters_a_ligature_would_join_keep_their_places() {
|
||||
// Each character at its own width in the bundled Source Sans 3, as a PDF converter writes them.
|
||||
let (_, wf) = glyph('f');
|
||||
let (_, wi) = glyph('i');
|
||||
let (f, i) = (50.0 * wf, 50.0 * wi);
|
||||
let r = read(&format!("/SourceSans3-Regular findfont 50 scalefont setfont 10 20 moveto (fit) [{f} {i} 0] xshow"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t[0].plain_text(), "fit");
|
||||
let o = origins(&t[0]);
|
||||
assert_eq!(o.len(), 3, "{o:?}");
|
||||
assert!((o[1] - o[0] - f).abs() < 0.05 && (o[2] - o[1] - i).abs() < 0.05, "{o:?} {f} {i}");
|
||||
}
|
||||
|
||||
/// A move that is finite in user space but not along the type's baseline (a font matrix
|
||||
/// squashed a thousandfold) is an `undefinedresult`, not a tracking that isn't a number.
|
||||
#[test]
|
||||
fn moves_too_large_for_the_baseline_are_refused() {
|
||||
let r = read("0 0 10 10 rectfill /Helvetica findfont [0.001 0 0 50 0 0] makefont setfont 10 20 moveto (A\\001) [1e307 -1e307] xshow");
|
||||
assert!(texts(&r).iter().all(|t| t.runs.iter().all(|r| r.style.tracking.is_finite())), "{:?}", texts(&r));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("undefinedresult")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Moves that overflow (two `1e308` numbers in `xshow`) in type drawn from the font's outlines
|
||||
/// are an `undefinedresult` before anything is painted, as for live type: no glyph lands at
|
||||
/// infinity.
|
||||
#[test]
|
||||
fn outline_moves_that_overflow_are_refused() {
|
||||
let alt = alternate_glyph();
|
||||
let (h, _) = glyph('H');
|
||||
let r = read(&format!(
|
||||
"0 0 10 10 rectfill {}/Plain42 findfont 40 scalefont setfont 10 20 moveto (\\001\\002\\001) [1e308 1e308 0] xshow",
|
||||
font(&[h, alt])
|
||||
));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("undefinedresult")), "{:?}", r.warnings);
|
||||
assert_eq!(all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).count(), 1, "{:?}", all(&r.document));
|
||||
}
|
||||
|
||||
/// Reading a font's character map counts the mappings it visits, once, for the operation
|
||||
/// budget.
|
||||
#[test]
|
||||
fn reading_the_character_map_counts_its_mappings() {
|
||||
let f = super::type42::Font::new(FONT.to_vec());
|
||||
let (h, _) = glyph('H');
|
||||
assert_eq!(f.char_of(h), Some('H'));
|
||||
let n = f.take_char_map_work();
|
||||
assert!(n > 100, "{n}");
|
||||
f.char_of(h);
|
||||
assert_eq!(f.take_char_map_work(), 0);
|
||||
}
|
||||
|
||||
/// A glyph the character map doesn't reach is looked up by its glyph name once: the first lookup
|
||||
/// counts for the operation budget, a second one is remembered (a miss too).
|
||||
#[test]
|
||||
fn glyph_name_lookups_are_counted_once() {
|
||||
let f = super::type42::Font::new(FONT.to_vec());
|
||||
let alt = alternate_glyph();
|
||||
let first = f.char_of(alt);
|
||||
f.take_char_map_work();
|
||||
// The character map was read by the first call; the name lookup is remembered.
|
||||
let g = FontRef::new(FONT).unwrap().glyph_names().num_glyphs() + 5;
|
||||
assert_eq!(f.char_of(g), None);
|
||||
assert!(f.take_char_map_work() > 0);
|
||||
assert_eq!(f.char_of(g), None);
|
||||
assert_eq!(f.char_of(alt), first);
|
||||
assert_eq!(f.take_char_map_work(), 0);
|
||||
}
|
||||
|
||||
/// Control codes (a new string's zeros) set no type but are still read: showing or measuring a
|
||||
/// long string of them in a loop runs out of the operation budget.
|
||||
#[test]
|
||||
fn control_codes_in_a_loop_run_out_of_the_budget() {
|
||||
for body in ["{ s stringwidth pop pop } loop", "{ 0 0 moveto s show } loop"] {
|
||||
super::tests::runs_out_of_budget(&format!("/Helvetica findfont 10 scalefont setfont /s 1000000 string def {body}"));
|
||||
}
|
||||
}
|
||||
+336
-75
@@ -168,12 +168,12 @@ impl Interp<'_> {
|
||||
ResourceForAll => self.resource_for_all()?,
|
||||
Show | AShow | WidthShow | AWidthShow | XShow | YShow | XYShow | KShow => {
|
||||
let (s, spacing) = self.show_operands(op)?;
|
||||
if let Some(glyphs) = self.composite_glyphs(&s) {
|
||||
if let Some(glyphs) = self.composite_glyphs(&s)? {
|
||||
self.show_composite(&glyphs, &spacing)?;
|
||||
} else {
|
||||
match self.type3_font() {
|
||||
Some(font) => self.show_type3(&font, &s, &spacing)?,
|
||||
None => self.show(&s)?,
|
||||
None => self.show_plain(&s, &spacing)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,14 +277,20 @@ impl Interp<'_> {
|
||||
|
||||
/// How far `s` moves the current point (user space).
|
||||
fn width_of(&mut self, s: &[u8]) -> Res<Vec2> {
|
||||
if let Some(glyphs) = self.composite_glyphs(s) {
|
||||
if let Some(glyphs) = self.composite_glyphs(s)? {
|
||||
let mut w = Vec2::ZERO;
|
||||
for g in &glyphs {
|
||||
w += self.composite_glyph(g, Point::new(w.x, w.y), Draw::No)?;
|
||||
}
|
||||
return Ok(w);
|
||||
}
|
||||
let Some(font) = self.type3_font() else { return Ok(self.set_type(s, Point::ZERO)?.2) };
|
||||
let Some(font) = self.type3_font() else {
|
||||
if let Some((_, glyphs)) = self.type42_plain(s)? {
|
||||
let (_, f1) = self.font()?;
|
||||
return Ok(glyphs.iter().map(|g| f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO).sum());
|
||||
}
|
||||
return Ok(self.set_type(s, Point::ZERO)?.2);
|
||||
};
|
||||
let mut w = Vec2::ZERO;
|
||||
for &code in s {
|
||||
w += self.type3_glyph(&font, Glyph::Code(code), Point::new(w.x, w.y), false)?;
|
||||
@@ -421,10 +427,18 @@ impl Interp<'_> {
|
||||
/// `s` (read as Latin-1) set in the current font at user point `at`: point type placed there,
|
||||
/// its layout in text space, and how far it moves the current point (user space).
|
||||
fn set_type(&mut self, s: &[u8], at: Point) -> Res<(TextObject, TextLayout, Vec2)> {
|
||||
// Every byte is read, also the control codes left out of the type (a new string's zeros).
|
||||
self.spend(s.len() as u64)?;
|
||||
let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
|
||||
self.set_text(&text, at)
|
||||
}
|
||||
|
||||
/// [`Self::set_type`] for the characters `text`.
|
||||
fn set_text(&mut self, text: &str, at: Point) -> Res<(TextObject, TextLayout, Vec2)> {
|
||||
// Laying out type costs about as much as 300 operations, and 64 more a character
|
||||
// (measured), so `{ (a) stringwidth pop pop } loop` or `cshow` on a long string ends
|
||||
// within the budget like any other loop instead of running for minutes.
|
||||
self.spend((s.len() as u64).saturating_mul(64).saturating_add(300))?;
|
||||
self.spend((text.len() as u64).saturating_mul(64).saturating_add(300))?;
|
||||
let (name, f1) = self.font()?;
|
||||
// Text space (y down, `size` points an em) onto the document.
|
||||
let m = self.xf() * Affine::translate(at.to_vec2()) * f1;
|
||||
@@ -435,10 +449,9 @@ impl Interp<'_> {
|
||||
let (family, font_style) = family_style(&name);
|
||||
// The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei".
|
||||
let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect();
|
||||
let fill = if self.g.paint.is_none() { Paint::solid(vectorcraft_color::Color::BLACK) } else { self.g.paint.clone() };
|
||||
let style = CharStyle { font_family, font_style, size, fill, stroke: Paint::None, ..CharStyle::default() };
|
||||
let mut t = TextObject::point(Point::ZERO, &text, style);
|
||||
let mut t = TextObject::point(Point::ZERO, text, style);
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
t.xf = m * Affine::scale_non_uniform(1.0 / size, -1.0 / size);
|
||||
t.cached_bounds = Some(layout.bounds);
|
||||
@@ -448,19 +461,193 @@ impl Interp<'_> {
|
||||
|
||||
/// `show`: point type at the current point, which moves past it.
|
||||
fn show(&mut self, s: &[u8]) -> Res {
|
||||
self.show_plain(s, &Spacing::Plain)
|
||||
}
|
||||
|
||||
/// `s` shown in a font that isn't a Type 3 or composite font, spaced as `spacing` says: point
|
||||
/// type at the current point, which moves past it as far as the file says, each character
|
||||
/// where the file puts it. A Type 42 font that carries its TrueType font gives each code its
|
||||
/// character (by its glyph name, else the character its glyph stands for in the font's own
|
||||
/// character map) and its glyph's width; type in a font found by name is spaced by the
|
||||
/// installed font's advances. Where the file's spacing differs from the advances of the
|
||||
/// installed (or fallback) font, each character's tracking makes up the difference, so type
|
||||
/// in a font that isn't installed keeps the file's line lengths and justified lines don't
|
||||
/// drift, as PDF import does. Glyphs that stand for no character are drawn from the font's
|
||||
/// outlines (the whole string, as one group).
|
||||
fn show_plain(&mut self, s: &[u8], spacing: &Spacing) -> Res {
|
||||
// Every code gets a move, also the control codes left out of the type.
|
||||
self.spend(s.len() as u64)?;
|
||||
let at = self.user_point()?;
|
||||
let (t, _, adv) = self.set_type(s, at)?;
|
||||
if !self.g.paint.is_none() && !t.plain_text().trim().is_empty() {
|
||||
let (_, f1) = self.font()?;
|
||||
let plain = self.type42_plain(s)?;
|
||||
if let Some((data, glyphs)) = &plain
|
||||
&& glyphs.iter().any(|g| g.ch.is_none() && g.gid != 0)
|
||||
{
|
||||
return self.show_type42_outlines(data, glyphs, s, spacing, at, f1);
|
||||
}
|
||||
// Each code's character, if it has one that shows (control codes don't).
|
||||
let chars: Vec<Option<char>> = match &plain {
|
||||
Some((_, glyphs)) => glyphs.iter().map(|g| g.ch).collect::<Vec<_>>(),
|
||||
None => s.iter().map(|b| Some(char::from(*b))).collect(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|c| c.filter(|c| !c.is_control()))
|
||||
.collect();
|
||||
let text: String = chars.iter().flatten().collect();
|
||||
let (mut t, mut layout, _) = self.set_text(&text, at)?;
|
||||
let size = t.runs.first().map_or(1.0, |r| r.style.size);
|
||||
let starts: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
|
||||
// The file places each character's glyph: where the layout joins characters in a
|
||||
// ligature, the type is set without ligatures, so each character has its own advance.
|
||||
let glyph_starts: std::collections::HashSet<usize> = layout.glyphs.iter().map(|g| g.byte).collect();
|
||||
if starts.iter().any(|b| !glyph_starts.contains(b)) {
|
||||
for run in &mut t.runs {
|
||||
run.style.features.push("-liga".into());
|
||||
}
|
||||
layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
t.cached_bounds = Some(layout.bounds);
|
||||
}
|
||||
// Each character's advance in the layout (text space).
|
||||
let mut natural = vec![0.0; starts.len()];
|
||||
for g in &layout.glyphs {
|
||||
if let Some(n) = starts.binary_search(&g.byte).ok().and_then(|k| natural.get_mut(k)) {
|
||||
*n += g.advance;
|
||||
}
|
||||
}
|
||||
// Each code's move (user space) and that move in text space, along the baseline and across.
|
||||
let em = |x: f64| f1 * Point::new(x, 0.0) - f1 * Point::ZERO;
|
||||
let inv = f1.inverse();
|
||||
let mut moves = Vec::with_capacity(s.len());
|
||||
let mut k = 0;
|
||||
for (i, (&code, c)) in s.iter().zip(&chars).enumerate() {
|
||||
let own = match &plain {
|
||||
Some((_, glyphs)) => glyphs.get(i).map_or(0.0, |g| g.width),
|
||||
None if c.is_some() => natural.get(k).copied().unwrap_or(0.0) / size,
|
||||
None => 0.0,
|
||||
};
|
||||
k += usize::from(c.is_some());
|
||||
let d = spacing.advance(i, i64::from(code), em(own));
|
||||
let e = inv * Point::new(d.x, d.y) - inv * Point::ZERO;
|
||||
if ![d.x, d.y, e.x * size, e.y * size].iter().all(|v| v.is_finite()) {
|
||||
return ps_err("undefinedresult", "a show");
|
||||
}
|
||||
moves.push((d, e.x * size, e.y * size));
|
||||
}
|
||||
let total: Vec2 = moves.iter().map(|m| m.0).sum();
|
||||
if !(total.x.is_finite() && total.y.is_finite()) {
|
||||
return ps_err("undefinedresult", "a show");
|
||||
}
|
||||
let across = moves.iter().any(|m| m.2.abs() > size * 1e-3);
|
||||
// Nothing shows on the null device (`nulldevice`, a Type 3 glyph only measured).
|
||||
let painted = !self.g.paint.is_none() && !self.g.null;
|
||||
if across {
|
||||
// Moves across the baseline (`yshow`, `xyshow`): each character where the file puts it.
|
||||
let mut pen = at;
|
||||
for ((d, ..), c) in moves.iter().zip(&chars) {
|
||||
if let Some(c) = c
|
||||
&& painted
|
||||
&& !c.is_whitespace()
|
||||
{
|
||||
let (t, ..) = self.set_text(c.encode_utf8(&mut [0; 4]), pen)?;
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
||||
}
|
||||
pen += *d;
|
||||
}
|
||||
} else if painted && !text.trim().is_empty() {
|
||||
// The moves of codes before the first character move the type; those of codes after
|
||||
// a character, before the next, are that character's.
|
||||
let mut lead = Vec2::ZERO;
|
||||
let mut target: Vec<f64> = Vec::with_capacity(natural.len());
|
||||
for ((d, along, _), c) in moves.iter().zip(&chars) {
|
||||
match (c, target.last_mut()) {
|
||||
(Some(_), _) => target.push(*along),
|
||||
(None, Some(last)) => *last += along,
|
||||
(None, None) => lead += *d,
|
||||
}
|
||||
}
|
||||
let tracking: Vec<f64> = target
|
||||
.iter()
|
||||
.zip(&natural)
|
||||
// Hundredths of a thousandth of an em: no drift along a line, no noise in the panels.
|
||||
.map(|(want, has)| (((want - has) / size * 1000.0).clamp(-1000.0, 10000.0) * 100.0).round() / 100.0)
|
||||
.collect();
|
||||
if tracking.iter().any(|t| *t != 0.0)
|
||||
&& let Some(run) = t.runs.first().cloned()
|
||||
{
|
||||
let mut runs: Vec<vectorcraft_doc::TextRun> = Vec::new();
|
||||
for (c, tr) in text.chars().zip(&tracking) {
|
||||
match runs.last_mut() {
|
||||
Some(last) if last.style.tracking == *tr => last.text.push(c),
|
||||
_ => runs.push(vectorcraft_doc::TextRun {
|
||||
text: c.to_string(),
|
||||
style: CharStyle { tracking: *tr, ..run.style.clone() },
|
||||
inline: run.inline.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
t.runs = runs;
|
||||
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
|
||||
}
|
||||
if lead != Vec2::ZERO {
|
||||
t.xf = Affine::translate(self.xf() * Point::new(lead.x, lead.y) - self.xf() * Point::ZERO) * t.xf;
|
||||
}
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
||||
}
|
||||
self.g.cur = Some(self.xf() * (at + adv));
|
||||
self.g.cur = Some(self.xf() * (at + total));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `s` in a Type 42 font, some of whose glyphs stand for no character, drawn from the font's
|
||||
/// outlines at the current point (one group), spaced as `spacing` says.
|
||||
fn show_type42_outlines(&mut self, data: &super::type42::Font, glyphs: &[Plain], s: &[u8], spacing: &Spacing, at: Point, f1: Affine) -> Res {
|
||||
// Each glyph's place, checked before anything is painted: moves that overflow (two
|
||||
// `1e308` numbers in `xshow`) are an error, as in `show_plain`, not glyphs at infinity.
|
||||
let mut places = Vec::with_capacity(glyphs.len());
|
||||
let mut pen = at;
|
||||
for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() {
|
||||
places.push(pen);
|
||||
pen += spacing.advance(i, i64::from(code), f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO);
|
||||
if !(pen.x.is_finite() && pen.y.is_finite()) {
|
||||
return ps_err("undefinedresult", "a show");
|
||||
}
|
||||
}
|
||||
let start = self.out.drawn.len();
|
||||
let mut text = String::new();
|
||||
for (g, place) in glyphs.iter().zip(places) {
|
||||
if let Some(outline) = super::type42::glyph(&data.data, g.gid).map(|o| o.0) {
|
||||
self.type42_paint(outline, place, f1, Draw::Fill)?;
|
||||
}
|
||||
text.push(g.ch.unwrap_or('\u{2022}'));
|
||||
}
|
||||
self.g.cur = Some(self.xf() * pen);
|
||||
self.group_glyphs(start, &text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Glyph outline `outline` (glyph space) under font matrix `fm` at user point `at`: filled, or
|
||||
/// added to the current path (`charpath`).
|
||||
fn type42_paint(&mut self, mut outline: vectorcraft_geom::BezPath, at: Point, fm: Affine, paint: Draw) -> Res {
|
||||
// Measuring a glyph reads its outline too, so `stringwidth` in a loop pays for it.
|
||||
self.spend((outline.elements().len() as u64).saturating_add(16))?;
|
||||
if paint == Draw::No {
|
||||
return Ok(());
|
||||
}
|
||||
outline.apply_affine(self.xf() * Affine::translate(at.to_vec2()) * fm);
|
||||
if paint == Draw::Fill {
|
||||
return self.fill_path(outline, vectorcraft_geom::FillRule::NonZero);
|
||||
}
|
||||
if self.g.path.elements().len().saturating_add(outline.elements().len()) > super::graphics::MAX_PATH {
|
||||
return Err(PsError::Limit("a path has too many points"));
|
||||
}
|
||||
self.g.path.extend(outline);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `charpath`: the glyph outlines of `s` added to the path.
|
||||
fn char_path(&mut self, s: &[u8]) -> Res {
|
||||
if let Some(glyphs) = self.composite_glyphs(s) {
|
||||
if let Some(glyphs) = self.composite_glyphs(s)? {
|
||||
for g in &glyphs {
|
||||
let at = self.user_point()?;
|
||||
let w = self.composite_glyph(g, at, Draw::Path)?;
|
||||
@@ -468,6 +655,18 @@ impl Interp<'_> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some((data, glyphs)) = self.type42_plain(s)? {
|
||||
let (_, f1) = self.font()?;
|
||||
let mut pen = self.user_point()?;
|
||||
for g in &glyphs {
|
||||
if let Some((outline, _)) = super::type42::glyph(&data.data, g.gid) {
|
||||
self.type42_paint(outline, pen, f1, Draw::Path)?;
|
||||
}
|
||||
pen += f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO;
|
||||
}
|
||||
self.g.cur = Some(self.xf() * pen);
|
||||
return Ok(());
|
||||
}
|
||||
let at = self.user_point()?;
|
||||
let (t, layout, adv) = self.set_type(s, at)?;
|
||||
for g in &layout.glyphs {
|
||||
@@ -563,15 +762,26 @@ impl Interp<'_> {
|
||||
/// (`FMapType` 2: a byte picking the descendant through `Encoding` and `FDepVector`, then the
|
||||
/// code in it: PLRM 3rd ed., §5.10.1 "Type 0 Font Dictionaries"); `None` for other fonts and
|
||||
/// mappings, which are set as type in the font of the composite font's name.
|
||||
fn composite_glyphs(&self, s: &[u8]) -> Option<Vec<Mapped>> {
|
||||
let font = self.g.font.clone()?;
|
||||
let f = font.borrow();
|
||||
if font_type(&f) != Some(0.0) || f.get(&Key::name("FMapType")).and_then(Obj::as_num) != Some(2.0) {
|
||||
return None;
|
||||
fn composite_glyphs(&mut self, s: &[u8]) -> Res<Option<Vec<Mapped>>> {
|
||||
let Some(font) = self.g.font.clone() else { return Ok(None) };
|
||||
{
|
||||
let f = font.borrow();
|
||||
if font_type(&f) != Some(0.0) || f.get(&Key::name("FMapType")).and_then(Obj::as_num) != Some(2.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
// The scan visits every byte pair, also those that map to no glyph and so cost nothing
|
||||
// further: charged by the string's length first, so a long string of unmapped characters
|
||||
// shown in a loop runs out of the budget.
|
||||
self.spend(s.len() as u64)?;
|
||||
Ok(Self::map_composite(&font.borrow(), s))
|
||||
}
|
||||
|
||||
/// The characters of `s` in composite font `f` (8/8 mapping), as [`Self::composite_glyphs`].
|
||||
fn map_composite(f: &Dict, s: &[u8]) -> Option<Vec<Mapped>> {
|
||||
let (encoding, fonts) = (f.get(&Key::name("Encoding"))?.items()?.clone(), f.get(&Key::name("FDepVector"))?.items()?.clone());
|
||||
let (encoding, fonts) = (encoding.borrow(), fonts.borrow());
|
||||
let outer = font_matrix(&f, 1.0);
|
||||
let outer = font_matrix(f, 1.0);
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
for &[pick, code] in s.as_chunks::<2>().0 {
|
||||
let index = encoding.get(usize::from(pick)).and_then(Obj::as_num).filter(|i| *i >= 0.0 && i.fract() == 0.0);
|
||||
@@ -623,20 +833,7 @@ impl Interp<'_> {
|
||||
}
|
||||
let fm = g.outer * font_matrix(&g.font.borrow(), 1.0);
|
||||
if let Some((outline, advance)) = self.type42_glyph(&g.font, g.code)? {
|
||||
// Measuring a glyph reads its outline too, so `stringwidth` in a loop pays for it.
|
||||
self.spend((outline.elements().len() as u64).saturating_add(16))?;
|
||||
if paint != Draw::No {
|
||||
let mut p = outline;
|
||||
p.apply_affine(self.xf() * Affine::translate(at.to_vec2()) * fm);
|
||||
if paint == Draw::Fill {
|
||||
self.fill_path(p, vectorcraft_geom::FillRule::NonZero)?;
|
||||
} else {
|
||||
if self.g.path.elements().len().saturating_add(p.elements().len()) > super::graphics::MAX_PATH {
|
||||
return Err(PsError::Limit("a path has too many points"));
|
||||
}
|
||||
self.g.path.extend(p);
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -665,59 +862,103 @@ impl Interp<'_> {
|
||||
/// Glyph `code` of Type 42 font `font` from the TrueType font it carries: its outline and
|
||||
/// width in its glyph space (ems), or `None` when it isn't a Type 42 font with one.
|
||||
fn type42_glyph(&mut self, font: &DictRef, code: u8) -> Res<Option<(vectorcraft_geom::BezPath, f64)>> {
|
||||
let (sfnts, gid) = {
|
||||
let Some(data) = self.type42_data(font)? else { return Ok(None) };
|
||||
let gid = {
|
||||
let f = font.borrow();
|
||||
let name = glyph_name(&f, code);
|
||||
char_string(&f, &name).or_else(|| char_string(&f, ".notdef")).unwrap_or(0)
|
||||
};
|
||||
Ok(super::type42::glyph(&data.data, gid))
|
||||
}
|
||||
|
||||
/// The codes of `s` in the current font when it is a Type 42 font that carries its TrueType
|
||||
/// font (not a descendant of a composite font): the font, and each code's glyph.
|
||||
fn type42_plain(&mut self, s: &[u8]) -> Res<Option<(Rc<super::type42::Font>, Vec<Plain>)>> {
|
||||
let Some(font) = self.g.font.clone() else { return Ok(None) };
|
||||
let Some(data) = self.type42_data(&font)? else { return Ok(None) };
|
||||
self.spend((s.len() as u64).saturating_mul(4))?;
|
||||
let f = font.borrow();
|
||||
let glyphs = s
|
||||
.iter()
|
||||
.map(|&code| {
|
||||
let (ch, gid) = type42_code(&f, &data, code);
|
||||
Plain { ch, gid, width: super::type42::advance(&data.data, gid) }
|
||||
})
|
||||
.collect();
|
||||
drop(f);
|
||||
// Reading the font's character map (once) visits each of its mappings.
|
||||
self.spend(data.take_char_map_work())?;
|
||||
Ok(Some((data, glyphs)))
|
||||
}
|
||||
|
||||
/// The TrueType font Type 42 font `font` carries (read once for each `sfnts` array), or
|
||||
/// `None` when it isn't a Type 42 font with one.
|
||||
fn type42_data(&mut self, font: &DictRef) -> Res<Option<Rc<super::type42::Font>>> {
|
||||
let sfnts = {
|
||||
let f = font.borrow();
|
||||
if font_type(&f) != Some(42.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(Obj::Array { items: sfnts, .. }) = f.get(&Key::name("sfnts")).cloned() else { return Ok(None) };
|
||||
let name = glyph_name(&f, code);
|
||||
let strings = f.get(&Key::name("CharStrings")).and_then(|c| match c {
|
||||
Obj::Dict(d) => Some(d.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let gid = strings
|
||||
.and_then(|d| {
|
||||
let d = d.borrow();
|
||||
d.get(&Key::name(&name)).or_else(|| d.get(&Key::name(".notdef"))).and_then(Obj::as_num)
|
||||
})
|
||||
.filter(|g| (0.0..=f64::from(u16::MAX)).contains(g))
|
||||
.map_or(0, |g| g as u32);
|
||||
(sfnts, gid)
|
||||
sfnts
|
||||
};
|
||||
// The least recently used font goes first: a hit moves to the end.
|
||||
let data = match self.type42.iter().position(|(k, _)| k.same(&sfnts)) {
|
||||
Some(i) => {
|
||||
let hit = self.type42.remove(i);
|
||||
let data = hit.1.clone();
|
||||
self.type42.push(hit);
|
||||
data
|
||||
}
|
||||
None => {
|
||||
// Joining the strings visits each element and copies its bytes, whether or not
|
||||
// the font can then be read: charged first, as other bulk copies are.
|
||||
let (elements, bytes) = super::type42::font_size(&sfnts.borrow());
|
||||
self.spend_bytes(bytes)?;
|
||||
self.spend((elements / 16) as u64)?;
|
||||
// Fonts whose data can't be read are kept too, so they are copied only once.
|
||||
let data = super::type42::font_data(&sfnts.borrow()).map(Rc::new);
|
||||
if let Some(d) = &data
|
||||
&& self.type42_counted.insert(sfnts.clone())
|
||||
{
|
||||
self.alloc(d.len())?;
|
||||
}
|
||||
if self.type42.len() >= MAX_TYPE42 {
|
||||
self.type42.remove(0);
|
||||
}
|
||||
self.type42.push((sfnts, data.clone()));
|
||||
data
|
||||
}
|
||||
};
|
||||
let Some(data) = data else { return Ok(None) };
|
||||
Ok(super::type42::glyph(&data, gid))
|
||||
if let Some(i) = self.type42.iter().position(|(k, _)| k.same(&sfnts)) {
|
||||
let hit = self.type42.remove(i);
|
||||
let data = hit.1.clone();
|
||||
self.type42.push(hit);
|
||||
return Ok(data);
|
||||
}
|
||||
// Joining the strings visits each element and copies its bytes, whether or not the font
|
||||
// can then be read: charged first, as other bulk copies are.
|
||||
let (elements, bytes) = super::type42::font_size(&sfnts.borrow());
|
||||
self.spend_bytes(bytes)?;
|
||||
self.spend((elements / 16) as u64)?;
|
||||
// Fonts whose data can't be read are kept too, so they are copied only once.
|
||||
let data = super::type42::font_data(&sfnts.borrow()).map(|d| Rc::new(super::type42::Font::new(d)));
|
||||
if let Some(d) = &data
|
||||
&& self.type42_counted.insert(sfnts.clone())
|
||||
{
|
||||
self.alloc(d.data.len())?;
|
||||
}
|
||||
if self.type42.len() >= MAX_TYPE42 {
|
||||
self.type42.remove(0);
|
||||
}
|
||||
self.type42.push((sfnts, data.clone()));
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// A character code of a Type 42 font: the character it stands for (`None` when neither its
|
||||
/// glyph name nor the font says), its glyph and the glyph's width (ems).
|
||||
struct Plain {
|
||||
ch: Option<char>,
|
||||
gid: u32,
|
||||
width: f64,
|
||||
}
|
||||
|
||||
/// The glyph index `CharStrings` gives glyph name `name` in Type 42 font `f`.
|
||||
fn char_string(f: &Dict, name: &str) -> Option<u32> {
|
||||
let Some(Obj::Dict(d)) = f.get(&Key::name("CharStrings")) else { return None };
|
||||
d.borrow().get(&Key::name(name)).and_then(Obj::as_num).filter(|g| (0.0..=f64::from(u16::MAX)).contains(g)).map(|g| g as u32)
|
||||
}
|
||||
|
||||
/// The character and glyph of code `code` in Type 42 font `f`, carrying TrueType font `data`:
|
||||
/// by the glyph name its `Encoding` gives it (a name such as `A`, `uni20AC` or `Euro` says the
|
||||
/// character; else the glyph `CharStrings` gives it stands for one in the font's character map
|
||||
/// or `post` names). A code the encoding leaves `.notdef` (the standard encodings here have no
|
||||
/// names) is read as Latin-1, drawn by that character's glyph.
|
||||
fn type42_code(f: &Dict, data: &super::type42::Font, code: u8) -> (Option<char>, u32) {
|
||||
let name = glyph_name(f, code);
|
||||
if &*name == ".notdef" {
|
||||
let c = char::from(code);
|
||||
return (Some(c), data.gid_of(c).or_else(|| char_string(f, ".notdef")).unwrap_or(0));
|
||||
}
|
||||
let named = named_char(code, &name);
|
||||
let gid = char_string(f, &name).or_else(|| named.and_then(|c| data.gid_of(c))).or_else(|| char_string(f, ".notdef")).unwrap_or(0);
|
||||
(named.or_else(|| data.char_of(gid)), gid)
|
||||
}
|
||||
|
||||
/// Most Type 42 fonts whose TrueType data is kept between glyphs.
|
||||
const MAX_TYPE42: usize = 16;
|
||||
|
||||
@@ -735,6 +976,26 @@ 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"))
|
||||
}
|
||||
|
||||
/// The character glyph name `name` (at code `code`) says: a single character or `uniXXXX`; a
|
||||
/// name of the standard encoding's printable ASCII range (`hyphen`, `quotedbl`: PLRM 3rd ed.,
|
||||
/// Appendix E.6); one of the Windows-1252 names; else `None`.
|
||||
fn named_char(code: u8, name: &str) -> Option<char> {
|
||||
match glyph_char(name) {
|
||||
'\u{2022}' if name != "bullet" => {}
|
||||
c => return Some(c),
|
||||
}
|
||||
use skrifa::raw::ps::encoding::PredefinedEncoding;
|
||||
if let Some(c) = (0x20u8..0x7F).find(|c| PredefinedEncoding::Standard.name(*c) == name) {
|
||||
// The standard encoding has curly quotes where ASCII has the straight quote and the grave.
|
||||
return Some(match c {
|
||||
0x27 => '\u{2019}',
|
||||
0x60 => '\u{2018}',
|
||||
c => char::from(c),
|
||||
});
|
||||
}
|
||||
crate::latin::glyph(code, name)
|
||||
}
|
||||
|
||||
/// A font scaled or transformed by `m` (`scalefont`, `makefont`).
|
||||
fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
let mut d = font.borrow().clone();
|
||||
@@ -744,7 +1005,7 @@ fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
}
|
||||
|
||||
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
|
||||
fn glyph_char(name: &str) -> char {
|
||||
pub(super) fn glyph_char(name: &str) -> char {
|
||||
let mut chars = name.chars();
|
||||
if let (Some(c), None) = (chars.next(), chars.next()) {
|
||||
return c;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use skrifa::{FontRef, GlyphId, MetadataProvider};
|
||||
use vectorcraft_geom::BezPath;
|
||||
|
||||
@@ -45,6 +48,74 @@ pub(super) fn font_size(sfnts: &[Obj]) -> (usize, usize) {
|
||||
(sfnts.len(), bytes)
|
||||
}
|
||||
|
||||
/// The TrueType font a Type 42 font carries, with the character each glyph stands for (read
|
||||
/// when first asked).
|
||||
pub(super) struct Font {
|
||||
pub data: Vec<u8>,
|
||||
chars: OnceCell<HashMap<u32, char>>,
|
||||
/// The characters glyph names give glyphs the character map doesn't reach, misses too:
|
||||
/// a custom `post` name is found by scanning the names before it, so each glyph is looked
|
||||
/// up once.
|
||||
names: RefCell<HashMap<u32, Option<char>>>,
|
||||
/// The character-map mappings visited reading `chars`, not yet counted against the
|
||||
/// operation budget (see [`Self::take_char_map_work`]).
|
||||
work: Cell<u64>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(data: Vec<u8>) -> Self {
|
||||
Self { data, chars: OnceCell::new(), names: RefCell::new(HashMap::new()), work: Cell::new(0) }
|
||||
}
|
||||
|
||||
/// The work reading the character map did since last asked (mappings visited).
|
||||
pub fn take_char_map_work(&self) -> u64 {
|
||||
self.work.take()
|
||||
}
|
||||
|
||||
/// The character glyph `gid` stands for: the one the font's character map maps to it
|
||||
/// (the lowest, when it maps several), else the one its `post` table's glyph name says
|
||||
/// (`uni20AC`, a single character). `None` for a glyph neither names, and for one a symbol
|
||||
/// font maps only from its private-use codes (U+F000–U+F0FF), which aren't characters.
|
||||
pub fn char_of(&self, gid: u32) -> Option<char> {
|
||||
let chars = self.chars.get_or_init(|| {
|
||||
let mut m = HashMap::new();
|
||||
if let Ok(font) = FontRef::new(&self.data) {
|
||||
for (c, g) in font.charmap().mappings() {
|
||||
self.work.set(self.work.get().saturating_add(1));
|
||||
// Not a symbol font's private-use codes, controls or the soft hyphen, which
|
||||
// doesn't show (a hyphen glyph mapped from it is the hyphen's).
|
||||
let Some(c) = char::from_u32(c).filter(|c| !('\u{F000}'..='\u{F0FF}').contains(c) && !c.is_control() && *c != '\u{AD}') else {
|
||||
continue;
|
||||
};
|
||||
m.entry(g.to_u32()).and_modify(|e: &mut char| *e = (*e).min(c)).or_insert(c);
|
||||
}
|
||||
}
|
||||
m
|
||||
});
|
||||
if let Some(c) = chars.get(&gid) {
|
||||
return Some(*c);
|
||||
}
|
||||
if let Some(c) = self.names.borrow().get(&gid) {
|
||||
return *c;
|
||||
}
|
||||
// Finding a custom name reads the names before it: about one operation for every 16.
|
||||
self.work.set(self.work.get().saturating_add(u64::from(gid / 16) + 1));
|
||||
let c = FontRef::new(&self.data).ok().and_then(|font| {
|
||||
let name = font.glyph_names().get(GlyphId::new(gid))?;
|
||||
let c = super::text::glyph_char(name.as_str());
|
||||
(c != '\u{2022}' || name.as_str() == "bullet").then_some(c)
|
||||
});
|
||||
self.names.borrow_mut().insert(gid, c);
|
||||
c
|
||||
}
|
||||
|
||||
/// The glyph the font's character map gives `c`.
|
||||
pub fn gid_of(&self, c: char) -> Option<u32> {
|
||||
let font = FontRef::new(&self.data).ok()?;
|
||||
font.charmap().map(c).map(|g| g.to_u32()).filter(|g| *g != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Glyph `gid` of TrueType font `data`: its outline and advance width, in ems (y up), or `None`
|
||||
/// when the font can't be read.
|
||||
pub(super) fn glyph(data: &[u8], gid: u32) -> Option<(BezPath, f64)> {
|
||||
@@ -65,6 +136,14 @@ pub(super) fn glyph(data: &[u8], gid: u32) -> Option<(BezPath, f64)> {
|
||||
Some((pen.0, if advance.is_finite() { advance } else { 0.0 }))
|
||||
}
|
||||
|
||||
/// The advance width of glyph `gid` of TrueType font `data`, in ems (0 when it can't be read).
|
||||
pub(super) fn advance(data: &[u8], gid: u32) -> f64 {
|
||||
let Ok(font) = FontRef::new(data) else { return 0.0 };
|
||||
let upem = f64::from(font.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
|
||||
let w = font.glyph_metrics(Size::unscaled(), LocationRef::default()).advance_width(GlyphId::new(gid)).map_or(0.0, f64::from) / upem;
|
||||
if w.is_finite() { w } else { 0.0 }
|
||||
}
|
||||
|
||||
/// A pen drawing an outline scaled from font units to ems.
|
||||
struct Pen(BezPath, f64);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (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: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -281,7 +281,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
| Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) |
|
||||
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure with template layers and ruler guides, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus |
|
||||
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings and CID-keyed fonts are still set as type in the font of their name); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths; composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings and CID-keyed fonts are still set as type in the font of their name); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| 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: a plain Type 42 font that carries its TrueType font ([Adobe Technical Note #5012](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5012.Type42_Spec.pdf)) gives each code its character by its glyph name (single characters, `uniXXXX`, the standard encoding's and Windows-1252's names) or, when the name says nothing (`/g12`), by the glyph `CharStrings` gives it in the font's own character map or `post` names, instead of reading the code as Latin-1 (a Euro at code 128 was dropped); the font's own widths move the current point (`stringwidth`, `show`), `charpath` outlines its glyphs, and a string with a glyph that stands for no character is drawn from the font's outlines. Live type keeps the spacing the file gives it: where the show operator's numbers (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`: PLRM 3rd ed., chapter 8) or the embedded font's widths differ from the installed or fallback font's advances, each character's tracking makes up the difference, as PDF import does, so justified type in a font that isn't installed no longer drifts (pdftocairo and pdftops EPS of a justified PDF page set in a missing font: 10.2% and 9.0% of the page differing from Ghostscript's render, now 6.2% and 5.8%). Type under `nulldevice` no longer shows. |
|
||||
| 2026-11-06 | minor | EPS import: the buffers an image decoder makes for itself (its sample planes, its copies of the strings a procedure data source hands back, and the split buffers of an interleaved masked image) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept, and anything the program's own procedures allocate, still count. A user's Illustrator 25.2 EPS with 71 CMYK images (used locally, not committed) used to stop with "the program uses too much memory" and come in as its screen preview; it now opens with its art. The budget itself is unchanged. Scores and estimates unchanged. |
|
||||
| 2026-11-07 | minor | EPS import: a sampled function (type 0) of two or more inputs took the nearest sample, so a function-based shading (type 1) over one came in as flat blocks; it is now interpolated between its samples (multilinearly), and with `Order 3` by cubics through them (Catmull-Rom, kept in the function's range), as the PostScript Language Reference (3rd ed., §3.10.1, type 0 functions) defines. An axial or radial shading whose function is a linearly interpolated sampled function of one input has a stop at each sample, not 33 evenly spaced ones. Tests `sampled_functions_interpolate_linearly_or_by_cubics`, `mesh_and_function_shadings_become_gradient_meshes`; a 2 × 2 function-based shading is 1.2% from Ghostscript's render (it was 45.9%); scores unchanged |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document keeps its paragraphs' tab stops (positions and alignments; leaders aren't read), at most 1,024 a paragraph and 2^20 a story, past which the default stops apply with a warning. A user's `.ai` (used locally, not committed) whose price lists came in on the default stops now lays them out as its page has them (its type 0.8% from its page, from 1.5%; what differs is hyphenation in Hungarian and type in a font that isn't installed). Scores and estimates unchanged. |
|
||||
|
||||
+1
-1
@@ -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 font programs are skipped). In a file in Illustrator's own format
|
||||
point type in the font the file names (embedded Type 1 font programs are skipped; a Type 42 font's characters and widths come from the TrueType font it carries, and the show operators' spacing is kept as tracking). 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,
|
||||
|
||||
Reference in New Issue
Block a user