From 597413f1139ca9603d96dc0908f74c3dec313852 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 21:14:37 -0800 Subject: [PATCH] EPS import: cshow runs once for each character of a composite font In a composite font the file carries (FMapType 2 or a CMap), cshow ran its procedure once for each byte of the string, with that byte's width in the composite font and the composite font still current. It now runs once for each character the font maps, with the last byte of its code and its width, and with the descendant that draws it current, at the size the composite font sets it, while rootfont gives the composite font (PLRM 3rd ed., cshow and rootfont). While the procedure runs, showing the byte it was given in that descendant shows the character the code maps to: a CMap's CID, base-font code or glyph name. In a CIDFont any other string is a rangecheck error. --- crates/eps/src/import/cid.rs | 11 ++- crates/eps/src/import/interp.rs | 2 +- crates/eps/src/import/tests_cid.rs | 49 ++++++++++++ crates/eps/src/import/tests_composite.rs | 19 +++++ crates/eps/src/import/text.rs | 95 ++++++++++++++++++++---- 5 files changed, 156 insertions(+), 20 deletions(-) diff --git a/crates/eps/src/import/cid.rs b/crates/eps/src/import/cid.rs index 4c00486de9..8fb5137362 100644 --- a/crates/eps/src/import/cid.rs +++ b/crates/eps/src/import/cid.rs @@ -70,6 +70,8 @@ pub(super) struct CidChar { /// The base-font code or glyph name a `bfchar` or `bfrange` mapping gives it, instead of a /// CID. pub base: Option, + /// The last byte of its code. + pub last: u8, } impl CMap { @@ -136,6 +138,7 @@ impl CMap { }; let n = n.clamp(1, rest.len()); let c = code(rest.get(..n).unwrap_or_default()); + let last = rest.get(..n).and_then(<[u8]>::last).copied().unwrap_or(0); let find = |maps: &[(usize, u32, u32, u32, usize)], work: &mut u64| { let at = maps.iter().position(|m| m.0 == n && (m.1..=m.2).contains(&c)); *work += at.map_or(maps.len(), |k| k + 1) as u64; @@ -146,12 +149,12 @@ impl CMap { work += at.map_or(self.bases.len(), |k| k + 1) as u64; let base = at.and_then(|k| self.bases.get(k)).and_then(|m| Some((m.4, base_of(&m.3, c - m.1)?))); out.push(match (matched.is_some(), find(&self.cids, &mut work), base) { - (true, Some((font, cid, _)), _) => CidChar { font, cid, notdef, base: None }, - (true, None, Some((font, base))) => CidChar { font, cid: 0, notdef: None, base: Some(base) }, + (true, Some((font, cid, _)), _) => CidChar { font, cid, notdef, base: None, last }, + (true, None, Some((font, base))) => CidChar { font, cid: 0, notdef: None, base: Some(base), last }, // No character mapping: the notdef mapping's CID, else font 0's CID 0. _ => match find(&self.notdefs, &mut work) { - Some((font, _, base)) if matched.is_some() => CidChar { font, cid: base, notdef: None, base: None }, - _ => CidChar { font: 0, cid: 0, notdef: None, base: None }, + Some((font, _, base)) if matched.is_some() => CidChar { font, cid: base, notdef: None, base: None, last }, + _ => CidChar { font: 0, cid: 0, notdef: None, base: None, last }, }, }); i += n; diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 0ea7017c11..43346988ff 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -98,7 +98,7 @@ pub(crate) struct Interp<'a> { /// The font a `kshow` procedure sees as current while type in a CIDFont only named is set in /// a font of its own (see `text`). pub kshow_font: Option, - /// The `cshow` procedure running over type in a CIDFont only named, with the font it sees as + /// The `cshow` procedure running over a composite font's type, with the font it sees as /// current (see `text`). pub cshow: Option, pub g: GState, diff --git a/crates/eps/src/import/tests_cid.rs b/crates/eps/src/import/tests_cid.rs index 8350cd514b..3e5c846737 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -136,6 +136,31 @@ fn codes_map_by_their_codespace_with_notdef_fallbacks() { width("<01>", 30.0); } +/// `cshow` over a CMap runs its procedure once for each code, whatever its length, with the +/// code's last byte and its width (PLRM 3rd ed., `cshow`). +#[test] +fn cshow_runs_once_for_each_cmap_code() { + let fonts = String::from_utf8(cid_font(true)).unwrap() + CMAP; + let check = |cond: &str| check(&format!("{fonts} /TestFont 50 selectfont {cond}")); + check("[ {pop pop} <418001> cshow ] dup length 2 eq exch aload pop 1 eq exch 65 eq and and"); + check("0 {pop exch pop add} <418001> cshow 55 sub abs 0.01 lt"); + check( + "{pop pop pop currentfont /CIDFontName get /seen exch def rootfont /FontName get /root exch def} <41> cshow seen /TestCID eq root /TestFont eq and", + ); + // With the CIDFont current, the byte the procedure was given shows the glyph of its code's + // CID: <8001>'s last byte draws CID 2, not code 01's CID 1. Another string is an error. + let shown = "{pop pop ( ) dup 0 4 -1 roll put show} <418001> cshow"; + check(&format!("gsave nulldevice 10 20 moveto {shown} currentpoint grestore pop 65 sub abs 0.01 lt")); + check("gsave nulldevice 10 20 moveto {{pop pop pop (B) show} <41> cshow} stopped grestore"); + let r = import(&program(true, &format!("/TestFont 50 selectfont 10 20 moveto {shown}"))).unwrap(); + let want = import(&program(true, "/TestFont 50 selectfont 10 20 moveto <418001> show")).unwrap(); + let (p, q) = (paths(&r), paths(&want)); + assert_eq!(p.len(), 2, "{p:?}"); + for (a, b) in p.iter().zip(&q) { + assert!((a.x0 - b.x0).abs() < 0.01 && (a.width() - b.width()).abs() < 0.01 && (a.height() - b.height()).abs() < 0.01, "{p:?} {q:?}"); + } +} + /// `usecmap` brings in another CMap's ranges and mappings; the CMap's own come after them. #[test] fn usecmap_takes_the_parents_mappings() { @@ -375,6 +400,30 @@ fn cmaps_map_codes_to_base_fonts_codes_and_glyph_names() { }) .collect(); assert_eq!(text, "ABC"); + // A `cshow` procedure that shows the byte it was given, in the base font current while it + // runs, shows the character the code maps to, as `show` does. + let body = format!( + "{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 20 scalefont setfont 10 20 moveto \ + {{pop pop ( ) dup 0 4 -1 roll put show}} <001000110012> cshow" + ); + let r = super::tests::read(&body); + let text: String = all(&r.document) + .iter() + .filter_map(|n| match &n.kind { + NodeKind::Text(t) => Some(t.plain_text()), + _ => None, + }) + .collect(); + assert_eq!(text, "ABC"); + let body = format!( + "{plain}{BF_CMAP}/BF /TestBF-H [/Plain42 /Helvetica] composefont 50 scalefont setfont 10 20 moveto \ + {{pop pop ( ) dup 0 4 -1 roll put show}} <000100020003> cshow" + ); + let q = paths(&super::tests::read(&body)); + assert_eq!(q.len(), 3, "{q:?}"); + for (a, b) in p.iter().zip(&q) { + assert!((a.x0 - b.x0).abs() < 0.01 && (a.width() - b.width()).abs() < 0.01, "{p:?} {q:?}"); + } } /// A CID-keyed font over a predefined Unicode CMap the file doesn't define, its CIDFont only diff --git a/crates/eps/src/import/tests_composite.rs b/crates/eps/src/import/tests_composite.rs index e682ce10ec..d7eaa358cc 100644 --- a/crates/eps/src/import/tests_composite.rs +++ b/crates/eps/src/import/tests_composite.rs @@ -119,6 +119,25 @@ fn widthshow_matches_a_composite_character_by_font_number_and_code() { check(format!("{} {} sub abs 0.01 lt", end(2), 10.0 + 100.0 * wh)); } +/// `cshow` runs its procedure once for each character of a composite font, not for each byte, +/// with the last byte of its code and its width, and with the descendant that draws it as the +/// current font while `rootfont` gives the composite font (PLRM 3rd ed., `cshow` and `rootfont`). +#[test] +fn cshow_runs_once_for_each_composite_character_in_its_descendant() { + let (_, wh) = glyph('H'); + let (_, wi) = glyph('I'); + let check = |cond: String| super::tests::check(&format!("{} /Comp findfont 50 scalefont setfont {cond}", fonts())); + check("[ {pop pop} (\\000\\001\\000\\002) cshow ] dup length 2 eq exch aload pop 2 eq exch 1 eq and and".into()); + check(format!("0 {{pop exch pop add}} (\\000\\001\\000\\002) cshow {} sub abs 0.01 lt", 50.0 * (wh + wi))); + check("{pop pop pop currentfont /FontName get /seen exch def rootfont /FontName get /root exch def} (\\000\\001) cshow seen /T42 eq root /Comp eq and".into()); + check("{pop pop pop} (\\000\\001) cshow currentfont /FontName get /Comp eq".into()); + // A character the procedure shows in the descendant is as wide as the composite font sets it. + check(format!( + "gsave nulldevice 10 20 moveto {{pop pop ( ) dup 0 4 -1 roll put show}} (\\000\\001\\000\\002) cshow currentpoint grestore pop {} sub abs 0.01 lt", + 10.0 + 50.0 * (wh + wi) + )); +} + /// 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] diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index 515e0b12ec..83a27a8035 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -251,6 +251,15 @@ impl Interp<'_> { self.g.font = root; return r; } + // In a composite font the file carries, once for each character it maps. + if let Some(glyphs) = self.composite_glyphs(&s)? { + let root = self.g.font.clone(); + let before = self.cshow.take(); + let r = self.composite_cshow(&glyphs, &proc, root.clone()); + self.cshow = before; + self.g.font = root; + return r; + } for b in s { let v = self.width_of(&[b])?; self.push(Obj::Int(i64::from(b)))?; @@ -989,6 +998,7 @@ impl Spacing { /// A character of a composite font: the descendant font that draws it, its code there (its last /// byte for a CID), its CID and the notdef mapping's (`FMapType` 9), the composite font's matrix /// (over the descendant's own) and whether it is set vertically (`WMode` 1). +#[derive(Clone)] struct Mapped { font: DictRef, /// The byte that picked the descendant (the font number). @@ -999,6 +1009,8 @@ struct Mapped { cid: Option<(u32, Option)>, outer: Affine, vertical: bool, + /// The last byte of the character's code in the shown string (what `cshow` gives its procedure). + last: u8, } /// Type in a CIDFont only named, over a predefined CMap ([`Interp::named_cid_type`]): its codes @@ -1011,13 +1023,15 @@ struct NamedCid { ems: bool, } -/// A `cshow` procedure running over type in a CIDFont only named ([`Interp::named_cshow`]): the -/// CIDFont it sees as current, the composite font `rootfont` gives while that CIDFont is current, -/// and the type of the code it was given. +/// A `cshow` procedure running over a composite font's type ([`Interp::named_cshow`], +/// [`Interp::composite_cshow`]): the descendant it sees as current, the composite font `rootfont` +/// gives while that descendant is current, and the code it was given: over a CIDFont only named, +/// its type; over a composite font the file carries, the character it maps to. pub(super) struct CShowFont { selected: DictRef, root: DictRef, - code: NamedCid, + code: Option, + mapped: Option, } /// What drawing a glyph does with it. @@ -1064,6 +1078,22 @@ impl Interp<'_> { /// codes mapped to a descendant and a CID, ยง5.11.5); `None` for other fonts and mappings, /// which are set as type in the font of the composite font's name. fn composite_glyphs(&mut self, s: &[u8]) -> Res>> { + // Inside a `cshow` procedure over a composite font the file carries, with the descendant + // it selected current, the one byte the procedure was given stands for the character its + // code maps to: a CMap's CID, base-font code or glyph name. In a CIDFont any other string + // is a `rangecheck` error (PLRM 3rd ed., `cshow`: the glyph of the selected CID); in a + // base font it is shown as it is. + if let Some(c) = self.cshow.as_ref() + && let Some(mapped) = c.mapped.as_ref() + && self.g.font.as_ref().is_some_and(|f| f.same(&c.selected)) + { + if s == [mapped.last] { + return Ok(Some(vec![mapped.clone()])); + } + if mapped.cid.is_some() { + return ps_err("rangecheck", "a string shown in cshow"); + } + } let Some(font) = self.g.font.clone() else { return Ok(None) }; let map = { let f = font.borrow(); @@ -1097,7 +1127,7 @@ impl Interp<'_> { if let Some(Obj::Dict(d)) = index.and_then(|i| fonts.get(i as usize)) && font_type(&d.borrow()) != Some(0.0) { - out.push(Mapped { font: d.clone(), pick, code, name: None, cid: None, outer, vertical: false }); + out.push(Mapped { font: d.clone(), pick, code, name: None, cid: None, outer, vertical: false, last: code }); } } Ok(Some(out)) @@ -1124,21 +1154,28 @@ impl Interp<'_> { .filter_map(|c| match (fonts.get(c.font), c.base) { // A base font's code, or its glyph name (at the code its encoding gives it, if any). (Some(Obj::Dict(d)), Some(Base::Code(code))) => { - Some(Mapped { font: d.clone(), pick: 0, code, name: None, cid: None, outer, vertical }) + Some(Mapped { font: d.clone(), pick: 0, code, name: None, cid: None, outer, vertical, last: c.last }) } (Some(Obj::Dict(d)), Some(Base::Name(name))) => { let code = encoded(&d.borrow(), &name); let name = code.is_none().then_some(name); - Some(Mapped { font: d.clone(), pick: 0, code: code.unwrap_or(0), name, cid: None, outer, vertical }) + Some(Mapped { font: d.clone(), pick: 0, code: code.unwrap_or(0), name, cid: None, outer, vertical, last: c.last }) } // A code without a mapping in a base font (not a CIDFont): its code 0, the // `.notdef` glyph CID 0 stands for in a CIDFont. (Some(Obj::Dict(d)), None) if !d.borrow().contains_key(&Key::name("CIDFontType")) => { - Some(Mapped { font: d.clone(), pick: 0, code: 0, name: None, cid: None, outer, vertical }) - } - (Some(Obj::Dict(d)), None) => { - Some(Mapped { font: d.clone(), pick: 0, code: (c.cid & 0xff) as u8, name: None, cid: Some((c.cid, c.notdef)), outer, vertical }) + Some(Mapped { font: d.clone(), pick: 0, code: 0, name: None, cid: None, outer, vertical, last: c.last }) } + (Some(Obj::Dict(d)), None) => Some(Mapped { + font: d.clone(), + pick: 0, + code: (c.cid & 0xff) as u8, + name: None, + cid: Some((c.cid, c.notdef)), + outer, + vertical, + last: c.last, + }), _ => None, }) .collect()) @@ -1207,17 +1244,18 @@ impl Interp<'_> { /// that code when `code` and with none otherwise, while the CIDFont it selected is current. fn cshow_code(&self, code: bool) -> Option { let c = self.cshow.as_ref()?; + let given = c.code.as_ref()?; if !self.g.font.as_ref().is_some_and(|f| f.same(&c.selected)) { return None; } let mut codes = Decoded::default(); if code { - let mut chars = c.code.codes.text.chars(); - for &(value, n) in &c.code.codes.codes { + let mut chars = given.codes.text.chars(); + for &(value, n) in &given.codes.codes { codes.push(value, chars.by_ref().take(n)); } } - Some(NamedCid { codes, font: c.code.font.clone(), vertical: c.code.vertical, ems: c.code.ems }) + Some(NamedCid { codes, font: given.font.clone(), vertical: given.vertical, ems: given.ems }) } /// The CMap's name, the composite font, the CIDFont's name and the writing mode of @@ -1305,7 +1343,7 @@ impl Interp<'_> { self.push(Obj::Int(i64::from(code & 0xff)))?; self.push_num(v.x)?; self.push_num(v.y)?; - self.cshow = Some(CShowFont { selected: selected.clone(), root: composite.clone(), code: one }); + self.cshow = Some(CShowFont { selected: selected.clone(), root: composite.clone(), code: Some(one), mapped: None }); self.g.font = Some(selected.clone()); let go = self.body(proc); self.g.font = root.clone(); @@ -1316,6 +1354,33 @@ impl Interp<'_> { Ok(()) } + /// `cshow`'s procedure `proc` run for each character `glyphs` of composite font `root` maps, + /// with the last byte of its code and its width (PLRM 3rd ed., `cshow`). While it runs the + /// descendant that draws the character, at the size the composite font sets it, is the current + /// font and `rootfont` gives `root`. The current font is restored when the procedure completes. + fn composite_cshow(&mut self, glyphs: &[Mapped], proc: &Obj, root: Option) -> Res { + let Some(composite) = root.clone() else { return Ok(()) }; + for g in glyphs { + let v = self.composite_glyph(g, Point::ZERO, Draw::No)?; + self.push(Obj::Int(i64::from(g.last)))?; + self.push_num(v.x)?; + self.push_num(v.y)?; + // Scaling the descendant copies its dictionary for every character: charged by its + // size, so a huge font dictionary in a loop runs out of the budget. + let entries = g.font.borrow().len(); + self.spend(entries as u64)?; + let selected = transformed(&g.font, g.outer); + self.cshow = Some(CShowFont { selected: selected.clone(), root: composite.clone(), code: None, mapped: Some(g.clone()) }); + self.g.font = Some(selected); + let go = self.body(proc); + self.g.font = root.clone(); + if !go? { + break; + } + } + Ok(()) + } + /// How far codes `codes` of type `named` move the current point (user space) when /// [`Self::show_named_cid`] shows them with no extra spacing. fn named_cid_width(&mut self, named: &NamedCid, codes: &Decoded) -> Res {