mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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.
|
||||
|
||||
_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
|
||||
|
||||
@@ -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:
|
||||
- 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.
|
||||
- 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.
|
||||
- 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).
|
||||
@@ -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.
|
||||
- **Geometry and effects:**
|
||||
- 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.
|
||||
- **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.
|
||||
@@ -133,7 +133,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Files (M4):**
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export.
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text, 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.
|
||||
- **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`).
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -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",
|
||||
"[", "]", "<<", ">>", "{", "}", "matrix", "currentmatrix", "setmatrix", "transform", "itransform", "invertmatrix", "pathbbox",
|
||||
"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",
|
||||
"/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>]",
|
||||
|
||||
@@ -12,7 +12,7 @@ use vectorcraft_geom::Affine;
|
||||
use super::graphics::{Space, process};
|
||||
use super::interp::{Interp, matrix_of};
|
||||
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;
|
||||
|
||||
/// Most bytes one filter or image reads.
|
||||
@@ -223,20 +223,15 @@ impl Interp<'_> {
|
||||
ReadHexString | ReadString => {
|
||||
let s = self.pop_str()?;
|
||||
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 full = got.len() == n;
|
||||
self.alloc(got.len())?;
|
||||
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))?;
|
||||
self.fill(s, &got)?;
|
||||
self.push(Obj::Bool(got.len() == n))?;
|
||||
}
|
||||
ReadLine => {
|
||||
let s = self.pop_str()?;
|
||||
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 ended = false;
|
||||
while line.len() < n {
|
||||
@@ -252,8 +247,7 @@ impl Interp<'_> {
|
||||
Some(b) => line.push(*b),
|
||||
}
|
||||
}
|
||||
self.alloc(line.len())?;
|
||||
self.push(Obj::string(line))?;
|
||||
self.fill(s, &line)?;
|
||||
self.push(Obj::Bool(ended))?;
|
||||
}
|
||||
FlushFile | CloseFile => {
|
||||
@@ -307,12 +301,20 @@ impl Interp<'_> {
|
||||
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
|
||||
/// first read (the data follows the operator that reads it); over other data, at once.
|
||||
fn filter(&mut self) -> Res {
|
||||
let name = self.pop()?.text().ok_or(PsError::Ps("typecheck", "filter".into()))?;
|
||||
let sub = if &*name == "SubFileDecode" {
|
||||
let eod = self.pop_str()?.borrow().clone();
|
||||
let eod = self.pop_str()?.to_vec();
|
||||
let count = self.pop_count()?;
|
||||
(count, eod)
|
||||
} else {
|
||||
@@ -687,8 +689,8 @@ struct Pixels {
|
||||
fn source(o: &Obj) -> Res<Source> {
|
||||
Ok(match o {
|
||||
Obj::File(f) => Source::File(f.clone()),
|
||||
Obj::Str(s) => Source::Str(s.borrow().clone()),
|
||||
p @ Obj::Array { .. } => Source::Proc(Obj::Array { items: p.items().cloned().unwrap_or_default(), exec: true }),
|
||||
Obj::Str(s) => Source::Str(s.to_vec()),
|
||||
Obj::Array { items, .. } => Source::Proc(Obj::Array { items: items.clone(), exec: true }),
|
||||
_ => return ps_err("typecheck", "DataSource"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ pub(crate) struct GState {
|
||||
pub cur: Option<Point>,
|
||||
pub start: Option<Point>,
|
||||
pub clips: Vec<Arc<Clip>>,
|
||||
/// The clips `clipsave` saved, for `cliprestore`.
|
||||
pub saved_clips: Vec<Rc<[Arc<Clip>]>>,
|
||||
pub overprint: bool,
|
||||
pub font: Option<DictRef>,
|
||||
}
|
||||
@@ -128,6 +130,7 @@ impl Default for GState {
|
||||
cur: None,
|
||||
start: None,
|
||||
clips: vec![],
|
||||
saved_clips: vec![],
|
||||
overprint: false,
|
||||
font: None,
|
||||
}
|
||||
@@ -489,7 +492,7 @@ impl Interp<'_> {
|
||||
let (family, items) = match o {
|
||||
Obj::Name(n) | Obj::Exec(n) => (n.clone(), vec![]),
|
||||
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()))?;
|
||||
(n, v)
|
||||
}
|
||||
@@ -702,7 +705,7 @@ impl Interp<'_> {
|
||||
let d = match f {
|
||||
Obj::Dict(d) => d.clone(),
|
||||
Obj::Array { items, .. } => {
|
||||
let fs = items.borrow().clone();
|
||||
let fs = items.to_vec();
|
||||
let mut out = vec![];
|
||||
for g in &fs {
|
||||
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())
|
||||
}
|
||||
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 encode = nums(get("Encode"));
|
||||
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;
|
||||
}
|
||||
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 => {
|
||||
let bp = self.g.path.clone();
|
||||
self.clip_with(bp, if op == EoClip { FillRule::EvenOdd } else { FillRule::NonZero });
|
||||
@@ -1118,7 +1134,7 @@ impl Interp<'_> {
|
||||
}
|
||||
RectStroke => {
|
||||
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,
|
||||
};
|
||||
let mut bp = self.rects()?;
|
||||
@@ -1157,7 +1173,7 @@ fn linear_points(f: &Obj) -> Option<Vec<f64>> {
|
||||
match num("FunctionType")? {
|
||||
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
|
||||
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())) {
|
||||
return None;
|
||||
}
|
||||
|
||||
+139
-96
@@ -11,7 +11,7 @@ use vectorcraft_geom::Affine;
|
||||
|
||||
use super::graphics::{GState, Out};
|
||||
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.
|
||||
const MAX_OPS: u64 = 50_000_000;
|
||||
@@ -61,6 +61,12 @@ fn encoding() -> Obj {
|
||||
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> {
|
||||
pub fn new(src: &'a [u8], g: GState, out: Out) -> Self {
|
||||
let system = new_dict();
|
||||
@@ -72,7 +78,15 @@ impl<'a> Interp<'a> {
|
||||
for name in ["StandardEncoding", "ISOLatin1Encoding"] {
|
||||
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();
|
||||
system.borrow_mut().insert(Key::name("FontDirectory"), 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(),
|
||||
error: new_dict(),
|
||||
fonts,
|
||||
resources: new_dict(),
|
||||
resources,
|
||||
depth: 0,
|
||||
ops: 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()? {
|
||||
Obj::Str(s) => Ok(s),
|
||||
_ => ps_err("typecheck", "a string"),
|
||||
@@ -183,7 +197,7 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
|
||||
/// 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()? {
|
||||
Obj::Array { items, .. } => Ok(items),
|
||||
_ => ps_err("typecheck", "an array"),
|
||||
@@ -197,15 +211,9 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
{
|
||||
let mut v = items.borrow_mut();
|
||||
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);
|
||||
}
|
||||
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)) {
|
||||
return ps_err("rangecheck", "a matrix");
|
||||
}
|
||||
self.push(Obj::Array { items, exec: false })
|
||||
}
|
||||
@@ -276,24 +284,24 @@ impl<'a> Interp<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_proc(&mut self, items: &Rc<RefCell<Vec<Obj>>>) -> Res {
|
||||
fn run_proc(&mut self, items: &Shared<Obj>) -> Res {
|
||||
self.enter()?;
|
||||
let r = self.run_items(items);
|
||||
self.depth -= 1;
|
||||
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;
|
||||
loop {
|
||||
let Some(o) = items.borrow().get(i).cloned() else { return Ok(()) };
|
||||
let Some(o) = items.get(i) else { return Ok(()) };
|
||||
i += 1;
|
||||
self.exec_token(o)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
match self.call(proc.clone()) {
|
||||
Ok(()) => Ok(true),
|
||||
@@ -303,7 +311,7 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
|
||||
/// 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()? {
|
||||
Obj::Array { items, .. } => Ok(Obj::Array { items, exec: true }),
|
||||
o @ (Obj::Op(_) | Obj::Exec(_)) => Ok(o),
|
||||
@@ -480,7 +488,7 @@ impl<'a> Interp<'a> {
|
||||
Gt | Ge | Lt | Le => {
|
||||
let (b, a) = (self.pop()?, self.pop()?);
|
||||
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)),
|
||||
}
|
||||
.ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
@@ -646,14 +654,10 @@ impl<'a> Interp<'a> {
|
||||
} else {
|
||||
show_text(&self.pop()?)
|
||||
};
|
||||
let n = text.len();
|
||||
if n > s.borrow().len() {
|
||||
if !s.write(0, text.as_bytes()) {
|
||||
return ps_err("rangecheck", "");
|
||||
}
|
||||
if let Some(d) = s.borrow_mut().get_mut(..n) {
|
||||
d.copy_from_slice(text.as_bytes());
|
||||
}
|
||||
self.push_interval(Obj::Str(s), 0, n)?;
|
||||
self.push_interval(Obj::Str(s), 0, text.len())?;
|
||||
}
|
||||
ReadOnly | ExecuteOnly | NoAccess | SetPacking | SetGlobal | SetObjectFormat => {
|
||||
if matches!(op, SetPacking | SetGlobal | SetObjectFormat) {
|
||||
@@ -683,8 +687,8 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Length => {
|
||||
let n = match self.pop()? {
|
||||
Obj::Array { items, .. } => items.borrow().len(),
|
||||
Obj::Str(s) => s.borrow().len(),
|
||||
Obj::Array { items, .. } => items.len(),
|
||||
Obj::Str(s) => s.len(),
|
||||
Obj::Dict(d) => d.borrow().len(),
|
||||
Obj::Name(n) | Obj::Exec(n) => n.len(),
|
||||
_ => return ps_err("typecheck", ""),
|
||||
@@ -699,14 +703,8 @@ impl<'a> Interp<'a> {
|
||||
Get => {
|
||||
let k = self.pop()?;
|
||||
let o = match self.pop()? {
|
||||
Obj::Array { items, .. } => {
|
||||
let i = index(&k)?;
|
||||
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::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::Dict(d) => {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let v = d.borrow().get(&key).cloned();
|
||||
@@ -718,21 +716,21 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Put => {
|
||||
let (v, k, c) = (self.pop()?, self.pop()?, self.pop()?);
|
||||
match c {
|
||||
Obj::Array { items, .. } => {
|
||||
let i = index(&k)?;
|
||||
*items.borrow_mut().get_mut(i).ok_or(PsError::Ps("rangecheck", String::new()))? = v;
|
||||
}
|
||||
let written = match c {
|
||||
Obj::Array { items, .. } => items.write(index(&k)?, std::slice::from_ref(&v)),
|
||||
Obj::Str(s) => {
|
||||
let i = index(&k)?;
|
||||
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) => {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
insert(&d, key, v)?;
|
||||
true
|
||||
}
|
||||
_ => return ps_err("typecheck", ""),
|
||||
};
|
||||
if !written {
|
||||
return ps_err("rangecheck", "");
|
||||
}
|
||||
}
|
||||
GetInterval => {
|
||||
@@ -744,41 +742,35 @@ impl<'a> Interp<'a> {
|
||||
PutInterval => {
|
||||
let src = self.pop()?;
|
||||
let at = self.pop_count()?;
|
||||
match (self.pop()?, src) {
|
||||
(Obj::Array { items, .. }, Obj::Array { items: from, .. }) => {
|
||||
let from = from.borrow().clone();
|
||||
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);
|
||||
}
|
||||
let written = match (self.pop()?, src) {
|
||||
(Obj::Array { items, .. }, Obj::Array { items: from, .. }) => items.write(at, &from.to_vec()),
|
||||
(Obj::Str(s), Obj::Str(from)) => s.write(at, &from.to_vec()),
|
||||
_ => return ps_err("typecheck", ""),
|
||||
};
|
||||
if !written {
|
||||
return ps_err("rangecheck", "");
|
||||
}
|
||||
}
|
||||
Aload => {
|
||||
let items = self.pop_array()?;
|
||||
let all = items.borrow().clone();
|
||||
for o in all {
|
||||
for o in items.to_vec() {
|
||||
self.push(o)?;
|
||||
}
|
||||
self.push(Obj::Array { items, exec: false })?;
|
||||
}
|
||||
Astore => {
|
||||
let items = self.pop_array()?;
|
||||
let n = items.borrow().len();
|
||||
let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
||||
*items.borrow_mut() = self.stack.split_off(at);
|
||||
let at = self.stack.len().checked_sub(items.len()).ok_or(PsError::Ps("stackunderflow", String::new()))?;
|
||||
if !items.write(0, self.stack.get(at..).unwrap_or_default()) {
|
||||
return ps_err("rangecheck", "");
|
||||
}
|
||||
self.stack.truncate(at);
|
||||
self.push(Obj::Array { items, exec: false })?;
|
||||
}
|
||||
Search | AnchorSearch => {
|
||||
let seek = self.pop_str()?.borrow().clone();
|
||||
let seek = self.pop_str()?.to_vec();
|
||||
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) };
|
||||
match at {
|
||||
Some(i) => {
|
||||
@@ -888,7 +880,6 @@ impl<'a> Interp<'a> {
|
||||
self.push(Obj::Int(v))?;
|
||||
}
|
||||
}
|
||||
LanguageLevel => self.push(Obj::Int(3))?,
|
||||
Version => self.push(Obj::string(b"3010".to_vec()))?,
|
||||
Product => self.push(Obj::string(b"VectorCraft".to_vec()))?,
|
||||
RealTime | UserTime => self.push(Obj::Int(0))?,
|
||||
@@ -922,19 +913,18 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
}
|
||||
Obj::Array { items, exec } => {
|
||||
let from = self.pop_array()?.borrow().clone();
|
||||
let n = from.len();
|
||||
{
|
||||
let mut v = items.borrow_mut();
|
||||
v.get_mut(..n).ok_or(PsError::Ps("rangecheck", String::new()))?.clone_from_slice(&from);
|
||||
let from = self.pop_array()?.to_vec();
|
||||
if !items.write(0, &from) {
|
||||
return ps_err("rangecheck", "copy");
|
||||
}
|
||||
self.push_interval(Obj::Array { items, exec }, 0, n)?;
|
||||
self.push_interval(Obj::Array { items, exec }, 0, from.len())?;
|
||||
}
|
||||
Obj::Str(s) => {
|
||||
let from = self.pop_str()?.borrow().clone();
|
||||
let n = from.len();
|
||||
s.borrow_mut().get_mut(..n).ok_or(PsError::Ps("rangecheck", String::new()))?.copy_from_slice(&from);
|
||||
self.push_interval(Obj::Str(s), 0, n)?;
|
||||
let from = self.pop_str()?.to_vec();
|
||||
if !s.write(0, &from) {
|
||||
return ps_err("rangecheck", "copy");
|
||||
}
|
||||
self.push_interval(Obj::Str(s), 0, from.len())?;
|
||||
}
|
||||
Obj::Dict(d) => {
|
||||
let from = self.pop_dict()?.borrow().clone();
|
||||
@@ -946,16 +936,12 @@ impl<'a> Interp<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `n` items of an array or string from `at` (a copy: shared storage isn't modelled).
|
||||
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()))?;
|
||||
self.alloc(n * if matches!(o, Obj::Str(_)) { 1 } else { std::mem::size_of::<Obj>() })?;
|
||||
/// `n` items of an array or string from `at`, sharing its storage.
|
||||
pub fn push_interval(&mut self, o: Obj, at: usize, n: usize) -> Res {
|
||||
let range = || PsError::Ps("rangecheck", String::new());
|
||||
let r = match o {
|
||||
Obj::Array { items, exec } => {
|
||||
let v = items.borrow().get(at..end).ok_or(PsError::Ps("rangecheck", String::new()))?.to_vec();
|
||||
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()),
|
||||
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)?),
|
||||
_ => return ps_err("typecheck", "getinterval"),
|
||||
};
|
||||
self.push(r)
|
||||
@@ -966,7 +952,7 @@ impl<'a> Interp<'a> {
|
||||
match self.pop()? {
|
||||
Obj::Array { items, .. } => {
|
||||
let mut i = 0;
|
||||
while let Some(o) = items.borrow().get(i).cloned() {
|
||||
while let Some(o) = items.get(i) {
|
||||
i += 1;
|
||||
self.push(o)?;
|
||||
if !self.body(&p)? {
|
||||
@@ -975,8 +961,7 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
}
|
||||
Obj::Str(s) => {
|
||||
let bytes = s.borrow().clone();
|
||||
for b in bytes {
|
||||
for b in s.to_vec() {
|
||||
self.push(Obj::Int(i64::from(b)))?;
|
||||
if !self.body(&p)? {
|
||||
break;
|
||||
@@ -1000,22 +985,19 @@ impl<'a> Interp<'a> {
|
||||
|
||||
/// Replace the names in a procedure that stand for operators by the operators, down through
|
||||
/// 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 {
|
||||
return;
|
||||
}
|
||||
let n = items.borrow().len();
|
||||
for i in 0..n {
|
||||
let o = items.borrow().get(i).cloned();
|
||||
match o {
|
||||
for i in 0..items.len() {
|
||||
match items.get(i) {
|
||||
Some(Obj::Exec(name)) => {
|
||||
if let Some(Obj::Op(op)) = self.lookup(&name)
|
||||
&& let Some(slot) = items.borrow_mut().get_mut(i)
|
||||
{
|
||||
*slot = Obj::Op(op);
|
||||
if let Some(o @ Obj::Op(_)) = self.lookup(&name) {
|
||||
// `i` is in range and nothing is reading the procedure: the write can't fail.
|
||||
items.write(i, &[o]);
|
||||
}
|
||||
}
|
||||
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", ""),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -1057,7 +1100,7 @@ fn equal(a: &Obj, b: &Obj) -> bool {
|
||||
match (a, b) {
|
||||
(Obj::Null, Obj::Null) | (Obj::Mark, Obj::Mark) => true,
|
||||
(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::Op(x), Obj::Op(y)) => x == y,
|
||||
_ => 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.
|
||||
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();
|
||||
if w == 0 || h == 0 {
|
||||
return None;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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::rc::Rc;
|
||||
|
||||
@@ -31,10 +31,10 @@ pub(crate) enum Obj {
|
||||
Name(Rc<str>),
|
||||
/// An executable name: looked up and run.
|
||||
Exec(Rc<str>),
|
||||
Str(Rc<RefCell<Vec<u8>>>),
|
||||
Str(Shared<u8>),
|
||||
/// An array; executable ones are procedures.
|
||||
Array {
|
||||
items: Rc<RefCell<Vec<Obj>>>,
|
||||
items: Shared<Obj>,
|
||||
exec: bool,
|
||||
},
|
||||
Dict(DictRef),
|
||||
@@ -53,15 +53,15 @@ impl Obj {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
Self::Array { items: Rc::new(RefCell::new(items)), exec: true }
|
||||
Self::Array { items: Shared::new(items), exec: true }
|
||||
}
|
||||
|
||||
pub fn dict(d: Dict) -> Self {
|
||||
@@ -98,7 +98,7 @@ impl Obj {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
Self::Array { items, .. } => Some(items),
|
||||
_ => 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 {
|
||||
pub fn name(s: &str) -> Self {
|
||||
Self::Name(Rc::from(s))
|
||||
@@ -209,7 +274,7 @@ ops! {
|
||||
ErrorDict = "errordict", DollarError = "$error", Bind = "bind",
|
||||
// Virtual memory and the environment.
|
||||
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",
|
||||
// Graphics state.
|
||||
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",
|
||||
Arcto = "arcto", ClosePath = "closepath", FlattenPath = "flattenpath", ReversePath = "reversepath",
|
||||
PathBBox = "pathbbox", ClipPath = "clippath", InitClip = "initclip", Clip = "clip", EoClip = "eoclip",
|
||||
RectClip = "rectclip", Fill = "fill", EoFill = "eofill", Stroke = "stroke", RectFill = "rectfill",
|
||||
RectStroke = "rectstroke", ShFill = "shfill",
|
||||
RectClip = "rectclip", ClipSave = "clipsave", ClipRestore = "cliprestore", Fill = "fill", EoFill = "eofill",
|
||||
Stroke = "stroke", RectFill = "rectfill", RectStroke = "rectstroke", ShFill = "shfill",
|
||||
// Images.
|
||||
Image = "image", ImageMask = "imagemask", ColorImage = "colorimage",
|
||||
// Files.
|
||||
@@ -252,7 +317,8 @@ ops! {
|
||||
// Fonts and type.
|
||||
FindFont = "findfont", ScaleFont = "scalefont", MakeFont = "makefont", SetFont = "setfont", SelectFont = "selectfont",
|
||||
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",
|
||||
KShow = "kshow", GlyphShow = "glyphshow", StringWidth = "stringwidth", CharPath = "charpath",
|
||||
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!(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 v = self.pop()?;
|
||||
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)? };
|
||||
dict.borrow_mut().insert(k, v.clone());
|
||||
// A category is defined by its implementation dictionary.
|
||||
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)?;
|
||||
}
|
||||
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 => {
|
||||
let category = self.pop()?;
|
||||
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 = dict.borrow().contains_key(&k);
|
||||
let known = self.instances(&category)?.borrow().contains_key(&k);
|
||||
if known {
|
||||
self.push(Obj::Int(1))?;
|
||||
self.push(Obj::Int(0))?;
|
||||
}
|
||||
self.push(Obj::Bool(known))?;
|
||||
}
|
||||
ResourceForAll => self.resource_for_all()?,
|
||||
Show => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.show(&s)?;
|
||||
}
|
||||
AShow => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.nums::<2>()?;
|
||||
self.show(&s)?;
|
||||
}
|
||||
WidthShow => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.pop()?;
|
||||
self.nums::<2>()?;
|
||||
self.show(&s)?;
|
||||
}
|
||||
AWidthShow => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.nums::<2>()?;
|
||||
self.pop()?;
|
||||
self.nums::<2>()?;
|
||||
@@ -149,11 +157,11 @@ impl Interp<'_> {
|
||||
}
|
||||
XShow | YShow | XYShow => {
|
||||
self.pop()?;
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.show(&s)?;
|
||||
}
|
||||
KShow => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.pop()?;
|
||||
self.show(&s)?;
|
||||
}
|
||||
@@ -163,14 +171,14 @@ impl Interp<'_> {
|
||||
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
|
||||
}
|
||||
StringWidth => {
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
let (_, _, v) = self.set_type(&s, Point::ZERO)?;
|
||||
self.push_num(v.x)?;
|
||||
self.push_num(v.y)?;
|
||||
}
|
||||
CharPath => {
|
||||
self.pop_bool()?;
|
||||
let s = self.pop_str()?.borrow().clone();
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.char_path(&s)?;
|
||||
}
|
||||
SetCacheDevice => {
|
||||
|
||||
+110
-1
@@ -1,5 +1,6 @@
|
||||
//! 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;
|
||||
|
||||
@@ -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())?;
|
||||
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
|
||||
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),
|
||||
`gsave`/`grestore`, `save`/`restore`, transforms, procedures with `bind def`, loops and dictionaries, axial and radial
|
||||
shadings and shading patterns (gradients), images and image masks (data in the file through ASCII85, hex, run-length,
|
||||
Flate, LZW or DCT filters, or from procedures), and type as point type in the font the file names (embedded font
|
||||
programs are skipped). The artboard is the `%%HiResBoundingBox` (else `%%BoundingBox`; a letter page without one).
|
||||
A program the interpreter can't run (an operator it doesn't know, an error, a runaway loop) or that draws nothing comes
|
||||
in as its TIFF preview 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.
|
||||
CMYK, indexed and spot colours (a Separation ink becomes a spot swatch, painted at its tint), clips (clipping groups;
|
||||
`clipsave`/`cliprestore`), `gsave`/`grestore`, `save`/`restore`, transforms, procedures with `bind def`, loops,
|
||||
dictionaries, arrays and strings whose intervals share storage, resources (categories made from `Generic`,
|
||||
`resourceforall`), axial and radial shadings and shading patterns (gradients), images and image masks (data in the
|
||||
file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as point type in the
|
||||
font the file names (embedded font programs are skipped). The artboard is the `%%HiResBoundingBox` (else
|
||||
`%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know, an
|
||||
error, a runaway loop) or that draws nothing comes in as its TIFF preview (palette previews with an alpha channel too)
|
||||
with a warning; without a preview, the art drawn up to the error is kept with a warning, and a file with none is
|
||||
refused with a message saying why.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/logo.eps"}}}
|
||||
|
||||
Reference in new issue
Block a user