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 {