From 7e0ed6b183b19312ac83166d6c5d1dd88986a3e1 Mon Sep 17 00:00:00 2001 From: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:06:56 -0400 Subject: [PATCH] EPS import: EPS files from the reference app open as vectors #249 made the reduced repros of #234 open, but a real file saved by the reference app (version 30) still fell back to its screen-resolution preview with "typecheck in `a dictionary`". Running its prolog to the end needed: - The standard dictionaries (systemdict, userdict, globaldict, statusdict, errordict, $error) as values in systemdict, so `/globaldict where { /globaldict get begin } if` gets a dictionary, not an operator. SharedFontDirectory names the font directory too. errordict has a handleerror for prologs that wrap it. - `dictstack`, `internaldict`, `currentsystemparams` and `currentuserparams` (with the cache sizes programs `get`), and `cshow`. - Executable files: `cvx` and `cvlit` set a file's attribute, `xcheck` reads it, and executing one runs its data as a program, as the prolog does with `(...) 0 () /SubFileDecode filter cvx exec`. - SubFileDecode with a count of 0 and an empty end string passes its whole source through instead of nothing. - `flushfile` on a filter over the program's data skips that data (the file's XMP packet). A filter chained from a pending one leaves it nothing to skip, so the writer's own `VCsrc flushfile` is unchanged. The interpreter empties its standard dictionaries when it finishes, since systemdict now holds itself. Both real files tested now import as 15 vector paths in CMYK with no warnings, and round-trip through SVG and PDF pixel for pixel. The EPS fuzzer exercises the new operators. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 4 +- crates/engine/tests/import_fuzz.rs | 2 + crates/eps/src/import/data.rs | 38 +++++++++++---- crates/eps/src/import/graphics.rs | 10 ++++ crates/eps/src/import/interp.rs | 76 ++++++++++++++++++++++++++++-- crates/eps/src/import/mod.rs | 1 + crates/eps/src/import/obj.rs | 15 ++++-- crates/eps/src/import/tests.rs | 51 ++++++++++++++++++++ crates/eps/src/import/text.rs | 14 ++++++ docs/mcp.md | 15 +++--- 10 files changed, 199 insertions(+), 27 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 53ca39fc5..0288717c8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,7 +138,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Files (M4):** - `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export. - SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). - - EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export. EPS import runs Illustrator's AGM/CoolType prologs (resource categories, `resourceforall`, `clipsave`/`cliprestore`, subarrays and substrings that share storage) and falls back to its palette TIFF preview. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). + - EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export. EPS import runs Illustrator's AGM/CoolType prologs (resource categories, `resourceforall`, `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters, `cshow`, metadata skipped with `flushfile`), so Illustrator 30 EPS files open as vectors, and falls back to its palette TIFF preview. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). - Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours. - **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. - **Tests and robustness:** ~2,840 automated tests: model-based property tests, a junk-parameter sweep over every command, import fuzzing (SVG, PDF, libraries), golden renders, and MCP end-to-end tests over stdio. Shipped code never panics (workspace lints and a rollback safety net; see `docs/development.md`). @@ -196,7 +196,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Layers, artboards, document setup | 5 | 80% | Layers panel options depth, artboard presets/rearrange polish (Document Setup and New Document are done) | 5–8 | | View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 | | Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth, ruler-drag modifiers (Shift snaps to ticks, Alt swaps orientation) and dragging existing guides | 4–8 | -| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, Illustrator EPS checked against real files (only reduced repros so far), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers and presets are done | 6–10 | +| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, Illustrator EPS checked against more real files (two Illustrator 30 files open as vectors so far), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers and presets are done | 6–10 | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | | Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 | diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index c73bd1b3f..d8a97431a 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -913,6 +913,8 @@ fn arb_ps_token() -> impl Strategy { "clipsave", "cliprestore", "languagelevel", "cvx", "findresource", "defineresource", "undefineresource", "resourcestatus", "resourceforall", "/Category", "/Generic", "/ProcSet", "(*)", "(G?n*\\\\*?)", "{pop}", "8 string", "(*) {pop} 8 string /Category resourceforall", "/Generic /Category findresource dup length dict copy /Category defineresource", + "dictstack", "8 array", "1183615869 internaldict", "cshow", "currentsystemparams", "currentuserparams", "flushfile", "xcheck", + "systemdict", "/userdict", "SharedFontDirectory", "(1 2 add) 0 () /SubFileDecode filter cvx exec", "currentfile cvx exec", "/ASCII85Decode", "/ASCIIHexDecode", "/FlateDecode", "/RunLengthDecode", "/LZWDecode", "/DCTDecode", "/SubFileDecode", "/DeviceRGB", "/DeviceCMYK", "/DeviceGray", "/Pattern", "/x", "x", "/Helvetica", "(abc)", "", "<~87cURD]i,\"Ebo80~>", "true", "false", "null", "[/Separation (S) /DeviceCMYK {dup dup dup}]", "[/Indexed /DeviceRGB 1 ]", diff --git a/crates/eps/src/import/data.rs b/crates/eps/src/import/data.rs index b067dda8c..46129efc4 100644 --- a/crates/eps/src/import/data.rs +++ b/crates/eps/src/import/data.rs @@ -185,7 +185,8 @@ fn decode(f: &Filter, raw: &[u8]) -> Res<(Vec, usize, Option)> { "SubFileDecode" => { let (count, eod) = &f.sub; if eod.is_empty() { - let n = (*count).min(raw.len()); + // A count of 0 without an end string sets no end: the data passes through whole. + let n = if *count == 0 { raw.len() } else { (*count).min(raw.len()) }; (raw.get(..n).unwrap_or_default().to_vec(), n) } else { let (mut at, mut seen) = (0, 0); @@ -218,11 +219,11 @@ impl Interp<'_> { pub fn data_op(&mut self, op: Op) -> Res { use Op::*; match op { - CurrentFile => self.push(Obj::File(Rc::new(RefCell::new(Stream::Current))))?, + CurrentFile => self.push(Obj::File { stream: Rc::new(RefCell::new(Stream::Current)), exec: false })?, Filter => self.filter()?, ReadHexString | ReadString => { let s = self.pop_str()?; - let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") }; + let Obj::File { stream: f, .. } = self.pop()? else { return ps_err("typecheck", "") }; let n = s.len(); let got = if op == ReadString { self.read(&f, n)? } else { self.read_hex(&f, n)? }; self.fill(s, &got)?; @@ -230,7 +231,7 @@ impl Interp<'_> { } ReadLine => { let s = self.pop_str()?; - let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") }; + let Obj::File { stream: f, .. } = self.pop()? else { return ps_err("typecheck", "") }; let n = s.len(); let mut line = vec![]; let mut ended = false; @@ -250,7 +251,14 @@ impl Interp<'_> { self.fill(s, &line)?; self.push(Obj::Bool(ended))?; } - FlushFile | CloseFile => { + FlushFile => { + // An input file is read to its end: a filter over the program's data skips it + // (metadata read with `flushfile`). The program's own file goes on being run. + if let Obj::File { stream, .. } = self.pop()? { + self.materialize(&stream)?; + } + } + CloseFile => { self.pop()?; } File => { @@ -329,7 +337,7 @@ impl Interp<'_> { } let spec = Filter { name, sub }; let stream = match self.pop()? { - Obj::File(f) => { + Obj::File { stream: f, .. } => { if let Stream::Buf { data, pos, .. } = &*f.borrow() { self.alloc(data.len().saturating_sub(*pos))?; } @@ -340,6 +348,11 @@ impl Interp<'_> { }; match chain { Some(mut c) => { + // The new filter reads a pending one's data: it has none left of its own + // (`flushfile` on it skips nothing more). + if !c.is_empty() { + *f.borrow_mut() = Stream::Buf { data: vec![], pos: 0, rgb: None }; + } c.push(spec); Stream::Pending(c) } @@ -364,7 +377,16 @@ impl Interp<'_> { } _ => return ps_err("typecheck", "filter"), }; - self.push(Obj::File(Rc::new(RefCell::new(stream)))) + self.push(Obj::File { stream: Rc::new(RefCell::new(stream)), exec: false }) + } + + /// The data an executable file runs as a program: what is left of it. `None` for the + /// program's own file, which is being run already. + pub(super) fn program_of(&mut self, f: &Rc>) -> Res>> { + if matches!(&*f.borrow(), Stream::Current) { + return Ok(None); + } + self.read(f, MAX_DATA).map(Some) } /// Decode the filters a file waits to run over the program's data, from where it is now. @@ -688,7 +710,7 @@ struct Pixels { fn source(o: &Obj) -> Res { Ok(match o { - Obj::File(f) => Source::File(f.clone()), + Obj::File { stream: f, .. } => Source::File(f.clone()), Obj::Str(s) => Source::Str(s.to_vec()), Obj::Array { items, .. } => Source::Proc(Obj::Array { items: items.clone(), exec: true }), _ => return ps_err("typecheck", "DataSource"), diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 633e4f66a..28c9e9f26 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -946,6 +946,16 @@ impl Interp<'_> { d.insert(Key::name("PageSize"), Obj::array(vec![Obj::Real(f.width()), Obj::Real(f.height())])); self.push(Obj::dict(d))?; } + // The cache and stack sizes programs read (they `get` them without asking first). + CurrentUserParams | CurrentSystemParams => { + let keys: &[&str] = if op == CurrentUserParams { + &["MaxFontItem", "MaxFormItem", "MaxPatternItem", "MaxUPathItem", "MaxOpStack", "MaxDictStack", "MaxExecStack"] + } else { + &["MaxFontCache", "MaxFormCache", "MaxPatternCache", "MaxUPathCache", "MaxScreenStorage", "MaxDisplayList"] + }; + let d: super::obj::Dict = keys.iter().map(|k| (Key::name(k), Obj::Int(1 << 20))).collect(); + self.push(Obj::dict(d))?; + } ShowPage => return Err(PsError::Quit), CopyPage | ErasePage => {} Matrix => self.push(matrix_obj(Affine::IDENTITY))?, diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 09da400b9..9f0f862db 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -38,6 +38,8 @@ pub(crate) struct Interp<'a> { global: DictRef, status: DictRef, error: DictRef, + /// `internaldict`'s dictionary, for font programs. + internal: DictRef, /// `FontDirectory`: fonts defined with `definefont`. pub fonts: DictRef, /// Resources by category (`defineresource`). @@ -89,8 +91,24 @@ impl<'a> Interp<'a> { resources.borrow_mut().insert(Key::name("Category"), Obj::dict(categories)); let fonts = new_dict(); system.borrow_mut().insert(Key::name("FontDirectory"), Obj::Dict(fonts.clone())); - system.borrow_mut().insert(Key::name("GlobalFontDirectory"), Obj::Dict(fonts.clone())); - let (user, global) = (new_dict(), new_dict()); + for name in ["GlobalFontDirectory", "SharedFontDirectory"] { + system.borrow_mut().insert(Key::name(name), Obj::Dict(fonts.clone())); + } + let (user, global, status, error) = (new_dict(), new_dict(), new_dict(), new_dict()); + // Errors don't reach the program's handler, but prologs wrap the one they find. + error.borrow_mut().insert(Key::name("handleerror"), Obj::proc(vec![])); + // The standard dictionaries are values in systemdict, so `get`, `load` and `where` find + // them as dictionaries (`/globaldict where { /globaldict get begin } if`). + for (name, d) in [ + ("systemdict", &system), + ("userdict", &user), + ("globaldict", &global), + ("statusdict", &status), + ("errordict", &error), + ("$error", &error), + ] { + system.borrow_mut().insert(Key::name(name), Obj::Dict(d.clone())); + } Self { lex: Lexer::new(src), stack: vec![], @@ -98,8 +116,9 @@ impl<'a> Interp<'a> { system, user, global, - status: new_dict(), - error: new_dict(), + status, + error, + internal: new_dict(), fonts, resources, depth: 0, @@ -123,6 +142,18 @@ impl<'a> Interp<'a> { } } + /// Empty the standard dictionaries, so the memory they hold is freed with the interpreter: + /// systemdict holds itself, and programs link dictionaries into each other. + pub fn release(&mut self) { + for d in [&self.system, &self.user, &self.global, &self.status, &self.error, &self.internal, &self.fonts, &self.resources] { + if let Ok(mut d) = d.try_borrow_mut() { + d.clear(); + } + } + self.dicts.clear(); + self.stack.clear(); + } + // ---------- the operand stack ---------- pub fn push(&mut self, o: Obj) -> Res { @@ -262,6 +293,7 @@ impl<'a> Interp<'a> { pub fn call(&mut self, v: Obj) -> Res { match v { Obj::Array { items, exec: true } => self.run_proc(&items), + Obj::File { stream, exec: true } => self.run_file(&stream), Obj::Op(op) => self.op(op), Obj::Exec(name) => { self.enter()?; @@ -291,6 +323,26 @@ impl<'a> Interp<'a> { r } + /// Run what is left of an executable file as a program, token by token. + fn run_file(&mut self, f: &Rc>) -> Res { + let Some(program) = self.program_of(f)? else { return Ok(()) }; + self.enter()?; + let mut lex = Lexer::new(&program); + let r = loop { + match lex.next() { + Ok(Some(o)) => { + if let Err(e) = self.exec_token(o) { + break Err(e); + } + } + Ok(None) => break Ok(()), + Err(e) => break Err(e), + } + }; + self.depth -= 1; + r + } + fn run_items(&mut self, items: &Shared) -> Res { let mut i = 0; loop { @@ -620,6 +672,7 @@ impl<'a> Interp<'a> { Cvx => { let o = match self.pop()? { Obj::Array { items, .. } => Obj::Array { items, exec: true }, + Obj::File { stream, .. } => Obj::File { stream, exec: true }, Obj::Name(n) => Obj::Exec(n), o => o, }; @@ -628,6 +681,7 @@ impl<'a> Interp<'a> { Cvlit => { let o = match self.pop()? { Obj::Array { items, .. } => Obj::Array { items, exec: false }, + Obj::File { stream, .. } => Obj::File { stream, exec: false }, Obj::Exec(n) => Obj::Name(n), o => o, }; @@ -635,7 +689,7 @@ impl<'a> Interp<'a> { } Xcheck => { let o = self.pop()?; - self.push(Obj::Bool(matches!(o, Obj::Array { exec: true, .. } | Obj::Exec(_) | Obj::Op(_))))?; + self.push(Obj::Bool(matches!(o, Obj::Array { exec: true, .. } | Obj::File { exec: true, .. } | Obj::Exec(_) | Obj::Op(_))))?; } Cvn => { let s = self.pop_str()?; @@ -853,11 +907,23 @@ impl<'a> Interp<'a> { self.push(Obj::Dict(d))?; } CountDictStack => self.push(Obj::Int(self.dicts.len() as i64))?, + DictStack => { + // The dictionary stack, bottom first, stored into the array's start. + let items = self.pop_array()?; + let dicts: Vec = self.dicts.iter().map(|d| Obj::Dict(d.clone())).collect(); + let part = items.sub(0, dicts.len()).filter(|p| p.write(0, &dicts)).ok_or(PsError::Ps("rangecheck", String::new()))?; + self.push(Obj::Array { items: part, exec: false })?; + } SystemDict => self.push(Obj::Dict(self.system.clone()))?, UserDict => self.push(Obj::Dict(self.user.clone()))?, GlobalDict => self.push(Obj::Dict(self.global.clone()))?, StatusDict => self.push(Obj::Dict(self.status.clone()))?, ErrorDict | DollarError => self.push(Obj::Dict(self.error.clone()))?, + InternalDict => { + // The operand is a password that every interpreter accepts. + self.pop_int()?; + self.push(Obj::Dict(self.internal.clone()))?; + } Bind => { let o = self.pop()?; if let Obj::Array { items, .. } = &o { diff --git a/crates/eps/src/import/mod.rs b/crates/eps/src/import/mod.rs index ac044ddc9..fd079b7c6 100644 --- a/crates/eps/src/import/mod.rs +++ b/crates/eps/src/import/mod.rs @@ -137,6 +137,7 @@ pub fn import(bytes: &[u8]) -> Result { let page = Affine::new([1.0, 0.0, 0.0, -1.0, -llx, ury]); let mut it = Interp::new(ps, GState::default(), Out::new(doc, page, frame)); let result = it.run(); + it.release(); let mut out = it.out; if let Some(n) = dsc.pages.filter(|n| *n > 1) { out.warn(&format!("only the first of the file's {n} pages was read")); diff --git a/crates/eps/src/import/obj.rs b/crates/eps/src/import/obj.rs index 7fb82a153..7ac4c4c82 100644 --- a/crates/eps/src/import/obj.rs +++ b/crates/eps/src/import/obj.rs @@ -40,7 +40,11 @@ pub(crate) enum Obj { Dict(DictRef), Op(Op), Mark, - File(Rc>), + /// A file; executable ones run their data as a program (`cvx exec`). + File { + stream: Rc>, + exec: bool, + }, /// A `save` level (the graphics state depth it restores to). Save(usize), /// A graphics state object (`gstate`, `currentgstate`). @@ -118,7 +122,7 @@ impl Obj { Self::Dict(_) => "dicttype", Self::Op(_) => "operatortype", Self::Mark => "marktype", - Self::File(_) => "filetype", + Self::File { .. } => "filetype", Self::Save(_) => "savetype", Self::GState(_) => "gstatetype", } @@ -270,8 +274,8 @@ ops! { // Dictionaries. NewDict = "dict", MaxLength = "maxlength", Begin = "begin", End = "end", Def = "def", Load = "load", Store = "store", Known = "known", Where = "where", Undef = "undef", CurrentDict = "currentdict", CountDictStack = "countdictstack", - SystemDict = "systemdict", UserDict = "userdict", GlobalDict = "globaldict", StatusDict = "statusdict", - ErrorDict = "errordict", DollarError = "$error", Bind = "bind", + DictStack = "dictstack", SystemDict = "systemdict", UserDict = "userdict", GlobalDict = "globaldict", StatusDict = "statusdict", + ErrorDict = "errordict", DollarError = "$error", InternalDict = "internaldict", Bind = "bind", // Virtual memory and the environment. Save = "save", Restore = "restore", SetGlobal = "setglobal", CurrentGlobal = "currentglobal", VmStatus = "vmstatus", Version = "version", Product = "product", RealTime = "realtime", UserTime = "usertime", @@ -292,6 +296,7 @@ ops! { SetUnderColorRemoval = "setundercolorremoval", SetColorRendering = "setcolorrendering", SetSmoothness = "setsmoothness", SetStrokeAdjust = "setstrokeadjust", CurrentStrokeAdjust = "currentstrokeadjust", SetPageDevice = "setpagedevice", CurrentPageDevice = "currentpagedevice", SetUserParams = "setuserparams", SetSystemParams = "setsystemparams", + CurrentUserParams = "currentuserparams", CurrentSystemParams = "currentsystemparams", SetObjectFormat = "setobjectformat", ShowPage = "showpage", CopyPage = "copypage", ErasePage = "erasepage", CurrentScreen = "currentscreen", SetColorScreen = "setcolorscreen", CurrentHalftone = "currenthalftone", CurrentTransfer = "currenttransfer", CurrentColorTransfer = "currentcolortransfer", @@ -320,6 +325,6 @@ ops! { DefineResource = "defineresource", UndefineResource = "undefineresource", ResourceStatus = "resourcestatus", ResourceForAll = "resourceforall", Show = "show", AShow = "ashow", WidthShow = "widthshow", AWidthShow = "awidthshow", XShow = "xshow", YShow = "yshow", XYShow = "xyshow", - KShow = "kshow", GlyphShow = "glyphshow", StringWidth = "stringwidth", CharPath = "charpath", + KShow = "kshow", CShow = "cshow", GlyphShow = "glyphshow", StringWidth = "stringwidth", CharPath = "charpath", SetCacheDevice = "setcachedevice", SetCharWidth = "setcharwidth", } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index c5fc68da8..1d72ba048 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -569,6 +569,57 @@ fn resourceforall_enumerates_matching_names() { check("/n 0 def (Gen*) { pop /n n 1 add def } 16 string /Category resourceforall n 1 eq"); } +#[test] +fn the_standard_dictionaries_are_dictionaries_in_systemdict() { + // The reference app's EPS prolog looks them up by name rather than running them. + check("systemdict /userdict get type /dicttype eq"); + check("/globaldict where { /globaldict get begin currentdict globaldict eq end } { false } ifelse"); + check("systemdict /systemdict get systemdict eq systemdict /statusdict known and"); + check("userdict /x 5 put systemdict /userdict get /x get 5 eq"); + check("/errordict load /handleerror known errordict $error eq and"); + check("SharedFontDirectory type /dicttype eq"); +} + +#[test] +fn dictstack_and_internaldict_answer() { + check("10 array dictstack length countdictstack eq"); + check("userdict begin 10 array dictstack dup length 1 sub get userdict eq end"); + check("{ 1 array dictstack } stopped"); + check("1183615869 internaldict /k 1 put 1183615869 internaldict /k get 1 eq"); + check("currentsystemparams /MaxPatternCache get type /integertype eq"); + check("<< /MaxPatternItem currentsystemparams /MaxPatternCache get >> setuserparams currentuserparams /MaxFontItem known"); +} + +#[test] +fn an_executable_filter_runs_its_data_as_a_program() { + // A string run as a program through a filter (0 and an empty string set no end of data). + check("(1 2 add) 0 () /SubFileDecode filter cvx exec 3 eq"); + check("(1 2 add) 0 () /SubFileDecode filter dup xcheck not exch cvx xcheck and"); + check("(abcd) 2 () /SubFileDecode filter 8 string readstring pop (ab) eq"); + check("{ (1 add) 0 () /SubFileDecode filter cvx exec } stopped"); + // The data following it in the file: `74727565` is `true`. + check("currentfile /ASCIIHexDecode filter cvx exec 74727565>"); + // A literal file is only pushed. + check("(1 2 add) 0 () /SubFileDecode filter exec type /filetype eq"); +} + +#[test] +fn flushfile_skips_the_data_of_a_filter() { + // Metadata that isn't PostScript, skipped up to its end marker. + blue("currentfile 0 (%%EndPacket) /SubFileDecode filter flushfile\n\n%%EndPacket\n0 0 1 setrgbcolor"); +} + +#[test] +fn cshow_runs_its_procedure_for_each_character() { + check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq"); + // Each call gets the character's code and width. + check("/c 0 def { pop pop /c exch def } (A) cshow c 65 eq"); + check( + "/Helvetica findfont 10 scalefont setfont /w 0 def { pop w add /w exch def pop } (ab) cshow \ + (ab) stringwidth pop w sub abs 0.001 lt", + ); +} + /// A `w` × `h` 8-bit palette TIFF with an alpha sample (`ExtraSamples` [1]), uncompressed in /// strips of `per` rows, in either byte order, as the reference app writes its EPS previews: /// pixel (x, y) is entry `(x + y) % 3` of a red, green, blue map, at alpha `12 x`. diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index b9d4408f0..5450021d0 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -165,6 +165,20 @@ impl Interp<'_> { self.pop()?; self.show(&s)?; } + CShow => { + // 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()?; + for b in s { + let (_, _, v) = self.set_type(&[b], Point::ZERO)?; + self.push(Obj::Int(i64::from(b)))?; + self.push_num(v.x)?; + self.push_num(v.y)?; + if !self.body(&proc)? { + break; + } + } + } GlyphShow => { let name = self.pop()?.text().unwrap_or_else(|| Rc::from("")); let ch = glyph_char(&name); diff --git a/docs/mcp.md b/docs/mcp.md index 0b9a65a0e..f8de129fa 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1682,13 +1682,14 @@ Other files are run through a small PostScript interpreter (Level 3, first page their width, caps, joins, miter limit and dashes (a fill and a stroke of the same path become one object), grey, RGB, CMYK, indexed and spot colours (a Separation ink becomes a spot swatch, painted at its tint), clips (clipping groups; `clipsave`/`cliprestore`), `gsave`/`grestore`, `save`/`restore`, transforms, procedures with `bind def`, loops, -dictionaries, arrays and strings whose intervals share storage, resources (categories made from `Generic`, -`resourceforall`), axial and radial shadings and shading patterns (gradients), images and image masks (data in the -file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as point type in the -font the file names (embedded font programs are skipped). The artboard is the `%%HiResBoundingBox` (else -`%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know, an -error, a runaway loop) or that draws nothing comes in as its TIFF preview (palette previews with an alpha channel too) -with a warning; without a preview, the art drawn up to the error is kept with a warning, and a file with none is +dictionaries (the standard ones are values in `systemdict`; `dictstack`, `internaldict`), arrays and strings whose +intervals share storage, resources (categories made from `Generic`, `resourceforall`), executable filters (`cvx exec` +runs their data; `flushfile` skips it), axial and radial shadings and shading patterns (gradients), images and image +masks (data in the file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as +point type in the font the file names (embedded font programs are skipped). The artboard is the `%%HiResBoundingBox` +(else `%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know, +an error, a runaway loop) or that draws nothing comes in as its TIFF preview (palette previews with an alpha channel +too) with a warning; without a preview, the art drawn up to the error is kept with a warning, and a file with none is refused with a message saying why. ```json