mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 20:01:37 +00:00
Merge pull request #5999 from mockydevs/fix/eps-composite-fonts
EPS import: composite fonts draw their Type 42 glyphs (pdftops EPS shows its type)
This commit is contained in:
Generated
+1
@@ -6338,6 +6338,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"ruzstd",
|
||||
"serde_json",
|
||||
"skrifa 0.47.0",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
|
||||
@@ -2381,6 +2381,53 @@ proptest! {
|
||||
);
|
||||
survive("TIFF-predicted EPS images", || vectorcraft_eps::import(text.as_bytes()).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// Damaged or hostile TrueType data in a Type 42 font that a composite font shows (its table
|
||||
/// directory and its glyphs), any glyph index: drawn, measured and outlined, or left out.
|
||||
#[test]
|
||||
fn eps_type42_glyphs_never_panic(
|
||||
head in prop::collection::vec((0usize..2048, any::<u8>()), 0..12),
|
||||
body in prop::collection::vec((0usize..TYPE42_FONT.len(), any::<u8>()), 0..12),
|
||||
gid in 0u32..70_000,
|
||||
) {
|
||||
let mut font = TYPE42_FONT.to_vec();
|
||||
for (at, b) in head.into_iter().chain(body) {
|
||||
if let Some(x) = font.get_mut(at) {
|
||||
*x = b;
|
||||
}
|
||||
}
|
||||
let text = type42_composite_eps(&font, gid);
|
||||
survive("damaged Type 42 glyphs", || vectorcraft_eps::import(text.as_bytes()).ok().map(|r| r.document))?;
|
||||
}
|
||||
}
|
||||
|
||||
/// The TrueType font [`type42_composite_eps`] carries.
|
||||
const TYPE42_FONT: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
|
||||
/// An EPS showing glyph `gid` of TrueType font `font` through a Type 42 font and a composite font
|
||||
/// over it, and measuring and outlining it.
|
||||
fn type42_composite_eps(font: &[u8], gid: u32) -> String {
|
||||
let sfnts: String = font.chunks(32_000).map(|c| format!("<{}>\n", c.iter().map(|b| format!("{b:02x}")).collect::<String>())).collect();
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\
|
||||
10 dict begin /FontName /T42 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 /c01 put\n\
|
||||
/CharStrings 2 dict def CharStrings begin /.notdef 0 def /c01 {gid} def end /sfnts [\n{sfnts}] def FontName currentdict end definefont pop\n\
|
||||
16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def /Encoding [0] def\n\
|
||||
/FDepVector [/T42 findfont] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 40 scalefont setfont 10 20 moveto (\\000\\001\\000\\001) show (\\000\\001) stringwidth pop pop\n\
|
||||
10 60 moveto (\\000\\001) true charpath fill\nshowpage\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// [`type42_composite_eps`] as it is: the glyph draws as filled outlines, not as type.
|
||||
#[test]
|
||||
fn the_type42_fixture_draws_its_glyphs() {
|
||||
let r = vectorcraft_eps::import(type42_composite_eps(TYPE42_FONT, 9).as_bytes()).unwrap();
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let mut kinds = vec![];
|
||||
r.document.walk(|n| kinds.push(n.kind_label()));
|
||||
assert!(!kinds.contains(&"Text") && kinds.iter().filter(|k| **k == "Path").count() >= 3, "{kinds:?}");
|
||||
}
|
||||
|
||||
/// An EPS with two 8 × 3 CCITT fax images that the PostScript interpreter reads, coded by hand from
|
||||
|
||||
@@ -17,6 +17,8 @@ vectorcraft-effects = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
vectorcraft-brush = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
# The glyph outlines of Type 42 fonts a composite font shows.
|
||||
skrifa = { workspace = true }
|
||||
# Windows metafile previews, opened when a file's PostScript can't be read.
|
||||
vectorcraft-metafile = { workspace = true }
|
||||
# Non-native art in Illustrator editing data (a placed PDF's content, kept as a PDF).
|
||||
|
||||
@@ -44,10 +44,13 @@ mod obj;
|
||||
mod preview;
|
||||
mod shading;
|
||||
mod text;
|
||||
mod type42;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_composite;
|
||||
#[cfg(test)]
|
||||
mod tests_fontnames;
|
||||
#[cfg(test)]
|
||||
mod tests_generators;
|
||||
|
||||
@@ -42,7 +42,7 @@ const MAX_DASH_ENTRIES: usize = 65_535;
|
||||
/// Most entries an indexed colour space's table has.
|
||||
const MAX_HIVAL: usize = 4095;
|
||||
/// Most elements the current path may have.
|
||||
const MAX_PATH: usize = 1 << 20;
|
||||
pub(crate) const MAX_PATH: usize = 1 << 20;
|
||||
/// Most path elements drawn in all (each object's clips and groups counting as elements too).
|
||||
const MAX_DRAWN: usize = 1 << 24;
|
||||
/// Samples taken of a shading function that isn't a plain interpolation.
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Interp<'_> {
|
||||
|
||||
// ---------- painting ----------
|
||||
|
||||
pub(super) fn fill_path(&mut self, bp: BezPath, rule: FillRule) -> Res {
|
||||
pub(in super::super) fn fill_path(&mut self, bp: BezPath, rule: FillRule) -> Res {
|
||||
if bp.elements().is_empty() || self.g.null {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ pub(crate) struct Interp<'a> {
|
||||
pub fault: Option<Fault>,
|
||||
/// The width a Type 3 glyph procedure gave (`setcachedevice`, `setcharwidth`).
|
||||
pub glyph_width: Option<[f64; 2]>,
|
||||
/// The TrueType data of the Type 42 fonts drawn so far, by their `sfnts` array (see
|
||||
/// `type42`; `None` when it can't be read), the most recently used last.
|
||||
pub type42: Type42Cache,
|
||||
/// The `sfnts` arrays whose TrueType data has been counted against [`MAX_MEMORY`]: a font
|
||||
/// read again after it left `type42` doesn't count again.
|
||||
pub type42_counted: HashSet<Shared<Obj>>,
|
||||
pub g: GState,
|
||||
pub saved: Vec<GState>,
|
||||
pub out: Out,
|
||||
@@ -94,6 +100,9 @@ pub(crate) struct Fault {
|
||||
pub procs: Vec<Rc<str>>,
|
||||
}
|
||||
|
||||
/// 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>>>)>;
|
||||
|
||||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
|
||||
@@ -189,6 +198,8 @@ impl<'a> Interp<'a> {
|
||||
journal: vec![],
|
||||
fault: None,
|
||||
glyph_width: None,
|
||||
type42: vec![],
|
||||
type42_counted: HashSet::new(),
|
||||
g,
|
||||
saved: vec![],
|
||||
out,
|
||||
@@ -362,7 +373,7 @@ impl<'a> Interp<'a> {
|
||||
|
||||
/// Charge for work on `n` bytes or objects in one operator: one operation per 256, as `search`
|
||||
/// charges per 64 bytes, so a loop of big copies runs out of the budget.
|
||||
fn spend_bytes(&mut self, n: usize) -> Res {
|
||||
pub fn spend_bytes(&mut self, n: usize) -> Res {
|
||||
self.spend((n / 256) as u64)
|
||||
}
|
||||
|
||||
|
||||
@@ -1125,7 +1125,7 @@ fn read_within(body: &str, secs: u64) -> crate::Imported {
|
||||
/// budget. Running to the end of the budget can take over 20 s on a slow CI runner
|
||||
/// (`search_in_a_loop_runs_out_of_the_budget` searches about 3 GB, 14 s on a fast desktop), so the
|
||||
/// limit here only catches a program that never stops.
|
||||
fn runs_out_of_budget(body: &str) {
|
||||
pub(super) fn runs_out_of_budget(body: &str) {
|
||||
let r = read_within(&format!("0 0 10 10 rectfill {body}"), 120);
|
||||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{body}: {:?}", r.warnings);
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
//! Composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write a PDF's
|
||||
//! two-byte fonts: each character a byte picking the descendant font and a byte for the code in
|
||||
//! it (PLRM 3rd ed., §5.10). Type 42 descendants draw their glyphs from the TrueType font they
|
||||
//! carry (Adobe Technical Note #5012); the fonts here are written from scratch around the bundled
|
||||
//! Source Sans 3.
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::{FontRef, MetadataProvider};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::NodeKind;
|
||||
|
||||
use super::tests::{all, bounds, fill, read};
|
||||
|
||||
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) {
|
||||
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);
|
||||
let adv = font.glyph_metrics(Size::unscaled(), LocationRef::default()).advance_width(gid).unwrap();
|
||||
(gid.to_u32(), f64::from(adv) / upem)
|
||||
}
|
||||
|
||||
/// `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 {
|
||||
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) {
|
||||
out.push_str(&format!("<{}>\n", hex(chunk)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A program defining Type 42 font `/T42` (codes 1 and 2 are H and I, by glyph names that
|
||||
/// don't say which characters they are) and composite font `/Comp` over it.
|
||||
fn fonts() -> String {
|
||||
let (h, _) = glyph('H');
|
||||
let (i, _) = glyph('I');
|
||||
format!(
|
||||
"10 dict begin /FontName /T42 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 /c01 put Encoding 2 /c02 put\n\
|
||||
/CharStrings 3 dict def CharStrings begin /.notdef 0 def /c01 {h} def /c02 {i} def end\n\
|
||||
/sfnts [\n{}] def FontName currentdict end definefont pop\n\
|
||||
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 [/T42 findfont] def FontName currentdict end definefont pop\n",
|
||||
sfnts()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type42_descendants_draw_their_own_glyphs() {
|
||||
let (_, wh) = glyph('H');
|
||||
let (_, wi) = glyph('I');
|
||||
let r = read(&format!("{}/Comp findfont 50 scalefont setfont 0 0 1 setrgbcolor 10 20 moveto (\\000\\001\\000\\002) show", fonts()));
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
let nodes = all(&r.document);
|
||||
assert!(!nodes.iter().any(|n| matches!(n.kind, NodeKind::Text(_))));
|
||||
let paths: Vec<_> = nodes.iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).collect();
|
||||
assert_eq!(paths.len(), 2);
|
||||
for p in &paths {
|
||||
assert_eq!(fill(p), Some(&Paint::solid(Color::rgb(0.0, 0.0, 1.0))));
|
||||
}
|
||||
// The H, then the I one H's width along; both stand on the baseline, a cap height tall.
|
||||
let (bh, bi) = (bounds(paths[0]), bounds(paths[1]));
|
||||
assert!((bh.x0 - 10.0).abs() < 8.0 && bh.width() > 20.0 && bh.width() < 50.0 * wh, "{bh:?}");
|
||||
assert!((bh.height() - bi.height()).abs() < 0.5 && bh.height() > 25.0 && bh.height() < 40.0, "{bh:?} {bi:?}");
|
||||
assert!(bi.x0 > 10.0 + 50.0 * wh - 1.0 && bi.x1 < 10.0 + 50.0 * (wh + wi), "{bi:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type42_descendants_measure_space_and_outline_their_glyphs() {
|
||||
let (_, wh) = glyph('H');
|
||||
let (_, wi) = glyph('I');
|
||||
let check = |cond: String| super::tests::check(&format!("{} /Comp findfont 50 scalefont setfont {cond}", fonts()));
|
||||
// The width its font gives each glyph.
|
||||
check(format!("(\\000\\001\\000\\002) stringwidth pop {} sub abs 0.01 lt", 50.0 * (wh + wi)));
|
||||
// xyshow places each character where its numbers say.
|
||||
check("gsave nulldevice 10 20 moveto (\\000\\001\\000\\002) [30 0 7 0] xyshow currentpoint grestore pop 47 sub abs 0.01 lt".into());
|
||||
// charpath outlines them.
|
||||
check("newpath 10 20 moveto (\\000\\001) true charpath pathbbox exch pop sub abs 25 gt exch 10 sub abs 8 lt and".into());
|
||||
}
|
||||
|
||||
/// A descendant that is a font by name only (its program isn't run) sets its characters as type,
|
||||
/// at the size the composite font's matrix gives them, not at 1000 times it.
|
||||
#[test]
|
||||
fn named_descendants_set_type_at_the_composite_fonts_size() {
|
||||
let r = read(
|
||||
"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 [/Helvetica findfont] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 30 scalefont setfont 10 20 moveto (\\000A\\000B) show",
|
||||
);
|
||||
let sizes: Vec<f64> = all(&r.document)
|
||||
.iter()
|
||||
.filter_map(|n| match &n.kind {
|
||||
NodeKind::Text(t) => Some(t.runs.iter().map(|r| r.style.size * t.xf.determinant().abs().sqrt()).sum::<f64>() / t.runs.len() as f64),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(!sizes.is_empty() && sizes.iter().all(|s| (s - 30.0).abs() < 0.5), "{sizes:?}");
|
||||
let bounds_all = all(&r.document).iter().map(|n| bounds(n)).reduce(|a, b| a.union(b)).unwrap();
|
||||
assert!(bounds_all.width() < 60.0, "{bounds_all:?}");
|
||||
}
|
||||
|
||||
/// `widthshow` adds its spacing to the composite character whose font number times 256 plus code
|
||||
/// is its `char` (PLRM 3rd ed., `widthshow` in chapter 8), not to every character with that code.
|
||||
#[test]
|
||||
fn widthshow_matches_a_composite_character_by_font_number_and_code() {
|
||||
let (_, wh) = glyph('H');
|
||||
let fonts = fonts().replace("/Encoding [0] def", "/Encoding [0 0] def");
|
||||
let check = |cond: String| super::tests::check(&format!("{fonts} /Comp findfont 50 scalefont setfont {cond}"));
|
||||
let end = |ch: u32| format!("gsave nulldevice 10 20 moveto 20 0 {ch} (\\000\\001\\001\\001) widthshow currentpoint grestore pop");
|
||||
// 257 is font 1's code 1: the second H only.
|
||||
check(format!("{} {} sub abs 0.01 lt", end(257), 10.0 + 100.0 * wh + 20.0));
|
||||
// 1 is font 0's code 1: the first H only.
|
||||
check(format!("{} {} sub abs 0.01 lt", end(1), 10.0 + 100.0 * wh + 20.0));
|
||||
// 2 is neither.
|
||||
check(format!("{} {} sub abs 0.01 lt", end(2), 10.0 + 100.0 * wh));
|
||||
}
|
||||
|
||||
/// Measuring a composite font's Type 42 glyphs (`stringwidth` on a long string in a `loop`)
|
||||
/// counts against the operation budget, as drawing them does.
|
||||
#[test]
|
||||
fn measuring_type42_descendants_in_a_loop_runs_out_of_the_budget() {
|
||||
super::tests::runs_out_of_budget(&format!(
|
||||
"{} /Comp findfont 50 scalefont setfont /s 100000 string def {{ s stringwidth pop pop }} loop",
|
||||
fonts()
|
||||
));
|
||||
}
|
||||
|
||||
/// A descendant whose TrueType data can't be read (an `sfnts` element that isn't a string, after
|
||||
/// 32 MB of strings) is read once, not again for every character measured: copying its strings
|
||||
/// costs about 130,000 operations, so 1,000 measures finish only if it is copied once (reading
|
||||
/// it each time would need 130 million, over the budget of 50 million).
|
||||
#[test]
|
||||
fn unreadable_type42_data_is_read_once() {
|
||||
super::tests::check(
|
||||
"10 dict begin /FontName /Bad 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 /.notdef 0 put /sfnts [ 0 1 7 { pop 4000000 string } for 0 ] def FontName currentdict end definefont pop\n\
|
||||
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 [/Bad findfont] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont 1000 { (\\000\\001) stringwidth pop pop } repeat true",
|
||||
);
|
||||
}
|
||||
|
||||
/// A Type 3 descendant draws and measures its glyphs with its own procedure, under the composite
|
||||
/// font's matrix: here 50 pt and twice as wide, so a glyph 500 × 700 units wide (600 units
|
||||
/// apart) is 50 × 35 pt, 60 pt apart (PLRM 3rd ed., §5.10: a descendant's glyphs are under the
|
||||
/// composite font's matrix as well as its own).
|
||||
#[test]
|
||||
fn type3_descendants_draw_and_measure_under_the_composite_matrix() {
|
||||
let fonts = "8 dict begin /FontName /T3 def /FontType 3 def /FontMatrix [0.001 0 0 0.001 0 0] def /FontBBox [0 0 1000 1000] def\n\
|
||||
/Encoding 256 array def 0 1 255 { Encoding exch /.notdef put } for Encoding 1 /box put\n\
|
||||
/BuildChar { exch begin 1 eq { 600 0 0 0 500 700 setcachedevice 0 0 500 700 rectfill } { 0 0 setcharwidth } ifelse end } def\n\
|
||||
FontName currentdict end definefont pop\n\
|
||||
16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [2 0 0 1 0 0] def /FMapType 2 def\n\
|
||||
/Encoding [0] def /FDepVector [/T3 findfont] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont";
|
||||
let r = read(&format!("{fonts} 0 0 1 setrgbcolor 10 20 moveto (\\000\\001\\000\\001) show"));
|
||||
let paths: Vec<_> = all(&r.document).into_iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).collect();
|
||||
assert_eq!(paths.len(), 2, "{:?}", r.warnings);
|
||||
let (a, b) = (bounds(&paths[0]), bounds(&paths[1]));
|
||||
for g in [a, b] {
|
||||
assert!((g.width() - 50.0).abs() < 0.01 && (g.height() - 35.0).abs() < 0.01, "{g:?}");
|
||||
}
|
||||
assert!((a.x0 - 10.0).abs() < 0.01 && (b.x0 - 70.0).abs() < 0.01, "{a:?} {b:?}");
|
||||
let check = |cond: &str| super::tests::check(&format!("{fonts} {cond}"));
|
||||
check("(\\000\\001\\000\\001) stringwidth pop 120 sub abs 0.01 lt");
|
||||
check("newpath 10 20 moveto (\\000\\001) true charpath currentpoint pop 70 sub abs 0.01 lt");
|
||||
}
|
||||
|
||||
/// A descendant found by name is scaled for each character it sets: copying a huge font
|
||||
/// dictionary for every character counts against the operation budget, so a `loop` of it stops.
|
||||
#[test]
|
||||
fn a_huge_named_descendant_in_a_loop_runs_out_of_the_budget() {
|
||||
super::tests::runs_out_of_budget(
|
||||
"200000 dict begin 0 1 199999 { dup def } for /FontName /Big def /FontType 1 def /FontMatrix [0.001 0 0 0.001 0 0] def\n\
|
||||
FontName currentdict end definefont pop\n\
|
||||
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 [/Big findfont] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont { (\\000A) stringwidth pop pop } loop",
|
||||
);
|
||||
}
|
||||
|
||||
/// A composite font's TrueType data stays read while it is in use: the font a string uses for
|
||||
/// every other character isn't dropped (and read again, counting its size against the memory
|
||||
/// budget each time) while 16 other fonts are read between its uses.
|
||||
#[test]
|
||||
fn type42_data_in_use_stays_read() {
|
||||
let mut others = String::new();
|
||||
for k in 1..=16 {
|
||||
others.push_str(&format!(
|
||||
"10 dict begin /FontName /B{k} 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 /.notdef 0 put /sfnts [<00>] def FontName currentdict end definefont pop\n"
|
||||
));
|
||||
}
|
||||
let deps: String = (1..=16).map(|k| format!(" /B{k} findfont")).collect();
|
||||
let fonts = fonts().replace(
|
||||
"/Encoding [0] def /FDepVector [/T42 findfont] def",
|
||||
&format!("/Encoding [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] def /FDepVector [/T42 findfont{deps}] def"),
|
||||
);
|
||||
let s: String = (1..=16).map(|k| format!("\\000\\001\\{k:03o}\\001")).collect();
|
||||
super::tests::check(&format!("{others}{fonts} /Comp findfont 50 scalefont setfont 3000 {{ ({s}) stringwidth pop pop }} repeat true"));
|
||||
}
|
||||
|
||||
/// A page that cycles through more Type 42 fonts than are kept read reads each again when it
|
||||
/// comes back, but counts its data against the memory budget once.
|
||||
#[test]
|
||||
fn type42_data_read_again_counts_once() {
|
||||
let mut fonts = format!("/S [\n{}] def\n", sfnts());
|
||||
for k in 0..17 {
|
||||
fonts.push_str(&format!(
|
||||
"10 dict begin /FontName /F{k} 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 /.notdef 0 put /sfnts [ S aload pop ] def FontName currentdict end definefont pop\n"
|
||||
));
|
||||
}
|
||||
let deps: String = (0..17).map(|k| format!(" /F{k} findfont")).collect();
|
||||
let picks: String = (0..17).map(|k| format!(" {k}")).collect();
|
||||
fonts.push_str(&format!(
|
||||
"16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def\n\
|
||||
/Encoding [{picks}] def /FDepVector [{deps}] def FontName currentdict end definefont pop\n"
|
||||
));
|
||||
let s: String = (0..17).map(|k| format!("\\{k:03o}\\001")).collect();
|
||||
// 17 fonts of 431 kB, each read 200 times: 1.4 GB if each read counted.
|
||||
super::tests::check(&format!("{fonts} /Comp findfont 50 scalefont setfont 200 {{ ({s}) stringwidth pop pop }} repeat true"));
|
||||
}
|
||||
|
||||
/// Reading `sfnts` data that turns out unreadable is charged too: a composite font cycling
|
||||
/// through more such descendants than are kept read copies their strings on every glyph, and a
|
||||
/// `loop` of it runs out of the budget.
|
||||
#[test]
|
||||
fn unreadable_type42_data_read_again_runs_out_of_the_budget() {
|
||||
let mut fonts = String::from("/S [ 0 1 7 { pop 4000000 string } for ] def\n");
|
||||
for k in 0..17 {
|
||||
fonts.push_str(&format!(
|
||||
"10 dict begin /FontName /Bad{k} 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 /.notdef 0 put /sfnts [ S aload pop 0 ] def FontName currentdict end definefont pop\n"
|
||||
));
|
||||
}
|
||||
let deps: String = (0..17).map(|k| format!(" /Bad{k} findfont")).collect();
|
||||
let picks: String = (0..17).map(|k| format!(" {k}")).collect();
|
||||
let s: String = (0..17).map(|k| format!("\\{k:03o}\\001")).collect();
|
||||
super::tests::runs_out_of_budget(&format!(
|
||||
"{fonts}16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def\n\
|
||||
/Encoding [{picks}] def /FDepVector [{deps}] def FontName currentdict end definefont pop\n\
|
||||
/Comp findfont 50 scalefont setfont {{ ({s}) stringwidth pop pop }} loop"
|
||||
));
|
||||
}
|
||||
+273
-22
@@ -168,9 +168,13 @@ impl Interp<'_> {
|
||||
ResourceForAll => self.resource_for_all()?,
|
||||
Show | AShow | WidthShow | AWidthShow | XShow | YShow | XYShow | KShow => {
|
||||
let (s, spacing) = self.show_operands(op)?;
|
||||
match self.type3_font() {
|
||||
Some(font) => self.show_type3(&font, &s, &spacing)?,
|
||||
None => self.show(&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)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
CShow => {
|
||||
@@ -273,6 +277,13 @@ 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) {
|
||||
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 mut w = Vec2::ZERO;
|
||||
for &code in s {
|
||||
@@ -287,19 +298,7 @@ impl Interp<'_> {
|
||||
for (i, &code) in s.iter().enumerate() {
|
||||
let at = self.user_point()?;
|
||||
let w = self.type3_glyph(font, Glyph::Code(code), at, true)?;
|
||||
let d = match spacing {
|
||||
Spacing::Plain | Spacing::Kern(_) => w,
|
||||
Spacing::Add(a, c) => w + *a + c.filter(|(ch, _)| *ch == i64::from(code)).map_or(Vec2::ZERO, |(_, v)| v),
|
||||
Spacing::Widths(v, op) => {
|
||||
let n = |k: usize| v.get(k).copied();
|
||||
match op {
|
||||
Op::XShow => n(i).map(|x| Vec2::new(x, 0.0)),
|
||||
Op::YShow => n(i).map(|y| Vec2::new(0.0, y)),
|
||||
_ => n(2 * i).zip(n(2 * i + 1)).map(|(x, y)| Vec2::new(x, y)),
|
||||
}
|
||||
.unwrap_or(w)
|
||||
}
|
||||
};
|
||||
let d = spacing.advance(i, i64::from(code), w);
|
||||
self.g.cur = Some(self.xf() * (at + d));
|
||||
if let (Spacing::Kern(p), Some(next)) = (spacing, s.get(i + 1)) {
|
||||
self.push(Obj::Int(i64::from(code)))?;
|
||||
@@ -315,10 +314,15 @@ impl Interp<'_> {
|
||||
/// Glyph `glyph` of Type 3 font `font` drawn by its `BuildGlyph` (or `BuildChar`) procedure at
|
||||
/// user point `at` (only measured unless `paint`) → its width (user space).
|
||||
fn type3_glyph(&mut self, font: &DictRef, glyph: Glyph, at: Point, paint: bool) -> Res<Vec2> {
|
||||
self.type3_glyph_in(font, glyph, at, paint, Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// [`Self::type3_glyph`] for a descendant of a composite font whose matrix is `outer`.
|
||||
fn type3_glyph_in(&mut self, font: &DictRef, glyph: Glyph, at: Point, paint: bool, outer: Affine) -> Res<Vec2> {
|
||||
let (fm, build_glyph, build_char, encoding) = {
|
||||
let f = font.borrow();
|
||||
let get = |k: &str| f.get(&Key::name(k)).cloned();
|
||||
let fm = get("FontMatrix").and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
|
||||
let fm = outer * font_matrix(&f, 0.001);
|
||||
(fm, get("BuildGlyph"), get("BuildChar"), get("Encoding"))
|
||||
};
|
||||
let code_of = |name: &str| -> Option<u8> {
|
||||
@@ -402,11 +406,16 @@ impl Interp<'_> {
|
||||
};
|
||||
let f = font.borrow();
|
||||
let name = f.get(&Key::name("FontName")).and_then(Obj::text).map_or_else(|| DEFAULT_FONT.to_string(), |n| n.to_string());
|
||||
let m = f.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
|
||||
// Type 1 glyphs are in 1000-unit space; a Type 42 font's glyph space is one unit an em
|
||||
// (its FontMatrix, usually the identity, is already the em's).
|
||||
let units = if f.get(&Key::name("FontType")).and_then(Obj::as_num) == Some(42.0) { 1.0 } else { 1000.0 };
|
||||
Ok((name, m * Affine::scale(units)))
|
||||
// A composite font's glyphs are its descendants', in their glyph space under its matrix
|
||||
// (PLRM 3rd ed., §5.10 "Composite Fonts"): the first descendant stands for them all.
|
||||
if font_type(&f) == Some(0.0) {
|
||||
let first = f.get(&Key::name("FDepVector")).and_then(|v| v.items()?.get(0));
|
||||
if let Some(Obj::Dict(d)) = first {
|
||||
let d = d.borrow();
|
||||
return Ok((name, font_matrix(&f, 1.0) * font_matrix(&d, 0.001) * Affine::scale(units(&d))));
|
||||
}
|
||||
}
|
||||
Ok((name, font_matrix(&f, 0.001) * Affine::scale(units(&f))))
|
||||
}
|
||||
|
||||
/// `s` (read as Latin-1) set in the current font at user point `at`: point type placed there,
|
||||
@@ -451,6 +460,14 @@ impl Interp<'_> {
|
||||
|
||||
/// `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) {
|
||||
for g in &glyphs {
|
||||
let at = self.user_point()?;
|
||||
let w = self.composite_glyph(g, at, Draw::Path)?;
|
||||
self.g.cur = Some(self.xf() * (at + w));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let at = self.user_point()?;
|
||||
let (t, layout, adv) = self.set_type(s, at)?;
|
||||
for g in &layout.glyphs {
|
||||
@@ -484,6 +501,240 @@ enum Glyph {
|
||||
Name(Rc<str>),
|
||||
}
|
||||
|
||||
impl Spacing {
|
||||
/// How far glyph `i` (character code `code`, width `w`) moves the current point.
|
||||
fn advance(&self, i: usize, code: i64, w: Vec2) -> Vec2 {
|
||||
match self {
|
||||
Self::Plain | Self::Kern(_) => w,
|
||||
Self::Add(a, c) => w + *a + c.filter(|(ch, _)| *ch == code).map_or(Vec2::ZERO, |(_, v)| v),
|
||||
Self::Widths(v, op) => {
|
||||
let n = |k: usize| v.get(k).copied();
|
||||
match op {
|
||||
Op::XShow => n(i).map(|x| Vec2::new(x, 0.0)),
|
||||
Op::YShow => n(i).map(|y| Vec2::new(0.0, y)),
|
||||
_ => n(2 * i).zip(n(2 * i + 1)).map(|(x, y)| Vec2::new(x, y)),
|
||||
}
|
||||
.unwrap_or(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A character of a composite font: the descendant font that draws it, its code there, and the
|
||||
/// composite font's matrix (over the descendant's own).
|
||||
struct Mapped {
|
||||
font: DictRef,
|
||||
/// The byte that picked the descendant (the font number).
|
||||
pick: u8,
|
||||
code: u8,
|
||||
outer: Affine,
|
||||
}
|
||||
|
||||
/// What drawing a glyph does with it.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Draw {
|
||||
/// Fills it (`show`).
|
||||
Fill,
|
||||
/// Adds its outline to the current path (`charpath`).
|
||||
Path,
|
||||
/// Only measures it (`stringwidth`).
|
||||
No,
|
||||
}
|
||||
|
||||
/// Font `f`'s `FontType`.
|
||||
fn font_type(f: &Dict) -> Option<f64> {
|
||||
f.get(&Key::name("FontType")).and_then(Obj::as_num)
|
||||
}
|
||||
|
||||
/// Font `f`'s matrix, `default` times the identity when it has none.
|
||||
fn font_matrix(f: &Dict, default: f64) -> Affine {
|
||||
f.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(default))
|
||||
}
|
||||
|
||||
/// Font `f`'s glyph units in its matrix's space: Type 1 glyphs are in 1000-unit space; a Type 42
|
||||
/// font's glyph space is one unit an em (its FontMatrix, usually the identity, is already the
|
||||
/// em's).
|
||||
fn units(f: &Dict) -> f64 {
|
||||
if font_type(f) == Some(42.0) { 1.0 } else { 1000.0 }
|
||||
}
|
||||
|
||||
impl Interp<'_> {
|
||||
/// The characters of `s` in the current font when it is a composite font with the 8/8 mapping
|
||||
/// (`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;
|
||||
}
|
||||
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 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);
|
||||
// A character whose descendant is missing (or not a base font) shows nothing.
|
||||
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, outer });
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The characters of a composite font shown at the current point, spaced as `spacing` says.
|
||||
fn show_composite(&mut self, glyphs: &[Mapped], spacing: &Spacing) -> Res {
|
||||
let start = self.out.drawn.len();
|
||||
let mut text = String::new();
|
||||
for (i, g) in glyphs.iter().enumerate() {
|
||||
let at = self.user_point()?;
|
||||
let w = self.composite_glyph(g, at, Draw::Fill)?;
|
||||
// `widthshow` and `awidthshow` match a composite font's character by its font number
|
||||
// times 256 plus its code (PLRM 3rd ed., `widthshow` in chapter 8).
|
||||
let d = spacing.advance(i, i64::from(g.pick) * 256 + i64::from(g.code), w);
|
||||
self.g.cur = Some(self.xf() * (at + d));
|
||||
let c = code_char(&g.font.borrow(), g.code);
|
||||
if c != '\u{2022}' {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
if text.trim().is_empty()
|
||||
&& let Some(name) = self.g.font.as_ref().and_then(|f| f.borrow().get(&Key::name("FontName")).and_then(Obj::text))
|
||||
{
|
||||
text = name.to_string();
|
||||
}
|
||||
self.group_glyphs(start, &text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Character `g` of a composite font at user point `at`, drawn by its descendant: a Type 3
|
||||
/// font's procedure, a Type 42 font's TrueType outline, else as type in the font of the
|
||||
/// descendant's name (the character its glyph name stands for) → its width (user space).
|
||||
fn composite_glyph(&mut self, g: &Mapped, at: Point, paint: Draw) -> Res<Vec2> {
|
||||
let builds = {
|
||||
let f = g.font.borrow();
|
||||
font_type(&f) == Some(3.0) && (f.contains_key(&Key::name("BuildGlyph")) || f.contains_key(&Key::name("BuildChar")))
|
||||
};
|
||||
if builds {
|
||||
return self.type3_glyph_in(&g.font, Glyph::Code(g.code), at, paint == Draw::Fill, g.outer);
|
||||
}
|
||||
let fm = g.outer * font_matrix(&g.font.borrow(), 1.0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 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.
|
||||
let entries = g.font.borrow().len();
|
||||
self.spend(entries as u64)?;
|
||||
let saved = self.g.font.replace(transformed(&g.font, g.outer));
|
||||
let r = match paint {
|
||||
Draw::Fill => {
|
||||
self.g.cur = Some(self.xf() * at);
|
||||
self.show(&[byte]).and_then(|()| self.user_point()).map(|p| p - at)
|
||||
}
|
||||
Draw::Path => {
|
||||
self.g.cur = Some(self.xf() * at);
|
||||
self.char_path(&[byte]).and_then(|()| self.user_point()).map(|p| p - at)
|
||||
}
|
||||
Draw::No => self.set_type(&[byte], at).map(|t| t.2),
|
||||
};
|
||||
self.g.font = saved;
|
||||
r
|
||||
}
|
||||
|
||||
/// 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 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)
|
||||
};
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Most Type 42 fonts whose TrueType data is kept between glyphs.
|
||||
const MAX_TYPE42: usize = 16;
|
||||
|
||||
/// The character code `code` of font `f` stands for: its glyph name's, or the code read as
|
||||
/// Latin-1 when the font names no glyph for it (a font found by name, whose program isn't run).
|
||||
fn code_char(f: &Dict, code: u8) -> char {
|
||||
match &*glyph_name(f, code) {
|
||||
".notdef" => char::from(code),
|
||||
name => glyph_char(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"))
|
||||
}
|
||||
|
||||
/// A font scaled or transformed by `m` (`scalefont`, `makefont`).
|
||||
fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
let mut d = font.borrow().clone();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Type 42 fonts' glyphs drawn from the TrueType font they carry, for the type a composite font
|
||||
//! shows: its glyphs are picked by glyph index, often in a subset font without a character map,
|
||||
//! so they can't be set again as type in an installed font. The font is the `sfnts` strings
|
||||
//! joined, each string of odd length ending in a padding byte that isn't part of it (Adobe
|
||||
//! Technical Note #5012, "The Type 42 Font Format Specification", on the `sfnts` array); a glyph
|
||||
//! name maps to its glyph index through `CharStrings`.
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use skrifa::{FontRef, GlyphId, MetadataProvider};
|
||||
use vectorcraft_geom::BezPath;
|
||||
|
||||
use super::obj::Obj;
|
||||
|
||||
/// Most bytes of TrueType data a Type 42 font may carry.
|
||||
pub(super) const MAX_FONT: usize = 32 << 20;
|
||||
|
||||
/// The TrueType font of a Type 42 font's `sfnts` strings, or `None` when an element isn't a
|
||||
/// string or the font would be larger than [`MAX_FONT`].
|
||||
pub(super) fn font_data(sfnts: &[Obj]) -> Option<Vec<u8>> {
|
||||
let mut data = Vec::new();
|
||||
for s in sfnts {
|
||||
let Obj::Str(s) = s else { return None };
|
||||
let s = s.borrow();
|
||||
let len = s.len() - s.len() % 2;
|
||||
if data.len() + len > MAX_FONT {
|
||||
return None;
|
||||
}
|
||||
data.extend_from_slice(s.get(..len).unwrap_or_default());
|
||||
}
|
||||
Some(data)
|
||||
}
|
||||
|
||||
/// What [`font_data`] visits for `sfnts`: the elements up to the first that isn't a string, and
|
||||
/// the bytes of those strings (at most [`MAX_FONT`] and one string more).
|
||||
pub(super) fn font_size(sfnts: &[Obj]) -> (usize, usize) {
|
||||
let mut bytes = 0usize;
|
||||
for (i, s) in sfnts.iter().enumerate() {
|
||||
let Obj::Str(s) = s else { return (i + 1, bytes) };
|
||||
bytes = bytes.saturating_add(s.borrow().len());
|
||||
if bytes > MAX_FONT {
|
||||
return (i + 1, bytes.min(2 * MAX_FONT));
|
||||
}
|
||||
}
|
||||
(sfnts.len(), bytes)
|
||||
}
|
||||
|
||||
/// 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)> {
|
||||
let font = FontRef::new(data).ok()?;
|
||||
let upem = f64::from(font.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
|
||||
if upem <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let id = GlyphId::new(gid);
|
||||
let mut pen = Pen(BezPath::new(), 1.0 / upem);
|
||||
if gid != 0
|
||||
&& let Some(g) = font.outline_glyphs().get(id)
|
||||
{
|
||||
// A glyph that can't be drawn draws nothing; its width still moves the next one.
|
||||
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), &mut pen);
|
||||
}
|
||||
let advance = font.glyph_metrics(Size::unscaled(), LocationRef::default()).advance_width(id).map_or(0.0, f64::from) / upem;
|
||||
Some((pen.0, if advance.is_finite() { advance } else { 0.0 }))
|
||||
}
|
||||
|
||||
/// A pen drawing an outline scaled from font units to ems.
|
||||
struct Pen(BezPath, f64);
|
||||
|
||||
impl OutlinePen for Pen {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.0.move_to((f64::from(x) * self.1, f64::from(y) * self.1));
|
||||
}
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.0.line_to((f64::from(x) * self.1, f64::from(y) * self.1));
|
||||
}
|
||||
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
||||
let s = self.1;
|
||||
self.0.quad_to((f64::from(cx0) * s, f64::from(cy0) * s), (f64::from(x) * s, f64::from(y) * s));
|
||||
}
|
||||
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
||||
let s = self.1;
|
||||
self.0.curve_to((f64::from(cx0) * s, f64::from(cy0) * s), (f64::from(cx1) * s, f64::from(cy1) * s), (f64::from(x) * s, f64::from(y) * s));
|
||||
}
|
||||
fn close(&mut self) {
|
||||
self.0.close_path();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS and `.ai` editing data: opacity mask art without its own paint takes the paint before its dictionary; a transparency set inside a clipping group stays after its `Q`; 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: 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
|
||||
@@ -268,7 +268,7 @@ An object in a pattern's art or in a brush's art takes the file's art styles in
|
||||
| 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); 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); 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); 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); 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 |
|
||||
@@ -306,6 +306,7 @@ An object in a pattern's art or in a brush's art takes the file's art styles in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | minor | EPS import: composite (Type 0) fonts with the 8/8 mapping (`FMapType` 2: PLRM 3rd ed., §5.10) draw a Type 42 descendant's glyphs from the TrueType outlines its `sfnts` carry (Adobe Technical Note #5012), spaced by their own widths or the show operator's numbers, and `charpath` and `stringwidth` read them; descendants found by name set their characters as type at the composite font's size instead of a thousand times it. EPS from `pdftops` of a PDF with embedded TrueType fonts showed nothing, or one black shape, where its type was. |
|
||||
| 2026-11-07 | minor | EPS and `.ai` editing data: opacity mask art that sets no paint of its own paints with the paint before the mask's dictionary, not the black a dictionary's state starts with. A logo of white letters masked by a rectangle with no colour no longer opens hidden: a user's `.ai` (used locally, not committed) shows its six logos. Scores and estimates unchanged. |
|
||||
| 2026-11-06 | minor | EPS and `.ai` editing data: an art style's stroke aligned inside or outside its path (Stroke panel › Align Stroke, written as its own `Adobe Stroke Offset` filter) comes in as a stroke with that alignment, so objects whose style has one keep the style live instead of coming in as their drawn look. Without its inside/outside switch the drawn look stays. |
|
||||
| 2026-11-06 | minor | EPS and `.ai` editing data: `Q` restores the painting state but not the transparency (`Xy`), which the writer only sets when the next object's differs from the last one written. A Screen glow followed by a clipping group set back to Normal no longer leaves the type after the group in Screen: two users' Illustrator 26.5 and 30 EPS posters (used locally, not committed) now show their gold heading and crescent, and both open from their layers (2.5% and 2.7% from the page before, under the limit after). Scores and estimates unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user