Fix #474: Illustrator 8 EPS gradients open (executable strings, load by integer keys)

Stock EPS files saved in the Illustrator 8 format failed with "typecheck in `a
dictionary`": its gradient procset builds shading dictionaries with `bd` …
`ed`, where `ed` is `(>>) cvx` (or a Level 1 dictionary builder as a
string), and `cvx` left a string literal, so `shfill` got a string. Its
`discard` procedure also `load`s procedures by integer keys, which `load`
refused.

- Executable strings: `cvx` makes a string executable (`xcheck` true,
  `cvlit` back, still a `stringtype` for `length`/`get`/`forall`/
  `getinterval`); run by name or met in a procedure, it runs its text as a
  program, every time (an executable file shares the same runner).
- `load` takes any key (integers, booleans), as `where` already did.
- Test: an Illustrator 8-style gradient procset with both `ed` forms and
  `discard`; the import fuzz vocabulary has executable strings and integer
  keys. An Illustrator 2026 file saved in the Illustrator 8 format (made
  here, not committed) now opens as vectors with its radial gradient.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-08 18:24:05 +07:00
1 parent 9f3b992ecc
commit 4827780f03
5 files changed
+80 -19

No files matched your search

+2 -2
View File
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469); see [Honest assessment](#honest-assessment-2026-10-05))._
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469, #474); see [Honest assessment](#honest-assessment-2026-10-05))._
## Where we are
@@ -152,7 +152,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 (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465).
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; 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, an `.ai`'s white page under its layers left out rather than coming in as art, 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). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320).
- 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. It also runs what other generators write (Discord feedback, #466, #469): Illustrator 2026's procsets (`gcheck`, `rootfont`, `currentcolorrendering`: every Illustrator 2026 EPS fell back to its preview before), cairo/Inkscape (`status`/`flushfile`, images with a mask, `ImageType 3`, reusable streams), matplotlib's Type 3 fonts, Ghostscript's `//name` immediate names, `pdfmark`, `token`, `execform` and user paths. Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. 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. It also runs what other generators write (Discord feedback, #466, #469): Illustrator 2026's procsets (`gcheck`, `rootfont`, `currentcolorrendering`: every Illustrator 2026 EPS fell back to its preview before), cairo/Inkscape (`status`/`flushfile`, images with a mask, `ImageType 3`, reusable streams), matplotlib's Type 3 fonts, Illustrator 8 EPS (stock art's usual format: executable strings such as its gradients' `ed`, `load` by integer keys, #474), Ghostscript's `//name` immediate names, `pdfmark`, `token`, `execform` and user paths. Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. 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; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
- **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.
- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so the desktop app asks for the power-saving GPU by default (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306). Preferences › Performance › Graphics Processor (`gpuPreference`) or `WGPU_POWER_PREF` picks the discrete one; Help › About and `ui.inspect` name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web).
+2
View File
@@ -948,6 +948,8 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
"/T3 << /FontType 3 /FontMatrix [0.1 0 0 0.1 0 0] /FontBBox [0 0 9 9] /Encoding [/a /b] /BuildChar { pop pop 9 0 setcharwidth 0 0 5 5 rectfill } >> definefont setfont",
"/T4 << /FontType 3 /FontMatrix [1 0 0 1 0 0] /BuildGlyph { pop pop (x) show } /Encoding [/a] >> definefont setfont",
"glyphshow", "xshow", "xyshow", "awidthshow", "kshow", "/a", "[1 2 3]",
// Executable strings and integer keys (Illustrator 8's procsets).
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
])
.prop_map(str::to_string),
]
+30 -15
View File
@@ -260,7 +260,7 @@ impl<'a> Interp<'a> {
pub fn pop_str(&mut self) -> Res<Shared<u8>> {
match self.pop()? {
Obj::Str(s) => Ok(s),
Obj::Str(s) | Obj::ExecStr(s) => Ok(s),
_ => ps_err("typecheck", "a string"),
}
}
@@ -291,8 +291,12 @@ impl<'a> Interp<'a> {
/// The value of name `name` on the dictionary stack.
pub fn lookup(&self, name: &Rc<str>) -> Option<Obj> {
let key = Key::Name(name.clone());
self.dicts.iter().rev().find_map(|d| d.borrow().get(&key).cloned())
self.lookup_key(&Key::Name(name.clone()))
}
/// The value of any key (`load` takes numbers too) on the dictionary stack.
fn lookup_key(&self, key: &Key) -> Option<Obj> {
self.dicts.iter().rev().find_map(|d| d.borrow().get(key).cloned())
}
/// Count `bytes` the program makes against [`MAX_MEMORY`].
@@ -340,6 +344,8 @@ impl<'a> Interp<'a> {
r
}
Obj::Op(op) => self.op(op),
// An executable string runs, as an operator does (procedures are pushed).
s @ Obj::ExecStr(_) => self.call(s),
o => self.push(o),
}
}
@@ -349,7 +355,11 @@ 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::File { stream, exec: true } => match self.program_of(&stream)? {
Some(program) => self.run_program(&program),
None => Ok(()),
},
Obj::ExecStr(s) => self.run_program(&s.to_vec()),
Obj::Op(op) => self.op(op),
Obj::Exec(name) => {
self.enter()?;
@@ -379,11 +389,10 @@ 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<RefCell<super::data::Stream>>) -> Res {
let Some(program) = self.program_of(f)? else { return Ok(()) };
/// Run `program` (what is left of an executable file, an executable string), token by token.
fn run_program(&mut self, program: &[u8]) -> Res {
self.enter()?;
let mut lex = Lexer::new(&program);
let mut lex = Lexer::new(program);
let r = loop {
match lex.next() {
Ok(Some(o)) => {
@@ -760,6 +769,7 @@ impl<'a> Interp<'a> {
let o = match self.pop()? {
Obj::Array { items, .. } => Obj::Array { items, exec: true },
Obj::File { stream, .. } => Obj::File { stream, exec: true },
Obj::Str(s) => Obj::ExecStr(s),
Obj::Name(n) => Obj::Exec(n),
o => o,
};
@@ -769,6 +779,7 @@ impl<'a> Interp<'a> {
let o = match self.pop()? {
Obj::Array { items, .. } => Obj::Array { items, exec: false },
Obj::File { stream, .. } => Obj::File { stream, exec: false },
Obj::ExecStr(s) => Obj::Str(s),
Obj::Exec(n) => Obj::Name(n),
o => o,
};
@@ -776,7 +787,10 @@ impl<'a> Interp<'a> {
}
Xcheck => {
let o = self.pop()?;
self.push(Obj::Bool(matches!(o, Obj::Array { exec: true, .. } | Obj::File { exec: true, .. } | Obj::Exec(_) | Obj::Op(_))))?;
self.push(Obj::Bool(matches!(
o,
Obj::Array { exec: true, .. } | Obj::File { exec: true, .. } | Obj::ExecStr(_) | Obj::Exec(_) | Obj::Op(_)
)))?;
}
Cvn => {
let s = self.pop_str()?;
@@ -829,7 +843,7 @@ impl<'a> Interp<'a> {
Length => {
let n = match self.pop()? {
Obj::Array { items, .. } => items.len(),
Obj::Str(s) => s.len(),
Obj::Str(s) | Obj::ExecStr(s) => s.len(),
Obj::Dict(d) => d.borrow().len(),
Obj::Name(n) | Obj::Exec(n) => n.len(),
_ => return ps_err("typecheck", ""),
@@ -845,7 +859,7 @@ impl<'a> Interp<'a> {
let k = self.pop()?;
let o = match self.pop()? {
Obj::Array { items, .. } => items.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?,
Obj::Str(s) => Obj::Int(i64::from(s.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?)),
Obj::Str(s) | Obj::ExecStr(s) => Obj::Int(i64::from(s.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?)),
Obj::Dict(d) => {
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
let v = d.borrow().get(&key).cloned();
@@ -958,8 +972,8 @@ impl<'a> Interp<'a> {
}
Load => {
let k = self.pop()?;
let name = k.text().ok_or(PsError::Ps("typecheck", String::new()))?;
let v = self.lookup(&name).ok_or_else(|| PsError::Ps("undefined", name.to_string()))?;
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
let v = self.lookup_key(&key).ok_or_else(|| PsError::Ps("undefined", show_text(&k)))?;
self.push(v)?;
}
Store => {
@@ -1143,6 +1157,7 @@ impl<'a> Interp<'a> {
let r = match o {
Obj::Array { items, exec } => Obj::Array { items: items.sub(at, n).ok_or_else(range)?, exec },
Obj::Str(s) => Obj::Str(s.sub(at, n).ok_or_else(range)?),
Obj::ExecStr(s) => Obj::ExecStr(s.sub(at, n).ok_or_else(range)?),
_ => return ps_err("typecheck", "getinterval"),
};
self.push(r)
@@ -1161,7 +1176,7 @@ impl<'a> Interp<'a> {
}
}
}
Obj::Str(s) => {
Obj::Str(s) | Obj::ExecStr(s) => {
for b in s.to_vec() {
self.push(Obj::Int(i64::from(b)))?;
if !self.body(&p)? {
@@ -1321,7 +1336,7 @@ fn show_text(o: &Obj) -> String {
}
Obj::Bool(b) => b.to_string(),
Obj::Name(n) | Obj::Exec(n) => n.to_string(),
Obj::Str(s) => String::from_utf8_lossy(&s.borrow()).into_owned(),
Obj::Str(s) | Obj::ExecStr(s) => String::from_utf8_lossy(&s.borrow()).into_owned(),
Obj::Op(op) => op.name().to_string(),
_ => "--nostringval--".into(),
}
+4 -2
View File
@@ -32,6 +32,8 @@ pub(crate) enum Obj {
/// An executable name: looked up and run.
Exec(Rc<str>),
Str(Shared<u8>),
/// An executable string (`cvx`): run, it runs its text as a program.
ExecStr(Shared<u8>),
/// An array; executable ones are procedures.
Array {
items: Shared<Obj>,
@@ -85,7 +87,7 @@ impl Obj {
pub fn text(&self) -> Option<Rc<str>> {
match self {
Self::Name(n) | Self::Exec(n) => Some(n.clone()),
Self::Str(s) => Some(Rc::from(String::from_utf8_lossy(&s.borrow()).as_ref())),
Self::Str(s) | Self::ExecStr(s) => Some(Rc::from(String::from_utf8_lossy(&s.borrow()).as_ref())),
_ => None,
}
}
@@ -117,7 +119,7 @@ impl Obj {
Self::Int(_) => "integertype",
Self::Real(_) => "realtype",
Self::Name(_) | Self::Exec(_) => "nametype",
Self::Str(_) => "stringtype",
Self::Str(_) | Self::ExecStr(_) => "stringtype",
Self::Array { .. } => "arraytype",
Self::Dict(_) => "dicttype",
Self::Op(_) => "operatortype",
+42
View File
@@ -949,3 +949,45 @@ fn hostile_patterns_glyphs_and_shadings_end() {
clean(&r);
assert_eq!(objects(&r.document).len(), 1);
}
/// Illustrator 8 EPS (what stock art often comes as, #474): its gradient procset builds shading
/// dictionaries with `bd` … `ed`, where `ed` is an executable string (`(>>) cvx`, or a Level 1
/// dictionary builder), and its `discard` runs procedures it `load`s by integer keys.
#[test]
fn illustrator_8_gradients_and_executable_strings() {
let program = r##"%%BeginProlog
userdict /AGM_Gradient 20 dict dup begin put
/AGM_Gradient_private 201 dict def
/initialize {
AGM_Gradient begin AGM_Gradient_private begin
/bd systemdict /mark get def
/ed /languagelevel where { pop languagelevel 2 ge } { false } ifelse
{ (>>) } { (counttomark 2 idiv dup dict begin {def} repeat pop currentdict end) } ifelse cvx def
/ed1 (counttomark 2 idiv dup dict begin {def} repeat pop currentdict end) cvx def
end end
} def
userdict /discardDict 4 dict dup begin put
0 { 0 1 0 setrgbcolor } def
2 { 0 0 1 setrgbcolor } def
end
/gt38? false def
/discard { discardDict begin /endString exch def gt38? { 2 add } if load stopped pop end } bind def
%%EndProlog
initialize
AGM_Gradient begin AGM_Gradient_private begin
bd /ShadingType 3 /ColorSpace /DeviceRGB
/Function bd /FunctionType 2 /Domain [0 1] /C0 [1 1 0] /C1 [0 0.5 0] /N 1 ed
/Extend [true true] /Coords [50 50 0 50 50 40] ed
gsave 10 10 80 80 rectclip shfill grestore
bd /Width 3 ed1 /Width get 3 eq { 0 (%AI5_EndPalette) discard 10 10 20 20 rectfill } if
end end
(x) cvx xcheck (x) cvx cvlit xcheck not and { 50 50 10 10 rectfill } if
showpage"##;
let r = open("Adobe Illustrator(R) 8.0", program);
let d = &r.document;
let Some(Paint::Gradient(g)) = all(d).into_iter().find_map(|n| fill(&n).cloned()) else { panic!("no gradient") };
assert_eq!(g.gradient.kind, vectorcraft_color::GradientKind::Radial);
// `discard` ran the procedure under key 0 (green), not the one under 2.
assert!(all(d).iter().any(|n| fill(n).and_then(Paint::color) == Some(Color::rgb(0.0, 1.0, 0.0))));
assert_eq!(objects(d).len(), 3);
}