Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a89a3211 | ||
|
|
fbcadf317c | ||
|
|
64891c1a2e | ||
|
|
83faf568b0 |
No files matched your search
+5
-5
@@ -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.
|
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-07 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7 and the MCP prompts, resource templates, completions and logging; see [Honest assessment](#honest-assessment-2026-10-05))._
|
_Last updated: 2026-10-07 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, and the 2026-10-07 issue fixes; see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||||
|
|
||||||
## Where we are
|
## Where we are
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||||
- Medium Dark theme, categorized and Advanced toolbars, 35 pt document tabs, 33 pt panel tabs.
|
- Medium Dark theme, categorized and Advanced toolbars, 35 pt document tabs, 33 pt panel tabs.
|
||||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||||
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan) ships complete, Czech covers every menu label and Japanese the main menus, with system-locale detection on macOS, Windows and Linux, a VectorCraft ▸ Language menu and a Language preference.
|
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan) ships complete, Czech covers every menu label and Japanese the main menus, with system-locale detection on macOS, Windows and Linux, a VectorCraft ▸ Language menu and a Language preference. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool.
|
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool.
|
||||||
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
|
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
|
||||||
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
|
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
|
||||||
@@ -113,7 +113,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||||||
- **Drawing aids:** Smart Guides and snapping, and Draw Normal / Behind / Inside modes.
|
- **Drawing aids:** Smart Guides and snapping, and Draw Normal / Behind / Inside modes.
|
||||||
- **Geometry and effects:**
|
- **Geometry and effects:**
|
||||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below.
|
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below.
|
||||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||||
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline.
|
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline.
|
||||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional Chinese covers the whole interface, Czech every menu label (tested) and Japanese the main menus; untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko, upright glyphs centred on the column, point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional Chinese covers the whole interface, Czech every menu label (tested) and Japanese the main menus; untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko, upright glyphs centred on the column, point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
||||||
@@ -133,7 +133,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||||||
- **Files (M4):**
|
- **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.
|
- `.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, 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).
|
- 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, 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 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).
|
||||||
- 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.
|
- 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.
|
- **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`).
|
- **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`).
|
||||||
@@ -191,7 +191,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 |
|
| 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 |
|
| 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 | 4–8 |
|
| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth | 4–8 |
|
||||||
| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, 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 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 |
|
||||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
| 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 |
|
| 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 |
|
| Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 |
|
||||||
|
|||||||
@@ -910,6 +910,9 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
|
|||||||
"stopped", "begin", "end", "dict", "array", "string", "get", "put", "getinterval", "putinterval", "aload", "astore", "forall",
|
"stopped", "begin", "end", "dict", "array", "string", "get", "put", "getinterval", "putinterval", "aload", "astore", "forall",
|
||||||
"[", "]", "<<", ">>", "{", "}", "matrix", "currentmatrix", "setmatrix", "transform", "itransform", "invertmatrix", "pathbbox",
|
"[", "]", "<<", ">>", "{", "}", "matrix", "currentmatrix", "setmatrix", "transform", "itransform", "invertmatrix", "pathbbox",
|
||||||
"clippath", "initclip", "currentpoint", "eexec", "cleartomark", "mark", "counttomark", "showpage", "gstate", "setgstate",
|
"clippath", "initclip", "currentpoint", "eexec", "cleartomark", "mark", "counttomark", "showpage", "gstate", "setgstate",
|
||||||
|
"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",
|
||||||
"/ASCII85Decode", "/ASCIIHexDecode", "/FlateDecode", "/RunLengthDecode", "/LZWDecode", "/DCTDecode", "/SubFileDecode",
|
"/ASCII85Decode", "/ASCIIHexDecode", "/FlateDecode", "/RunLengthDecode", "/LZWDecode", "/DCTDecode", "/SubFileDecode",
|
||||||
"/DeviceRGB", "/DeviceCMYK", "/DeviceGray", "/Pattern", "/x", "x", "/Helvetica", "(abc)", "<ff00>", "<~87cURD]i,\"Ebo80~>",
|
"/DeviceRGB", "/DeviceCMYK", "/DeviceGray", "/Pattern", "/x", "x", "/Helvetica", "(abc)", "<ff00>", "<~87cURD]i,\"Ebo80~>",
|
||||||
"true", "false", "null", "[/Separation (S) /DeviceCMYK {dup dup dup}]", "[/Indexed /DeviceRGB 1 <ff000000ff00>]",
|
"true", "false", "null", "[/Separation (S) /DeviceCMYK {dup dup dup}]", "[/Indexed /DeviceRGB 1 <ff000000ff00>]",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use vectorcraft_geom::Affine;
|
|||||||
use super::graphics::{Space, process};
|
use super::graphics::{Space, process};
|
||||||
use super::interp::{Interp, matrix_of};
|
use super::interp::{Interp, matrix_of};
|
||||||
use super::lex::{find, hex_decode};
|
use super::lex::{find, hex_decode};
|
||||||
use super::obj::{DictRef, Key, Obj, Op, PsError, Res, ps_err};
|
use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
||||||
use crate::ps;
|
use crate::ps;
|
||||||
|
|
||||||
/// Most bytes one filter or image reads.
|
/// Most bytes one filter or image reads.
|
||||||
@@ -223,20 +223,15 @@ impl Interp<'_> {
|
|||||||
ReadHexString | ReadString => {
|
ReadHexString | ReadString => {
|
||||||
let s = self.pop_str()?;
|
let s = self.pop_str()?;
|
||||||
let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") };
|
let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") };
|
||||||
let n = s.borrow().len();
|
let n = s.len();
|
||||||
let got = if op == ReadString { self.read(&f, n)? } else { self.read_hex(&f, n)? };
|
let got = if op == ReadString { self.read(&f, n)? } else { self.read_hex(&f, n)? };
|
||||||
let full = got.len() == n;
|
self.fill(s, &got)?;
|
||||||
self.alloc(got.len())?;
|
self.push(Obj::Bool(got.len() == n))?;
|
||||||
if let Some(d) = s.borrow_mut().get_mut(..got.len()) {
|
|
||||||
d.copy_from_slice(&got);
|
|
||||||
}
|
|
||||||
self.push(Obj::string(got))?;
|
|
||||||
self.push(Obj::Bool(full))?;
|
|
||||||
}
|
}
|
||||||
ReadLine => {
|
ReadLine => {
|
||||||
let s = self.pop_str()?;
|
let s = self.pop_str()?;
|
||||||
let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") };
|
let Obj::File(f) = self.pop()? else { return ps_err("typecheck", "") };
|
||||||
let n = s.borrow().len();
|
let n = s.len();
|
||||||
let mut line = vec![];
|
let mut line = vec![];
|
||||||
let mut ended = false;
|
let mut ended = false;
|
||||||
while line.len() < n {
|
while line.len() < n {
|
||||||
@@ -252,8 +247,7 @@ impl Interp<'_> {
|
|||||||
Some(b) => line.push(*b),
|
Some(b) => line.push(*b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.alloc(line.len())?;
|
self.fill(s, &line)?;
|
||||||
self.push(Obj::string(line))?;
|
|
||||||
self.push(Obj::Bool(ended))?;
|
self.push(Obj::Bool(ended))?;
|
||||||
}
|
}
|
||||||
FlushFile | CloseFile => {
|
FlushFile | CloseFile => {
|
||||||
@@ -307,12 +301,20 @@ impl Interp<'_> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read bytes `got` into string `s` and push the part of `s` they fill.
|
||||||
|
fn fill(&mut self, s: Shared<u8>, got: &[u8]) -> Res {
|
||||||
|
if !s.write(0, got) {
|
||||||
|
return ps_err("rangecheck", "");
|
||||||
|
}
|
||||||
|
self.push_interval(Obj::Str(s), 0, got.len())
|
||||||
|
}
|
||||||
|
|
||||||
/// `filter`: a file that decodes its source. Over the program's own data it decodes when
|
/// `filter`: a file that decodes its source. Over the program's own data it decodes when
|
||||||
/// first read (the data follows the operator that reads it); over other data, at once.
|
/// first read (the data follows the operator that reads it); over other data, at once.
|
||||||
fn filter(&mut self) -> Res {
|
fn filter(&mut self) -> Res {
|
||||||
let name = self.pop()?.text().ok_or(PsError::Ps("typecheck", "filter".into()))?;
|
let name = self.pop()?.text().ok_or(PsError::Ps("typecheck", "filter".into()))?;
|
||||||
let sub = if &*name == "SubFileDecode" {
|
let sub = if &*name == "SubFileDecode" {
|
||||||
let eod = self.pop_str()?.borrow().clone();
|
let eod = self.pop_str()?.to_vec();
|
||||||
let count = self.pop_count()?;
|
let count = self.pop_count()?;
|
||||||
(count, eod)
|
(count, eod)
|
||||||
} else {
|
} else {
|
||||||
@@ -687,8 +689,8 @@ struct Pixels {
|
|||||||
fn source(o: &Obj) -> Res<Source> {
|
fn source(o: &Obj) -> Res<Source> {
|
||||||
Ok(match o {
|
Ok(match o {
|
||||||
Obj::File(f) => Source::File(f.clone()),
|
Obj::File(f) => Source::File(f.clone()),
|
||||||
Obj::Str(s) => Source::Str(s.borrow().clone()),
|
Obj::Str(s) => Source::Str(s.to_vec()),
|
||||||
p @ Obj::Array { .. } => Source::Proc(Obj::Array { items: p.items().cloned().unwrap_or_default(), exec: true }),
|
Obj::Array { items, .. } => Source::Proc(Obj::Array { items: items.clone(), exec: true }),
|
||||||
_ => return ps_err("typecheck", "DataSource"),
|
_ => return ps_err("typecheck", "DataSource"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ pub(crate) struct GState {
|
|||||||
pub cur: Option<Point>,
|
pub cur: Option<Point>,
|
||||||
pub start: Option<Point>,
|
pub start: Option<Point>,
|
||||||
pub clips: Vec<Arc<Clip>>,
|
pub clips: Vec<Arc<Clip>>,
|
||||||
|
/// The clips `clipsave` saved, for `cliprestore`.
|
||||||
|
pub saved_clips: Vec<Rc<[Arc<Clip>]>>,
|
||||||
pub overprint: bool,
|
pub overprint: bool,
|
||||||
pub font: Option<DictRef>,
|
pub font: Option<DictRef>,
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,7 @@ impl Default for GState {
|
|||||||
cur: None,
|
cur: None,
|
||||||
start: None,
|
start: None,
|
||||||
clips: vec![],
|
clips: vec![],
|
||||||
|
saved_clips: vec![],
|
||||||
overprint: false,
|
overprint: false,
|
||||||
font: None,
|
font: None,
|
||||||
}
|
}
|
||||||
@@ -489,7 +492,7 @@ impl Interp<'_> {
|
|||||||
let (family, items) = match o {
|
let (family, items) = match o {
|
||||||
Obj::Name(n) | Obj::Exec(n) => (n.clone(), vec![]),
|
Obj::Name(n) | Obj::Exec(n) => (n.clone(), vec![]),
|
||||||
Obj::Array { items, .. } => {
|
Obj::Array { items, .. } => {
|
||||||
let v = items.borrow().clone();
|
let v = items.to_vec();
|
||||||
let n = v.first().and_then(Obj::text).ok_or(PsError::Ps("typecheck", "setcolorspace".into()))?;
|
let n = v.first().and_then(Obj::text).ok_or(PsError::Ps("typecheck", "setcolorspace".into()))?;
|
||||||
(n, v)
|
(n, v)
|
||||||
}
|
}
|
||||||
@@ -702,7 +705,7 @@ impl Interp<'_> {
|
|||||||
let d = match f {
|
let d = match f {
|
||||||
Obj::Dict(d) => d.clone(),
|
Obj::Dict(d) => d.clone(),
|
||||||
Obj::Array { items, .. } => {
|
Obj::Array { items, .. } => {
|
||||||
let fs = items.borrow().clone();
|
let fs = items.to_vec();
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
for g in &fs {
|
for g in &fs {
|
||||||
out.extend(self.eval(g, t, depth + 1)?.first().copied());
|
out.extend(self.eval(g, t, depth + 1)?.first().copied());
|
||||||
@@ -730,7 +733,7 @@ impl Interp<'_> {
|
|||||||
Ok(c0.iter().zip(&c1).map(|(a, b)| a + x * (b - a)).collect())
|
Ok(c0.iter().zip(&c1).map(|(a, b)| a + x * (b - a)).collect())
|
||||||
}
|
}
|
||||||
Some(3.0) => {
|
Some(3.0) => {
|
||||||
let fs = get("Functions").and_then(|o| o.items().map(|i| i.borrow().clone())).unwrap_or_default();
|
let fs = get("Functions").and_then(|o| o.items().map(|i| i.to_vec())).unwrap_or_default();
|
||||||
let bounds = nums(get("Bounds"));
|
let bounds = nums(get("Bounds"));
|
||||||
let encode = nums(get("Encode"));
|
let encode = nums(get("Encode"));
|
||||||
let (lo, hi) = (domain.first().copied().unwrap_or(0.0), domain.get(1).copied().unwrap_or(1.0));
|
let (lo, hi) = (domain.first().copied().unwrap_or(0.0), domain.get(1).copied().unwrap_or(1.0));
|
||||||
@@ -1095,6 +1098,19 @@ impl Interp<'_> {
|
|||||||
self.g.start = self.g.cur;
|
self.g.start = self.g.cur;
|
||||||
}
|
}
|
||||||
InitClip => self.g.clips.clear(),
|
InitClip => self.g.clips.clear(),
|
||||||
|
ClipSave => {
|
||||||
|
if self.g.saved_clips.len() >= MAX_GSAVE {
|
||||||
|
return Err(PsError::Limit("too many saved clips"));
|
||||||
|
}
|
||||||
|
self.alloc(std::mem::size_of_val(self.g.clips.as_slice()))?;
|
||||||
|
let clips = self.g.clips.as_slice().into();
|
||||||
|
self.g.saved_clips.push(clips);
|
||||||
|
}
|
||||||
|
ClipRestore => {
|
||||||
|
if let Some(clips) = self.g.saved_clips.pop() {
|
||||||
|
self.g.clips = clips.to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
Clip | EoClip => {
|
Clip | EoClip => {
|
||||||
let bp = self.g.path.clone();
|
let bp = self.g.path.clone();
|
||||||
self.clip_with(bp, if op == EoClip { FillRule::EvenOdd } else { FillRule::NonZero });
|
self.clip_with(bp, if op == EoClip { FillRule::EvenOdd } else { FillRule::NonZero });
|
||||||
@@ -1118,7 +1134,7 @@ impl Interp<'_> {
|
|||||||
}
|
}
|
||||||
RectStroke => {
|
RectStroke => {
|
||||||
let m = match self.stack.last() {
|
let m = match self.stack.last() {
|
||||||
Some(Obj::Array { items, .. }) if items.borrow().len() == 6 => Some(self.pop_matrix()?),
|
Some(Obj::Array { items, .. }) if items.len() == 6 => Some(self.pop_matrix()?),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let mut bp = self.rects()?;
|
let mut bp = self.rects()?;
|
||||||
@@ -1157,7 +1173,7 @@ fn linear_points(f: &Obj) -> Option<Vec<f64>> {
|
|||||||
match num("FunctionType")? {
|
match num("FunctionType")? {
|
||||||
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
|
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
|
||||||
3.0 => {
|
3.0 => {
|
||||||
let fs = d.get(&Key::name("Functions"))?.items()?.borrow().clone();
|
let fs = d.get(&Key::name("Functions"))?.items()?.to_vec();
|
||||||
if !fs.iter().all(|g| linear_points(g).is_some_and(|p| p.is_empty())) {
|
if !fs.iter().all(|g| linear_points(g).is_some_and(|p| p.is_empty())) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-96
@@ -11,7 +11,7 @@ use vectorcraft_geom::Affine;
|
|||||||
|
|
||||||
use super::graphics::{GState, Out};
|
use super::graphics::{GState, Out};
|
||||||
use super::lex::Lexer;
|
use super::lex::Lexer;
|
||||||
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, ps_err};
|
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
||||||
|
|
||||||
/// Most operations one file may run.
|
/// Most operations one file may run.
|
||||||
const MAX_OPS: u64 = 50_000_000;
|
const MAX_OPS: u64 = 50_000_000;
|
||||||
@@ -61,6 +61,12 @@ fn encoding() -> Obj {
|
|||||||
Obj::array(vec![Obj::name(".notdef"); 256])
|
Obj::array(vec![Obj::name(".notdef"); 256])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resource categories there are to begin with (instances of `Category`).
|
||||||
|
const CATEGORIES: &str = "Category Generic Font CIDFont CMap FontSet Encoding Form Pattern ProcSet ColorSpace Halftone \r
|
||||||
|
ColorRendering IdiomSet InkParams TrapParams OutputDevice ControlLanguage Localization PDL HWOptions \r
|
||||||
|
Filter ColorSpaceFamily Emulator IODevice ColorRenderingType FMapType FontType FormType HalftoneType \r
|
||||||
|
ImageType PatternType FunctionType ShadingType";
|
||||||
|
|
||||||
impl<'a> Interp<'a> {
|
impl<'a> Interp<'a> {
|
||||||
pub fn new(src: &'a [u8], g: GState, out: Out) -> Self {
|
pub fn new(src: &'a [u8], g: GState, out: Out) -> Self {
|
||||||
let system = new_dict();
|
let system = new_dict();
|
||||||
@@ -72,7 +78,15 @@ impl<'a> Interp<'a> {
|
|||||||
for name in ["StandardEncoding", "ISOLatin1Encoding"] {
|
for name in ["StandardEncoding", "ISOLatin1Encoding"] {
|
||||||
d.insert(Key::name(name), encoding());
|
d.insert(Key::name(name), encoding());
|
||||||
}
|
}
|
||||||
|
// An integer, as interpreters have it (`systemdict /languagelevel get 2 ge`): run, the
|
||||||
|
// name pushes it all the same.
|
||||||
|
d.insert(Key::name("languagelevel"), Obj::Int(3));
|
||||||
}
|
}
|
||||||
|
// Each category's implementation dictionary (`/Generic /Category findresource`).
|
||||||
|
let categories: Dict =
|
||||||
|
CATEGORIES.split_whitespace().map(|c| (Key::name(c), Obj::dict(Dict::from([(Key::name("Category"), Obj::name(c))])))).collect();
|
||||||
|
let resources = new_dict();
|
||||||
|
resources.borrow_mut().insert(Key::name("Category"), Obj::dict(categories));
|
||||||
let fonts = new_dict();
|
let fonts = new_dict();
|
||||||
system.borrow_mut().insert(Key::name("FontDirectory"), Obj::Dict(fonts.clone()));
|
system.borrow_mut().insert(Key::name("FontDirectory"), Obj::Dict(fonts.clone()));
|
||||||
system.borrow_mut().insert(Key::name("GlobalFontDirectory"), Obj::Dict(fonts.clone()));
|
system.borrow_mut().insert(Key::name("GlobalFontDirectory"), Obj::Dict(fonts.clone()));
|
||||||
@@ -87,7 +101,7 @@ impl<'a> Interp<'a> {
|
|||||||
status: new_dict(),
|
status: new_dict(),
|
||||||
error: new_dict(),
|
error: new_dict(),
|
||||||
fonts,
|
fonts,
|
||||||
resources: new_dict(),
|
resources,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
ops: 0,
|
ops: 0,
|
||||||
allocated: 0,
|
allocated: 0,
|
||||||
@@ -175,7 +189,7 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_str(&mut self) -> Res<Rc<RefCell<Vec<u8>>>> {
|
pub fn pop_str(&mut self) -> Res<Shared<u8>> {
|
||||||
match self.pop()? {
|
match self.pop()? {
|
||||||
Obj::Str(s) => Ok(s),
|
Obj::Str(s) => Ok(s),
|
||||||
_ => ps_err("typecheck", "a string"),
|
_ => ps_err("typecheck", "a string"),
|
||||||
@@ -183,7 +197,7 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An array or procedure.
|
/// An array or procedure.
|
||||||
pub fn pop_array(&mut self) -> Res<Rc<RefCell<Vec<Obj>>>> {
|
pub fn pop_array(&mut self) -> Res<Shared<Obj>> {
|
||||||
match self.pop()? {
|
match self.pop()? {
|
||||||
Obj::Array { items, .. } => Ok(items),
|
Obj::Array { items, .. } => Ok(items),
|
||||||
_ => ps_err("typecheck", "an array"),
|
_ => ps_err("typecheck", "an array"),
|
||||||
@@ -197,15 +211,9 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Store `m` into the matrix operand `items` and push it back.
|
/// Store `m` into the matrix operand `items` and push it back.
|
||||||
pub fn put_matrix(&mut self, items: Rc<RefCell<Vec<Obj>>>, m: Affine) -> Res {
|
pub fn put_matrix(&mut self, items: Shared<Obj>, m: Affine) -> Res {
|
||||||
{
|
if items.len() != 6 || !items.write(0, &m.as_coeffs().map(Obj::Real)) {
|
||||||
let mut v = items.borrow_mut();
|
return ps_err("rangecheck", "a matrix");
|
||||||
if v.len() != 6 {
|
|
||||||
return ps_err("rangecheck", "a matrix");
|
|
||||||
}
|
|
||||||
for (slot, c) in v.iter_mut().zip(m.as_coeffs()) {
|
|
||||||
*slot = Obj::Real(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.push(Obj::Array { items, exec: false })
|
self.push(Obj::Array { items, exec: false })
|
||||||
}
|
}
|
||||||
@@ -276,24 +284,24 @@ impl<'a> Interp<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_proc(&mut self, items: &Rc<RefCell<Vec<Obj>>>) -> Res {
|
fn run_proc(&mut self, items: &Shared<Obj>) -> Res {
|
||||||
self.enter()?;
|
self.enter()?;
|
||||||
let r = self.run_items(items);
|
let r = self.run_items(items);
|
||||||
self.depth -= 1;
|
self.depth -= 1;
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_items(&mut self, items: &Rc<RefCell<Vec<Obj>>>) -> Res {
|
fn run_items(&mut self, items: &Shared<Obj>) -> Res {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
loop {
|
loop {
|
||||||
let Some(o) = items.borrow().get(i).cloned() else { return Ok(()) };
|
let Some(o) = items.get(i) else { return Ok(()) };
|
||||||
i += 1;
|
i += 1;
|
||||||
self.exec_token(o)?;
|
self.exec_token(o)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `proc` as a loop body: `Ok(false)` when it ran `exit`.
|
/// Run `proc` as a loop body: `Ok(false)` when it ran `exit`.
|
||||||
fn body(&mut self, proc: &Obj) -> Res<bool> {
|
pub fn body(&mut self, proc: &Obj) -> Res<bool> {
|
||||||
self.tick()?;
|
self.tick()?;
|
||||||
match self.call(proc.clone()) {
|
match self.call(proc.clone()) {
|
||||||
Ok(()) => Ok(true),
|
Ok(()) => Ok(true),
|
||||||
@@ -303,7 +311,7 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run a procedure operand, executable or not (`exec`, `if`, loops).
|
/// Run a procedure operand, executable or not (`exec`, `if`, loops).
|
||||||
fn pop_proc(&mut self) -> Res<Obj> {
|
pub fn pop_proc(&mut self) -> Res<Obj> {
|
||||||
match self.pop()? {
|
match self.pop()? {
|
||||||
Obj::Array { items, .. } => Ok(Obj::Array { items, exec: true }),
|
Obj::Array { items, .. } => Ok(Obj::Array { items, exec: true }),
|
||||||
o @ (Obj::Op(_) | Obj::Exec(_)) => Ok(o),
|
o @ (Obj::Op(_) | Obj::Exec(_)) => Ok(o),
|
||||||
@@ -480,7 +488,7 @@ impl<'a> Interp<'a> {
|
|||||||
Gt | Ge | Lt | Le => {
|
Gt | Ge | Lt | Le => {
|
||||||
let (b, a) = (self.pop()?, self.pop()?);
|
let (b, a) = (self.pop()?, self.pop()?);
|
||||||
let ord = match (&a, &b) {
|
let ord = match (&a, &b) {
|
||||||
(Obj::Str(x), Obj::Str(y)) => x.borrow().as_slice().partial_cmp(y.borrow().as_slice()),
|
(Obj::Str(x), Obj::Str(y)) => x.borrow().partial_cmp(&*y.borrow()),
|
||||||
_ => a.as_num().zip(b.as_num()).and_then(|(x, y)| x.partial_cmp(&y)),
|
_ => a.as_num().zip(b.as_num()).and_then(|(x, y)| x.partial_cmp(&y)),
|
||||||
}
|
}
|
||||||
.ok_or(PsError::Ps("typecheck", String::new()))?;
|
.ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||||
@@ -646,14 +654,10 @@ impl<'a> Interp<'a> {
|
|||||||
} else {
|
} else {
|
||||||
show_text(&self.pop()?)
|
show_text(&self.pop()?)
|
||||||
};
|
};
|
||||||
let n = text.len();
|
if !s.write(0, text.as_bytes()) {
|
||||||
if n > s.borrow().len() {
|
|
||||||
return ps_err("rangecheck", "");
|
return ps_err("rangecheck", "");
|
||||||
}
|
}
|
||||||
if let Some(d) = s.borrow_mut().get_mut(..n) {
|
self.push_interval(Obj::Str(s), 0, text.len())?;
|
||||||
d.copy_from_slice(text.as_bytes());
|
|
||||||
}
|
|
||||||
self.push_interval(Obj::Str(s), 0, n)?;
|
|
||||||
}
|
}
|
||||||
ReadOnly | ExecuteOnly | NoAccess | SetPacking | SetGlobal | SetObjectFormat => {
|
ReadOnly | ExecuteOnly | NoAccess | SetPacking | SetGlobal | SetObjectFormat => {
|
||||||
if matches!(op, SetPacking | SetGlobal | SetObjectFormat) {
|
if matches!(op, SetPacking | SetGlobal | SetObjectFormat) {
|
||||||
@@ -683,8 +687,8 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
Length => {
|
Length => {
|
||||||
let n = match self.pop()? {
|
let n = match self.pop()? {
|
||||||
Obj::Array { items, .. } => items.borrow().len(),
|
Obj::Array { items, .. } => items.len(),
|
||||||
Obj::Str(s) => s.borrow().len(),
|
Obj::Str(s) => s.len(),
|
||||||
Obj::Dict(d) => d.borrow().len(),
|
Obj::Dict(d) => d.borrow().len(),
|
||||||
Obj::Name(n) | Obj::Exec(n) => n.len(),
|
Obj::Name(n) | Obj::Exec(n) => n.len(),
|
||||||
_ => return ps_err("typecheck", ""),
|
_ => return ps_err("typecheck", ""),
|
||||||
@@ -699,14 +703,8 @@ impl<'a> Interp<'a> {
|
|||||||
Get => {
|
Get => {
|
||||||
let k = self.pop()?;
|
let k = self.pop()?;
|
||||||
let o = match self.pop()? {
|
let o = match self.pop()? {
|
||||||
Obj::Array { items, .. } => {
|
Obj::Array { items, .. } => items.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?,
|
||||||
let i = index(&k)?;
|
Obj::Str(s) => Obj::Int(i64::from(s.get(index(&k)?).ok_or(PsError::Ps("rangecheck", String::new()))?)),
|
||||||
items.borrow().get(i).cloned().ok_or(PsError::Ps("rangecheck", String::new()))?
|
|
||||||
}
|
|
||||||
Obj::Str(s) => {
|
|
||||||
let i = index(&k)?;
|
|
||||||
Obj::Int(i64::from(*s.borrow().get(i).ok_or(PsError::Ps("rangecheck", String::new()))?))
|
|
||||||
}
|
|
||||||
Obj::Dict(d) => {
|
Obj::Dict(d) => {
|
||||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||||
let v = d.borrow().get(&key).cloned();
|
let v = d.borrow().get(&key).cloned();
|
||||||
@@ -718,21 +716,21 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
Put => {
|
Put => {
|
||||||
let (v, k, c) = (self.pop()?, self.pop()?, self.pop()?);
|
let (v, k, c) = (self.pop()?, self.pop()?, self.pop()?);
|
||||||
match c {
|
let written = match c {
|
||||||
Obj::Array { items, .. } => {
|
Obj::Array { items, .. } => items.write(index(&k)?, std::slice::from_ref(&v)),
|
||||||
let i = index(&k)?;
|
|
||||||
*items.borrow_mut().get_mut(i).ok_or(PsError::Ps("rangecheck", String::new()))? = v;
|
|
||||||
}
|
|
||||||
Obj::Str(s) => {
|
Obj::Str(s) => {
|
||||||
let i = index(&k)?;
|
|
||||||
let b = v.as_num().ok_or(PsError::Ps("typecheck", String::new()))?;
|
let b = v.as_num().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||||
*s.borrow_mut().get_mut(i).ok_or(PsError::Ps("rangecheck", String::new()))? = b as i64 as u8;
|
s.write(index(&k)?, &[b as i64 as u8])
|
||||||
}
|
}
|
||||||
Obj::Dict(d) => {
|
Obj::Dict(d) => {
|
||||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||||
insert(&d, key, v)?;
|
insert(&d, key, v)?;
|
||||||
|
true
|
||||||
}
|
}
|
||||||
_ => return ps_err("typecheck", ""),
|
_ => return ps_err("typecheck", ""),
|
||||||
|
};
|
||||||
|
if !written {
|
||||||
|
return ps_err("rangecheck", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GetInterval => {
|
GetInterval => {
|
||||||
@@ -744,41 +742,35 @@ impl<'a> Interp<'a> {
|
|||||||
PutInterval => {
|
PutInterval => {
|
||||||
let src = self.pop()?;
|
let src = self.pop()?;
|
||||||
let at = self.pop_count()?;
|
let at = self.pop_count()?;
|
||||||
match (self.pop()?, src) {
|
let written = match (self.pop()?, src) {
|
||||||
(Obj::Array { items, .. }, Obj::Array { items: from, .. }) => {
|
(Obj::Array { items, .. }, Obj::Array { items: from, .. }) => items.write(at, &from.to_vec()),
|
||||||
let from = from.borrow().clone();
|
(Obj::Str(s), Obj::Str(from)) => s.write(at, &from.to_vec()),
|
||||||
let mut v = items.borrow_mut();
|
|
||||||
let d = v.get_mut(at..at + from.len()).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
||||||
d.clone_from_slice(&from);
|
|
||||||
}
|
|
||||||
(Obj::Str(s), Obj::Str(from)) => {
|
|
||||||
let from = from.borrow().clone();
|
|
||||||
let mut v = s.borrow_mut();
|
|
||||||
let d = v.get_mut(at..at + from.len()).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
|
||||||
d.copy_from_slice(&from);
|
|
||||||
}
|
|
||||||
_ => return ps_err("typecheck", ""),
|
_ => return ps_err("typecheck", ""),
|
||||||
|
};
|
||||||
|
if !written {
|
||||||
|
return ps_err("rangecheck", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Aload => {
|
Aload => {
|
||||||
let items = self.pop_array()?;
|
let items = self.pop_array()?;
|
||||||
let all = items.borrow().clone();
|
for o in items.to_vec() {
|
||||||
for o in all {
|
|
||||||
self.push(o)?;
|
self.push(o)?;
|
||||||
}
|
}
|
||||||
self.push(Obj::Array { items, exec: false })?;
|
self.push(Obj::Array { items, exec: false })?;
|
||||||
}
|
}
|
||||||
Astore => {
|
Astore => {
|
||||||
let items = self.pop_array()?;
|
let items = self.pop_array()?;
|
||||||
let n = items.borrow().len();
|
let at = self.stack.len().checked_sub(items.len()).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
||||||
let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
if !items.write(0, self.stack.get(at..).unwrap_or_default()) {
|
||||||
*items.borrow_mut() = self.stack.split_off(at);
|
return ps_err("rangecheck", "");
|
||||||
|
}
|
||||||
|
self.stack.truncate(at);
|
||||||
self.push(Obj::Array { items, exec: false })?;
|
self.push(Obj::Array { items, exec: false })?;
|
||||||
}
|
}
|
||||||
Search | AnchorSearch => {
|
Search | AnchorSearch => {
|
||||||
let seek = self.pop_str()?.borrow().clone();
|
let seek = self.pop_str()?.to_vec();
|
||||||
let s = self.pop_str()?;
|
let s = self.pop_str()?;
|
||||||
let text = s.borrow().clone();
|
let text = s.to_vec();
|
||||||
let at = if op == Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) };
|
let at = if op == Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) };
|
||||||
match at {
|
match at {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
@@ -888,7 +880,6 @@ impl<'a> Interp<'a> {
|
|||||||
self.push(Obj::Int(v))?;
|
self.push(Obj::Int(v))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LanguageLevel => self.push(Obj::Int(3))?,
|
|
||||||
Version => self.push(Obj::string(b"3010".to_vec()))?,
|
Version => self.push(Obj::string(b"3010".to_vec()))?,
|
||||||
Product => self.push(Obj::string(b"VectorCraft".to_vec()))?,
|
Product => self.push(Obj::string(b"VectorCraft".to_vec()))?,
|
||||||
RealTime | UserTime => self.push(Obj::Int(0))?,
|
RealTime | UserTime => self.push(Obj::Int(0))?,
|
||||||
@@ -922,19 +913,18 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Obj::Array { items, exec } => {
|
Obj::Array { items, exec } => {
|
||||||
let from = self.pop_array()?.borrow().clone();
|
let from = self.pop_array()?.to_vec();
|
||||||
let n = from.len();
|
if !items.write(0, &from) {
|
||||||
{
|
return ps_err("rangecheck", "copy");
|
||||||
let mut v = items.borrow_mut();
|
|
||||||
v.get_mut(..n).ok_or(PsError::Ps("rangecheck", String::new()))?.clone_from_slice(&from);
|
|
||||||
}
|
}
|
||||||
self.push_interval(Obj::Array { items, exec }, 0, n)?;
|
self.push_interval(Obj::Array { items, exec }, 0, from.len())?;
|
||||||
}
|
}
|
||||||
Obj::Str(s) => {
|
Obj::Str(s) => {
|
||||||
let from = self.pop_str()?.borrow().clone();
|
let from = self.pop_str()?.to_vec();
|
||||||
let n = from.len();
|
if !s.write(0, &from) {
|
||||||
s.borrow_mut().get_mut(..n).ok_or(PsError::Ps("rangecheck", String::new()))?.copy_from_slice(&from);
|
return ps_err("rangecheck", "copy");
|
||||||
self.push_interval(Obj::Str(s), 0, n)?;
|
}
|
||||||
|
self.push_interval(Obj::Str(s), 0, from.len())?;
|
||||||
}
|
}
|
||||||
Obj::Dict(d) => {
|
Obj::Dict(d) => {
|
||||||
let from = self.pop_dict()?.borrow().clone();
|
let from = self.pop_dict()?.borrow().clone();
|
||||||
@@ -946,16 +936,12 @@ impl<'a> Interp<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `n` items of an array or string from `at` (a copy: shared storage isn't modelled).
|
/// `n` items of an array or string from `at`, sharing its storage.
|
||||||
fn push_interval(&mut self, o: Obj, at: usize, n: usize) -> Res {
|
pub fn push_interval(&mut self, o: Obj, at: usize, n: usize) -> Res {
|
||||||
let end = at.checked_add(n).ok_or(PsError::Ps("rangecheck", String::new()))?;
|
let range = || PsError::Ps("rangecheck", String::new());
|
||||||
self.alloc(n * if matches!(o, Obj::Str(_)) { 1 } else { std::mem::size_of::<Obj>() })?;
|
|
||||||
let r = match o {
|
let r = match o {
|
||||||
Obj::Array { items, exec } => {
|
Obj::Array { items, exec } => Obj::Array { items: items.sub(at, n).ok_or_else(range)?, exec },
|
||||||
let v = items.borrow().get(at..end).ok_or(PsError::Ps("rangecheck", String::new()))?.to_vec();
|
Obj::Str(s) => Obj::Str(s.sub(at, n).ok_or_else(range)?),
|
||||||
Obj::Array { items: Rc::new(RefCell::new(v)), exec }
|
|
||||||
}
|
|
||||||
Obj::Str(s) => Obj::string(s.borrow().get(at..end).ok_or(PsError::Ps("rangecheck", String::new()))?.to_vec()),
|
|
||||||
_ => return ps_err("typecheck", "getinterval"),
|
_ => return ps_err("typecheck", "getinterval"),
|
||||||
};
|
};
|
||||||
self.push(r)
|
self.push(r)
|
||||||
@@ -966,7 +952,7 @@ impl<'a> Interp<'a> {
|
|||||||
match self.pop()? {
|
match self.pop()? {
|
||||||
Obj::Array { items, .. } => {
|
Obj::Array { items, .. } => {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while let Some(o) = items.borrow().get(i).cloned() {
|
while let Some(o) = items.get(i) {
|
||||||
i += 1;
|
i += 1;
|
||||||
self.push(o)?;
|
self.push(o)?;
|
||||||
if !self.body(&p)? {
|
if !self.body(&p)? {
|
||||||
@@ -975,8 +961,7 @@ impl<'a> Interp<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Obj::Str(s) => {
|
Obj::Str(s) => {
|
||||||
let bytes = s.borrow().clone();
|
for b in s.to_vec() {
|
||||||
for b in bytes {
|
|
||||||
self.push(Obj::Int(i64::from(b)))?;
|
self.push(Obj::Int(i64::from(b)))?;
|
||||||
if !self.body(&p)? {
|
if !self.body(&p)? {
|
||||||
break;
|
break;
|
||||||
@@ -1000,22 +985,19 @@ impl<'a> Interp<'a> {
|
|||||||
|
|
||||||
/// Replace the names in a procedure that stand for operators by the operators, down through
|
/// Replace the names in a procedure that stand for operators by the operators, down through
|
||||||
/// nested procedures.
|
/// nested procedures.
|
||||||
fn bind(&mut self, items: &Rc<RefCell<Vec<Obj>>>, depth: u32) {
|
fn bind(&mut self, items: &Shared<Obj>, depth: u32) {
|
||||||
if depth > 64 {
|
if depth > 64 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let n = items.borrow().len();
|
for i in 0..items.len() {
|
||||||
for i in 0..n {
|
match items.get(i) {
|
||||||
let o = items.borrow().get(i).cloned();
|
|
||||||
match o {
|
|
||||||
Some(Obj::Exec(name)) => {
|
Some(Obj::Exec(name)) => {
|
||||||
if let Some(Obj::Op(op)) = self.lookup(&name)
|
if let Some(o @ Obj::Op(_)) = self.lookup(&name) {
|
||||||
&& let Some(slot) = items.borrow_mut().get_mut(i)
|
// `i` is in range and nothing is reading the procedure: the write can't fail.
|
||||||
{
|
items.write(i, &[o]);
|
||||||
*slot = Obj::Op(op);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Obj::Array { items: inner, exec: true }) if !Rc::ptr_eq(&inner, items) => self.bind(&inner, depth + 1),
|
Some(Obj::Array { items: inner, exec: true }) if !inner.shares(items) => self.bind(&inner, depth + 1),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1032,6 +1014,67 @@ impl<'a> Interp<'a> {
|
|||||||
_ => ps_err("typecheck", ""),
|
_ => ps_err("typecheck", ""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The instances of `category`: `FontDirectory` for fonts.
|
||||||
|
pub fn instances(&mut self, category: &Obj) -> Res<DictRef> {
|
||||||
|
if category.text().as_deref() == Some("Font") { Ok(self.fonts.clone()) } else { self.category(category) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `template proc scratch category resourceforall`: `proc` run on each instance name that
|
||||||
|
/// matches `template`, copied into `scratch` (in name order).
|
||||||
|
pub fn resource_for_all(&mut self) -> Res {
|
||||||
|
let category = self.pop()?;
|
||||||
|
let scratch = self.pop_str()?;
|
||||||
|
let p = self.pop_proc()?;
|
||||||
|
let template = self.pop_str()?.to_vec();
|
||||||
|
let mut names: Vec<Vec<u8>> = self.instances(&category)?.borrow().keys().map(|k| show_text(&k.obj()).into_bytes()).collect();
|
||||||
|
names.sort();
|
||||||
|
for name in names {
|
||||||
|
if !self.matches(&template, &name)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !scratch.write(0, &name) {
|
||||||
|
return ps_err("rangecheck", "resourceforall");
|
||||||
|
}
|
||||||
|
self.push_interval(Obj::Str(scratch.clone()), 0, name.len())?;
|
||||||
|
if !self.body(&p)? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `name` match a `resourceforall` template: `*` any run of bytes, `?` any one, `\` the
|
||||||
|
/// next one as it is? Each step counts as an operation.
|
||||||
|
fn matches(&mut self, template: &[u8], name: &[u8]) -> Res<bool> {
|
||||||
|
let (mut t, mut n) = (0, 0);
|
||||||
|
// Where the last `*` was followed (template, name): a mismatch after it retries from there.
|
||||||
|
let mut star: Option<(usize, usize)> = None;
|
||||||
|
loop {
|
||||||
|
self.tick()?;
|
||||||
|
if n == name.len() {
|
||||||
|
return Ok(template.get(t..).unwrap_or_default().iter().all(|b| *b == b'*'));
|
||||||
|
}
|
||||||
|
let (want, width) = match template.get(t) {
|
||||||
|
Some(b'*') => {
|
||||||
|
t += 1;
|
||||||
|
star = Some((t, n));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Some(b'?') => (name.get(n).copied(), 1),
|
||||||
|
Some(b'\\') => (template.get(t + 1).copied(), 2),
|
||||||
|
c => (c.copied(), 1),
|
||||||
|
};
|
||||||
|
if want.is_some() && want == name.get(n).copied() {
|
||||||
|
(t, n) = (t + width, n + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The last `*` takes one more byte, else there is no match.
|
||||||
|
let Some((st, sn)) = star else { return Ok(false) };
|
||||||
|
star = Some((st, sn + 1));
|
||||||
|
(t, n) = (st, sn + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
||||||
@@ -1057,7 +1100,7 @@ fn equal(a: &Obj, b: &Obj) -> bool {
|
|||||||
match (a, b) {
|
match (a, b) {
|
||||||
(Obj::Null, Obj::Null) | (Obj::Mark, Obj::Mark) => true,
|
(Obj::Null, Obj::Null) | (Obj::Mark, Obj::Mark) => true,
|
||||||
(Obj::Bool(x), Obj::Bool(y)) => x == y,
|
(Obj::Bool(x), Obj::Bool(y)) => x == y,
|
||||||
(Obj::Array { items: x, .. }, Obj::Array { items: y, .. }) => Rc::ptr_eq(x, y),
|
(Obj::Array { items: x, .. }, Obj::Array { items: y, .. }) => x.same(y),
|
||||||
(Obj::Dict(x), Obj::Dict(y)) => Rc::ptr_eq(x, y),
|
(Obj::Dict(x), Obj::Dict(y)) => Rc::ptr_eq(x, y),
|
||||||
(Obj::Op(x), Obj::Op(y)) => x == y,
|
(Obj::Op(x), Obj::Op(y)) => x == y,
|
||||||
_ => match (a.as_num(), b.as_num()) {
|
_ => match (a.as_num(), b.as_num()) {
|
||||||
|
|||||||
@@ -192,7 +192,10 @@ fn finish(mut out: Out) -> Imported {
|
|||||||
|
|
||||||
/// A document of `frame`'s size showing TIFF preview `tiff` stretched over it.
|
/// A document of `frame`'s size showing TIFF preview `tiff` stretched over it.
|
||||||
fn preview(tiff: &[u8], frame: Rect) -> Option<Document> {
|
fn preview(tiff: &[u8], frame: Rect) -> Option<Document> {
|
||||||
let img = image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff).ok()?.to_rgba8();
|
let img = match image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff) {
|
||||||
|
Ok(img) => img.to_rgba8(),
|
||||||
|
Err(_) => crate::tiff::palette_rgba(tiff)?,
|
||||||
|
};
|
||||||
let (w, h) = img.dimensions();
|
let (w, h) = img.dimensions();
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! The PostScript objects the interpreter works with, its operators and its errors.
|
//! The PostScript objects the interpreter works with, its operators and its errors.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Ref, RefCell};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ pub(crate) enum Obj {
|
|||||||
Name(Rc<str>),
|
Name(Rc<str>),
|
||||||
/// An executable name: looked up and run.
|
/// An executable name: looked up and run.
|
||||||
Exec(Rc<str>),
|
Exec(Rc<str>),
|
||||||
Str(Rc<RefCell<Vec<u8>>>),
|
Str(Shared<u8>),
|
||||||
/// An array; executable ones are procedures.
|
/// An array; executable ones are procedures.
|
||||||
Array {
|
Array {
|
||||||
items: Rc<RefCell<Vec<Obj>>>,
|
items: Shared<Obj>,
|
||||||
exec: bool,
|
exec: bool,
|
||||||
},
|
},
|
||||||
Dict(DictRef),
|
Dict(DictRef),
|
||||||
@@ -53,15 +53,15 @@ impl Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn string(bytes: Vec<u8>) -> Self {
|
pub fn string(bytes: Vec<u8>) -> Self {
|
||||||
Self::Str(Rc::new(RefCell::new(bytes)))
|
Self::Str(Shared::new(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn array(items: Vec<Obj>) -> Self {
|
pub fn array(items: Vec<Obj>) -> Self {
|
||||||
Self::Array { items: Rc::new(RefCell::new(items)), exec: false }
|
Self::Array { items: Shared::new(items), exec: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn proc(items: Vec<Obj>) -> Self {
|
pub fn proc(items: Vec<Obj>) -> Self {
|
||||||
Self::Array { items: Rc::new(RefCell::new(items)), exec: true }
|
Self::Array { items: Shared::new(items), exec: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dict(d: Dict) -> Self {
|
pub fn dict(d: Dict) -> Self {
|
||||||
@@ -98,7 +98,7 @@ impl Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The items of an array or procedure.
|
/// The items of an array or procedure.
|
||||||
pub fn items(&self) -> Option<&Rc<RefCell<Vec<Obj>>>> {
|
pub fn items(&self) -> Option<&Shared<Obj>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Array { items, .. } => Some(items),
|
Self::Array { items, .. } => Some(items),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -125,6 +125,71 @@ impl Obj {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The values of a string or an array: a run of storage that other strings or arrays may share
|
||||||
|
/// (`getinterval` makes a new object over part of the same values, as PostScript does).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Shared<T> {
|
||||||
|
buf: Rc<RefCell<Vec<T>>>,
|
||||||
|
at: usize,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Shared<T> {
|
||||||
|
pub fn new(v: Vec<T>) -> Self {
|
||||||
|
Self { len: v.len(), buf: Rc::new(RefCell::new(v)), at: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The values. Only [`Self::write`] borrows the storage mutably, for the copy alone, so this
|
||||||
|
/// never meets it.
|
||||||
|
pub fn borrow(&self) -> Ref<'_, [T]> {
|
||||||
|
let (at, end) = (self.at, self.at.saturating_add(self.len));
|
||||||
|
Ref::map(self.buf.borrow(), |v| v.get(at..end).unwrap_or(&[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, i: usize) -> Option<T> {
|
||||||
|
self.borrow().get(i).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> Vec<T> {
|
||||||
|
self.borrow().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` values from `at`, sharing the storage (`getinterval`); `None` past the end.
|
||||||
|
pub fn sub(&self, at: usize, n: usize) -> Option<Self> {
|
||||||
|
(at.checked_add(n)? <= self.len).then(|| Self { buf: self.buf.clone(), at: self.at + at, len: n })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `vals` from `at`: false when they don't fit (or `vals` is borrowed from the storage).
|
||||||
|
pub fn write(&self, at: usize, vals: &[T]) -> bool {
|
||||||
|
if at.saturating_add(vals.len()) > self.len {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let start = self.at + at;
|
||||||
|
let Ok(mut v) = self.buf.try_borrow_mut() else { return false };
|
||||||
|
match v.get_mut(start..start + vals.len()) {
|
||||||
|
Some(d) => {
|
||||||
|
d.clone_from_slice(vals);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same object (`eq` on arrays): the same values of the same storage.
|
||||||
|
pub fn same(&self, o: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.buf, &o.buf) && self.at == o.at && self.len == o.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do both see the same storage?
|
||||||
|
pub fn shares(&self, o: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.buf, &o.buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
pub fn name(s: &str) -> Self {
|
pub fn name(s: &str) -> Self {
|
||||||
Self::Name(Rc::from(s))
|
Self::Name(Rc::from(s))
|
||||||
@@ -209,7 +274,7 @@ ops! {
|
|||||||
ErrorDict = "errordict", DollarError = "$error", Bind = "bind",
|
ErrorDict = "errordict", DollarError = "$error", Bind = "bind",
|
||||||
// Virtual memory and the environment.
|
// Virtual memory and the environment.
|
||||||
Save = "save", Restore = "restore", SetGlobal = "setglobal", CurrentGlobal = "currentglobal", VmStatus = "vmstatus",
|
Save = "save", Restore = "restore", SetGlobal = "setglobal", CurrentGlobal = "currentglobal", VmStatus = "vmstatus",
|
||||||
LanguageLevel = "languagelevel", Version = "version", Product = "product", RealTime = "realtime", UserTime = "usertime",
|
Version = "version", Product = "product", RealTime = "realtime", UserTime = "usertime",
|
||||||
Print = "print", EqPrint = "=", EqEqPrint = "==", Pstack = "pstack", Stack = "stack", Flush = "flush",
|
Print = "print", EqPrint = "=", EqEqPrint = "==", Pstack = "pstack", Stack = "stack", Flush = "flush",
|
||||||
// Graphics state.
|
// Graphics state.
|
||||||
Gsave = "gsave", Grestore = "grestore", GrestoreAll = "grestoreall", InitGraphics = "initgraphics",
|
Gsave = "gsave", Grestore = "grestore", GrestoreAll = "grestoreall", InitGraphics = "initgraphics",
|
||||||
@@ -242,8 +307,8 @@ ops! {
|
|||||||
RLineTo = "rlineto", CurveTo = "curveto", RCurveTo = "rcurveto", Arc = "arc", Arcn = "arcn", Arct = "arct",
|
RLineTo = "rlineto", CurveTo = "curveto", RCurveTo = "rcurveto", Arc = "arc", Arcn = "arcn", Arct = "arct",
|
||||||
Arcto = "arcto", ClosePath = "closepath", FlattenPath = "flattenpath", ReversePath = "reversepath",
|
Arcto = "arcto", ClosePath = "closepath", FlattenPath = "flattenpath", ReversePath = "reversepath",
|
||||||
PathBBox = "pathbbox", ClipPath = "clippath", InitClip = "initclip", Clip = "clip", EoClip = "eoclip",
|
PathBBox = "pathbbox", ClipPath = "clippath", InitClip = "initclip", Clip = "clip", EoClip = "eoclip",
|
||||||
RectClip = "rectclip", Fill = "fill", EoFill = "eofill", Stroke = "stroke", RectFill = "rectfill",
|
RectClip = "rectclip", ClipSave = "clipsave", ClipRestore = "cliprestore", Fill = "fill", EoFill = "eofill",
|
||||||
RectStroke = "rectstroke", ShFill = "shfill",
|
Stroke = "stroke", RectFill = "rectfill", RectStroke = "rectstroke", ShFill = "shfill",
|
||||||
// Images.
|
// Images.
|
||||||
Image = "image", ImageMask = "imagemask", ColorImage = "colorimage",
|
Image = "image", ImageMask = "imagemask", ColorImage = "colorimage",
|
||||||
// Files.
|
// Files.
|
||||||
@@ -252,7 +317,8 @@ ops! {
|
|||||||
// Fonts and type.
|
// Fonts and type.
|
||||||
FindFont = "findfont", ScaleFont = "scalefont", MakeFont = "makefont", SetFont = "setfont", SelectFont = "selectfont",
|
FindFont = "findfont", ScaleFont = "scalefont", MakeFont = "makefont", SetFont = "setfont", SelectFont = "selectfont",
|
||||||
CurrentFont = "currentfont", DefineFont = "definefont", UndefineFont = "undefinefont", FindResource = "findresource",
|
CurrentFont = "currentfont", DefineFont = "definefont", UndefineFont = "undefinefont", FindResource = "findresource",
|
||||||
DefineResource = "defineresource", ResourceStatus = "resourcestatus", Show = "show", AShow = "ashow",
|
DefineResource = "defineresource", UndefineResource = "undefineresource", ResourceStatus = "resourcestatus",
|
||||||
|
ResourceForAll = "resourceforall", Show = "show", AShow = "ashow",
|
||||||
WidthShow = "widthshow", AWidthShow = "awidthshow", XShow = "xshow", YShow = "yshow", XYShow = "xyshow",
|
WidthShow = "widthshow", AWidthShow = "awidthshow", XShow = "xshow", YShow = "yshow", XYShow = "xyshow",
|
||||||
KShow = "kshow", GlyphShow = "glyphshow", StringWidth = "stringwidth", CharPath = "charpath",
|
KShow = "kshow", GlyphShow = "glyphshow", StringWidth = "stringwidth", CharPath = "charpath",
|
||||||
SetCacheDevice = "setcachedevice", SetCharWidth = "setcharwidth",
|
SetCacheDevice = "setcachedevice", SetCharWidth = "setcharwidth",
|
||||||
|
|||||||
@@ -452,3 +452,234 @@ fn device_settings_are_accepted_and_change_nothing() {
|
|||||||
assert_eq!(objects(&r.document).len(), 1, "{:?}", r.warnings);
|
assert_eq!(objects(&r.document).len(), 1, "{:?}", r.warnings);
|
||||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that `body` draws one square, blue: it sets blue when what it checks holds, else red.
|
||||||
|
fn blue(body: &str) {
|
||||||
|
let r = read(&format!("{body} 10 10 80 80 rectfill"));
|
||||||
|
let o = objects(&r.document);
|
||||||
|
assert_eq!(o.len(), 1, "{body}: {:?}", r.warnings);
|
||||||
|
assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::rgb(0.0, 0.0, 1.0))), "{body}");
|
||||||
|
assert!(r.warnings.is_empty(), "{body}: {:?}", r.warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that PostScript `cond` leaves `true`.
|
||||||
|
fn check(cond: &str) {
|
||||||
|
blue(&format!("{cond} {{ 0 0 1 setrgbcolor }} {{ 1 0 0 setrgbcolor }} ifelse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_generic_category_defines_new_categories() {
|
||||||
|
// #234: the reference app's prolog.
|
||||||
|
blue("/Generic /Category findresource pop 0 0 1 setrgbcolor");
|
||||||
|
blue(
|
||||||
|
"[/CSA /Gradient /Procedure] { /Generic /Category findresource dup length dict copy /Category defineresource pop } forall 0 0 1 setrgbcolor",
|
||||||
|
);
|
||||||
|
let define = "[/CSA /Gradient] { /Generic /Category findresource dup length dict copy /Category defineresource pop } forall \
|
||||||
|
/G1 << /Kind 7 >> /Gradient defineresource pop";
|
||||||
|
check(&format!("{define} /G1 /Gradient findresource /Kind get 7 eq"));
|
||||||
|
check(&format!("{define} /Gradient /Category resourcestatus {{ pop pop true }} {{ false }} ifelse"));
|
||||||
|
check(&format!("{define} /G1 /Gradient resourcestatus {{ pop pop true }} {{ false }} ifelse"));
|
||||||
|
check(&format!("{define} /G1 /Gradient undefineresource /G1 /Gradient resourcestatus not"));
|
||||||
|
check(&format!("{define} {{ /G2 /Gradient findresource }} stopped"));
|
||||||
|
// Unknown categories aren't instances of Category; a category is a dictionary.
|
||||||
|
check("/Nonsense /Category resourcestatus not");
|
||||||
|
check("{ /Nonsense /Category findresource } stopped");
|
||||||
|
check("{ /X 5 /Category defineresource } stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clipsave_and_cliprestore_bring_the_clip_back() {
|
||||||
|
// #234.
|
||||||
|
blue("clipsave 0 0 50 50 rectclip cliprestore 0 0 1 setrgbcolor");
|
||||||
|
// The fill after cliprestore covers the whole square, unclipped.
|
||||||
|
let r = read("clipsave 0 0 50 50 rectclip 1 0 0 setrgbcolor 0 0 10 10 rectfill cliprestore 0 0 1 setrgbcolor 10 10 80 80 rectfill");
|
||||||
|
let o = objects(&r.document);
|
||||||
|
assert_eq!(o.len(), 2, "{:?}", r.warnings);
|
||||||
|
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }), "{:?}", o[0].kind);
|
||||||
|
assert!(matches!(o[1].kind, NodeKind::Path { clipping: false, .. }), "{:?}", o[1].kind);
|
||||||
|
assert!(near(bounds(&o[1]), Rect::new(10.0, 10.0, 90.0, 90.0)), "{:?}", bounds(&o[1]));
|
||||||
|
// Nested, leaving the rest of the graphics state alone.
|
||||||
|
let r = read(
|
||||||
|
"0 0 60 60 rectclip clipsave 0 0 20 20 rectclip clipsave 0 0 5 5 rectclip cliprestore 0 1 0 setrgbcolor cliprestore \
|
||||||
|
10 10 80 80 rectfill",
|
||||||
|
);
|
||||||
|
let NodeKind::Group { children, clip: true } = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
|
||||||
|
assert!(near(bounds(&children[0]), Rect::new(0.0, 40.0, 60.0, 100.0)), "{:?}", bounds(&children[0]));
|
||||||
|
assert_eq!(fill(&children[1]), Some(&Paint::solid(Color::rgb(0.0, 1.0, 0.0))));
|
||||||
|
// grestore undoes the clipsaves after its gsave; cliprestore with nothing saved does nothing.
|
||||||
|
let o = objects(&read("cliprestore gsave clipsave 0 0 50 50 rectclip grestore cliprestore 10 10 80 80 rectfill").document);
|
||||||
|
assert!(matches!(o[0].kind, NodeKind::Path { .. }), "{:?}", o[0].kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn languagelevel_is_an_integer_and_runs() {
|
||||||
|
// #234.
|
||||||
|
blue(
|
||||||
|
"/Level2? systemdict /languagelevel known dup { pop systemdict /languagelevel get 2 ge } if def \
|
||||||
|
Level2? { 0 0 1 setrgbcolor } { 1 0 0 setrgbcolor } ifelse",
|
||||||
|
);
|
||||||
|
check("languagelevel 3 eq");
|
||||||
|
check("{ languagelevel } bind exec 3 eq");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intervals_share_their_arrays_and_strings() {
|
||||||
|
// #234: astore into a subarray stores into the array.
|
||||||
|
blue("/a 4 array def 1 2 a 0 2 getinterval astore pop a 0 get 1 eq { 0 0 1 setrgbcolor } { 1 0 0 setrgbcolor } ifelse");
|
||||||
|
// put, putinterval and copy into an interval; an interval of an interval.
|
||||||
|
check("/a [0 0 0 0 0] def a 1 3 getinterval 1 2 getinterval 0 7 put a 2 get 7 eq");
|
||||||
|
check("/a [0 0 0 0] def a 2 2 getinterval 0 [8 9] putinterval a 3 get 9 eq");
|
||||||
|
check("/a [0 0 0 0] def [5 6] a 1 2 getinterval copy pop a 2 get 6 eq");
|
||||||
|
check("/s (abcd) def s 1 2 getinterval 0 (XY) putinterval s (aXYd) eq");
|
||||||
|
check("/s (abcd) def s 2 1 getinterval 0 90 put s (abZd) eq");
|
||||||
|
check("/s 8 string def 42 s 2 6 getinterval cvs pop s 2 2 getinterval (42) eq");
|
||||||
|
// The same interval is the same object; another one is not.
|
||||||
|
check("/a 4 array def a 0 2 getinterval a 0 2 getinterval eq");
|
||||||
|
check("/a 4 array def a 0 2 getinterval a 0 3 getinterval ne a 0 2 getinterval a 1 2 getinterval ne and");
|
||||||
|
// Reads and writes stay inside the interval.
|
||||||
|
check("/a [1 2 3 4] def { a 1 2 getinterval 2 get } stopped { a 1 2 getinterval 2 0 put } stopped and");
|
||||||
|
check("/a [1 2 3 4] def a 1 2 getinterval length 2 eq a 1 2 getinterval aload pop 3 eq exch 2 eq and and");
|
||||||
|
check("/n 0 def [1 2 3 4] 1 2 getinterval { n add /n exch def } forall n 5 eq");
|
||||||
|
// A procedure from an interval runs only its part.
|
||||||
|
check("{ 1 2 3 } 1 2 getinterval cvx exec add 5 eq");
|
||||||
|
// The prolog's stack save: the operands into part of one array, and back.
|
||||||
|
check(
|
||||||
|
"/stk 10 array def /lev 3 array def 1 2 3 count dup lev exch 0 exch put stk exch 0 exch getinterval astore pop \
|
||||||
|
clear stk 0 lev 0 get getinterval aload pop add add 6 eq",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resourceforall_enumerates_matching_names() {
|
||||||
|
// #234.
|
||||||
|
blue("(*) { pop } 128 string /Category resourceforall 0 0 1 setrgbcolor");
|
||||||
|
let count = |template: &str| format!("/n 0 def ({template}) {{ pop /n n 1 add def }} 128 string /ProcSet resourceforall n");
|
||||||
|
let define = "[/Foo /Fop /Bar /F*x] { 1 dict /ProcSet defineresource pop } forall";
|
||||||
|
check(&format!("{define} {} 4 eq", count("*")));
|
||||||
|
check(&format!("{define} {} 2 eq", count("Fo?")));
|
||||||
|
check(&format!("{define} {} 3 eq", count("F*")));
|
||||||
|
check(&format!("{define} {} 1 eq", count("F\\\\*x")));
|
||||||
|
check(&format!("{define} {} 0 eq", count("Q*")));
|
||||||
|
check(&format!("{define} {} 1 eq", count("*a*")));
|
||||||
|
// Each name is copied into the scratch string; exit stops.
|
||||||
|
check(&format!("{define} /last () def (B*) {{ dup length string copy /last exch def }} 8 string /ProcSet resourceforall last (Bar) eq"));
|
||||||
|
check(&format!("{define} /n 0 def (*) {{ pop /n n 1 add def exit }} 8 string /ProcSet resourceforall n 1 eq"));
|
||||||
|
// A scratch string too small for the names, as the prolog probes it.
|
||||||
|
check(&format!("{define} {{ (*) {{ pop }} 2 string /ProcSet resourceforall }} stopped"));
|
||||||
|
check("/n 0 def (Gen*) { pop /n n 1 add def } 16 string /Category resourceforall n 1 eq");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`.
|
||||||
|
fn palette_tiff(w: u16, h: u16, per: u16, big: bool) -> Vec<u8> {
|
||||||
|
let u16b = |v: u16| if big { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||||
|
let u32b = |v: u32| if big { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||||
|
// Shorts in an entry, left-justified.
|
||||||
|
let shorts = |v: &[u16]| -> [u8; 4] {
|
||||||
|
let mut f = [0; 4];
|
||||||
|
for (i, s) in v.iter().enumerate() {
|
||||||
|
f[2 * i..2 * i + 2].copy_from_slice(&u16b(*s));
|
||||||
|
}
|
||||||
|
f
|
||||||
|
};
|
||||||
|
let row = u32::from(w) * 2;
|
||||||
|
let mut out = vec![];
|
||||||
|
out.extend(if big { *b"MM\0*" } else { *b"II*\0" });
|
||||||
|
out.extend([0; 4]);
|
||||||
|
let data_at = out.len() as u32;
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
out.extend([((x + y) % 3) as u8, (x * 12) as u8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let map_at = out.len() as u32;
|
||||||
|
for (c, full) in [0xFFFF, 0x8000, 0xFFFF].into_iter().enumerate() {
|
||||||
|
for i in 0..256 {
|
||||||
|
out.extend(u16b(if i == c { full } else { 0 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let strips: Vec<u16> = (0..h.div_ceil(per)).collect();
|
||||||
|
let offsets_at = out.len() as u32;
|
||||||
|
for k in &strips {
|
||||||
|
out.extend(u32b(data_at + u32::from(k * per) * row));
|
||||||
|
}
|
||||||
|
let counts_at = out.len() as u32;
|
||||||
|
for k in &strips {
|
||||||
|
out.extend(u32b(u32::from(per.min(h - k * per)) * row));
|
||||||
|
}
|
||||||
|
let ifd = out.len() as u32;
|
||||||
|
out[4..8].copy_from_slice(&u32b(ifd));
|
||||||
|
let n = strips.len() as u32;
|
||||||
|
let entries: [(u16, u16, u32, [u8; 4]); 12] = [
|
||||||
|
(256, 3, 1, shorts(&[w])),
|
||||||
|
(257, 3, 1, shorts(&[h])),
|
||||||
|
(258, 3, 2, shorts(&[8, 8])),
|
||||||
|
(259, 3, 1, shorts(&[1])),
|
||||||
|
(262, 3, 1, shorts(&[3])),
|
||||||
|
(273, 4, n, u32b(if n == 1 { data_at } else { offsets_at })),
|
||||||
|
(277, 3, 1, shorts(&[2])),
|
||||||
|
(278, 3, 1, shorts(&[per])),
|
||||||
|
(279, 4, n, u32b(if n == 1 { u32::from(h) * row } else { counts_at })),
|
||||||
|
(284, 3, 1, shorts(&[1])),
|
||||||
|
(320, 3, 768, u32b(map_at)),
|
||||||
|
(338, 3, 1, shorts(&[1])),
|
||||||
|
];
|
||||||
|
out.extend(u16b(entries.len() as u16));
|
||||||
|
for (tag, kind, count, field) in entries {
|
||||||
|
out.extend(u16b(tag));
|
||||||
|
out.extend(u16b(kind));
|
||||||
|
out.extend(u32b(count));
|
||||||
|
out.extend(field);
|
||||||
|
}
|
||||||
|
out.extend([0; 4]);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_palette_preview_with_alpha_is_placed() {
|
||||||
|
// #234: the program fails, so the preview is placed.
|
||||||
|
let ps = eps("0 0 1 setrgbcolor 10 10 80 80 rectfill frobnicate");
|
||||||
|
for (big, per) in [(false, 5), (true, 5), (false, 10), (true, 3)] {
|
||||||
|
let tiff = palette_tiff(20, 10, per, big);
|
||||||
|
let r = import(&crate::dos_eps(&ps, &tiff).unwrap()).unwrap();
|
||||||
|
assert!(r.preview, "{big} {per}");
|
||||||
|
assert!(r.warnings[0].contains("preview"), "{:?}", r.warnings);
|
||||||
|
let o = objects(&r.document);
|
||||||
|
let NodeKind::Image(im) = &o[0].kind else { panic!("{:?}", o[0].kind) };
|
||||||
|
assert_eq!((im.width, im.height), (20, 10));
|
||||||
|
assert!(near(bounds(&o[0]), Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||||||
|
let png = image::load_from_memory(&r.document.images[&im.key].bytes).unwrap().to_rgba8();
|
||||||
|
for (x, y) in [(0, 0), (1, 0), (2, 0), (19, 9), (7, 4), (3, 6)] {
|
||||||
|
let [r, g, b] = [[255, 0, 0], [0, 128, 0], [0, 0, 255]][((x + y) % 3) as usize];
|
||||||
|
assert_eq!(png.get_pixel(x, y).0, [r, g, b, (x * 12) as u8], "({x}, {y}) {big} {per}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_damaged_palette_preview_is_refused_without_panicking() {
|
||||||
|
let tiff = palette_tiff(20, 10, 5, false);
|
||||||
|
assert!(crate::tiff::palette_rgba(&tiff).is_some());
|
||||||
|
// Cut anywhere before the last entry's value (the directory is at the end): nothing.
|
||||||
|
for cut in 0..tiff.len() - 6 {
|
||||||
|
assert!(crate::tiff::palette_rgba(&tiff[..cut]).is_none(), "{cut}");
|
||||||
|
}
|
||||||
|
// Any byte changed: a picture or nothing.
|
||||||
|
for big in [false, true] {
|
||||||
|
let tiff = palette_tiff(20, 10, 5, big);
|
||||||
|
for at in 0..tiff.len() {
|
||||||
|
for v in [0, 1, 2, 0x7f, 0x80, 0xff] {
|
||||||
|
let mut t = tiff.clone();
|
||||||
|
t[at] = v;
|
||||||
|
let _ = crate::tiff::palette_rgba(&t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A damaged preview of an unreadable program: the hard error.
|
||||||
|
let ps = eps("frobnicate");
|
||||||
|
for t in [&tiff[..tiff.len() / 2], b"II*\0garbage", &[0; 64]] {
|
||||||
|
let e = import(&crate::dos_eps(&ps, t).unwrap()).unwrap_err();
|
||||||
|
assert!(e.contains("can't be read"), "{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,38 +110,46 @@ impl Interp<'_> {
|
|||||||
let category = self.pop()?;
|
let category = self.pop()?;
|
||||||
let v = self.pop()?;
|
let v = self.pop()?;
|
||||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "defineresource".into()))?;
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "defineresource".into()))?;
|
||||||
let dict = if category.text().as_deref() == Some("Font") { self.fonts.clone() } else { self.category(&category)? };
|
// A category is defined by its implementation dictionary.
|
||||||
dict.borrow_mut().insert(k, v.clone());
|
if category.text().as_deref() == Some("Category") && !matches!(v, Obj::Dict(_)) {
|
||||||
|
return ps_err("typecheck", "defineresource");
|
||||||
|
}
|
||||||
|
self.instances(&category)?.borrow_mut().insert(k, v.clone());
|
||||||
self.push(v)?;
|
self.push(v)?;
|
||||||
}
|
}
|
||||||
|
UndefineResource => {
|
||||||
|
let category = self.pop()?;
|
||||||
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "undefineresource".into()))?;
|
||||||
|
self.instances(&category)?.borrow_mut().remove(&k);
|
||||||
|
}
|
||||||
ResourceStatus => {
|
ResourceStatus => {
|
||||||
let category = self.pop()?;
|
let category = self.pop()?;
|
||||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "resourcestatus".into()))?;
|
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", "resourcestatus".into()))?;
|
||||||
let dict = if category.text().as_deref() == Some("Font") { self.fonts.clone() } else { self.category(&category)? };
|
let known = self.instances(&category)?.borrow().contains_key(&k);
|
||||||
let known = dict.borrow().contains_key(&k);
|
|
||||||
if known {
|
if known {
|
||||||
self.push(Obj::Int(1))?;
|
self.push(Obj::Int(1))?;
|
||||||
self.push(Obj::Int(0))?;
|
self.push(Obj::Int(0))?;
|
||||||
}
|
}
|
||||||
self.push(Obj::Bool(known))?;
|
self.push(Obj::Bool(known))?;
|
||||||
}
|
}
|
||||||
|
ResourceForAll => self.resource_for_all()?,
|
||||||
Show => {
|
Show => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.show(&s)?;
|
self.show(&s)?;
|
||||||
}
|
}
|
||||||
AShow => {
|
AShow => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.nums::<2>()?;
|
self.nums::<2>()?;
|
||||||
self.show(&s)?;
|
self.show(&s)?;
|
||||||
}
|
}
|
||||||
WidthShow => {
|
WidthShow => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.pop()?;
|
self.pop()?;
|
||||||
self.nums::<2>()?;
|
self.nums::<2>()?;
|
||||||
self.show(&s)?;
|
self.show(&s)?;
|
||||||
}
|
}
|
||||||
AWidthShow => {
|
AWidthShow => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.nums::<2>()?;
|
self.nums::<2>()?;
|
||||||
self.pop()?;
|
self.pop()?;
|
||||||
self.nums::<2>()?;
|
self.nums::<2>()?;
|
||||||
@@ -149,11 +157,11 @@ impl Interp<'_> {
|
|||||||
}
|
}
|
||||||
XShow | YShow | XYShow => {
|
XShow | YShow | XYShow => {
|
||||||
self.pop()?;
|
self.pop()?;
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.show(&s)?;
|
self.show(&s)?;
|
||||||
}
|
}
|
||||||
KShow => {
|
KShow => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.pop()?;
|
self.pop()?;
|
||||||
self.show(&s)?;
|
self.show(&s)?;
|
||||||
}
|
}
|
||||||
@@ -163,14 +171,14 @@ impl Interp<'_> {
|
|||||||
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
|
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
|
||||||
}
|
}
|
||||||
StringWidth => {
|
StringWidth => {
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
let (_, _, v) = self.set_type(&s, Point::ZERO)?;
|
let (_, _, v) = self.set_type(&s, Point::ZERO)?;
|
||||||
self.push_num(v.x)?;
|
self.push_num(v.x)?;
|
||||||
self.push_num(v.y)?;
|
self.push_num(v.y)?;
|
||||||
}
|
}
|
||||||
CharPath => {
|
CharPath => {
|
||||||
self.pop_bool()?;
|
self.pop_bool()?;
|
||||||
let s = self.pop_str()?.borrow().clone();
|
let s = self.pop_str()?.to_vec();
|
||||||
self.char_path(&s)?;
|
self.char_path(&s)?;
|
||||||
}
|
}
|
||||||
SetCacheDevice => {
|
SetCacheDevice => {
|
||||||
|
|||||||
+110
-1
@@ -1,5 +1,6 @@
|
|||||||
//! The TIFF preview (little-endian, PackBits strips, written by the raster exports' TIFF writer):
|
//! The TIFF preview (little-endian, PackBits strips, written by the raster exports' TIFF writer):
|
||||||
//! 1-bit black and white, or 8-bit RGB with or without an alpha channel. Also the PNG thumbnail.
|
//! 1-bit black and white, or 8-bit RGB with or without an alpha channel. Also the PNG thumbnail,
|
||||||
|
//! and a reader for the palette previews other apps write that the `image` crate can't decode.
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
@@ -50,3 +51,111 @@ pub(crate) fn png(img: &Raster) -> Result<Vec<u8>, String> {
|
|||||||
rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).map_err(|e| e.to_string())?;
|
rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).map_err(|e| e.to_string())?;
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Most pixels a palette preview may have.
|
||||||
|
const MAX_PIXELS: usize = 1 << 26;
|
||||||
|
/// Most values a TIFF field may have.
|
||||||
|
const MAX_VALUES: usize = 1 << 20;
|
||||||
|
|
||||||
|
/// A TIFF's bytes in its byte order.
|
||||||
|
struct Tiff<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
big: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tiff<'_> {
|
||||||
|
fn u16(&self, at: usize) -> Option<u16> {
|
||||||
|
let b: [u8; 2] = self.b.get(at..at.checked_add(2)?)?.try_into().ok()?;
|
||||||
|
Some(if self.big { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32(&self, at: usize) -> Option<u32> {
|
||||||
|
let b: [u8; 4] = self.b.get(at..at.checked_add(4)?)?.try_into().ok()?;
|
||||||
|
Some(if self.big { u32::from_be_bytes(b) } else { u32::from_le_bytes(b) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The values of the directory entry at `at` (bytes, shorts or longs; in the entry when they
|
||||||
|
/// fit in four bytes).
|
||||||
|
fn values(&self, at: usize) -> Option<Vec<u32>> {
|
||||||
|
let (kind, n) = (self.u16(at.checked_add(2)?)?, self.u32(at.checked_add(4)?)? as usize);
|
||||||
|
let size = match kind {
|
||||||
|
1 => 1,
|
||||||
|
3 => 2,
|
||||||
|
4 => 4,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if n > MAX_VALUES {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let field = at.checked_add(8)?;
|
||||||
|
let start = if n * size <= 4 { field } else { self.u32(field)? as usize };
|
||||||
|
(0..n)
|
||||||
|
.map(|i| {
|
||||||
|
let at = start.checked_add(i * size)?;
|
||||||
|
match size {
|
||||||
|
1 => self.b.get(at).map(|v| u32::from(*v)),
|
||||||
|
2 => self.u16(at).map(u32::from),
|
||||||
|
_ => self.u32(at),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An 8-bit palette TIFF, with or without an alpha sample (`ExtraSamples`), uncompressed and
|
||||||
|
/// interleaved, as RGBA: the layout of other apps' EPS previews. `None` for anything else.
|
||||||
|
pub(crate) fn palette_rgba(bytes: &[u8]) -> Option<image::RgbaImage> {
|
||||||
|
let big = match bytes.get(..4)? {
|
||||||
|
b"II*\0" => false,
|
||||||
|
b"MM\0*" => true,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let t = Tiff { b: bytes, big };
|
||||||
|
let ifd = t.u32(4)? as usize;
|
||||||
|
let entries = usize::from(t.u16(ifd)?);
|
||||||
|
let mut fields = std::collections::HashMap::new();
|
||||||
|
for i in 0..entries {
|
||||||
|
let at = ifd.checked_add(2 + i * 12)?;
|
||||||
|
fields.insert(t.u16(at)?, at);
|
||||||
|
}
|
||||||
|
let get = |tag: u16| fields.get(&tag).and_then(|at| t.values(*at));
|
||||||
|
let one = |tag: u16| get(tag).and_then(|v| v.first().copied());
|
||||||
|
let (w, h) = (one(256)? as usize, one(257)? as usize);
|
||||||
|
let samples = one(277).unwrap_or(1) as usize;
|
||||||
|
let alpha = samples == 2 && matches!(get(338)?.as_slice(), [0..=2]);
|
||||||
|
let plain = one(259).unwrap_or(1) == 1 && one(262)? == 3 && one(284).unwrap_or(1) == 1;
|
||||||
|
if !plain || !(samples == 1 || alpha) || get(258)?.iter().any(|b| *b != 8) || w == 0 || h == 0 || w.checked_mul(h)? > MAX_PIXELS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let map = get(320)?;
|
||||||
|
if map.len() != 3 * 256 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Strips of `per` rows each, in order.
|
||||||
|
let (offsets, counts) = (get(273)?, get(279)?);
|
||||||
|
let per = (one(278).unwrap_or(u32::MAX) as usize).min(h);
|
||||||
|
let row = w * samples;
|
||||||
|
// The strips are in the file: so is all that's allocated.
|
||||||
|
if per == 0 || offsets.len() != counts.len() || offsets.len() < h.div_ceil(per) || row * h > bytes.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut data = Vec::with_capacity(row * h);
|
||||||
|
for (k, (at, n)) in offsets.iter().zip(&counts).take(h.div_ceil(per)).enumerate() {
|
||||||
|
let want = per.min(h - k * per) * row;
|
||||||
|
if (*n as usize) < want {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let at = *at as usize;
|
||||||
|
data.extend_from_slice(bytes.get(at..at.checked_add(want)?)?);
|
||||||
|
}
|
||||||
|
// Map entries are 16-bit.
|
||||||
|
let channel = |c: usize, i: usize| map.get(c * 256 + i).map_or(0, |v| ((u64::from(*v).min(65_535) * 255 + 32_767) / 65_535) as u8);
|
||||||
|
let rgba = data
|
||||||
|
.chunks_exact(samples)
|
||||||
|
.flat_map(|px| {
|
||||||
|
let i = usize::from(px.first().copied().unwrap_or(0));
|
||||||
|
[channel(0, i), channel(1, i), channel(2, i), px.get(1).copied().unwrap_or(255)]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
image::RgbaImage::from_raw(w as u32, h as u32, rgba)
|
||||||
|
}
|
||||||
+10
-8
@@ -1679,14 +1679,16 @@ PostScript is read instead and the first warning says why.
|
|||||||
|
|
||||||
Other files are run through a small PostScript interpreter (Level 3, first page only): paths, fills and strokes with
|
Other files are run through a small PostScript interpreter (Level 3, first page only): paths, fills and strokes with
|
||||||
their width, caps, joins, miter limit and dashes (a fill and a stroke of the same path become one object), grey, RGB,
|
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),
|
CMYK, indexed and spot colours (a Separation ink becomes a spot swatch, painted at its tint), clips (clipping groups;
|
||||||
`gsave`/`grestore`, `save`/`restore`, transforms, procedures with `bind def`, loops and dictionaries, axial and radial
|
`clipsave`/`cliprestore`), `gsave`/`grestore`, `save`/`restore`, transforms, procedures with `bind def`, loops,
|
||||||
shadings and shading patterns (gradients), images and image masks (data in the file through ASCII85, hex, run-length,
|
dictionaries, arrays and strings whose intervals share storage, resources (categories made from `Generic`,
|
||||||
Flate, LZW or DCT filters, or from procedures), and type as point type in the font the file names (embedded font
|
`resourceforall`), axial and radial shadings and shading patterns (gradients), images and image masks (data in the
|
||||||
programs are skipped). The artboard is the `%%HiResBoundingBox` (else `%%BoundingBox`; a letter page without one).
|
file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as point type in the
|
||||||
A program the interpreter can't run (an operator it doesn't know, an error, a runaway loop) or that draws nothing comes
|
font the file names (embedded font programs are skipped). The artboard is the `%%HiResBoundingBox` (else
|
||||||
in as its TIFF preview with a warning; without a preview, the art drawn up to the error is kept with a warning, and a
|
`%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know, an
|
||||||
file with none is refused with a message saying why.
|
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
|
```json
|
||||||
{"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/logo.eps"}}}
|
{"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/logo.eps"}}}
|
||||||
|
|||||||
Reference in new issue
Block a user