From ebf498a823c2d5628843cbc29c9fe11610d096f7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 03:20:55 -0800 Subject: [PATCH] EPS import: kshow runs its procedure between the characters of a composite font In a composite font the file carries (FMapType 2, or a CMap), kshow ran its procedure zero times. It now runs between each two characters with the values of their codes (font number times 256 plus code, or the CMap code), and the next character follows where the procedure moves the current point (PLRM 3rd ed., kshow). What the procedure draws goes after the type, outside the glyphs' group, as for live type. --- crates/eps/src/import/cid.rs | 10 +++--- crates/eps/src/import/tests_cid.rs | 10 ++++++ crates/eps/src/import/tests_composite.rs | 29 ++++++++++++++++ crates/eps/src/import/text.rs | 42 ++++++++++++++++++++---- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/crates/eps/src/import/cid.rs b/crates/eps/src/import/cid.rs index 4c00486de9..18498352f7 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 value of its code (`<8001>` is 32769). + pub value: u32, } impl CMap { @@ -146,12 +148,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, value: c }, + (true, None, Some((font, base))) => CidChar { font, cid: 0, notdef: None, base: Some(base), value: c }, // 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, value: c }, + _ => CidChar { font: 0, cid: 0, notdef: None, base: None, value: c }, }, }); i += n; diff --git a/crates/eps/src/import/tests_cid.rs b/crates/eps/src/import/tests_cid.rs index 8350cd514b..876d3df412 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -136,6 +136,16 @@ fn codes_map_by_their_codespace_with_notdef_fallbacks() { width("<01>", 30.0); } +/// `kshow` over a CMap runs its procedure between each two codes with their values, whatever +/// their length, and the next character follows where it moves the point (PLRM 3rd ed., `kshow`). +#[test] +fn kshow_runs_between_each_two_cmap_codes() { + let fonts = String::from_utf8(cid_font(true)).unwrap() + CMAP; + let check = |cond: &str| check(&format!("{fonts} /TestFont 50 selectfont {cond}")); + check("gsave nulldevice 10 20 moveto [ {} <418001> kshow ] grestore aload length 2 eq exch 16#8001 eq and exch 65 eq and"); + check("gsave nulldevice 10 20 moveto {pop pop 100 0 rmoveto} <418001> kshow currentpoint grestore pop 165 sub abs 0.01 lt"); +} + /// `usecmap` brings in another CMap's ranges and mappings; the CMap's own come after them. #[test] fn usecmap_takes_the_parents_mappings() { diff --git a/crates/eps/src/import/tests_composite.rs b/crates/eps/src/import/tests_composite.rs index e682ce10ec..2f4074af97 100644 --- a/crates/eps/src/import/tests_composite.rs +++ b/crates/eps/src/import/tests_composite.rs @@ -119,6 +119,35 @@ 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)); } +/// `kshow` runs its procedure between each two characters of a composite font, with the values of +/// their codes (font number times 256 plus code), and the next character follows where it moves +/// the current point (PLRM 3rd ed., `kshow`). +#[test] +fn kshow_runs_between_each_two_composite_characters() { + let (_, wh) = glyph('H'); + let (_, wi) = glyph('I'); + 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}")); + check("gsave nulldevice 10 20 moveto [ {} (\\000\\001\\001\\002\\000\\001) kshow ] grestore aload length 4 eq exch 1 eq and exch 258 eq and exch 258 eq and exch 1 eq and".into()); + check(format!( + "gsave nulldevice 10 20 moveto {{pop pop 100 0 rmoveto}} (\\000\\001\\000\\002) kshow currentpoint grestore pop {} sub abs 0.01 lt", + 10.0 + 50.0 * (wh + wi) + 100.0 + )); + // Drawn, the I starts where the procedure moved the point. + let r = read(&format!("{fonts}/Comp findfont 50 scalefont setfont 10 20 moveto {{pop pop 100 0 rmoveto}} (\\000\\001\\000\\002) kshow")); + let paths: Vec<_> = all(&r.document).iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).map(|n| bounds(n)).collect(); + assert_eq!(paths.len(), 2, "{paths:?}"); + assert!(paths[1].x0 > 10.0 + 50.0 * wh + 100.0 - 1.0, "{paths:?}"); + // A procedure that draws: its square goes after the glyphs' group, outside it. + let r = read(&format!( + "{fonts}/Comp findfont 50 scalefont setfont 10 20 moveto {{pop pop currentpoint 2 2 rectfill}} (\\000\\001\\000\\002\\000\\001) kshow" + )); + let top = super::tests::objects(&r.document); + assert_eq!(top.len(), 3, "{top:?}"); + assert_eq!(top[0].children().map(Vec::len), Some(3)); + assert!(top[1..].iter().all(|n| matches!(n.kind, NodeKind::Path { .. }) && (bounds(n).width() - 2.0).abs() < 0.01), "{top:?}"); +} + /// 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..15b9ca34c6 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -999,6 +999,8 @@ struct Mapped { cid: Option<(u32, Option)>, outer: Affine, vertical: bool, + /// The value of the character's code in the shown string (what `kshow` gives its procedure). + value: u32, } /// Type in a CIDFont only named, over a predefined CMap ([`Interp::named_cid_type`]): its codes @@ -1097,7 +1099,8 @@ 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 }); + let value = u32::from(pick) * 256 + u32::from(code); + out.push(Mapped { font: d.clone(), pick, code, name: None, cid: None, outer, vertical: false, value }); } } Ok(Some(out)) @@ -1124,21 +1127,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, value: c.value }) } (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, value: c.value }) } // 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, value: c.value }) } + (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, + value: c.value, + }), _ => None, }) .collect()) @@ -1346,6 +1356,12 @@ impl Interp<'_> { fn show_composite(&mut self, glyphs: &[Mapped], spacing: &Spacing) -> Res { let start = self.out.drawn.len(); let mut text = String::new(); + let kern: Vec = match spacing { + Spacing::Kern(_) => glyphs.iter().map(|g| g.value).collect(), + _ => Vec::new(), + }; + // Where in the objects drawn each run of `kshow`'s procedure put its art. + let mut kerned = Vec::new(); for (i, g) in glyphs.iter().enumerate() { let at = self.user_point()?; let w = self.composite_glyph(g, at, Draw::Fill)?; @@ -1353,6 +1369,12 @@ impl Interp<'_> { // 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)); + // `kshow`'s procedure runs between each two characters with the values of their codes. + let before = self.out.drawn.len(); + self.kern(spacing, &kern, i, at + d)?; + if self.out.drawn.len() > before { + kerned.push(before..self.out.drawn.len()); + } // A CID stands for no character (its code is only the CID's low byte). let c = if g.cid.is_some() { '\u{2022}' } else { g.name.as_deref().map_or_else(|| code_char(&g.font.borrow(), g.code), glyph_char) }; if c != '\u{2022}' { @@ -1364,7 +1386,13 @@ impl Interp<'_> { { text = name.to_string(); } + // What `kshow`'s procedure draws goes after the type, outside its group. + let mut art = Vec::new(); + for run in kerned.into_iter().rev() { + art.splice(0..0, self.out.drawn.drain(run)); + } self.group_glyphs(start, &text); + self.out.drawn.extend(art); Ok(()) }