From 3e46408ab572d2e61d1504e8ae37c06774f84e9f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 09:25:14 -0800 Subject: [PATCH] EPS import: cshow and kshow procedures see the font PLRM gives them In a CID-keyed font whose CIDFont the file only names, a cshow procedure now runs with the CIDFont as the current font, rootfont gives the composite font, and the procedure gets each code's last byte. A show in the CIDFont shows the character of that code, and any other string is a rangecheck error. The current font is restored after each procedure. A kshow procedure no longer leaves a font it sets current: the current font is restored when the procedure completes, for every font, and a kshow the procedure runs sees the font the procedure set. In GBT-H and CNS2-H, whose mappings are not read, a byte that starts no row-and-cell code is a code of its own. docs/gaps.md dates this G9 work in a bullet of its own. --- crates/eps/src/import/interp.rs | 4 + crates/eps/src/import/predefined_cmap.rs | 7 +- crates/eps/src/import/tests_cid.rs | 57 +++++++++-- crates/eps/src/import/text.rs | 123 +++++++++++++++++------ docs/gaps.md | 3 +- 5 files changed, 150 insertions(+), 44 deletions(-) diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 6b96aabc25..0ea7017c11 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -98,6 +98,9 @@ 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 + /// current (see `text`). + pub cshow: Option, pub g: GState, pub saved: Vec, pub out: Out, @@ -240,6 +243,7 @@ impl<'a> Interp<'a> { cmap_font: 0, predefined_cmaps: vec![], kshow_font: None, + cshow: None, type42_counted: HashSet::new(), g, saved: vec![], diff --git a/crates/eps/src/import/predefined_cmap.rs b/crates/eps/src/import/predefined_cmap.rs index 19525fdf36..c688ebf9ae 100644 --- a/crates/eps/src/import/predefined_cmap.rs +++ b/crates/eps/src/import/predefined_cmap.rs @@ -288,7 +288,7 @@ fn code_space(cmap: &str) -> Option { "83pv-RKSJ" | "90pv-RKSJ" => RKSJ_MAC, _ if stem.ends_with("RKSJ") => RKSJ, "EUC" | "78-EUC" => EUC_JP, - "" | "78" | "Add" | "Ext" | "NWP" => ROW_CELL, + "" | "78" | "Add" | "Ext" | "NWP" | "GBT" | "CNS2" => ROW_CELL, "GB-EUC" | "GBT-EUC" | "KSC-EUC" => EUC, "GBpc-EUC" | "GBTpc-EUC" => GB_PC, "GBK-EUC" | "GBKp-EUC" => GBK, @@ -327,8 +327,9 @@ pub(super) fn known(cmap: &str) -> bool { /// The codes of string `bytes` in predefined CMap `cmap` when its mappings can't be read, each /// standing for no character: as long as the code space range it falls in ([`code_space`]; a byte -/// that starts no code is a code of its own), or two bytes when the code space isn't listed (the -/// row-and-cell codes of `GBT-H` and `CNS2-H`). A 0x0D or 0x09 byte is always a code of its own. +/// that starts no code is a code of its own, as in the row-and-cell code space of `GBT-H` and +/// `CNS2-H`), or two bytes when the code space isn't listed. A 0x0D or 0x09 byte is always a code +/// of its own. pub(super) fn unread_codes(cmap: &str, bytes: &[u8]) -> Decoded { let space = code_space(cmap); let mut out = Decoded::default(); diff --git a/crates/eps/src/import/tests_cid.rs b/crates/eps/src/import/tests_cid.rs index 25ed81c2b0..8350cd514b 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -487,21 +487,29 @@ fn named_cidfonts_over_predefined_cmaps_take_the_show_operators_spacing() { measured(&format!("{set} 10 20 moveto <93fa967b> [30 30] xshow currentpoint pop 70 sub abs 0.01 lt")); measured(&format!("{set} 10 20 moveto <93fa967b> [10 10] yshow currentpoint 40 sub abs 0.01 lt exch 10 sub abs 0.01 lt and")); measured(&format!("{set} 10 20 moveto <93fa967b> [30 10 30 10] xyshow currentpoint 40 sub abs 0.01 lt exch 70 sub abs 0.01 lt and")); - // `kshow` runs its procedure between each two codes: it moves the point 100 more here, and a - // font it sets stays current. + // `kshow` runs its procedure between each two codes: it moves the point 100 more here. The + // current font is restored when the procedure completes (PLRM 3rd ed., `kshow`), so a font it + // sets doesn't stay current, here or for a font found by name. measured(&format!( "{set} 10 20 moveto <93fa967b> show currentpoint 10 20 moveto {{pop pop 100 0 rmoveto}} <93fa967b> kshow currentpoint \ 3 -1 roll sub abs 0.01 lt 3 1 roll sub -100 sub abs 0.01 lt and" )); measured(&format!( - "{set} 10 20 moveto {{pop pop /Helvetica findfont 10 scalefont setfont}} <93fa967b> kshow currentfont /FontName get /Helvetica eq" + "{set} 10 20 moveto {{pop pop /Helvetica findfont 10 scalefont setfont}} <93fa967b> kshow currentfont /FontName get /Ryumin-Light-90ms-RKSJ-H eq" )); - // The procedure sees the font the file set as current, and the next one the font an earlier - // one set. + measured( + "/Helvetica findfont 20 scalefont setfont 10 20 moveto {pop pop /Courier findfont 10 scalefont setfont} (AB) kshow currentfont /FontName get /Helvetica eq", + ); + // Each procedure sees the font the file set as current, also after an earlier one set another. measured(&format!("{set} 10 20 moveto {{pop pop currentfont /FontName get /seen exch def}} <93fa967b> kshow seen /Ryumin-Light-90ms-RKSJ-H eq")); measured(&format!( "{set} 10 20 moveto /n 0 def {{pop pop /n n 1 add def n 1 eq {{/Helvetica findfont 10 scalefont setfont}} {{currentfont /FontName get /seen exch def}} ifelse}} \ - <93fa967b41> kshow seen /Helvetica eq" + <93fa967b41> kshow seen /Ryumin-Light-90ms-RKSJ-H eq" + )); + // A `kshow` the procedure runs in a font it set sees that font. + measured(&format!( + "{set} 10 20 moveto {{pop pop /Helvetica findfont 10 scalefont setfont {{pop pop currentfont /FontName get /seen exch def}} (AB) kshow}} \ + <93fa967b> kshow seen /Helvetica eq" )); // The procedure gets the two codes' values (left on the stack here). measured(&format!( @@ -575,8 +583,9 @@ fn named_cid_type_is_set_alike_by_every_show_operator() { /// `charpath`, `cshow` and `glyphshow` in a CID-keyed font over a predefined CMap, its CIDFont only /// named: `charpath` adds the outlines of the characters the codes stand for and moves the point -/// as `show` does; `cshow`'s procedure gets each code (two bytes for a two-byte code) with its -/// width; `glyphshow` sets the named character itself. +/// as `show` does; `cshow`'s procedure gets each code's last byte with its width and runs with the +/// CIDFont current, a show in it setting the code's character; `glyphshow` sets the named +/// character itself. #[test] fn named_cid_type_in_charpath_cshow_and_glyphshow() { let set = "/Ryumin-Light-90ms-RKSJ-H findfont 20 scalefont setfont"; @@ -595,9 +604,35 @@ fn named_cid_type_in_charpath_cshow_and_glyphshow() { top("fd41"), top("41") )); - // The procedure leaves each code: 日 (0x93FA), then A. - check(&format!("{set} [ {{pop pop}} <93fa41> cshow ] dup length 2 eq exch dup 0 get 16#93fa eq exch 1 get 65 eq and and")); + // The procedure leaves each code's last byte (PLRM 3rd ed., `cshow`): 日 (0x93FA), then A. + check(&format!("{set} [ {{pop pop}} <93fa41> cshow ] dup length 2 eq exch dup 0 get 16#fa eq exch 1 get 65 eq and and")); check(&format!("{set} 0 {{pop exch pop add}} <93fa967b41> cshow <93fa967b41> stringwidth pop sub abs 0.01 lt")); + // While the procedure runs, the current font is the CIDFont and `rootfont` gives the composite + // font (PLRM 3rd ed., `cshow` and `rootfont`); the current font is restored after each run, + // also when the procedure sets another. + check(&format!( + "{set} {{pop pop pop currentfont dup /FontType get /t exch def /CIDFontName get /seen exch def}} <93fa> cshow \ + seen /Ryumin-Light eq t 9 eq and" + )); + check(&format!("{set} {{pop pop pop rootfont /FontName get /seen exch def}} <93fa> cshow seen /Ryumin-Light-90ms-RKSJ-H eq")); + check(&format!( + "{set} /n 0 def {{pop pop pop /n n 1 add def n 1 eq {{/Helvetica findfont 10 scalefont setfont rootfont /FontName get /r exch def}} \ + {{currentfont /CIDFontName get /seen exch def}} ifelse}} <93fa41> cshow seen /Ryumin-Light eq r /Helvetica eq and \ + currentfont /FontName get /Ryumin-Light-90ms-RKSJ-H eq and" + )); + // A show in the CIDFont shows the character of the code the procedure was given, the string + // holding its last byte, and moves the point as far as `show` does. Any other string, an + // empty one included, is a `rangecheck` error. + for s in ["()", "(AB)", "<41>"] { + check(&format!("gsave nulldevice {set} 10 20 moveto {{{{pop pop pop {s} show}} <93fa> cshow}} stopped grestore")); + } + check(&format!("gsave nulldevice {set} 10 20 moveto {{{{pop pop pop show}} <93fa> cshow}} stopped not grestore")); + let r = super::tests::read(&format!("{set} 10 20 moveto {{pop pop ( ) dup 0 4 -1 roll put show}} <93fa967b> cshow")); + assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::(), "日本"); + check(&format!( + "gsave nulldevice {set} 10 20 moveto <93fa967b> show currentpoint 10 20 moveto {{pop pop ( ) dup 0 4 -1 roll put show}} <93fa967b> cshow \ + currentpoint grestore 3 -1 roll sub abs 0.01 lt 3 1 roll sub abs 0.01 lt and" + )); let r = super::tests::read(&format!("{set} 10 20 moveto /uni00E9 glyphshow")); assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::>(), ["é"]); } @@ -640,6 +675,8 @@ fn latin_names_ending_in_h_and_cmaps_that_cannot_be_read() { "/Ryumin-Light-GBT-V findfont 20 scalefont setfont 10 100 moveto <21212121> show currentpoint 60 sub abs 0.01 lt exch 10 sub abs 0.01 lt and", // A tab is a code of its own: three codes. "/Ryumin-Light-GBT-H findfont 20 scalefont setfont <09092121> stringwidth abs 0.01 lt exch 60 sub abs 0.01 lt and", + // A byte that starts no row-and-cell code is a code of its own, also before a tab: three. + "/Ryumin-Light-GBT-H findfont 20 scalefont setfont <21092121> stringwidth abs 0.01 lt exch 60 sub abs 0.01 lt and", ] { let r = super::tests::read(&format!("{cond} {{ 0 0 10 10 rectfill }} if")); assert!(all(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Path { .. })), "{cond}"); diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index 73fc4776d0..989450f440 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -186,22 +186,15 @@ impl Interp<'_> { // The procedure shows or places each character: it gets its code and width. let s = self.pop_str()?.to_vec(); let proc = self.pop_proc()?; - // In a CIDFont only named, a code may be several bytes: the procedure gets each - // code's value. + // In a CIDFont only named, a code may be several bytes: the procedure runs once for + // each code. if let Some(named) = self.named_cid_type(&s)? { - let mut chars = named.codes.text.chars(); - for &(code, n) in &named.codes.codes { - let mut one = Decoded::default(); - one.push(code, chars.by_ref().take(n)); - let v = self.named_cid_width(&named, &one)?; - self.push(Obj::Int(i64::from(code)))?; - self.push_num(v.x)?; - self.push_num(v.y)?; - if !self.body(&proc)? { - break; - } - } - return Ok(()); + let root = self.g.font.clone(); + let before = self.cshow.take(); + let r = self.named_cshow(named, &proc, root.clone()); + self.cshow = before; + self.g.font = root; + return r; } for b in s { let v = self.width_of(&[b])?; @@ -252,7 +245,12 @@ impl Interp<'_> { SetCharWidth => self.glyph_width = Some(self.nums::<2>()?), SetCacheDevice2 => self.glyph_width = Some(self.nums::<10>()?[..2].try_into().unwrap_or_default()), RootFont => { - let font = match self.g.font.clone() { + // Inside a `cshow` procedure the composite font, unless the procedure set a font. + let root = match (&self.cshow, &self.g.font) { + (Some(c), Some(f)) if f.same(&c.selected) => Some(c.root.clone()), + _ => None, + }; + let font = match root.or_else(|| self.g.font.clone()) { Some(f) => f, None => self.find_font(&Obj::name(DEFAULT_FONT))?, }; @@ -791,18 +789,16 @@ impl Interp<'_> { self.push(Obj::Int(code.into()))?; self.push(Obj::Int(next.into()))?; // The procedure sees the font the file set, not the one type in a CIDFont only named is - // set in; a font it sets stays current. - let Some(outer) = self.kshow_font.take() else { - self.call(p.clone())?; - return Ok(self.user_point()? - pen); + // set in, and the current font is restored when it completes (PLRM 3rd ed., `kshow`). + // A `kshow` the procedure runs sees the font the procedure set. + let outer = self.kshow_font.take(); + let inner = match outer.clone() { + Some(outer) => self.g.font.replace(outer), + None => self.g.font.clone(), }; - let inner = self.g.font.replace(outer.clone()); let r = self.call(p.clone()); - // Once the procedure sets a font, it and the later procedures see that one. - if self.g.font.as_ref().is_some_and(|f| f.same(&outer)) { - self.g.font = inner; - self.kshow_font = Some(outer); - } + self.kshow_font = outer; + self.g.font = inner; r?; Ok(self.user_point()? - pen) } @@ -960,6 +956,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. +pub(super) struct CShowFont { + selected: DictRef, + root: DictRef, + code: NamedCid, +} + /// What drawing a glyph does with it. #[derive(Clone, Copy, PartialEq)] enum Draw { @@ -1092,7 +1097,17 @@ impl Interp<'_> { /// stands for none), set as type in the font of its CIDFont's name, vertical over a vertical /// CMap (`WMode` 1). Control codes stand for no character. Type in a CMap VectorCraft can't /// read is left out, with a warning, each of its codes moving the point one em. + /// Inside a `cshow` procedure over such type, with the CIDFont current, the string must be the + /// one byte the procedure was given, else a `rangecheck` error, and it stands for the code + /// the procedure was given (PLRM 3rd ed., `cshow`: the glyph of the selected CID). fn named_cid_type(&mut self, s: &[u8]) -> Res> { + if let Some(named) = self.cshow_code(true) { + let given = named.codes.codes.first().map(|&(code, _)| code & 0xff); + if s.len() != 1 || given != s.first().map(|&b| u32::from(b)) { + return ps_err("rangecheck", "a string shown in cshow"); + } + return Ok(Some(named)); + } let Some(cmap) = self.named_cid_parts().map(|(cmap, ..)| cmap) else { return Ok(None) }; // Each code is looked up in the CMap's mappings. self.spend(s.len() as u64)?; @@ -1120,6 +1135,9 @@ impl Interp<'_> { /// The font [`Self::named_cid_type`] sets type in, with no codes yet, when the current font is /// a CID-keyed font over a predefined CMap the file doesn't define. fn named_cid_font(&mut self) -> Res> { + if let Some(named) = self.cshow_code(false) { + return Ok(Some(named)); + } let Some((_, font, name, vertical)) = self.named_cid_parts() else { return Ok(None) }; // The composite font is copied under the CIDFont's name for every string: charged by // its size, so a huge font dictionary shown in a loop runs out of the budget. @@ -1130,6 +1148,23 @@ impl Interp<'_> { Ok(Some(NamedCid { codes: Decoded::default(), font: DictRef::new(named), vertical, ems: vertical })) } + /// The type of the code a running `cshow` procedure was given ([`Self::named_cshow`]), with + /// 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()?; + 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 { + codes.push(value, chars.by_ref().take(n)); + } + } + Some(NamedCid { codes, font: c.code.font.clone(), vertical: c.code.vertical, ems: c.code.ems }) + } + /// The CMap's name, the composite font, the CIDFont's name and the writing mode of /// [`Self::named_cid_type`]. fn named_cid_parts(&self) -> Option<(String, DictRef, Obj, bool)> { @@ -1191,13 +1226,41 @@ impl Interp<'_> { let before = std::mem::replace(&mut self.kshow_font, saved.clone()); let r = self.show_codes(&named.codes, widths.as_deref(), spacing, named.vertical, &kern, draw); self.kshow_font = before; - // A font the `kshow` procedure set stays current. - if self.g.font.as_ref().is_none_or(|f| f.same(&named.font)) { - self.g.font = saved; - } + self.g.font = saved; r } + /// `cshow`'s procedure `proc` run for each code of type `named` in composite font `root`, with + /// the code's last byte and its width (PLRM 3rd ed., `cshow`). While it runs the CIDFont is the + /// current font and `rootfont` gives `root`; a show operator in the CIDFont shows the code's + /// character, whatever its string (see [`Self::named_cid_type`]). The current font is restored + /// when the procedure completes. + fn named_cshow(&mut self, named: NamedCid, proc: &Obj, root: Option) -> Res { + let Some(composite) = root.clone() else { return Ok(()) }; + let dep = named.font.borrow().get(&Key::name("FDepVector")).and_then(|v| v.items()?.get(0)); + let selected = match dep { + Some(Obj::Dict(d)) => d, + _ => named.font.clone(), + }; + let mut chars = named.codes.text.chars(); + for &(code, n) in &named.codes.codes { + let mut one = NamedCid { codes: Decoded::default(), font: named.font.clone(), vertical: named.vertical, ems: named.ems }; + one.codes.push(code, chars.by_ref().take(n)); + let v = self.named_cid_width(&one, &one.codes)?; + 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.g.font = Some(selected.clone()); + 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 { diff --git a/docs/gaps.md b/docs/gaps.md index 09c31c8ae8..c668a3f9c9 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -195,7 +195,8 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi - **Narrowed (#6165, 2026-11-09):** editable PDF import preserves explicit ActualText once, with empty/nested spans, authored order and glyph advances covered by `crates/pdf/src/tests/actual_text.rs`. One replacement uses the first placed glyph's line/style; exact redistribution across lines/styles and general mixed-direction layout remain. Input is bounded to depth 128 and 64 KiB encoded per accepted replacement. This does not close G9's broader `.ai`/EPS gaps or change its estimate. -- **Done (2026-11-09):** the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined CMaps (Unicode, Shift-JIS, EUC, GBK, Big Five, UHC) set as type, vertical over `-V` CMaps) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. This narrows G9. +- **Narrowed (2026-11-12):** named CIDFonts over predefined CMaps other than Unicode CMaps (Shift-JIS, EUC, GBK, Big Five, UHC) are set as type too, vertical over `-V` CMaps, also by `charpath`, `cshow`, `kshow` and `glyphshow`. This narrows G9; scores and estimates unchanged. +- **Done (2026-11-09):** the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. This narrows G9. - **Done (2026-11-06):** an EPS page that paints its gradients as rows of abutting one-colour strips imports them as linear gradients, so the seams between strips no longer show and an Illustrator EPS drawn that way keeps its layers instead of opening as its page. This narrows G9; scores and estimates unchanged. - **Done (2026-10-30):** from an audit of a user's `.ai`/EPS files: All Caps and Small Caps; placed files at their box's corner; pages cropped to the art; gradient mesh corners with handle weights of 0 and the transparency written after a mesh, a masked mesh and a blend; a layer's state block with the stroke style; type on a path spaced as the file lays it out (the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box, and the fit under Gravity does not reproduce the file's layout); EPS10 legacy-format type; a PDF of some artboards compared on the artboard its page shows. Comparison limits remain unchanged. This narrows G9; scores and estimates unchanged.