From fdbd11657764e642296f4253dceb183ee8cf435d Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:33:27 -0500 Subject: [PATCH] EPS import: cvi and cvr read radix numbers cvi and cvr read a string as the PostScript syntax reads a number (PLRM 3rd ed., cvi, cvr), so (16#20) cvi is 32. The reader parsed strings as decimal only. Ghostscript's eps2write prolog undoes a name's #20 escape that way, so its files with a spot colour named with spaces stopped reading at that colour. --- crates/eps/src/import/interp.rs | 17 ++++++++++++++--- crates/eps/src/import/lex.rs | 2 +- crates/eps/src/import/tests.rs | 12 ++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index bc57cfb12..fe0c63919 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -12,7 +12,7 @@ use vectorcraft_geom::Affine; use super::data::Stream; use super::graphics::{GState, Out}; -use super::lex::{IMMEDIATE, Lexer}; +use super::lex::{IMMEDIATE, Lexer, number}; use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err}; /// Most operations one file may run. @@ -775,7 +775,7 @@ impl<'a> Interp<'a> { }, Cvi => { let v = match self.pop()? { - Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).trim().parse::().map_err(|_| PsError::Ps("typecheck", String::new()))?, + Obj::Str(s) => string_number(&s.borrow())?, o => o.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, }; if !(v.is_finite() && v.abs() < 9e15) { @@ -785,7 +785,7 @@ impl<'a> Interp<'a> { } Cvr => { let v = match self.pop()? { - Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).trim().parse::().map_err(|_| PsError::Ps("typecheck", String::new()))?, + Obj::Str(s) => string_number(&s.borrow())?, o => o.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, }; self.push_num(v)?; @@ -1471,6 +1471,17 @@ impl<'a> Interp<'a> { } /// An array or string index. +/// The number a string holds by the PostScript syntax (`cvi`, `cvr`: PLRM 3rd ed., chapter 8), +/// radix numbers such as `16#20` included. +fn string_number(s: &[u8]) -> Res { + let text = String::from_utf8_lossy(s); + match number(text.trim_matches(|c: char| c.is_ascii_whitespace() || c == '\0')) { + Some(Obj::Int(i)) => Ok(i as f64), + Some(Obj::Real(r)) => Ok(r), + _ => ps_err("typecheck", ""), + } +} + fn index(k: &Obj) -> Res { match *k { Obj::Int(i) if i >= 0 => Ok(i as usize), diff --git a/crates/eps/src/import/lex.rs b/crates/eps/src/import/lex.rs index 57d3e73e2..e01b90c23 100644 --- a/crates/eps/src/import/lex.rs +++ b/crates/eps/src/import/lex.rs @@ -276,7 +276,7 @@ pub(crate) fn hex_decode(body: &[u8]) -> Option> { } /// A number token (`12`, `-3.5`, `.5`, `1e-3`, `16#ff`), or `None`. -fn number(w: &str) -> Option { +pub(crate) fn number(w: &str) -> Option { let first = w.bytes().next()?; if !(first.is_ascii_digit() || matches!(first, b'+' | b'-' | b'.')) { return None; diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 852598e1b..2bda9c230 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1022,6 +1022,18 @@ fn known_answers_false_for_keys_no_dictionary_holds() { check("{ 1 dict null known } stopped"); } +/// `cvi` and `cvr` read a string as the PostScript syntax reads a number (PLRM 3rd ed., chapter +/// 8), radix numbers included: prologs turn a name's `#20` escape back into its character with +/// `(16#20) cvi`. +#[test] +fn cvi_and_cvr_read_radix_numbers() { + check("(16#20) cvi 32 eq (8#17) cvi 15 eq and ( 2#101 ) cvi 5 eq and (36#Z) cvi 35 eq and"); + check("(16#ff) cvr 255 eq (3.7) cvi 3 eq and (-2.5e1) cvr -25 eq and (12) cvr 12 eq and"); + check("{ (16#zz) cvi } stopped { (inf) cvr } stopped and { (16#-1) cvi } stopped and { () cvi } stopped and"); + // A name's escapes, as a prolog undoes them for a spot colour named `PANTONE 300 C`. + check("(PANTONE#20300) dup 7 3 getinterval (16#--) dup 3 4 -1 roll 1 2 getinterval putinterval cvi 32 eq exch pop"); +} + #[test] fn dictstack_and_internaldict_answer() { check("10 array dictstack length countdictstack eq");