From 821e979debcba42bc477189e517a4e866820776c Mon Sep 17 00:00:00 2001 From: "markus.doebele" Date: Fri, 16 Oct 2026 08:21:50 +0200 Subject: [PATCH 1/7] feat(ps): add craft-ps with an EPS reader (read feature) The reading half of craft-ps (#115): a clean-room PostScript interpreter that runs an EPS and returns what it paints as a one-page vector PDF of its bounding box, colours kept, so apps can draw and export EPS as a placed PDF. Moved from DesignCraft (storytold/designcraft, where it was written for placed EPS), with its app-specific names made neutral. Written from the PostScript Language Reference (3rd ed.), the EPS specification, the Type 1 Font Format and Adobe TN 5012/5176/5177. Covers the language, paths, clipping, strokepath, device/Separation/ DeviceN/Indexed/ICCBased/Lab colour, images with lazily decoded filters, shadings 1-7 and patterns, and Type 1/3/42/CFF/CID text through CMaps, horizontal and vertical. Bounded: each operator charges work proportional to its operands against the step limit as it goes, every byte read or decoded counts, copies count against the memory cap; defaults 50 M steps / 1 GiB, and 20 M / 256 MiB on wasm32. No panics (deny lints, fuzzed in debug builds). 34 tests, the Type 42 one on a TrueType font built in the test. No new external crates: pdf-writer, miniz_oxide, weezl, skrifa and kurbo are already locked at these versions. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 12 + Cargo.toml | 5 + crates/eps/README.md | 2 + crates/eps/src/container.rs | 36 +- crates/eps/src/dsc.rs | 14 +- crates/eps/src/lib.rs | 4 +- crates/eps/src/tests.rs | 18 + crates/ps/Cargo.toml | 27 + crates/ps/README.md | 49 ++ crates/ps/examples/eps2pdf.rs | 41 + crates/ps/src/lib.rs | 9 + crates/ps/src/read/cff.rs | 604 ++++++++++++++ crates/ps/src/read/cff_strings.rs | 252 ++++++ crates/ps/src/read/composite.rs | 549 +++++++++++++ crates/ps/src/read/encodings.rs | 318 ++++++++ crates/ps/src/read/font.rs | 523 ++++++++++++ crates/ps/src/read/gstate.rs | 342 ++++++++ crates/ps/src/read/interp.rs | 678 ++++++++++++++++ crates/ps/src/read/mod.rs | 118 +++ crates/ps/src/read/obj.rs | 301 +++++++ crates/ps/src/read/ops/basic.rs | 1080 +++++++++++++++++++++++++ crates/ps/src/read/ops/gfx.rs | 1248 +++++++++++++++++++++++++++++ crates/ps/src/read/ops/image.rs | 366 +++++++++ crates/ps/src/read/ops/mesh.rs | 261 ++++++ crates/ps/src/read/ops/misc.rs | 740 +++++++++++++++++ crates/ps/src/read/ops/mod.rs | 226 ++++++ crates/ps/src/read/ops/shading.rs | 389 +++++++++ crates/ps/src/read/pdf.rs | 619 ++++++++++++++ crates/ps/src/read/scan.rs | 375 +++++++++ crates/ps/src/read/source.rs | 926 +++++++++++++++++++++ crates/ps/src/read/tests.rs | 524 ++++++++++++ crates/ps/src/read/type1.rs | 357 +++++++++ crates/ps/src/read/type42.rs | 81 ++ 33 files changed, 11081 insertions(+), 13 deletions(-) create mode 100644 crates/ps/Cargo.toml create mode 100644 crates/ps/README.md create mode 100644 crates/ps/examples/eps2pdf.rs create mode 100644 crates/ps/src/lib.rs create mode 100644 crates/ps/src/read/cff.rs create mode 100644 crates/ps/src/read/cff_strings.rs create mode 100644 crates/ps/src/read/composite.rs create mode 100644 crates/ps/src/read/encodings.rs create mode 100644 crates/ps/src/read/font.rs create mode 100644 crates/ps/src/read/gstate.rs create mode 100644 crates/ps/src/read/interp.rs create mode 100644 crates/ps/src/read/mod.rs create mode 100644 crates/ps/src/read/obj.rs create mode 100644 crates/ps/src/read/ops/basic.rs create mode 100644 crates/ps/src/read/ops/gfx.rs create mode 100644 crates/ps/src/read/ops/image.rs create mode 100644 crates/ps/src/read/ops/mesh.rs create mode 100644 crates/ps/src/read/ops/misc.rs create mode 100644 crates/ps/src/read/ops/mod.rs create mode 100644 crates/ps/src/read/ops/shading.rs create mode 100644 crates/ps/src/read/pdf.rs create mode 100644 crates/ps/src/read/scan.rs create mode 100644 crates/ps/src/read/source.rs create mode 100644 crates/ps/src/read/tests.rs create mode 100644 crates/ps/src/read/type1.rs create mode 100644 crates/ps/src/read/type42.rs diff --git a/Cargo.lock b/Cargo.lock index 722a8f1f99..6581c75ac2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2360,6 +2360,18 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "craft-ps" +version = "0.1.0" +dependencies = [ + "craft-eps", + "kurbo 0.13.1", + "miniz_oxide 0.8.9", + "pdf-writer", + "skrifa 0.47.0", + "weezl 0.1.12", +] + [[package]] name = "craft-psd" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 57b7d3ef7c..38b82c843d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,13 @@ craft-affinity = { path = "crates/affinity" } craft-ai-core = { path = "crates/ai-core" } craft-anicraft-format = { path = "crates/anicraft-format" } craft-pdf = { path = "crates/pdf", version = "=0.1.0" } +craft-ps = { path = "crates/ps", version = "=0.1.0" } craft-pdn = { path = "crates/pdn" } craft-plate-test = { path = "crates/plate-test" } +pdf-writer = "0.15" +miniz_oxide = "0.8" +weezl = "0.1" +skrifa = "0.47" krilla = { version = "=0.8.2", default-features = false, features = ["raster-images"] } hayro = { version = "=0.7.1", default-features = false } hayro-interpret = { version = "=0.7.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] } diff --git a/crates/eps/README.md b/crates/eps/README.md index f2abd0b2c8..25c8a7eda3 100644 --- a/crates/eps/README.md +++ b/crates/eps/README.md @@ -8,8 +8,10 @@ nothing. |---|---| | `sniff`, `is_eps` | EPS (binary header, or `%!PS-Adobe-… EPSF-…`), plain PostScript or PDF, and whether it is an Illustrator file: PostScript-based (`%AI…` comments, or Illustrator as `%%Creator`) or PDF-based (the `AIPrivateData` / `AIPDFPrivateData` keys near the start or end of the PDF, a hint only) | | `sections` | The parts behind the binary header ("DOS EPS"): the PostScript, the TIFF preview, the Windows metafile preview. A header that points outside the file is an error | +| `postscript_section` | The PostScript program alone; a damaged preview does not prevent a reader from using a valid program section | | `wrap` | PostScript and its previews behind a binary header, as `sections` reads them back | | `Comments::read` | `%%BoundingBox` and `%%HiResBoundingBox` (the first in the header; `(atend)` taken from the trailer, outside embedded documents), `%%Pages`, `%%LanguageLevel`, `%%Creator`, `%%Title` | +| `Comments::read_with_header_limit` | The same comments with a caller-selected header scan limit capped at 256 KiB; `read` defaults to 64 KiB | | `epsi` | The device-independent preview (`%%BeginPreview`, 1, 2, 4 or 8 bits) as an 8-bit grey bitmap | | `ascii85::encode`, `ascii85::decode` | The ASCII85 data encoding; lines never start with `%`, decoding is bounded | diff --git a/crates/eps/src/container.rs b/crates/eps/src/container.rs index ba721f5c2c..245252b838 100644 --- a/crates/eps/src/container.rs +++ b/crates/eps/src/container.rs @@ -27,6 +27,27 @@ pub struct Sections<'a> { pub metafile: Option<&'a [u8]>, } +fn word(bytes: &[u8], at: usize) -> Result { + let end = at.checked_add(4).ok_or(Error::OutsideFile)?; + let b = bytes.get(at..end).and_then(|b| <[u8; 4]>::try_from(b).ok()).ok_or(Error::OutsideFile)?; + usize::try_from(u32::from_le_bytes(b)).map_err(|_| Error::OutsideFile) +} + +fn part(bytes: &[u8], at: usize) -> Result<&[u8], Error> { + let len_at = at.checked_add(4).ok_or(Error::OutsideFile)?; + let (start, len) = (word(bytes, at)?, word(bytes, len_at)?); + start.checked_add(len).and_then(|end| bytes.get(start..end)).ok_or(Error::OutsideFile) +} + +/// The PostScript program in an EPS, ignoring preview ranges in its binary header. +/// +/// This is useful to readers that execute only PostScript and historically accepted a valid +/// program section even when a separate preview range was damaged. Use [`sections`] when every +/// declared section must be validated. +pub fn postscript_section(bytes: &[u8]) -> Result<&[u8], Error> { + if bytes.starts_with(&DOS_MAGIC) { part(bytes, 4) } else { Ok(bytes) } +} + /// The sections of an EPS file: its PostScript and previews behind a binary header, or the whole /// file as PostScript. A header that points outside the file is an error, whichever section it /// is: such a file is cut short or not an EPS. @@ -34,17 +55,12 @@ pub fn sections(bytes: &[u8]) -> Result, Error> { if !bytes.starts_with(&DOS_MAGIC) { return Ok(Sections { postscript: bytes, tiff: None, metafile: None }); } - let word = |at: usize| -> Result { - let b = bytes.get(at..at.saturating_add(4)).and_then(|b| <[u8; 4]>::try_from(b).ok()).ok_or(Error::OutsideFile)?; - usize::try_from(u32::from_le_bytes(b)).map_err(|_| Error::OutsideFile) - }; - let part = |at: usize| -> Result<&[u8], Error> { - let (start, len) = (word(at)?, word(at + 4)?); - start.checked_add(len).and_then(|end| bytes.get(start..end)).ok_or(Error::OutsideFile) - }; // A preview with no offset or no length is absent. - let preview = |at: usize| -> Result, Error> { if word(at)? == 0 || word(at + 4)? == 0 { Ok(None) } else { part(at).map(Some) } }; - Ok(Sections { postscript: part(4)?, metafile: preview(12)?, tiff: preview(20)? }) + let preview = |at: usize| -> Result, Error> { + let len_at = at.checked_add(4).ok_or(Error::OutsideFile)?; + if word(bytes, at)? == 0 || word(bytes, len_at)? == 0 { Ok(None) } else { part(bytes, at).map(Some) } + }; + Ok(Sections { postscript: postscript_section(bytes)?, metafile: preview(12)?, tiff: preview(20)? }) } /// `postscript` behind a binary header, followed by its previews. With neither preview the diff --git a/crates/eps/src/dsc.rs b/crates/eps/src/dsc.rs index 835f9b23f8..2334e1d901 100644 --- a/crates/eps/src/dsc.rs +++ b/crates/eps/src/dsc.rs @@ -2,6 +2,8 @@ /// Bytes of a file's start read for its header comments. pub const MAX_HEADER: usize = 64 << 10; +/// Largest header scan available for readers preserving a wider historical bound. +pub const MAX_EXTENDED_HEADER: usize = 256 << 10; /// Bytes of a file's end read for comments its header defers there with `(atend)`. pub const MAX_TRAILER: usize = 64 << 10; @@ -34,7 +36,17 @@ impl Comments { /// A bounding box the header defers with `(atend)` is the last one of the trailer, outside /// any document the file embeds. pub fn read(ps: &[u8]) -> Self { - let head = String::from_utf8_lossy(ps.get(..MAX_HEADER).unwrap_or(ps)); + Self::read_with_header_limit(ps, MAX_HEADER) + } + + /// Read comments with a caller-selected scan limit for the header. + /// + /// The limit is capped at [`MAX_EXTENDED_HEADER`], and the trailer remains capped at + /// [`MAX_TRAILER`]. This lets readers preserve a previously supported wider scan without + /// making header or trailer work unbounded. + pub fn read_with_header_limit(ps: &[u8], header_limit: usize) -> Self { + let header_limit = header_limit.min(MAX_EXTENDED_HEADER).min(ps.len()); + let head = String::from_utf8_lossy(&ps[..header_limit]); let mut c = Comments::default(); let (mut box_atend, mut hires_atend) = (false, false); // Lines may end in `\r` alone (classic Mac files). diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index c3878830bf..c9a2574f57 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -25,8 +25,8 @@ pub mod postscript; mod preview; mod rectangles; -pub use container::{DOS_HEADER, DOS_MAGIC, Sections, sections, wrap}; -pub use dsc::{Comments, MAX_HEADER, MAX_TRAILER}; +pub use container::{DOS_HEADER, DOS_MAGIC, Sections, postscript_section, sections, wrap}; +pub use dsc::{Comments, MAX_EXTENDED_HEADER, MAX_HEADER, MAX_TRAILER}; pub use embedded::{EmbeddedFontProgram, embedded_font_programs}; pub use preview::{Bitmap, MAX_EPSI, epsi}; pub use rectangles::{MAX_SIMPLE_RECTANGLE_SAVE_DEPTH, MAX_SIMPLE_RECTANGLE_TOKENS, MAX_SIMPLE_RECTANGLES, Rectangle, simple_rectangles}; diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index 7ac4c84cac..7a26ed09af 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -29,6 +29,24 @@ fn sections_of_plain_and_binary_files() { assert_eq!(sections(&file), Ok(Sections { postscript: EPS, tiff: Some(b"TIFF"), metafile: Some(b"WMF") })); } +#[test] +fn postscript_section_survives_a_damaged_preview_range() { + let ps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10 10\n"; + let mut bytes = wrap(ps, Some(b"tiff"), None).unwrap(); + bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes()); + bytes[24..28].copy_from_slice(&1u32.to_le_bytes()); + assert_eq!(postscript_section(&bytes), Ok(ps.as_slice())); + assert_eq!(sections(&bytes), Err(Error::OutsideFile)); +} + +#[test] +fn extended_comment_scan_keeps_its_upper_bound() { + let mut ps = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec(); + ps.resize(ps.len() + MAX_EXTENDED_HEADER + 1, b' '); + ps.extend_from_slice(b"\n%%BoundingBox: 0 0 20 10\n%%EndComments\n"); + assert_eq!(Comments::read_with_header_limit(&ps, usize::MAX).page_box(), None); +} + #[test] fn a_header_that_points_outside_the_file_is_refused() { let file = dos(EPS, b"WMF", b"TIFF"); diff --git a/crates/ps/Cargo.toml b/crates/ps/Cargo.toml new file mode 100644 index 0000000000..baa9050e4d --- /dev/null +++ b/crates/ps/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "craft-ps" +description = "PostScript and EPS for Craft apps: an EPS interpreter that paints into PDF" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[features] +default = [] +read = ["dep:craft-eps", "dep:pdf-writer", "dep:miniz_oxide", "dep:weezl", "dep:skrifa", "dep:kurbo"] + +[dependencies] +craft-eps = { workspace = true, optional = true } +pdf-writer = { workspace = true, optional = true } +miniz_oxide = { workspace = true, optional = true } +weezl = { workspace = true, optional = true } +skrifa = { workspace = true, optional = true } +kurbo = { workspace = true, optional = true } + +[[example]] +name = "eps2pdf" +required-features = ["read"] diff --git a/crates/ps/README.md b/crates/ps/README.md new file mode 100644 index 0000000000..db0729a86e --- /dev/null +++ b/crates/ps/README.md @@ -0,0 +1,49 @@ +# craft-ps + +PostScript and EPS for the Craft apps. No feature is enabled by default. + +| Feature | Provides | +| --- | --- | +| `read` | An EPS interpreter: runs the program and returns what it paints as a one-page PDF | + +Writing PostScript and EPS (craft-libs #115) is planned as a separate feature next to `read`. + +## Reading EPS + +`read::eps_to_pdf(bytes, limits)` runs an EPS (plain, or DOS EPS with a binary header) and +returns a one-page PDF 1.4 the size of its bounding box: vectors stay vectors, CMYK and spot +colours stay as they are, so an app can draw and export the result as a placed PDF. +`read::cached_pdf` converts with the default limits and remembers the result by content. +`is_eps`, `bounding_box` and `postscript_section` read the file without running it. +EPS binary sections and DSC comments are read by `craft-eps`. + +It is written from the PostScript Language Reference (third edition), the EPS specification, +the Type 1 Font Format and Adobe Technical Notes 5012 (Type 42), 5176 (CFF) and 5177 (Type 2 +charstrings); no GPL code was used. It covers what EPS files use: + +- the language: Level 3 operators, an explicit execution stack (no Rust recursion for + PostScript control flow), `stopped`, resources, `eexec`; +- graphics: paths, painting, clipping, `strokepath`, user paths and forms; +- colour: device spaces, Separation and DeviceN, Indexed, ICCBased, Lab; +- images, with filters decoded as they are read (ASCIIHex, ASCII85, RunLength, LZW, Flate with + predictors, SubFile, ReusableStream; JPEG is passed through); +- smooth shadings (types 1–7, meshes included), shading and tiling patterns; +- text in Type 1, Type 3, Type 42 and CFF fonts (`StartData`), CIDFonts and composite fonts + through CMaps, horizontal and vertical. + +Not covered: Level 2 binary tokens, CIDFontType 0 with Type 1 charstrings, CIDFontType 1 and +the old composite FMapTypes other than 2 and 9. + +## Limits + +`Limits { steps, memory }` bounds one conversion. Every operator charges the work it does in +proportion to its operands (elements copied or searched, path segments, glyphs, samples) +against `steps` as it goes, and every byte read or decoded at any filter level counts too, so +the step limit bounds run time. `memory` bounds everything a conversion makes, copies +included. The defaults are 50 million steps and 1 GiB, or 20 million and 256 MiB on wasm32; +real EPS files use at most a few million steps and tens of megabytes, and a program that runs +into the limits ends within seconds. A program that fails or exceeds them before painting +anything gives `Err`; one that fails later keeps what it painted (with a warning). + +`cargo run -p craft-ps --features read --example eps2pdf -- in.eps out.pdf` converts one file +and prints the steps and memory it used. diff --git a/crates/ps/examples/eps2pdf.rs b/crates/ps/examples/eps2pdf.rs new file mode 100644 index 0000000000..a3cbfe0cf0 --- /dev/null +++ b/crates/ps/examples/eps2pdf.rs @@ -0,0 +1,41 @@ +//! `cargo run -p craft-ps --features read --example eps2pdf -- in.eps out.pdf`: convert one EPS. + +fn main() -> std::process::ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let (Some(input), Some(output)) = (args.first(), args.get(1)) else { + eprintln!("usage: eps2pdf in.eps out.pdf"); + return std::process::ExitCode::FAILURE; + }; + let bytes = match std::fs::read(input) { + Ok(b) => b, + Err(e) => { + eprintln!("{input}: {e}"); + return std::process::ExitCode::FAILURE; + } + }; + let t = std::time::Instant::now(); + match craft_ps::read::eps_to_pdf(&bytes, craft_ps::read::Limits::default()) { + Ok(c) => { + eprintln!( + "{input}: {:.0} ms, bbox {:?}, {} warnings, {} steps, {} KiB", + t.elapsed().as_secs_f64() * 1000.0, + c.bbox, + c.warnings.len(), + c.steps, + c.memory / 1024 + ); + for w in c.warnings.iter().take(20) { + eprintln!(" {w}"); + } + if let Err(e) = std::fs::write(output, &c.pdf) { + eprintln!("{output}: {e}"); + return std::process::ExitCode::FAILURE; + } + std::process::ExitCode::SUCCESS + } + Err(e) => { + eprintln!("{input}: {e} ({:.0} ms)", t.elapsed().as_secs_f64() * 1000.0); + std::process::ExitCode::FAILURE + } + } +} diff --git a/crates/ps/src/lib.rs b/crates/ps/src/lib.rs new file mode 100644 index 0000000000..ab53832595 --- /dev/null +++ b/crates/ps/src/lib.rs @@ -0,0 +1,9 @@ +//! PostScript and EPS for the Craft apps. +//! +//! - [`read`] (feature `read`): runs an EPS and gives what it paints as a one-page vector PDF, +//! for apps that place EPS files as graphics. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#![forbid(unsafe_code)] + +#[cfg(feature = "read")] +pub mod read; diff --git a/crates/ps/src/read/cff.rs b/crates/ps/src/read/cff.rs new file mode 100644 index 0000000000..bbf7c4c8de --- /dev/null +++ b/crates/ps/src/read/cff.rs @@ -0,0 +1,604 @@ +//! CFF font sets and Type 2 charstrings (Adobe Technical Notes 5176 and 5177): name-keyed and +//! CID-keyed fonts, as `StartData` defines them. + +use crate::read::gstate::Seg; +use crate::read::type1::Glyph; + +/// The most glyphs, subroutines or INDEX entries read. +const MAX_ENTRIES: usize = 200_000; +const MAX_OPS: usize = 200_000; +const MAX_DEPTH: usize = 10; + +#[derive(Debug, Clone, Default)] +struct Private { + subrs: Vec<(usize, usize)>, + default_width: f64, + nominal_width: f64, +} + +#[derive(Debug, Clone)] +pub struct Font { + pub name: Vec, + pub matrix: [f64; 6], + pub bbox: [f64; 4], + /// Glyph names (name-keyed) by glyph index. + pub names: Vec>, + /// Code → glyph index (name-keyed fonts). + pub encoding: Vec<(u8, u32)>, + /// CID by glyph index (CID-keyed fonts), and the registry/ordering/supplement. + pub cids: Option>, + pub ros: Option<(Vec, Vec, i64)>, + /// CID → glyph index, made on first lookup. + by_cid: std::cell::OnceCell>, + charstrings: Vec<(usize, usize)>, + privates: Vec, + fd_select: Vec, +} + +#[derive(Debug)] +pub struct FontSet { + pub data: Vec, + gsubrs: Vec<(usize, usize)>, + pub fonts: Vec, +} + +fn u16_at(d: &[u8], i: usize) -> Option { + Some(usize::from(u16::from_be_bytes([*d.get(i)?, *d.get(i + 1)?]))) +} + +fn offset(d: &[u8], i: usize, size: usize) -> Option { + let mut v = 0usize; + for k in 0..size { + v = v.checked_mul(256)?.checked_add(usize::from(*d.get(i + k)?))?; + } + Some(v) +} + +/// An INDEX at `at`: its entries as (start, end) in `d`, and where it ends. +fn index(d: &[u8], at: usize) -> Option<(Vec<(usize, usize)>, usize)> { + let count = u16_at(d, at)?; + if count == 0 { + return Some((Vec::new(), at + 2)); + } + if count > MAX_ENTRIES { + return None; + } + let size = usize::from(*d.get(at + 2)?); + if !(1..=4).contains(&size) { + return None; + } + let offs_at = at + 3; + let data_at = offs_at + (count + 1) * size - 1; + let mut out = Vec::with_capacity(count); + let mut prev = offset(d, offs_at, size)?; + for i in 1..=count { + let next = offset(d, offs_at + i * size, size)?; + let (s, e) = (data_at.checked_add(prev)?, data_at.checked_add(next)?); + if e < s || e > d.len() { + return None; + } + out.push((s, e)); + prev = next; + } + let end = data_at.checked_add(prev)?; + Some((out, end)) +} + +#[derive(Debug, Default)] +struct Dict { + entries: Vec<(u16, Vec)>, +} + +impl Dict { + fn get(&self, op: u16) -> Option<&[f64]> { + self.entries.iter().find(|(o, _)| *o == op).map(|(_, v)| v.as_slice()) + } + + fn num(&self, op: u16) -> Option { + self.get(op)?.first().copied() + } +} + +fn dict(d: &[u8]) -> Dict { + let mut out = Dict::default(); + let mut ops: Vec = Vec::new(); + let mut i = 0; + while let Some(&b) = d.get(i) { + i += 1; + match b { + 0..=21 => { + let op = if b == 12 { + let e = d.get(i).copied().unwrap_or(0); + i += 1; + 1200 + u16::from(e) + } else { + u16::from(b) + }; + out.entries.push((op, std::mem::take(&mut ops))); + } + 28 => { + let v = i16::from_be_bytes([d.get(i).copied().unwrap_or(0), d.get(i + 1).copied().unwrap_or(0)]); + i += 2; + ops.push(f64::from(v)); + } + 29 => { + let v = i32::from_be_bytes([0, 1, 2, 3].map(|k| d.get(i + k).copied().unwrap_or(0))); + i += 4; + ops.push(f64::from(v)); + } + 30 => { + // A real: nibbles. + let mut s = String::new(); + 'real: while let Some(&n) = d.get(i) { + i += 1; + for nib in [n >> 4, n & 15] { + match nib { + 0..=9 => s.push(char::from(b'0' + nib)), + 0xa => s.push('.'), + 0xb => s.push('E'), + 0xc => s.push_str("E-"), + 0xe => s.push('-'), + 0xf => break 'real, + _ => {} + } + } + } + ops.push(s.parse().unwrap_or(0.0)); + } + 32..=246 => ops.push(f64::from(b) - 139.0), + 247..=250 => { + let w = d.get(i).copied().unwrap_or(0); + i += 1; + ops.push((f64::from(b) - 247.0) * 256.0 + f64::from(w) + 108.0); + } + 251..=254 => { + let w = d.get(i).copied().unwrap_or(0); + i += 1; + ops.push(-(f64::from(b) - 251.0) * 256.0 - f64::from(w) - 108.0); + } + _ => {} + } + if ops.len() > 64 { + break; + } + } + out +} + +fn private(d: &[u8], top: &Dict) -> Private { + let Some(v) = top.get(18) else { return Private::default() }; + let (size, off) = (v.first().copied().unwrap_or(0.0).max(0.0) as usize, v.get(1).copied().unwrap_or(0.0).max(0.0) as usize); + let Some(bytes) = off.checked_add(size).and_then(|e| d.get(off..e)) else { return Private::default() }; + let p = dict(bytes); + let subrs = p.num(19).and_then(|rel| index(d, off.checked_add(rel.max(0.0) as usize)?)).map(|(v, _)| v).unwrap_or_default(); + Private { subrs, default_width: p.num(20).unwrap_or(0.0), nominal_width: p.num(21).unwrap_or(0.0) } +} + +/// The glyph index → SID (or CID) charset. +fn charset(d: &[u8], at: usize, glyphs: usize) -> Vec { + let mut out = vec![0u32]; + // Predefined charsets (ISOAdobe and the expert ones): glyph index = SID. + if at <= 2 { + return (0..glyphs as u32).collect(); + } + let Some(&format) = d.get(at) else { return out }; + let mut i = at + 1; + while out.len() < glyphs { + match format { + 0 => match u16_at(d, i) { + Some(s) => { + out.push(s as u32); + i += 2; + } + None => break, + }, + 1 | 2 => { + let (Some(first), Some(n)) = (u16_at(d, i), if format == 1 { d.get(i + 2).map(|v| usize::from(*v)) } else { u16_at(d, i + 2) }) + else { + break; + }; + i += if format == 1 { 3 } else { 4 }; + for k in 0..=n { + if out.len() >= glyphs { + break; + } + out.push((first + k) as u32); + } + } + _ => break, + } + } + out +} + +fn fd_select(d: &[u8], at: usize, glyphs: usize) -> Vec { + let mut out = vec![0u8; glyphs]; + match d.get(at) { + Some(0) => { + for (g, slot) in out.iter_mut().enumerate() { + *slot = d.get(at + 1 + g).copied().unwrap_or(0); + } + } + Some(3) => { + let n = u16_at(d, at + 1).unwrap_or(0); + for r in 0..n { + let base = at + 3 + r * 3; + let (Some(first), Some(fd), Some(next)) = (u16_at(d, base), d.get(base + 2), u16_at(d, base + 3)) else { break }; + for g in first..next.min(glyphs) { + if let Some(slot) = out.get_mut(g) { + *slot = *fd; + } + } + } + } + _ => {} + } + out +} + +fn sid_name(sid: u32, strings: &[(usize, usize)], d: &[u8]) -> Vec { + let sid = sid as usize; + match crate::read::cff_strings::standard(sid) { + Some(n) => n.into_bytes(), + None => strings + .get(sid - crate::read::cff_strings::COUNT) + .and_then(|(s, e)| d.get(*s..*e)) + .map(<[u8]>::to_vec) + .unwrap_or_else(|| b".notdef".to_vec()), + } +} + +pub fn parse(data: Vec) -> Option { + let d = &data; + let hdr = usize::from(*d.get(2)?); + let (names, after_names) = index(d, hdr)?; + let (tops, after_tops) = index(d, after_names)?; + let (strings, after_strings) = index(d, after_tops)?; + let (gsubrs, _) = index(d, after_strings)?; + let mut fonts = Vec::new(); + for (k, &(ts, te)) in tops.iter().enumerate() { + let top = dict(d.get(ts..te)?); + let name = names.get(k).and_then(|(s, e)| d.get(*s..*e)).map(<[u8]>::to_vec).unwrap_or_default(); + let (charstrings, _) = index(d, top.num(17)?.max(0.0) as usize)?; + let glyphs = charstrings.len(); + let matrix = + top.get(1207).filter(|m| m.len() == 6).map(|m| [m[0], m[1], m[2], m[3], m[4], m[5]]).unwrap_or([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]); + let bbox = top.get(5).filter(|b| b.len() == 4).map(|b| [b[0], b[1], b[2], b[3]]).unwrap_or([0.0, 0.0, 1000.0, 1000.0]); + let cs = charset(d, top.num(15).unwrap_or(0.0).max(0.0) as usize, glyphs); + let ros = top.get(1230).map(|v| { + let s = |i: usize| sid_name(v.get(i).copied().unwrap_or(0.0) as u32, &strings, d); + (s(0), s(1), v.get(2).copied().unwrap_or(0.0) as i64) + }); + let (privates, fds, names_out, cids) = if ros.is_some() { + let fdarray = top.num(1236).and_then(|o| index(d, o.max(0.0) as usize)).map(|(v, _)| v).unwrap_or_default(); + let privates = fdarray.iter().map(|(s, e)| d.get(*s..*e).map(|b| private(d, &dict(b))).unwrap_or_default()).collect(); + let fds = top.num(1237).map(|o| fd_select(d, o.max(0.0) as usize, glyphs)).unwrap_or_else(|| vec![0; glyphs]); + (privates, fds, Vec::new(), Some(cs)) + } else { + let names_out: Vec> = cs.iter().map(|sid| sid_name(*sid, &strings, d)).collect(); + (vec![private(d, &top)], Vec::new(), names_out, None) + }; + // Encoding (name-keyed): predefined Standard, or format 0/1 code lists. + let mut encoding = Vec::new(); + if cids.is_none() { + match top.num(16).unwrap_or(0.0) as usize { + 0 => { + let std = crate::read::encodings::standard(); + for (code, n) in std.iter().enumerate() { + if let Some(g) = names_out.iter().position(|x| x == n.as_bytes()) { + encoding.push((code as u8, g as u32)); + } + } + } + 1 => {} + at => { + let fmt = d.get(at).copied().unwrap_or(0) & 0x7f; + let n = usize::from(d.get(at + 1).copied().unwrap_or(0)); + if fmt == 0 { + for g in 0..n { + if let Some(c) = d.get(at + 2 + g) { + encoding.push((*c, g as u32 + 1)); + } + } + } else { + let mut g = 1u32; + for r in 0..n { + let (Some(first), Some(left)) = (d.get(at + 2 + r * 2), d.get(at + 3 + r * 2)) else { break }; + for k in 0..=u32::from(*left) { + encoding.push((first.wrapping_add(k as u8), g)); + g += 1; + } + } + } + } + } + } + fonts.push(Font { + name, + matrix, + bbox, + names: names_out, + encoding, + cids, + ros, + by_cid: std::cell::OnceCell::new(), + charstrings, + privates, + fd_select: fds, + }); + } + Some(FontSet { data, gsubrs, fonts }) +} + +fn bias(n: usize) -> i64 { + if n < 1240 { + 107 + } else if n < 33900 { + 1131 + } else { + 32768 + } +} + +struct Run { + path: Vec, + x: f64, + y: f64, + open: bool, + stack: Vec, + hints: usize, + width: Option, + ops: usize, + done: bool, +} + +impl Run { + fn move_to(&mut self, dx: f64, dy: f64) { + if self.open { + self.path.push(Seg::Close); + } + self.x += dx; + self.y += dy; + self.path.push(Seg::Move(self.x, self.y)); + self.open = true; + } + + fn line(&mut self, dx: f64, dy: f64) { + self.x += dx; + self.y += dy; + self.path.push(Seg::Line(self.x, self.y)); + } + + fn curve(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) { + let (x1, y1) = (self.x + a, self.y + b); + let (x2, y2) = (x1 + c, y1 + d); + let (x3, y3) = (x2 + e, y2 + f); + self.path.push(Seg::Curve(x1, y1, x2, y2, x3, y3)); + self.x = x3; + self.y = y3; + } + + /// The first stack-clearing operator may carry the width before its arguments: there is + /// one when the count's parity differs from the operator's own (`expected_odd`). + fn take_width(&mut self, expected_odd: bool) { + if self.width.is_none() { + let present = !self.stack.is_empty() && (self.stack.len() % 2 == 1) != expected_odd; + self.width = Some(if present { self.stack.remove(0) } else { f64::NAN }); + } + } +} + +impl FontSet { + fn bytes(&self, r: (usize, usize)) -> &[u8] { + self.data.get(r.0..r.1).unwrap_or(&[]) + } + + fn exec(&self, code: &[u8], local: &[(usize, usize)], run: &mut Run, depth: usize) -> Option<()> { + if depth > MAX_DEPTH { + return None; + } + let mut i = 0; + while i < code.len() && !run.done { + run.ops += 1; + if run.ops > MAX_OPS || run.stack.len() > 96 { + return None; + } + let b = *code.get(i)?; + i += 1; + match b { + 32..=246 => run.stack.push(f64::from(b) - 139.0), + 247..=250 => { + let w = *code.get(i)?; + i += 1; + run.stack.push((f64::from(b) - 247.0) * 256.0 + f64::from(w) + 108.0); + } + 251..=254 => { + let w = *code.get(i)?; + i += 1; + run.stack.push(-(f64::from(b) - 251.0) * 256.0 - f64::from(w) - 108.0); + } + 28 => { + let v = i16::from_be_bytes([*code.get(i)?, *code.get(i + 1)?]); + i += 2; + run.stack.push(f64::from(v)); + } + 255 => { + let v = i32::from_be_bytes([*code.get(i)?, *code.get(i + 1)?, *code.get(i + 2)?, *code.get(i + 3)?]); + i += 4; + run.stack.push(f64::from(v) / 65536.0); + } + // hstem, vstem, hstemhm, vstemhm: count the hints (an odd count carries the width). + 1 | 3 | 18 | 23 => { + run.take_width(false); + run.hints += run.stack.len() / 2; + run.stack.clear(); + } + 19 | 20 => { + // hintmask, cntrmask: implicit vstems, then the mask bytes. + run.take_width(false); + run.hints += run.stack.len() / 2; + run.stack.clear(); + i += run.hints.div_ceil(8); + } + 21 => { + run.take_width(false); + let s = std::mem::take(&mut run.stack); + run.move_to(s.first().copied().unwrap_or(0.0), s.get(1).copied().unwrap_or(0.0)); + } + 22 | 4 => { + run.take_width(true); + let s = std::mem::take(&mut run.stack); + let v = s.first().copied().unwrap_or(0.0); + if b == 22 { run.move_to(v, 0.0) } else { run.move_to(0.0, v) } + } + 5 => { + let s = std::mem::take(&mut run.stack); + for &[x, y] in s.as_chunks::<2>().0 { + run.line(x, y); + } + } + 6 | 7 => { + let s = std::mem::take(&mut run.stack); + let mut horizontal = b == 6; + for v in s { + if horizontal { + run.line(v, 0.0) + } else { + run.line(0.0, v) + } + horizontal = !horizontal; + } + } + 8 => { + let s = std::mem::take(&mut run.stack); + for &[a, b2, c, d, e, f] in s.as_chunks::<6>().0 { + run.curve(a, b2, c, d, e, f); + } + } + 24 => { + // rcurveline + let s = std::mem::take(&mut run.stack); + let n = s.len().saturating_sub(2) / 6; + for &[a, b2, c, d, e, f] in s.as_chunks::<6>().0.iter().take(n) { + run.curve(a, b2, c, d, e, f); + } + if let (Some(a), Some(b2)) = (s.get(n * 6), s.get(n * 6 + 1)) { + run.line(*a, *b2); + } + } + 25 => { + // rlinecurve + let s = std::mem::take(&mut run.stack); + let lines = s.len().saturating_sub(6) / 2; + for &[x, y] in s.as_chunks::<2>().0.iter().take(lines) { + run.line(x, y); + } + if let Some(&[a, b2, c, d, e, f]) = s.get(lines * 2..lines * 2 + 6).and_then(|w| <&[f64; 6]>::try_from(w).ok()) { + run.curve(a, b2, c, d, e, f); + } + } + 26 | 27 => { + // vvcurveto, hhcurveto + let mut s = std::mem::take(&mut run.stack); + let mut first = if s.len() % 4 == 1 { Some(s.remove(0)) } else { None }; + for &[p, q, r, t] in s.as_chunks::<4>().0 { + let d1 = first.take().unwrap_or(0.0); + if b == 26 { run.curve(d1, p, q, r, 0.0, t) } else { run.curve(p, d1, q, r, t, 0.0) } + } + } + 30 | 31 => { + // vhcurveto, hvcurveto: alternating curves; the last may end off-axis. + let s = std::mem::take(&mut run.stack); + let mut horizontal = b == 31; + let mut k = 0; + while let Some(&[a, bb, c, d]) = s.get(k..k + 4).and_then(|w| <&[f64; 4]>::try_from(w).ok()) { + let extra = if s.len() - k == 5 { s.get(k + 4).copied().unwrap_or(0.0) } else { 0.0 }; + if horizontal { + run.curve(a, 0.0, bb, c, extra, d) + } else { + run.curve(0.0, a, bb, c, d, extra) + } + horizontal = !horizontal; + k += 4; + } + } + 10 | 29 => { + let n = run.stack.pop()? as i64; + let (list, bias) = if b == 10 { (local, bias(local.len())) } else { (&self.gsubrs[..], bias(self.gsubrs.len())) }; + let sub = *list.get(usize::try_from(n + bias).ok()?)?; + let sub = self.bytes(sub).to_vec(); + self.exec(&sub, local, run, depth + 1)?; + } + 11 => return Some(()), + 14 => { + run.take_width(false); + if run.open { + run.path.push(Seg::Close); + run.open = false; + } + run.done = true; + } + 12 => { + let e = *code.get(i)?; + i += 1; + let s = std::mem::take(&mut run.stack); + let a = |k: usize| s.get(k).copied().unwrap_or(0.0); + match e { + 35 => { + run.curve(a(0), a(1), a(2), a(3), a(4), a(5)); + run.curve(a(6), a(7), a(8), a(9), a(10), a(11)); + } + 34 => { + // hflex: dx1 dx2 dy2 dx3 dx4 dx5 dx6, back to the start height. + run.curve(a(0), 0.0, a(1), a(2), a(3), 0.0); + run.curve(a(4), 0.0, a(5), -a(2), a(6), 0.0); + } + 36 => { + // hflex1: dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6. + run.curve(a(0), a(1), a(2), a(3), a(4), 0.0); + run.curve(a(5), 0.0, a(6), a(7), a(8), -(a(1) + a(3) + a(7))); + } + 37 => { + // flex1: the last coordinate runs along the longer direction. + let dx = a(0) + a(2) + a(4) + a(6) + a(8); + let dy = a(1) + a(3) + a(5) + a(7) + a(9); + let (ex, ey) = if dx.abs() > dy.abs() { (a(10), -dy) } else { (-dx, a(10)) }; + run.curve(a(0), a(1), a(2), a(3), a(4), a(5)); + run.curve(a(6), a(7), a(8), a(9), ex, ey); + } + _ => {} + } + } + _ => {} + } + } + Some(()) + } + + /// Glyph `gid` of font `k` in glyph space (multiplied out by its FontMatrix later). + pub fn glyph(&self, k: usize, gid: u32) -> Option { + let font = self.fonts.get(k)?; + let cs = *font.charstrings.get(gid as usize)?; + let fd = usize::from(font.fd_select.get(gid as usize).copied().unwrap_or(0)); + let private = font.privates.get(fd).or_else(|| font.privates.first()).cloned().unwrap_or_default(); + let mut run = Run { path: Vec::new(), x: 0.0, y: 0.0, open: false, stack: Vec::new(), hints: 0, width: None, ops: 0, done: false }; + let code = self.bytes(cs).to_vec(); + self.exec(&code, &private.subrs, &mut run, 0)?; + if run.open { + run.path.push(Seg::Close); + } + let w = match run.width { + Some(w) if w.is_finite() => private.nominal_width + w, + _ => private.default_width, + }; + Some(Glyph { cost: run.ops + run.path.len(), path: run.path, width: (w, 0.0) }) + } + + /// The glyph index of a CID in font `k`. + pub fn gid_of_cid(&self, k: usize, cid: u32) -> Option { + let font = self.fonts.get(k)?; + let cids = font.cids.as_ref()?; + let map = font.by_cid.get_or_init(|| cids.iter().enumerate().filter_map(|(g, c)| Some((*c, u32::try_from(g).ok()?))).collect()); + map.get(&cid).copied() + } +} diff --git a/crates/ps/src/read/cff_strings.rs b/crates/ps/src/read/cff_strings.rs new file mode 100644 index 0000000000..38061243ba --- /dev/null +++ b/crates/ps/src/read/cff_strings.rs @@ -0,0 +1,252 @@ +//! CFF standard strings (Technical Note 5176, Appendix A): SIDs below 391 name these. + +/// SIDs 0–228: the Standard and ISO Latin glyph names. +const NAMES: [&str; 229] = [ + ".notdef", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "exclamdown", + "cent", + "sterling", + "fraction", + "yen", + "florin", + "section", + "currency", + "quotesingle", + "quotedblleft", + "guillemotleft", + "guilsinglleft", + "guilsinglright", + "fi", + "fl", + "endash", + "dagger", + "daggerdbl", + "periodcentered", + "paragraph", + "bullet", + "quotesinglbase", + "quotedblbase", + "quotedblright", + "guillemotright", + "ellipsis", + "perthousand", + "questiondown", + "grave", + "acute", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "dieresis", + "ring", + "cedilla", + "hungarumlaut", + "ogonek", + "caron", + "emdash", + "AE", + "ordfeminine", + "Lslash", + "Oslash", + "OE", + "ordmasculine", + "ae", + "dotlessi", + "lslash", + "oslash", + "oe", + "germandbls", + "onesuperior", + "logicalnot", + "mu", + "trademark", + "Eth", + "onehalf", + "plusminus", + "Thorn", + "onequarter", + "divide", + "brokenbar", + "degree", + "thorn", + "threequarters", + "twosuperior", + "registered", + "minus", + "eth", + "multiply", + "threesuperior", + "copyright", + "Aacute", + "Acircumflex", + "Adieresis", + "Agrave", + "Aring", + "Atilde", + "Ccedilla", + "Eacute", + "Ecircumflex", + "Edieresis", + "Egrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Igrave", + "Ntilde", + "Oacute", + "Ocircumflex", + "Odieresis", + "Ograve", + "Otilde", + "Scaron", + "Uacute", + "Ucircumflex", + "Udieresis", + "Ugrave", + "Yacute", + "Ydieresis", + "Zcaron", + "aacute", + "acircumflex", + "adieresis", + "agrave", + "aring", + "atilde", + "ccedilla", + "eacute", + "ecircumflex", + "edieresis", + "egrave", + "iacute", + "icircumflex", + "idieresis", + "igrave", + "ntilde", + "oacute", + "ocircumflex", + "odieresis", + "ograve", + "otilde", + "scaron", + "uacute", + "ucircumflex", + "udieresis", + "ugrave", + "yacute", + "ydieresis", + "zcaron", +]; + +/// SIDs 379–390. +const TAIL: [&str; 12] = ["001.000", "001.001", "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold"]; + +/// The number of standard strings. +pub const COUNT: usize = 391; + +/// The glyph name a standard SID stands for. The expert-set names (229–378) are given as +/// `sid`, which only has to be consistent within one font (they are its own glyph names). +pub fn standard(sid: usize) -> Option { + if let Some(n) = NAMES.get(sid) { + return Some((*n).to_string()); + } + if let Some(n) = sid.checked_sub(379).and_then(|i| TAIL.get(i)) { + return Some((*n).to_string()); + } + (sid < COUNT).then(|| format!("sid{sid}")) +} diff --git a/crates/ps/src/read/composite.rs b/crates/ps/src/read/composite.rs new file mode 100644 index 0000000000..06ae3bc778 --- /dev/null +++ b/crates/ps/src/read/composite.rs @@ -0,0 +1,549 @@ +//! CFF font sets (`StartData`), CIDFonts, CMaps (`CIDInit`) and composite (Type 0) fonts. + +use std::rc::Rc; + +use crate::read::gstate::M; +use crate::read::interp::{Interp, R, err, matrix_obj}; +use crate::read::obj::{Key, Obj, PArr, PDict, PStr, name}; +use crate::read::type1::Glyph; + +/// A CMap: code space ranges, code → CID ranges (single codes are ranges of one), a parent. +#[derive(Debug, Default)] +pub struct CMap { + codespace: Vec<(Vec, Vec)>, + ranges: Vec<(Vec, Vec, u32)>, + parent: Option, + identity: bool, + /// Vertical writing (WMode 1). + pub vertical: bool, +} + +const MAX_CMAPS: usize = 10_000; +const MAX_RANGES: usize = 500_000; + +fn code_value(b: &[u8]) -> u64 { + b.iter().fold(0u64, |a, x| a << 8 | u64::from(*x)) +} + +fn in_range(code: &[u8], lo: &[u8], hi: &[u8]) -> bool { + code.len() == lo.len() && code.len() == hi.len() && code.iter().zip(lo).zip(hi).all(|((c, l), h)| c >= l && c <= h) +} + +impl Interp { + fn cmap_of(&self, d: &PDict) -> Option { + match d.get_name(b"CraftPsCMap") { + Some(Obj::Int(i)) => usize::try_from(i).ok(), + _ => None, + } + } + + /// The CMap the current dictionary is building (made on first use). + fn building(&mut self) -> R { + let d = self.current_dict()?; + if let Some(i) = self.cmap_of(&d) { + return Ok(i); + } + if self.cmaps.len() >= MAX_CMAPS { + return err("limitcheck"); + } + self.cmaps.push(CMap::default()); + let i = self.cmaps.len() - 1; + d.put_name(b"CraftPsCMap", Obj::Int(i as i64)); + Ok(i) + } + + /// CID for a code through CMap `i` (and its parents); None when unmapped. + fn cid(&self, i: usize, code: &[u8], depth: usize) -> Option { + let m = self.cmaps.get(i)?; + if m.identity { + return u32::try_from(code_value(code)).ok(); + } + for (lo, hi, start) in &m.ranges { + if in_range(code, lo, hi) { + return u32::try_from(u64::from(*start) + code_value(code).saturating_sub(code_value(lo))).ok(); + } + } + if depth < 8 { m.parent.and_then(|p| self.cid(p, code, depth + 1)) } else { None } + } + + /// How many ranges a lookup through CMap `i` may compare. + fn cmap_cost(&self, i: usize) -> u64 { + let mut n = 0u64; + let mut cur = Some(i); + for _ in 0..8 { + let Some(m) = cur.and_then(|c| self.cmaps.get(c)) else { break }; + n = n.saturating_add((m.ranges.len() + m.codespace.len()) as u64); + cur = m.parent; + } + n + } + + fn code_len(&self, i: usize, s: &[u8], depth: usize) -> Option { + let m = self.cmaps.get(i)?; + for n in 1..=4 { + let Some(code) = s.get(..n) else { break }; + if m.codespace.iter().any(|(lo, hi)| in_range(code, lo, hi)) { + return Some(n); + } + } + if depth < 8 { m.parent.and_then(|p| self.code_len(p, s, depth + 1)) } else { None } + } +} + +// ----- CIDInit procedures ----- + +pub fn begincmap(_ip: &mut Interp) -> R { + Ok(()) +} + +pub fn endcmap(ip: &mut Interp) -> R { + let i = ip.building()?; + let vertical = ip.current_dict()?.get_name(b"WMode").and_then(|o| o.num()) == Some(1.0); + if let Some(m) = ip.cmaps.get_mut(i) { + m.vertical = vertical; + } + Ok(()) +} + +pub fn usecmap(ip: &mut Interp) -> R { + let n = ip.pop()?; + let parent = find_cmap(ip, &n)?; + let i = ip.building()?; + if let Some(m) = ip.cmaps.get_mut(i) { + m.parent = Some(parent); + } + Ok(()) +} + +/// `n begin…` pushes a mark for its entries. +pub fn begin_block(ip: &mut Interp) -> R { + ip.pop_int()?; + ip.push(Obj::Mark) +} + +fn block(ip: &mut Interp) -> R> { + let i = crate::read::ops::basic::mark_pos(ip)?; + let items = ip.o.split_off(i + 1); + ip.o.pop(); + Ok(items) +} + +fn bytes_of(o: &Obj) -> R> { + match o { + Obj::Str(s, _) => Ok(s.bytes()), + _ => err("typecheck"), + } +} + +pub fn endcodespacerange(ip: &mut Interp) -> R { + let items = block(ip)?; + let i = ip.building()?; + for p in items.chunks(2) { + if let [lo, hi] = p { + let (lo, hi) = (bytes_of(lo)?, bytes_of(hi)?); + if let Some(m) = ip.cmaps.get_mut(i) { + m.codespace.push((lo, hi)); + } + } + } + Ok(()) +} + +fn add_ranges(ip: &mut Interp, triples: bool) -> R { + let items = block(ip)?; + let i = ip.building()?; + let step = if triples { 3 } else { 2 }; + for e in items.chunks(step) { + let (lo, hi, cid) = match (triples, e) { + (true, [lo, hi, cid]) => (bytes_of(lo)?, bytes_of(hi)?, cid), + (false, [code, cid]) => (bytes_of(code)?, bytes_of(code)?, cid), + _ => continue, + }; + let Some(cid) = cid.num() else { continue }; + if let Some(m) = ip.cmaps.get_mut(i) + && m.ranges.len() < MAX_RANGES + { + m.ranges.push((lo, hi, cid.max(0.0) as u32)); + } + } + Ok(()) +} + +pub fn endcidrange(ip: &mut Interp) -> R { + add_ranges(ip, true) +} + +pub fn endcidchar(ip: &mut Interp) -> R { + add_ranges(ip, false) +} + +/// bf (Unicode) and notdef blocks: read and dropped. +pub fn end_ignored(ip: &mut Interp) -> R { + block(ip).map(|_| ()) +} + +fn identity(ip: &mut Interp, n: &[u8]) -> PDict { + let d = PDict::new(4); + d.put_name(b"CMapName", Obj::lit(n)); + let vertical = n.ends_with(b"-V"); + ip.cmaps.push(CMap { codespace: vec![(vec![0, 0], vec![0xff, 0xff])], identity: true, vertical, ..Default::default() }); + d.put_name(b"CraftPsCMap", Obj::Int(ip.cmaps.len() as i64 - 1)); + d.put_name(b"WMode", Obj::Int(i64::from(n.ends_with(b"-V")))); + d +} + +/// A CMap by name or dictionary: as an index into `ip.cmaps`. +fn find_cmap(ip: &mut Interp, o: &Obj) -> R { + let d = match o { + Obj::Dict(d) => d.clone(), + n => { + let cats = category(ip, b"CMap"); + match cats.get(&n.key()) { + Some(Obj::Dict(d)) => d, + _ => { + // Unknown CMaps read two-byte codes as CIDs, as Identity does. + let d = identity(ip, &crate::read::ops::basic::text_of(n)); + cats.put(n.clone(), Obj::Dict(d.clone())); + d + } + } + } + }; + match ip.cmap_of(&d) { + Some(i) => Ok(i), + None => { + let i = ip.cmaps.len(); + ip.cmaps.push(CMap { identity: true, codespace: vec![(vec![0, 0], vec![0xff, 0xff])], ..Default::default() }); + d.put_name(b"CraftPsCMap", Obj::Int(i as i64)); + Ok(i) + } + } +} + +fn category(ip: &mut Interp, cat: &[u8]) -> PDict { + match ip.resources.get_name(cat) { + Some(Obj::Dict(d)) => d, + _ => { + let d = PDict::new(20); + ip.resources.put_name(cat, Obj::Dict(d.clone())); + d + } + } +} + +fn add_fid(ip: &mut Interp, d: &PDict) { + if !d.known(&Key::Name(name(b"FID"))) { + d.put_name(b"FID", Obj::FontId(ip.next_fid)); + ip.next_fid += 1; + } +} + +// ----- StartData ----- + +fn nums(v: &[f64]) -> Obj { + Obj::Array(PArr::new(v.iter().map(|x| Obj::Real(*x)).collect()), false) +} + +/// `/name length StartData`: the CFF font set in the next `length` bytes of the current file; +/// its fonts become Font (name-keyed) or CIDFont (CID-keyed) resources. +pub fn startdata(ip: &mut Interp) -> R { + let len = ip.pop_usize()?; + let _set_name = ip.pop()?; + if len > 256 << 20 { + return err("limitcheck"); + } + let file = + ip.e.iter() + .rev() + .find_map(|f| match f { + crate::read::interp::Frame::File { id } => Some(*id), + _ => None, + }) + .unwrap_or(ip.main); + ip.alloc(len)?; + let mut data = Vec::with_capacity(len); + while data.len() < len { + match ip.srcs.read(file) { + Some(c) => data.push(c), + None => break, + } + } + // StartData ends the FontSetInit dictionary its program began. + let fsi = match category(ip, b"ProcSet").get_name(b"FontSetInit") { + Some(Obj::Dict(d)) => Some(d), + _ => None, + }; + if let (Some(top), Some(fsi)) = (ip.d.last(), fsi) + && top.same(&fsi) + && ip.d.len() > 3 + { + ip.d.pop(); + } + // Parsing walks the data, and a font set's glyph tables are as large again. + ip.charge_bytes(data.len())?; + ip.alloc(data.len())?; + let Some(set) = crate::read::cff::parse(data) else { + ip.warnings.push("unreadable CFF font set".into()); + return Ok(()); + }; + let set = Rc::new(set); + let si = ip.cff.len(); + ip.cff.push(set.clone()); + for (k, f) in set.fonts.iter().enumerate() { + let d = PDict::new(16); + let reference = Obj::Array(PArr::new(vec![Obj::Int(si as i64), Obj::Int(k as i64)]), false); + d.put_name(b"CraftPsCff", reference); + d.put_name(b"FontMatrix", nums(&f.matrix)); + d.put_name(b"FontBBox", nums(&f.bbox)); + d.put_name(b"PaintType", Obj::Int(0)); + if let Some((reg, ord, sup)) = &f.ros { + d.put_name(b"CIDFontName", Obj::lit(&f.name)); + d.put_name(b"CIDFontType", Obj::Int(0)); + d.put_name(b"FontType", Obj::Int(9)); + let info = PDict::new(3); + info.put_name(b"Registry", Obj::str(reg)); + info.put_name(b"Ordering", Obj::str(ord)); + info.put_name(b"Supplement", Obj::Int(*sup)); + d.put_name(b"CIDSystemInfo", Obj::Dict(info)); + add_fid(ip, &d); + category(ip, b"CIDFont").put(Obj::lit(&f.name), Obj::Dict(d)); + } else { + d.put_name(b"FontName", Obj::lit(&f.name)); + d.put_name(b"FontType", Obj::Int(2)); + let mut enc = vec![Obj::lit(b".notdef"); 256]; + for (code, gid) in &f.encoding { + if let (Some(slot), Some(n)) = (enc.get_mut(usize::from(*code)), f.names.get(*gid as usize)) { + *slot = Obj::lit(n); + } + } + d.put_name(b"Encoding", Obj::Array(PArr::new(enc), false)); + let cs = PDict::new(f.names.len()); + for (g, n) in f.names.iter().enumerate() { + cs.put_name(n, Obj::Int(g as i64)); + } + d.put_name(b"CharStrings", Obj::Dict(cs)); + d.put_name(b"Private", Obj::Dict(PDict::new(1))); + add_fid(ip, &d); + ip.fontdir.put(Obj::lit(&f.name), Obj::Dict(d.clone())); + category(ip, b"Font").put(Obj::lit(&f.name), Obj::Dict(d)); + } + } + Ok(()) +} + +/// The CFF font set and font index behind a font dictionary. +pub fn cff_of(ip: &Interp, d: &PDict) -> Option<(Rc, usize)> { + let Some(Obj::Array(a, _)) = d.get_name(b"CraftPsCff") else { return None }; + let si = a.get(0)?.num()? as usize; + let k = a.get(1)?.num()? as usize; + Some((ip.cff.get(si)?.clone(), k)) +} + +// ----- composite fonts ----- + +/// `name cmap [cidfonts] composefont`: a Type 0 font. +pub fn composefont(ip: &mut Interp) -> R { + let fonts = ip.pop_arr()?; + let cmap = ip.pop()?; + let n = ip.pop()?; + let font = compose(ip, &n, &cmap, fonts.items())?; + ip.push(Obj::Dict(font)) +} + +fn compose(ip: &mut Interp, n: &Obj, cmap: &Obj, descendants: Vec) -> R { + let i = find_cmap(ip, cmap)?; + let mut desc = Vec::new(); + for f in descendants { + desc.push(match f { + Obj::Dict(d) => Obj::Dict(d), + k => match category(ip, b"CIDFont").get(&k.key()) { + Some(d @ Obj::Dict(_)) => d, + _ => Obj::Dict(crate::read::font::find_font(ip, &k)), + }, + }); + } + let d = PDict::new(10); + d.put_name(b"FontName", n.clone()); + d.put_name(b"FontType", Obj::Int(0)); + d.put_name(b"FMapType", Obj::Int(9)); + d.put_name(b"FontMatrix", matrix_obj(&M::ID)); + d.put_name(b"Encoding", Obj::Array(PArr::new(vec![Obj::Int(0)]), false)); + d.put_name(b"FDepVector", Obj::Array(PArr::new(desc), false)); + let cm = PDict::new(2); + cm.put_name(b"CraftPsCMap", Obj::Int(i as i64)); + d.put_name(b"CMap", Obj::Dict(cm)); + let vertical = ip.cmaps.get(i).is_some_and(|m| m.vertical); + d.put_name(b"WMode", Obj::Int(i64::from(vertical))); + add_fid(ip, &d); + ip.fontdir.put(n.clone(), Obj::Dict(d.clone())); + Ok(d) +} + +/// `findfont` of `CIDFont-CMap` (as Adobe interpreters do): the two composed. +pub fn compose_by_name(ip: &mut Interp, n: &[u8]) -> Option { + let cats = category(ip, b"CIDFont"); + for (k, v) in cats.entries() { + let kn = crate::read::ops::basic::text_of(&k); + if n.len() > kn.len() + 1 && n.starts_with(&kn) && n.get(kn.len()) == Some(&b'-') { + let cmap = n.get(kn.len() + 1..)?; + return compose(ip, &Obj::lit(n), &Obj::lit(cmap), vec![v]).ok(); + } + } + None +} + +/// Whether a font writes vertically (its WMode, or its CMap's). +pub fn vertical(ip: &Interp, font: &PDict) -> bool { + match font.get_name(b"WMode").and_then(|o| o.num()) { + Some(w) => w == 1.0, + None => match font.get_name(b"CMap") { + Some(Obj::Dict(d)) => ip.cmap_of(&d).and_then(|i| ip.cmaps.get(i)).is_some_and(|m| m.vertical), + _ => false, + }, + } +} + +/// A CIDFont's vertical metrics for a CID in glyph space: the advance (w1) and the position +/// vector (v) from the horizontal origin to the vertical one. `Metrics2` gives them per CID; +/// otherwise the defaults: one em down, and v = (half the width, 0.88 em). +pub fn vertical_metrics(cidfont: &PDict, cid: u32, w0: f64, em: f64) -> ((f64, f64), (f64, f64)) { + if let Some(Obj::Dict(m)) = cidfont.get_name(b"Metrics2") + && let Some(Obj::Array(a, _)) = m.get(&Key::Int(i64::from(cid))) + { + let v: Vec = a.items().iter().filter_map(Obj::num).collect(); + if let [w1x, w1y, vx, vy] = v[..] { + return ((w1x, w1y), (vx, vy)); + } + } + ((0.0, -em), (w0 / 2.0, 0.88 * em)) +} + +/// What one glyph of a shown string draws with. +pub enum Key2 { + Code(u8), + Cid(u32), +} + +/// The glyphs of a string in a Type 0 font: (code, leaf font, glyph → text-space matrix, key). +pub fn decode(ip: &mut Interp, font: &PDict, s: &[u8]) -> R> { + let fm = font_matrix(font); + let desc: Vec = match font.get_name(b"FDepVector") { + Some(Obj::Array(a, _)) => a.items().into_iter().filter_map(|o| if let Obj::Dict(d) = o { Some(d) } else { None }).collect(), + _ => Vec::new(), + }; + let Some(first) = desc.first().cloned() else { return err("invalidfont") }; + let fmap = font.get_name(b"FMapType").and_then(|o| o.num()).unwrap_or(9.0) as i64; + let mut out = Vec::new(); + if fmap == 2 { + // 8/8 mapping: a font number, then a code in it. + for p in s.chunks(2) { + if let [f, c] = p { + let leaf = desc.get(usize::from(*f)).cloned().unwrap_or_else(|| first.clone()); + let m = font_matrix(&leaf).then(&fm); + out.push((u32::from(*c), leaf, m, Key2::Code(*c))); + } + } + return Ok(out); + } + let cmap = match font.get_name(b"CMap") { + Some(Obj::Dict(d)) => ip.cmap_of(&d), + _ => None, + }; + let m = font_matrix(&first).then(&fm); + let mut i = 0; + // Each code is looked up through the CMap's ranges (and its parents'). + let per_code = cmap.map_or(1, |c| ip.cmap_cost(c)) / 16 + 1; + while i < s.len() { + ip.charge(per_code)?; + let rest = s.get(i..).unwrap_or(&[]); + let n = cmap.and_then(|c| ip.code_len(c, rest, 0)).unwrap_or(2).min(rest.len()).max(1); + let code = rest.get(..n).unwrap_or(&[]); + let cid = cmap.and_then(|c| ip.cid(c, code, 0)).unwrap_or(0); + out.push((u32::try_from(code_value(code)).unwrap_or(0), first.clone(), m, Key2::Cid(cid))); + i += n; + } + Ok(out) +} + +fn font_matrix(d: &PDict) -> M { + match d.get_name(b"FontMatrix") { + Some(Obj::Array(a, _)) => crate::read::interp::matrix_of(&a).unwrap_or(M([0.001, 0.0, 0.0, 0.001, 0.0, 0.0])), + _ => M([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]), + } +} + +/// A CID's glyph in a CIDFont: CFF (CIDFontType 0) or TrueType (CIDFontType 2, through CIDMap). +pub fn cid_glyph(ip: &mut Interp, cidfont: &PDict, cid: u32) -> Option { + if let Some((set, k)) = cff_of(ip, cidfont) { + let gid = set.gid_of_cid(k, cid).unwrap_or(0); + return set.glyph(k, gid); + } + if cidfont.get_name(b"CIDFontType").and_then(|o| o.num()) == Some(2.0) { + let gid = match cidfont.get_name(b"CIDMap") { + Some(Obj::Int(off)) => u32::try_from(i64::from(cid) + off).ok()?, + Some(Obj::Dict(m)) => m.get(&Key::Int(i64::from(cid))).and_then(|o| o.num()).map_or(0, |v| v as u32), + Some(Obj::Str(s, _)) => gid_from(&[&s], cidfont, cid), + Some(Obj::Array(a, _)) => { + let strs: Vec = a.items().into_iter().filter_map(|o| if let Obj::Str(s, _) = o { Some(s) } else { None }).collect(); + gid_from(&strs.iter().collect::>(), cidfont, cid) + } + _ => cid, + }; + let data = crate::read::type42::sfnts(cidfont)?; + return crate::read::type42::glyph(&data, gid); + } + None +} + +/// A glyph index read from CIDMap strings (GDBytes bytes per CID, across the strings). +fn gid_from(strs: &[&PStr], cidfont: &PDict, cid: u32) -> u32 { + let gd = cidfont.get_name(b"GDBytes").and_then(|o| o.num()).unwrap_or(2.0).clamp(1.0, 4.0) as usize; + let mut at = (cid as usize).saturating_mul(gd); + for s in strs { + if at < s.len { + return (0..gd).fold(0u32, |a, k| a << 8 | u32::from(s.get(at + k).unwrap_or(0))); + } + at -= s.len; + } + 0 +} + +/// The ProcSets EPS files ask for, and the identity CMaps. +pub fn install(ip: &mut Interp) { + let procsets = category(ip, b"ProcSet"); + let fsi = PDict::new(2); + if let Some(op) = crate::read::ops::op_id(b"StartData") { + fsi.put_name(b"StartData", Obj::Op(op)); + } + procsets.put_name(b"FontSetInit", Obj::Dict(fsi)); + let cid = PDict::new(24); + for n in [ + "begincmap", + "endcmap", + "usecmap", + "begincodespacerange", + "endcodespacerange", + "begincidrange", + "endcidrange", + "begincidchar", + "endcidchar", + "beginbfchar", + "endbfchar", + "beginbfrange", + "endbfrange", + "beginnotdefrange", + "endnotdefrange", + "beginnotdefchar", + "endnotdefchar", + ] { + if let Some(op) = crate::read::ops::op_id(n.as_bytes()) { + cid.put_name(n.as_bytes(), Obj::Op(op)); + } + } + procsets.put_name(b"CIDInit", Obj::Dict(cid)); + let cmaps = category(ip, b"CMap"); + for n in [&b"Identity-H"[..], b"Identity-V"] { + let d = identity(ip, n); + cmaps.put_name(n, Obj::Dict(d)); + } +} diff --git a/crates/ps/src/read/encodings.rs b/crates/ps/src/read/encodings.rs new file mode 100644 index 0000000000..7ed3ff2530 --- /dev/null +++ b/crates/ps/src/read/encodings.rs @@ -0,0 +1,318 @@ +//! The two encodings every interpreter provides (PostScript Language Reference, Appendix E). + +/// Glyph names for codes 32–126, shared by both encodings except where noted. +const ASCII: [&str; 95] = [ + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", +]; + +const STANDARD_HIGH: &[(u8, &str)] = &[ + (161, "exclamdown"), + (162, "cent"), + (163, "sterling"), + (164, "fraction"), + (165, "yen"), + (166, "florin"), + (167, "section"), + (168, "currency"), + (169, "quotesingle"), + (170, "quotedblleft"), + (171, "guillemotleft"), + (172, "guilsinglleft"), + (173, "guilsinglright"), + (174, "fi"), + (175, "fl"), + (177, "endash"), + (178, "dagger"), + (179, "daggerdbl"), + (180, "periodcentered"), + (182, "paragraph"), + (183, "bullet"), + (184, "quotesinglbase"), + (185, "quotedblbase"), + (186, "quotedblright"), + (187, "guillemotright"), + (188, "ellipsis"), + (189, "perthousand"), + (191, "questiondown"), + (193, "grave"), + (194, "acute"), + (195, "circumflex"), + (196, "tilde"), + (197, "macron"), + (198, "breve"), + (199, "dotaccent"), + (200, "dieresis"), + (202, "ring"), + (203, "cedilla"), + (205, "hungarumlaut"), + (206, "ogonek"), + (207, "caron"), + (208, "emdash"), + (225, "AE"), + (227, "ordfeminine"), + (232, "Lslash"), + (233, "Oslash"), + (234, "OE"), + (235, "ordmasculine"), + (241, "ae"), + (245, "dotlessi"), + (248, "lslash"), + (249, "oslash"), + (250, "oe"), + (251, "germandbls"), +]; + +/// ISOLatin1Encoding: accents at 144–159, then ISO 8859-1 from 160. +const LATIN1_HIGH: [&str; 112] = [ + "dotlessi", + "grave", + "acute", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "dieresis", + ".notdef", + "ring", + "cedilla", + ".notdef", + "hungarumlaut", + "ogonek", + "caron", + "space", + "exclamdown", + "cent", + "sterling", + "currency", + "yen", + "brokenbar", + "section", + "dieresis", + "copyright", + "ordfeminine", + "guillemotleft", + "logicalnot", + "hyphen", + "registered", + "macron", + "degree", + "plusminus", + "twosuperior", + "threesuperior", + "acute", + "mu", + "paragraph", + "periodcentered", + "cedilla", + "onesuperior", + "ordmasculine", + "guillemotright", + "onequarter", + "onehalf", + "threequarters", + "questiondown", + "Agrave", + "Aacute", + "Acircumflex", + "Atilde", + "Adieresis", + "Aring", + "AE", + "Ccedilla", + "Egrave", + "Eacute", + "Ecircumflex", + "Edieresis", + "Igrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Eth", + "Ntilde", + "Ograve", + "Oacute", + "Ocircumflex", + "Otilde", + "Odieresis", + "multiply", + "Oslash", + "Ugrave", + "Uacute", + "Ucircumflex", + "Udieresis", + "Yacute", + "Thorn", + "germandbls", + "agrave", + "aacute", + "acircumflex", + "atilde", + "adieresis", + "aring", + "ae", + "ccedilla", + "egrave", + "eacute", + "ecircumflex", + "edieresis", + "igrave", + "iacute", + "icircumflex", + "idieresis", + "eth", + "ntilde", + "ograve", + "oacute", + "ocircumflex", + "otilde", + "odieresis", + "divide", + "oslash", + "ugrave", + "uacute", + "ucircumflex", + "udieresis", + "yacute", + "thorn", + "ydieresis", +]; + +pub fn standard() -> Vec<&'static str> { + let mut v = vec![".notdef"; 256]; + for (i, n) in ASCII.iter().enumerate() { + if let Some(slot) = v.get_mut(32 + i) { + *slot = n; + } + } + for (c, n) in STANDARD_HIGH { + if let Some(slot) = v.get_mut(usize::from(*c)) { + *slot = n; + } + } + v +} + +pub fn iso_latin1() -> Vec<&'static str> { + let mut v = vec![".notdef"; 256]; + for (i, n) in ASCII.iter().enumerate() { + if let Some(slot) = v.get_mut(32 + i) { + *slot = n; + } + } + // Latin-1 differs from Standard at the minus sign. + if let Some(slot) = v.get_mut(45) { + *slot = "minus"; + } + for (i, n) in LATIN1_HIGH.iter().enumerate() { + if let Some(slot) = v.get_mut(144 + i) { + *slot = n; + } + } + v +} + +#[cfg(test)] +mod tests { + #[test] + fn encodings_place_their_glyphs() { + let s = super::standard(); + assert_eq!((s[65], s[39], s[251], s[200]), ("A", "quoteright", "germandbls", "dieresis")); + let l = super::iso_latin1(); + assert_eq!((l[228], l[45], l[173], l[223], l[255]), ("adieresis", "minus", "hyphen", "germandbls", "ydieresis")); + } +} diff --git a/crates/ps/src/read/font.rs b/crates/ps/src/read/font.rs new file mode 100644 index 0000000000..e5fd42cdeb --- /dev/null +++ b/crates/ps/src/read/font.rs @@ -0,0 +1,523 @@ +//! Fonts: dictionaries, scaling, and showing text. Type 3 fonts draw through their BuildGlyph +//! or BuildChar procedures; other font types advance by their widths (or an estimate) without +//! drawing until their glyph programs are read. + +use crate::read::gstate::M; +use crate::read::interp::{Interp, PsErr, R, err, matrix_obj}; +use crate::read::obj::{Key, Obj, PArr, PDict, name}; + +fn font_matrix(d: &PDict) -> M { + match d.get_name(b"FontMatrix") { + Some(Obj::Array(a, _)) => crate::read::interp::matrix_of(&a).unwrap_or(M([0.001, 0.0, 0.0, 0.001, 0.0, 0.0])), + _ => M([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]), + } +} + +/// A stand-in for a font the file doesn't carry: a Type 1 shell with StandardEncoding. +fn substitute(ip: &mut Interp, font_name: &[u8]) -> PDict { + let d = PDict::new(12); + d.put_name(b"FontName", Obj::lit(font_name)); + d.put_name(b"FontType", Obj::Int(1)); + d.put_name(b"FontMatrix", matrix_obj(&M([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]))); + d.put_name(b"FontBBox", Obj::Array(PArr::new(vec![Obj::Int(0), Obj::Int(-200), Obj::Int(1000), Obj::Int(900)]), false)); + d.put_name(b"PaintType", Obj::Int(0)); + if let Some(enc) = ip.systemdict.get_name(b"StandardEncoding") { + d.put_name(b"Encoding", enc); + } + d.put_name(b"CharStrings", Obj::Dict(PDict::new(1))); + d.put_name(b"Private", Obj::Dict(PDict::new(1))); + d.put_name(b"CraftPsSubstitute", Obj::Bool(true)); + add_fid(ip, &d); + d +} + +fn add_fid(ip: &mut Interp, d: &PDict) { + if !d.known(&Key::Name(name(b"FID"))) { + d.put_name(b"FID", Obj::FontId(ip.next_fid)); + ip.next_fid += 1; + } +} + +pub fn find_font(ip: &mut Interp, key: &Obj) -> PDict { + if let Some(Obj::Dict(d)) = ip.fontdir.get(&key.key()) { + return d; + } + if let Some(Obj::Dict(fonts)) = ip.resources.get_name(b"Font") + && let Some(Obj::Dict(d)) = fonts.get(&key.key()) + { + return d; + } + let n = crate::read::ops::basic::text_of(key); + if let Some(d) = crate::read::composite::compose_by_name(ip, &n) { + return d; + } + let d = substitute(ip, &n); + ip.fontdir.put(key.clone(), Obj::Dict(d.clone())); + d +} + +pub fn findfont(ip: &mut Interp) -> R { + let k = ip.pop()?; + let d = find_font(ip, &k); + ip.push(Obj::Dict(d)) +} + +pub fn definefont(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + let k = ip.pop()?; + add_fid(ip, &d); + ip.fontdir.put(k, Obj::Dict(d.clone())); + ip.push(Obj::Dict(d)) +} + +pub fn undefinefont(ip: &mut Interp) -> R { + let k = ip.pop()?; + ip.fontdir.remove(&k.key()); + Ok(()) +} + +/// A copy of a font dictionary with its FontMatrix replaced (and a new FID). +fn transformed(ip: &mut Interp, d: &PDict, m: &M) -> PDict { + let copy = PDict::new(d.len() + 2); + for (k, v) in d.entries() { + copy.put(k, v); + } + copy.put_name(b"FontMatrix", matrix_obj(&font_matrix(d).then(m))); + copy.put_name(b"FID", Obj::FontId(ip.next_fid)); + ip.next_fid += 1; + copy +} + +pub fn scalefont(ip: &mut Interp) -> R { + let s = ip.pop_num()?; + let d = ip.pop_dict()?; + let f = transformed(ip, &d, &M([s, 0.0, 0.0, s, 0.0, 0.0])); + ip.push(Obj::Dict(f)) +} + +pub fn makefont(ip: &mut Interp) -> R { + let m = ip.pop_matrix()?; + let d = ip.pop_dict()?; + let f = transformed(ip, &d, &m); + ip.push(Obj::Dict(f)) +} + +pub fn setfont(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + ip.gs.font = Some(d); + Ok(()) +} + +pub fn currentfont(ip: &mut Interp) -> R { + let f = match ip.gs.font.clone() { + Some(f) => f, + None => find_font(ip, &Obj::lit(b"Courier")), + }; + ip.push(Obj::Dict(f)) +} + +pub fn selectfont(ip: &mut Interp) -> R { + let m = match ip.pop()? { + Obj::Array(a, _) => crate::read::interp::matrix_of(&a)?, + o => { + let s = o.num().ok_or(PsErr::Ps("typecheck"))?; + M([s, 0.0, 0.0, s, 0.0, 0.0]) + } + }; + let k = ip.pop()?; + let d = match k { + Obj::Dict(d) => d, + k => find_font(ip, &k), + }; + let f = transformed(ip, &d, &m); + ip.gs.font = Some(f); + Ok(()) +} + +/// The glyph name for a character code through the font's Encoding. +fn glyph_name(font: &PDict, code: u8) -> Vec { + match font.get_name(b"Encoding") { + Some(Obj::Array(a, _)) => match a.get(usize::from(code)) { + Some(Obj::Name(n, _)) => n.to_vec(), + _ => b".notdef".to_vec(), + }, + _ => b".notdef".to_vec(), + } +} + +/// A glyph's advance from the font's Metrics or CharStrings-free estimate, in glyph space. +fn estimated_width(font: &PDict, code: u8) -> (f64, f64) { + let g = glyph_name(font, code); + if let Some(Obj::Dict(m)) = font.get_name(b"Metrics") + && let Some(v) = m.get(&Key::Name(name(&g))) + { + match v { + Obj::Array(a, _) if a.len >= 2 => { + let n: Vec = a.items().iter().filter_map(Obj::num).collect(); + // The last two numbers are the width (an entry without two numbers gives none). + let w = n.len().checked_sub(2).and_then(|i| n.get(i)).copied().unwrap_or(0.0); + return (w, n.last().copied().unwrap_or(0.0)); + } + o => return (o.num().unwrap_or(0.0), 0.0), + } + } + // An average glyph: half the em, as glyph space usually has 1000 units to it. + let em = 1.0 / font_matrix(font).0[0].abs().max(1e-6); + (if code == b' ' { 0.25 } else { 0.5 } * em, 0.0) +} + +/// A Type 1 glyph of `font` (cached), None for fonts without charstrings. +fn outline(ip: &mut Interp, font: &PDict, code: u8) -> Option> { + if font.get_name(b"CraftPsSubstitute").is_some() { + return None; + } + let fid = match font.get_name(b"FID") { + Some(Obj::FontId(f)) => f, + _ => 0, + }; + let g = glyph_name(font, code); + if let Some(hit) = ip.glyphs.get(&(fid, g.clone())) { + return Some(hit.clone()); + } + let glyph = if let Some((set, k)) = crate::read::composite::cff_of(ip, font) { + let gid = match font.get_name(b"CharStrings") { + Some(Obj::Dict(cs)) => cs.get(&Key::Name(name(&g))).and_then(|o| o.num()).map_or(0, |v| v as u32), + _ => 0, + }; + std::rc::Rc::new(set.glyph(k, gid)?) + } else if font.get_name(b"FontType").and_then(|o| o.num()) == Some(42.0) { + let data = match ip.sfnts.get(&fid) { + Some(d) => d.clone(), + None => { + let d = std::rc::Rc::new(crate::read::type42::sfnts(font)?); + if ip.sfnts.len() < 256 { + ip.sfnts.insert(fid, d.clone()); + } + d + } + }; + std::rc::Rc::new(crate::read::type42::glyph(&data, crate::read::type42::glyph_index(font, &g))?) + } else { + let std = crate::read::encodings::standard(); + std::rc::Rc::new(crate::read::type1::glyph(font, &g, &std)?) + }; + // Running the glyph's program was work; failing to charge it ends the run at the next step. + let _ = ip.charge(glyph.cost as u64 / 4 + 1); + let _ = ip.alloc(glyph.path.len().saturating_mul(crate::read::interp::SEG_BYTES)); + if ip.glyphs.len() < 100_000 { + ip.glyphs.insert((fid, g), glyph.clone()); + } + Some(glyph) +} + +/// A CID's glyph in a CIDFont (cached by font and CID). +fn cid_outline(ip: &mut Interp, font: &PDict, cid: u32) -> Option> { + let fid = match font.get_name(b"FID") { + Some(Obj::FontId(f)) => f, + _ => 0, + }; + let key = (fid, format!("\0cid{cid}").into_bytes()); + if let Some(hit) = ip.glyphs.get(&key) { + return Some(hit.clone()); + } + let g = std::rc::Rc::new(crate::read::composite::cid_glyph(ip, font, cid)?); + let _ = ip.charge(g.cost as u64 / 4 + 1); + let _ = ip.alloc(g.path.len().saturating_mul(crate::read::interp::SEG_BYTES)); + if ip.glyphs.len() < 100_000 { + ip.glyphs.insert(key, g.clone()); + } + Some(g) +} + +/// Where a glyph at the current point lands: glyph space (through `fm`) → device space. `v` +/// moves the glyph's origin (vertical writing hangs a glyph from its vertical origin). +fn glyph_to_device(ip: &Interp, fm: &M, v: (f64, f64)) -> R { + let (x, y) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let (ux, uy) = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?.apply(x, y); + Ok(M([1.0, 0.0, 0.0, 1.0, -v.0, -v.1]).then(fm).then(&M([1.0, 0.0, 0.0, 1.0, ux, uy])).then(&ip.gs.ctm)) +} + +/// Run one Type 3 glyph at the current point: its advance in glyph space. +fn type3_glyph(ip: &mut Interp, font: &PDict, code: u8) -> R<(f64, f64)> { + let (x, y) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let fm = font_matrix(font); + ip.gsave()?; + let r = (|| -> R<(f64, f64)> { + let (ux, uy) = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?.apply(x, y); + ip.gs.ctm = fm.then(&M([1.0, 0.0, 0.0, 1.0, ux, uy]).then(&ip.gs.ctm)); + ip.gs.path.clear(); + ip.gs.cur = None; + ip.gs.char_width = None; + if let Some(bg) = font.get_name(b"BuildGlyph") { + ip.push(Obj::Dict(font.clone()))?; + ip.push(Obj::lit(&glyph_name(font, code)))?; + ip.call(bg)?; + } else if let Some(bc) = font.get_name(b"BuildChar") { + ip.push(Obj::Dict(font.clone()))?; + ip.push(Obj::Int(i64::from(code)))?; + ip.call(bc)?; + } + Ok(ip.gs.char_width.unwrap_or((0.0, 0.0))) + })(); + ip.grestore(); + r +} + +/// What a glyph adds to (or, for `xshow`, replaces) its advance: given the code and its index. +type PerGlyph<'a> = dyn FnMut(&mut Interp, u32, usize) -> R<(f64, f64)> + 'a; + +/// Show (or, with `draw` false, measure) a string: the total advance in user space. +/// `per_char` gives each glyph's extra displacement (or, with `replace`, its whole advance). +fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: &mut PerGlyph<'_>) -> R<(f64, f64)> { + let font = match ip.gs.font.clone() { + Some(f) => f, + None => return err("invalidfont"), + }; + let vertical = crate::read::composite::vertical(ip, &font); + // The list of glyphs is made first: at most one per byte. + ip.alloc(s.len().saturating_mul(GLYPH_JOB_BYTES))?; + let jobs: Vec<(u32, PDict, M, crate::read::composite::Key2)> = if font.get_name(b"FontType").and_then(|o| o.num()) == Some(0.0) { + crate::read::composite::decode(ip, &font, s)? + } else { + let fm = font_matrix(&font); + s.iter().map(|c| (u32::from(*c), font.clone(), fm, crate::read::composite::Key2::Code(*c))).collect() + }; + let mut total = (0.0, 0.0); + for (i, (code, leaf, fm, key)) in jobs.into_iter().enumerate() { + ip.charge(GLYPH_STEPS)?; + let type3 = leaf.get_name(b"FontType").and_then(|o| o.num()) == Some(3.0); + let w = match key { + crate::read::composite::Key2::Code(c) if type3 => { + if draw && ip.gs.cur.is_some() { + type3_glyph(ip, &leaf, c)? + } else { + let saved = ip.gs.nulldev; + ip.gs.nulldev = true; + if ip.gs.cur.is_none() { + ip.gs.cur = Some((0.0, 0.0)); + } + let w = type3_glyph(ip, &leaf, c); + ip.gs.nulldev = saved; + w? + } + } + key => { + let cid = match key { + crate::read::composite::Key2::Cid(cid) => Some(cid), + crate::read::composite::Key2::Code(_) => None, + }; + let g = match key { + crate::read::composite::Key2::Code(c) => outline(ip, &leaf, c), + crate::read::composite::Key2::Cid(cid) => cid_outline(ip, &leaf, cid), + }; + // Vertical writing: the glyph hangs from its vertical origin and advances down. + let em = 1.0 / font_matrix(&leaf).0[3].abs().max(1e-9); + let vmetrics = match (vertical, &g) { + (true, Some(g)) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), g.width.0, em)), + (true, None) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), em, em)), + _ => None, + }; + let origin = vmetrics.map_or((0.0, 0.0), |(_, v)| v); + match g { + Some(g) => { + if draw && !g.path.is_empty() && ip.gs.cur.is_some() { + ip.charge_path(g.path.len())?; + let m = glyph_to_device(ip, &fm, origin)?; + let path: Vec = g.path.iter().map(|s| s.map(&m)).collect(); + if ip.charpath { + ip.gs.path.extend(path); + } else { + crate::read::ops::gfx::fill_path(ip, &path, false)?; + } + } + match (vmetrics, leaf.get_name(b"Metrics"), key_code(code)) { + (Some((w1, _)), _, _) => w1, + (None, Some(_), Some(c)) => estimated_width(&leaf, c), + _ => g.width, + } + } + None => match vmetrics { + Some((w1, _)) => w1, + None => estimated_width(&leaf, key_code(code).unwrap_or(0)), + }, + } + } + }; + let (wx, wy) = fm.delta(w.0, w.1); + let (ex, ey) = per_char(ip, code, i)?; + let (dx, dy) = if replace { (ex, ey) } else { (wx + ex, wy + ey) }; + total = (total.0 + dx, total.1 + dy); + if draw && let Some((cx, cy)) = ip.gs.cur { + let (ddx, ddy) = ip.gs.ctm.delta(dx, dy); + ip.gs.cur = Some((cx + ddx, cy + ddy)); + } + } + Ok(total) +} + +/// What showing one glyph costs, in steps (it takes about ten times a plain operator), and the +/// memory its place in the list of glyphs takes. +const GLYPH_STEPS: u64 = 10; +const GLYPH_JOB_BYTES: usize = 96; + +fn key_code(code: u32) -> Option { + u8::try_from(code).ok() +} + +fn show_string(ip: &mut Interp, s: &[u8], draw: bool, per_char: &mut PerGlyph<'_>) -> R<(f64, f64)> { + show_glyphs(ip, s, draw, false, per_char) +} + +pub fn show(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + if ip.gs.cur.is_none() { + return err("nocurrentpoint"); + } + show_string(ip, &s, true, &mut |_, _, _| Ok((0.0, 0.0))).map(|_| ()) +} + +pub fn ashow(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let ay = ip.pop_num()?; + let ax = ip.pop_num()?; + show_string(ip, &s, true, &mut |_, _, _| Ok((ax, ay))).map(|_| ()) +} + +pub fn widthshow(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let ch = ip.pop_int()?; + let cy = ip.pop_num()?; + let cx = ip.pop_num()?; + show_string(ip, &s, true, &mut |_, c, _| Ok(if i64::from(c) == ch { (cx, cy) } else { (0.0, 0.0) })).map(|_| ()) +} + +pub fn awidthshow(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let ay = ip.pop_num()?; + let ax = ip.pop_num()?; + let ch = ip.pop_int()?; + let cy = ip.pop_num()?; + let cx = ip.pop_num()?; + show_string(ip, &s, true, &mut |_, c, _| Ok(if i64::from(c) == ch { (cx + ax, cy + ay) } else { (ax, ay) })).map(|_| ()) +} + +/// `xshow`, `yshow`, `xyshow`: explicit displacements replace the widths. +fn explicit_show(ip: &mut Interp, xs: bool, ys: bool) -> R { + let disp: Vec = match ip.pop()? { + Obj::Array(a, _) => a.items().iter().filter_map(Obj::num).collect(), + // An encoded number string is rare in EPS: the glyphs' own widths then. + Obj::Str(_, _) => Vec::new(), + _ => return err("typecheck"), + }; + let s = ip.pop_str()?.bytes(); + if ip.gs.cur.is_none() { + return err("nocurrentpoint"); + } + let step = if xs && ys { 2 } else { 1 }; + show_glyphs(ip, &s, true, true, &mut |_, _, i| { + let d = |k: usize| disp.get(i * step + k).copied().unwrap_or(0.0); + Ok(match (xs, ys) { + (true, true) => (d(0), d(1)), + (true, false) => (d(0), 0.0), + _ => (0.0, d(0)), + }) + }) + .map(|_| ()) +} + +pub fn xshow(ip: &mut Interp) -> R { + explicit_show(ip, true, false) +} + +pub fn yshow(ip: &mut Interp) -> R { + explicit_show(ip, false, true) +} + +pub fn xyshow(ip: &mut Interp) -> R { + explicit_show(ip, true, true) +} + +pub fn kshow(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let proc = ip.pop()?; + for (i, c) in s.iter().enumerate() { + show_string(ip, &[*c], true, &mut |_, _, _| Ok((0.0, 0.0)))?; + if let Some(next) = s.get(i + 1) { + ip.push(Obj::Int(i64::from(*c)))?; + ip.push(Obj::Int(i64::from(*next)))?; + ip.call(proc.clone())?; + } + } + Ok(()) +} + +pub fn cshow(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let proc = ip.pop()?; + for c in s { + let (wx, wy) = show_string(ip, &[c], false, &mut |_, _, _| Ok((0.0, 0.0)))?; + ip.push(Obj::Int(i64::from(c)))?; + ip.push(Obj::Real(wx))?; + ip.push(Obj::Real(wy))?; + ip.call(proc.clone())?; + } + Ok(()) +} + +pub fn glyphshow(ip: &mut Interp) -> R { + let _ = ip.pop()?; + // Advance by an average glyph. + let font = ip.gs.font.clone().ok_or(PsErr::Ps("invalidfont"))?; + let (wx, wy) = font_matrix(&font).delta(500.0, 0.0); + if let Some((cx, cy)) = ip.gs.cur { + let (dx, dy) = ip.gs.ctm.delta(wx, wy); + ip.gs.cur = Some((cx + dx, cy + dy)); + } + Ok(()) +} + +pub fn stringwidth(ip: &mut Interp) -> R { + let s = ip.pop_str()?.bytes(); + let (wx, wy) = show_string(ip, &s, false, &mut |_, _, _| Ok((0.0, 0.0)))?; + ip.push(Obj::Real(wx))?; + ip.push(Obj::Real(wy)) +} + +pub fn charpath(ip: &mut Interp) -> R { + let _stroke = ip.pop_bool()?; + let s = ip.pop_str()?.bytes(); + let saved = ip.charpath; + ip.charpath = true; + let cur = ip.gs.cur; + let r = show_string(ip, &s, true, &mut |_, _, _| Ok((0.0, 0.0))); + ip.charpath = saved; + // The path ends at the advanced point. + if let (Some(p), Ok(_)) = (ip.gs.cur, &r) { + ip.gs.path.push(crate::read::gstate::Seg::Move(p.0, p.1)); + ip.gs.start = Some(p); + } + let _ = cur; + r.map(|_| ()) +} + +pub fn setcharwidth(ip: &mut Interp) -> R { + let wy = ip.pop_num()?; + let wx = ip.pop_num()?; + ip.gs.char_width = Some((wx, wy)); + Ok(()) +} + +pub fn setcachedevice(ip: &mut Interp) -> R { + for _ in 0..4 { + ip.pop_num()?; + } + setcharwidth(ip) +} + +/// `w0x w0y llx lly urx ury w1x w1y vx vy setcachedevice2`: the mode-0 width counts. +pub fn setcachedevice2(ip: &mut Interp) -> R { + for _ in 0..8 { + ip.pop_num()?; + } + setcharwidth(ip) +} diff --git a/crates/ps/src/read/gstate.rs b/crates/ps/src/read/gstate.rs new file mode 100644 index 0000000000..d27df9d812 --- /dev/null +++ b/crates/ps/src/read/gstate.rs @@ -0,0 +1,342 @@ +//! The graphics state: transformation, current path (kept in device space, as PostScript +//! defines it), colour, line parameters and clipping. + +use std::rc::Rc; + +use crate::read::obj::{Obj, PDict}; + +/// An affine matrix `[a b c d e f]`: (x, y) → (a·x + c·y + e, b·x + d·y + f). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct M(pub [f64; 6]); + +impl M { + pub const ID: M = M([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + + /// `self` first, then `o` (PostScript's `concat` is `m · CTM`). + pub fn then(&self, o: &M) -> M { + let [a, b, c, d, e, f] = self.0; + let [p, q, r, s, t, u] = o.0; + M([a * p + b * r, a * q + b * s, c * p + d * r, c * q + d * s, e * p + f * r + t, e * q + f * s + u]) + } + + pub fn apply(&self, x: f64, y: f64) -> (f64, f64) { + let [a, b, c, d, e, f] = self.0; + (a * x + c * y + e, b * x + d * y + f) + } + + pub fn delta(&self, x: f64, y: f64) -> (f64, f64) { + let [a, b, c, d, ..] = self.0; + (a * x + c * y, b * x + d * y) + } + + pub fn inv(&self) -> Option { + let [a, b, c, d, e, f] = self.0; + let det = a * d - b * c; + if det == 0.0 || !det.is_finite() { + return None; + } + let m = M([d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det]); + m.0.iter().all(|v| v.is_finite()).then_some(m) + } + + pub fn finite(&self) -> bool { + self.0.iter().all(|v| v.is_finite()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Seg { + Move(f64, f64), + Line(f64, f64), + Curve(f64, f64, f64, f64, f64, f64), + Close, +} + +impl Seg { + pub fn map(&self, m: &M) -> Seg { + match *self { + Seg::Move(x, y) => { + let (x, y) = m.apply(x, y); + Seg::Move(x, y) + } + Seg::Line(x, y) => { + let (x, y) = m.apply(x, y); + Seg::Line(x, y) + } + Seg::Curve(a, b, c, d, e, f) => { + let (a, b) = m.apply(a, b); + let (c, d) = m.apply(c, d); + let (e, f) = m.apply(e, f); + Seg::Curve(a, b, c, d, e, f) + } + Seg::Close => Seg::Close, + } + } +} + +/// The most segments one path may hold. +pub const MAX_PATH: usize = 4_000_000; + +/// Bézier segments for an arc of the circle at (cx, cy) with radius r, from angle a0 to a1 +/// (degrees), in user space: the first point and the curves. +pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> ((f64, f64), Vec<[f64; 6]>) { + let (a0, a1) = (a0.to_radians(), a1.to_radians()); + let start = (cx + r * a0.cos(), cy + r * a0.sin()); + let sweep = a1 - a0; + if !sweep.is_finite() || sweep == 0.0 { + return (start, Vec::new()); + } + let n = ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 64); + let step = sweep / n as f64; + let k = 4.0 / 3.0 * (step / 4.0).tan(); + let mut out = Vec::with_capacity(n); + let mut a = a0; + for _ in 0..n { + let b = a + step; + let (ca, sa, cb, sb) = (a.cos(), a.sin(), b.cos(), b.sin()); + out.push([cx + r * (ca - k * sa), cy + r * (sa + k * ca), cx + r * (cb + k * sb), cy + r * (sb - k * cb), cx + r * cb, cy + r * sb]); + a = b; + } + (start, out) +} + +#[derive(Clone, Debug)] +pub enum Space { + Gray, + Rgb, + Cmyk, + /// Separation or DeviceN: the colourant names, the alternate space and its tint transform. + Tint { + names: Vec>, + alt: Box, + tint: Obj, + }, + Indexed { + base: Box, + hival: usize, + lookup: Vec, + }, + Lab, + Pattern, +} + +impl Space { + pub fn comps(&self) -> usize { + match self { + Space::Gray | Space::Indexed { .. } | Space::Pattern => 1, + Space::Rgb | Space::Lab => 3, + Space::Cmyk => 4, + Space::Tint { names, .. } => names.len().max(1), + } + } + + pub fn initial(&self) -> Vec { + match self { + Space::Cmyk => vec![0.0, 0.0, 0.0, 1.0], + Space::Tint { names, .. } => vec![1.0; names.len().max(1)], + Space::Lab => vec![0.0, 0.0, 0.0], + _ => vec![0.0; self.comps()], + } + } +} + +/// A device colour as written to the PDF. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DevColor { + Gray(f64), + Rgb(f64, f64, f64), + Cmyk(f64, f64, f64, f64), +} + +/// A clipping path, intersected with the ones before it. +#[derive(Debug)] +pub struct Clip { + pub path: Vec, + pub eo: bool, + pub parent: Option>, + pub depth: usize, +} + +pub fn clip_chain(c: &Option>) -> Vec> { + let mut out = Vec::new(); + let mut cur = c.clone(); + while let Some(n) = cur { + cur = n.parent.clone(); + out.push(n); + } + out.reverse(); + out +} + +#[derive(Clone, Debug)] +pub struct GState { + pub ctm: M, + pub path: Vec, + /// The current point, in device space. + pub cur: Option<(f64, f64)>, + /// Where the current subpath started (closepath returns there). + pub start: Option<(f64, f64)>, + pub space: Space, + pub color: Vec, + /// The pattern dictionary when the space is Pattern. + pub pattern: Option, + pub line_width: f64, + pub cap: i64, + pub join: i64, + pub miter: f64, + pub dash: (Vec, f64), + pub flat: f64, + pub font: Option, + pub clip: Option>, + pub overprint: bool, + pub stroke_adjust: bool, + /// Clips saved by `clipsave`. + pub clip_saves: Vec>>, + /// The colour space under an uncoloured pattern (`[/Pattern /DeviceRGB]`). + pub pattern_base: Option>, + /// Inside an uncoloured pattern's PaintProc: colour operators are ignored. + pub color_locked: bool, + /// Painting goes nowhere (`nulldevice`, measuring glyphs). + pub nulldev: bool, + /// Glyph widths reported by `setcharwidth`/`setcachedevice` while a glyph runs. + pub char_width: Option<(f64, f64)>, +} + +impl Default for GState { + fn default() -> Self { + GState { + ctm: M::ID, + path: Vec::new(), + cur: None, + start: None, + space: Space::Gray, + color: vec![0.0], + pattern: None, + line_width: 1.0, + cap: 0, + join: 0, + miter: 10.0, + dash: (Vec::new(), 0.0), + flat: 1.0, + font: None, + clip: None, + overprint: false, + stroke_adjust: false, + clip_saves: Vec::new(), + color_locked: false, + pattern_base: None, + nulldev: false, + char_width: None, + } + } +} + +/// The bounding box of a path's points (control points included), or None for an empty path. +pub fn bbox(path: &[Seg]) -> Option<[f64; 4]> { + let mut b: Option<[f64; 4]> = None; + let mut add = |x: f64, y: f64| { + let r = b.get_or_insert([x, y, x, y]); + r[0] = r[0].min(x); + r[1] = r[1].min(y); + r[2] = r[2].max(x); + r[3] = r[3].max(y); + }; + for s in path { + match *s { + Seg::Move(x, y) | Seg::Line(x, y) => add(x, y), + Seg::Curve(a, b2, c, d, e, f) => { + add(a, b2); + add(c, d); + add(e, f); + } + Seg::Close => {} + } + } + b +} + +/// The most dashes a stroke may break into; beyond, it is drawn solid (a tiny pattern on a +/// long path would otherwise make billions of pieces). +pub const MAX_DASHES: f64 = 100_000.0; + +/// Whether dash pattern `dash` on `path` (in the space the pattern is measured in) stays within +/// [`MAX_DASHES`]; its lengths must be finite and not all zero. +pub fn dashes_ok(path: &[Seg], dash: &[f64]) -> bool { + let period: f64 = dash.iter().sum(); + if dash.is_empty() || !period.is_finite() || period <= 0.0 || dash.iter().any(|d| !d.is_finite() || *d < 0.0) { + return false; + } + let mut len = 0.0; + let mut cur = (0.0, 0.0); + for s in path { + match *s { + Seg::Move(x, y) => cur = (x, y), + Seg::Line(x, y) => { + len += (x - cur.0).hypot(y - cur.1); + cur = (x, y); + } + Seg::Curve(a, b, c, d, e, f) => { + len += (a - cur.0).hypot(b - cur.1) + (c - a).hypot(d - b) + (e - c).hypot(f - d); + cur = (e, f); + } + Seg::Close => {} + } + } + len.is_finite() && len / period * dash.len() as f64 <= MAX_DASHES +} + +/// A path with its curves replaced by line segments (`flattenpath`). +pub fn flatten(path: &[Seg], tol: f64) -> Vec { + let mut out = Vec::with_capacity(path.len()); + let mut cur = (0.0, 0.0); + for s in path { + match *s { + Seg::Curve(x1, y1, x2, y2, x3, y3) => { + let d = ((x1 - cur.0).hypot(y1 - cur.1) + (x2 - x1).hypot(y2 - y1) + (x3 - x2).hypot(y3 - y2)).max(0.0); + let n = ((d / tol.max(0.05)).sqrt().ceil() as usize).clamp(1, 256); + for i in 1..=n { + let t = i as f64 / n as f64; + let mt = 1.0 - t; + let x = mt * mt * mt * cur.0 + 3.0 * mt * mt * t * x1 + 3.0 * mt * t * t * x2 + t * t * t * x3; + let y = mt * mt * mt * cur.1 + 3.0 * mt * mt * t * y1 + 3.0 * mt * t * t * y2 + t * t * t * y3; + out.push(Seg::Line(x, y)); + } + cur = (x3, y3); + } + Seg::Move(x, y) | Seg::Line(x, y) => { + cur = (x, y); + out.push(*s); + } + Seg::Close => out.push(*s), + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matrices_compose_like_postscript() { + let t = M([1.0, 0.0, 0.0, 1.0, 10.0, 20.0]); + let s = M([2.0, 0.0, 0.0, 3.0, 0.0, 0.0]); + // `10 20 translate 2 3 scale`: scale applies first, then translate. + let ctm = s.then(&t); + assert_eq!(ctm.apply(1.0, 1.0), (12.0, 23.0)); + let inv = ctm.inv().unwrap(); + let (x, y) = inv.apply(12.0, 23.0); + assert!((x - 1.0).abs() < 1e-12 && (y - 1.0).abs() < 1e-12); + assert!(M([0.0; 6]).inv().is_none()); + } + + #[test] + fn a_full_circle_is_four_quarter_curves() { + let (start, curves) = arc_curves(0.0, 0.0, 10.0, 0.0, 360.0); + assert!((start.0 - 10.0).abs() < 1e-9); + assert_eq!(curves.len(), 4); + let last = curves.last().unwrap(); + assert!((last[4] - 10.0).abs() < 1e-9 && last[5].abs() < 1e-9); + assert!(arc_curves(0.0, 0.0, 1.0, 0.0, f64::INFINITY).1.is_empty()); + } +} diff --git a/crates/ps/src/read/interp.rs b/crates/ps/src/read/interp.rs new file mode 100644 index 0000000000..d43198fa18 --- /dev/null +++ b/crates/ps/src/read/interp.rs @@ -0,0 +1,678 @@ +//! The interpreter: stacks, the execution loop and error handling. +//! +//! Execution never recurses on the Rust stack for PostScript control flow: procedures, loops +//! and `stopped` are frames on an explicit execution stack. Operators that must run a procedure +//! and use its result (tint transforms, BuildChar, `pathforall`) call [`Interp::call`], a +//! nested run of the loop with a bounded depth. + +use crate::read::gstate::GState; +use crate::read::obj::{Key, Obj, PArr, PDict, PStr, name}; +use crate::read::ops::{self, OpId}; +use crate::read::pdf::Out; +use crate::read::scan; +use crate::read::source::{Source, Sources}; + +/// A PostScript error (`typecheck`, …), `stop`, `quit`, or a fatal problem that ends the run. +#[derive(Debug, Clone, PartialEq)] +pub enum PsErr { + Ps(&'static str), + Stop, + Quit, + Fatal(String), +} + +pub type R = Result; + +pub fn err(name: &'static str) -> R { + Err(PsErr::Ps(name)) +} + +#[derive(Debug, Clone)] +pub enum Frame { + Proc { + arr: PArr, + i: usize, + }, + File { + id: usize, + }, + For { + cur: f64, + inc: f64, + limit: f64, + int: bool, + proc: Obj, + }, + Repeat { + n: i64, + proc: Obj, + }, + Loop { + proc: Obj, + }, + ForallArr { + arr: PArr, + i: usize, + proc: Obj, + }, + ForallStr { + s: PStr, + i: usize, + proc: Obj, + }, + ForallDict { + items: Vec<(Obj, Obj)>, + i: usize, + proc: Obj, + }, + Stopped, + /// The bottom of a nested run ([`Interp::call`]). + Barrier, +} + +impl Frame { + pub fn is_loop(&self) -> bool { + matches!( + self, + Frame::For { .. } + | Frame::Repeat { .. } + | Frame::Loop { .. } + | Frame::ForallArr { .. } + | Frame::ForallStr { .. } + | Frame::ForallDict { .. } + ) + } +} + +/// How much work and memory one EPS may use. +#[derive(Debug, Clone, Copy)] +pub struct Limits { + pub steps: u64, + pub memory: usize, +} + +impl Default for Limits { + /// Generous for real EPS files: an Illustrator file with its procsets takes about 0.6 million + /// steps and 2 MB, one carrying a 15 MB CJK font 2 million steps and 30 MB. A program that + /// runs into the step limit ends within a few seconds. Smaller on the web. + fn default() -> Self { + if cfg!(target_arch = "wasm32") { Limits { steps: 20_000_000, memory: 256 << 20 } } else { Limits { steps: 50_000_000, memory: 1 << 30 } } + } +} + +/// Memory a path segment and a graphics state take. +pub const SEG_BYTES: usize = std::mem::size_of::(); +pub const GSTATE_BYTES: usize = 512; + +/// Content-stream bytes one written path segment takes, about. +pub const OUT_SEG_BYTES: usize = 24; + +/// Memory a file takes (charged when one is made). +pub const SOURCE_BYTES: usize = 128; + +/// Bytes read, copied or scanned per step of work charged. +pub const BYTES_PER_STEP: u64 = 16; + +const MAX_OSTACK: usize = 500_000; +const MAX_ESTACK: usize = 100_000; +const MAX_DSTACK: usize = 1_000; +const MAX_GSTACK: usize = 10_000; +const MAX_NAME_DEPTH: usize = 48; +const MAX_CALL_DEPTH: usize = 48; + +pub struct Interp { + pub o: Vec, + pub d: Vec, + pub e: Vec, + pub srcs: Sources, + pub gs: GState, + pub gstack: Vec, + /// Graphics-state depths that `save` objects restore to. + pub saves: Vec, + pub gstates: Vec, + pub systemdict: PDict, + pub globaldict: PDict, + pub userdict: PDict, + pub errordict: PDict, + pub dollar_error: PDict, + pub statusdict: PDict, + pub fontdir: PDict, + /// Resource categories: name → dictionary of instances. + pub resources: PDict, + pub out: Out, + pub limits: Limits, + pub steps: u64, + pub memory: usize, + /// How many of `srcs.read` have been charged as steps, and of the segments written. + pub read_charged: u64, + pub written_charged: u64, + pub depth: usize, + /// Executable names can resolve to each other without entering a procedure frame. + pub name_depth: usize, + pub next_fid: usize, + pub main: usize, + pub warnings: Vec, + /// Tiling pattern cells already written, by pattern and colour. + pub cells: std::collections::HashMap<(crate::read::obj::Key, Vec), usize>, + /// CMaps by index (CMap dictionaries refer to them). + pub cmaps: Vec, + /// CFF font sets read by StartData. + pub cff: Vec>, + /// Type 42 fonts' TrueType data by FID. + pub sfnts: std::collections::HashMap>>, + /// `charpath` is running: glyphs add to the path instead of painting. + pub charpath: bool, + /// Glyph outlines by font (FID) and glyph name. + pub glyphs: std::collections::HashMap<(usize, Vec), std::rc::Rc>, + /// The last names executed (for error reports). + pub recent: std::collections::VecDeque, +} + +impl Interp { + pub fn new(program: Vec, origin: (f64, f64), limits: Limits) -> Self { + let mut srcs = Sources { max_read: limits.steps.saturating_mul(BYTES_PER_STEP), ..Sources::default() }; + let main = srcs.add(Source::bytes(program)); + let systemdict = PDict::new(1000); + let globaldict = PDict::new(100); + let userdict = PDict::new(200); + let mut ip = Interp { + o: Vec::new(), + d: vec![systemdict.clone(), globaldict.clone(), userdict.clone()], + e: Vec::new(), + srcs, + gs: GState::default(), + gstack: Vec::new(), + saves: Vec::new(), + gstates: Vec::new(), + systemdict, + globaldict, + userdict, + errordict: PDict::new(40), + dollar_error: PDict::new(10), + statusdict: PDict::new(40), + fontdir: PDict::new(100), + resources: PDict::new(20), + out: Out::new(origin), + limits, + steps: 0, + memory: 0, + read_charged: 0, + written_charged: 0, + depth: 0, + name_depth: 0, + next_fid: 1, + main, + warnings: Vec::new(), + recent: std::collections::VecDeque::new(), + glyphs: std::collections::HashMap::new(), + charpath: false, + sfnts: std::collections::HashMap::new(), + cmaps: Vec::new(), + cells: std::collections::HashMap::new(), + cff: Vec::new(), + }; + ops::install(&mut ip); + ip + } + + // ----- operand stack ----- + + pub fn push(&mut self, o: Obj) -> R { + if self.o.len() >= MAX_OSTACK { + return err("stackoverflow"); + } + self.o.push(o); + Ok(()) + } + + pub fn pop(&mut self) -> R { + self.o.pop().ok_or(PsErr::Ps("stackunderflow")) + } + + pub fn top(&self, n: usize) -> R<&Obj> { + let i = self.o.len().checked_sub(n + 1).ok_or(PsErr::Ps("stackunderflow"))?; + self.o.get(i).ok_or(PsErr::Ps("stackunderflow")) + } + + pub fn need(&self, n: usize) -> R { + if self.o.len() < n { err("stackunderflow") } else { Ok(()) } + } + + pub fn pop_num(&mut self) -> R { + match self.pop()? { + o @ (Obj::Int(_) | Obj::Real(_)) => o.num().ok_or(PsErr::Ps("typecheck")), + _ => err("typecheck"), + } + } + + pub fn pop_int(&mut self) -> R { + match self.pop()? { + Obj::Int(i) => Ok(i), + Obj::Real(r) if r.fract() == 0.0 && r.abs() < 9.0e15 => Ok(r as i64), + _ => err("typecheck"), + } + } + + pub fn pop_usize(&mut self) -> R { + usize::try_from(self.pop_int()?).map_err(|_| PsErr::Ps("rangecheck")) + } + + pub fn pop_bool(&mut self) -> R { + match self.pop()? { + Obj::Bool(b) => Ok(b), + _ => err("typecheck"), + } + } + + pub fn pop_dict(&mut self) -> R { + match self.pop()? { + Obj::Dict(d) => Ok(d), + _ => err("typecheck"), + } + } + + pub fn pop_arr(&mut self) -> R { + match self.pop()? { + Obj::Array(a, _) => Ok(a), + _ => err("typecheck"), + } + } + + pub fn pop_str(&mut self) -> R { + match self.pop()? { + Obj::Str(s, _) => Ok(s), + _ => err("typecheck"), + } + } + + /// A name, or a string read as one. + pub fn pop_name(&mut self) -> R> { + match self.pop()? { + Obj::Name(n, _) => Ok(n.to_vec()), + Obj::Str(s, _) => Ok(s.bytes()), + _ => err("typecheck"), + } + } + + pub fn pop_matrix(&mut self) -> R { + let a = self.pop_arr()?; + matrix_of(&a) + } + + /// Charge `units` steps of work (elements visited, or [`BYTES_PER_STEP`] bytes each): what an + /// operator does in proportion to its operands counts against the step limit, checked as it + /// goes rather than after. + pub fn charge(&mut self, units: u64) -> R { + self.steps = self.steps.saturating_add(units); + if self.steps > self.limits.steps { Err(PsErr::Fatal("the program runs too long".into())) } else { Ok(()) } + } + + /// Charge work for `n` bytes handled. + pub fn charge_bytes(&mut self, n: usize) -> R { + self.charge((n as u64) / BYTES_PER_STEP + 1) + } + + /// Charge memory for `n` bytes made (new objects and copies alike; the cap bounds what one + /// conversion makes in all, not only what it keeps). + pub fn alloc(&mut self, n: usize) -> R { + self.memory = self.memory.saturating_add(n); + if self.memory > self.limits.memory { Err(PsErr::Fatal("out of memory".into())) } else { Ok(()) } + } + + // ----- dictionaries ----- + + pub fn lookup(&self, k: &Key) -> Option { + self.d.iter().rev().find_map(|d| d.get(k)) + } + + pub fn begin(&mut self, d: PDict) -> R { + if self.d.len() >= MAX_DSTACK { + return err("dictstackoverflow"); + } + self.d.push(d); + Ok(()) + } + + pub fn current_dict(&self) -> R { + self.d.last().cloned().ok_or(PsErr::Ps("dictstackunderflow")) + } + + // ----- execution ----- + + pub fn push_frame(&mut self, f: Frame) -> R { + if self.e.len() >= MAX_ESTACK { + return err("execstackoverflow"); + } + self.e.push(f); + Ok(()) + } + + /// Execute an object as `exec` does: procedures run, names are looked up, strings and files + /// are scanned as programs, anything else is pushed. + pub fn exec_value(&mut self, o: Obj) -> R { + match o { + Obj::Array(a, true) => { + if a.len > 0 { + self.push_frame(Frame::Proc { arr: a, i: 0 })?; + } + Ok(()) + } + Obj::Name(n, true) => { + if self.name_depth >= MAX_NAME_DEPTH { + return Err(PsErr::Fatal("executable name resolution limit".into())); + } + self.name_depth += 1; + if self.recent.len() >= 24 { + self.recent.pop_front(); + } + self.recent.push_back(n.clone()); + let result = match self.lookup(&Key::Name(n.clone())) { + Some(v) => self.exec_found(v), + None => { + self.dollar_error.put_name(b"offending", Obj::Name(n, false)); + Err(PsErr::Ps("undefined")) + } + }; + self.name_depth -= 1; + result + } + Obj::Op(op) => self.call_op(op), + Obj::Str(s, true) => { + // A string runs as a file reading it in place. + self.alloc(SOURCE_BYTES)?; + let id = self.srcs.add(Source::string(&s)); + self.push_frame(Frame::File { id }) + } + Obj::File(id) => self.push_frame(Frame::File { id }), + other => self.push(other), + } + } + + /// The value of an executable name: executed (procedures run). + fn exec_found(&mut self, v: Obj) -> R { + match v { + Obj::Array(a, true) => { + if a.len > 0 { + self.push_frame(Frame::Proc { arr: a, i: 0 })?; + } + Ok(()) + } + Obj::Op(op) => self.call_op(op), + // A name bound to another executable name: `exec_value` bounds this resolution chain. + // Files are literal unless `exec` runs them (`cvx exec`). + o @ (Obj::Name(_, true) | Obj::Str(_, true)) => self.exec_value(o), + other => self.push(other), + } + } + + pub fn call_op(&mut self, op: OpId) -> R { + let (opname, f) = ops::OPS.get(op as usize).copied().ok_or(PsErr::Ps("undefined"))?; + match f(self) { + Err(PsErr::Ps(e)) => { + self.dollar_error.put_name(b"command", Obj::lit(opname)); + Err(PsErr::Ps(e)) + } + r => r, + } + } + + /// Execute one object met in a procedure body or a program: executable names and operators + /// run, everything else (procedures included) is pushed. + fn exec_direct(&mut self, o: Obj) -> R { + match o { + Obj::Name(_, true) | Obj::Op(_) => self.exec_value(o), + other => self.push(other), + } + } + + fn step(&mut self) -> R { + // Bytes read since the last step are work too (decoding included). + let read = self.srcs.read; + let fresh = read.saturating_sub(self.read_charged); + self.read_charged = read; + self.charge(1 + fresh / BYTES_PER_STEP)?; + // So is what was written: re-emitted clips included. + let written = self.out.segments() as u64; + let fresh = written.saturating_sub(self.written_charged); + if fresh > 0 { + self.written_charged = written; + self.charge(fresh / 4)?; + self.alloc(usize::try_from(fresh).unwrap_or(usize::MAX).saturating_mul(OUT_SEG_BYTES))?; + } + if self.srcs.exhausted { + return Err(PsErr::Fatal("the program reads too much data".into())); + } + let Some(frame) = self.e.last_mut() else { return Ok(()) }; + match frame { + Frame::Proc { arr, i } => { + let Some(o) = arr.get(*i) else { + self.e.pop(); + return Ok(()); + }; + *i += 1; + if *i >= arr.len { + // The last element: drop the frame first, so tail recursion doesn't grow it. + self.e.pop(); + } + self.exec_direct(o) + } + Frame::File { id } => { + let id = *id; + let d = &self.d; + let mut resolve = |n: &[u8]| d.iter().rev().find_map(|dict| dict.get(&Key::Name(name(n)))); + let before = self.srcs.read; + let token = scan::next(&mut self.srcs, id, &mut resolve); + // What the scanner read became objects: memory. + let scanned = usize::try_from(self.srcs.read.saturating_sub(before)).unwrap_or(usize::MAX); + self.alloc(scanned)?; + match token { + Ok(Some(o)) => self.exec_direct(o), + Ok(None) => { + self.e.pop(); + Ok(()) + } + Err(msg) => { + self.e.pop(); + self.warnings.push(msg); + err("syntaxerror") + } + } + } + Frame::For { cur, inc, limit, int, proc } => { + let done = if *inc > 0.0 { *cur > *limit } else { *cur < *limit }; + if done || !cur.is_finite() { + self.e.pop(); + return Ok(()); + } + let v = if *int { Obj::Int(*cur as i64) } else { Obj::Real(*cur) }; + *cur += *inc; + let p = proc.clone(); + self.push(v)?; + self.exec_value(p) + } + Frame::Repeat { n, proc } => { + if *n <= 0 { + self.e.pop(); + return Ok(()); + } + *n -= 1; + let p = proc.clone(); + self.exec_value(p) + } + Frame::Loop { proc } => { + let p = proc.clone(); + self.exec_value(p) + } + Frame::ForallArr { arr, i, proc } => { + let Some(v) = arr.get(*i) else { + self.e.pop(); + return Ok(()); + }; + *i += 1; + let p = proc.clone(); + self.push(v)?; + self.exec_value(p) + } + Frame::ForallStr { s, i, proc } => { + let Some(v) = s.get(*i) else { + self.e.pop(); + return Ok(()); + }; + *i += 1; + let p = proc.clone(); + self.push(Obj::Int(i64::from(v)))?; + self.exec_value(p) + } + Frame::ForallDict { items, i, proc } => { + let Some((k, v)) = items.get(*i).cloned() else { + self.e.pop(); + return Ok(()); + }; + *i += 1; + let p = proc.clone(); + self.push(k)?; + self.push(v)?; + self.exec_value(p) + } + Frame::Stopped => { + self.e.pop(); + self.push(Obj::Bool(false)) + } + Frame::Barrier => Ok(()), + } + } + + /// Run until the execution stack is back to `floor` frames. + pub fn run_to(&mut self, floor: usize) -> R { + while self.e.len() > floor { + if matches!(self.e.last(), Some(Frame::Barrier)) && self.e.len() == floor + 1 { + break; + } + match self.step() { + Ok(()) => {} + Err(PsErr::Fatal(m)) => return Err(PsErr::Fatal(m)), + Err(PsErr::Quit) => return Err(PsErr::Quit), + Err(e) => { + if let PsErr::Ps(name) = &e { + self.dollar_error.put_name(b"errorname", Obj::lit(name.as_bytes())); + self.dollar_error.put_name(b"newerror", Obj::Bool(true)); + } + // Unwind to the nearest `stopped` above the floor; none: the error is ours to return. + match self.e.iter().rposition(|f| matches!(f, Frame::Stopped | Frame::Barrier)) { + Some(i) if i >= floor && matches!(self.e.get(i), Some(Frame::Stopped)) => { + self.e.truncate(i); + self.push(Obj::Bool(true))?; + } + _ => { + self.e.truncate(floor); + return Err(e); + } + } + } + } + } + Ok(()) + } + + /// Run `proc` to completion (a nested run), for operators that need its results. + pub fn call(&mut self, proc: Obj) -> R { + if self.depth >= MAX_CALL_DEPTH { + return err("limitcheck"); + } + self.depth += 1; + let floor = self.e.len(); + let r = self.push_frame(Frame::Barrier).and_then(|()| self.exec_value(proc)).and_then(|()| self.run_to(floor + 1)); + // Drop the barrier (and anything an error left above the floor). + self.e.truncate(floor); + self.depth -= 1; + r + } + + /// Run the program. + pub fn run(&mut self) -> Result<(), String> { + self.e.push(Frame::File { id: self.main }); + match self.run_to(0) { + Ok(()) | Err(PsErr::Quit) => Ok(()), + Err(PsErr::Stop) => Ok(()), + Err(PsErr::Ps(e)) => { + let cmd = self.dollar_error.get_name(b"command").map(|c| match c { + Obj::Name(n, _) => String::from_utf8_lossy(&n).into_owned(), + _ => String::new(), + }); + let offending = match self.dollar_error.get_name(b"offending") { + Some(Obj::Name(n, _)) => format!(" ({})", String::from_utf8_lossy(&n)), + _ => String::new(), + }; + let recent: Vec = self.recent.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect(); + let stack: Vec = self + .o + .iter() + .rev() + .take(6) + .map(|o| { + String::from_utf8_lossy(&crate::read::ops::basic::text_of(o)).chars().take(24).collect::() + + ":" + + &String::from_utf8_lossy(o.type_name()) + }) + .collect(); + Err(format!( + "PostScript error {e} in {}{offending}; after {}; stack {}", + cmd.unwrap_or_default(), + recent.join(" "), + stack.join(" | ") + )) + } + Err(PsErr::Fatal(m)) => Err(m), + } + } + + // ----- graphics-state stack ----- + + pub fn gsave(&mut self) -> R { + if self.gstack.len() >= MAX_GSTACK { + return err("limitcheck"); + } + self.charge_gstate()?; + self.gstack.push(self.gs.clone()); + Ok(()) + } + + /// Copying the graphics state copies its path: work and memory in proportion. + pub fn charge_gstate(&mut self) -> R { + let n = self.gs.path.len() + self.gs.dash.0.len() + self.gs.clip_saves.len() + self.gs.color.len(); + self.charge(n as u64 / 4 + 1)?; + self.alloc(n.saturating_mul(SEG_BYTES).saturating_add(GSTATE_BYTES)) + } + + /// Charge work and memory for copying or walking `n` path segments. + pub fn charge_path(&mut self, n: usize) -> R { + self.charge(n as u64 / 4 + 1)?; + self.alloc(n.saturating_mul(SEG_BYTES)) + } + + pub fn grestore(&mut self) { + // Not below the innermost save. + let floor = self.saves.last().copied().unwrap_or(0); + if self.gstack.len() > floor + && let Some(g) = self.gstack.pop() + { + self.gs = g; + } + } +} + +/// A six-number array as a matrix. +pub fn matrix_of(a: &PArr) -> R { + if a.len != 6 { + return err("rangecheck"); + } + let mut m = [0.0; 6]; + for (i, slot) in m.iter_mut().enumerate() { + *slot = a.get(i).and_then(|o| o.num()).ok_or(PsErr::Ps("typecheck"))?; + } + Ok(crate::read::gstate::M(m)) +} + +pub fn matrix_obj(m: &crate::read::gstate::M) -> Obj { + Obj::Array(PArr::new(m.0.iter().map(|v| Obj::Real(*v)).collect()), false) +} diff --git a/crates/ps/src/read/mod.rs b/crates/ps/src/read/mod.rs new file mode 100644 index 0000000000..52386a43f2 --- /dev/null +++ b/crates/ps/src/read/mod.rs @@ -0,0 +1,118 @@ +//! Reading EPS: a PostScript interpreter that runs the program and writes what it paints as a +//! one-page PDF the size of its bounding box, in vector form with its colours kept (CMYK stays +//! CMYK), so an app can show and print it as a placed PDF. +//! +//! Written from the PostScript Language Reference (third edition), the EPS specification, the +//! Type 1 Font Format and Adobe Technical Notes 5012 (Type 42), 5176 (CFF) and 5177 (Type 2 +//! charstrings). +//! Covered: the language (Level 3 operators EPS files use), paths, painting, clipping, device and +//! Separation/DeviceN/Indexed colour, sampled images with the standard filters, smooth shadings +//! (axial, radial, function-based, triangle and patch meshes), shading and tiling patterns, and +//! text in Type 1, Type 3, Type 42 and CFF fonts (`StartData`), CIDFonts and composite fonts +//! through CMaps, written horizontally or vertically. + +mod cff; +mod cff_strings; +mod composite; +mod encodings; +mod font; +mod gstate; +mod interp; +mod obj; +mod ops; +mod pdf; +mod scan; +mod source; +mod type1; +mod type42; + +use craft_eps::{Comments, Kind, MAX_EXTENDED_HEADER, postscript_section as shared_postscript_section}; + +pub use interp::Limits; + +/// A converted EPS. +#[derive(Debug)] +pub struct Converted { + /// A one-page PDF of the bounding box. + pub pdf: Vec, + /// The bounding box in points (x0, y0, x1, y1). + pub bbox: [f64; 4], + /// Things the program did that weren't drawn (unsupported features). + pub warnings: Vec, + /// The work and memory the conversion used, as [`Limits`] counts them. + pub steps: u64, + pub memory: usize, +} + +/// The PostScript part of an EPS (inside a DOS EPS binary header, when there is one). +pub fn postscript_section(bytes: &[u8]) -> &[u8] { + shared_postscript_section(bytes).unwrap_or(&[]) +} + +/// The bounding box from the DSC comments: the high-resolution one when it is valid. +pub fn bounding_box(ps: &[u8]) -> Option<[f64; 4]> { + Comments::read_with_header_limit(ps, MAX_EXTENDED_HEADER).page_box() +} + +/// An EPS file (plain, or DOS EPS with a binary header)? +pub fn is_eps(bytes: &[u8]) -> bool { + craft_eps::sniff(bytes).is_some_and(|file| file.kind == Kind::Eps) +} + +/// How many converted EPS files are kept. +const CACHE_ENTRIES: usize = 64; + +type Cache = std::collections::HashMap<(u64, usize), Option>>>; + +fn cache() -> &'static std::sync::Mutex { + static CACHE: std::sync::OnceLock> = std::sync::OnceLock::new(); + CACHE.get_or_init(|| std::sync::Mutex::new(Cache::new())) +} + +/// [`eps_to_pdf`] with the default limits, remembered by content: the screen, thumbnails and +/// every export convert an EPS once. None when it can't be converted (callers show its preview). +pub fn cached_pdf(eps: &[u8]) -> Option>> { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + eps.hash(&mut h); + let key = (h.finish(), eps.len()); + if let Some(hit) = cache().lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key) { + return hit.clone(); + } + // Convert outside the lock: other EPS files keep drawing meanwhile. + let pdf = eps_to_pdf(eps, Limits::default()).ok().map(|c| std::sync::Arc::new(c.pdf)); + let mut c = cache().lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if c.len() >= CACHE_ENTRIES { + c.clear(); + } + c.insert(key, pdf.clone()); + pdf +} + +/// Run an EPS and write what it paints as PDF. `Err` when it isn't an EPS with a bounding box, +/// when the program fails before painting anything, or when it exceeds `limits`. +pub fn eps_to_pdf(bytes: &[u8], limits: Limits) -> Result { + let ps = postscript_section(bytes); + if !ps.starts_with(b"%!") { + return Err("not PostScript".into()); + } + let bbox = bounding_box(ps).ok_or("the EPS has no bounding box")?; + let mut ip = interp::Interp::new(ps.to_vec(), (bbox[0], bbox[1]), limits); + let result = ip.run(); + let mut warnings = std::mem::take(&mut ip.warnings); + warnings.dedup(); + if let Err(e) = result { + // What was painted before an error is kept (the rest of the page is lost, as on a + // printer); with nothing painted, the caller falls back to the preview. + if !ip.out.painted { + return Err(e); + } + warnings.push(e); + } + let size = (bbox[2] - bbox[0], bbox[3] - bbox[1]); + let (steps, memory) = (ip.steps, ip.memory); + Ok(Converted { pdf: ip.out.finish(size), bbox, warnings, steps, memory }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/ps/src/read/obj.rs b/crates/ps/src/read/obj.rs new file mode 100644 index 0000000000..411450123a --- /dev/null +++ b/crates/ps/src/read/obj.rs @@ -0,0 +1,301 @@ +//! PostScript objects. Composite objects (strings, arrays, dictionaries) share their storage the +//! way the language requires: `dup` copies the reference, `getinterval` a window onto the same +//! values. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use crate::read::ops::OpId; + +/// A name: its bytes, shared. +pub type Name = Rc<[u8]>; + +pub fn name(s: &[u8]) -> Name { + Rc::from(s) +} + +#[derive(Clone, Debug)] +pub struct PStr { + pub buf: Rc>>, + pub off: usize, + pub len: usize, +} + +impl PStr { + pub fn new(bytes: Vec) -> Self { + let len = bytes.len(); + PStr { buf: Rc::new(RefCell::new(bytes)), off: 0, len } + } + + /// The bytes in view (empty when the window no longer fits its storage). + pub fn bytes(&self) -> Vec { + let b = self.buf.borrow(); + self.off.checked_add(self.len).and_then(|end| b.get(self.off..end)).map(<[u8]>::to_vec).unwrap_or_default() + } + + pub fn get(&self, i: usize) -> Option { + if i >= self.len { + return None; + } + self.buf.borrow().get(self.off.checked_add(i)?).copied() + } + + pub fn set(&self, i: usize, v: u8) -> bool { + if i >= self.len { + return false; + } + let Some(at) = self.off.checked_add(i) else { return false }; + match self.buf.borrow_mut().get_mut(at) { + Some(slot) => { + *slot = v; + true + } + None => false, + } + } + + pub fn interval(&self, start: usize, count: usize) -> Option { + (start.checked_add(count)? <= self.len).then(|| PStr { buf: self.buf.clone(), off: self.off + start, len: count }) + } + + /// Copy `src` into the start of this string: the written window, or None when it doesn't fit. + pub fn put_bytes(&self, at: usize, src: &[u8]) -> Option { + if at.checked_add(src.len())? > self.len { + return None; + } + let mut b = self.buf.borrow_mut(); + let start = self.off.checked_add(at)?; + b.get_mut(start..start.checked_add(src.len())?)?.copy_from_slice(src); + Some(PStr { buf: self.buf.clone(), off: start, len: src.len() }) + } +} + +#[derive(Clone, Debug)] +pub struct PArr { + pub buf: Rc>>, + pub off: usize, + pub len: usize, +} + +impl PArr { + pub fn new(v: Vec) -> Self { + let len = v.len(); + PArr { buf: Rc::new(RefCell::new(v)), off: 0, len } + } + + pub fn get(&self, i: usize) -> Option { + if i >= self.len { + return None; + } + self.buf.borrow().get(self.off.checked_add(i)?).cloned() + } + + pub fn set(&self, i: usize, v: Obj) -> bool { + if i >= self.len { + return false; + } + let Some(at) = self.off.checked_add(i) else { return false }; + match self.buf.borrow_mut().get_mut(at) { + Some(slot) => { + *slot = v; + true + } + None => false, + } + } + + pub fn items(&self) -> Vec { + let b = self.buf.borrow(); + self.off.checked_add(self.len).and_then(|end| b.get(self.off..end)).map(<[Obj]>::to_vec).unwrap_or_default() + } + + pub fn interval(&self, start: usize, count: usize) -> Option { + (start.checked_add(count)? <= self.len).then(|| PArr { buf: self.buf.clone(), off: self.off + start, len: count }) + } + + pub fn same(&self, o: &PArr) -> bool { + Rc::ptr_eq(&self.buf, &o.buf) && self.off == o.off && self.len == o.len + } +} + +/// A dictionary key: names (strings become names), integers (integral reals too), booleans, and +/// anything else by identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Key { + Name(Name), + Int(i64), + Real(u64), + Bool(bool), + Null, + Ident(usize), +} + +#[derive(Debug, Default)] +pub struct DictInner { + pub map: HashMap, + pub cap: usize, +} + +#[derive(Clone, Debug)] +pub struct PDict(pub Rc>); + +impl PDict { + pub fn new(cap: usize) -> Self { + PDict(Rc::new(RefCell::new(DictInner { map: HashMap::new(), cap }))) + } + + pub fn get(&self, k: &Key) -> Option { + self.0.borrow().map.get(k).map(|(_, v)| v.clone()) + } + + pub fn get_name(&self, n: &[u8]) -> Option { + self.get(&Key::Name(name(n))) + } + + pub fn put(&self, key: Obj, v: Obj) { + let k = key.key(); + self.0.borrow_mut().map.insert(k, (key, v)); + } + + pub fn put_name(&self, n: &[u8], v: Obj) { + self.put(Obj::Name(name(n), false), v); + } + + pub fn remove(&self, k: &Key) { + self.0.borrow_mut().map.remove(k); + } + + pub fn len(&self) -> usize { + self.0.borrow().map.len() + } + + pub fn known(&self, k: &Key) -> bool { + self.0.borrow().map.contains_key(k) + } + + pub fn entries(&self) -> Vec<(Obj, Obj)> { + self.0.borrow().map.values().cloned().collect() + } + + pub fn same(&self, o: &PDict) -> bool { + Rc::ptr_eq(&self.0, &o.0) + } + + fn ident(&self) -> usize { + Rc::as_ptr(&self.0) as *const u8 as usize + } +} + +/// An open file: an index into the interpreter's sources. +pub type FileId = usize; + +#[derive(Clone, Debug)] +pub enum Obj { + Null, + Bool(bool), + Int(i64), + Real(f64), + /// A name; `true` when executable. + Name(Name, bool), + Str(PStr, bool), + Array(PArr, bool), + Dict(PDict), + Mark, + Op(OpId), + File(FileId), + /// A `save` snapshot: the graphics-state depth it restores to. + Save(usize), + /// A font identifier (`FID`). + FontId(usize), + /// A saved graphics state (`gstate`). + GState(usize), +} + +impl Obj { + pub fn key(&self) -> Key { + match self { + Obj::Name(n, _) => Key::Name(n.clone()), + Obj::Str(s, _) => Key::Name(name(&s.bytes())), + Obj::Int(i) => Key::Int(*i), + Obj::Real(r) if r.fract() == 0.0 && r.abs() < 9.0e15 => Key::Int(*r as i64), + Obj::Real(r) => Key::Real(r.to_bits()), + Obj::Bool(b) => Key::Bool(*b), + Obj::Null => Key::Null, + Obj::Array(a, _) => Key::Ident(Rc::as_ptr(&a.buf) as *const u8 as usize ^ a.off), + Obj::Dict(d) => Key::Ident(d.ident()), + Obj::Op(o) => Key::Ident(0x4000_0000 + *o as usize), + Obj::File(f) => Key::Ident(0x5000_0000 + f), + Obj::FontId(f) => Key::Ident(0x6000_0000 + f), + Obj::Mark => Key::Ident(1), + Obj::Save(s) => Key::Ident(0x7000_0000 + s), + Obj::GState(g) => Key::Ident(0x7800_0000 + g), + } + } + + pub fn type_name(&self) -> &'static [u8] { + match self { + Obj::Null => b"nulltype", + Obj::Bool(_) => b"booleantype", + Obj::Int(_) => b"integertype", + Obj::Real(_) => b"realtype", + Obj::Name(..) => b"nametype", + Obj::Str(..) => b"stringtype", + Obj::Array(..) => b"arraytype", + Obj::Dict(_) => b"dicttype", + Obj::Mark => b"marktype", + Obj::Op(_) => b"operatortype", + Obj::File(_) => b"filetype", + Obj::Save(_) => b"savetype", + Obj::FontId(_) => b"fonttype", + Obj::GState(_) => b"gstatetype", + } + } + + pub fn is_exec(&self) -> bool { + match self { + Obj::Name(_, x) | Obj::Str(_, x) | Obj::Array(_, x) => *x, + Obj::Op(_) => true, + _ => false, + } + } + + pub fn num(&self) -> Option { + match self { + Obj::Int(i) => Some(*i as f64), + Obj::Real(r) => Some(*r), + _ => None, + } + } + + pub fn str(s: &[u8]) -> Obj { + Obj::Str(PStr::new(s.to_vec()), false) + } + + pub fn lit(n: &[u8]) -> Obj { + Obj::Name(name(n), false) + } + + pub fn proc(v: Vec) -> Obj { + Obj::Array(PArr::new(v), true) + } + + /// Equality as `eq` sees it: numbers by value, strings by contents, names and strings + /// interchangeably, composites by identity. + pub fn ps_eq(&self, o: &Obj) -> bool { + match (self, o) { + (a, b) if a.num().is_some() && b.num().is_some() => a.num() == b.num(), + (Obj::Str(a, _), Obj::Str(b, _)) => a.bytes() == b.bytes(), + (Obj::Name(a, _), Obj::Name(b, _)) => a == b, + (Obj::Name(a, _), Obj::Str(b, _)) | (Obj::Str(b, _), Obj::Name(a, _)) => **a == *b.bytes(), + (Obj::Bool(a), Obj::Bool(b)) => a == b, + (Obj::Null, Obj::Null) | (Obj::Mark, Obj::Mark) => true, + (Obj::Array(a, _), Obj::Array(b, _)) => a.same(b), + (Obj::Dict(a), Obj::Dict(b)) => a.same(b), + (Obj::Op(a), Obj::Op(b)) => a == b, + (Obj::File(a), Obj::File(b)) | (Obj::FontId(a), Obj::FontId(b)) | (Obj::Save(a), Obj::Save(b)) => a == b, + (Obj::GState(a), Obj::GState(b)) => a == b, + _ => false, + } + } +} diff --git a/crates/ps/src/read/ops/basic.rs b/crates/ps/src/read/ops/basic.rs new file mode 100644 index 0000000000..6760032ef8 --- /dev/null +++ b/crates/ps/src/read/ops/basic.rs @@ -0,0 +1,1080 @@ +//! The language: stack, arithmetic, relations, control, types, dictionaries, strings, arrays. + +use crate::read::interp::{Frame, Interp, PsErr, R, err}; +use crate::read::obj::{Key, Obj, PArr, PDict, PStr, name}; + +// ----- stack ----- + +pub fn pop(ip: &mut Interp) -> R { + ip.pop().map(|_| ()) +} + +pub fn exch(ip: &mut Interp) -> R { + ip.need(2)?; + let n = ip.o.len(); + ip.o.swap(n - 1, n - 2); + Ok(()) +} + +pub fn dup(ip: &mut Interp) -> R { + let o = ip.top(0)?.clone(); + ip.push(o) +} + +pub fn copy(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::Int(n) => { + let n = usize::try_from(n).map_err(|_| PsErr::Ps("rangecheck"))?; + ip.need(n)?; + let start = ip.o.len() - n; + ip.charge(n as u64)?; + ip.alloc(n.saturating_mul(OBJ_BYTES))?; + let items: Vec = ip.o.get(start..).map(<[Obj]>::to_vec).unwrap_or_default(); + for o in items { + ip.push(o)?; + } + Ok(()) + } + Obj::Array(dst, x) => { + let src = ip.pop_arr()?; + if src.len > dst.len { + return err("rangecheck"); + } + ip.charge(src.len as u64)?; + ip.alloc(src.len.saturating_mul(OBJ_BYTES))?; + for (i, o) in src.items().into_iter().enumerate() { + dst.set(i, o); + } + let sub = dst.interval(0, src.len).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Array(sub, x)) + } + Obj::Str(dst, x) => { + let src = ip.pop_str()?; + ip.charge_bytes(src.len)?; + ip.alloc(src.len)?; + let w = dst.put_bytes(0, &src.bytes()).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(w, x)) + } + Obj::Dict(dst) => { + let src = ip.pop_dict()?; + ip.charge(src.len() as u64)?; + ip.alloc(src.len().saturating_mul(2 * OBJ_BYTES))?; + for (k, v) in src.entries() { + dst.put(k, v); + } + ip.push(Obj::Dict(dst)) + } + _ => err("typecheck"), + } +} + +pub fn index(ip: &mut Interp) -> R { + let n = ip.pop_usize()?; + let o = ip.top(n).map_err(|_| PsErr::Ps("rangecheck"))?.clone(); + ip.push(o) +} + +pub fn roll(ip: &mut Interp) -> R { + let j = ip.pop_int()?; + let n = ip.pop_usize()?; + ip.need(n)?; + if n == 0 { + return Ok(()); + } + let start = ip.o.len() - n; + let k = usize::try_from(j.rem_euclid(n as i64)).unwrap_or(0); + if let Some(s) = ip.o.get_mut(start..) { + s.rotate_right(k); + } + Ok(()) +} + +pub fn clear(ip: &mut Interp) -> R { + ip.o.clear(); + Ok(()) +} + +pub fn count(ip: &mut Interp) -> R { + let n = ip.o.len() as i64; + ip.push(Obj::Int(n)) +} + +pub fn mark(ip: &mut Interp) -> R { + ip.push(Obj::Mark) +} + +/// Where the topmost mark is; the search is work (a deep stack takes long). +pub fn mark_pos(ip: &mut Interp) -> R { + let i = ip.o.iter().rposition(|o| matches!(o, Obj::Mark)); + ip.charge((ip.o.len() - i.map_or(0, |i| i + 1)) as u64)?; + i.ok_or(PsErr::Ps("unmatchedmark")) +} + +/// Memory one object takes, for charging copies of arrays and stacks. +pub const OBJ_BYTES: usize = 32; + +/// A string used as a dictionary key is copied into a name: work and memory. +pub fn key_cost(ip: &mut Interp, k: &Obj) -> R { + if let Obj::Str(s, _) = k { + ip.charge_bytes(s.len)?; + ip.alloc(s.len)?; + } + Ok(()) +} + +pub fn cleartomark(ip: &mut Interp) -> R { + let i = mark_pos(ip)?; + ip.o.truncate(i); + Ok(()) +} + +pub fn counttomark(ip: &mut Interp) -> R { + let i = mark_pos(ip)?; + let n = (ip.o.len() - i - 1) as i64; + ip.push(Obj::Int(n)) +} + +pub fn array_close(ip: &mut Interp) -> R { + let i = mark_pos(ip)?; + let items = ip.o.split_off(i + 1); + ip.o.pop(); + ip.alloc(items.len().saturating_mul(OBJ_BYTES))?; + ip.push(Obj::Array(PArr::new(items), false)) +} + +pub fn dict_close(ip: &mut Interp) -> R { + let i = mark_pos(ip)?; + let items = ip.o.split_off(i + 1); + ip.o.pop(); + if !items.len().is_multiple_of(2) { + return err("rangecheck"); + } + ip.alloc(items.len().saturating_mul(OBJ_BYTES))?; + for k in items.iter().step_by(2) { + key_cost(ip, k)?; + } + let d = PDict::new(items.len() / 2); + let mut it = items.into_iter(); + while let (Some(k), Some(v)) = (it.next(), it.next()) { + d.put(k, v); + } + ip.push(Obj::Dict(d)) +} + +// ----- arithmetic ----- + +fn arith(ip: &mut Interp, fi: fn(i64, i64) -> Option, fr: fn(f64, f64) -> f64) -> R { + let b = ip.pop()?; + let a = ip.pop()?; + match (&a, &b) { + (Obj::Int(x), Obj::Int(y)) => match fi(*x, *y).filter(|v| i32::try_from(*v).is_ok()) { + Some(v) => ip.push(Obj::Int(v)), + None => ip.push(Obj::Real(fr(*x as f64, *y as f64))), + }, + _ => { + let (Some(x), Some(y)) = (a.num(), b.num()) else { return err("typecheck") }; + let v = fr(x, y); + if !v.is_finite() { + return err("undefinedresult"); + } + ip.push(Obj::Real(v)) + } + } +} + +pub fn add(ip: &mut Interp) -> R { + arith(ip, i64::checked_add, |a, b| a + b) +} + +pub fn sub(ip: &mut Interp) -> R { + arith(ip, i64::checked_sub, |a, b| a - b) +} + +pub fn mul(ip: &mut Interp) -> R { + arith(ip, i64::checked_mul, |a, b| a * b) +} + +pub fn div(ip: &mut Interp) -> R { + let b = ip.pop_num()?; + let a = ip.pop_num()?; + if b == 0.0 { + return err("undefinedresult"); + } + ip.push(Obj::Real(a / b)) +} + +pub fn idiv(ip: &mut Interp) -> R { + let b = ip.pop_int()?; + let a = ip.pop_int()?; + let v = a.checked_div(b).ok_or(PsErr::Ps("undefinedresult"))?; + ip.push(Obj::Int(v)) +} + +pub fn mod_(ip: &mut Interp) -> R { + let b = ip.pop_int()?; + let a = ip.pop_int()?; + let v = a.checked_rem(b).ok_or(PsErr::Ps("undefinedresult"))?; + ip.push(Obj::Int(v)) +} + +fn unary(ip: &mut Interp, fi: fn(i64) -> Option, fr: fn(f64) -> f64) -> R { + match ip.pop()? { + Obj::Int(i) => match fi(i) { + Some(v) => ip.push(Obj::Int(v)), + None => ip.push(Obj::Real(fr(i as f64))), + }, + Obj::Real(r) => ip.push(Obj::Real(fr(r))), + _ => err("typecheck"), + } +} + +pub fn neg(ip: &mut Interp) -> R { + unary(ip, i64::checked_neg, |r| -r) +} + +pub fn abs(ip: &mut Interp) -> R { + unary(ip, i64::checked_abs, f64::abs) +} + +pub fn ceiling(ip: &mut Interp) -> R { + unary(ip, Some, f64::ceil) +} + +pub fn floor(ip: &mut Interp) -> R { + unary(ip, Some, f64::floor) +} + +pub fn round(ip: &mut Interp) -> R { + // PostScript rounds halves up (toward +∞). + unary(ip, Some, |r| (r + 0.5).floor()) +} + +pub fn truncate(ip: &mut Interp) -> R { + unary(ip, Some, f64::trunc) +} + +fn real_fn(ip: &mut Interp, f: fn(f64) -> f64) -> R { + let a = ip.pop_num()?; + let v = f(a); + if !v.is_finite() { + return err("undefinedresult"); + } + ip.push(Obj::Real(v)) +} + +pub fn sqrt(ip: &mut Interp) -> R { + let a = ip.pop_num()?; + if a < 0.0 { + return err("rangecheck"); + } + ip.push(Obj::Real(a.sqrt())) +} + +pub fn sin(ip: &mut Interp) -> R { + real_fn(ip, |d| d.to_radians().sin()) +} + +pub fn cos(ip: &mut Interp) -> R { + real_fn(ip, |d| d.to_radians().cos()) +} + +pub fn atan(ip: &mut Interp) -> R { + let den = ip.pop_num()?; + let num = ip.pop_num()?; + if num == 0.0 && den == 0.0 { + return err("undefinedresult"); + } + let mut a = num.atan2(den).to_degrees(); + if a < 0.0 { + a += 360.0; + } + ip.push(Obj::Real(a)) +} + +pub fn exp(ip: &mut Interp) -> R { + let e = ip.pop_num()?; + let b = ip.pop_num()?; + let v = b.powf(e); + if !v.is_finite() { + return err("undefinedresult"); + } + ip.push(Obj::Real(v)) +} + +pub fn ln(ip: &mut Interp) -> R { + let a = ip.pop_num()?; + if a <= 0.0 { + return err("rangecheck"); + } + ip.push(Obj::Real(a.ln())) +} + +pub fn log(ip: &mut Interp) -> R { + let a = ip.pop_num()?; + if a <= 0.0 { + return err("rangecheck"); + } + ip.push(Obj::Real(a.log10())) +} + +pub fn rand(ip: &mut Interp) -> R { + // A fixed, repeatable sequence (no entropy in conversions). + ip.steps = ip.steps.wrapping_add(1); + let v = (ip.steps.wrapping_mul(1_103_515_245).wrapping_add(12345) >> 1) & 0x7FFF_FFFF; + ip.push(Obj::Int(v as i64)) +} + +pub fn srand(ip: &mut Interp) -> R { + ip.pop_int().map(|_| ()) +} + +pub fn rrand(ip: &mut Interp) -> R { + ip.push(Obj::Int(0)) +} + +// ----- relations, booleans, bits ----- + +/// Comparing strings compares (and copies) their bytes. +fn compare_cost(ip: &mut Interp, a: &Obj, b: &Obj) -> R { + let len = |o: &Obj| match o { + Obj::Str(s, _) => s.len, + Obj::Name(n, _) => n.len(), + _ => 0, + }; + if matches!(a, Obj::Str(..)) || matches!(b, Obj::Str(..)) { + let n = len(a).saturating_add(len(b)); + ip.charge_bytes(n)?; + ip.alloc(n)?; + } + Ok(()) +} + +pub fn eq(ip: &mut Interp) -> R { + let b = ip.pop()?; + let a = ip.pop()?; + compare_cost(ip, &a, &b)?; + ip.push(Obj::Bool(a.ps_eq(&b))) +} + +pub fn ne(ip: &mut Interp) -> R { + let b = ip.pop()?; + let a = ip.pop()?; + compare_cost(ip, &a, &b)?; + ip.push(Obj::Bool(!a.ps_eq(&b))) +} + +fn compare(ip: &mut Interp, f: fn(std::cmp::Ordering) -> bool) -> R { + let b = ip.pop()?; + let a = ip.pop()?; + compare_cost(ip, &a, &b)?; + let ord = match (&a, &b) { + (Obj::Str(x, _), Obj::Str(y, _)) => x.bytes().cmp(&y.bytes()), + _ => { + let (Some(x), Some(y)) = (a.num(), b.num()) else { return err("typecheck") }; + x.partial_cmp(&y).ok_or(PsErr::Ps("undefinedresult"))? + } + }; + ip.push(Obj::Bool(f(ord))) +} + +pub fn gt(ip: &mut Interp) -> R { + compare(ip, |o| o.is_gt()) +} + +pub fn ge(ip: &mut Interp) -> R { + compare(ip, |o| o.is_ge()) +} + +pub fn lt(ip: &mut Interp) -> R { + compare(ip, |o| o.is_lt()) +} + +pub fn le(ip: &mut Interp) -> R { + compare(ip, |o| o.is_le()) +} + +fn logic(ip: &mut Interp, fb: fn(bool, bool) -> bool, fi: fn(i64, i64) -> i64) -> R { + let b = ip.pop()?; + let a = ip.pop()?; + match (a, b) { + (Obj::Bool(x), Obj::Bool(y)) => ip.push(Obj::Bool(fb(x, y))), + (Obj::Int(x), Obj::Int(y)) => ip.push(Obj::Int(fi(x, y))), + _ => err("typecheck"), + } +} + +pub fn and(ip: &mut Interp) -> R { + logic(ip, |a, b| a & b, |a, b| a & b) +} + +pub fn or(ip: &mut Interp) -> R { + logic(ip, |a, b| a | b, |a, b| a | b) +} + +pub fn xor(ip: &mut Interp) -> R { + logic(ip, |a, b| a ^ b, |a, b| a ^ b) +} + +pub fn not(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::Bool(b) => ip.push(Obj::Bool(!b)), + Obj::Int(i) => ip.push(Obj::Int(!i)), + _ => err("typecheck"), + } +} + +pub fn bitshift(ip: &mut Interp) -> R { + let s = ip.pop_int()?; + let v = ip.pop_int()? as i32 as u32; + let r = if s >= 0 { v.checked_shl(s.min(32) as u32).unwrap_or(0) } else { v.checked_shr((-s).min(32) as u32).unwrap_or(0) }; + ip.push(Obj::Int(i64::from(r as i32))) +} + +pub fn true_(ip: &mut Interp) -> R { + ip.push(Obj::Bool(true)) +} + +pub fn false_(ip: &mut Interp) -> R { + ip.push(Obj::Bool(false)) +} + +pub fn null(ip: &mut Interp) -> R { + ip.push(Obj::Null) +} + +// ----- control ----- + +pub fn exec(ip: &mut Interp) -> R { + let o = ip.pop()?; + ip.exec_value(o) +} + +pub fn if_(ip: &mut Interp) -> R { + let p = ip.pop()?; + if ip.pop_bool()? { ip.exec_value(p) } else { Ok(()) } +} + +pub fn ifelse(ip: &mut Interp) -> R { + let pf = ip.pop()?; + let pt = ip.pop()?; + if ip.pop_bool()? { ip.exec_value(pt) } else { ip.exec_value(pf) } +} + +pub fn for_(ip: &mut Interp) -> R { + let proc = ip.pop()?; + let limit = ip.pop()?; + let inc = ip.pop()?; + let init = ip.pop()?; + let int = matches!((&init, &inc), (Obj::Int(_), Obj::Int(_))); + let (Some(cur), Some(inc), Some(limit)) = (init.num(), inc.num(), limit.num()) else { return err("typecheck") }; + if inc == 0.0 { + // Runs forever in PostScript; the step limit would end it. Treat as empty. + return Ok(()); + } + ip.push_frame(Frame::For { cur, inc, limit, int, proc }) +} + +pub fn repeat(ip: &mut Interp) -> R { + let proc = ip.pop()?; + let n = ip.pop_int()?; + if n < 0 { + return err("rangecheck"); + } + ip.push_frame(Frame::Repeat { n, proc }) +} + +pub fn loop_(ip: &mut Interp) -> R { + let proc = ip.pop()?; + ip.push_frame(Frame::Loop { proc }) +} + +pub fn exit(ip: &mut Interp) -> R { + let depth = ip.e.len() as u64; + ip.charge(depth / 16)?; + let i = ip.e.iter().rposition(|f| f.is_loop() || matches!(f, Frame::Stopped | Frame::Barrier)).ok_or(PsErr::Ps("invalidexit"))?; + if !ip.e.get(i).is_some_and(Frame::is_loop) { + return err("invalidexit"); + } + ip.e.truncate(i); + Ok(()) +} + +pub fn stop(_ip: &mut Interp) -> R { + Err(PsErr::Stop) +} + +pub fn stopped(ip: &mut Interp) -> R { + let p = ip.pop()?; + ip.push_frame(Frame::Stopped)?; + ip.exec_value(p) +} + +pub fn quit(_ip: &mut Interp) -> R { + Err(PsErr::Quit) +} + +pub fn forall(ip: &mut Interp) -> R { + let proc = ip.pop()?; + match ip.pop()? { + Obj::Array(arr, _) => ip.push_frame(Frame::ForallArr { arr, i: 0, proc }), + Obj::Str(s, _) => ip.push_frame(Frame::ForallStr { s, i: 0, proc }), + Obj::Dict(d) => { + // The entries are copied first (the procedure may change the dictionary). + ip.charge(d.len() as u64)?; + ip.alloc(d.len().saturating_mul(2 * OBJ_BYTES))?; + ip.push_frame(Frame::ForallDict { items: d.entries(), i: 0, proc }) + } + _ => err("typecheck"), + } +} + +pub fn countexecstack(ip: &mut Interp) -> R { + let n = ip.e.len() as i64; + ip.push(Obj::Int(n)) +} + +pub fn execstack(ip: &mut Interp) -> R { + let a = ip.pop_arr()?; + let n = ip.e.len().min(a.len); + let sub = a.interval(0, n).ok_or(PsErr::Ps("rangecheck"))?; + ip.charge(n as u64)?; + for i in 0..n { + sub.set(i, Obj::Null); + } + ip.push(Obj::Array(sub, false)) +} + +// ----- types and conversion ----- + +pub fn type_(ip: &mut Interp) -> R { + let o = ip.pop()?; + ip.push(Obj::Name(name(o.type_name()), true)) +} + +pub fn cvlit(ip: &mut Interp) -> R { + let o = match ip.pop()? { + Obj::Name(n, _) => Obj::Name(n, false), + Obj::Array(a, _) => Obj::Array(a, false), + Obj::Str(s, _) => Obj::Str(s, false), + o => o, + }; + ip.push(o) +} + +pub fn cvx(ip: &mut Interp) -> R { + let o = match ip.pop()? { + Obj::Name(n, _) => Obj::Name(n, true), + Obj::Array(a, _) => Obj::Array(a, true), + Obj::Str(s, _) => Obj::Str(s, true), + o => o, + }; + ip.push(o) +} + +pub fn xcheck(ip: &mut Interp) -> R { + let o = ip.pop()?; + ip.push(Obj::Bool(o.is_exec())) +} + +/// Access attributes aren't enforced: everything stays readable and writable. +pub fn noop_keep(ip: &mut Interp) -> R { + ip.top(0).map(|_| ()) +} + +pub fn check_true(ip: &mut Interp) -> R { + ip.pop()?; + ip.push(Obj::Bool(true)) +} + +pub fn cvi(ip: &mut Interp) -> R { + let v = match ip.pop()? { + Obj::Int(i) => i, + Obj::Real(r) if r.is_finite() && r.abs() < 2.2e9 => r.trunc() as i64, + Obj::Str(s, _) if s.len > 4096 => return err("limitcheck"), + Obj::Str(s, _) => match crate::read::scan::number(&s.bytes()) { + Some(o) => o.num().filter(|v| v.abs() < 2.2e9).ok_or(PsErr::Ps("rangecheck"))?.trunc() as i64, + None => return err("syntaxerror"), + }, + Obj::Real(_) => return err("rangecheck"), + _ => return err("typecheck"), + }; + ip.push(Obj::Int(v)) +} + +pub fn cvr(ip: &mut Interp) -> R { + let v = match ip.pop()? { + o @ (Obj::Int(_) | Obj::Real(_)) => o.num().unwrap_or(0.0), + Obj::Str(s, _) if s.len > 4096 => return err("limitcheck"), + Obj::Str(s, _) => crate::read::scan::number(&s.bytes()).and_then(|o| o.num()).ok_or(PsErr::Ps("syntaxerror"))?, + _ => return err("typecheck"), + }; + ip.push(Obj::Real(v)) +} + +pub fn cvn(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::Str(s, x) => { + ip.charge_bytes(s.len)?; + ip.alloc(s.len)?; + ip.push(Obj::Name(name(&s.bytes()), x)) + } + n @ Obj::Name(..) => ip.push(n), + _ => err("typecheck"), + } +} + +/// The text `cvs`/`=` print for an object. +pub fn text_of(o: &Obj) -> Vec { + match o { + Obj::Int(i) => i.to_string().into_bytes(), + Obj::Real(r) => { + if r.fract() == 0.0 && r.abs() < 1e15 { + format!("{r:.1}").into_bytes() + } else { + format!("{r}").into_bytes() + } + } + Obj::Bool(b) => { + if *b { + b"true".to_vec() + } else { + b"false".to_vec() + } + } + Obj::Name(n, _) => n.to_vec(), + Obj::Str(s, _) => s.bytes(), + Obj::Op(op) => crate::read::ops::OPS.get(*op as usize).map(|(n, _)| n.to_vec()).unwrap_or_default(), + _ => b"--nostringval--".to_vec(), + } +} + +pub fn cvs(ip: &mut Interp) -> R { + let dst = ip.pop_str()?; + let o = ip.pop()?; + ip.charge_bytes(dst.len)?; + let t = text_of(&o); + let w = dst.put_bytes(0, &t).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(w, false)) +} + +pub fn cvrs(ip: &mut Interp) -> R { + let dst = ip.pop_str()?; + let radix = ip.pop_int()?; + let n = ip.pop_num()?; + if !(2..=36).contains(&radix) { + return err("rangecheck"); + } + let t = if radix == 10 { + text_of(&Obj::Real(n)) + } else { + let mut v = (n as i64 as i32) as u32; + let mut digits = Vec::new(); + loop { + let d = (v % radix as u32) as u8; + digits.push(if d < 10 { b'0' + d } else { b'A' + d - 10 }); + v /= radix as u32; + if v == 0 { + break; + } + } + digits.reverse(); + digits + }; + let w = dst.put_bytes(0, &t).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(w, false)) +} + +// ----- dictionaries ----- + +pub fn dict(ip: &mut Interp) -> R { + let n = ip.pop_usize()?; + ip.alloc(64)?; + ip.push(Obj::Dict(PDict::new(n.min(100_000)))) +} + +pub fn maxlength(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + let n = d.0.borrow().cap.max(d.len()) as i64; + ip.push(Obj::Int(n)) +} + +pub fn begin(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + ip.begin(d) +} + +pub fn end(ip: &mut Interp) -> R { + // systemdict, globaldict and userdict stay. + if ip.d.len() <= 3 { + return err("dictstackunderflow"); + } + ip.d.pop(); + Ok(()) +} + +pub fn def(ip: &mut Interp) -> R { + let v = ip.pop()?; + let k = ip.pop()?; + key_cost(ip, &k)?; + ip.alloc(2 * OBJ_BYTES)?; + ip.current_dict()?.put(k, v); + Ok(()) +} + +pub fn load(ip: &mut Interp) -> R { + let k = ip.pop()?; + key_cost(ip, &k)?; + let Some(v) = ip.lookup(&k.key()) else { + ip.dollar_error.put_name(b"offending", k); + return err("undefined"); + }; + ip.push(v) +} + +pub fn store(ip: &mut Interp) -> R { + let v = ip.pop()?; + let k = ip.pop()?; + key_cost(ip, &k)?; + let key = k.key(); + let d = ip.d.iter().rev().find(|d| d.known(&key)).cloned().map_or_else(|| ip.current_dict(), Ok)?; + d.put(k, v); + Ok(()) +} + +pub fn get(ip: &mut Interp) -> R { + let k = ip.pop()?; + key_cost(ip, &k)?; + match ip.pop()? { + Obj::Dict(d) => { + let Some(v) = d.get(&k.key()) else { + ip.dollar_error.put_name(b"offending", k); + return err("undefined"); + }; + ip.push(v) + } + Obj::Array(a, _) => { + let i = usize::try_from(int_of(&k)?).map_err(|_| PsErr::Ps("rangecheck"))?; + let v = a.get(i).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(v) + } + Obj::Str(s, _) => { + let i = usize::try_from(int_of(&k)?).map_err(|_| PsErr::Ps("rangecheck"))?; + let v = s.get(i).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Int(i64::from(v))) + } + _ => err("typecheck"), + } +} + +fn int_of(o: &Obj) -> R { + match o { + Obj::Int(i) => Ok(*i), + _ => err("typecheck"), + } +} + +pub fn put(ip: &mut Interp) -> R { + let v = ip.pop()?; + let k = ip.pop()?; + match ip.pop()? { + Obj::Dict(d) => { + key_cost(ip, &k)?; + ip.alloc(2 * OBJ_BYTES)?; + d.put(k, v); + Ok(()) + } + Obj::Array(a, _) => { + let i = usize::try_from(int_of(&k)?).map_err(|_| PsErr::Ps("rangecheck"))?; + if a.set(i, v) { Ok(()) } else { err("rangecheck") } + } + Obj::Str(s, _) => { + let i = usize::try_from(int_of(&k)?).map_err(|_| PsErr::Ps("rangecheck"))?; + let b = u8::try_from(int_of(&v)? & 0xFF).unwrap_or(0); + if s.set(i, b) { Ok(()) } else { err("rangecheck") } + } + _ => err("typecheck"), + } +} + +pub fn known(ip: &mut Interp) -> R { + let k = ip.pop()?; + key_cost(ip, &k)?; + let d = ip.pop_dict()?; + ip.push(Obj::Bool(d.known(&k.key()))) +} + +pub fn undef(ip: &mut Interp) -> R { + let k = ip.pop()?; + key_cost(ip, &k)?; + let d = ip.pop_dict()?; + d.remove(&k.key()); + Ok(()) +} + +pub fn where_(ip: &mut Interp) -> R { + let k = ip.pop()?; + key_cost(ip, &k)?; + ip.charge(ip.d.len() as u64 / 16)?; + let k = k.key(); + match ip.d.iter().rev().find(|d| d.known(&k)).cloned() { + Some(d) => { + ip.push(Obj::Dict(d))?; + ip.push(Obj::Bool(true)) + } + None => ip.push(Obj::Bool(false)), + } +} + +pub fn currentdict(ip: &mut Interp) -> R { + let d = ip.current_dict()?; + ip.push(Obj::Dict(d)) +} + +pub fn countdictstack(ip: &mut Interp) -> R { + let n = ip.d.len() as i64; + ip.push(Obj::Int(n)) +} + +pub fn dictstack(ip: &mut Interp) -> R { + let a = ip.pop_arr()?; + let n = ip.d.len().min(a.len); + for (i, d) in ip.d.iter().take(n).enumerate() { + a.set(i, Obj::Dict(d.clone())); + } + let sub = a.interval(0, n).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Array(sub, false)) +} + +pub fn cleardictstack(ip: &mut Interp) -> R { + ip.d.truncate(3); + Ok(()) +} + +// ----- strings and arrays ----- + +pub fn array(ip: &mut Interp) -> R { + let n = ip.pop_usize()?; + ip.alloc(n.saturating_mul(OBJ_BYTES))?; + ip.charge(n as u64)?; + ip.push(Obj::Array(PArr::new(vec![Obj::Null; n]), false)) +} + +pub fn string(ip: &mut Interp) -> R { + let n = ip.pop_usize()?; + ip.alloc(n)?; + ip.charge_bytes(n)?; + ip.push(Obj::Str(PStr::new(vec![0; n]), false)) +} + +pub fn length(ip: &mut Interp) -> R { + let n = match ip.pop()? { + Obj::Array(a, _) => a.len, + Obj::Str(s, _) => s.len, + Obj::Dict(d) => d.len(), + Obj::Name(n, _) => n.len(), + _ => return err("typecheck"), + }; + ip.push(Obj::Int(n as i64)) +} + +pub fn getinterval(ip: &mut Interp) -> R { + let count = ip.pop_usize()?; + let start = ip.pop_usize()?; + match ip.pop()? { + Obj::Array(a, x) => { + let s = a.interval(start, count).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Array(s, x)) + } + Obj::Str(s, x) => { + let s = s.interval(start, count).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(s, x)) + } + _ => err("typecheck"), + } +} + +pub fn putinterval(ip: &mut Interp) -> R { + let src = ip.pop()?; + let start = ip.pop_usize()?; + match (ip.pop()?, src) { + (Obj::Array(dst, _), Obj::Array(src, _)) => { + if start.checked_add(src.len).is_none_or(|e| e > dst.len) { + return err("rangecheck"); + } + ip.charge(src.len as u64)?; + ip.alloc(src.len.saturating_mul(OBJ_BYTES))?; + for (i, o) in src.items().into_iter().enumerate() { + dst.set(start + i, o); + } + Ok(()) + } + (Obj::Str(dst, _), Obj::Str(src, _)) => { + ip.charge_bytes(src.len)?; + ip.alloc(src.len)?; + dst.put_bytes(start, &src.bytes()).map(|_| ()).ok_or(PsErr::Ps("rangecheck")) + } + _ => err("typecheck"), + } +} + +pub fn aload(ip: &mut Interp) -> R { + let o = ip.pop()?; + let Obj::Array(a, _) = &o else { return err("typecheck") }; + ip.charge(a.len as u64)?; + ip.alloc(a.len.saturating_mul(OBJ_BYTES))?; + for v in a.items() { + ip.push(v)?; + } + ip.push(o) +} + +pub fn astore(ip: &mut Interp) -> R { + let o = ip.pop()?; + let Obj::Array(a, _) = &o else { return err("typecheck") }; + ip.need(a.len)?; + ip.charge(a.len as u64)?; + let start = ip.o.len() - a.len; + let items = ip.o.split_off(start); + for (i, v) in items.into_iter().enumerate() { + a.set(i, v); + } + ip.push(o) +} + +pub fn packedarray(ip: &mut Interp) -> R { + let n = ip.pop_usize()?; + ip.need(n)?; + ip.charge(n as u64)?; + ip.alloc(n.saturating_mul(OBJ_BYTES))?; + let start = ip.o.len() - n; + let items = ip.o.split_off(start); + ip.push(Obj::Array(PArr::new(items), false)) +} + +pub fn search(ip: &mut Interp) -> R { + let seek = ip.pop_str()?.bytes(); + let s = ip.pop_str()?; + ip.charge_bytes(s.len.saturating_add(seek.len()))?; + ip.alloc(s.len.saturating_add(seek.len()))?; + let hay = s.bytes(); + let found = find(ip, &hay, &seek)?; + match found { + Some(i) => { + let post = s.interval(i + seek.len(), hay.len() - i - seek.len()).ok_or(PsErr::Ps("rangecheck"))?; + let m = s.interval(i, seek.len()).ok_or(PsErr::Ps("rangecheck"))?; + let pre = s.interval(0, i).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(post, false))?; + ip.push(Obj::Str(m, false))?; + ip.push(Obj::Str(pre, false))?; + ip.push(Obj::Bool(true)) + } + None => { + ip.push(Obj::Str(s, false))?; + ip.push(Obj::Bool(false)) + } + } +} + +/// Where `seek` first occurs in `hay`, charging the bytes compared as it goes (a naive search +/// costs |hay| × |seek| on inputs made to defeat it). +fn find(ip: &mut Interp, hay: &[u8], seek: &[u8]) -> R> { + if seek.is_empty() { + return Ok(Some(0)); + } + let mut compared = 0usize; + for (i, w) in hay.windows(seek.len()).enumerate() { + let same = w.iter().zip(seek).take_while(|(a, b)| a == b).count(); + compared += same + 1; + if same == seek.len() { + ip.charge_bytes(compared)?; + return Ok(Some(i)); + } + if compared >= 1 << 16 { + ip.charge_bytes(compared)?; + compared = 0; + } + } + ip.charge_bytes(compared)?; + Ok(None) +} + +pub fn anchorsearch(ip: &mut Interp) -> R { + let seek = ip.pop_str()?.bytes(); + let s = ip.pop_str()?; + ip.charge_bytes(s.len.saturating_add(seek.len()))?; + ip.alloc(s.len.saturating_add(seek.len()))?; + if s.bytes().starts_with(&seek) { + let post = s.interval(seek.len(), s.len - seek.len()).ok_or(PsErr::Ps("rangecheck"))?; + let m = s.interval(0, seek.len()).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(post, false))?; + ip.push(Obj::Str(m, false))?; + ip.push(Obj::Bool(true)) + } else { + ip.push(Obj::Str(s, false))?; + ip.push(Obj::Bool(false)) + } +} + +/// `token` on a string: the first object and the rest. +pub fn token(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::Str(s, _) => { + ip.alloc(crate::read::interp::SOURCE_BYTES)?; + let id = ip.srcs.add(crate::read::source::Source::string(&s)); + let d = &ip.d; + let mut resolve = |n: &[u8]| d.iter().rev().find_map(|dict| dict.get(&Key::Name(name(n)))); + match crate::read::scan::next(&mut ip.srcs, id, &mut resolve) { + Ok(Some(o)) => { + let used = ip.srcs.position(id).unwrap_or(s.len).min(s.len); + let rest = s.interval(used, s.len - used).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(rest, false))?; + ip.push(o)?; + ip.push(Obj::Bool(true)) + } + Ok(None) => ip.push(Obj::Bool(false)), + Err(_) => err("syntaxerror"), + } + } + Obj::File(id) => { + let d = &ip.d; + let mut resolve = |n: &[u8]| d.iter().rev().find_map(|dict| dict.get(&Key::Name(name(n)))); + match crate::read::scan::next(&mut ip.srcs, id, &mut resolve) { + Ok(Some(o)) => { + ip.push(o)?; + ip.push(Obj::Bool(true)) + } + Ok(None) => ip.push(Obj::Bool(false)), + Err(_) => err("syntaxerror"), + } + } + _ => err("typecheck"), + } +} + +/// `bind`: executable names that name operators become the operators, through nested +/// procedures (each procedure once). +pub fn bind(ip: &mut Interp) -> R { + let o = ip.top(0)?.clone(); + let Obj::Array(root, _) = o else { return Ok(()) }; + let mut todo = vec![root]; + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + while let Some(a) = todo.pop() { + let id = std::rc::Rc::as_ptr(&a.buf) as *const u8 as usize; + if seen.len() > 100_000 || !seen.insert(id) { + continue; + } + ip.charge(a.len as u64)?; + for i in 0..a.len { + match a.get(i) { + Some(Obj::Name(n, true)) => { + if let Some(Obj::Op(op)) = ip.lookup(&Key::Name(n)) { + a.set(i, Obj::Op(op)); + } + } + Some(Obj::Array(sub, true)) => todo.push(sub), + _ => {} + } + } + } + Ok(()) +} diff --git a/crates/ps/src/read/ops/gfx.rs b/crates/ps/src/read/ops/gfx.rs new file mode 100644 index 0000000000..83a9e17d6b --- /dev/null +++ b/crates/ps/src/read/ops/gfx.rs @@ -0,0 +1,1248 @@ +//! Graphics: the graphics state, matrices, paths, painting, clipping and colour. + +use std::rc::Rc; + +use crate::read::gstate::{Clip, DevColor, GState, M, MAX_PATH, Seg, Space, arc_curves, bbox, flatten}; +use crate::read::interp::{Interp, PsErr, R, err, matrix_obj}; +use crate::read::obj::{Obj, PArr, PDict}; +use crate::read::pdf::StrokeParams; + +// ----- graphics-state stack ----- + +pub fn gsave(ip: &mut Interp) -> R { + ip.gsave() +} + +pub fn grestore(ip: &mut Interp) -> R { + ip.grestore(); + Ok(()) +} + +pub fn grestoreall(ip: &mut Interp) -> R { + let floor = ip.saves.last().copied().unwrap_or(0); + while ip.gstack.len() > floor { + ip.grestore(); + } + Ok(()) +} + +pub fn initgraphics(ip: &mut Interp) -> R { + let font = ip.gs.font.clone(); + ip.gs = GState { font, ..GState::default() }; + Ok(()) +} + +pub fn save(ip: &mut Interp) -> R { + ip.gsave()?; + ip.saves.push(ip.gstack.len()); + let n = ip.saves.len(); + ip.push(Obj::Save(n)) +} + +pub fn restore(ip: &mut Interp) -> R { + let Obj::Save(n) = ip.pop()? else { return err("typecheck") }; + if n == 0 || n > ip.saves.len() { + return err("invalidrestore"); + } + let depth = ip.saves.get(n - 1).copied().unwrap_or(0); + ip.saves.truncate(n - 1); + // Back to the state saved with it (save did a gsave, leaving `depth` states stacked). + while ip.gstack.len() >= depth.max(1) { + let Some(g) = ip.gstack.pop() else { break }; + ip.gs = g; + } + Ok(()) +} + +pub fn gstate(ip: &mut Interp) -> R { + ip.charge_gstate()?; + if ip.gstates.len() > 100_000 { + return err("limitcheck"); + } + ip.gstates.push(ip.gs.clone()); + let n = ip.gstates.len() - 1; + ip.push(Obj::GState(n)) +} + +pub fn currentgstate(ip: &mut Interp) -> R { + ip.charge_gstate()?; + let Obj::GState(n) = ip.pop()? else { return err("typecheck") }; + if let Some(slot) = ip.gstates.get_mut(n) { + *slot = ip.gs.clone(); + } + ip.push(Obj::GState(n)) +} + +pub fn setgstate(ip: &mut Interp) -> R { + ip.charge_gstate()?; + let Obj::GState(n) = ip.pop()? else { return err("typecheck") }; + if let Some(g) = ip.gstates.get(n) { + ip.gs = g.clone(); + } + Ok(()) +} + +// ----- line parameters ----- + +pub fn setlinewidth(ip: &mut Interp) -> R { + ip.gs.line_width = ip.pop_num()?.abs(); + Ok(()) +} + +pub fn currentlinewidth(ip: &mut Interp) -> R { + let w = ip.gs.line_width; + ip.push(Obj::Real(w)) +} + +pub fn setlinecap(ip: &mut Interp) -> R { + ip.gs.cap = ip.pop_int()?.clamp(0, 2); + Ok(()) +} + +pub fn currentlinecap(ip: &mut Interp) -> R { + let v = ip.gs.cap; + ip.push(Obj::Int(v)) +} + +pub fn setlinejoin(ip: &mut Interp) -> R { + ip.gs.join = ip.pop_int()?.clamp(0, 2); + Ok(()) +} + +pub fn currentlinejoin(ip: &mut Interp) -> R { + let v = ip.gs.join; + ip.push(Obj::Int(v)) +} + +pub fn setmiterlimit(ip: &mut Interp) -> R { + ip.gs.miter = ip.pop_num()?.max(1.0); + Ok(()) +} + +pub fn currentmiterlimit(ip: &mut Interp) -> R { + let v = ip.gs.miter; + ip.push(Obj::Real(v)) +} + +pub fn setdash(ip: &mut Interp) -> R { + let phase = ip.pop_num()?; + let a = ip.pop_arr()?; + let mut v = Vec::with_capacity(a.len.min(64)); + for o in a.items().iter().take(64) { + v.push(o.num().ok_or(PsErr::Ps("typecheck"))?); + } + ip.gs.dash = (v, phase); + Ok(()) +} + +pub fn currentdash(ip: &mut Interp) -> R { + let (v, phase) = ip.gs.dash.clone(); + ip.push(Obj::Array(PArr::new(v.into_iter().map(Obj::Real).collect()), false))?; + ip.push(Obj::Real(phase)) +} + +pub fn setflat(ip: &mut Interp) -> R { + ip.gs.flat = ip.pop_num()?.clamp(0.2, 100.0); + Ok(()) +} + +pub fn currentflat(ip: &mut Interp) -> R { + let v = ip.gs.flat; + ip.push(Obj::Real(v)) +} + +pub fn setstrokeadjust(ip: &mut Interp) -> R { + ip.gs.stroke_adjust = ip.pop_bool()?; + Ok(()) +} + +pub fn currentstrokeadjust(ip: &mut Interp) -> R { + let v = ip.gs.stroke_adjust; + ip.push(Obj::Bool(v)) +} + +pub fn setoverprint(ip: &mut Interp) -> R { + ip.gs.overprint = ip.pop_bool()?; + Ok(()) +} + +pub fn currentoverprint(ip: &mut Interp) -> R { + let v = ip.gs.overprint; + ip.push(Obj::Bool(v)) +} + +// ----- matrices ----- + +fn fill_matrix(a: &PArr, m: &M) -> R { + if a.len != 6 { + return err("rangecheck"); + } + for (i, v) in m.0.iter().enumerate() { + a.set(i, Obj::Real(*v)); + } + Ok(()) +} + +pub fn matrix(ip: &mut Interp) -> R { + ip.push(matrix_obj(&M::ID)) +} + +pub fn identmatrix(ip: &mut Interp) -> R { + let a = ip.pop_arr()?; + fill_matrix(&a, &M::ID)?; + ip.push(Obj::Array(a, false)) +} + +pub fn defaultmatrix(ip: &mut Interp) -> R { + identmatrix(ip) +} + +pub fn initmatrix(ip: &mut Interp) -> R { + ip.gs.ctm = M::ID; + Ok(()) +} + +pub fn currentmatrix(ip: &mut Interp) -> R { + let a = ip.pop_arr()?; + fill_matrix(&a, &ip.gs.ctm)?; + ip.push(Obj::Array(a, false)) +} + +pub fn setmatrix(ip: &mut Interp) -> R { + let m = ip.pop_matrix()?; + if !m.finite() { + return err("undefinedresult"); + } + ip.gs.ctm = m; + Ok(()) +} + +/// Operators that either change the CTM or, given a matrix operand, fill it in. +fn modify(ip: &mut Interp, args: usize, make: fn(&[f64]) -> M) -> R { + if let Ok(Obj::Array(a, _)) = ip.top(0).cloned() { + ip.pop()?; + let mut v = vec![0.0; args]; + for slot in v.iter_mut().rev() { + *slot = ip.pop_num()?; + } + fill_matrix(&a, &make(&v))?; + return ip.push(Obj::Array(a, false)); + } + let mut v = vec![0.0; args]; + for slot in v.iter_mut().rev() { + *slot = ip.pop_num()?; + } + let m = make(&v).then(&ip.gs.ctm); + if !m.finite() { + return err("undefinedresult"); + } + ip.gs.ctm = m; + Ok(()) +} + +fn arg(v: &[f64], i: usize) -> f64 { + v.get(i).copied().unwrap_or(0.0) +} + +pub fn translate(ip: &mut Interp) -> R { + modify(ip, 2, |v| M([1.0, 0.0, 0.0, 1.0, arg(v, 0), arg(v, 1)])) +} + +pub fn scale(ip: &mut Interp) -> R { + modify(ip, 2, |v| M([arg(v, 0), 0.0, 0.0, arg(v, 1), 0.0, 0.0])) +} + +pub fn rotate(ip: &mut Interp) -> R { + modify(ip, 1, |v| { + let a = arg(v, 0).to_radians(); + let (s, c) = a.sin_cos(); + M([c, s, -s, c, 0.0, 0.0]) + }) +} + +pub fn concat(ip: &mut Interp) -> R { + let m = ip.pop_matrix()?; + let r = m.then(&ip.gs.ctm); + if !r.finite() { + return err("undefinedresult"); + } + ip.gs.ctm = r; + Ok(()) +} + +pub fn concatmatrix(ip: &mut Interp) -> R { + let dst = ip.pop_arr()?; + let m2 = ip.pop_matrix()?; + let m1 = ip.pop_matrix()?; + fill_matrix(&dst, &m1.then(&m2))?; + ip.push(Obj::Array(dst, false)) +} + +pub fn invertmatrix(ip: &mut Interp) -> R { + let dst = ip.pop_arr()?; + let m = ip.pop_matrix()?; + let inv = m.inv().ok_or(PsErr::Ps("undefinedresult"))?; + fill_matrix(&dst, &inv)?; + ip.push(Obj::Array(dst, false)) +} + +fn xform(ip: &mut Interp, f: fn(&M, f64, f64) -> Option<(f64, f64)>) -> R { + let m = match ip.top(0)? { + Obj::Array(..) => ip.pop_matrix()?, + _ => ip.gs.ctm, + }; + let y = ip.pop_num()?; + let x = ip.pop_num()?; + let (x, y) = f(&m, x, y).ok_or(PsErr::Ps("undefinedresult"))?; + ip.push(Obj::Real(x))?; + ip.push(Obj::Real(y)) +} + +pub fn transform(ip: &mut Interp) -> R { + xform(ip, |m, x, y| Some(m.apply(x, y))) +} + +pub fn itransform(ip: &mut Interp) -> R { + xform(ip, |m, x, y| Some(m.inv()?.apply(x, y))) +} + +pub fn dtransform(ip: &mut Interp) -> R { + xform(ip, |m, x, y| Some(m.delta(x, y))) +} + +pub fn idtransform(ip: &mut Interp) -> R { + xform(ip, |m, x, y| Some(m.inv()?.delta(x, y))) +} + +// ----- paths ----- + +fn add_seg(ip: &mut Interp, s: Seg) -> R { + if ip.gs.path.len() >= MAX_PATH { + return err("limitcheck"); + } + ip.gs.path.push(s); + Ok(()) +} + +pub fn newpath(ip: &mut Interp) -> R { + ip.gs.path.clear(); + ip.gs.cur = None; + ip.gs.start = None; + Ok(()) +} + +fn user_point(ip: &mut Interp) -> R<(f64, f64)> { + let y = ip.pop_num()?; + let x = ip.pop_num()?; + Ok(ip.gs.ctm.apply(x, y)) +} + +fn rel_point(ip: &mut Interp) -> R<(f64, f64)> { + let dy = ip.pop_num()?; + let dx = ip.pop_num()?; + let (cx, cy) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let (dx, dy) = ip.gs.ctm.delta(dx, dy); + Ok((cx + dx, cy + dy)) +} + +fn move_dev(ip: &mut Interp, p: (f64, f64)) -> R { + // Consecutive movetos keep only the last. + if let Some(Seg::Move(..)) = ip.gs.path.last() { + ip.gs.path.pop(); + } + add_seg(ip, Seg::Move(p.0, p.1))?; + ip.gs.cur = Some(p); + ip.gs.start = Some(p); + Ok(()) +} + +fn line_dev(ip: &mut Interp, p: (f64, f64)) -> R { + if ip.gs.cur.is_none() { + return err("nocurrentpoint"); + } + if matches!(ip.gs.path.last(), Some(Seg::Close)) + && let Some(s) = ip.gs.start + { + add_seg(ip, Seg::Move(s.0, s.1))?; + } + add_seg(ip, Seg::Line(p.0, p.1))?; + ip.gs.cur = Some(p); + Ok(()) +} + +fn curve_dev(ip: &mut Interp, a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> R { + if ip.gs.cur.is_none() { + return err("nocurrentpoint"); + } + if matches!(ip.gs.path.last(), Some(Seg::Close)) + && let Some(s) = ip.gs.start + { + add_seg(ip, Seg::Move(s.0, s.1))?; + } + add_seg(ip, Seg::Curve(a.0, a.1, b.0, b.1, c.0, c.1))?; + ip.gs.cur = Some(c); + Ok(()) +} + +pub fn moveto(ip: &mut Interp) -> R { + let p = user_point(ip)?; + move_dev(ip, p) +} + +pub fn rmoveto(ip: &mut Interp) -> R { + let p = rel_point(ip)?; + move_dev(ip, p) +} + +pub fn lineto(ip: &mut Interp) -> R { + let p = user_point(ip)?; + line_dev(ip, p) +} + +pub fn rlineto(ip: &mut Interp) -> R { + let p = rel_point(ip)?; + line_dev(ip, p) +} + +pub fn curveto(ip: &mut Interp) -> R { + let c = user_point(ip)?; + let b = user_point(ip)?; + let a = user_point(ip)?; + curve_dev(ip, a, b, c) +} + +pub fn rcurveto(ip: &mut Interp) -> R { + let mut v = [0.0; 6]; + for slot in v.iter_mut().rev() { + *slot = ip.pop_num()?; + } + let (cx, cy) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let m = ip.gs.ctm; + let d = |x: f64, y: f64| { + let (dx, dy) = m.delta(x, y); + (cx + dx, cy + dy) + }; + curve_dev(ip, d(v[0], v[1]), d(v[2], v[3]), d(v[4], v[5])) +} + +pub fn closepath(ip: &mut Interp) -> R { + if ip.gs.cur.is_some() && !matches!(ip.gs.path.last(), Some(Seg::Close) | None) { + add_seg(ip, Seg::Close)?; + ip.gs.cur = ip.gs.start; + } + Ok(()) +} + +fn arc_common(ip: &mut Interp, negative: bool) -> R { + let mut a1 = ip.pop_num()?; + let a0 = ip.pop_num()?; + let r = ip.pop_num()?; + let cy = ip.pop_num()?; + let cx = ip.pop_num()?; + if negative { + while a1 > a0 { + a1 -= 360.0; + } + } else { + while a1 < a0 { + a1 += 360.0; + } + } + let (start, curves) = arc_curves(cx, cy, r, a0, a1); + let m = ip.gs.ctm; + let s = m.apply(start.0, start.1); + if ip.gs.cur.is_some() { + line_dev(ip, s)? + } else { + move_dev(ip, s)? + } + for c in curves { + curve_dev(ip, m.apply(c[0], c[1]), m.apply(c[2], c[3]), m.apply(c[4], c[5]))?; + } + Ok(()) +} + +pub fn arc(ip: &mut Interp) -> R { + arc_common(ip, false) +} + +pub fn arcn(ip: &mut Interp) -> R { + arc_common(ip, true) +} + +/// `arct` and `arcto`: a line toward (x1, y1), then an arc of radius r tangent to both lines. +fn arct_common(ip: &mut Interp, push_points: bool) -> R { + let r = ip.pop_num()?; + let y2 = ip.pop_num()?; + let x2 = ip.pop_num()?; + let y1 = ip.pop_num()?; + let x1 = ip.pop_num()?; + let inv = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?; + let (dx0, dy0) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let (x0, y0) = inv.apply(dx0, dy0); + let (ux, uy) = (x0 - x1, y0 - y1); + let (vx, vy) = (x2 - x1, y2 - y1); + let (lu, lv) = (ux.hypot(uy), vx.hypot(vy)); + let m = ip.gs.ctm; + if lu == 0.0 || lv == 0.0 || r == 0.0 { + line_dev(ip, m.apply(x1, y1))?; + if push_points { + for v in [x1, y1, x1, y1] { + ip.push(Obj::Real(v))?; + } + } + return Ok(()); + } + let cos = ((ux * vx + uy * vy) / (lu * lv)).clamp(-1.0, 1.0); + let theta = cos.acos(); + let t = r / (theta / 2.0).tan(); + if !t.is_finite() { + line_dev(ip, m.apply(x1, y1))?; + return Ok(()); + } + let (t1x, t1y) = (x1 + ux / lu * t, y1 + uy / lu * t); + let (t2x, t2y) = (x1 + vx / lv * t, y1 + vy / lv * t); + line_dev(ip, m.apply(t1x, t1y))?; + // One cubic for the arc between the tangent points. + let sweep = std::f64::consts::PI - theta; + let k = 4.0 / 3.0 * (sweep / 4.0).tan(); + let c1 = (t1x + (x1 - t1x) / t.max(1e-12) * k * r, t1y + (y1 - t1y) / t.max(1e-12) * k * r); + let c2 = (t2x + (x1 - t2x) / t.max(1e-12) * k * r, t2y + (y1 - t2y) / t.max(1e-12) * k * r); + curve_dev(ip, m.apply(c1.0, c1.1), m.apply(c2.0, c2.1), m.apply(t2x, t2y))?; + if push_points { + for v in [t1x, t1y, t2x, t2y] { + ip.push(Obj::Real(v))?; + } + } + Ok(()) +} + +pub fn arct(ip: &mut Interp) -> R { + arct_common(ip, false) +} + +pub fn arcto(ip: &mut Interp) -> R { + arct_common(ip, true) +} + +pub fn currentpoint(ip: &mut Interp) -> R { + let (x, y) = ip.gs.cur.ok_or(PsErr::Ps("nocurrentpoint"))?; + let (x, y) = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?.apply(x, y); + ip.push(Obj::Real(x))?; + ip.push(Obj::Real(y)) +} + +pub fn flattenpath(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len().saturating_mul(16))?; + let tol = ip.gs.flat; + let flat = flatten(&ip.gs.path, tol); + if flat.len() > MAX_PATH { + return err("limitcheck"); + } + ip.gs.path = flat; + Ok(()) +} + +pub fn reversepath(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len())?; + // Reverse each subpath. + let mut out = Vec::with_capacity(ip.gs.path.len()); + let mut sub: Vec = Vec::new(); + let flush = |sub: &mut Vec, out: &mut Vec| { + let closed = matches!(sub.last(), Some(Seg::Close)); + let pts: Vec<(f64, f64, Option<[f64; 4]>)> = sub + .iter() + .filter_map(|s| match *s { + Seg::Move(x, y) | Seg::Line(x, y) => Some((x, y, None)), + Seg::Curve(a, b, c, d, e, f) => Some((e, f, Some([a, b, c, d]))), + Seg::Close => None, + }) + .collect(); + if let Some(last) = pts.last() { + out.push(Seg::Move(last.0, last.1)); + for i in (1..pts.len()).rev() { + let (Some(cur), Some(prev)) = (pts.get(i), pts.get(i - 1)) else { continue }; + match cur.2 { + Some([a, b, c, d]) => out.push(Seg::Curve(c, d, a, b, prev.0, prev.1)), + None => out.push(Seg::Line(prev.0, prev.1)), + } + } + if closed { + out.push(Seg::Close); + } + } + sub.clear(); + }; + for s in ip.gs.path.clone() { + if matches!(s, Seg::Move(..)) { + flush(&mut sub, &mut out); + } + sub.push(s); + } + flush(&mut sub, &mut out); + ip.gs.path = out; + Ok(()) +} + +/// `strokepath`: the outline of what `stroke` would paint, as the current path. The pen is +/// shaped in user space (width, caps, joins, miter limit, dashes), as for `stroke`. +pub fn strokepath(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len().saturating_mul(4))?; + use kurbo::{Cap, Join, PathEl, Point, Stroke, StrokeOpts}; + let ctm = ip.gs.ctm; + let Some(inv) = ctm.inv() else { + ip.gs.path.clear(); + return Ok(()); + }; + let p = |x: f64, y: f64| { + let (x, y) = inv.apply(x, y); + Point::new(x, y) + }; + let mut els = Vec::with_capacity(ip.gs.path.len()); + for s in &ip.gs.path { + els.push(match *s { + Seg::Move(x, y) => PathEl::MoveTo(p(x, y)), + Seg::Line(x, y) => PathEl::LineTo(p(x, y)), + Seg::Curve(a, b, c, d, e, f) => PathEl::CurveTo(p(a, b), p(c, d), p(e, f)), + Seg::Close => PathEl::ClosePath, + }); + } + let g = &ip.gs; + let cap = match g.cap { + 1 => Cap::Round, + 2 => Cap::Square, + _ => Cap::Butt, + }; + let join = match g.join { + 1 => Join::Round, + 2 => Join::Bevel, + _ => Join::Miter, + }; + let mut style = Stroke::new(g.line_width.max(1e-3)).with_caps(cap).with_join(join).with_miter_limit(g.miter.max(1.0)); + let user: Vec = g.path.iter().map(|s| s.map(&inv)).collect(); + if !user.iter().all(|s| match *s { + Seg::Move(x, y) | Seg::Line(x, y) => x.is_finite() && y.is_finite(), + Seg::Curve(a, b, c, d, e, f) => [a, b, c, d, e, f].iter().all(|v| v.is_finite()), + Seg::Close => true, + }) { + ip.gs.path.clear(); + return Ok(()); + } + if crate::read::gstate::dashes_ok(&user, &g.dash.0) && g.dash.1.is_finite() { + style = style.with_dashes(g.dash.1, g.dash.0.clone()); + } + // Tolerance in user space, about a twentieth of a device unit. + let scale = (ctm.0[0] * ctm.0[3] - ctm.0[1] * ctm.0[2]).abs().sqrt().max(1e-9); + let out = kurbo::stroke(els, &style, &StrokeOpts::default(), (0.05 / scale).max(1e-6)); + let mut path = Vec::with_capacity(out.elements().len()); + let mut cur = (0.0, 0.0); + for el in out.elements() { + if path.len() >= MAX_PATH { + return err("limitcheck"); + } + let d = |q: Point| ctm.apply(q.x, q.y); + match *el { + PathEl::MoveTo(q) => { + cur = d(q); + path.push(Seg::Move(cur.0, cur.1)); + } + PathEl::LineTo(q) => { + cur = d(q); + path.push(Seg::Line(cur.0, cur.1)); + } + PathEl::QuadTo(c, q) => { + let (c, e) = (d(c), d(q)); + let a = (cur.0 + 2.0 / 3.0 * (c.0 - cur.0), cur.1 + 2.0 / 3.0 * (c.1 - cur.1)); + let b = (e.0 + 2.0 / 3.0 * (c.0 - e.0), e.1 + 2.0 / 3.0 * (c.1 - e.1)); + path.push(Seg::Curve(a.0, a.1, b.0, b.1, e.0, e.1)); + cur = e; + } + PathEl::CurveTo(a, b, q) => { + let (a, b, e) = (d(a), d(b), d(q)); + path.push(Seg::Curve(a.0, a.1, b.0, b.1, e.0, e.1)); + cur = e; + } + PathEl::ClosePath => path.push(Seg::Close), + } + } + ip.gs.path = path; + ip.gs.cur = Some(cur); + Ok(()) +} + +pub fn pathbbox(ip: &mut Interp) -> R { + ip.charge(ip.gs.path.len() as u64 / 4 + 1)?; + let b = bbox(&ip.gs.path).ok_or(PsErr::Ps("nocurrentpoint"))?; + let inv = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?; + let corners = [inv.apply(b[0], b[1]), inv.apply(b[2], b[1]), inv.apply(b[0], b[3]), inv.apply(b[2], b[3])]; + let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN); + for (x, y) in corners { + x0 = x0.min(x); + y0 = y0.min(y); + x1 = x1.max(x); + y1 = y1.max(y); + } + for v in [x0, y0, x1, y1] { + ip.push(Obj::Real(v))?; + } + Ok(()) +} + +pub fn pathforall(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len())?; + let close = ip.pop()?; + let curve = ip.pop()?; + let line = ip.pop()?; + let mv = ip.pop()?; + let inv = ip.gs.ctm.inv().ok_or(PsErr::Ps("undefinedresult"))?; + for s in ip.gs.path.clone() { + let (proc, pts) = match s { + Seg::Move(x, y) => (&mv, vec![inv.apply(x, y)]), + Seg::Line(x, y) => (&line, vec![inv.apply(x, y)]), + Seg::Curve(a, b, c, d, e, f) => (&curve, vec![inv.apply(a, b), inv.apply(c, d), inv.apply(e, f)]), + Seg::Close => (&close, vec![]), + }; + for (x, y) in pts { + ip.push(Obj::Real(x))?; + ip.push(Obj::Real(y))?; + } + ip.call(proc.clone())?; + } + Ok(()) +} + +pub fn rect_path(ip: &mut Interp) -> R> { + let mut rects: Vec<[f64; 4]> = Vec::new(); + match ip.pop()? { + Obj::Array(a, _) => { + ip.charge_path(a.len)?; + let v: Vec = a.items().iter().filter_map(Obj::num).collect(); + for r in v.as_chunks::<4>().0 { + rects.push(*r); + } + } + Obj::Str(_, _) => return err("typecheck"), + h => { + let h = h.num().ok_or(PsErr::Ps("typecheck"))?; + let w = ip.pop_num()?; + let y = ip.pop_num()?; + let x = ip.pop_num()?; + rects.push([x, y, w, h]); + } + } + let m = ip.gs.ctm; + let mut path = Vec::with_capacity(rects.len() * 5); + for [x, y, w, h] in rects { + let p = |a: f64, b: f64| m.apply(a, b); + let (a, b, c, d) = (p(x, y), p(x + w, y), p(x + w, y + h), p(x, y + h)); + path.extend([Seg::Move(a.0, a.1), Seg::Line(b.0, b.1), Seg::Line(c.0, c.1), Seg::Line(d.0, d.1), Seg::Close]); + } + Ok(path) +} + +// ----- painting ----- + +pub fn fill_path(ip: &mut Interp, path: &[Seg], eo: bool) -> R { + ip.charge(path.len() as u64 / 4 + 1)?; + if ip.gs.nulldev { + return Ok(()); + } + if matches!(ip.gs.space, Space::Pattern) && crate::read::ops::shading::fill_pattern(ip, path, eo)? { + return Ok(()); + } + let color = current_color(ip)?; + ip.out.fill(&ip.gs.clip, path, eo, color).map_err(PsErr::Fatal) +} + +pub fn fill(ip: &mut Interp) -> R { + let path = std::mem::take(&mut ip.gs.path); + fill_path(ip, &path, false)?; + newpath(ip) +} + +pub fn eofill(ip: &mut Interp) -> R { + let path = std::mem::take(&mut ip.gs.path); + fill_path(ip, &path, true)?; + newpath(ip) +} + +pub fn stroke_path(ip: &mut Interp, path: &[Seg]) -> R { + ip.charge_path(path.len())?; + if ip.gs.nulldev { + return Ok(()); + } + let color = current_color(ip)?; + let g = &ip.gs; + let p = StrokeParams { width: g.line_width, cap: g.cap, join: g.join, miter: g.miter, dash: &g.dash }; + ip.out.stroke(&g.clip, path, &g.ctm, &p, color).map_err(PsErr::Fatal) +} + +pub fn stroke(ip: &mut Interp) -> R { + let path = std::mem::take(&mut ip.gs.path); + stroke_path(ip, &path)?; + newpath(ip) +} + +pub fn rectfill(ip: &mut Interp) -> R { + let path = rect_path(ip)?; + fill_path(ip, &path, false) +} + +pub fn rectstroke(ip: &mut Interp) -> R { + // An optional matrix operand changes the pen: applied to the CTM for this stroke. + let m = match ip.top(0)? { + Obj::Array(a, _) if a.len == 6 && ip.o.len() >= 2 && !matches!(ip.top(1), Ok(Obj::Array(..)) | Err(_)) => Some(ip.pop_matrix()?), + _ => None, + }; + let path = rect_path(ip)?; + match m { + Some(m) => { + let saved = ip.gs.ctm; + ip.gs.ctm = m.then(&saved); + let r = stroke_path(ip, &path); + ip.gs.ctm = saved; + r + } + None => stroke_path(ip, &path), + } +} + +fn clip_with(ip: &mut Interp, path: Vec, eo: bool) -> R { + let depth = ip.gs.clip.as_ref().map_or(0, |c| c.depth + 1); + if depth > 2000 { + return err("limitcheck"); + } + ip.gs.clip = Some(Rc::new(Clip { path, eo, parent: ip.gs.clip.clone(), depth })); + Ok(()) +} + +pub fn clip(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len())?; + let path = ip.gs.path.clone(); + clip_with(ip, path, false) +} + +pub fn eoclip(ip: &mut Interp) -> R { + ip.charge_path(ip.gs.path.len())?; + let path = ip.gs.path.clone(); + clip_with(ip, path, true) +} + +pub fn rectclip(ip: &mut Interp) -> R { + let path = rect_path(ip)?; + clip_with(ip, path, false)?; + newpath(ip) +} + +pub fn clipsave(ip: &mut Interp) -> R { + if ip.gs.clip_saves.len() >= 1000 { + return err("limitcheck"); + } + let c = ip.gs.clip.clone(); + ip.gs.clip_saves.push(c); + Ok(()) +} + +pub fn cliprestore(ip: &mut Interp) -> R { + if let Some(c) = ip.gs.clip_saves.pop() { + ip.gs.clip = c; + } + Ok(()) +} + +pub fn initclip(ip: &mut Interp) -> R { + ip.gs.clip = None; + Ok(()) +} + +pub fn clippath(ip: &mut Interp) -> R { + let n = ip.gs.clip.as_ref().map_or(0, |c| c.path.len()); + ip.charge_path(n)?; + let path = match &ip.gs.clip { + Some(c) => c.path.clone(), + None => { + // The whole page: a generous square. + let s = 1.0e5; + vec![Seg::Move(-s, -s), Seg::Line(s, -s), Seg::Line(s, s), Seg::Line(-s, s), Seg::Close] + } + }; + ip.gs.cur = path.iter().rev().find_map(|s| match s { + Seg::Move(x, y) | Seg::Line(x, y) => Some((*x, *y)), + _ => None, + }); + ip.gs.path = path; + Ok(()) +} + +// ----- colour ----- + +/// A colour space from a name or array operand. +pub fn parse_space(ip: &mut Interp, o: &Obj, depth: usize) -> R { + if depth > 8 { + return err("limitcheck"); + } + let (family, params) = match o { + Obj::Name(n, _) => (n.to_vec(), Vec::new()), + Obj::Array(a, _) => { + let items = a.items(); + match items.first() { + Some(Obj::Name(n, _)) => (n.to_vec(), items.get(1..).map(<[Obj]>::to_vec).unwrap_or_default()), + _ => return err("typecheck"), + } + } + _ => return err("typecheck"), + }; + Ok(match family.as_slice() { + b"DeviceGray" | b"CalGray" | b"G" | b"CIEBasedA" => Space::Gray, + b"DeviceRGB" | b"CalRGB" | b"RGB" | b"CIEBasedABC" | b"CIEBasedDEF" => Space::Rgb, + b"DeviceCMYK" | b"CMYK" | b"CIEBasedDEFG" => Space::Cmyk, + b"Lab" => Space::Lab, + b"Pattern" => Space::Pattern, + b"ICCBased" => { + let n = match params.first() { + Some(Obj::Dict(d)) => d.get_name(b"N").and_then(|o| o.num()).unwrap_or(3.0) as usize, + _ => 3, + }; + match n { + 1 => Space::Gray, + 4 => Space::Cmyk, + _ => Space::Rgb, + } + } + b"Separation" | b"DeviceN" => { + let names = match params.first() { + Some(Obj::Array(a, _)) => a.items().iter().map(crate::read::ops::basic::text_of).collect(), + Some(o) => vec![crate::read::ops::basic::text_of(o)], + None => return err("rangecheck"), + }; + let alt = parse_space(ip, params.get(1).ok_or(PsErr::Ps("rangecheck"))?, depth + 1)?; + let tint = params.get(2).cloned().ok_or(PsErr::Ps("rangecheck"))?; + Space::Tint { names, alt: Box::new(alt), tint } + } + b"Indexed" | b"I" => { + let base = parse_space(ip, params.first().ok_or(PsErr::Ps("rangecheck"))?, depth + 1)?; + let hival = params.get(1).and_then(Obj::num).unwrap_or(0.0).clamp(0.0, 4095.0) as usize; + let n = base.comps(); + let lookup = match params.get(2) { + Some(Obj::Str(s, _)) => s.bytes(), + Some(p @ Obj::Array(_, true)) => { + let mut out = Vec::with_capacity((hival + 1) * n); + for i in 0..=hival { + ip.push(Obj::Int(i as i64))?; + ip.call(p.clone())?; + let mut comps = vec![0.0; n]; + for c in comps.iter_mut().rev() { + *c = ip.pop_num()?; + } + out.extend(comps.iter().map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8)); + } + out + } + _ => return err("typecheck"), + }; + Space::Indexed { base: Box::new(base), hival, lookup } + } + _ => return err("undefined"), + }) +} + +/// Evaluate a tint transform (a procedure or a Type 2 function dictionary) on `comps`. +fn eval_tint(ip: &mut Interp, tint: &Obj, comps: &[f64], out_n: usize) -> R> { + match tint { + Obj::Dict(d) => { + // Type 2 exponential: C0 + x^N (C1 − C0). + let get = |k: &[u8], dflt: f64| -> Vec { + match d.get_name(k) { + Some(Obj::Array(a, _)) => a.items().iter().filter_map(Obj::num).collect(), + _ => vec![dflt; out_n], + } + }; + let c0 = get(b"C0", 0.0); + let c1 = get(b"C1", 1.0); + let n = d.get_name(b"N").and_then(|o| o.num()).unwrap_or(1.0); + let x = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0).powf(n); + Ok((0..out_n) + .map(|i| c0.get(i).copied().unwrap_or(0.0) + x * (c1.get(i).copied().unwrap_or(1.0) - c0.get(i).copied().unwrap_or(0.0))) + .collect()) + } + _ => { + for c in comps { + ip.push(Obj::Real(*c))?; + } + ip.call(tint.clone())?; + let mut v = vec![0.0; out_n]; + for slot in v.iter_mut().rev() { + *slot = ip.pop_num()?; + } + Ok(v) + } + } +} + +pub fn lab_to_rgb(l: f64, a: f64, b: f64) -> (f64, f64, f64) { + let fy = (l + 16.0) / 116.0; + let fx = fy + a / 500.0; + let fz = fy - b / 200.0; + let g = |t: f64| if t > 6.0 / 29.0 { t * t * t } else { 3.0 * (6.0f64 / 29.0).powi(2) * (t - 4.0 / 29.0) }; + let (x, y, z) = (0.9505 * g(fx), g(fy), 1.089 * g(fz)); + let lin = [3.2406 * x - 1.5372 * y - 0.4986 * z, -0.9689 * x + 1.8758 * y + 0.0415 * z, 0.0557 * x - 0.2040 * y + 1.0570 * z]; + let enc = |c: f64| { + let c = c.clamp(0.0, 1.0); + if c <= 0.0031308 { 12.92 * c } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 } + }; + (enc(lin[0]), enc(lin[1]), enc(lin[2])) +} + +/// A colour in `space` as a device colour. +pub fn to_device(ip: &mut Interp, space: &Space, comps: &[f64], depth: usize) -> R { + if depth > 8 { + return err("limitcheck"); + } + let c = |i: usize| comps.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0); + Ok(match space { + Space::Gray => DevColor::Gray(c(0)), + Space::Rgb => DevColor::Rgb(c(0), c(1), c(2)), + Space::Cmyk => DevColor::Cmyk(c(0), c(1), c(2), c(3)), + Space::Lab => { + let (r, g, b) = + lab_to_rgb(comps.first().copied().unwrap_or(0.0), comps.get(1).copied().unwrap_or(0.0), comps.get(2).copied().unwrap_or(0.0)); + DevColor::Rgb(r, g, b) + } + Space::Tint { names, alt, tint } => { + // The colourant "None" paints nothing; "All" is registration black. + if names.len() == 1 && names.first().is_some_and(|n| n == b"All") { + return Ok(DevColor::Cmyk(c(0), c(0), c(0), c(0))); + } + let v = eval_tint(ip, &tint.clone(), comps, alt.comps())?; + to_device(ip, &alt.clone(), &v, depth + 1)? + } + Space::Indexed { base, hival, lookup } => { + let i = (comps.first().copied().unwrap_or(0.0).round().max(0.0) as usize).min(*hival); + let n = base.comps(); + let v: Vec = (0..n).map(|k| f64::from(lookup.get(i * n + k).copied().unwrap_or(0)) / 255.0).collect(); + to_device(ip, &base.clone(), &v, depth + 1)? + } + // Patterns aren't painted yet: a mid grey stands in. + Space::Pattern => DevColor::Gray(0.5), + }) +} + +pub fn current_color(ip: &mut Interp) -> R { + let space = ip.gs.space.clone(); + let comps = ip.gs.color.clone(); + to_device(ip, &space, &comps, 0) +} + +fn set_space(ip: &mut Interp, space: Space) { + if ip.gs.color_locked { + return; + } + ip.gs.color = space.initial(); + ip.gs.space = space; + ip.gs.pattern = None; +} + +/// Set the colour values (not inside an uncoloured pattern cell). +fn set_color(ip: &mut Interp, v: Vec) { + if !ip.gs.color_locked { + ip.gs.color = v; + } +} + +pub fn setgray(ip: &mut Interp) -> R { + let g = ip.pop_num()?; + set_space(ip, Space::Gray); + set_color(ip, vec![g]); + Ok(()) +} + +pub fn setrgbcolor(ip: &mut Interp) -> R { + let b = ip.pop_num()?; + let g = ip.pop_num()?; + let r = ip.pop_num()?; + set_space(ip, Space::Rgb); + set_color(ip, vec![r, g, b]); + Ok(()) +} + +pub fn setcmykcolor(ip: &mut Interp) -> R { + let k = ip.pop_num()?; + let y = ip.pop_num()?; + let m = ip.pop_num()?; + let c = ip.pop_num()?; + set_space(ip, Space::Cmyk); + set_color(ip, vec![c, m, y, k]); + Ok(()) +} + +pub fn sethsbcolor(ip: &mut Interp) -> R { + let v = ip.pop_num()?.clamp(0.0, 1.0); + let s = ip.pop_num()?.clamp(0.0, 1.0); + let h = ip.pop_num()?.rem_euclid(1.0) * 6.0; + let i = h.floor(); + let f = h - i; + let (p, q, t) = (v * (1.0 - s), v * (1.0 - s * f), v * (1.0 - s * (1.0 - f))); + let (r, g, b) = match i as i64 { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + _ => (v, p, q), + }; + set_space(ip, Space::Rgb); + set_color(ip, vec![r, g, b]); + Ok(()) +} + +pub fn currentgray(ip: &mut Interp) -> R { + let g = match current_color(ip)? { + DevColor::Gray(g) => g, + DevColor::Rgb(r, g, b) => 0.3 * r + 0.59 * g + 0.11 * b, + DevColor::Cmyk(c, m, y, k) => 1.0 - (0.3 * c + 0.59 * m + 0.11 * y + k).min(1.0), + }; + ip.push(Obj::Real(g)) +} + +fn rgb_of(d: DevColor) -> (f64, f64, f64) { + match d { + DevColor::Gray(g) => (g, g, g), + DevColor::Rgb(r, g, b) => (r, g, b), + DevColor::Cmyk(c, m, y, k) => (1.0 - (c + k).min(1.0), 1.0 - (m + k).min(1.0), 1.0 - (y + k).min(1.0)), + } +} + +pub fn currentrgbcolor(ip: &mut Interp) -> R { + let (r, g, b) = rgb_of(current_color(ip)?); + for v in [r, g, b] { + ip.push(Obj::Real(v))?; + } + Ok(()) +} + +pub fn currenthsbcolor(ip: &mut Interp) -> R { + let (r, g, b) = rgb_of(current_color(ip)?); + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let d = max - min; + let h = if d == 0.0 { + 0.0 + } else if max == r { + ((g - b) / d).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / d + 2.0) / 6.0 + } else { + ((r - g) / d + 4.0) / 6.0 + }; + let s = if max == 0.0 { 0.0 } else { d / max }; + for v in [h, s, max] { + ip.push(Obj::Real(v))?; + } + Ok(()) +} + +pub fn currentcmykcolor(ip: &mut Interp) -> R { + let (c, m, y, k) = match current_color(ip)? { + DevColor::Cmyk(c, m, y, k) => (c, m, y, k), + DevColor::Gray(g) => (0.0, 0.0, 0.0, 1.0 - g), + DevColor::Rgb(r, g, b) => { + let (c, m, y) = (1.0 - r, 1.0 - g, 1.0 - b); + let k = c.min(m).min(y); + (c - k, m - k, y - k, k) + } + }; + for v in [c, m, y, k] { + ip.push(Obj::Real(v))?; + } + Ok(()) +} + +pub fn setcolorspace(ip: &mut Interp) -> R { + let o = ip.pop()?; + let space = parse_space(ip, &o, 0)?; + let base = match (&space, &o) { + (Space::Pattern, Obj::Array(a, _)) => match a.get(1) { + Some(b) => Some(Box::new(parse_space(ip, &b, 1)?)), + None => None, + }, + _ => None, + }; + set_space(ip, space); + if !ip.gs.color_locked { + ip.gs.pattern_base = base; + } + Ok(()) +} + +pub fn currentcolorspace(ip: &mut Interp) -> R { + let n: &[u8] = match ip.gs.space { + Space::Gray => b"DeviceGray", + Space::Rgb => b"DeviceRGB", + Space::Cmyk => b"DeviceCMYK", + Space::Pattern => b"Pattern", + Space::Lab => b"Lab", + Space::Indexed { .. } => b"Indexed", + Space::Tint { .. } => b"Separation", + }; + ip.push(Obj::Array(PArr::new(vec![Obj::lit(n)]), false)) +} + +pub fn setcolor(ip: &mut Interp) -> R { + if let Space::Pattern = ip.gs.space { + // An uncoloured pattern comes after its colour: `c1 … cn pattern setcolor`. + if let Obj::Dict(d) = ip.pop()? + && !ip.gs.color_locked + { + let uncolored = d.get_name(b"PaintType").and_then(|o| o.num()) == Some(2.0); + let mut under = Vec::new(); + while uncolored && ip.top(0).is_ok_and(|o| o.num().is_some()) && under.len() < 4 { + under.push(ip.pop_num()?); + } + under.reverse(); + ip.gs.color = under; + ip.gs.pattern = Some(d); + } + return Ok(()); + } + let n = ip.gs.space.comps(); + let mut v = vec![0.0; n]; + for slot in v.iter_mut().rev() { + *slot = ip.pop_num()?; + } + set_color(ip, v); + Ok(()) +} + +pub fn currentcolor(ip: &mut Interp) -> R { + for v in ip.gs.color.clone() { + ip.push(Obj::Real(v))?; + } + Ok(()) +} + +pub fn setpattern(ip: &mut Interp) -> R { + let d: PDict = ip.pop_dict()?; + if ip.gs.color_locked { + return Ok(()); + } + // Under an uncoloured pattern, the current colour stays its colour. + let under = ip.gs.color.clone(); + set_space(ip, Space::Pattern); + if d.get_name(b"PaintType").and_then(|o| o.num()) == Some(2.0) { + ip.gs.color = under; + } + ip.gs.pattern = Some(d); + Ok(()) +} + +/// A pattern instance: a copy of the prototype with its pattern space (matrix · CTM). +pub fn makepattern(ip: &mut Interp) -> R { + let m = ip.pop_matrix()?; + let d = ip.pop_dict()?; + let inst = PDict::new(d.len() + 2); + for (k, v) in d.entries() { + inst.put(k, v); + } + inst.put_name(b"CraftPsMatrix", matrix_obj(&m.then(&ip.gs.ctm))); + ip.push(Obj::Dict(inst)) +} diff --git a/crates/ps/src/read/ops/image.rs b/crates/ps/src/read/ops/image.rs new file mode 100644 index 0000000000..3d4db607c3 --- /dev/null +++ b/crates/ps/src/read/ops/image.rs @@ -0,0 +1,366 @@ +//! Sampled images: `image`, `colorimage`, `imagemask`. + +use crate::read::gstate::{DevColor, M, Space}; +use crate::read::interp::{Interp, PsErr, R, err, matrix_of}; +use crate::read::obj::{Obj, PDict}; +use crate::read::ops::gfx; +use crate::read::pdf::{Image, ImgSpace}; + +/// The most samples one image may hold. +const MAX_SAMPLES: usize = 400_000_000; + +/// Where samples come from: a string (reused), a file, or a procedure (called for more). +fn read_source(ip: &mut Interp, src: &Obj, need: usize) -> R<(Vec, bool)> { + let mut out = Vec::new(); + match src { + Obj::Str(s, _) => { + let b = s.bytes(); + if b.is_empty() { + return Ok((vec![0; need], false)); + } + while out.len() < need { + out.extend_from_slice(&b); + } + } + Obj::File(id) => { + if let Some(raw) = ip.srcs.dct(*id) { + return Ok((raw.to_vec(), true)); + } + while out.len() < need { + match ip.srcs.read(*id) { + Some(c) => out.push(c), + None => break, + } + } + // A filter is read to its end, as printers do: its end marker is part of the data. + if ip.srcs.is_filter(*id) { + ip.srcs.drain(*id); + } + } + p @ Obj::Array(_, true) => { + let mut empty_runs = 0; + while out.len() < need { + ip.call(p.clone())?; + let s = ip.pop_str()?; + let b = s.bytes(); + if b.is_empty() { + empty_runs += 1; + if empty_runs > 1 || out.is_empty() { + break; + } + continue; + } + out.extend_from_slice(&b); + } + } + _ => return err("typecheck"), + } + out.resize(need, 0); + Ok((out, false)) +} + +struct Params { + w: usize, + h: usize, + bpc: usize, + matrix: M, + sources: Vec, + decode: Option>, + interpolate: bool, +} + +fn dict_params(ip: &mut Interp, d: &PDict, comps: usize) -> R { + let num = |k: &[u8]| d.get_name(k).and_then(|o| o.num()); + let w = num(b"Width").ok_or(PsErr::Ps("typecheck"))?.max(0.0) as usize; + let h = num(b"Height").ok_or(PsErr::Ps("typecheck"))?.max(0.0) as usize; + let bpc = num(b"BitsPerComponent").unwrap_or(8.0) as usize; + let matrix = match d.get_name(b"ImageMatrix") { + Some(Obj::Array(a, _)) => matrix_of(&a)?, + _ => return err("typecheck"), + }; + let multi = matches!(d.get_name(b"MultipleDataSources"), Some(Obj::Bool(true))); + let sources = match d.get_name(b"DataSource").ok_or(PsErr::Ps("typecheck"))? { + Obj::Array(a, false) if multi => a.items(), + o => vec![o], + }; + if multi && sources.len() < comps { + return err("rangecheck"); + } + let decode = match d.get_name(b"Decode") { + Some(Obj::Array(a, _)) => Some(a.items().iter().filter_map(Obj::num).collect()), + _ => None, + }; + let interpolate = matches!(d.get_name(b"Interpolate"), Some(Obj::Bool(true))); + let _ = ip; + Ok(Params { w, h, bpc, matrix, sources, decode, interpolate }) +} + +/// Read all samples, pixel-interleaved (`comps` per pixel), rows padded to bytes. +fn samples(ip: &mut Interp, p: &Params, comps: usize) -> R<(Vec, bool)> { + let total = p.w.checked_mul(p.h).and_then(|n| n.checked_mul(comps)).ok_or(PsErr::Ps("limitcheck"))?; + if total > MAX_SAMPLES || !matches!(p.bpc, 1 | 2 | 4 | 8 | 12 | 16) { + return err("limitcheck"); + } + ip.alloc(total)?; + ip.charge_bytes(total)?; + if p.sources.len() <= 1 { + let row = (p.w * comps * p.bpc).div_ceil(8); + let src = p.sources.first().cloned().ok_or(PsErr::Ps("typecheck"))?; + return read_source(ip, &src, row * p.h); + } + // One source per component, read in turn (each call or string gives the next piece of its + // component, as Photoshop's row-by-row procedures expect), then interleaved (8-bit only). + if p.bpc != 8 { + return err("limitcheck"); + } + let plane = p.w * p.h; + let mut planes: Vec> = vec![Vec::new(); comps]; + let sources: Vec = p.sources.iter().take(comps).cloned().collect(); + let mut stalled = 0; + while planes.iter().any(|pl| pl.len() < plane) && stalled < 2 { + let mut progress = false; + for (pl, src) in planes.iter_mut().zip(&sources) { + if pl.len() >= plane { + continue; + } + let piece = match src { + p @ Obj::Array(_, true) => { + ip.call(p.clone())?; + ip.pop_str()?.bytes() + } + Obj::Str(s, _) => s.bytes(), + Obj::File(id) => { + let row = p.w.max(1); + let mut v = Vec::with_capacity(row); + while v.len() < row { + match ip.srcs.read(*id) { + Some(c) => v.push(c), + None => break, + } + } + v + } + _ => return err("typecheck"), + }; + progress |= !piece.is_empty(); + pl.extend_from_slice(&piece); + } + stalled = if progress { 0 } else { stalled + 1 }; + } + for pl in &mut planes { + pl.resize(plane, 0); + } + let mut out = Vec::with_capacity(total); + for i in 0..plane { + for pl in &planes { + out.push(pl.get(i).copied().unwrap_or(0)); + } + } + Ok((out, false)) +} + +/// The unit square → page matrix for an image of w × h samples. +fn placement(ip: &Interp, p: &Params) -> R { + let inv = p.matrix.inv().ok_or(PsErr::Ps("undefinedresult"))?; + let s = M([p.w as f64, 0.0, 0.0, -(p.h as f64), 0.0, p.h as f64]); + Ok(s.then(&inv).then(&ip.gs.ctm)) +} + +/// 12- and 16-bit samples as 8-bit. +fn to_8bit(data: Vec, bpc: usize) -> Vec { + match bpc { + 16 => data.chunks(2).map(|c| c.first().copied().unwrap_or(0)).collect(), + 12 => { + let mut out = Vec::with_capacity(data.len() * 2 / 3); + for c in data.chunks(3) { + let (a, b, d) = (c.first().copied().unwrap_or(0), c.get(1).copied().unwrap_or(0), c.get(2).copied().unwrap_or(0)); + out.push(a); + out.push((b << 4) | (d >> 4)); + } + out + } + _ => data, + } +} + +fn paint(ip: &mut Interp, p: Params, space: Space) -> R { + let comps = space.comps(); + let (data, dct) = samples(ip, &p, comps)?; + let cm = placement(ip, &p)?; + if ip.gs.nulldev { + return Ok(()); + } + let (data, bpc) = if !dct && matches!(p.bpc, 12 | 16) { (to_8bit(data, p.bpc), 8) } else { (data, p.bpc) }; + let decode = p.decode.map(|d| d.iter().map(|v| *v as f32).collect::>()); + let img_space = match &space { + Space::Gray => Some(ImgSpace::Gray), + Space::Rgb => Some(ImgSpace::Rgb), + Space::Cmyk => Some(ImgSpace::Cmyk), + Space::Indexed { base, hival, lookup } if matches!(**base, Space::Gray | Space::Rgb | Space::Cmyk) => Some(ImgSpace::Indexed { + base: Box::new(match **base { + Space::Rgb => ImgSpace::Rgb, + Space::Cmyk => ImgSpace::Cmyk, + _ => ImgSpace::Gray, + }), + hival: u8::try_from(*hival).unwrap_or(255), + lookup: lookup.clone(), + }), + _ => None, + }; + if let Some(space) = img_space { + let img = + Image { w: p.w as u32, h: p.h as u32, bpc: bpc as u8, space: Some(space), decode, data, dct, interpolate: p.interpolate, smask: None }; + return ip.out.image(&ip.gs.clip, &cm, img, None).map_err(PsErr::Fatal); + } + // Other spaces (Separation, DeviceN, Indexed over them, Lab): convert each pixel, through a + // table of the 256 values when there is one component. + if dct || bpc != 8 { + ip.warnings.push("image in an unsupported colour space skipped".into()); + return Ok(()); + } + // Converting each pixel: work and the converted copy. + ip.charge(data.len() as u64 / 4 + 1)?; + ip.alloc(p.w.saturating_mul(p.h).saturating_mul(4))?; + let mut rgb = Vec::with_capacity(p.w * p.h * 4); + if comps == 1 { + let mut table = Vec::with_capacity(256); + let dmax = match &space { + Space::Indexed { hival, .. } => *hival as f64, + _ => 1.0, + }; + for v in 0..256 { + let x = f64::from(v) / 255.0 * dmax; + table.push(gfx::to_device(ip, &space, &[x], 0)?); + } + let cmyk = table.iter().any(|c| matches!(c, DevColor::Cmyk(..))); + for s in &data { + push_color(&mut rgb, table.get(usize::from(*s)).copied().unwrap_or(DevColor::Gray(0.0)), cmyk); + } + let space = if cmyk { ImgSpace::Cmyk } else { ImgSpace::Rgb }; + let img = Image { + w: p.w as u32, + h: p.h as u32, + bpc: 8, + space: Some(space), + decode: None, + data: rgb, + dct: false, + interpolate: p.interpolate, + smask: None, + }; + return ip.out.image(&ip.gs.clip, &cm, img, None).map_err(PsErr::Fatal); + } + if p.w * p.h > 2_000_000 { + ip.warnings.push("multi-component image in a special colour space too large to convert".into()); + return Ok(()); + } + let mut cache: std::collections::HashMap, DevColor> = std::collections::HashMap::new(); + let mut colors = Vec::with_capacity(p.w * p.h); + for px in data.chunks(comps) { + let c = match cache.get(px) { + Some(c) => *c, + None => { + let v: Vec = px.iter().map(|b| f64::from(*b) / 255.0).collect(); + let c = gfx::to_device(ip, &space, &v, 0)?; + if cache.len() < 65536 { + cache.insert(px.to_vec(), c); + } + c + } + }; + colors.push(c); + } + let cmyk = colors.iter().any(|c| matches!(c, DevColor::Cmyk(..))); + for c in colors { + push_color(&mut rgb, c, cmyk); + } + let space = if cmyk { ImgSpace::Cmyk } else { ImgSpace::Rgb }; + let img = Image { + w: p.w as u32, + h: p.h as u32, + bpc: 8, + space: Some(space), + decode: None, + data: rgb, + dct: false, + interpolate: p.interpolate, + smask: None, + }; + ip.out.image(&ip.gs.clip, &cm, img, None).map_err(PsErr::Fatal) +} + +fn push_color(out: &mut Vec, c: DevColor, cmyk: bool) { + let b = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + match (c, cmyk) { + (DevColor::Cmyk(c, m, y, k), true) => out.extend([b(c), b(m), b(y), b(k)]), + (DevColor::Gray(g), true) => out.extend([0, 0, 0, b(1.0 - g)]), + (DevColor::Rgb(r, g, bl), true) => out.extend([b(1.0 - r), b(1.0 - g), b(1.0 - bl), 0]), + (DevColor::Gray(g), false) => out.extend([b(g); 3]), + (DevColor::Rgb(r, g, bl), false) => out.extend([b(r), b(g), b(bl)]), + (DevColor::Cmyk(c, m, y, k), false) => out.extend([b(1.0 - (c + k).min(1.0)), b(1.0 - (m + k).min(1.0)), b(1.0 - (y + k).min(1.0))]), + } +} + +pub fn image(ip: &mut Interp) -> R { + if let Obj::Dict(d) = ip.top(0)?.clone() { + ip.pop()?; + let space = ip.gs.space.clone(); + let p = dict_params(ip, &d, space.comps())?; + return paint(ip, p, space); + } + let src = ip.pop()?; + let matrix = ip.pop_matrix()?; + let bpc = ip.pop_usize()?; + let h = ip.pop_usize()?; + let w = ip.pop_usize()?; + paint(ip, Params { w, h, bpc, matrix, sources: vec![src], decode: None, interpolate: false }, Space::Gray) +} + +pub fn colorimage(ip: &mut Interp) -> R { + let ncomp = ip.pop_usize()?; + let multi = ip.pop_bool()?; + let space = match ncomp { + 1 => Space::Gray, + 3 => Space::Rgb, + 4 => Space::Cmyk, + _ => return err("rangecheck"), + }; + let mut sources = Vec::new(); + for _ in 0..if multi { ncomp } else { 1 } { + sources.push(ip.pop()?); + } + sources.reverse(); + let matrix = ip.pop_matrix()?; + let bpc = ip.pop_usize()?; + let h = ip.pop_usize()?; + let w = ip.pop_usize()?; + paint(ip, Params { w, h, bpc, matrix, sources, decode: None, interpolate: false }, space) +} + +pub fn imagemask(ip: &mut Interp) -> R { + let (p, polarity) = if let Obj::Dict(d) = ip.top(0)?.clone() { + ip.pop()?; + let mut p = dict_params(ip, &d, 1)?; + let inverted = p.decode.as_ref().and_then(|d| d.first()).is_some_and(|v| *v > 0.5); + p.decode = None; + (p, inverted) + } else { + let src = ip.pop()?; + let matrix = ip.pop_matrix()?; + let polarity = ip.pop_bool()?; + let h = ip.pop_usize()?; + let w = ip.pop_usize()?; + (Params { w, h, bpc: 1, matrix, sources: vec![src], decode: None, interpolate: false }, polarity) + }; + let p = Params { bpc: 1, ..p }; + let (data, _) = samples(ip, &p, 1)?; + let cm = placement(ip, &p)?; + if ip.gs.nulldev { + return Ok(()); + } + let color = gfx::current_color(ip)?; + // PDF stencil masks paint 0 bits; polarity true paints the 1 bits. + let decode = if polarity { Some(vec![1.0, 0.0]) } else { None }; + let img = Image { w: p.w as u32, h: p.h as u32, bpc: 1, space: None, decode, data, dct: false, interpolate: false, smask: None }; + ip.out.image(&ip.gs.clip, &cm, img, Some(color)).map_err(PsErr::Fatal) +} diff --git a/crates/ps/src/read/ops/mesh.rs b/crates/ps/src/read/ops/mesh.rs new file mode 100644 index 0000000000..d661e7b222 --- /dev/null +++ b/crates/ps/src/read/ops/mesh.rs @@ -0,0 +1,261 @@ +//! Mesh shadings (types 4–7): free-form and lattice triangles, Coons and tensor patches, read +//! from an array, a string or a file, their colours made device colours, and written to the PDF +//! as meshes again (each triangle and patch whole, so shared edges need no state there). + +use crate::read::gstate::{DevColor, Space}; +use crate::read::interp::{Interp, R, err}; +use crate::read::obj::{Obj, PDict}; +use crate::read::ops::gfx; +use crate::read::ops::shading::{Func, eval, pack}; +use crate::read::pdf::{ShadeKind, Shading}; + +/// A patch's control points and corner colours (the next patch may share an edge of it). +type Patch = (Vec<(f64, f64)>, Vec); + +/// The most vertices or patches read from one shading. +const MAX_ELEMENTS: usize = 2_000_000; + +/// The numbers of a mesh: from an array as they are, or unpacked from bits with Decode. +struct Reader { + nums: Vec, + bits: Option<(Vec, usize)>, + pos: usize, + bpc: usize, + bpcomp: usize, + bpflag: usize, + decode: Vec, +} + +impl Reader { + fn raw(&mut self, n: usize) -> Option { + let (data, bit) = self.bits.as_mut()?; + let mut v = 0u64; + for _ in 0..n { + let byte = *data.get(*bit / 8)?; + v = v << 1 | u64::from(byte >> (7 - *bit % 8) & 1); + *bit += 1; + } + Some(v) + } + + fn align(&mut self) { + if let Some((_, bit)) = self.bits.as_mut() { + *bit = bit.div_ceil(8) * 8; + } + } + + fn array_next(&mut self) -> Option { + let v = self.nums.get(self.pos).copied(); + self.pos += 1; + v + } + + fn decoded(&mut self, bits: usize, range: usize) -> Option { + if self.bits.is_none() { + return self.array_next(); + } + let v = self.raw(bits)?; + let max = ((1u128 << bits) - 1) as f64; + let (d0, d1) = (self.decode.get(2 * range).copied().unwrap_or(0.0), self.decode.get(2 * range + 1).copied().unwrap_or(1.0)); + Some(d0 + v as f64 / max * (d1 - d0)) + } + + fn flag(&mut self) -> Option { + if self.bits.is_none() { + return self.array_next().map(|v| v as u8); + } + self.raw(self.bpflag).map(|v| v as u8) + } + + fn point(&mut self) -> Option<(f64, f64)> { + let b = self.bpc; + Some((self.decoded(b, 0)?, self.decoded(b, 1)?)) + } + + fn color(&mut self, n: usize) -> Option> { + let b = self.bpcomp; + (0..n).map(|i| self.decoded(b, 2 + i)).collect() + } +} + +pub fn build(ip: &mut Interp, d: &PDict, kind: i64, space: &Space, func: Option<&Func>) -> R> { + let num = |k: &[u8]| d.get_name(k).and_then(|o| o.num()).unwrap_or(0.0); + let ncomp = if func.is_some() { 1 } else { space.comps() }; + let decode: Vec = match d.get_name(b"Decode") { + Some(Obj::Array(a, _)) => a.items().iter().filter_map(Obj::num).collect(), + _ => Vec::new(), + }; + let mut r = Reader { + nums: Vec::new(), + bits: None, + pos: 0, + bpc: num(b"BitsPerCoordinate").clamp(1.0, 32.0) as usize, + bpcomp: num(b"BitsPerComponent").clamp(1.0, 16.0) as usize, + bpflag: num(b"BitsPerFlag").clamp(2.0, 8.0) as usize, + decode, + }; + match d.get_name(b"DataSource") { + Some(Obj::Array(a, _)) => r.nums = a.items().iter().filter_map(Obj::num).collect(), + Some(Obj::Str(s, _)) => r.bits = Some((s.bytes(), 0)), + Some(Obj::File(id)) => { + let mut v = Vec::new(); + while let Some(c) = ip.srcs.read(id) { + v.push(c); + if v.len() > 64 << 20 { + break; + } + } + r.bits = Some((v, 0)); + } + _ => return err("typecheck"), + } + let to_dev = |ip: &mut Interp, c: &[f64]| -> R { + let comps = match func { + Some(f) => eval(ip, f, c, 0)?, + None => c.to_vec(), + }; + gfx::to_device(ip, space, &comps, 0) + }; + let mut points: Vec<(f64, f64)> = Vec::new(); + let mut colors: Vec = Vec::new(); + let shade = match kind { + 4 | 5 => { + // Vertices, then triangles of them. + let mut verts: Vec<((f64, f64), DevColor)> = Vec::new(); + let mut tris: Vec<[usize; 3]> = Vec::new(); + let per_row = num(b"VerticesPerRow").max(2.0) as usize; + let mut pending: Vec = Vec::new(); + while verts.len() < MAX_ELEMENTS { + ip.charge(1)?; + let flag = if kind == 4 { + match r.flag() { + Some(f) => f, + None => break, + } + } else { + 0 + }; + let (Some(p), Some(c)) = (r.point(), r.color(ncomp)) else { break }; + if kind == 4 && r.bits.is_some() { + r.align(); + } + let c = to_dev(ip, &c)?; + verts.push((p, c)); + let n = verts.len() - 1; + if kind != 4 { + continue; + } + // Flag 0 starts a triangle of three new vertices; 1 and 2 continue from the + // last triangle's (b, c) or (a, c) edge. + match (flag, tris.last().copied()) { + (1, Some([_, b, c])) => tris.push([b, c, n]), + (2, Some([a, _, c])) => tris.push([a, c, n]), + _ => { + pending.push(n); + if pending.len() == 3 { + tris.push([pending[0], pending[1], pending[2]]); + pending.clear(); + } + } + } + } + if kind == 5 { + let rows = verts.len() / per_row; + for row in 0..rows.saturating_sub(1) { + for i in 0..per_row - 1 { + let a = row * per_row + i; + let (b, c, e) = (a + 1, a + per_row, a + per_row + 1); + tris.push([a, b, c]); + tris.push([b, e, c]); + } + } + } + for t in tris { + for i in t { + let Some((p, c)) = verts.get(i) else { return err("rangecheck") }; + points.push(*p); + colors.push(*c); + } + } + ShadeKind::Triangles + } + _ => { + // Coons (12 control points) and tensor (16) patches; flags 1–3 share an edge. + let npts = if kind == 6 { 12 } else { 16 }; + let mut prev: Option = None; + let mut count = 0; + loop { + if count >= MAX_ELEMENTS { + break; + } + ip.charge(4)?; + let Some(flag) = r.flag() else { break }; + let (shared_pts, shared_cols) = match (flag, &prev) { + (1..=3, Some((pp, pc))) => { + // The edge of the previous patch this one starts from (per the patch order). + let (pi, ci): (&[usize], [usize; 2]) = match flag { + 1 => (&[3, 4, 5, 6], [1, 2]), + 2 => (&[6, 7, 8, 9], [2, 3]), + _ => (&[9, 10, 11, 0], [3, 0]), + }; + ( + pi.iter().filter_map(|i| pp.get(*i).copied()).collect::>(), + ci.iter().filter_map(|i| pc.get(*i).copied()).collect::>(), + ) + } + _ => (Vec::new(), Vec::new()), + }; + let new_pts = if shared_pts.len() == 4 { npts - 4 } else { npts }; + let new_cols = if shared_cols.len() == 2 { 2 } else { 4 }; + let mut pts = shared_pts.clone(); + if pts.len() != 4 { + pts.clear(); + } + let mut ok = true; + for _ in 0..new_pts { + match r.point() { + Some(p) => pts.push(p), + None => { + ok = false; + break; + } + } + } + let mut cols = if shared_cols.len() == 2 { shared_cols } else { Vec::new() }; + for _ in 0..new_cols { + match r.color(ncomp) { + Some(c) => cols.push(to_dev(ip, &c)?), + None => { + ok = false; + break; + } + } + } + if r.bits.is_some() { + r.align(); + } + if !ok || pts.len() != npts || cols.len() != 4 { + break; + } + points.extend(&pts); + colors.extend(&cols); + prev = Some((pts, cols)); + count += 1; + } + if kind == 6 { ShadeKind::Coons } else { ShadeKind::Tensor } + } + }; + if points.is_empty() { + return Ok(None); + } + let bbox = match d.get_name(b"BBox") { + Some(Obj::Array(a, _)) => { + let v: Vec = a.items().iter().filter_map(Obj::num).collect(); + <[f64; 4]>::try_from(v).ok() + } + _ => None, + }; + let (space, data) = pack(&colors); + let n = colors.len(); + Ok(Some(Shading { kind: shade, extend: [false, false], space, data, samples: n, bbox, points })) +} diff --git a/crates/ps/src/read/ops/misc.rs b/crates/ps/src/read/ops/misc.rs new file mode 100644 index 0000000000..9542d859ea --- /dev/null +++ b/crates/ps/src/read/ops/misc.rs @@ -0,0 +1,740 @@ +//! Files, filters, resources, forms, output, and device parameters EPS programs query or set. + +use crate::read::interp::{Interp, PsErr, R, err, matrix_of}; +use crate::read::obj::{Key, Obj, PArr, PDict, name}; +use crate::read::source::{FilterParams, Source}; + +// ----- files ----- + +pub fn currentfile(ip: &mut Interp) -> R { + let id = + ip.e.iter() + .rev() + .find_map(|f| match f { + crate::read::interp::Frame::File { id } => Some(*id), + _ => None, + }) + .unwrap_or(ip.main); + ip.push(Obj::File(id)) +} + +fn pop_file(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::File(id) => Ok(id), + _ => err("typecheck"), + } +} + +pub fn read(ip: &mut Interp) -> R { + let f = pop_file(ip)?; + match ip.srcs.read(f) { + Some(c) => { + ip.push(Obj::Int(i64::from(c)))?; + ip.push(Obj::Bool(true)) + } + None => ip.push(Obj::Bool(false)), + } +} + +pub fn readstring(ip: &mut Interp) -> R { + let s = ip.pop_str()?; + let f = pop_file(ip)?; + let mut n = 0; + while n < s.len { + match ip.srcs.read(f) { + Some(c) => { + s.set(n, c); + n += 1; + } + None => break, + } + } + let full = n == s.len; + let sub = s.interval(0, n).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(sub, false))?; + ip.push(Obj::Bool(full)) +} + +pub fn readhexstring(ip: &mut Interp) -> R { + let s = ip.pop_str()?; + let f = pop_file(ip)?; + let mut n = 0; + let mut hi: Option = None; + while n < s.len { + let Some(c) = ip.srcs.read(f) else { break }; + let v = match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => continue, + }; + match hi.take() { + Some(h) => { + s.set(n, h << 4 | v); + n += 1; + } + None => hi = Some(v), + } + } + let full = n == s.len; + let sub = s.interval(0, n).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(sub, false))?; + ip.push(Obj::Bool(full)) +} + +pub fn readline(ip: &mut Interp) -> R { + let s = ip.pop_str()?; + let f = pop_file(ip)?; + let mut n = 0; + let mut eol = false; + while let Some(c) = ip.srcs.read(f) { + if c == b'\n' { + eol = true; + break; + } + if c == b'\r' { + if ip.srcs.peek(f) == Some(b'\n') { + ip.srcs.read(f); + } + eol = true; + break; + } + if n >= s.len { + return err("rangecheck"); + } + s.set(n, c); + n += 1; + } + let sub = s.interval(0, n).ok_or(PsErr::Ps("rangecheck"))?; + ip.push(Obj::Str(sub, false))?; + ip.push(Obj::Bool(eol || n > 0)) +} + +pub fn bytesavailable(ip: &mut Interp) -> R { + let f = pop_file(ip)?; + let n = ip.srcs.available(f); + ip.push(Obj::Int(n)) +} + +pub fn closefile(ip: &mut Interp) -> R { + let f = pop_file(ip)?; + ip.srcs.close(f); + Ok(()) +} + +pub fn resetfile(ip: &mut Interp) -> R { + let f = pop_file(ip)?; + ip.srcs.reset(f); + Ok(()) +} + +pub fn fileposition(ip: &mut Interp) -> R { + let f = pop_file(ip)?; + let p = ip.srcs.position(f).ok_or(PsErr::Ps("ioerror"))?; + ip.push(Obj::Int(i64::try_from(p).unwrap_or(i64::MAX))) +} + +pub fn setfileposition(ip: &mut Interp) -> R { + let p = ip.pop_usize()?; + let f = pop_file(ip)?; + if ip.srcs.set_position(f, p) { Ok(()) } else { err("ioerror") } +} + +pub fn flushfile(ip: &mut Interp) -> R { + // On an input file: read and discard to its end. + let f = pop_file(ip)?; + if f != ip.main { + while ip.srcs.read(f).is_some() {} + } + Ok(()) +} + +pub fn status(ip: &mut Interp) -> R { + match ip.pop()? { + Obj::File(f) => { + let open = ip.srcs.available(f) >= 0; + ip.push(Obj::Bool(open)) + } + _ => ip.push(Obj::Bool(false)), + } +} + +pub fn file(ip: &mut Interp) -> R { + let _access = ip.pop()?; + let fname = ip.pop_name()?; + match fname.as_slice() { + b"%stdin" => ip.push(Obj::File(ip.main)), + // Output files swallow what is written. + b"%stdout" | b"%stderr" | b"%lineedit" | b"%statementedit" => { + let id = ip.srcs.add(Source::bytes(Vec::new())); + ip.push(Obj::File(id)) + } + // An EPS can't reach the disk. + _ => err("invalidfileaccess"), + } +} + +pub fn write_ops(ip: &mut Interp) -> R { + // `file int write`, `file string writestring`: discarded. + ip.pop()?; + ip.pop()?; + Ok(()) +} + +pub fn eexec(ip: &mut Interp) -> R { + let up = match ip.pop()? { + Obj::File(id) => id, + Obj::Str(s, _) => ip.srcs.add(Source::bytes(s.bytes())), + _ => return err("typecheck"), + }; + let id = ip.srcs.add(Source::eexec(up)); + // eexec runs with systemdict on top, which its program removes with `end` (`closefile`). + let sd = ip.systemdict.clone(); + ip.begin(sd)?; + ip.push_frame(crate::read::interp::Frame::File { id }) +} + +/// `src [params] /Name filter`, with SubFileDecode's own `count string` operands. +pub fn filter(ip: &mut Interp) -> R { + let fname = ip.pop_name()?; + let mut params = FilterParams { early_change: true, ..Default::default() }; + if fname == b"SubFileDecode" { + let eod = ip.pop()?; + params.eod_string = match eod { + Obj::Str(s, _) => s.bytes(), + _ => return err("typecheck"), + }; + params.eod_count = ip.pop_usize()?; + if let Ok(Obj::Dict(_)) = ip.top(0) { + ip.pop()?; + } + } else if let Ok(Obj::Dict(d)) = ip.top(0).cloned() { + // A dictionary operand is parameters unless it is the data source itself (never: a + // dictionary can't be a source). + ip.pop()?; + let num = |k: &[u8]| d.get_name(k).and_then(|o| o.num()); + params.early_change = num(b"EarlyChange").is_none_or(|v| v != 0.0); + params.predictor = num(b"Predictor").unwrap_or(1.0) as i64; + params.colors = num(b"Colors").unwrap_or(1.0) as i64; + params.bpc = num(b"BitsPerComponent").unwrap_or(8.0) as i64; + params.columns = num(b"Columns").unwrap_or(1.0) as i64; + } + let src = ip.pop()?; + let up = match src { + Obj::File(id) => id, + Obj::Str(s, _) => ip.srcs.add(Source::bytes(s.bytes())), + p @ Obj::Array(_, true) => { + // A procedure source: call it until it gives an empty string. + let mut data = Vec::new(); + for _ in 0..1_000_000 { + ip.call(p.clone())?; + let s = ip.pop_str()?.bytes(); + if s.is_empty() { + break; + } + data.extend_from_slice(&s); + ip.alloc(s.len())?; + } + ip.srcs.add(Source::bytes(data)) + } + _ => return err("typecheck"), + }; + if fname.ends_with(b"Encode") && fname != b"NullEncode" { + // Encoding filters write; an EPS has nowhere to write to. + let id = ip.srcs.add(Source::bytes(Vec::new())); + return ip.push(Obj::File(id)); + } + match ip.srcs.filter(up, &fname, ¶ms) { + Ok(id) => ip.push(Obj::File(id)), + Err(e) => { + ip.warnings.push(e); + err("ioerror") + } + } +} + +// ----- output ----- + +pub fn pop1(ip: &mut Interp) -> R { + ip.pop().map(|_| ()) +} + +pub fn noop(_ip: &mut Interp) -> R { + Ok(()) +} + +pub fn showpage(_ip: &mut Interp) -> R { + Ok(()) +} + +// ----- environment ----- + +pub fn languagelevel(ip: &mut Interp) -> R { + ip.push(Obj::Int(3)) +} + +pub fn version(ip: &mut Interp) -> R { + ip.push(Obj::str(b"3010")) +} + +pub fn product(ip: &mut Interp) -> R { + ip.push(Obj::str(b"craft-ps")) +} + +pub fn revision(ip: &mut Interp) -> R { + ip.push(Obj::Int(1)) +} + +pub fn zero(ip: &mut Interp) -> R { + ip.push(Obj::Int(0)) +} + +pub fn vmstatus(ip: &mut Interp) -> R { + let used = i64::try_from(ip.memory).unwrap_or(i64::MAX); + ip.push(Obj::Int(i64::try_from(ip.saves.len()).unwrap_or(0)))?; + ip.push(Obj::Int(used))?; + ip.push(Obj::Int(1 << 30)) +} + +pub fn push_false(ip: &mut Interp) -> R { + ip.push(Obj::Bool(false)) +} + +pub fn empty_dict(ip: &mut Interp) -> R { + ip.push(Obj::Dict(PDict::new(4))) +} + +pub fn pop_dict(ip: &mut Interp) -> R { + ip.pop_dict().map(|_| ()) +} + +/// The system and user parameters programs read (all limits generous). +pub fn currentsystemparams(ip: &mut Interp) -> R { + let d = PDict::new(24); + for k in [ + "MaxPatternCache", + "MaxFontCache", + "MaxFormCache", + "MaxScreenStorage", + "MaxDisplayList", + "MaxOutlineCache", + "MaxUPathCache", + "MaxStoredScreenCache", + "MaxSourceList", + "MaxDisplayAndSourceList", + "MaxImageBuffer", + "MaxRasterMemory", + "MaxStackSize", + "CurFontCache", + "CurFormCache", + "CurPatternCache", + "CurOutlineCache", + "CurUPathCache", + "CurScreenStorage", + "CurDisplayList", + "CurSourceList", + "CurStoredScreenCache", + "CurInputDevice", + "CurOutputDevice", + ] { + let v = if k.starts_with("Cur") { 0 } else { 8 << 20 }; + d.put_name(k.as_bytes(), Obj::Int(v)); + } + d.put_name(b"ByteOrder", Obj::Bool(false)); + d.put_name(b"RealFormat", Obj::str(b"IEEE")); + d.put_name(b"BuildTime", Obj::Int(0)); + ip.push(Obj::Dict(d)) +} + +pub fn currentuserparams(ip: &mut Interp) -> R { + let d = PDict::new(20); + for (k, v) in [ + ("MaxPatternItem", 20_000), + ("MaxFormItem", 100_000), + ("MaxFontItem", 12_500), + ("MaxUPathItem", 0), + ("MaxLocalVM", 1 << 30), + ("MaxGlobalVM", 1 << 30), + ("VMReclaim", 0), + ("VMThreshold", 1 << 20), + ("MaxDictStack", 1000), + ("MaxExecStack", 100_000), + ("MaxOpStack", 500_000), + ("MinFontCompress", 100), + ("MaxSuperScreen", 1016), + ("MaxLocalVM", 1 << 30), + ("JobTimeout", 0), + ("WaitTimeout", 0), + ] { + d.put_name(k.as_bytes(), Obj::Int(v)); + } + d.put_name(b"AccurateScreens", Obj::Bool(false)); + d.put_name(b"HalftoneMode", Obj::Int(0)); + d.put_name(b"IdiomRecognition", Obj::Bool(false)); + ip.push(Obj::Dict(d)) +} + +pub fn currentpagedevice(ip: &mut Interp) -> R { + let d = PDict::new(8); + d.put_name(b"PageSize", Obj::Array(PArr::new(vec![Obj::Int(612), Obj::Int(792)]), false)); + d.put_name(b"HWResolution", Obj::Array(PArr::new(vec![Obj::Int(72), Obj::Int(72)]), false)); + d.put_name(b"Policies", Obj::Dict(PDict::new(2))); + d.put_name(b"ProcessColorModel", Obj::lit(b"DeviceCMYK")); + d.put_name(b"Separations", Obj::Bool(false)); + ip.push(Obj::Dict(d)) +} + +pub fn currentscreen(ip: &mut Interp) -> R { + ip.push(Obj::Real(60.0))?; + ip.push(Obj::Real(45.0))?; + ip.push(Obj::proc(vec![Obj::Op(crate::read::ops::op_id(b"pop").unwrap_or(0)), Obj::Real(0.0)])) +} + +pub fn setscreen(ip: &mut Interp) -> R { + for _ in 0..3 { + ip.pop()?; + } + Ok(()) +} + +pub fn setcolorscreen(ip: &mut Interp) -> R { + for _ in 0..12 { + ip.pop()?; + } + Ok(()) +} + +pub fn currentcolorscreen(ip: &mut Interp) -> R { + for _ in 0..4 { + currentscreen(ip)?; + } + Ok(()) +} + +pub fn empty_proc(ip: &mut Interp) -> R { + ip.push(Obj::proc(Vec::new())) +} + +pub fn currentcolortransfer(ip: &mut Interp) -> R { + for _ in 0..4 { + empty_proc(ip)?; + } + Ok(()) +} + +pub fn setcolortransfer(ip: &mut Interp) -> R { + for _ in 0..4 { + ip.pop()?; + } + Ok(()) +} + +pub fn findcolorrendering(ip: &mut Interp) -> R { + let n = ip.pop()?; + ip.push(n)?; + ip.push(Obj::Bool(true)) +} + +pub fn realtime(ip: &mut Interp) -> R { + ip.push(Obj::Int(0)) +} + +pub fn internaldict(ip: &mut Interp) -> R { + ip.pop_int()?; + let d = match ip.systemdict.get_name(b"CraftPsInternal") { + Some(Obj::Dict(d)) => d, + _ => { + let d = PDict::new(10); + ip.systemdict.put_name(b"CraftPsInternal", Obj::Dict(d.clone())); + d + } + }; + ip.push(Obj::Dict(d)) +} + +pub fn setglobal(ip: &mut Interp) -> R { + ip.pop_bool().map(|_| ()) +} + +pub fn print_any(ip: &mut Interp) -> R { + ip.pop().map(|_| ()) +} + +pub fn pstack(_ip: &mut Interp) -> R { + Ok(()) +} + +pub fn nulldevice(ip: &mut Interp) -> R { + ip.gs.nulldev = true; + Ok(()) +} + +pub fn execform(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + let m = match d.get_name(b"Matrix") { + Some(Obj::Array(a, _)) => matrix_of(&a)?, + _ => crate::read::gstate::M::ID, + }; + let paint = d.get_name(b"PaintProc").ok_or(PsErr::Ps("undefined"))?; + ip.gsave()?; + ip.gs.ctm = m.then(&ip.gs.ctm); + let r = (|| { + if let Some(Obj::Array(bb, _)) = d.get_name(b"BBox") { + ip.push(Obj::Array(bb, false))?; + crate::read::ops::gfx::rectclip(ip)?; + } + ip.push(Obj::Dict(d.clone()))?; + ip.call(paint) + })(); + ip.grestore(); + r +} + +// ----- resources ----- + +fn category(ip: &mut Interp, cat: &[u8]) -> PDict { + match ip.resources.get_name(cat) { + Some(Obj::Dict(d)) => d, + _ => { + let d = PDict::new(20); + ip.resources.put_name(cat, Obj::Dict(d.clone())); + d + } + } +} + +pub fn findresource(ip: &mut Interp) -> R { + let cat = ip.pop_name()?; + let key = ip.pop()?; + if cat == b"Font" { + let f = crate::read::font::find_font(ip, &key); + return ip.push(Obj::Dict(f)); + } + if cat == b"Category" { + let d = category(ip, &crate::read::ops::basic::text_of(&key)); + return ip.push(Obj::Dict(d)); + } + let d = category(ip, &cat); + if let Some(v) = d.get(&key.key()) { + return ip.push(v); + } + if cat == b"Encoding" + && let Some(e) = ip.systemdict.get(&key.key()) + { + return ip.push(e); + } + if cat == b"ColorSpace" || cat == b"ColorRendering" || cat == b"Halftone" { + return ip.push(Obj::Dict(PDict::new(1))); + } + ip.dollar_error.put_name(b"command", Obj::lit(b"findresource")); + err("undefinedresource") +} + +pub fn defineresource(ip: &mut Interp) -> R { + let cat = ip.pop_name()?; + let v = ip.pop()?; + let key = ip.pop()?; + if cat == b"CIDFont" + && let Obj::Dict(d) = &v + && !d.known(&Key::Name(name(b"FID"))) + { + d.put_name(b"FID", Obj::FontId(ip.next_fid)); + ip.next_fid += 1; + } + if cat == b"Font" + && let Obj::Dict(d) = &v + { + if !d.known(&Key::Name(name(b"FID"))) { + d.put_name(b"FID", Obj::FontId(ip.next_fid)); + ip.next_fid += 1; + } + ip.fontdir.put(key.clone(), v.clone()); + } + category(ip, &cat).put(key, v.clone()); + ip.push(v) +} + +pub fn undefineresource(ip: &mut Interp) -> R { + let cat = ip.pop_name()?; + let key = ip.pop()?; + category(ip, &cat).remove(&key.key()); + Ok(()) +} + +pub fn resourcestatus(ip: &mut Interp) -> R { + let cat = ip.pop_name()?; + let key = ip.pop()?; + let known = category(ip, &cat).known(&key.key()) || (cat == b"Font" && ip.fontdir.known(&key.key())); + if known { + ip.push(Obj::Int(1))?; + ip.push(Obj::Int(0))?; + ip.push(Obj::Bool(true)) + } else { + ip.push(Obj::Bool(false)) + } +} + +pub fn resourceforall(ip: &mut Interp) -> R { + let cat = ip.pop_name()?; + let scratch = ip.pop_str()?; + let proc = ip.pop()?; + let _template = ip.pop()?; + let entries = category(ip, &cat).entries(); + ip.charge(entries.len() as u64)?; + ip.alloc(entries.len().saturating_mul(2 * crate::read::ops::basic::OBJ_BYTES))?; + for (k, _) in entries { + let t = crate::read::ops::basic::text_of(&k); + if let Some(w) = scratch.put_bytes(0, &t) { + ip.push(Obj::Str(w, false))?; + ip.call(proc.clone())?; + } + } + Ok(()) +} + +// ----- user paths and objects ----- + +pub fn ucache(_ip: &mut Interp) -> R { + Ok(()) +} + +/// Run a user path (an array of numbers and operator names) to build the current path. +fn user_path(ip: &mut Interp, o: Obj) -> R { + let items = match o { + Obj::Array(a, _) => a.items(), + _ => return err("typecheck"), + }; + crate::read::ops::gfx::newpath(ip)?; + for it in items { + match it { + Obj::Name(n, _) => { + let n: &[u8] = &n; + if matches!(n, b"ucache" | b"setbbox") { + if n == b"setbbox" { + for _ in 0..4 { + ip.pop()?; + } + } + continue; + } + let op = crate::read::ops::op_id(n).ok_or(PsErr::Ps("typecheck"))?; + ip.call_op(op)?; + } + Obj::Op(op) => { + if crate::read::ops::OPS.get(op as usize).is_some_and(|(n, _)| *n == b"setbbox") { + for _ in 0..4 { + ip.pop()?; + } + continue; + } + ip.call_op(op)?; + } + o => ip.push(o)?, + } + } + Ok(()) +} + +pub fn uappend(ip: &mut Interp) -> R { + let o = ip.pop()?; + let saved = std::mem::take(&mut ip.gs.path); + user_path(ip, o)?; + let mut p = saved; + p.append(&mut ip.gs.path); + ip.gs.path = p; + Ok(()) +} + +fn upaint(ip: &mut Interp, op: &[u8]) -> R { + let mut m = None; + if let Ok(Obj::Array(a, _)) = ip.top(0).cloned() + && a.len == 6 + && a.items().iter().all(|o| o.num().is_some()) + && ip.o.len() >= 2 + && matches!(ip.top(1), Ok(Obj::Array(..))) + { + m = Some(ip.pop_matrix()?); + } + let o = ip.pop()?; + ip.gsave()?; + let r = (|| { + user_path(ip, o)?; + if let Some(m) = m { + ip.gs.ctm = m.then(&ip.gs.ctm); + } + match op { + b"ufill" => crate::read::ops::gfx::fill(ip), + b"ueofill" => crate::read::ops::gfx::eofill(ip), + _ => crate::read::ops::gfx::stroke(ip), + } + })(); + ip.grestore(); + r +} + +pub fn ufill(ip: &mut Interp) -> R { + upaint(ip, b"ufill") +} + +pub fn ueofill(ip: &mut Interp) -> R { + upaint(ip, b"ueofill") +} + +pub fn ustroke(ip: &mut Interp) -> R { + upaint(ip, b"ustroke") +} + +pub fn setbbox(ip: &mut Interp) -> R { + for _ in 0..4 { + ip.pop_num()?; + } + Ok(()) +} + +pub fn defineuserobject(ip: &mut Interp) -> R { + let v = ip.pop()?; + let i = ip.pop_usize()?; + let arr = match ip.userdict.get_name(b"UserObjects") { + Some(Obj::Array(a, _)) if a.len > i => a, + old => { + let mut items = match old { + Some(Obj::Array(a, _)) => a.items(), + _ => Vec::new(), + }; + if i >= 100_000 { + return err("limitcheck"); + } + ip.charge((i + 1) as u64)?; + ip.alloc((i + 1).saturating_mul(crate::read::ops::basic::OBJ_BYTES))?; + items.resize(i + 1, Obj::Null); + let a = PArr::new(items); + ip.userdict.put_name(b"UserObjects", Obj::Array(a.clone(), false)); + a + } + }; + arr.set(i, v); + Ok(()) +} + +pub fn execuserobject(ip: &mut Interp) -> R { + let i = ip.pop_usize()?; + let v = match ip.userdict.get_name(b"UserObjects") { + Some(Obj::Array(a, _)) => a.get(i).ok_or(PsErr::Ps("undefined"))?, + _ => return err("undefined"), + }; + ip.exec_value(v) +} + +pub fn undefineuserobject(ip: &mut Interp) -> R { + ip.pop_usize().map(|_| ()) +} + +pub fn string_noop(ip: &mut Interp) -> R { + // `setsmoothness` and the like: one number. + ip.pop_num().map(|_| ()) +} + +pub fn gcheck(ip: &mut Interp) -> R { + ip.pop()?; + ip.push(Obj::Bool(false)) +} diff --git a/crates/ps/src/read/ops/mod.rs b/crates/ps/src/read/ops/mod.rs new file mode 100644 index 0000000000..299abb0cda --- /dev/null +++ b/crates/ps/src/read/ops/mod.rs @@ -0,0 +1,226 @@ +//! The operator table: every name `systemdict` binds to an operator. + +pub mod basic; +pub mod gfx; +pub mod image; +pub mod mesh; +pub mod misc; +pub mod shading; + +use crate::read::interp::{Interp, R}; +use crate::read::obj::{Obj, PArr, PDict}; +use crate::read::{composite, font}; + +pub type OpId = u16; +pub type OpFn = fn(&mut Interp) -> R; + +macro_rules! named { + ($($n:literal => $f:expr),* $(,)?) => { &[$(($n.as_bytes(), $f as OpFn)),*] }; +} + +pub static OPS: &[(&[u8], OpFn)] = named! { + // stack + "pop" => basic::pop, "exch" => basic::exch, "dup" => basic::dup, "copy" => basic::copy, "index" => basic::index, + "roll" => basic::roll, "clear" => basic::clear, "count" => basic::count, "mark" => basic::mark, "[" => basic::mark, + "<<" => basic::mark, "]" => basic::array_close, ">>" => basic::dict_close, "cleartomark" => basic::cleartomark, + "counttomark" => basic::counttomark, + // arithmetic + "add" => basic::add, "sub" => basic::sub, "mul" => basic::mul, "div" => basic::div, "idiv" => basic::idiv, + "mod" => basic::mod_, "neg" => basic::neg, "abs" => basic::abs, "ceiling" => basic::ceiling, "floor" => basic::floor, + "round" => basic::round, "truncate" => basic::truncate, "sqrt" => basic::sqrt, "sin" => basic::sin, "cos" => basic::cos, + "atan" => basic::atan, "exp" => basic::exp, "ln" => basic::ln, "log" => basic::log, "rand" => basic::rand, + "srand" => basic::srand, "rrand" => basic::rrand, + // relations + "eq" => basic::eq, "ne" => basic::ne, "gt" => basic::gt, "ge" => basic::ge, "lt" => basic::lt, "le" => basic::le, + "and" => basic::and, "or" => basic::or, "xor" => basic::xor, "not" => basic::not, "bitshift" => basic::bitshift, + "true" => basic::true_, "false" => basic::false_, "null" => basic::null, + // control + "exec" => basic::exec, "if" => basic::if_, "ifelse" => basic::ifelse, "for" => basic::for_, "repeat" => basic::repeat, + "loop" => basic::loop_, "exit" => basic::exit, "stop" => basic::stop, "stopped" => basic::stopped, "quit" => basic::quit, + "forall" => basic::forall, "countexecstack" => basic::countexecstack, "execstack" => basic::execstack, + // types + "type" => basic::type_, "cvlit" => basic::cvlit, "cvx" => basic::cvx, "xcheck" => basic::xcheck, + "readonly" => basic::noop_keep, "executeonly" => basic::noop_keep, "noaccess" => basic::noop_keep, + "rcheck" => basic::check_true, "wcheck" => basic::check_true, "cvi" => basic::cvi, "cvr" => basic::cvr, + "cvn" => basic::cvn, "cvs" => basic::cvs, "cvrs" => basic::cvrs, "bind" => basic::bind, + // dictionaries + "dict" => basic::dict, "maxlength" => basic::maxlength, "begin" => basic::begin, "end" => basic::end, "def" => basic::def, + "load" => basic::load, "store" => basic::store, "get" => basic::get, "put" => basic::put, "known" => basic::known, + "undef" => basic::undef, "where" => basic::where_, "currentdict" => basic::currentdict, + "countdictstack" => basic::countdictstack, "dictstack" => basic::dictstack, "cleardictstack" => basic::cleardictstack, + // strings and arrays + "array" => basic::array, "string" => basic::string, "length" => basic::length, "getinterval" => basic::getinterval, + "putinterval" => basic::putinterval, "aload" => basic::aload, "astore" => basic::astore, + "packedarray" => basic::packedarray, "search" => basic::search, "anchorsearch" => basic::anchorsearch, + "token" => basic::token, + // graphics state + "gsave" => gfx::gsave, "grestore" => gfx::grestore, "grestoreall" => gfx::grestoreall, "initgraphics" => gfx::initgraphics, + "save" => gfx::save, "restore" => gfx::restore, "gstate" => gfx::gstate, "currentgstate" => gfx::currentgstate, + "setgstate" => gfx::setgstate, "setlinewidth" => gfx::setlinewidth, "currentlinewidth" => gfx::currentlinewidth, + "setlinecap" => gfx::setlinecap, "currentlinecap" => gfx::currentlinecap, "setlinejoin" => gfx::setlinejoin, + "currentlinejoin" => gfx::currentlinejoin, "setmiterlimit" => gfx::setmiterlimit, + "currentmiterlimit" => gfx::currentmiterlimit, "setdash" => gfx::setdash, "currentdash" => gfx::currentdash, + "setflat" => gfx::setflat, "currentflat" => gfx::currentflat, "setstrokeadjust" => gfx::setstrokeadjust, + "currentstrokeadjust" => gfx::currentstrokeadjust, "setoverprint" => gfx::setoverprint, + "currentoverprint" => gfx::currentoverprint, + // matrices + "matrix" => gfx::matrix, "initmatrix" => gfx::initmatrix, "identmatrix" => gfx::identmatrix, + "defaultmatrix" => gfx::defaultmatrix, "currentmatrix" => gfx::currentmatrix, "setmatrix" => gfx::setmatrix, + "translate" => gfx::translate, "scale" => gfx::scale, "rotate" => gfx::rotate, "concat" => gfx::concat, + "concatmatrix" => gfx::concatmatrix, "invertmatrix" => gfx::invertmatrix, "transform" => gfx::transform, + "itransform" => gfx::itransform, "dtransform" => gfx::dtransform, "idtransform" => gfx::idtransform, + // paths + "newpath" => gfx::newpath, "moveto" => gfx::moveto, "rmoveto" => gfx::rmoveto, "lineto" => gfx::lineto, + "rlineto" => gfx::rlineto, "curveto" => gfx::curveto, "rcurveto" => gfx::rcurveto, "closepath" => gfx::closepath, + "arc" => gfx::arc, "arcn" => gfx::arcn, "arct" => gfx::arct, "arcto" => gfx::arcto, "currentpoint" => gfx::currentpoint, + "flattenpath" => gfx::flattenpath, "reversepath" => gfx::reversepath, "strokepath" => gfx::strokepath, + "pathbbox" => gfx::pathbbox, "pathforall" => gfx::pathforall, "clippath" => gfx::clippath, "initclip" => gfx::initclip, + "clipsave" => gfx::clipsave, "cliprestore" => gfx::cliprestore, "clip" => gfx::clip, "eoclip" => gfx::eoclip, "rectclip" => gfx::rectclip, + // painting + "fill" => gfx::fill, "eofill" => gfx::eofill, "stroke" => gfx::stroke, "rectfill" => gfx::rectfill, + "rectstroke" => gfx::rectstroke, "shfill" => shading::shfill, "erasepage" => misc::noop, + "image" => image::image, "colorimage" => image::colorimage, "imagemask" => image::imagemask, + // colour + "setgray" => gfx::setgray, "currentgray" => gfx::currentgray, "setrgbcolor" => gfx::setrgbcolor, + "currentrgbcolor" => gfx::currentrgbcolor, "sethsbcolor" => gfx::sethsbcolor, "currenthsbcolor" => gfx::currenthsbcolor, + "setcmykcolor" => gfx::setcmykcolor, "currentcmykcolor" => gfx::currentcmykcolor, "setcolorspace" => gfx::setcolorspace, + "currentcolorspace" => gfx::currentcolorspace, "setcolor" => gfx::setcolor, "currentcolor" => gfx::currentcolor, + "setpattern" => gfx::setpattern, "makepattern" => gfx::makepattern, + // fonts + "findfont" => font::findfont, "definefont" => font::definefont, "undefinefont" => font::undefinefont, + "scalefont" => font::scalefont, "makefont" => font::makefont, "setfont" => font::setfont, + "currentfont" => font::currentfont, "rootfont" => font::currentfont, "selectfont" => font::selectfont, + "show" => font::show, "ashow" => font::ashow, "widthshow" => font::widthshow, "awidthshow" => font::awidthshow, + "xshow" => font::xshow, "yshow" => font::yshow, "xyshow" => font::xyshow, "kshow" => font::kshow, "cshow" => font::cshow, + "glyphshow" => font::glyphshow, "stringwidth" => font::stringwidth, "charpath" => font::charpath, + "setcharwidth" => font::setcharwidth, "setcachedevice" => font::setcachedevice, + "setcachedevice2" => font::setcachedevice2, "setcachelimit" => misc::pop1, "cachestatus" => misc::noop, + // files + "currentfile" => misc::currentfile, "read" => misc::read, "readstring" => misc::readstring, + "readhexstring" => misc::readhexstring, "readline" => misc::readline, "bytesavailable" => misc::bytesavailable, + "closefile" => misc::closefile, "resetfile" => misc::resetfile, "fileposition" => misc::fileposition, + "setfileposition" => misc::setfileposition, "flushfile" => misc::flushfile, "status" => misc::status, + "file" => misc::file, "write" => misc::write_ops, "writestring" => misc::write_ops, "writehexstring" => misc::write_ops, + "eexec" => misc::eexec, "filter" => misc::filter, "flush" => misc::noop, + "print" => misc::print_any, "=" => misc::print_any, "==" => misc::print_any, "stack" => misc::pstack, + "pstack" => misc::pstack, + // resources and forms + "findresource" => misc::findresource, "defineresource" => misc::defineresource, + "undefineresource" => misc::undefineresource, "resourcestatus" => misc::resourcestatus, + "resourceforall" => misc::resourceforall, "execform" => misc::execform, + // user paths and objects + "ucache" => misc::ucache, "setbbox" => misc::setbbox, "uappend" => misc::uappend, "ufill" => misc::ufill, + "ueofill" => misc::ueofill, "ustroke" => misc::ustroke, "defineuserobject" => misc::defineuserobject, + "execuserobject" => misc::execuserobject, "undefineuserobject" => misc::undefineuserobject, + // environment and device + "showpage" => misc::showpage, "copypage" => misc::noop, "nulldevice" => misc::nulldevice, + "languagelevel" => misc::languagelevel, "version" => misc::version, "product" => misc::product, + "revision" => misc::revision, "serialnumber" => misc::zero, "realtime" => misc::realtime, "usertime" => misc::realtime, + "vmstatus" => misc::vmstatus, "vmreclaim" => misc::pop1, "setvmthreshold" => misc::pop1, + "setglobal" => misc::setglobal, "currentglobal" => misc::push_false, "setpacking" => misc::setglobal, + "currentpacking" => misc::push_false, "gcheck" => misc::gcheck, "setshared" => misc::setglobal, + "currentshared" => misc::push_false, "setpagedevice" => misc::pop_dict, "currentpagedevice" => misc::currentpagedevice, + "setuserparams" => misc::pop_dict, "currentuserparams" => misc::currentuserparams, "setsystemparams" => misc::pop_dict, + "currentsystemparams" => misc::currentsystemparams, "setdevparams" => misc::write_ops, "currentdevparams" => misc::empty_dict, + "setscreen" => misc::setscreen, "currentscreen" => misc::currentscreen, "setcolorscreen" => misc::setcolorscreen, + "currentcolorscreen" => misc::currentcolorscreen, "sethalftone" => misc::pop_dict, "currenthalftone" => misc::empty_dict, + "settransfer" => misc::pop1, "currenttransfer" => misc::empty_proc, "setcolortransfer" => misc::setcolortransfer, + "currentcolortransfer" => misc::currentcolortransfer, "setblackgeneration" => misc::pop1, + "currentblackgeneration" => misc::empty_proc, "setundercolorremoval" => misc::pop1, + "currentundercolorremoval" => misc::empty_proc, "setcolorrendering" => misc::pop_dict, + "currentcolorrendering" => misc::empty_dict, "findcolorrendering" => misc::findcolorrendering, + "setsmoothness" => misc::string_noop, "currentsmoothness" => misc::zero, "internaldict" => misc::internaldict, + "setucacheparams" => misc::noop, "ucachestatus" => misc::noop, "setobjectformat" => misc::pop1, + "currentobjectformat" => misc::zero, "sethalftonephase" => misc::setbbox, "echo" => misc::pop1, + "prompt" => misc::noop, "executive" => misc::noop, "start" => misc::noop, + // CFF font sets, CMaps and composite fonts + "StartData" => composite::startdata, "composefont" => composite::composefont, + "begincmap" => composite::begincmap, "endcmap" => composite::endcmap, "usecmap" => composite::usecmap, + "begincodespacerange" => composite::begin_block, "endcodespacerange" => composite::endcodespacerange, + "begincidrange" => composite::begin_block, "endcidrange" => composite::endcidrange, + "begincidchar" => composite::begin_block, "endcidchar" => composite::endcidchar, + "beginbfchar" => composite::begin_block, "endbfchar" => composite::end_ignored, + "beginbfrange" => composite::begin_block, "endbfrange" => composite::end_ignored, + "beginnotdefrange" => composite::begin_block, "endnotdefrange" => composite::end_ignored, + "beginnotdefchar" => composite::begin_block, "endnotdefchar" => composite::end_ignored, +}; + +pub fn op_id(n: &[u8]) -> Option { + OPS.iter().position(|(name, _)| *name == n).and_then(|i| OpId::try_from(i).ok()) +} + +fn encoding(names: Vec<&'static str>) -> Obj { + Obj::Array(PArr::new(names.into_iter().map(|n| Obj::lit(n.as_bytes())).collect()), false) +} + +/// Fill systemdict and the standard dictionaries. +pub fn install(ip: &mut Interp) { + for (i, (n, _)) in OPS.iter().enumerate() { + if let Ok(id) = OpId::try_from(i) { + ip.systemdict.put_name(n, Obj::Op(id)); + } + } + // Environment queries are plain values: executing the name pushes them, and programs that + // `get` them from systemdict (`systemdict /languagelevel get 2 ge`) find a number. + ip.systemdict.put_name(b"languagelevel", Obj::Int(3)); + for (n, d) in [ + (&b"systemdict"[..], ip.systemdict.clone()), + (b"userdict", ip.userdict.clone()), + (b"globaldict", ip.globaldict.clone()), + (b"errordict", ip.errordict.clone()), + (b"$error", ip.dollar_error.clone()), + (b"statusdict", ip.statusdict.clone()), + (b"FontDirectory", ip.fontdir.clone()), + (b"GlobalFontDirectory", ip.fontdir.clone()), + (b"SharedFontDirectory", ip.fontdir.clone()), + ] { + ip.systemdict.put_name(n, Obj::Dict(d)); + } + ip.systemdict.put_name(b"version", Obj::str(b"3010")); + ip.systemdict.put_name(b"product", Obj::str(b"craft-ps")); + ip.systemdict.put_name(b"revision", Obj::Int(1)); + ip.systemdict.put_name(b"serialnumber", Obj::Int(0)); + ip.systemdict.put_name(b"StandardEncoding", encoding(crate::read::encodings::standard())); + ip.systemdict.put_name(b"ISOLatin1Encoding", encoding(crate::read::encodings::iso_latin1())); + // Errors are handled by `stopped`; the handlers exist for programs that look at them. + for e in [ + "dictfull", + "dictstackoverflow", + "dictstackunderflow", + "execstackoverflow", + "handleerror", + "interrupt", + "invalidaccess", + "invalidexit", + "invalidfileaccess", + "invalidfont", + "invalidrestore", + "ioerror", + "limitcheck", + "nocurrentpoint", + "rangecheck", + "stackoverflow", + "stackunderflow", + "syntaxerror", + "timeout", + "typecheck", + "undefined", + "undefinedfilename", + "undefinedresult", + "unmatchedmark", + "unregistered", + "VMerror", + "configurationerror", + "undefinedresource", + ] { + let stop = op_id(b"stop").map(Obj::Op).unwrap_or(Obj::Null); + ip.errordict.put_name(e.as_bytes(), Obj::proc(vec![stop])); + } + ip.dollar_error.put_name(b"newerror", Obj::Bool(false)); + ip.statusdict.put_name(b"product", Obj::str(b"craft-ps")); + ip.statusdict.put_name(b"processcolors", Obj::proc(vec![Obj::Int(4)])); + ip.userdict.put_name(b"#copies", Obj::Int(1)); + let cats = PDict::new(4); + ip.resources.put_name(b"Category", Obj::Dict(cats)); + composite::install(ip); +} diff --git a/crates/ps/src/read/ops/shading.rs b/crates/ps/src/read/ops/shading.rs new file mode 100644 index 0000000000..599a069211 --- /dev/null +++ b/crates/ps/src/read/ops/shading.rs @@ -0,0 +1,389 @@ +//! Smooth shading: `shfill` and shading patterns. Functions (types 0, 2 and 3, or procedures) +//! are evaluated here in the shading's colour space and sampled into device colours, which the +//! PDF carries as a sampled function. + +use crate::read::gstate::{DevColor, M, Seg}; +use crate::read::interp::{Interp, PsErr, R, err, matrix_of}; +use crate::read::obj::{Obj, PDict}; +use crate::read::ops::gfx; +use crate::read::pdf::{OutSpace, ShadeKind, Shading}; + +/// Samples along an axial or radial shading, and per side of a function-based one. +const SAMPLES: usize = 256; +const GRID: usize = 48; +/// The most inputs a sampled function may take. +const MAX_INPUTS: usize = 4; +/// The most samples a sampled function may hold. +const MAX_SAMPLES: usize = 1 << 22; + +pub enum Func { + Exp { + c0: Vec, + c1: Vec, + n: f64, + domain: [f64; 2], + }, + Stitch { + fns: Vec, + bounds: Vec, + encode: Vec, + domain: [f64; 2], + }, + Sampled { + size: Vec, + domain: Vec, + encode: Vec, + decode: Vec, + samples: Vec, + outs: usize, + }, + Proc(Obj), + /// One function per output. + Each(Vec), +} + +fn nums(o: Option) -> Option> { + match o? { + Obj::Array(a, _) => Some(a.items().iter().filter_map(Obj::num).collect()), + _ => None, + } +} + +fn pair(v: &[f64], dflt: [f64; 2]) -> [f64; 2] { + [v.first().copied().unwrap_or(dflt[0]), v.get(1).copied().unwrap_or(dflt[1])] +} + +fn parse_fn(ip: &mut Interp, o: &Obj, depth: usize) -> R { + if depth > 8 { + return err("limitcheck"); + } + let d = match o { + Obj::Dict(d) => d.clone(), + Obj::Array(a, false) => return a.items().iter().map(|f| parse_fn(ip, f, depth + 1)).collect::>>().map(Func::Each), + p @ Obj::Array(_, true) => return Ok(Func::Proc(p.clone())), + _ => return err("typecheck"), + }; + let domain = nums(d.get_name(b"Domain")).unwrap_or_else(|| vec![0.0, 1.0]); + match d.get_name(b"FunctionType").and_then(|o| o.num()).unwrap_or(-1.0) as i64 { + 2 => Ok(Func::Exp { + c0: nums(d.get_name(b"C0")).unwrap_or_else(|| vec![0.0]), + c1: nums(d.get_name(b"C1")).unwrap_or_else(|| vec![1.0]), + n: d.get_name(b"N").and_then(|o| o.num()).unwrap_or(1.0), + domain: pair(&domain, [0.0, 1.0]), + }), + 3 => { + let fns = match d.get_name(b"Functions") { + Some(Obj::Array(a, _)) => a.items().iter().map(|f| parse_fn(ip, f, depth + 1)).collect::>>()?, + _ => return err("typecheck"), + }; + Ok(Func::Stitch { + fns, + bounds: nums(d.get_name(b"Bounds")).unwrap_or_default(), + encode: nums(d.get_name(b"Encode")).unwrap_or_default(), + domain: pair(&domain, [0.0, 1.0]), + }) + } + 0 => { + let size: Vec = nums(d.get_name(b"Size")).unwrap_or_default().iter().map(|v| v.max(1.0) as usize).collect(); + let range = nums(d.get_name(b"Range")).unwrap_or_default(); + let outs = range.len() / 2; + let bps = d.get_name(b"BitsPerSample").and_then(|o| o.num()).unwrap_or(8.0) as usize; + let count = size.iter().try_fold(outs.max(1), |a, s| a.checked_mul(*s)).filter(|n| *n <= MAX_SAMPLES).ok_or(PsErr::Ps("limitcheck"))?; + // At most four inputs: interpolation visits 2^inputs corners per value. + if size.is_empty() || size.len() > MAX_INPUTS || outs == 0 || !matches!(bps, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) { + return err("rangecheck"); + } + ip.charge(count as u64)?; + ip.alloc(count.saturating_mul(8))?; + let bytes_needed = (count * bps).div_ceil(8); + let data = match d.get_name(b"DataSource") { + Some(Obj::Str(s, _)) => s.bytes(), + Some(Obj::File(id)) => { + let mut v = Vec::with_capacity(bytes_needed); + while v.len() < bytes_needed { + match ip.srcs.read(id) { + Some(c) => v.push(c), + None => break, + } + } + v + } + _ => return err("typecheck"), + }; + let max = ((1u64 << bps) - 1) as f64; + let mut samples = Vec::with_capacity(count); + let mut bit = 0usize; + for _ in 0..count { + let mut v = 0u64; + for _ in 0..bps { + let byte = data.get(bit / 8).copied().unwrap_or(0); + v = v << 1 | u64::from(byte >> (7 - bit % 8) & 1); + bit += 1; + } + samples.push(v as f64 / max); + } + let encode = nums(d.get_name(b"Encode")).unwrap_or_else(|| size.iter().flat_map(|s| [0.0, (*s as f64 - 1.0).max(0.0)]).collect()); + let decode = nums(d.get_name(b"Decode")).unwrap_or(range); + Ok(Func::Sampled { size, domain, encode, decode, samples, outs }) + } + _ => err("rangecheck"), + } +} + +pub fn eval(ip: &mut Interp, f: &Func, x: &[f64], depth: usize) -> R> { + if depth > 16 { + return err("limitcheck"); + } + Ok(match f { + Func::Exp { c0, c1, n, domain } => { + let t = x.first().copied().unwrap_or(0.0).clamp(domain[0].min(domain[1]), domain[0].max(domain[1])); + let k = t.powf(*n); + let len = c0.len().max(c1.len()); + (0..len) + .map(|i| c0.get(i).copied().unwrap_or(0.0) + k * (c1.get(i).copied().unwrap_or(1.0) - c0.get(i).copied().unwrap_or(0.0))) + .collect() + } + Func::Stitch { fns, bounds, encode, domain } => { + let t = x.first().copied().unwrap_or(0.0).clamp(domain[0], domain[1]); + let i = bounds.iter().take_while(|b| t >= **b).count().min(fns.len().saturating_sub(1)); + let lo = if i == 0 { domain[0] } else { bounds.get(i - 1).copied().unwrap_or(domain[0]) }; + let hi = bounds.get(i).copied().unwrap_or(domain[1]); + let (e0, e1) = (encode.get(2 * i).copied().unwrap_or(0.0), encode.get(2 * i + 1).copied().unwrap_or(1.0)); + let u = if hi > lo { e0 + (t - lo) / (hi - lo) * (e1 - e0) } else { e0 }; + match fns.get(i) { + Some(sub) => eval(ip, sub, &[u], depth + 1)?, + None => vec![0.0], + } + } + Func::Sampled { size, domain, encode, decode, samples, outs } => { + // Multilinear interpolation between the samples around the point. + let m = size.len(); + let mut base = 0usize; + let mut stride = *outs; + let mut corners: Vec<(usize, f64)> = vec![(0, 1.0)]; + for k in 0..m { + let (d0, d1) = (domain.get(2 * k).copied().unwrap_or(0.0), domain.get(2 * k + 1).copied().unwrap_or(1.0)); + let s = size.get(k).copied().unwrap_or(1); + let xk = x.get(k).copied().unwrap_or(0.0).clamp(d0.min(d1), d0.max(d1)); + let (e0, e1) = (encode.get(2 * k).copied().unwrap_or(0.0), encode.get(2 * k + 1).copied().unwrap_or((s - 1) as f64)); + let e = if d1 != d0 { e0 + (xk - d0) / (d1 - d0) * (e1 - e0) } else { e0 }; + let e = e.clamp(0.0, (s - 1) as f64); + let i0 = e.floor() as usize; + let i1 = (i0 + 1).min(s - 1); + let frac = e - i0 as f64; + base += i0 * stride; + let step = (i1 - i0) * stride; + corners = corners.into_iter().flat_map(|(off, w)| [(off, w * (1.0 - frac)), (off + step, w * frac)]).collect(); + stride *= s; + } + (0..*outs) + .map(|j| { + let v: f64 = corners.iter().map(|(off, w)| samples.get(base + off + j).copied().unwrap_or(0.0) * w).sum(); + let (d0, d1) = (decode.get(2 * j).copied().unwrap_or(0.0), decode.get(2 * j + 1).copied().unwrap_or(1.0)); + d0 + v * (d1 - d0) + }) + .collect() + } + Func::Proc(p) => { + for v in x { + ip.push(Obj::Real(*v))?; + } + let before = ip.o.len() - x.len(); + ip.call(p.clone())?; + let n = ip.o.len().saturating_sub(before); + let mut out = vec![0.0; n]; + for slot in out.iter_mut().rev() { + *slot = ip.pop_num()?; + } + out + } + Func::Each(fns) => { + let mut out = Vec::with_capacity(fns.len()); + for f in fns { + out.push(eval(ip, f, x, depth + 1)?.first().copied().unwrap_or(0.0)); + } + out + } + }) +} + +/// Device colours as bytes in one output space (CMYK when every colour is CMYK). +pub fn pack(colors: &[DevColor]) -> (OutSpace, Vec) { + let b = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + let all_cmyk = colors.iter().all(|c| matches!(c, DevColor::Cmyk(..))); + let all_gray = colors.iter().all(|c| matches!(c, DevColor::Gray(..))); + let mut out = Vec::new(); + for c in colors { + match (*c, all_cmyk, all_gray) { + (DevColor::Cmyk(c, m, y, k), true, _) => out.extend([b(c), b(m), b(y), b(k)]), + (DevColor::Gray(g), _, true) => out.push(b(g)), + (DevColor::Gray(g), _, _) => out.extend([b(g); 3]), + (DevColor::Rgb(r, g, bl), _, _) => out.extend([b(r), b(g), b(bl)]), + (DevColor::Cmyk(c, m, y, k), _, _) => out.extend([b(1.0 - (c + k).min(1.0)), b(1.0 - (m + k).min(1.0)), b(1.0 - (y + k).min(1.0))]), + } + } + let space = if all_cmyk { + OutSpace::Cmyk + } else if all_gray { + OutSpace::Gray + } else { + OutSpace::Rgb + }; + (space, out) +} + +/// A shading dictionary as the PDF will draw it, or None for kinds not drawn (with a warning). +pub fn build(ip: &mut Interp, d: &PDict) -> R> { + let kind = d.get_name(b"ShadingType").and_then(|o| o.num()).unwrap_or(0.0) as i64; + let space_obj = d.get_name(b"ColorSpace").ok_or(PsErr::Ps("undefined"))?; + let space = gfx::parse_space(ip, &space_obj, 0)?; + let func = match d.get_name(b"Function") { + Some(f) => Some(parse_fn(ip, &f, 0)?), + None => None, + }; + let bbox = nums(d.get_name(b"BBox")).filter(|b| b.len() == 4).map(|b| [b[0], b[1], b[2], b[3]]); + let extend = match d.get_name(b"Extend") { + Some(Obj::Array(a, _)) => [matches!(a.get(0), Some(Obj::Bool(true))), matches!(a.get(1), Some(Obj::Bool(true)))], + _ => [false, false], + }; + let color_at = |ip: &mut Interp, x: &[f64]| -> R { + let comps = match &func { + Some(f) => eval(ip, f, x, 0)?, + None => x.to_vec(), + }; + gfx::to_device(ip, &space, &comps, 0) + }; + let shade = match kind { + 2 | 3 => { + let coords = nums(d.get_name(b"Coords")).unwrap_or_default(); + let domain = pair(&nums(d.get_name(b"Domain")).unwrap_or_default(), [0.0, 1.0]); + let mut colors = Vec::with_capacity(SAMPLES); + for i in 0..SAMPLES { + let t = domain[0] + (domain[1] - domain[0]) * i as f64 / (SAMPLES - 1) as f64; + colors.push(color_at(ip, &[t])?); + } + let k = if kind == 2 { + let [a, b, c, e] = <[f64; 4]>::try_from(coords).map_err(|_| PsErr::Ps("rangecheck"))?; + ShadeKind::Axial([a, b, c, e]) + } else { + let [a, b, c, e, f, g] = <[f64; 6]>::try_from(coords).map_err(|_| PsErr::Ps("rangecheck"))?; + ShadeKind::Radial([a, b, c, e, f, g]) + }; + let (space, data) = pack(&colors); + Shading { kind: k, extend, space, data, samples: SAMPLES, bbox, points: Vec::new() } + } + 1 => { + let dom = nums(d.get_name(b"Domain")).unwrap_or_else(|| vec![0.0, 1.0, 0.0, 1.0]); + let [x0, x1, y0, y1] = <[f64; 4]>::try_from(dom).map_err(|_| PsErr::Ps("rangecheck"))?; + let matrix = match d.get_name(b"Matrix") { + Some(Obj::Array(a, _)) => matrix_of(&a)?, + _ => M::ID, + }; + let mut colors = Vec::with_capacity(GRID * GRID); + for j in 0..GRID { + for i in 0..GRID { + let x = x0 + (x1 - x0) * i as f64 / (GRID - 1) as f64; + let y = y0 + (y1 - y0) * j as f64 / (GRID - 1) as f64; + colors.push(color_at(ip, &[x, y])?); + } + } + let (space, data) = pack(&colors); + Shading { kind: ShadeKind::Function { domain: [x0, x1, y0, y1], matrix }, extend, space, data, samples: GRID, bbox, points: Vec::new() } + } + 4..=7 => match crate::read::ops::mesh::build(ip, d, kind, &space, func.as_ref())? { + Some(s) => s, + None => return Ok(None), + }, + _ => { + ip.warnings.push(format!("shading type {kind} not drawn")); + return Ok(None); + } + }; + Ok(Some(shade)) +} + +pub fn shfill(ip: &mut Interp) -> R { + let d = ip.pop_dict()?; + if ip.gs.nulldev { + return Ok(()); + } + let Some(s) = build(ip, &d)? else { return Ok(()) }; + let ctm = ip.gs.ctm; + ip.out.shade(&ip.gs.clip, None, false, &ctm, s).map_err(PsErr::Fatal) +} + +/// Fill `path` with the current shading pattern; false when the pattern isn't a shading. +pub fn fill_pattern(ip: &mut Interp, path: &[Seg], eo: bool) -> R { + let Some(p) = ip.gs.pattern.clone() else { return Ok(false) }; + if p.get_name(b"PatternType").and_then(|o| o.num()) == Some(1.0) { + return tiling(ip, &p, path, eo).map(|()| true); + } + if p.get_name(b"PatternType").and_then(|o| o.num()) != Some(2.0) { + return Ok(false); + } + let Some(Obj::Dict(sh)) = p.get_name(b"Shading") else { return Ok(false) }; + let m = match p.get_name(b"CraftPsMatrix") { + Some(Obj::Array(a, _)) => matrix_of(&a)?, + _ => M::ID, + }; + let Some(s) = build(ip, &sh)? else { return Ok(true) }; + ip.out.shade(&ip.gs.clip, Some(path), eo, &m, s).map_err(PsErr::Fatal)?; + Ok(true) +} + +/// Fill with a tiling pattern: its cell painted once (per colour, for an uncoloured one) into a +/// PDF tiling pattern, cached on the pattern. +fn tiling(ip: &mut Interp, p: &PDict, path: &[Seg], eo: bool) -> R { + let uncolored = p.get_name(b"PaintType").and_then(|o| o.num()) == Some(2.0); + let color = if uncolored { Some(ip.gs.color.clone()) } else { None }; + let key = (crate::read::obj::Obj::Dict(p.clone()).key(), color.clone().unwrap_or_default().iter().map(|v| v.to_bits()).collect::>()); + let n = match ip.cells.get(&key) { + Some(n) => *n, + None => { + let n = cell(ip, p, color)?; + if ip.cells.len() < 10_000 { + ip.cells.insert(key, n); + } + n + } + }; + ip.out.fill_tiling(&ip.gs.clip, path, eo, n).map_err(PsErr::Fatal) +} + +fn cell(ip: &mut Interp, p: &PDict, color: Option>) -> R { + let bbox = nums(p.get_name(b"BBox")).filter(|b| b.len() == 4).map(|b| [b[0], b[1], b[2], b[3]]).ok_or(PsErr::Ps("rangecheck"))?; + let num = |k: &[u8]| p.get_name(k).and_then(|o| o.num()); + let step = (num(b"XStep").unwrap_or(bbox[2] - bbox[0]), num(b"YStep").unwrap_or(bbox[3] - bbox[1])); + if step.0 == 0.0 || step.1 == 0.0 || !step.0.is_finite() || !step.1.is_finite() { + return err("rangecheck"); + } + let m = match p.get_name(b"CraftPsMatrix") { + Some(Obj::Array(a, _)) => matrix_of(&a)?, + _ => M::ID, + }; + let paint = p.get_name(b"PaintProc").ok_or(PsErr::Ps("undefined"))?; + let under = ip.gs.pattern_base.as_deref().cloned(); + let cap = ip.out.begin_cell(); + ip.gsave()?; + // The cell is drawn in pattern space, unclipped, from a fresh path. + ip.gs.ctm = M::ID; + ip.gs.clip = None; + ip.gs.path.clear(); + ip.gs.cur = None; + ip.gs.nulldev = false; + if let Some(c) = color { + // An uncoloured cell paints in the colour given with the pattern. + ip.gs.space = under.unwrap_or(match c.len() { + 3 => crate::read::gstate::Space::Rgb, + 4 => crate::read::gstate::Space::Cmyk, + _ => crate::read::gstate::Space::Gray, + }); + ip.gs.color = c; + ip.gs.pattern = None; + ip.gs.color_locked = true; + } + let r = ip.push(Obj::Dict(p.clone())).and_then(|()| ip.call(paint)); + ip.grestore(); + let n = ip.out.end_cell(cap, bbox, step, m).map_err(PsErr::Fatal)?; + r.map(|()| n) +} diff --git a/crates/ps/src/read/pdf.rs b/crates/ps/src/read/pdf.rs new file mode 100644 index 0000000000..6363475dd4 --- /dev/null +++ b/crates/ps/src/read/pdf.rs @@ -0,0 +1,619 @@ +//! What the program paints, written as a one-page PDF. +//! +//! Paths arrive in device space (the EPS's default user space). Clipping is applied lazily: +//! before each painting operation the clips in force are matched against those the content +//! stream has applied, popping (`Q`) and pushing (`q … W n`) the difference, so the PostScript +//! program's own gsave/grestore pattern doesn't matter. + +use std::rc::Rc; + +use pdf_writer::types::{LineCapStyle, LineJoinStyle}; +use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref}; + +use crate::read::gstate::{Clip, DevColor, M, Seg, clip_chain}; + +/// The most path segments and images written. +const MAX_SEGMENTS: usize = 20_000_000; +const MAX_IMAGES: usize = 100_000; + +#[derive(Clone, Debug, PartialEq)] +pub enum ImgSpace { + Gray, + Rgb, + Cmyk, + Indexed { base: Box, hival: u8, lookup: Vec }, +} + +#[derive(Debug)] +pub struct Image { + pub w: u32, + pub h: u32, + pub bpc: u8, + /// None for a stencil mask (`imagemask`). + pub space: Option, + pub decode: Option>, + pub data: Vec, + /// `data` is a JPEG (DCTDecode passed through). + pub dct: bool, + pub interpolate: bool, + /// An 8-bit soft mask (w, h, alpha). + pub smask: Option<(u32, u32, Vec)>, +} + +/// The device space a shading's colours were sampled in. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum OutSpace { + Gray, + Rgb, + Cmyk, +} + +impl OutSpace { + fn comps(self) -> usize { + match self { + OutSpace::Gray => 1, + OutSpace::Rgb => 3, + OutSpace::Cmyk => 4, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ShadeKind { + Axial([f64; 4]), + Radial([f64; 6]), + /// A function of (x, y) over its domain, mapped by `matrix`. + Function { + domain: [f64; 4], + matrix: M, + }, + /// Vertices in threes; `Coons` and `Tensor` patches of 12 and 16 points. + Triangles, + Coons, + Tensor, +} + +/// A smooth shading with its colours sampled: along the axis (`samples` of them), over a +/// `samples` × `samples` grid, or one per vertex or patch corner. +#[derive(Clone, Debug)] +pub struct Shading { + pub kind: ShadeKind, + pub extend: [bool; 2], + pub space: OutSpace, + pub data: Vec, + pub samples: usize, + pub bbox: Option<[f64; 4]>, + pub points: Vec<(f64, f64)>, +} + +/// A tiling pattern cell: its content (in pattern space), cell box, steps and pattern matrix. +pub struct Tiling { + content: Vec, + bbox: [f64; 4], + step: (f64, f64), + matrix: M, +} + +/// What a pattern cell's content replaces while it is captured. +pub struct Capture { + c: Content, + applied: Vec>, + state: PaintState, + saved: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct PaintState { + fill: Option, + stroke: Option, +} + +pub struct StrokeParams<'a> { + pub width: f64, + pub cap: i64, + pub join: i64, + pub miter: f64, + pub dash: &'a (Vec, f64), +} + +pub struct Out { + c: Content, + images: Vec, + shadings: Vec, + tilings: Vec, + origin: (f64, f64), + applied: Vec>, + state: PaintState, + saved: Vec, + segments: usize, + /// Anything painted at all. + pub painted: bool, +} + +fn f(v: f64) -> f32 { + if v.is_finite() { v.clamp(-3.0e7, 3.0e7) as f32 } else { 0.0 } +} + +impl Out { + pub fn new(origin: (f64, f64)) -> Self { + let mut c = Content::new(); + c.transform([1.0, 0.0, 0.0, 1.0, f(-origin.0), f(-origin.1)]); + Out { + c, + images: Vec::new(), + shadings: Vec::new(), + tilings: Vec::new(), + origin, + applied: Vec::new(), + state: PaintState::default(), + saved: Vec::new(), + segments: 0, + painted: false, + } + } + + fn sync_clip(&mut self, clip: &Option>) -> Result<(), String> { + let target = clip_chain(clip); + let common = self.applied.iter().zip(&target).take_while(|(a, b)| Rc::ptr_eq(a, b)).count(); + while self.applied.len() > common { + self.applied.pop(); + self.c.restore_state(); + self.state = self.saved.pop().unwrap_or_default(); + } + for node in target.into_iter().skip(common) { + self.c.save_state(); + self.saved.push(self.state.clone()); + self.path(&node.path)?; + if node.eo { + self.c.clip_even_odd(); + } else { + self.c.clip_nonzero(); + } + self.c.end_path(); + self.applied.push(node); + } + Ok(()) + } + + fn path(&mut self, path: &[Seg]) -> Result<(), String> { + self.segments = self.segments.saturating_add(path.len()); + if self.segments > MAX_SEGMENTS { + return Err("too many path segments".into()); + } + for s in path { + match *s { + Seg::Move(x, y) => { + self.c.move_to(f(x), f(y)); + } + Seg::Line(x, y) => { + self.c.line_to(f(x), f(y)); + } + Seg::Curve(a, b, c, d, e, g) => { + self.c.cubic_to(f(a), f(b), f(c), f(d), f(e), f(g)); + } + Seg::Close => { + self.c.close_path(); + } + } + } + Ok(()) + } + + fn set_fill(&mut self, color: DevColor) { + if self.state.fill == Some(color) { + return; + } + match color { + DevColor::Gray(g) => self.c.set_fill_gray(f(g)), + DevColor::Rgb(r, g, b) => self.c.set_fill_rgb(f(r), f(g), f(b)), + DevColor::Cmyk(c, m, y, k) => self.c.set_fill_cmyk(f(c), f(m), f(y), f(k)), + }; + self.state.fill = Some(color); + } + + fn set_stroke(&mut self, color: DevColor) { + if self.state.stroke == Some(color) { + return; + } + match color { + DevColor::Gray(g) => self.c.set_stroke_gray(f(g)), + DevColor::Rgb(r, g, b) => self.c.set_stroke_rgb(f(r), f(g), f(b)), + DevColor::Cmyk(c, m, y, k) => self.c.set_stroke_cmyk(f(c), f(m), f(y), f(k)), + }; + self.state.stroke = Some(color); + } + + pub fn fill(&mut self, clip: &Option>, path: &[Seg], eo: bool, color: DevColor) -> Result<(), String> { + if !path.iter().any(|s| !matches!(s, Seg::Move(..))) { + return Ok(()); + } + self.sync_clip(clip)?; + self.set_fill(color); + self.path(path)?; + if eo { + self.c.fill_even_odd(); + } else { + self.c.fill_nonzero(); + } + self.painted = true; + Ok(()) + } + + /// Stroke a device-space path drawn under `ctm` (the pen is shaped in user space). + pub fn stroke(&mut self, clip: &Option>, path: &[Seg], ctm: &M, p: &StrokeParams, color: DevColor) -> Result<(), String> { + if !path.iter().any(|s| !matches!(s, Seg::Move(..))) { + return Ok(()); + } + let Some(inv) = ctm.inv() else { return Ok(()) }; + self.sync_clip(clip)?; + self.set_stroke(color); + self.c.save_state(); + self.c.transform(ctm.0.map(f)); + let user: Vec = path.iter().map(|s| s.map(&inv)).collect(); + self.c.set_line_width(f(p.width.max(0.0))); + self.c.set_line_cap(match p.cap { + 1 => LineCapStyle::RoundCap, + 2 => LineCapStyle::ProjectingSquareCap, + _ => LineCapStyle::ButtCap, + }); + self.c.set_line_join(match p.join { + 1 => LineJoinStyle::RoundJoin, + 2 => LineJoinStyle::BevelJoin, + _ => LineJoinStyle::MiterJoin, + }); + self.c.set_miter_limit(f(p.miter.max(1.0))); + if crate::read::gstate::dashes_ok(&user, &p.dash.0) { + self.c.set_dash_pattern(p.dash.0.iter().map(|d| f(*d)), f(p.dash.1)); + } + self.path(&user)?; + self.c.stroke(); + self.c.restore_state(); + self.painted = true; + Ok(()) + } + + /// Paint an image: `cm` maps the unit square onto the page; stencil masks use `fill`. + pub fn image(&mut self, clip: &Option>, cm: &M, img: Image, fill: Option) -> Result<(), String> { + if self.images.len() >= MAX_IMAGES { + return Err("too many images".into()); + } + if !cm.finite() || img.w == 0 || img.h == 0 { + return Ok(()); + } + self.sync_clip(clip)?; + if let Some(color) = fill { + self.set_fill(color); + } + let n = self.images.len(); + self.images.push(img); + self.c.save_state(); + self.c.transform(cm.0.map(f)); + let name = format!("Im{n}"); + self.c.x_object(Name(name.as_bytes())); + self.c.restore_state(); + self.painted = true; + Ok(()) + } + + /// Start a pattern cell: painting goes to its own content until [`Out::end_cell`]. + pub fn begin_cell(&mut self) -> Capture { + Capture { + c: std::mem::replace(&mut self.c, Content::new()), + applied: std::mem::take(&mut self.applied), + state: std::mem::take(&mut self.state), + saved: std::mem::take(&mut self.saved), + } + } + + /// Finish a pattern cell: its pattern number. + pub fn end_cell(&mut self, cap: Capture, bbox: [f64; 4], step: (f64, f64), matrix: M) -> Result { + let mut cell = std::mem::replace(&mut self.c, cap.c); + for _ in 0..self.applied.len() { + cell.restore_state(); + } + self.applied = cap.applied; + self.state = cap.state; + self.saved = cap.saved; + if self.tilings.len() >= MAX_IMAGES { + return Err("too many patterns".into()); + } + // Pattern space maps to the page's default space, which the content moved to the origin. + let matrix = matrix.then(&M([1.0, 0.0, 0.0, 1.0, -self.origin.0, -self.origin.1])); + self.tilings.push(Tiling { content: cell.finish().into_vec(), bbox, step, matrix }); + Ok(self.tilings.len() - 1) + } + + /// Fill a device-space path with tiling pattern `n`. + pub fn fill_tiling(&mut self, clip: &Option>, path: &[Seg], eo: bool, n: usize) -> Result<(), String> { + if !path.iter().any(|s| !matches!(s, Seg::Move(..))) { + return Ok(()); + } + self.sync_clip(clip)?; + self.c.set_fill_color_space(pdf_writer::types::ColorSpaceOperand::Pattern); + let name = format!("P{n}"); + self.c.set_fill_pattern(None, Name(name.as_bytes())); + self.state.fill = None; + self.path(path)?; + if eo { + self.c.fill_even_odd(); + } else { + self.c.fill_nonzero(); + } + self.painted = true; + Ok(()) + } + + /// Paint a shading in shading space `m` (inside `path` too, for a shading pattern fill). + pub fn shade(&mut self, clip: &Option>, path: Option<&[Seg]>, eo: bool, m: &M, s: Shading) -> Result<(), String> { + if self.shadings.len() >= MAX_IMAGES { + return Err("too many shadings".into()); + } + if !m.finite() || m.inv().is_none() { + return Ok(()); + } + self.sync_clip(clip)?; + self.c.save_state(); + if let Some(p) = path { + self.path(p)?; + if eo { + self.c.clip_even_odd(); + } else { + self.c.clip_nonzero(); + } + self.c.end_path(); + } + self.c.transform(m.0.map(f)); + if let Some([x0, y0, x1, y1]) = s.bbox { + self.c.rect(f(x0), f(y0), f(x1 - x0), f(y1 - y0)); + self.c.clip_nonzero(); + self.c.end_path(); + } + let name = format!("Sh{}", self.shadings.len()); + self.shadings.push(s); + self.c.shading(Name(name.as_bytes())); + self.c.restore_state(); + self.painted = true; + Ok(()) + } + + /// The PDF: one page of `size`, its content, images and shadings. + /// Path segments written so far (clips included). + pub fn segments(&self) -> usize { + self.segments + } + + pub fn finish(self, size: (f64, f64)) -> Vec { + let mut pdf = Pdf::new(); + // Nothing newer is used (no transparency groups): PDF/X-1a files can take it. + pdf.set_version(1, 4); + let (catalog, pages, page, content) = (Ref::new(1), Ref::new(2), Ref::new(3), Ref::new(4)); + let mut next = 5; + let mut alloc = || { + let r = Ref::new(next); + next += 1; + r + }; + let image_refs: Vec<(Ref, Option)> = self.images.iter().map(|i| (alloc(), i.smask.as_ref().map(|_| alloc()))).collect(); + let shading_refs: Vec<(Ref, Ref)> = self.shadings.iter().map(|_| (alloc(), alloc())).collect(); + let tiling_refs: Vec = self.tilings.iter().map(|_| alloc()).collect(); + // Every resource is listed for the page and for each pattern cell (names are global). + let resources = |res: &mut pdf_writer::writers::Resources<'_>| { + let mut xo = res.x_objects(); + for (i, (r, _)) in image_refs.iter().enumerate() { + let name = format!("Im{i}"); + xo.pair(Name(name.as_bytes()), *r); + } + xo.finish(); + let mut sh = res.shadings(); + for (i, (r, _)) in shading_refs.iter().enumerate() { + let name = format!("Sh{i}"); + sh.pair(Name(name.as_bytes()), *r); + } + sh.finish(); + let mut pa = res.patterns(); + for (i, r) in tiling_refs.iter().enumerate() { + let name = format!("P{i}"); + pa.pair(Name(name.as_bytes()), *r); + } + pa.finish(); + }; + pdf.catalog(catalog).pages(pages); + pdf.pages(pages).kids([page]).count(1); + { + let mut p = pdf.page(page); + p.parent(pages); + p.media_box(Rect::new(0.0, 0.0, f(size.0.max(1.0)), f(size.1.max(1.0)))); + p.contents(content); + let mut res = p.resources(); + resources(&mut res); + res.finish(); + p.finish(); + } + let mut c = self.c; + for _ in 0..self.applied.len() { + c.restore_state(); + } + let raw = c.finish().into_vec(); + let z = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6); + pdf.stream(content, &z).filter(Filter::FlateDecode); + for (img, (r, smask)) in self.images.iter().zip(&image_refs) { + let packed; + let (data, filter) = if img.dct { + (&img.data[..], Filter::DctDecode) + } else { + packed = miniz_oxide::deflate::compress_to_vec_zlib(&img.data, 6); + (&packed[..], Filter::FlateDecode) + }; + let mut x = pdf.image_xobject(*r, data); + x.filter(filter); + x.width(i32::try_from(img.w).unwrap_or(i32::MAX)); + x.height(i32::try_from(img.h).unwrap_or(i32::MAX)); + match &img.space { + None => { + x.image_mask(true); + } + Some(space) => { + x.bits_per_component(i32::from(img.bpc)); + match space { + ImgSpace::Gray => x.color_space().device_gray(), + ImgSpace::Rgb => x.color_space().device_rgb(), + ImgSpace::Cmyk => x.color_space().device_cmyk(), + ImgSpace::Indexed { base, hival, lookup } => { + let base = match **base { + ImgSpace::Rgb => Name(b"DeviceRGB"), + ImgSpace::Cmyk => Name(b"DeviceCMYK"), + _ => Name(b"DeviceGray"), + }; + x.color_space().indexed(base, i32::from(*hival), lookup); + } + } + } + } + if img.space.is_none() { + x.bits_per_component(1); + } + if let Some(d) = &img.decode { + x.decode(d.iter().copied()); + } + if img.interpolate { + x.interpolate(true); + } + if let Some(m) = smask { + x.s_mask(*m); + } + x.finish(); + if let (Some(m), Some((w, h, alpha))) = (smask, &img.smask) { + let z = miniz_oxide::deflate::compress_to_vec_zlib(alpha, 6); + let mut s = pdf.image_xobject(*m, &z); + s.filter(Filter::FlateDecode); + s.width(i32::try_from(*w).unwrap_or(i32::MAX)); + s.height(i32::try_from(*h).unwrap_or(i32::MAX)); + s.color_space().device_gray(); + s.bits_per_component(8); + s.finish(); + } + } + for (s, (r, fr)) in self.shadings.iter().zip(&shading_refs) { + write_shading(&mut pdf, s, *r, *fr); + } + for (t, r) in self.tilings.iter().zip(&tiling_refs) { + let z = miniz_oxide::deflate::compress_to_vec_zlib(&t.content, 6); + let mut p = pdf.tiling_pattern(*r, &z); + p.filter(Filter::FlateDecode); + p.paint_type(pdf_writer::types::PaintType::Colored); + p.tiling_type(pdf_writer::types::TilingType::ConstantSpacing); + let [x0, y0, x1, y1] = t.bbox; + p.bbox(Rect::new(f(x0), f(y0), f(x1), f(y1))); + p.x_step(f(t.step.0)); + p.y_step(f(t.step.1)); + p.matrix(t.matrix.0.map(f)); + let mut res = p.resources(); + resources(&mut res); + res.finish(); + p.finish(); + } + pdf.finish() + } +} + +fn set_space(cs: pdf_writer::writers::ColorSpace<'_>, s: OutSpace) { + match s { + OutSpace::Gray => cs.device_gray(), + OutSpace::Rgb => cs.device_rgb(), + OutSpace::Cmyk => cs.device_cmyk(), + } +} + +fn write_shading(pdf: &mut Pdf, s: &Shading, r: Ref, fr: Ref) { + use pdf_writer::types::FunctionShadingType; + use pdf_writer::writers::StreamShadingType; + let n = s.space.comps(); + let range: Vec = (0..n).flat_map(|_| [0.0, 1.0]).collect(); + let samples = i32::try_from(s.samples.max(2)).unwrap_or(2); + match &s.kind { + ShadeKind::Axial(_) | ShadeKind::Radial(_) | ShadeKind::Function { .. } => { + { + let mut func = pdf.sampled_function(fr, &s.data); + if let ShadeKind::Function { domain, .. } = &s.kind { + func.domain(domain.map(f)); + func.size([samples, samples]); + } else { + func.domain([0.0, 1.0]); + func.size([samples]); + } + func.range(range.iter().copied()); + func.bits_per_sample(8); + } + let mut sh = pdf.function_shading(r); + match &s.kind { + ShadeKind::Axial(c) => { + sh.shading_type(FunctionShadingType::Axial); + sh.coords(c.map(f)); + sh.extend(s.extend); + } + ShadeKind::Radial(c) => { + sh.shading_type(FunctionShadingType::Radial); + sh.coords(c.map(f)); + sh.extend(s.extend); + } + ShadeKind::Function { domain, matrix } => { + sh.shading_type(FunctionShadingType::Function); + sh.domain(domain.map(f)); + sh.matrix(matrix.0.map(f)); + } + _ => {} + } + set_space(sh.color_space(), s.space); + sh.function(fr); + } + ShadeKind::Triangles | ShadeKind::Coons | ShadeKind::Tensor => { + let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN); + for (x, y) in &s.points { + x0 = x0.min(*x); + y0 = y0.min(*y); + x1 = x1.max(*x); + y1 = y1.max(*y); + } + if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) { + return; + } + let (x1, y1) = (x1.max(x0 + 1e-3), y1.max(y0 + 1e-3)); + let q = |v: f64, lo: f64, hi: f64| (((v - lo) / (hi - lo)).clamp(0.0, 1.0) * f64::from(u32::MAX)).round() as u32; + let mut data = Vec::new(); + let (per, cols) = match s.kind { + ShadeKind::Triangles => (1, 1), + ShadeKind::Coons => (12, 4), + _ => (16, 4), + }; + let colors: Vec<&[u8]> = s.data.chunks(n).collect(); + for (i, chunk) in s.points.chunks(per).enumerate() { + if chunk.len() != per { + break; + } + data.push(0u8); + for (x, y) in chunk { + data.extend(q(*x, x0, x1).to_be_bytes()); + data.extend(q(*y, y0, y1).to_be_bytes()); + } + for k in 0..cols { + data.extend_from_slice(colors.get(i * cols + k).copied().unwrap_or(&[])); + } + } + let z = miniz_oxide::deflate::compress_to_vec_zlib(&data, 6); + let mut sh = pdf.stream_shading(r, &z); + sh.filter(Filter::FlateDecode); + sh.shading_type(match s.kind { + ShadeKind::Triangles => StreamShadingType::FreeformGouraud, + ShadeKind::Coons => StreamShadingType::CoonsPatch, + _ => StreamShadingType::TensorProductPatch, + }); + set_space(sh.color_space(), s.space); + sh.bits_per_coordinate(32); + sh.bits_per_component(8); + sh.bits_per_flag(8); + let mut decode = vec![f(x0), f(x1), f(y0), f(y1)]; + decode.extend(range); + sh.decode(decode); + } + } +} diff --git a/crates/ps/src/read/scan.rs b/crates/ps/src/read/scan.rs new file mode 100644 index 0000000000..9f6aa14c3b --- /dev/null +++ b/crates/ps/src/read/scan.rs @@ -0,0 +1,375 @@ +//! The scanner: turns a file's bytes into objects, building procedures `{ … }` whole. + +use crate::read::obj::{Obj, PArr, PStr, name}; +use crate::read::source::Sources; + +/// How deeply procedures may nest. +const MAX_NESTING: usize = 1000; +/// The longest string or name token. +const MAX_TOKEN: usize = 64 << 20; + +pub enum Token { + Obj(Obj), + /// `//name`: replaced by its value when scanned. + Immediate(Vec), +} + +pub fn is_ws(c: u8) -> bool { + matches!(c, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ') +} + +pub fn is_delim(c: u8) -> bool { + matches!(c, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%') +} + +/// One token from `src`, or None at its end. Procedures come back whole, as executable arrays; +/// `//name` inside them is resolved through `resolve`. +pub fn next(srcs: &mut Sources, src: usize, resolve: &mut dyn FnMut(&[u8]) -> Option) -> Result, String> { + let mut stack: Vec> = Vec::new(); + loop { + let Some(tok) = token(srcs, src)? else { + return if stack.is_empty() { Ok(None) } else { Err("unterminated procedure".into()) }; + }; + let obj = match tok { + Token::Obj(Obj::Name(n, true)) if &*n == b"{" => { + if stack.len() >= MAX_NESTING { + return Err("procedures nest too deeply".into()); + } + stack.push(Vec::new()); + continue; + } + Token::Obj(Obj::Name(n, true)) if &*n == b"}" => { + let Some(body) = stack.pop() else { return Err("unmatched }".into()) }; + Obj::Array(PArr::new(body), true) + } + Token::Obj(o) => o, + Token::Immediate(n) => resolve(&n).ok_or_else(|| format!("undefined //{}", String::from_utf8_lossy(&n)))?, + }; + match stack.last_mut() { + Some(body) => body.push(obj), + None => return Ok(Some(obj)), + } + } +} + +fn token(srcs: &mut Sources, src: usize) -> Result, String> { + // Whitespace and comments. + let c = loop { + let Some(c) = srcs.read(src) else { return Ok(None) }; + if is_ws(c) { + continue; + } + if c == b'%' { + while let Some(c) = srcs.read(src) { + if c == b'\n' || c == b'\r' { + break; + } + } + continue; + } + break c; + }; + match c { + b'(' => string(srcs, src).map(|s| Some(Token::Obj(Obj::Str(PStr::new(s), false)))), + b'<' => match srcs.read(src) { + Some(b'<') => Ok(Some(Token::Obj(Obj::Name(name(b"<<"), true)))), + Some(b'~') => ascii85(srcs, src).map(|s| Some(Token::Obj(Obj::Str(PStr::new(s), false)))), + Some(c) => { + srcs.unread(src); + let _ = c; + hex(srcs, src).map(|s| Some(Token::Obj(Obj::Str(PStr::new(s), false)))) + } + None => Err("unterminated hex string".into()), + }, + b'>' => match srcs.read(src) { + Some(b'>') => Ok(Some(Token::Obj(Obj::Name(name(b">>"), true)))), + _ => Err("stray >".into()), + }, + b'[' | b']' | b'{' | b'}' => Ok(Some(Token::Obj(Obj::Name(name(&[c]), true)))), + b')' => Err("stray )".into()), + b'/' => { + let immediate = srcs.peek(src) == Some(b'/'); + if immediate { + srcs.read(src); + } + let n = regular(srcs, src, None)?; + Ok(Some(if immediate { Token::Immediate(n) } else { Token::Obj(Obj::Name(name(&n), false)) })) + } + _ => { + let t = regular(srcs, src, Some(c))?; + Ok(Some(Token::Obj(number(&t).unwrap_or_else(|| Obj::Name(name(&t), true))))) + } + } +} + +/// A run of regular characters. The whitespace character that ends it is consumed (a CR LF +/// pair as one), a delimiter is left for the next token. +fn regular(srcs: &mut Sources, src: usize, first: Option) -> Result, String> { + let mut t: Vec = first.into_iter().collect(); + while let Some(c) = srcs.read(src) { + if is_ws(c) { + if c == b'\r' && srcs.peek(src) == Some(b'\n') { + srcs.read(src); + } + break; + } + if is_delim(c) { + srcs.unread(src); + break; + } + t.push(c); + if t.len() > MAX_TOKEN { + return Err("token too long".into()); + } + } + Ok(t) +} + +fn string(srcs: &mut Sources, src: usize) -> Result, String> { + let mut out = Vec::new(); + let mut depth = 0usize; + loop { + let c = srcs.read(src).ok_or("unterminated string")?; + match c { + b'(' => { + depth += 1; + out.push(c); + } + b')' => { + if depth == 0 { + return Ok(out); + } + depth -= 1; + out.push(c); + } + b'\\' => { + let e = srcs.read(src).ok_or("unterminated string")?; + match e { + b'n' => out.push(b'\n'), + b'r' => out.push(b'\r'), + b't' => out.push(b'\t'), + b'b' => out.push(8), + b'f' => out.push(12), + b'\r' => { + if srcs.peek(src) == Some(b'\n') { + srcs.read(src); + } + } + b'\n' => {} + b'0'..=b'7' => { + let mut v = u32::from(e - b'0'); + for _ in 0..2 { + match srcs.peek(src) { + Some(d @ b'0'..=b'7') => { + srcs.read(src); + v = v * 8 + u32::from(d - b'0'); + } + _ => break, + } + } + out.push((v & 0xFF) as u8); + } + other => out.push(other), + } + } + // An end of line inside a string is a newline, whatever its form. + b'\r' => { + if srcs.peek(src) == Some(b'\n') { + srcs.read(src); + } + out.push(b'\n'); + } + _ => out.push(c), + } + if out.len() > MAX_TOKEN { + return Err("string too long".into()); + } + } +} + +fn hex(srcs: &mut Sources, src: usize) -> Result, String> { + let mut out = Vec::new(); + let mut hi: Option = None; + loop { + let c = srcs.read(src).ok_or("unterminated hex string")?; + if c == b'>' { + break; + } + if is_ws(c) { + continue; + } + let v = match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => return Err("bad hex string".into()), + }; + match hi.take() { + Some(h) => out.push(h << 4 | v), + None => hi = Some(v), + } + if out.len() > MAX_TOKEN { + return Err("string too long".into()); + } + } + if let Some(h) = hi { + out.push(h << 4); + } + Ok(out) +} + +fn ascii85(srcs: &mut Sources, src: usize) -> Result, String> { + let mut out = Vec::new(); + let mut group = [0u64; 5]; + let mut n = 0; + loop { + let c = srcs.read(src).ok_or("unterminated ASCII85 string")?; + if is_ws(c) { + continue; + } + if c == b'~' { + if srcs.read(src) != Some(b'>') { + return Err("bad ASCII85 end".into()); + } + break; + } + if c == b'z' && n == 0 { + out.extend([0; 4]); + continue; + } + if !(b'!'..=b'u').contains(&c) { + return Err("bad ASCII85 string".into()); + } + if let Some(g) = group.get_mut(n) { + *g = u64::from(c - b'!'); + } + n += 1; + if n == 5 { + let v = group.iter().fold(0u64, |a, d| a * 85 + d); + out.extend(u32::try_from(v).map_err(|_| "ASCII85 overflow")?.to_be_bytes()); + n = 0; + } + if out.len() > MAX_TOKEN { + return Err("string too long".into()); + } + } + if n > 1 { + for g in group.iter_mut().skip(n) { + *g = 84; + } + let v = group.iter().fold(0u64, |a, d| a * 85 + d).min(u64::from(u32::MAX)); + let v = u32::try_from(v).unwrap_or(u32::MAX); + out.extend(v.to_be_bytes().iter().take(n - 1)); + } + Ok(out) +} + +/// An integer, radix number (`16#FF`) or real; None for anything else. +pub fn number(t: &[u8]) -> Option { + let s = std::str::from_utf8(t).ok()?; + let first = *t.first()?; + if !(first.is_ascii_digit() || matches!(first, b'+' | b'-' | b'.')) { + return None; + } + if let Some((base, digits)) = s.split_once('#') { + let base: u32 = base.parse().ok()?; + if !(2..=36).contains(&base) || digits.is_empty() { + return None; + } + let v = u64::from_str_radix(digits, base).ok()?; + // Radix numbers are unsigned 32-bit patterns. + return Some(Obj::Int(i64::from(v as u32 as i32))); + } + let digits_only = s.strip_prefix(['+', '-']).unwrap_or(s); + if !digits_only.is_empty() && digits_only.bytes().all(|b| b.is_ascii_digit()) { + return Some(match s.parse::() { + Ok(i) if i32::try_from(i).is_ok() => Obj::Int(i), + _ => Obj::Real(s.parse::().ok()?), + }); + } + // Reals: digits with a point and/or an exponent; nothing else (`1e` or `.` alone are names). + let mut seen_digit = false; + let mut seen_e = false; + for (i, b) in digits_only.bytes().enumerate() { + match b { + b'0'..=b'9' => seen_digit = true, + b'.' if !seen_e => {} + b'e' | b'E' if seen_digit && !seen_e => seen_e = true, + b'+' | b'-' if seen_e && matches!(digits_only.as_bytes().get(i.wrapping_sub(1)), Some(b'e' | b'E')) => {} + _ => return None, + } + } + if !seen_digit || digits_only.ends_with(['e', 'E', '+', '-']) { + return None; + } + let v: f64 = s.parse().ok()?; + v.is_finite().then_some(Obj::Real(v)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::read::source::Source; + + fn all(text: &[u8]) -> Vec { + let mut s = Sources::default(); + let id = s.add(Source::bytes(text.to_vec())); + let mut out = Vec::new(); + while let Some(o) = next(&mut s, id, &mut |_| Some(Obj::Int(42))).unwrap() { + out.push(o); + } + out + } + + #[test] + fn scans_the_token_kinds() { + let t = all(b"12 -3.5 1e3 16#FF /lit name (a\\(b\\)\\101\\\n c) <41 42> <~87cURDZ~> {1 {2} add} //x [ ] << >> % note\n.5 1.e2 -."); + let kinds: Vec = t + .iter() + .map(|o| match o { + Obj::Int(i) => format!("i{i}"), + Obj::Real(r) => format!("r{r}"), + Obj::Name(n, x) => format!("{}{}", if *x { "x" } else { "/" }, String::from_utf8_lossy(n)), + Obj::Str(s, _) => format!("s{}", String::from_utf8_lossy(&s.bytes())), + Obj::Array(a, true) => format!("p{}", a.len), + _ => "?".into(), + }) + .collect(); + assert_eq!( + kinds, + [ + "i12", "r-3.5", "r1000", "i255", "/lit", "xname", "sa(b)A c", "sAB", "sHello", "p3", "i42", "x[", "x]", "x<<", "x>>", "r0.5", "r100", + "x-." + ] + ); + } + + #[test] + fn a_token_ending_in_whitespace_consumes_it() { + let mut s = Sources::default(); + let id = s.add(Source::bytes(b"image\r\nDATA".to_vec())); + let _ = next(&mut s, id, &mut |_| None).unwrap(); + assert_eq!(s.read(id), Some(b'D')); + } + + #[test] + fn malformed_input_is_an_error() { + for bad in [&b"(open"[..], b"<4g>", b"{ 1 2", b"}", b"<~ab~x"] { + let mut s = Sources::default(); + let id = s.add(Source::bytes(bad.to_vec())); + let mut r = Ok(None); + for _ in 0..5 { + r = next(&mut s, id, &mut |_| None); + if r.is_err() { + break; + } + } + assert!(r.is_err(), "{}", String::from_utf8_lossy(bad)); + } + // Deep nesting is refused, not a stack overflow. + let deep = vec![b'{'; 5000]; + let mut s = Sources::default(); + let id = s.add(Source::bytes(deep)); + assert!(next(&mut s, id, &mut |_| None).is_err()); + } +} diff --git a/crates/ps/src/read/source.rs b/crates/ps/src/read/source.rs new file mode 100644 index 0000000000..f30d3bea41 --- /dev/null +++ b/crates/ps/src/read/source.rs @@ -0,0 +1,926 @@ +//! Files: the program itself, strings run as code, decoding filters and `eexec` sections. +//! +//! Filters decode as they are read, so a filter made over `currentfile` before the operator +//! that consumes it (`currentfile /ASCII85Decode filter … image`) reads the data that follows +//! that operator. A filter takes from the file beneath only what its encoding needs: bytes a +//! decompressor fetched past its end are pushed back, so the program carries on after the data. + +use std::collections::VecDeque; +use std::rc::Rc; + +/// The most bytes one filter may decode to, and one reusable stream may hold. +const MAX_DECODED: usize = 256 << 20; +/// How deep files may sit on one another. +const MAX_CHAIN: usize = 16; + +pub enum Dec { + Hex { + hi: Option, + }, + A85 { + group: [u8; 5], + n: usize, + first: bool, + }, + RunLength, + Lzw { + dec: Box, + pending: Vec, + }, + Flate { + state: Box, + pending: Vec, + }, + SubFile { + eod: Vec, + count: usize, + left: Option, + window: VecDeque, + }, + /// JPEG data: only images take it (whole, as it is). + Dct, +} + +pub enum Kind { + Bytes(Rc>), + /// A window onto a string's storage, read in place (`token`, executing a string). + Shared { + buf: Rc>>, + off: usize, + len: usize, + }, + /// Decrypting `eexec` over another file. `hex` is decided by the first four bytes. + Eexec { + up: usize, + r: u16, + hex: Option, + skipped: u8, + }, + Filter { + up: usize, + dec: Dec, + out: VecDeque, + done: bool, + produced: usize, + }, +} + +pub struct Source { + pub kind: Kind, + pub pos: usize, + pub closed: bool, + /// Raw JPEG data for a DCTDecode filter, once taken. + pub dct: Option>>, + /// The last byte given, and bytes pushed back (read again first; the last pushed first). + pub last: Option, + pub back: Vec, + /// A PNG or TIFF predictor under Flate or LZW. + pub predictor: Option, +} + +impl Source { + pub fn bytes(data: Vec) -> Self { + Self::shared(Rc::new(data)) + } + + pub fn shared(data: Rc>) -> Self { + Self::of(Kind::Bytes(data)) + } + + /// A string read in place, without copying it. + pub fn string(s: &crate::read::obj::PStr) -> Self { + Self::of(Kind::Shared { buf: s.buf.clone(), off: s.off, len: s.len }) + } + + pub fn eexec(up: usize) -> Self { + Self::of(Kind::Eexec { up, r: 55665, hex: None, skipped: 0 }) + } + + fn of(kind: Kind) -> Self { + Source { kind, pos: 0, closed: false, dct: None, last: None, back: Vec::new(), predictor: None } + } +} + +fn is_ws(c: u8) -> bool { + matches!(c, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ') +} + +fn hex_val(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, + } +} + +#[derive(Debug, Default)] +pub struct FilterParams { + pub early_change: bool, + pub predictor: i64, + pub colors: i64, + pub bpc: i64, + pub columns: i64, + pub eod_count: usize, + pub eod_string: Vec, +} + +#[derive(Debug, Clone)] +pub struct Predictor { + kind: i64, + bpp: usize, + row: usize, + prev: Vec, + cur: Vec, +} + +/// The sources, read through one another (`eexec` over the program, filters over both). +pub struct Sources { + pub list: Vec, + /// Bytes read so far, at every level (a filter's input and its output both count): the + /// interpreter charges them as work. + pub read: u64, + /// The most bytes that may be read; past it every file is at its end and `exhausted` is set. + pub max_read: u64, + pub exhausted: bool, +} + +impl Default for Sources { + fn default() -> Self { + Sources { list: Vec::new(), read: 0, max_read: u64::MAX, exhausted: false } + } +} + +impl Sources { + pub fn add(&mut self, s: Source) -> usize { + self.list.push(s); + self.list.len() - 1 + } + + pub fn read(&mut self, id: usize) -> Option { + self.read_depth(id, 0) + } + + fn read_depth(&mut self, id: usize, depth: usize) -> Option { + if self.read >= self.max_read { + self.exhausted = true; + return None; + } + let c = self.read_inner(id, depth); + if c.is_some() { + self.read += 1; + } + c + } + + fn read_inner(&mut self, id: usize, depth: usize) -> Option { + if depth > MAX_CHAIN { + return None; + } + let src = self.list.get_mut(id)?; + if src.closed { + return None; + } + if let Some(c) = src.back.pop() { + src.last = Some(c); + return Some(c); + } + let c = match &mut src.kind { + Kind::Bytes(data) => { + let c = data.get(src.pos).copied(); + if c.is_some() { + src.pos += 1; + } + return c; + } + Kind::Shared { buf, off, len } => { + let c = if src.pos < *len { off.checked_add(src.pos).and_then(|i| buf.borrow().get(i).copied()) } else { None }; + if c.is_some() { + src.pos += 1; + } + return c; + } + Kind::Eexec { .. } => self.read_eexec(id, depth), + Kind::Filter { .. } => self.read_filter(id, depth), + }; + if let Some(s) = self.list.get_mut(id) { + s.last = c; + if c.is_some() { + s.pos += 1; + } + } + c + } + + fn read_eexec(&mut self, id: usize, depth: usize) -> Option { + let Some(Source { kind: Kind::Eexec { up, hex, .. }, .. }) = self.list.get(id) else { return None }; + let (up, hex) = (*up, *hex); + let hex = match hex { + Some(h) => h, + None => { + let h = self.leading_hex(up, depth)?; + if let Some(Source { kind: Kind::Eexec { hex, .. }, .. }) = self.list.get_mut(id) { + *hex = Some(h); + } + h + } + }; + loop { + let cipher = if hex { + let mut digits = [0u8; 2]; + let mut n = 0; + while n < 2 { + let c = self.read_depth(up, depth + 1)?; + if is_ws(c) { + continue; + } + digits[n] = hex_val(c)?; + n += 1; + } + digits[0] << 4 | digits[1] + } else { + self.read_depth(up, depth + 1)? + }; + let Some(Source { kind: Kind::Eexec { r, skipped, .. }, .. }) = self.list.get_mut(id) else { return None }; + let plain = cipher ^ (*r >> 8) as u8; + *r = (u16::from(cipher).wrapping_add(*r)).wrapping_mul(52845).wrapping_add(22719); + // The first four plain bytes are random. + if *skipped < 4 { + *skipped += 1; + continue; + } + return Some(plain); + } + } + + /// Whether the next four bytes after whitespace are hex digits (the whitespace is consumed + /// when they are; the four bytes are pushed back either way). + fn leading_hex(&mut self, up: usize, depth: usize) -> Option { + let mut skipped = Vec::new(); + let first = loop { + let c = self.read_depth(up, depth + 1)?; + if !is_ws(c) { + break c; + } + skipped.push(c); + }; + let mut four = vec![first]; + while four.len() < 4 { + match self.read_depth(up, depth + 1) { + Some(c) => four.push(c), + None => break, + } + } + let hex = four.len() == 4 && four.iter().all(|c| hex_val(*c).is_some()); + self.unread_many(up, &four); + if !hex { + self.unread_many(up, &skipped); + } + Some(hex) + } + + /// Push bytes back so they are read again, in order. + pub fn unread_many(&mut self, id: usize, bytes: &[u8]) { + let Some(src) = self.list.get_mut(id) else { return }; + match src.kind { + Kind::Bytes(_) | Kind::Shared { .. } if src.back.is_empty() => src.pos = src.pos.saturating_sub(bytes.len()), + _ => src.back.extend(bytes.iter().rev()), + } + } + + /// Step back over the byte just read (the scanner's look-ahead). + pub fn unread(&mut self, id: usize) { + let Some(src) = self.list.get_mut(id) else { return }; + match src.kind { + Kind::Bytes(_) | Kind::Shared { .. } if src.back.is_empty() => src.pos = src.pos.saturating_sub(1), + _ => { + if let Some(c) = src.last { + src.back.push(c); + } + } + } + } + + pub fn peek(&mut self, id: usize) -> Option { + let c = self.read(id)?; + self.unread(id); + Some(c) + } + + /// A decoding filter over `up`. Unsupported filters give `Err`. + pub fn filter(&mut self, up: usize, name: &[u8], p: &FilterParams) -> Result { + let dec = match name { + b"ASCIIHexDecode" | b"AHx" => Dec::Hex { hi: None }, + b"ASCII85Decode" | b"A85" => Dec::A85 { group: [0; 5], n: 0, first: true }, + b"RunLengthDecode" | b"RL" => Dec::RunLength, + b"LZWDecode" | b"LZW" => { + let dec = if p.early_change { + weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8) + } else { + weezl::decode::Decoder::new(weezl::BitOrder::Msb, 8) + }; + Dec::Lzw { dec: Box::new(dec), pending: Vec::new() } + } + b"FlateDecode" | b"Fl" => { + Dec::Flate { state: miniz_oxide::inflate::stream::InflateState::new_boxed(miniz_oxide::DataFormat::Zlib), pending: Vec::new() } + } + b"DCTDecode" | b"DCT" => Dec::Dct, + b"SubFileDecode" => Dec::SubFile { + eod: p.eod_string.clone(), + count: p.eod_count, + // A byte count; with no count and no end string, everything passes through. + left: (p.eod_string.is_empty() && p.eod_count > 0).then_some(p.eod_count), + window: VecDeque::new(), + }, + b"NullEncode" => Dec::SubFile { eod: Vec::new(), count: 0, left: None, window: VecDeque::new() }, + b"ReusableStreamDecode" => { + // Reads its data when made, and can be read again (`resetfile`, positions). + let mut data = Vec::new(); + while let Some(c) = self.read(up) { + data.push(c); + if data.len() > MAX_DECODED { + return Err("reusable stream too large".into()); + } + } + return Ok(self.add(Source::bytes(data))); + } + _ => return Err(format!("unsupported filter {}", String::from_utf8_lossy(name))), + }; + let mut s = Source::of(Kind::Filter { up, dec, out: VecDeque::new(), done: false, produced: 0 }); + if p.predictor > 1 && matches!(name, b"FlateDecode" | b"Fl" | b"LZWDecode" | b"LZW") { + let colors = usize::try_from(p.colors.clamp(1, 32)).unwrap_or(1); + let bpc = usize::try_from(p.bpc.clamp(1, 16)).unwrap_or(8); + let columns = usize::try_from(p.columns.clamp(1, 1 << 20)).unwrap_or(1); + let row = (colors * bpc * columns).div_ceil(8); + s.predictor = Some(Predictor { kind: p.predictor, bpp: (colors * bpc).div_ceil(8).max(1), row, prev: vec![0; row], cur: Vec::new() }); + } + Ok(self.add(s)) + } + + fn read_filter(&mut self, id: usize, depth: usize) -> Option { + loop { + let src = self.list.get_mut(id)?; + let Kind::Filter { out, done, .. } = &mut src.kind else { return None }; + if let Some(c) = out.pop_front() { + return Some(c); + } + if *done { + return None; + } + if !self.fill(id, depth) + && let Some(Source { kind: Kind::Filter { done, .. }, .. }) = self.list.get_mut(id) + { + // What the last step produced is still read first. + *done = true; + } + } + } + + /// Decode some more of filter `id`; false once its data has ended. + fn fill(&mut self, id: usize, depth: usize) -> bool { + let Some(Source { kind: Kind::Filter { up, dec, .. }, .. }) = self.list.get(id) else { return false }; + let up = *up; + let mut produced: Vec = Vec::new(); + let mut more = true; + match dec { + Dec::Dct => more = false, + Dec::Hex { hi } => { + let mut hi = *hi; + while produced.is_empty() { + let Some(c) = self.read_depth(up, depth + 1) else { + more = false; + break; + }; + if c == b'>' { + more = false; + break; + } + let Some(v) = hex_val(c) else { continue }; + match hi.take() { + Some(h) => produced.push(h << 4 | v), + None => hi = Some(v), + } + } + if !more && let Some(h) = hi.take() { + produced.push(h << 4); + } + if let Some(Source { kind: Kind::Filter { dec: Dec::Hex { hi: slot }, .. }, .. }) = self.list.get_mut(id) { + *slot = hi; + } + } + Dec::A85 { group, n, first } => { + let (mut group, mut n, mut first) = (*group, *n, *first); + while produced.is_empty() { + let Some(c) = self.read_depth(up, depth + 1) else { + more = false; + break; + }; + if is_ws(c) { + continue; + } + if c == b'<' && first { + // A leading `<~`. + if self.read_depth(up, depth + 1) == Some(b'~') { + first = false; + continue; + } + more = false; + break; + } + first = false; + if c == b'~' { + let _ = self.read_depth(up, depth + 1); + more = false; + break; + } + if c == b'z' && n == 0 { + produced.extend([0; 4]); + continue; + } + if !(b'!'..=b'u').contains(&c) { + more = false; + break; + } + if let Some(slot) = group.get_mut(n) { + *slot = c - b'!'; + } + n += 1; + if n == 5 { + let v = group.iter().fold(0u64, |a, &d| a * 85 + u64::from(d)).min(u64::from(u32::MAX)); + produced.extend(u32::try_from(v).unwrap_or(u32::MAX).to_be_bytes()); + n = 0; + } + } + if !more && n > 1 { + for slot in group.iter_mut().skip(n) { + *slot = 84; + } + let v = group.iter().fold(0u64, |a, &d| a * 85 + u64::from(d)).min(u64::from(u32::MAX)); + produced.extend(u32::try_from(v).unwrap_or(u32::MAX).to_be_bytes().iter().take(n - 1)); + n = 0; + } + if let Some(Source { kind: Kind::Filter { dec: Dec::A85 { group: g, n: nn, first: f }, .. }, .. }) = self.list.get_mut(id) { + *g = group; + *nn = n; + *f = first; + } + } + Dec::RunLength => match self.read_depth(up, depth + 1) { + None | Some(128) => more = false, + Some(len @ 0..=127) => { + for _ in 0..=len { + match self.read_depth(up, depth + 1) { + Some(c) => produced.push(c), + None => { + more = false; + break; + } + } + } + } + Some(len) => match self.read_depth(up, depth + 1) { + Some(c) => produced.extend(std::iter::repeat_n(c, 257 - usize::from(len))), + None => more = false, + }, + }, + Dec::SubFile { eod, count, left, window } => { + let (eod, mut count, mut left, mut window) = (eod.clone(), *count, *left, window.clone()); + if let Some(n) = left.as_mut() { + if *n == 0 { + more = false; + } else if let Some(c) = self.read_depth(up, depth + 1) { + *n -= 1; + produced.push(c); + } else { + more = false; + } + } else if eod.is_empty() { + match self.read_depth(up, depth + 1) { + Some(c) => produced.push(c), + None => more = false, + } + } else { + while produced.is_empty() && more { + let Some(c) = self.read_depth(up, depth + 1) else { + produced.extend(window.drain(..)); + more = false; + break; + }; + window.push_back(c); + if window.len() >= eod.len() && window.iter().skip(window.len() - eod.len()).eq(eod.iter()) { + if count == 0 { + let keep = window.len() - eod.len(); + produced.extend(window.drain(..keep)); + window.clear(); + more = false; + break; + } + count -= 1; + } + while window.len() > eod.len() { + if let Some(b) = window.pop_front() { + produced.push(b); + } + } + } + } + if let Some(Source { kind: Kind::Filter { dec: Dec::SubFile { count: c, left: l, window: w, .. }, .. }, .. }) = self.list.get_mut(id) + { + *c = count; + *l = left; + *w = window; + } + } + Dec::Flate { .. } | Dec::Lzw { .. } => { + // Feed what is pending, or a chunk from beneath; keep what wasn't used. + let mut input = match self.list.get_mut(id).map(|s| &mut s.kind) { + Some(Kind::Filter { dec: Dec::Flate { pending, .. } | Dec::Lzw { pending, .. }, .. }) => std::mem::take(pending), + _ => Vec::new(), + }; + let mut eof = false; + if input.is_empty() { + while input.len() < 4096 { + match self.read_depth(up, depth + 1) { + Some(c) => input.push(c), + None => { + eof = true; + break; + } + } + } + } + let mut buf = vec![0u8; 64 * 1024]; + let (used, written, finished) = match self.list.get_mut(id).map(|s| &mut s.kind) { + Some(Kind::Filter { dec: Dec::Flate { state, .. }, .. }) => { + let r = miniz_oxide::inflate::stream::inflate(state, &input, &mut buf, miniz_oxide::MZFlush::None); + let finished = matches!(r.status, Ok(miniz_oxide::MZStatus::StreamEnd) | Err(_)); + (r.bytes_consumed, r.bytes_written, finished) + } + Some(Kind::Filter { dec: Dec::Lzw { dec, .. }, .. }) => { + // One byte at a time: the decoder must not read past its end code. + let (mut used, mut written, mut finished) = (0, 0, false); + while used < input.len() && written + 4096 < buf.len() { + let r = dec.decode_bytes(input.get(used..used + 1).unwrap_or(&[]), buf.get_mut(written..).unwrap_or(&mut [])); + used += r.consumed_in; + written += r.consumed_out; + if matches!(r.status, Ok(weezl::LzwStatus::Done) | Err(_)) { + finished = true; + break; + } + if r.consumed_in == 0 && r.consumed_out == 0 { + break; + } + } + (used, written, finished) + } + _ => (0, 0, true), + }; + produced.extend_from_slice(buf.get(..written).unwrap_or(&[])); + let rest = input.get(used..).unwrap_or(&[]).to_vec(); + if finished { + // The stream ended inside this chunk: what follows belongs to the file. + self.unread_many(up, &rest); + more = false; + } else if used == 0 && written == 0 && (eof || input.is_empty()) { + more = false; + } else if let Some(Source { kind: Kind::Filter { dec: Dec::Flate { pending, .. } | Dec::Lzw { pending, .. }, .. }, .. }) = + self.list.get_mut(id) + { + *pending = rest; + } + } + } + // Predictors work on whole rows. + let predicted = match self.list.get_mut(id).and_then(|s| s.predictor.as_mut()) { + Some(pr) => unpredict(pr, &produced, !more), + None => produced, + }; + let Some(Source { kind: Kind::Filter { out, produced: total, .. }, .. }) = self.list.get_mut(id) else { return false }; + *total = total.saturating_add(predicted.len()); + if *total > MAX_DECODED { + return false; + } + out.extend(predicted); + more + } + + /// The JPEG a DCTDecode filter reads: through its end marker when it sits on the program + /// itself, all of the filter beneath otherwise. + pub fn dct(&mut self, id: usize) -> Option>> { + let src = self.list.get(id)?; + if let Some(d) = &src.dct { + return Some(d.clone()); + } + let Kind::Filter { up, dec: Dec::Dct, .. } = &src.kind else { return None }; + let up = *up; + let direct = match self.list.get(up) { + Some(Source { kind: Kind::Bytes(data), pos, back, .. }) if back.is_empty() => Some((data.clone(), *pos)), + _ => None, + }; + let raw = match direct { + Some((data, pos)) => { + let rest = data.get(pos..).unwrap_or(&[]); + let end = jpeg_end(rest).unwrap_or(rest.len()); + if let Some(s) = self.list.get_mut(up) { + s.pos = s.pos.saturating_add(end); + } + rest.get(..end).unwrap_or(&[]).to_vec() + } + None => { + let mut raw = Vec::new(); + while let Some(c) = self.read(up) { + raw.push(c); + if raw.len() > MAX_DECODED { + break; + } + } + raw + } + }; + let raw = Rc::new(raw); + if let Some(s) = self.list.get_mut(id) { + s.dct = Some(raw.clone()); + } + Some(raw) + } + + pub fn is_filter(&self, id: usize) -> bool { + matches!(self.list.get(id).map(|s| &s.kind), Some(Kind::Filter { .. })) + } + + /// Read a filter to its end marker, so the file beneath carries on after the data. + pub fn drain(&mut self, id: usize) { + let mut n = 0usize; + while n < MAX_DECODED && self.read(id).is_some() { + n += 1; + } + } + + pub fn close(&mut self, id: usize) { + if let Some(s) = self.list.get_mut(id) { + s.closed = true; + } + } + + pub fn reset(&mut self, id: usize) { + if let Some(s) = self.list.get_mut(id) + && matches!(s.kind, Kind::Bytes(_) | Kind::Shared { .. }) + { + s.pos = 0; + s.back.clear(); + s.closed = false; + } + } + + pub fn position(&self, id: usize) -> Option { + let s = self.list.get(id)?; + Some(s.pos.saturating_sub(s.back.len())) + } + + pub fn set_position(&mut self, id: usize, pos: usize) -> bool { + match self.list.get_mut(id) { + Some(s) if matches!(&s.kind, Kind::Bytes(d) if pos <= d.len()) || matches!(&s.kind, Kind::Shared { len, .. } if pos <= *len) => { + s.pos = pos; + s.back.clear(); + s.closed = false; + true + } + _ => false, + } + } + + pub fn available(&self, id: usize) -> i64 { + match self.list.get(id) { + Some(s) if s.closed => -1, + Some(Source { kind: Kind::Bytes(d), pos, back, .. }) => i64::try_from(d.len().saturating_sub(*pos) + back.len()).unwrap_or(i64::MAX), + Some(Source { kind: Kind::Shared { len, .. }, pos, back, .. }) => { + i64::try_from(len.saturating_sub(*pos) + back.len()).unwrap_or(i64::MAX) + } + Some(_) => 1, + None => -1, + } + } +} + +/// Undo a predictor on newly decoded bytes (`last`: no more will come). +fn unpredict(p: &mut Predictor, data: &[u8], last: bool) -> Vec { + let mut out = Vec::new(); + if p.kind == 2 { + // TIFF: 8-bit horizontal differencing. + for &b in data { + p.cur.push(b); + if p.cur.len() == p.row { + for i in p.bpp..p.row { + let prev = p.cur.get(i - p.bpp).copied().unwrap_or(0); + if let Some(c) = p.cur.get_mut(i) { + *c = c.wrapping_add(prev); + } + } + out.append(&mut p.cur); + } + } + if last { + out.append(&mut p.cur); + } + return out; + } + // PNG: a filter byte before each row. + for &b in data { + p.cur.push(b); + if p.cur.len() == p.row + 1 { + let kind = p.cur.first().copied().unwrap_or(0); + let mut row: Vec = p.cur.get(1..).unwrap_or(&[]).to_vec(); + for i in 0..row.len() { + let a = if i >= p.bpp { row.get(i - p.bpp).copied().unwrap_or(0) } else { 0 }; + let up = p.prev.get(i).copied().unwrap_or(0); + let c = if i >= p.bpp { p.prev.get(i - p.bpp).copied().unwrap_or(0) } else { 0 }; + let add = match kind { + 1 => a, + 2 => up, + 3 => ((u16::from(a) + u16::from(up)) / 2) as u8, + 4 => { + let pp = i16::from(a) + i16::from(up) - i16::from(c); + let (pa, pb, pc) = ((pp - i16::from(a)).abs(), (pp - i16::from(up)).abs(), (pp - i16::from(c)).abs()); + if pa <= pb && pa <= pc { + a + } else if pb <= pc { + up + } else { + c + } + } + _ => 0, + }; + if let Some(v) = row.get_mut(i) { + *v = v.wrapping_add(add); + } + } + out.extend_from_slice(&row); + p.prev = row; + p.cur.clear(); + } + } + out +} + +/// The length of the JPEG at the start of `d` (through its EOI marker). +fn jpeg_end(d: &[u8]) -> Option { + if d.get(..2)? != [0xFF, 0xD8] { + return None; + } + let mut i = 2; + loop { + while d.get(i)? == &0xFF && d.get(i + 1)? == &0xFF { + i += 1; + } + if d.get(i)? != &0xFF { + return None; + } + let m = *d.get(i + 1)?; + i += 2; + match m { + 0xD9 => return Some(i), + 0x01 | 0xD0..=0xD7 => continue, + _ => { + let len = usize::from(u16::from_be_bytes([*d.get(i)?, *d.get(i + 1)?])); + i = i.checked_add(len)?; + if m == 0xDA { + // Entropy-coded data: up to the next marker that isn't a stuffed byte or restart. + loop { + if d.get(i)? == &0xFF { + let n = *d.get(i + 1)?; + if n != 0 && !(0xD0..=0xD7).contains(&n) { + break; + } + i += 2; + } else { + i += 1; + } + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn all(s: &mut Sources, id: usize) -> Vec { + let mut out = Vec::new(); + while let Some(c) = s.read(id) { + out.push(c); + } + out + } + + #[test] + fn filters_stop_at_their_end_markers() { + let mut s = Sources::default(); + let id = s.add(Source::bytes(b"48656c6c6f>rest".to_vec())); + let f = s.filter(id, b"ASCIIHexDecode", &FilterParams::default()).unwrap(); + assert_eq!(all(&mut s, f), b"Hello"); + assert_eq!(all(&mut s, id), b"rest"); + let id = s.add(Source::bytes(b"87cURD]i,\"Ebo80~>rest".to_vec())); + let f = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); + assert_eq!(all(&mut s, f), b"Hello World!"); + assert_eq!(all(&mut s, id), b"rest"); + let id = s.add(Source::bytes(vec![2, b'a', b'b', b'c', 254, b'x', 128, b'!'])); + let f = s.filter(id, b"RunLengthDecode", &FilterParams::default()).unwrap(); + assert_eq!(all(&mut s, f), b"abcxxx"); + assert_eq!(all(&mut s, id), b"!"); + let id = s.add(Source::bytes(b"data%%EndBinary tail".to_vec())); + let p = FilterParams { eod_string: b"%%EndBinary".to_vec(), ..Default::default() }; + let f = s.filter(id, b"SubFileDecode", &p).unwrap(); + assert_eq!(all(&mut s, f), b"data"); + assert_eq!(all(&mut s, id), b" tail"); + } + + fn ascii85(data: &[u8]) -> String { + let mut out = String::new(); + for chunk in data.chunks(4) { + let mut b = [0u8; 4]; + b[..chunk.len()].copy_from_slice(chunk); + let mut v = u32::from_be_bytes(b); + let mut digits = [0u8; 5]; + for d in digits.iter_mut().rev() { + *d = (v % 85) as u8 + b'!'; + v /= 85; + } + out.push_str(std::str::from_utf8(&digits[..chunk.len() + 1]).unwrap()); + } + out + } + + #[test] + fn flate_and_lzw_read_only_their_streams() { + let mut z = miniz_oxide::deflate::compress_to_vec_zlib(&b"abcabcabc".repeat(2000), 6); + z.extend_from_slice(b"tail"); + let mut s = Sources::default(); + let id = s.add(Source::bytes(z)); + let f = s.filter(id, b"FlateDecode", &FilterParams::default()).unwrap(); + assert_eq!(all(&mut s, f), b"abcabcabc".repeat(2000)); + assert_eq!(all(&mut s, id), b"tail"); + // Flate over ASCII85 over the program: bytes pushed back return to the filter beneath. + let z = miniz_oxide::deflate::compress_to_vec_zlib(b"xyz", 6); + let mut s = Sources::default(); + let id = s.add(Source::bytes(format!("{}~> after", ascii85(&z)).into_bytes())); + let a = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); + let f = s.filter(a, b"FlateDecode", &FilterParams::default()).unwrap(); + assert_eq!(all(&mut s, f), b"xyz"); + assert_eq!(all(&mut s, id), b" after"); + let mut enc = weezl::encode::Encoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8); + let mut l = enc.encode(b"lzw lzw lzw").unwrap(); + l.extend_from_slice(b"!"); + let mut s = Sources::default(); + let id = s.add(Source::bytes(l)); + let f = s.filter(id, b"LZWDecode", &FilterParams { early_change: true, ..Default::default() }).unwrap(); + assert_eq!(all(&mut s, f), b"lzw lzw lzw"); + assert_eq!(all(&mut s, id), b"!"); + } + + #[test] + fn eexec_decrypts_binary_and_hex() { + let encrypt = |plain: &[u8]| { + let mut r: u16 = 55665; + let mut out = Vec::new(); + for &p in [0u8, 0, 0, 0].iter().chain(plain) { + let c = p ^ (r >> 8) as u8; + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + out.push(c); + } + out + }; + let enc = encrypt(b"hello"); + for hex in [false, true] { + let data = if hex { enc.iter().map(|b| format!("{b:02x}")).collect::().into_bytes() } else { enc.clone() }; + let mut s = Sources::default(); + let up = s.add(Source::bytes(data)); + let e = s.add(Source::eexec(up)); + assert_eq!(s.peek(e), Some(b'h'), "look-ahead inside the section"); + let got: Vec = (0..5).filter_map(|_| s.read(e)).collect(); + assert_eq!(got, b"hello", "hex {hex}"); + } + } + + #[test] + fn broken_filter_data_ends_the_filter_not_the_program() { + for (name, data) in [ + (&b"ASCIIHexDecode"[..], &b"zz>"[..]), + (b"ASCII85Decode", b"\x7f~>"), + (b"FlateDecode", b"\x78\x9c\xff\xff"), + (b"RunLengthDecode", b"\x05a"), + (b"LZWDecode", b"\xff\xff\xff"), + ] { + let mut s = Sources::default(); + let id = s.add(Source::bytes(data.to_vec())); + let f = s.filter(id, name, &FilterParams::default()).unwrap(); + let _ = all(&mut s, f); + } + let mut s = Sources::default(); + let id = s.add(Source::bytes(b"\xff\xd8\xff".to_vec())); + let f = s.filter(id, b"DCTDecode", &FilterParams::default()).unwrap(); + assert!(s.dct(f).is_some()); + } +} diff --git a/crates/ps/src/read/tests.rs b/crates/ps/src/read/tests.rs new file mode 100644 index 0000000000..ab352806cb --- /dev/null +++ b/crates/ps/src/read/tests.rs @@ -0,0 +1,524 @@ +//! End-to-end: EPS programs in, PDF content out. + +use super::*; + +/// The page content stream of a converted EPS, inflated. +fn content(pdf: &[u8]) -> String { + // The content stream is object 4, written right after the page. + let at = pdf.windows(7).position(|w| w == b"4 0 obj").expect("content object"); + let rest = &pdf[at..]; + let s = rest.windows(6).position(|w| w == b"stream").unwrap() + 6; + let s = s + if rest[s] == b'\r' { 2 } else { 1 }; + let e = rest.windows(9).position(|w| w == b"endstream").unwrap(); + let raw = miniz_oxide::inflate::decompress_to_vec_zlib(&rest[s..e]) + .unwrap_or_else(|_| miniz_oxide::inflate::decompress_to_vec_zlib(&rest[s..e - 1]).unwrap()); + String::from_utf8_lossy(&raw).into_owned() +} + +fn eps(body: &str) -> Vec { + format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 20 110 70\n%%EndComments\n{body}\n%%EOF\n").into_bytes() +} + +fn run(body: &str) -> (String, Converted) { + let c = eps_to_pdf(&eps(body), Limits::default()).unwrap(); + (content(&c.pdf), c) +} + +#[test] +fn eps_sections_and_deferred_bounding_boxes_use_craft_eps() { + let ps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%HiResBoundingBox: (atend)\n%%BoundingBox: (atend)\n%%EndComments\n%%Trailer\n%%BoundingBox: 0 0 100 50\n%%HiResBoundingBox: 0.5 1.5 90.5 40.5\n"; + let wrapped = craft_eps::wrap(ps, Some(b"tiff"), None).unwrap(); + assert!(is_eps(&wrapped)); + assert_eq!(postscript_section(&wrapped), ps); + assert_eq!(bounding_box(ps), Some([0.5, 1.5, 90.5, 40.5])); +} + +#[test] +fn bounding_box_keeps_the_historical_256k_header_scan() { + let mut ps = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec(); + ps.resize(ps.len() + (70 << 10), b' '); + ps.extend_from_slice(b"\n%%BoundingBox: 0 0 20 10\n%%EndComments\n"); + assert_eq!(craft_eps::Comments::read(&ps).page_box(), None); + assert_eq!(bounding_box(&ps), Some([0.0, 0.0, 20.0, 10.0])); +} + +#[test] +fn damaged_dos_preview_does_not_hide_the_executable_postscript() { + let ps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10 10\n%%EndComments\n0 0 10 10 rectfill\n"; + let mut wrapped = craft_eps::wrap(ps, Some(b"tiff"), None).unwrap(); + wrapped[20..24].copy_from_slice(&u32::MAX.to_le_bytes()); + wrapped[24..28].copy_from_slice(&1u32.to_le_bytes()); + assert_eq!(postscript_section(&wrapped), ps); + assert!(craft_eps::sections(&wrapped).is_err()); + assert!(eps_to_pdf(&wrapped, Limits::default()).is_ok()); +} + +#[test] +fn paths_fill_and_stroke_in_their_colours() { + let (c, conv) = run("0 0 1 0 setcmykcolor 10 20 50 30 rectfill 1 0 0 setrgbcolor 2 setlinewidth newpath 10 20 moveto 100 60 lineto stroke"); + assert_eq!(conv.bbox, [10.0, 20.0, 110.0, 70.0]); + // The bounding box's corner moves to the page origin. + assert!(c.starts_with("1 0 0 1 -10 -20 cm"), "{c}"); + assert!(c.contains("0 0 1 0 k"), "CMYK stays CMYK: {c}"); + assert!(c.contains("10 20 m") && c.contains("f"), "{c}"); + assert!(c.contains("1 0 0 RG") && c.contains("2 w") && c.contains("S"), "{c}"); +} + +#[test] +fn procedures_loops_and_dictionaries_run() { + let (c, _) = run("/sq { newpath moveto 10 0 rlineto 0 10 rlineto -10 0 rlineto closepath fill } bind def + 0 1 2 { 15 mul 10 add 30 sq } for + /d 3 dict def d /n 7 put d /n get 7 eq { 0.5 setgray 60 20 5 5 rectfill } if + { 1 0 div } stopped { 0.25 setgray 70 20 5 5 rectfill } if + [ 1 2 3 ] { pop } forall 3 { } repeat 0 { 1 add dup 4 ge { exit } if } loop pop"); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 5, "{c}"); + assert!(c.contains("0.5 g") && c.contains("0.25 g"), "{c}"); +} + +#[test] +fn transforms_curves_and_clips() { + let (c, _) = run("gsave 60 45 translate 2 2 scale newpath 0 0 10 0 360 arc clip newpath -20 -20 40 40 rectfill grestore 10 20 5 5 rectfill"); + assert!(c.contains(" c"), "arcs become curves: {c}"); + assert!(c.contains("W") && c.contains("q") && c.contains("Q"), "clip in its own q: {c}"); + // The circle is centred at (60, 45) with radius 20 in device space. + assert!(c.contains("80 45 m"), "{c}"); +} + +#[test] +fn separation_colours_use_their_tint_transform() { + let (c, _) = run("[/Separation (Spot) /DeviceCMYK { dup 0 exch 0 } ] setcolorspace 0.5 setcolor 10 20 10 10 rectfill"); + assert!(c.contains("0.5 0 0.5 0 k") || c.contains(".5 0 .5 0 k"), "{c}"); +} + +#[test] +fn images_and_masks_become_xobjects() { + let (c, conv) = run("gsave 10 20 translate 20 10 scale 2 1 8 [2 0 0 -1 0 1] <00ff> image grestore + gsave 40 20 translate 10 10 scale 0 0 1 setrgbcolor 8 1 true [8 0 0 -1 0 1] imagemask grestore + gsave 60 20 translate 10 10 scale 1 1 8 [1 0 0 -1 0 1] currentfile /ASCIIHexDecode filter false 3 colorimage +ff8000> +grestore"); + assert_eq!(c.matches("Do").count(), 3, "{c}"); + let pdf = String::from_utf8_lossy(&conv.pdf); + assert!(pdf.contains("/ImageMask true") && pdf.contains("/DeviceRGB") && pdf.contains("/DeviceGray"), "{pdf}"); +} + +#[test] +fn type3_fonts_draw_their_glyphs() { + let (c, _) = run("/T3 8 dict dup begin /FontType 3 def /FontMatrix [0.001 0 0 0.001 0 0] def /FontBBox [0 0 1000 1000] def + /Encoding 256 array def 0 1 255 { Encoding exch /.notdef put } for Encoding 65 /A put + /BuildChar { pop pop 600 0 0 0 500 500 setcachedevice 0 0 500 500 rectfill } def end def + /T3 T3 definefont pop /T3 findfont 20 scalefont setfont 10 20 moveto (AA) show"); + // Two glyphs, the second 12 points (600 units at 20 pt) to the right. + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + assert!(c.contains("22 20 m"), "{c}"); +} + +#[test] +fn a_string_made_a_file_runs_as_a_program() { + // Adobe's idiom: `(code) 0 () /SubFileDecode filter cvx exec`. + let (c, _) = run("(0.5 setgray 10 20 5 5 rectfill) 0 () /SubFileDecode filter cvx exec"); + assert!(c.contains("0.5 g"), "{c}"); +} + +#[test] +fn eexec_sections_run() { + // `(1 0 0 setrgbcolor 10 20 5 5 rectfill) ` encrypted with the eexec key, as hex. + let plain = b"1 0 0 setrgbcolor 10 20 5 5 rectfill currentfile closefile\n"; + let mut r: u16 = 55665; + let mut hex = String::new(); + for &p in [1u8, 2, 3, 4].iter().chain(plain.iter()) { + let c = p ^ (r >> 8) as u8; + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + hex.push_str(&format!("{c:02x}")); + } + let (c, _) = run(&format!("currentfile eexec\n{hex}\n0 1 0 setrgbcolor 20 20 5 5 rectfill")); + assert!(c.contains("1 0 0 rg") && c.contains("0 1 0 rg"), "the program carries on after the section: {c}"); +} + +#[test] +fn hostile_programs_end_with_an_error_not_a_hang_or_panic() { + let limits = Limits { steps: 200_000, memory: 64 << 20 }; + for body in [ + "{ } loop", + "/f { f } def f", + "/f { 1 f } def f", + "{ 1 } loop", + "1000000000 array", + "0 1 1e9 { pop } for", + "[ 1 2", + "/x 10 dict def x begin x begin", + "100000 { gsave } repeat", + "{ { { { { { stop } exec } exec } exec } exec } exec } loop", + "(unterminated", + "1 0 idiv", + "mark { 0 0 moveto } 1000000 { dup exec } repeat", + "currentfile 100000000 string readstring", + "/A85 currentfile /ASCII85Decode filter def ~>", + "0 0 moveto 1 1 1e300 1e300 0 0 curveto stroke", + "[1e300 0 0 1e300 0 0] concat 0 0 1 1 rectfill", + ] { + let _ = eps_to_pdf(&eps(body), limits); + } + // Nothing painted and an error: no PDF, so the caller keeps the preview. + assert!(eps_to_pdf(&eps("1 0 div"), limits).is_err()); + assert!(eps_to_pdf(b"%!PS-Adobe-3.0 EPSF-3.0\n0 0 1 1 rectfill\n", limits).is_err(), "no bounding box"); + assert!(eps_to_pdf(b"not postscript", limits).is_err()); +} + +#[test] +fn mutually_bound_executable_names_stop_at_a_bounded_depth() { + let err = eps_to_pdf(&eps("/n /m cvx def /m /n cvx def n"), Limits::default()).unwrap_err(); + assert_eq!(err, "executable name resolution limit"); +} + +#[test] +fn a_file_in_a_name_is_data_until_executed() { + // cairo keeps its image filter in a name and calls `status` and `flushfile` on it. + let (c, _) = run("/f (0.5 setgray 10 20 5 5 rectfill) 0 () /SubFileDecode filter def f status pop f cvx exec"); + assert!(c.contains("0.5 g"), "{c}"); +} + +#[test] +fn smooth_shadings_become_pdf_shadings() { + let (c, conv) = run("<< /ShadingType 2 /ColorSpace /DeviceCMYK /Coords [10 20 110 20] /Extend [true true] + /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0 0] /C1 [0 0 1 0] /N 1 >> >> shfill + << /ShadingType 3 /ColorSpace [/Separation (Gold) /DeviceCMYK { dup 0.2 mul exch 0 exch 0 } ] + /Coords [60 45 0 60 45 20] /Function { } >> shfill"); + assert_eq!(c.matches(" sh").count(), 2, "{c}"); + let pdf = String::from_utf8_lossy(&conv.pdf); + assert!(pdf.contains("/ShadingType 2") && pdf.contains("/ShadingType 3"), "{pdf}"); + // CMYK and a spot made of CMYK stay CMYK. + assert_eq!(pdf.matches("/ColorSpace /DeviceCMYK").count(), 2, "{pdf}"); +} + +#[test] +fn shading_patterns_fill_their_path() { + let (c, _) = run("<< /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] + /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> >> >> matrix makepattern + setpattern 10 20 50 30 rectfill"); + assert!(c.contains("W") && c.contains(" sh"), "clipped to the rectangle, then shaded: {c}"); +} + +#[test] +fn mesh_shadings_keep_their_vertices() { + // Two free-form triangles from an array, the second sharing an edge (flag 1). + let (_, conv) = run("<< /ShadingType 4 /ColorSpace /DeviceRGB + /DataSource [0 10 20 1 0 0 0 110 20 0 1 0 0 60 70 0 0 1 1 110 70 1 1 0] >> shfill + << /ShadingType 6 /ColorSpace /DeviceGray /DataSource [0 10 20 10 30 10 40 10 50 20 50 30 50 40 50 50 50 50 40 50 30 50 20 40 20 30 20 20 20 0 0.3 0.6 1] >> shfill"); + let pdf = String::from_utf8_lossy(&conv.pdf); + assert!(pdf.contains("/ShadingType 4") && pdf.contains("/ShadingType 6"), "{pdf}"); + // Malformed meshes draw what they hold, or nothing. + for bad in [ + "<< /ShadingType 4 /ColorSpace /DeviceRGB /DataSource [0 1] >> shfill", + "<< /ShadingType 7 /ColorSpace /DeviceGray /DataSource (abc) /BitsPerCoordinate 32 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 1 0 1 0 1] >> shfill 0 0 1 1 rectfill", + ] { + let _ = eps_to_pdf(&eps(bad), Limits::default()); + } +} + +/// A two-glyph TrueType font: .notdef (empty) and a 500 × 700 box at x 50, 600 wide, in +/// 1000 units to the em. +fn tiny_ttf() -> Vec { + let be16 = |v: i32| (v as i16 as u16).to_be_bytes(); + // The box: one contour of four on-curve points, coordinates as 16-bit deltas. + let mut glyph = Vec::new(); + for v in [1, 50, 0, 550, 700, 3] { + glyph.extend(be16(v)); + } + glyph.extend([0, 0]); // no instructions + glyph.extend([1, 1, 1, 1]); // on-curve, long coordinates + for dx in [50, 500, 0, -500] { + glyph.extend(be16(dx)); + } + for dy in [0, 0, 700, 0] { + glyph.extend(be16(dy)); + } + let mut head = Vec::new(); + head.extend(0x0001_0000u32.to_be_bytes()); + head.extend(0x0001_0000u32.to_be_bytes()); + head.extend(0u32.to_be_bytes()); + head.extend(0x5F0F_3CF5u32.to_be_bytes()); + head.extend(be16(0)); + head.extend(be16(1000)); + head.extend([0; 16]); + for v in [0, 0, 600, 700, 0, 8, 2, 0, 0] { + head.extend(be16(v)); + } + let mut hhea = 0x0001_0000u32.to_be_bytes().to_vec(); + for v in [800, -200, 0, 600, 0, 0, 550, 1, 0, 0, 0, 0, 0, 0, 0, 2] { + hhea.extend(be16(v)); + } + let mut maxp = 0x0000_5000u32.to_be_bytes().to_vec(); + maxp.extend(be16(2)); + let mut hmtx = Vec::new(); + for v in [500, 0, 600, 50] { + hmtx.extend(be16(v)); + } + let mut loca = Vec::new(); + for v in [0, 0, glyph.len() as i32 / 2] { + loca.extend(be16(v)); + } + let tables: [(&[u8; 4], Vec); 6] = [(b"glyf", glyph), (b"head", head), (b"hhea", hhea), (b"hmtx", hmtx), (b"loca", loca), (b"maxp", maxp)]; + let mut out = 0x0001_0000u32.to_be_bytes().to_vec(); + out.extend(be16(tables.len() as i32)); + out.extend([0; 6]); + let mut at = 12 + 16 * tables.len(); + let mut body = Vec::new(); + for (tag, data) in &tables { + out.extend(*tag); + out.extend(0u32.to_be_bytes()); + out.extend((at as u32).to_be_bytes()); + out.extend((data.len() as u32).to_be_bytes()); + let mut d = data.clone(); + d.resize(data.len().div_ceil(4) * 4, 0); + at += d.len(); + body.extend(d); + } + out.extend(body); + out +} + +#[test] +fn type42_fonts_draw_their_truetype_glyphs() { + let ttf = tiny_ttf(); + // sfnts strings split at an even length below the string limit. + let sfnts: String = ttf.chunks(64).map(|c| format!("<{}>\n", c.iter().map(|b| format!("{b:02x}")).collect::())).collect(); + let (c, _) = run(&format!( + "/T42 11 dict dup begin /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /PaintType 0 def /FontBBox [0 0 1 1] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end definefont + 20 scalefont setfont 10 20 moveto (AA) show" + )); + // Two boxes, filled: 1 point (50 units at 20 pt) in, the second 12 points on (600 units). + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + assert!(c.contains("11 20 m") && c.contains("23 20 m"), "{c}"); +} + +/// A CFF INDEX of `items` (4-byte offsets). +fn cff_index(items: &[Vec]) -> Vec { + let mut out = (items.len() as u16).to_be_bytes().to_vec(); + if items.is_empty() { + return out; + } + out.push(4); + let mut off = 1u32; + out.extend(off.to_be_bytes()); + for it in items { + off += it.len() as u32; + out.extend(off.to_be_bytes()); + } + for it in items { + out.extend(it); + } + out +} + +/// A DICT integer operand in its five-byte form (so offsets don't change the length). +fn cff_int(v: i32) -> Vec { + [vec![29], v.to_be_bytes().to_vec()].concat() +} + +/// A one-glyph CFF font set: glyph 1 is a 500 × 700 box, 600 wide. CID-keyed fonts give it CID 5. +fn tiny_cff(cid: bool) -> Vec { + let n = |v: i32| vec![(v + 139) as u8]; + let box_glyph = + [n(-100), n(50), n(0), vec![21], vec![247, 136], n(0), vec![5], n(0), vec![248, 80], vec![5], vec![251, 136], n(0), vec![5], vec![14]] + .concat(); + // 600 = width as the first (odd) operand: nominalWidthX 700 → −100. + let charstrings = cff_index(&[vec![14], box_glyph]); + let charset = if cid { vec![0, 0, 5] } else { vec![0, 0, 34] }; + let private = [cff_int(700), vec![21]].concat(); + let strings = cff_index(&[b"Adobe".to_vec(), b"Identity".to_vec()]); + let head = vec![1, 0, 4, 4]; + let names = cff_index(&[b"T".to_vec()]); + // Lay out: header, names, top dict, strings, gsubrs, charset, charstrings, private, [fdarray, fdselect]. + let top_len = |extra: usize| 1 + 2 + 4 + 4 + (5 + 1) * 2 + (5 + 5 + 1) + extra; + let extra = if cid { 5 + 5 + 5 + 2 + 5 + 2 + 5 + 2 + 5 + 2 } else { 0 }; + let top_index_len = 2 + 1 + 8 + (top_len(extra) - 11); + let mut at = head.len() + names.len() + top_index_len + strings.len() + 2; + let charset_at = at; + at += charset.len(); + let cs_at = at; + at += charstrings.len(); + let priv_at = at; + at += private.len(); + let fd_private = private.clone(); + let fd_dict = [cff_int(fd_private.len() as i32), cff_int(priv_at as i32), vec![18]].concat(); + let fdarray = cff_index(&[fd_dict]); + let fdarray_at = at; + at += fdarray.len(); + let fdselect_at = at; + let fdselect = vec![0, 0, 0]; + let mut top = + [cff_int(charset_at as i32), vec![15], cff_int(cs_at as i32), vec![17], cff_int(private.len() as i32), cff_int(priv_at as i32), vec![18]] + .concat(); + if cid { + top = [ + cff_int(391), + cff_int(392), + cff_int(0), + vec![12, 30], + cff_int(6), + vec![12, 34], + cff_int(fdarray_at as i32), + vec![12, 36], + cff_int(fdselect_at as i32), + vec![12, 37], + top, + ] + .concat(); + } + let tops = cff_index(&[top]); + let mut out = [head, names, tops, strings, vec![0, 0]].concat(); + assert_eq!(out.len(), charset_at, "layout"); + out.extend(charset); + out.extend(charstrings); + out.extend(private); + if cid { + out.extend(fdarray); + out.extend(fdselect); + } + out +} + +fn startdata_eps(cff: &[u8], body: &str) -> Vec { + let mut v = + format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 20 110 70\n/FontSetInit /ProcSet findresource begin\n/T {} StartData ", cff.len()) + .into_bytes(); + v.extend_from_slice(cff); + v.extend_from_slice(format!("\n{body}\n%%EOF\n").as_bytes()); + v +} + +#[test] +fn cff_fonts_from_startdata_draw_their_glyphs() { + let c = content( + &eps_to_pdf(&startdata_eps(&tiny_cff(false), "/T findfont 10 scalefont setfont 10 20 moveto (AA) show"), Limits::default()).unwrap().pdf, + ); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + // The box starts 0.5 points (50 units) in; the second glyph 6 points (600 units) on. + assert!(c.contains("10.5 20 m") && c.contains("16.5 20 m"), "{c}"); +} + +#[test] +fn cid_fonts_draw_through_cmaps() { + let body = "/T-Identity-H findfont 10 scalefont setfont 10 20 moveto <00050005> show + /CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /One-H def + 1 begincodespacerange <00> endcodespacerange 1 begincidchar <41> 5 endcidchar + endcmap CMapName currentdict /CMap defineresource pop end end + /U /One-H [/T] composefont pop /U findfont 10 scalefont setfont 10 40 moveto (AA) show"; + let c = content(&eps_to_pdf(&startdata_eps(&tiny_cff(true), body), Limits::default()).unwrap().pdf); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 4, "{c}"); + assert!(c.contains("16.5 20 m") && c.contains("16.5 40 m"), "{c}"); + // Broken font data is a warning, not a failure. + let r = eps_to_pdf(&startdata_eps(b"\x01\x00\x04\x04\xff\xff\xff", "0 0 1 1 rectfill"), Limits::default()).unwrap(); + assert!(r.warnings.iter().any(|w| w.contains("CFF")), "{:?}", r.warnings); +} + +#[test] +fn tiling_patterns_become_pdf_patterns() { + let (c, conv) = run("<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 + /PaintProc { pop 1 0 0 setrgbcolor 0 0 5 5 rectfill } >> matrix makepattern setpattern + 10 20 50 30 rectfill + << /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 4 4] /XStep 4 /YStep 4 + /PaintProc { pop 1 0 0 setrgbcolor 0 0 2 4 rectfill } >> matrix makepattern /S exch def + [/Pattern /DeviceCMYK] setcolorspace 0 1 0 0 S setcolor 60 20 40 30 rectfill"); + assert!(c.contains("/Pattern cs") && c.contains("/P0 scn") && c.contains("/P1 scn"), "{c}"); + let pdf = String::from_utf8_lossy(&conv.pdf); + assert!(pdf.contains("/PatternType 1") && pdf.contains("/XStep 10"), "{pdf}"); + // A cell that fails or nests too deeply ends cleanly. + for bad in [ + "<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 0 /YStep 1 /PaintProc { pop } >> matrix makepattern setpattern 0 0 1 1 rectfill", + "/P << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 1 /YStep 1 /PaintProc { pop P setpattern 0 0 1 1 rectfill } >> matrix makepattern def P setpattern 0 0 1 1 rectfill", + "<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 1 /YStep 1 /PaintProc { 1 0 div } >> matrix makepattern setpattern 0 0 1 1 rectfill", + ] { + let _ = eps_to_pdf(&eps(bad), Limits::default()); + } +} + +#[test] +fn strokepath_outlines_the_stroke() { + // A 4-point butt-capped line from (10,20) to (60,20): its outline is the band y 18–22. + let (c, _) = run("newpath 10 30 moveto 60 30 lineto 4 setlinewidth strokepath 0 setgray fill"); + assert!(c.lines().any(|l| l == "f"), "{c}"); + assert!(!c.contains(" S\n") && !c.lines().any(|l| l == "S"), "filled, not stroked: {c}"); + for y in ["28", "32"] { + assert!(c.contains(&format!(" {y} l")) || c.contains(&format!(" {y} m")), "edge at y={y}: {c}"); + } + // Dashes split it; a path through a degenerate matrix ends empty; a clip to a stroke works. + let (c, _) = run("newpath 10 30 moveto 110 30 lineto 2 setlinewidth [10 10] 0 setdash strokepath fill"); + assert!(c.matches(" m").count() >= 5, "five dashes: {c}"); + let _ = run("[0 0 0 0 0 0] concat newpath 0 0 moveto 1 1 lineto strokepath fill"); + let (c, _) = run("newpath 10 30 moveto 100 30 lineto 10 setlinewidth strokepath clip newpath 10 20 100 50 rectfill"); + assert!(c.contains("W"), "{c}"); +} + +#[test] +fn vertical_cid_text_runs_down_from_its_vertical_origin() { + let body = "/T-Identity-V findfont 10 scalefont setfont 10 40 moveto <00050005> show"; + let c = content(&eps_to_pdf(&startdata_eps(&tiny_cff(true), body), Limits::default()).unwrap().pdf); + // v = (300, 880) units: the glyph's origin is 3 left and 8.8 below the point; the second + // glyph is one em (10) lower. The box starts 0.5 in from its origin. + assert!(c.contains("7.5 31.2 m") && c.contains("7.5 21.2 m"), "{c}"); + // A CMap program can make a font vertical too. + let body = "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /V1 def /WMode 1 def + 1 begincodespacerange <00> endcodespacerange 1 begincidchar <41> 5 endcidchar + endcmap CMapName currentdict /CMap defineresource pop end end + /U /V1 [/T] composefont pop /U findfont 10 scalefont setfont 10 40 moveto (AA) show"; + let c = content(&eps_to_pdf(&startdata_eps(&tiny_cff(true), body), Limits::default()).unwrap().pdf); + assert!(c.contains("7.5 31.2 m") && c.contains("7.5 21.2 m"), "{c}"); +} + +#[test] +fn a_tiny_dash_on_a_long_path_draws_solid() { + let body = "newpath 0 30 moveto 100000 30 lineto [0.0001] 0 setdash"; + let (c, _) = run(&format!("{body} stroke")); + assert!(!c.contains(" d\n") && !c.contains("] 0 d"), "{c}"); + let (c, _) = run(&format!("{body} strokepath fill")); + assert!(c.matches(" m").count() < 10, "{}", &c[..c.len().min(300)]); +} + +#[test] +fn metrics_without_two_numbers_dont_underflow() { + // `[/a /b]` has two elements but no numbers: the width index used to go below zero. + let (c, _) = run("/Helvetica findfont dup length 1 add dict copy dup /Metrics << /A [/a /b] /B [7] >> put + /H exch definefont 10 scalefont setfont 10 20 moveto (AB) show 10 20 1 1 rectfill"); + assert!(c.contains("re") || c.contains(" m"), "{c}"); +} + +#[test] +fn work_in_proportion_to_operands_counts_against_the_limits() { + let limits = Limits { steps: 200_000, memory: 8 << 20 }; + let ends = |body: &str| { + let r = eps_to_pdf(&eps(&format!("10 20 1 1 rectfill {body}")), limits).expect("what was painted is kept"); + let w = r.warnings.join(" "); + assert!(w.contains("too long") || w.contains("memory") || w.contains("too much data"), "{body}: {w}"); + // It stops at the charge that crosses the limit: overshoot is one operand's worth at most. + assert!(r.steps <= limits.steps + 1_000_000 || r.memory > limits.memory, "{body}: {} steps", r.steps); + }; + // Each would take seconds to minutes in a few steps without its charge. + ends("/h 1000000 string def /n 500 string def { h n search pop pop } loop"); + ends("100000 { 0 } repeat { 100000 copy 100000 { pop } repeat } loop"); + ends("/a 4000000 string def /b 4000000 string def { a b eq pop } loop"); + ends("newpath 0 0 moveto 100000 { 1 0 rlineto } repeat { gsave grestore } loop"); + ends("{ 4000000 string pop } loop"); + ends("/d 100000 dict def 0 1 99999 { d exch 0 put } for { d { pop pop } forall } loop"); + ends("/Helvetica findfont 10 scalefont setfont /s 1000000 string def { 0 0 moveto s show } loop"); + ends("mark 100000 { 0 } repeat { counttomark pop } loop"); + // Reading counts: a stream decoded over and over. + let z = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 1 << 20], 9); + let hex: String = z.iter().map(|b| format!("{b:02x}")).collect(); + ends(&format!("/z <{hex}> def {{ z /FlateDecode filter dup 2000000 string readstring pop pop closefile }} loop")); + // Tiny programs that only loop still end at the step limit. + ends("{ } loop"); +} + +#[test] +fn the_web_gets_smaller_default_limits() { + let l = Limits::default(); + if cfg!(target_arch = "wasm32") { + assert!(l.memory <= 256 << 20 && l.steps <= 20_000_000); + } else { + assert!(l.memory <= 1 << 30 && l.steps <= 50_000_000); + } +} diff --git a/crates/ps/src/read/type1.rs b/crates/ps/src/read/type1.rs new file mode 100644 index 0000000000..7ea3476e22 --- /dev/null +++ b/crates/ps/src/read/type1.rs @@ -0,0 +1,357 @@ +//! Type 1 glyphs: charstrings decrypted and run into outlines (Adobe Type 1 Font Format). + +use crate::read::gstate::Seg; +use crate::read::obj::{Key, Obj, PDict, name}; + +/// A glyph outline in glyph space and its advance. +#[derive(Debug, Clone, Default)] +pub struct Glyph { + pub path: Vec, + pub width: (f64, f64), + /// The work making it took (charstring operators run, or outline points). + pub cost: usize, +} + +/// The most charstring operators run for one glyph (subroutines included). +const MAX_OPS: usize = 100_000; +const MAX_CALL_DEPTH: usize = 12; + +pub fn decrypt(data: &[u8], skip: usize) -> Vec { + let mut r: u16 = 4330; + let mut out = Vec::with_capacity(data.len()); + for &c in data { + out.push(c ^ (r >> 8) as u8); + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + } + out.into_iter().skip(skip).collect() +} + +fn string_bytes(o: &Obj) -> Option> { + match o { + Obj::Str(s, _) => Some(s.bytes()), + _ => None, + } +} + +struct Font { + charstrings: PDict, + subrs: Vec>, + len_iv: i64, +} + +impl Font { + fn of(font: &PDict) -> Option { + let Some(Obj::Dict(charstrings)) = font.get_name(b"CharStrings") else { return None }; + let private = match font.get_name(b"Private") { + Some(Obj::Dict(p)) => Some(p), + _ => None, + }; + let len_iv = private.as_ref().and_then(|p| p.get_name(b"lenIV")).and_then(|o| o.num()).unwrap_or(4.0) as i64; + let subrs = match private.as_ref().and_then(|p| p.get_name(b"Subrs")) { + Some(Obj::Array(a, _)) => a.items().iter().map(|o| string_bytes(o).unwrap_or_default()).collect(), + _ => Vec::new(), + }; + Some(Font { charstrings, subrs, len_iv }) + } + + fn plain(&self, data: &[u8]) -> Vec { + if self.len_iv < 0 { data.to_vec() } else { decrypt(data, usize::try_from(self.len_iv).unwrap_or(4)) } + } + + fn charstring(&self, glyph: &[u8]) -> Option> { + let o = self.charstrings.get(&Key::Name(name(glyph))).or_else(|| self.charstrings.get(&Key::Name(name(b".notdef"))))?; + string_bytes(&o).map(|d| self.plain(&d)) + } +} + +#[derive(Default)] +struct Run { + path: Vec, + x: f64, + y: f64, + open: bool, + width: (f64, f64), + sb: (f64, f64), + stack: Vec, + ps: Vec, + flex: Option>, + ops: usize, + done: bool, +} + +impl Run { + fn move_to(&mut self, x: f64, y: f64) { + self.x = x; + self.y = y; + if let Some(pts) = self.flex.as_mut() { + pts.push((x, y)); + return; + } + if self.open { + self.path.push(Seg::Close); + } + self.path.push(Seg::Move(x, y)); + self.open = true; + } + + fn line_to(&mut self, x: f64, y: f64) { + if !self.open { + self.path.push(Seg::Move(self.x, self.y)); + self.open = true; + } + self.x = x; + self.y = y; + self.path.push(Seg::Line(x, y)); + } + + fn curve_to(&mut self, a: (f64, f64), b: (f64, f64), c: (f64, f64)) { + if !self.open { + self.path.push(Seg::Move(self.x, self.y)); + self.open = true; + } + self.x = c.0; + self.y = c.1; + self.path.push(Seg::Curve(a.0, a.1, b.0, b.1, c.0, c.1)); + } + + fn close(&mut self) { + if self.open { + self.path.push(Seg::Close); + self.open = false; + } + } +} + +fn exec(font: &Font, code: &[u8], run: &mut Run, depth: usize, std_glyph: &dyn Fn(usize) -> Option>) -> Option<()> { + if depth > MAX_CALL_DEPTH { + return None; + } + let mut i = 0; + while i < code.len() && !run.done { + run.ops += 1; + if run.ops > MAX_OPS || run.stack.len() > 64 { + return None; + } + let v = *code.get(i)?; + i += 1; + if v >= 32 { + let n = match v { + 32..=246 => f64::from(v) - 139.0, + 247..=250 => { + let w = *code.get(i)?; + i += 1; + (f64::from(v) - 247.0) * 256.0 + f64::from(w) + 108.0 + } + 251..=254 => { + let w = *code.get(i)?; + i += 1; + -(f64::from(v) - 251.0) * 256.0 - f64::from(w) - 108.0 + } + _ => { + let b: [u8; 4] = code.get(i..i + 4)?.try_into().ok()?; + i += 4; + f64::from(i32::from_be_bytes(b)) + } + }; + run.stack.push(n); + continue; + } + let s = std::mem::take(&mut run.stack); + let a = |k: usize| s.get(k).copied().unwrap_or(0.0); + match v { + // hstem, vstem: hints only. + 1 | 3 => {} + 4 => run.move_to(run.x, run.y + a(0)), + 5 => run.line_to(run.x + a(0), run.y + a(1)), + 6 => run.line_to(run.x + a(0), run.y), + 7 => run.line_to(run.x, run.y + a(0)), + 8 => { + let p1 = (run.x + a(0), run.y + a(1)); + let p2 = (p1.0 + a(2), p1.1 + a(3)); + let p3 = (p2.0 + a(4), p2.1 + a(5)); + run.curve_to(p1, p2, p3); + } + 9 => run.close(), + 10 => { + let n = usize::try_from(a(s.len().saturating_sub(1)) as i64).ok()?; + // Arguments below the subroutine number stay for it. + run.stack = s.get(..s.len().saturating_sub(1)).map(<[f64]>::to_vec).unwrap_or_default(); + let sub = font.subrs.get(n)?; + let plain = font.plain(sub); + exec(font, &plain, run, depth + 1, std_glyph)?; + continue; + } + 11 => { + run.stack = s; + return Some(()); + } + 12 => { + let e = *code.get(i)?; + i += 1; + match e { + // dotsection, vstem3, hstem3: hints. + 0..=2 => {} + 6 => { + // seac: an accented character from two StandardEncoding glyphs. + let (asb, adx, ady, bchar, achar) = (a(0), a(1), a(2), a(3) as usize, a(4) as usize); + let base = font.charstring(&std_glyph(bchar)?)?; + let accent = font.charstring(&std_glyph(achar)?)?; + let mut b = Run::default(); + exec(font, &base, &mut b, depth + 1, std_glyph)?; + let mut acc = Run::default(); + exec(font, &accent, &mut acc, depth + 1, std_glyph)?; + let (dx, dy) = (adx - asb + run.sb.0, ady); + run.path.extend(b.path); + run.path.extend(acc.path.iter().map(|s| s.map(&crate::read::gstate::M([1.0, 0.0, 0.0, 1.0, dx, dy])))); + run.done = true; + } + 7 => { + run.sb = (a(0), a(1)); + run.width = (a(2), a(3)); + run.x = a(0); + run.y = a(1); + } + 12 => { + let n = s.len(); + let d = a(n.saturating_sub(1)); + let q = if d != 0.0 { a(n.saturating_sub(2)) / d } else { 0.0 }; + run.stack = s.get(..n.saturating_sub(2)).map(<[f64]>::to_vec).unwrap_or_default(); + run.stack.push(q); + continue; + } + 16 => { + // callothersubr: flex (0–2) and hint replacement (3) are done here. + let n = s.len(); + let other = a(n.saturating_sub(1)) as i64; + let argc = usize::try_from(a(n.saturating_sub(2)) as i64).unwrap_or(0).min(n.saturating_sub(2)); + let args: Vec = s.get(n.saturating_sub(2) - argc..n.saturating_sub(2)).map(<[f64]>::to_vec).unwrap_or_default(); + run.stack = s.get(..n.saturating_sub(2) - argc).map(<[f64]>::to_vec).unwrap_or_default(); + match other { + 1 => run.flex = Some(Vec::new()), + 2 => {} + 0 => { + let pts = run.flex.take().unwrap_or_default(); + if let (Some(&p1), Some(&p2), Some(&p3), Some(&p4), Some(&p5), Some(&p6)) = + (pts.get(1), pts.get(2), pts.get(3), pts.get(4), pts.get(5), pts.get(6)) + { + run.curve_to(p1, p2, p3); + run.curve_to(p4, p5, p6); + } + // The end point for `pop pop setcurrentpoint`. + run.ps = vec![run.y, run.x]; + } + _ => { + // Hint replacement and others: their arguments come back. + run.ps = args.into_iter().rev().collect(); + } + } + continue; + } + 17 => { + let v = run.ps.pop().unwrap_or(0.0); + run.stack = s; + run.stack.push(v); + continue; + } + 33 => { + run.x = a(0); + run.y = a(1); + } + _ => {} + } + } + 13 => { + run.sb = (a(0), 0.0); + run.width = (a(1), 0.0); + run.x = a(0); + run.y = 0.0; + } + 14 => { + run.close(); + run.done = true; + } + 21 => run.move_to(run.x + a(0), run.y + a(1)), + 22 => run.move_to(run.x + a(0), run.y), + 30 => { + let p1 = (run.x, run.y + a(0)); + let p2 = (p1.0 + a(1), p1.1 + a(2)); + let p3 = (p2.0 + a(3), p2.1); + run.curve_to(p1, p2, p3); + } + 31 => { + let p1 = (run.x + a(0), run.y); + let p2 = (p1.0 + a(1), p1.1 + a(2)); + let p3 = (p2.0, p2.1 + a(3)); + run.curve_to(p1, p2, p3); + } + _ => {} + } + } + Some(()) +} + +/// The outline and advance of `glyph` in a Type 1 font dictionary, None when it has none. +pub fn glyph(font: &PDict, glyph: &[u8], std_names: &[&str]) -> Option { + let f = Font::of(font)?; + let code = f.charstring(glyph)?; + let mut run = Run::default(); + let std_glyph = |i: usize| std_names.get(i).map(|n| n.as_bytes().to_vec()); + exec(&f, &code, &mut run, 0, &std_glyph)?; + run.close(); + Some(Glyph { cost: run.ops + run.path.len(), path: run.path, width: run.width }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::read::obj::{PArr, PStr}; + + fn encrypt(plain: &[u8]) -> Vec { + let mut r: u16 = 4330; + let mut out = Vec::new(); + for &p in [0u8; 4].iter().chain(plain) { + let c = p ^ (r >> 8) as u8; + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + out.push(c); + } + out + } + + fn num(v: i32) -> Vec { + match v { + -107..=107 => vec![(v + 139) as u8], + 108..=1131 => { + let v = v - 108; + vec![(v / 256 + 247) as u8, (v % 256) as u8] + } + -1131..=-108 => { + let v = -v - 108; + vec![(v / 256 + 251) as u8, (v % 256) as u8] + } + _ => [vec![255], v.to_be_bytes().to_vec()].concat(), + } + } + + #[test] + fn a_square_glyph_and_its_width() { + // 50 600 hsbw; 0 0 rmoveto... : a 500 × 700 box through a subroutine. + let sub = [num(500), vec![6], num(700), vec![7], vec![11]].concat(); + let cs = [num(50), num(600), vec![13], num(0), num(0), vec![21], num(0), vec![10], num(-500), vec![6], vec![9], vec![14]].concat(); + let font = PDict::new(4); + let chars = PDict::new(2); + chars.put_name(b"A", Obj::Str(PStr::new(encrypt(&cs)), false)); + font.put_name(b"CharStrings", Obj::Dict(chars)); + let private = PDict::new(2); + private.put_name(b"Subrs", Obj::Array(PArr::new(vec![Obj::Str(PStr::new(encrypt(&sub)), false)]), false)); + font.put_name(b"Private", Obj::Dict(private)); + let g = glyph(&font, b"A", &[]).unwrap(); + assert_eq!(g.width, (600.0, 0.0)); + assert_eq!(g.path.first(), Some(&Seg::Move(50.0, 0.0))); + assert!(g.path.contains(&Seg::Line(550.0, 700.0)), "{:?}", g.path); + // Garbage doesn't panic or loop. + let chars = PDict::new(1); + chars.put_name(b"B", Obj::Str(PStr::new(encrypt(&[10, 10, 10, 255, 255])), false)); + font.put_name(b"CharStrings", Obj::Dict(chars)); + let _ = glyph(&font, b"B", &[]); + } +} diff --git a/crates/ps/src/read/type42.rs b/crates/ps/src/read/type42.rs new file mode 100644 index 0000000000..0b2bf7fbfa --- /dev/null +++ b/crates/ps/src/read/type42.rs @@ -0,0 +1,81 @@ +//! Type 42 fonts: a TrueType font in the `sfnts` strings, glyphs found through CharStrings +//! (glyph name → glyph index), drawn in em units (Adobe Technical Note 5012). + +use skrifa::instance::{LocationRef, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::TableProvider; +use skrifa::{GlyphId, MetadataProvider}; + +use crate::read::gstate::Seg; +use crate::read::obj::{Key, Obj, PDict, name}; +use crate::read::type1::Glyph; + +/// The TrueType data of a Type 42 font: its `sfnts` strings joined (an odd string's last byte +/// is padding). +pub fn sfnts(font: &PDict) -> Option> { + let Some(Obj::Array(a, _)) = font.get_name(b"sfnts") else { return None }; + let mut out = Vec::new(); + for s in a.items() { + let Obj::Str(s, _) = s else { return None }; + let mut b = s.bytes(); + if b.len() % 2 == 1 { + b.pop(); + } + out.extend_from_slice(&b); + } + Some(out) +} + +struct Pen { + path: Vec, + scale: f64, + cur: (f64, f64), +} + +impl OutlinePen for Pen { + fn move_to(&mut self, x: f32, y: f32) { + let p = (f64::from(x) * self.scale, f64::from(y) * self.scale); + self.path.push(Seg::Move(p.0, p.1)); + self.cur = p; + } + fn line_to(&mut self, x: f32, y: f32) { + let p = (f64::from(x) * self.scale, f64::from(y) * self.scale); + self.path.push(Seg::Line(p.0, p.1)); + self.cur = p; + } + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + // A quadratic as the cubic with the same shape. + let (c, p) = ((f64::from(cx) * self.scale, f64::from(cy) * self.scale), (f64::from(x) * self.scale, f64::from(y) * self.scale)); + let a = (self.cur.0 + 2.0 / 3.0 * (c.0 - self.cur.0), self.cur.1 + 2.0 / 3.0 * (c.1 - self.cur.1)); + let b = (p.0 + 2.0 / 3.0 * (c.0 - p.0), p.1 + 2.0 / 3.0 * (c.1 - p.1)); + self.path.push(Seg::Curve(a.0, a.1, b.0, b.1, p.0, p.1)); + self.cur = p; + } + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + let s = self.scale; + let p = (f64::from(x) * s, f64::from(y) * s); + self.path.push(Seg::Curve(f64::from(x1) * s, f64::from(y1) * s, f64::from(x2) * s, f64::from(y2) * s, p.0, p.1)); + self.cur = p; + } + fn close(&mut self) { + self.path.push(Seg::Close); + } +} + +/// The glyph index for a glyph name (CharStrings maps names to indices). +pub fn glyph_index(font: &PDict, glyph: &[u8]) -> u32 { + let Some(Obj::Dict(cs)) = font.get_name(b"CharStrings") else { return 0 }; + cs.get(&Key::Name(name(glyph))).and_then(|o| o.num()).and_then(|v| u32::try_from(v as i64).ok()).unwrap_or(0) +} + +/// Glyph `gid` of TrueType `data` in em units, with its advance. +pub fn glyph(data: &[u8], gid: u32) -> Option { + let f = skrifa::FontRef::new(data).ok()?; + let upem = f64::from(f.head().ok()?.units_per_em()).max(1.0); + let mut pen = Pen { path: Vec::new(), scale: 1.0 / upem, cur: (0.0, 0.0) }; + if let Some(g) = f.outline_glyphs().get(GlyphId::new(gid)) { + let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), &mut pen); + } + let adv = f.glyph_metrics(Size::unscaled(), LocationRef::default()).advance_width(GlyphId::new(gid)).map_or(0.0, f64::from) / upem; + Some(Glyph { cost: pen.path.len() * 4 + 16, path: pen.path, width: (adv, 0.0) }) +} From df8bfef1bb435820471d4dbb4a9b462f09be5c25 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 06:21:39 +0000 Subject: [PATCH 2/7] clippy(ps): document stateful decoder complexity --- crates/ps/src/read/ops/mesh.rs | 1 + crates/ps/src/read/source.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/ps/src/read/ops/mesh.rs b/crates/ps/src/read/ops/mesh.rs index d661e7b222..01a499b869 100644 --- a/crates/ps/src/read/ops/mesh.rs +++ b/crates/ps/src/read/ops/mesh.rs @@ -78,6 +78,7 @@ impl Reader { } } +#[allow(clippy::cognitive_complexity, reason = "one stateful reader handles the PostScript mesh record layouts")] pub fn build(ip: &mut Interp, d: &PDict, kind: i64, space: &Space, func: Option<&Func>) -> R> { let num = |k: &[u8]| d.get_name(k).and_then(|o| o.num()).unwrap_or(0.0); let ncomp = if func.is_some() { 1 } else { space.comps() }; diff --git a/crates/ps/src/read/source.rs b/crates/ps/src/read/source.rs index f30d3bea41..e87153cbe8 100644 --- a/crates/ps/src/read/source.rs +++ b/crates/ps/src/read/source.rs @@ -377,6 +377,7 @@ impl Sources { } /// Decode some more of filter `id`; false once its data has ended. + #[allow(clippy::cognitive_complexity, reason = "one streaming decoder state machine preserves filter read-ahead and predictor ordering")] fn fill(&mut self, id: usize, depth: usize) -> bool { let Some(Source { kind: Kind::Filter { up, dec, .. }, .. }) = self.list.get(id) else { return false }; let up = *up; From 2d80b30577f7c07db68f75d5aa3fd61d0c2f3307 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 06:59:13 +0000 Subject: [PATCH 3/7] fix(ps): bound huge arc angle normalization --- crates/ps/src/read/gstate.rs | 42 ++++++++++++++++++++++++++++++++++- crates/ps/src/read/ops/gfx.rs | 18 ++++++--------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/crates/ps/src/read/gstate.rs b/crates/ps/src/read/gstate.rs index d27df9d812..f569c0ff2f 100644 --- a/crates/ps/src/read/gstate.rs +++ b/crates/ps/src/read/gstate.rs @@ -77,6 +77,35 @@ impl Seg { /// The most segments one path may hold. pub const MAX_PATH: usize = 4_000_000; +/// Maximum number of complete turns an `arc`/`arcn` adds to the path before preserving parity. +pub const MAX_ARC_TURNS: f64 = 1024.0; +/// One quarter-circle cubic per turn keeps every retained sweep geometrically accurate. +const MAX_ARC_SEGMENTS: usize = 4096; + +/// The sweep PostScript's `arc` or `arcn` operator selects without iterating over whole turns. +/// +/// The modulo form matters for hostile angles: repeatedly adding or subtracting 360 can take +/// effectively forever for a finite `f64` with a huge magnitude. +pub fn bounded_arc_sweep(a0: f64, a1: f64, negative: bool) -> (f64, bool) { + let sweep = if !negative && a1 < a0 { + (a1 % 360.0 - a0 % 360.0).rem_euclid(360.0) + } else if negative && a1 > a0 { + -(a0 % 360.0 - a1 % 360.0).rem_euclid(360.0) + } else { + a1 - a0 + }; + let max = MAX_ARC_TURNS * 360.0; + if !sweep.is_finite() { + return (0.0, true); + } + if sweep.abs() <= max { + return (sweep, false); + } + let sign = sweep.signum(); + let remainder = (sign * (a1 % 720.0 - a0 % 720.0)).rem_euclid(720.0); + (sign * (max - 720.0 + remainder), true) +} + /// Bézier segments for an arc of the circle at (cx, cy) with radius r, from angle a0 to a1 /// (degrees), in user space: the first point and the curves. pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> ((f64, f64), Vec<[f64; 6]>) { @@ -86,7 +115,7 @@ pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> ((f64, f64), Ve if !sweep.is_finite() || sweep == 0.0 { return (start, Vec::new()); } - let n = ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 64); + let n = ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, MAX_ARC_SEGMENTS); let step = sweep / n as f64; let k = 4.0 / 3.0 * (step / 4.0).tan(); let mut out = Vec::with_capacity(n); @@ -338,5 +367,16 @@ mod tests { let last = curves.last().unwrap(); assert!((last[4] - 10.0).abs() < 1e-9 && last[5].abs() < 1e-9); assert!(arc_curves(0.0, 0.0, 1.0, 0.0, f64::INFINITY).1.is_empty()); + assert_eq!(arc_curves(0.0, 0.0, 1.0, 0.0, MAX_ARC_TURNS * 360.0).1.len(), MAX_ARC_SEGMENTS); + } + + #[test] + fn huge_arc_angles_are_reduced_without_unbounded_turn_loops() { + assert_eq!(bounded_arc_sweep(1e18, 0.0, false), (80.0, false)); + assert_eq!(bounded_arc_sweep(0.0, 1e18, true), (-80.0, false)); + let (sweep, skipped) = bounded_arc_sweep(0.0, 5e18, false); + assert!(skipped); + assert!(sweep.abs() <= MAX_ARC_TURNS * 360.0); + assert_eq!(bounded_arc_sweep(0.0, f64::INFINITY, false), (0.0, true)); } } diff --git a/crates/ps/src/read/ops/gfx.rs b/crates/ps/src/read/ops/gfx.rs index 83a9e17d6b..d1435f8c9b 100644 --- a/crates/ps/src/read/ops/gfx.rs +++ b/crates/ps/src/read/ops/gfx.rs @@ -2,7 +2,7 @@ use std::rc::Rc; -use crate::read::gstate::{Clip, DevColor, GState, M, MAX_PATH, Seg, Space, arc_curves, bbox, flatten}; +use crate::read::gstate::{Clip, DevColor, GState, M, MAX_PATH, Seg, Space, arc_curves, bbox, bounded_arc_sweep, flatten}; use crate::read::interp::{Interp, PsErr, R, err, matrix_obj}; use crate::read::obj::{Obj, PArr, PDict}; use crate::read::pdf::StrokeParams; @@ -434,21 +434,17 @@ pub fn closepath(ip: &mut Interp) -> R { } fn arc_common(ip: &mut Interp, negative: bool) -> R { - let mut a1 = ip.pop_num()?; + let a1 = ip.pop_num()?; let a0 = ip.pop_num()?; let r = ip.pop_num()?; let cy = ip.pop_num()?; let cx = ip.pop_num()?; - if negative { - while a1 > a0 { - a1 -= 360.0; - } - } else { - while a1 < a0 { - a1 += 360.0; - } + let (sweep, shortened) = bounded_arc_sweep(a0, a1, negative); + if shortened { + ip.warnings.push("arc (too many turns)".into()); } - let (start, curves) = arc_curves(cx, cy, r, a0, a1); + let start_angle = a0 % 360.0; + let (start, curves) = arc_curves(cx, cy, r, start_angle, start_angle + sweep); let m = ip.gs.ctm; let s = m.apply(start.0, start.1); if ip.gs.cur.is_some() { From aed73b9382f618a19af96d389cffa7ea8cfa6e4e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 08:18:28 +0000 Subject: [PATCH 4/7] fix(ps): preserve EffectCraft EPS text and limits parity --- crates/ps/README.md | 7 +- crates/ps/src/read/font.rs | 131 +++++++++++++++++++++++++++++++- crates/ps/src/read/gstate.rs | 28 ++++--- crates/ps/src/read/interp.rs | 47 +++++++++++- crates/ps/src/read/mod.rs | 6 +- crates/ps/src/read/ops/basic.rs | 30 ++++++-- crates/ps/src/read/ops/gfx.rs | 13 +++- crates/ps/src/read/ops/image.rs | 8 ++ crates/ps/src/read/ops/misc.rs | 47 +++++++++++- crates/ps/src/read/ops/mod.rs | 2 + crates/ps/src/read/pdf.rs | 14 +++- crates/ps/src/read/tests.rs | 56 ++++++++++++-- 12 files changed, 348 insertions(+), 41 deletions(-) diff --git a/crates/ps/README.md b/crates/ps/README.md index db0729a86e..51deedcc2c 100644 --- a/crates/ps/README.md +++ b/crates/ps/README.md @@ -36,14 +36,15 @@ the old composite FMapTypes other than 2 and 9. ## Limits -`Limits { steps, memory }` bounds one conversion. Every operator charges the work it does in +`Limits { steps, memory, stack_overflow_noop, skip_images, keep_unpainted, zero_division_zero, fallback_font }` bounds one conversion. The optional font callback supplies host stand-ins for missing fonts; booleans let a host retain prior operand-overflow, image, partial-page, and numeric-coercion behavior. Every operator charges the work it does in proportion to its operands (elements copied or searched, path segments, glyphs, samples) against `steps` as it goes, and every byte read or decoded at any filter level counts too, so the step limit bounds run time. `memory` bounds everything a conversion makes, copies included. The defaults are 50 million steps and 1 GiB, or 20 million and 256 MiB on wasm32; real EPS files use at most a few million steps and tens of megabytes, and a program that runs -into the limits ends within seconds. A program that fails or exceeds them before painting -anything gives `Err`; one that fails later keeps what it painted (with a warning). +into the limits ends within seconds. By default a program that fails or exceeds them before +painting anything gives `Err`; setting `keep_unpainted` returns a blank page with the warning. +One that fails later keeps what it painted (with a warning). `cargo run -p craft-ps --features read --example eps2pdf -- in.eps out.pdf` converts one file and prints the steps and memory it used. diff --git a/crates/ps/src/read/font.rs b/crates/ps/src/read/font.rs index e5fd42cdeb..931c33dad8 100644 --- a/crates/ps/src/read/font.rs +++ b/crates/ps/src/read/font.rs @@ -4,7 +4,7 @@ use crate::read::gstate::M; use crate::read::interp::{Interp, PsErr, R, err, matrix_obj}; -use crate::read::obj::{Key, Obj, PArr, PDict, name}; +use crate::read::obj::{Key, Obj, PArr, PDict, PStr, name}; fn font_matrix(d: &PDict) -> M { match d.get_name(b"FontMatrix") { @@ -15,6 +15,12 @@ fn font_matrix(d: &PDict) -> M { /// A stand-in for a font the file doesn't carry: a Type 1 shell with StandardEncoding. fn substitute(ip: &mut Interp, font_name: &[u8]) -> PDict { + if let Some(bytes) = ip.limits.fallback_font.and_then(|provider| provider(font_name)) + && let Some(d) = substitute_type42(ip, font_name, bytes) + { + add_fid(ip, &d); + return d; + } let d = PDict::new(12); d.put_name(b"FontName", Obj::lit(font_name)); d.put_name(b"FontType", Obj::Int(1)); @@ -31,6 +37,89 @@ fn substitute(ip: &mut Interp, font_name: &[u8]) -> PDict { d } +fn substitute_type42(ip: &Interp, font_name: &[u8], bytes: &[u8]) -> Option { + use skrifa::MetadataProvider; + + let font = skrifa::FontRef::new(bytes).ok()?; + let cmap = font.charmap(); + let charstrings = PDict::new(256); + for glyph_name in crate::read::encodings::standard() { + let Some(ch) = standard_glyph_char(glyph_name.as_bytes()) else { continue }; + let Some(gid) = cmap.map(ch) else { continue }; + charstrings.put(Obj::lit(glyph_name.as_bytes()), Obj::Int(i64::from(gid.to_u32()))); + } + let mut sfnt = bytes.to_vec(); + if sfnt.len() % 2 == 1 { + sfnt.push(0); + } + let d = PDict::new(12); + d.put_name(b"FontName", Obj::lit(font_name)); + d.put_name(b"FontType", Obj::Int(42)); + // Type 42 outlines are emitted in normalized em units by `type42::glyph`, unlike the + // 1000-unit CharStrings convention used by Type 1 fonts. + d.put_name(b"FontMatrix", matrix_obj(&M([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))); + d.put_name(b"FontBBox", Obj::Array(PArr::new(vec![Obj::Int(0), Obj::Int(-200), Obj::Int(1000), Obj::Int(900)]), false)); + d.put_name(b"PaintType", Obj::Int(0)); + if let Some(enc) = ip.systemdict.get_name(b"StandardEncoding") { + d.put_name(b"Encoding", enc); + } + d.put_name(b"CharStrings", Obj::Dict(charstrings)); + d.put_name(b"sfnts", Obj::Array(PArr::new(vec![Obj::Str(PStr::new(sfnt), false)]), false)); + Some(d) +} + +fn standard_glyph_char(name: &[u8]) -> Option { + if name.len() == 1 && name[0].is_ascii() { + return Some(char::from(name[0])); + } + Some(match name { + b"space" => ' ', + b"exclam" => '!', + b"quotedbl" => '"', + b"numbersign" => '#', + b"dollar" => '$', + b"percent" => '%', + b"ampersand" => '&', + b"quoteright" => '\'', + b"parenleft" => '(', + b"parenright" => ')', + b"asterisk" => '*', + b"plus" => '+', + b"comma" => ',', + b"hyphen" => '-', + b"period" => '.', + b"slash" => '/', + b"colon" => ':', + b"semicolon" => ';', + b"less" => '<', + b"equal" => '=', + b"greater" => '>', + b"question" => '?', + b"at" => '@', + b"bracketleft" => '[', + b"backslash" => '\\', + b"bracketright" => ']', + b"asciicircum" => '^', + b"underscore" => '_', + b"quoteleft" => '`', + b"braceleft" => '{', + b"bar" => '|', + b"braceright" => '}', + b"asciitilde" => '~', + b"zero" => '0', + b"one" => '1', + b"two" => '2', + b"three" => '3', + b"four" => '4', + b"five" => '5', + b"six" => '6', + b"seven" => '7', + b"eight" => '8', + b"nine" => '9', + _ => return None, + }) +} + fn add_fid(ip: &mut Interp, d: &PDict) { if !d.known(&Key::Name(name(b"FID"))) { d.put_name(b"FID", Obj::FontId(ip.next_fid)); @@ -47,6 +136,17 @@ pub fn find_font(ip: &mut Interp, key: &Obj) -> PDict { { return d; } + // An embedded Type 1 program can leave its FontName dictionary active after eexec without + // issuing definefont. Resolve that in-scope font before substituting a stand-in. + let active = + ip.d.iter() + .rev() + .find(|font| font.get_name(b"FontName").is_some_and(|n| n.key() == key.key()) && font.get_name(b"CharStrings").is_some()) + .cloned(); + if let Some(d) = active { + add_fid(ip, &d); + return d; + } let n = crate::read::ops::basic::text_of(key); if let Some(d) = crate::read::composite::compose_by_name(ip, &n) { return d; @@ -283,11 +383,12 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & s.iter().map(|c| (u32::from(*c), font.clone(), fm, crate::read::composite::Key2::Code(*c))).collect() }; let mut total = (0.0, 0.0); + let mut text_path = Vec::new(); for (i, (code, leaf, fm, key)) in jobs.into_iter().enumerate() { - ip.charge(GLYPH_STEPS)?; let type3 = leaf.get_name(b"FontType").and_then(|o| o.num()) == Some(3.0); let w = match key { crate::read::composite::Key2::Code(c) if type3 => { + ip.charge(GLYPH_STEPS)?; if draw && ip.gs.cur.is_some() { type3_glyph(ip, &leaf, c)? } else { @@ -310,6 +411,9 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & crate::read::composite::Key2::Code(c) => outline(ip, &leaf, c), crate::read::composite::Key2::Cid(cid) => cid_outline(ip, &leaf, cid), }; + // Empty outlines (usually spaces) still advance but need little work; their + // outline cache/decoder has already charged what it actually did. + ip.charge(if g.as_ref().is_some_and(|g| g.path.is_empty()) { 1 } else { GLYPH_STEPS })?; // Vertical writing: the glyph hangs from its vertical origin and advances down. let em = 1.0 / font_matrix(&leaf).0[3].abs().max(1e-9); let vmetrics = match (vertical, &g) { @@ -325,9 +429,15 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & let m = glyph_to_device(ip, &fm, origin)?; let path: Vec = g.path.iter().map(|s| s.map(&m)).collect(); if ip.charpath { + if ip.gs.path.len().saturating_add(path.len()) > crate::read::gstate::MAX_PATH { + return Err(PsErr::Fatal("PostScript (path limit)".into())); + } ip.gs.path.extend(path); } else { - crate::read::ops::gfx::fill_path(ip, &path, false)?; + if text_path.len().saturating_add(path.len()) > crate::read::gstate::MAX_PATH { + return Err(PsErr::Fatal("PostScript (path limit)".into())); + } + text_path.extend(path); } } match (vmetrics, leaf.get_name(b"Metrics"), key_code(code)) { @@ -352,6 +462,9 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & ip.gs.cur = Some((cx + ddx, cy + ddy)); } } + if draw && !ip.charpath && !text_path.is_empty() { + crate::read::ops::gfx::fill_path(ip, &text_path, false)?; + } Ok(total) } @@ -365,7 +478,17 @@ fn key_code(code: u32) -> Option { } fn show_string(ip: &mut Interp, s: &[u8], draw: bool, per_char: &mut PerGlyph<'_>) -> R<(f64, f64)> { - show_glyphs(ip, s, draw, false, per_char) + // Whitespace-only shows advance the text position but emit no painted glyphs; avoid bloating + // the content stream with an empty semantic marked-content wrapper for them. + if draw && !ip.charpath && s.iter().any(|b| !b.is_ascii_whitespace()) { + let text: String = s.iter().map(|b| char::from(*b)).collect(); + ip.out.begin_actual_text(&text); + let result = show_glyphs(ip, s, draw, false, per_char); + ip.out.end_actual_text(); + result + } else { + show_glyphs(ip, s, draw, false, per_char) + } } pub fn show(ip: &mut Interp) -> R { diff --git a/crates/ps/src/read/gstate.rs b/crates/ps/src/read/gstate.rs index f569c0ff2f..4a9eaf06d2 100644 --- a/crates/ps/src/read/gstate.rs +++ b/crates/ps/src/read/gstate.rs @@ -75,12 +75,10 @@ impl Seg { } /// The most segments one path may hold. -pub const MAX_PATH: usize = 4_000_000; +pub const MAX_PATH: usize = 1 << 20; /// Maximum number of complete turns an `arc`/`arcn` adds to the path before preserving parity. pub const MAX_ARC_TURNS: f64 = 1024.0; -/// One quarter-circle cubic per turn keeps every retained sweep geometrically accurate. -const MAX_ARC_SEGMENTS: usize = 4096; /// The sweep PostScript's `arc` or `arcn` operator selects without iterating over whole turns. /// @@ -108,14 +106,22 @@ pub fn bounded_arc_sweep(a0: f64, a1: f64, negative: bool) -> (f64, bool) { /// Bézier segments for an arc of the circle at (cx, cy) with radius r, from angle a0 to a1 /// (degrees), in user space: the first point and the curves. -pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> ((f64, f64), Vec<[f64; 6]>) { +pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, max_segments: usize) -> ((f64, f64), Vec<[f64; 6]>, bool) { let (a0, a1) = (a0.to_radians(), a1.to_radians()); let start = (cx + r * a0.cos(), cy + r * a0.sin()); let sweep = a1 - a0; if !sweep.is_finite() || sweep == 0.0 { - return (start, Vec::new()); + return (start, Vec::new(), false); } - let n = ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, MAX_ARC_SEGMENTS); + // The established EPS reader uses a 0.1-unit curve tolerance. A quarter-circle cubic has + // about 0.00027253*r maximum radial error; subdivision error falls with the sixth power of + // the segment angle. Estimate before allocation so huge radii stop at the path bound. + let subdivisions = (r.abs() * 0.00027253 / 0.1).max(1.0).powf(1.0 / 6.0).ceil(); + let required = (sweep.abs() / std::f64::consts::FRAC_PI_2 * subdivisions).ceil().max(1.0); + if !required.is_finite() || required > max_segments as f64 { + return (start, Vec::new(), true); + } + let n = required as usize; let step = sweep / n as f64; let k = 4.0 / 3.0 * (step / 4.0).tan(); let mut out = Vec::with_capacity(n); @@ -126,7 +132,7 @@ pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> ((f64, f64), Ve out.push([cx + r * (ca - k * sa), cy + r * (sa + k * ca), cx + r * (cb + k * sb), cy + r * (sb - k * cb), cx + r * cb, cy + r * sb]); a = b; } - (start, out) + (start, out, false) } #[derive(Clone, Debug)] @@ -361,13 +367,15 @@ mod tests { #[test] fn a_full_circle_is_four_quarter_curves() { - let (start, curves) = arc_curves(0.0, 0.0, 10.0, 0.0, 360.0); + let (start, curves, truncated) = arc_curves(0.0, 0.0, 10.0, 0.0, 360.0, MAX_PATH); + assert!(!truncated); assert!((start.0 - 10.0).abs() < 1e-9); assert_eq!(curves.len(), 4); let last = curves.last().unwrap(); assert!((last[4] - 10.0).abs() < 1e-9 && last[5].abs() < 1e-9); - assert!(arc_curves(0.0, 0.0, 1.0, 0.0, f64::INFINITY).1.is_empty()); - assert_eq!(arc_curves(0.0, 0.0, 1.0, 0.0, MAX_ARC_TURNS * 360.0).1.len(), MAX_ARC_SEGMENTS); + assert!(arc_curves(0.0, 0.0, 1.0, 0.0, f64::INFINITY, MAX_PATH).1.is_empty()); + assert_eq!(arc_curves(0.0, 0.0, 1.0, 0.0, MAX_ARC_TURNS * 360.0, MAX_PATH).1.len(), 4096); + assert!(arc_curves(0.0, 0.0, 2e17, 0.0, 360.0, 100).2); } #[test] diff --git a/crates/ps/src/read/interp.rs b/crates/ps/src/read/interp.rs index d43198fa18..35e272d448 100644 --- a/crates/ps/src/read/interp.rs +++ b/crates/ps/src/read/interp.rs @@ -85,10 +85,22 @@ impl Frame { } /// How much work and memory one EPS may use. +pub type FallbackFont = fn(&[u8]) -> Option<&'static [u8]>; + #[derive(Debug, Clone, Copy)] pub struct Limits { pub steps: u64, pub memory: usize, + /// Let hosts that historically ignored excess operands preserve that behavior. + pub stack_overflow_noop: bool, + /// Consume image data but leave drawing to hosts that historically skipped EPS images. + pub skip_images: bool, + /// Return an empty page plus a warning when execution fails before painting anything. + pub keep_unpainted: bool, + /// Match hosts that historically coerce zero divisors to a zero integer result. + pub zero_division_zero: bool, + /// Optional host font provider for stand-in fonts absent from an EPS file. + pub fallback_font: Option, } impl Default for Limits { @@ -96,7 +108,27 @@ impl Default for Limits { /// steps and 2 MB, one carrying a 15 MB CJK font 2 million steps and 30 MB. A program that /// runs into the step limit ends within a few seconds. Smaller on the web. fn default() -> Self { - if cfg!(target_arch = "wasm32") { Limits { steps: 20_000_000, memory: 256 << 20 } } else { Limits { steps: 50_000_000, memory: 1 << 30 } } + if cfg!(target_arch = "wasm32") { + Limits { + steps: 20_000_000, + memory: 256 << 20, + stack_overflow_noop: false, + skip_images: false, + keep_unpainted: false, + zero_division_zero: false, + fallback_font: None, + } + } else { + Limits { + steps: 50_000_000, + memory: 1 << 30, + stack_overflow_noop: false, + skip_images: false, + keep_unpainted: false, + zero_division_zero: false, + fallback_font: None, + } + } } } @@ -113,7 +145,8 @@ pub const SOURCE_BYTES: usize = 128; /// Bytes read, copied or scanned per step of work charged. pub const BYTES_PER_STEP: u64 = 16; -const MAX_OSTACK: usize = 500_000; +// Keep the operand limit in line with the EPS reader's established 100,000-value ceiling. +const MAX_OSTACK: usize = 100_000; const MAX_ESTACK: usize = 100_000; const MAX_DSTACK: usize = 1_000; const MAX_GSTACK: usize = 10_000; @@ -219,6 +252,9 @@ impl Interp { pub fn push(&mut self, o: Obj) -> R { if self.o.len() >= MAX_OSTACK { + if self.limits.stack_overflow_noop { + return Ok(()); + } return err("stackoverflow"); } self.o.push(o); @@ -238,6 +274,11 @@ impl Interp { if self.o.len() < n { err("stackunderflow") } else { Ok(()) } } + /// Whether an operator can append `n` values without exceeding the operand stack limit. + pub fn has_operand_room(&self, n: usize) -> bool { + self.o.len().checked_add(n).is_some_and(|len| len <= MAX_OSTACK) + } + pub fn pop_num(&mut self) -> R { match self.pop()? { o @ (Obj::Int(_) | Obj::Real(_)) => o.num().ok_or(PsErr::Ps("typecheck")), @@ -248,7 +289,7 @@ impl Interp { pub fn pop_int(&mut self) -> R { match self.pop()? { Obj::Int(i) => Ok(i), - Obj::Real(r) if r.fract() == 0.0 && r.abs() < 9.0e15 => Ok(r as i64), + Obj::Real(r) if r.is_finite() => Ok(r as i64), _ => err("typecheck"), } } diff --git a/crates/ps/src/read/mod.rs b/crates/ps/src/read/mod.rs index 52386a43f2..0fcc3b3634 100644 --- a/crates/ps/src/read/mod.rs +++ b/crates/ps/src/read/mod.rs @@ -28,7 +28,7 @@ mod type42; use craft_eps::{Comments, Kind, MAX_EXTENDED_HEADER, postscript_section as shared_postscript_section}; -pub use interp::Limits; +pub use interp::{FallbackFont, Limits}; /// A converted EPS. #[derive(Debug)] @@ -90,7 +90,7 @@ pub fn cached_pdf(eps: &[u8]) -> Option>> { } /// Run an EPS and write what it paints as PDF. `Err` when it isn't an EPS with a bounding box, -/// when the program fails before painting anything, or when it exceeds `limits`. +/// or when it fails before painting and `limits.keep_unpainted` is false. pub fn eps_to_pdf(bytes: &[u8], limits: Limits) -> Result { let ps = postscript_section(bytes); if !ps.starts_with(b"%!") { @@ -104,7 +104,7 @@ pub fn eps_to_pdf(bytes: &[u8], limits: Limits) -> Result { if let Err(e) = result { // What was painted before an error is kept (the rest of the page is lost, as on a // printer); with nothing painted, the caller falls back to the preview. - if !ip.out.painted { + if !ip.out.painted && !limits.keep_unpainted { return Err(e); } warnings.push(e); diff --git a/crates/ps/src/read/ops/basic.rs b/crates/ps/src/read/ops/basic.rs index 6760032ef8..00031cf441 100644 --- a/crates/ps/src/read/ops/basic.rs +++ b/crates/ps/src/read/ops/basic.rs @@ -26,8 +26,14 @@ pub fn copy(ip: &mut Interp) -> R { Obj::Int(n) => { let n = usize::try_from(n).map_err(|_| PsErr::Ps("rangecheck"))?; ip.need(n)?; + // EPS readers conventionally treat a copy that would exceed the operand limit as a + // no-op. Check before cloning or charging so a hostile count cannot partially mutate + // the stack or allocate a large temporary. + if !ip.has_operand_room(n) { + return Ok(()); + } let start = ip.o.len() - n; - ip.charge(n as u64)?; + ip.charge((n / 8) as u64)?; ip.alloc(n.saturating_mul(OBJ_BYTES))?; let items: Vec = ip.o.get(start..).map(<[Obj]>::to_vec).unwrap_or_default(); for o in items { @@ -206,14 +212,28 @@ pub fn div(ip: &mut Interp) -> R { pub fn idiv(ip: &mut Interp) -> R { let b = ip.pop_int()?; let a = ip.pop_int()?; - let v = a.checked_div(b).ok_or(PsErr::Ps("undefinedresult"))?; + if b == 0 { + if ip.limits.zero_division_zero { + return ip.push(Obj::Int(0)); + } + return err("undefinedresult"); + } + // The signed minimum divided by -1 overflows the host integer type. PostScript integer + // arithmetic wraps this one exceptional result to zero rather than raising an error. + let v = a.checked_div(b).unwrap_or(0); ip.push(Obj::Int(v)) } pub fn mod_(ip: &mut Interp) -> R { let b = ip.pop_int()?; let a = ip.pop_int()?; - let v = a.checked_rem(b).ok_or(PsErr::Ps("undefinedresult"))?; + if b == 0 { + if ip.limits.zero_division_zero { + return ip.push(Obj::Int(0)); + } + return err("undefinedresult"); + } + let v = a.checked_rem(b).unwrap_or(0); ip.push(Obj::Int(v)) } @@ -854,15 +874,15 @@ pub fn cleardictstack(ip: &mut Interp) -> R { pub fn array(ip: &mut Interp) -> R { let n = ip.pop_usize()?; - ip.alloc(n.saturating_mul(OBJ_BYTES))?; ip.charge(n as u64)?; + ip.alloc(n.saturating_mul(OBJ_BYTES))?; ip.push(Obj::Array(PArr::new(vec![Obj::Null; n]), false)) } pub fn string(ip: &mut Interp) -> R { let n = ip.pop_usize()?; - ip.alloc(n)?; ip.charge_bytes(n)?; + ip.alloc(n)?; ip.push(Obj::Str(PStr::new(vec![0; n]), false)) } diff --git a/crates/ps/src/read/ops/gfx.rs b/crates/ps/src/read/ops/gfx.rs index d1435f8c9b..17178008e9 100644 --- a/crates/ps/src/read/ops/gfx.rs +++ b/crates/ps/src/read/ops/gfx.rs @@ -318,7 +318,7 @@ pub fn idtransform(ip: &mut Interp) -> R { fn add_seg(ip: &mut Interp, s: Seg) -> R { if ip.gs.path.len() >= MAX_PATH { - return err("limitcheck"); + return Err(PsErr::Fatal("PostScript (path limit)".into())); } ip.gs.path.push(s); Ok(()) @@ -426,7 +426,9 @@ pub fn rcurveto(ip: &mut Interp) -> R { } pub fn closepath(ip: &mut Interp) -> R { - if ip.gs.cur.is_some() && !matches!(ip.gs.path.last(), Some(Seg::Close) | None) { + // Keep a Close element for every explicit operator call, as existing EPS consumers do. + // Consecutive closes paint identically, but each one counts toward bounded path storage. + if ip.gs.cur.is_some() { add_seg(ip, Seg::Close)?; ip.gs.cur = ip.gs.start; } @@ -444,7 +446,8 @@ fn arc_common(ip: &mut Interp, negative: bool) -> R { ip.warnings.push("arc (too many turns)".into()); } let start_angle = a0 % 360.0; - let (start, curves) = arc_curves(cx, cy, r, start_angle, start_angle + sweep); + let available = MAX_PATH.saturating_sub(ip.gs.path.len()); + let (start, curves, truncated) = arc_curves(cx, cy, r, start_angle, start_angle + sweep, available.saturating_sub(1)); let m = ip.gs.ctm; let s = m.apply(start.0, start.1); if ip.gs.cur.is_some() { @@ -452,9 +455,13 @@ fn arc_common(ip: &mut Interp, negative: bool) -> R { } else { move_dev(ip, s)? } + ip.charge(curves.len() as u64)?; for c in curves { curve_dev(ip, m.apply(c[0], c[1]), m.apply(c[2], c[3]), m.apply(c[4], c[5]))?; } + if truncated { + return Err(PsErr::Fatal("PostScript (path limit)".into())); + } Ok(()) } diff --git a/crates/ps/src/read/ops/image.rs b/crates/ps/src/read/ops/image.rs index 3d4db607c3..a58864d6d3 100644 --- a/crates/ps/src/read/ops/image.rs +++ b/crates/ps/src/read/ops/image.rs @@ -186,6 +186,10 @@ fn to_8bit(data: Vec, bpc: usize) -> Vec { fn paint(ip: &mut Interp, p: Params, space: Space) -> R { let comps = space.comps(); let (data, dct) = samples(ip, &p, comps)?; + if ip.limits.skip_images { + ip.warnings.push("image".into()); + return Ok(()); + } let cm = placement(ip, &p)?; if ip.gs.nulldev { return Ok(()); @@ -354,6 +358,10 @@ pub fn imagemask(ip: &mut Interp) -> R { }; let p = Params { bpc: 1, ..p }; let (data, _) = samples(ip, &p, 1)?; + if ip.limits.skip_images { + ip.warnings.push("image".into()); + return Ok(()); + } let cm = placement(ip, &p)?; if ip.gs.nulldev { return Ok(()); diff --git a/crates/ps/src/read/ops/misc.rs b/crates/ps/src/read/ops/misc.rs index 9542d859ea..3fa457d1fc 100644 --- a/crates/ps/src/read/ops/misc.rs +++ b/crates/ps/src/read/ops/misc.rs @@ -2,7 +2,7 @@ use crate::read::interp::{Interp, PsErr, R, err, matrix_of}; use crate::read::obj::{Key, Obj, PArr, PDict, name}; -use crate::read::source::{FilterParams, Source}; +use crate::read::source::{FilterParams, Kind, Source}; // ----- files ----- @@ -118,7 +118,26 @@ pub fn bytesavailable(ip: &mut Interp) -> R { pub fn closefile(ip: &mut Interp) -> R { let f = pop_file(ip)?; + let is_eexec = matches!(ip.srcs.list.get(f).map(|source| &source.kind), Some(Kind::Eexec { .. })); ip.srcs.close(f); + if is_eexec + && let Some(font) = ip.d.iter().rev().find(|dict| dict.get_name(b"FontName").is_some()).cloned() + && let Some(name) = font.get_name(b"FontName") + { + // Some embedded programs leave `/CharStrings dict` on the operand stack instead of + // installing it with `ND` before closing eexec. Attach that completed table to its font. + if font.get_name(b"CharStrings").is_none() + && let Some(chars) = ip.o.windows(2).rev().find_map(|pair| match pair { + [Obj::Name(key, _), Obj::Dict(chars)] if key.as_ref() == b"CharStrings" => Some(chars.clone()), + _ => None, + }) + { + font.put_name(b"CharStrings", Obj::Dict(chars)); + } + // Embedded Type 1 programs conventionally leave their completed font dictionary in + // the current dictionary stack; closefile marks the end of eexec, so make it findable. + ip.fontdir.put(name, Obj::Dict(font)); + } Ok(()) } @@ -188,6 +207,14 @@ pub fn eexec(ip: &mut Interp) -> R { _ => return err("typecheck"), }; let id = ip.srcs.add(Source::eexec(up)); + // A number of embedded Type 1 programs omit the usual `currentdict end` before eexec; + // their encrypted prolog starts with `dup /Private ...` and expects its font dictionary. + // Supply it only when the caller has not already left an operand on the stack. + if ip.o.is_empty() + && let Some(font) = ip.d.last() + { + ip.push(Obj::Dict(font.clone()))?; + } // eexec runs with systemdict on top, which its program removes with `end` (`closefile`). let sd = ip.systemdict.clone(); ip.begin(sd)?; @@ -245,7 +272,23 @@ pub fn filter(ip: &mut Interp) -> R { return ip.push(Obj::File(id)); } match ip.srcs.filter(up, &fname, ¶ms) { - Ok(id) => ip.push(Obj::File(id)), + Ok(mut id) => { + if fname == b"SubFileDecode" { + // EPS prologs commonly use `currentfile ... SubFileDecode filter pop` to skip + // inline data. Materialize the bounded filter so the interpreter resumes after + // its end marker rather than treating the payload as PostScript source. + let mut data = Vec::new(); + while let Some(c) = ip.srcs.read(id) { + if data.len() == data.capacity() { + data.try_reserve_exact(4096).map_err(|_| PsErr::Fatal("out of memory".into()))?; + } + ip.alloc(1)?; + data.push(c); + } + id = ip.srcs.add(Source::bytes(data)); + } + ip.push(Obj::File(id)) + } Err(e) => { ip.warnings.push(e); err("ioerror") diff --git a/crates/ps/src/read/ops/mod.rs b/crates/ps/src/read/ops/mod.rs index 299abb0cda..2c88d646d7 100644 --- a/crates/ps/src/read/ops/mod.rs +++ b/crates/ps/src/read/ops/mod.rs @@ -45,6 +45,8 @@ pub static OPS: &[(&[u8], OpFn)] = named! { "cvn" => basic::cvn, "cvs" => basic::cvs, "cvrs" => basic::cvrs, "bind" => basic::bind, // dictionaries "dict" => basic::dict, "maxlength" => basic::maxlength, "begin" => basic::begin, "end" => basic::end, "def" => basic::def, + // Conventional Type 1 font-prolog aliases. + "ND" => basic::def, "NP" => basic::put, "load" => basic::load, "store" => basic::store, "get" => basic::get, "put" => basic::put, "known" => basic::known, "undef" => basic::undef, "where" => basic::where_, "currentdict" => basic::currentdict, "countdictstack" => basic::countdictstack, "dictstack" => basic::dictstack, "cleardictstack" => basic::cleardictstack, diff --git a/crates/ps/src/read/pdf.rs b/crates/ps/src/read/pdf.rs index 6363475dd4..0faabe09a3 100644 --- a/crates/ps/src/read/pdf.rs +++ b/crates/ps/src/read/pdf.rs @@ -152,6 +152,15 @@ impl Out { } } + /// Preserve the semantic text for consumers that group outlined glyphs. + pub fn begin_actual_text(&mut self, text: &str) { + self.c.begin_marked_content_with_properties(Name(b"Span")).properties().actual_text(pdf_writer::TextStr(text)); + } + + pub fn end_actual_text(&mut self) { + self.c.end_marked_content(); + } + fn sync_clip(&mut self, clip: &Option>) -> Result<(), String> { let target = clip_chain(clip); let common = self.applied.iter().zip(&target).take_while(|(a, b)| Rc::ptr_eq(a, b)).count(); @@ -384,8 +393,9 @@ impl Out { pub fn finish(self, size: (f64, f64)) -> Vec { let mut pdf = Pdf::new(); - // Nothing newer is used (no transparency groups): PDF/X-1a files can take it. - pdf.set_version(1, 4); + // ActualText marked content is defined in PDF 1.5; the rest of this output uses basic + // page content, without transparency groups. + pdf.set_version(1, 5); let (catalog, pages, page, content) = (Ref::new(1), Ref::new(2), Ref::new(3), Ref::new(4)); let mut next = 5; let mut alloc = || { diff --git a/crates/ps/src/read/tests.rs b/crates/ps/src/read/tests.rs index ab352806cb..9836aa427f 100644 --- a/crates/ps/src/read/tests.rs +++ b/crates/ps/src/read/tests.rs @@ -75,6 +75,19 @@ fn procedures_loops_and_dictionaries_run() { assert!(c.contains("0.5 g") && c.contains("0.25 g"), "{c}"); } +#[test] +fn integer_division_overflow_returns_zero() { + let (c, _) = run("-9223372036854775808 -1 idiv 0 eq { 10 20 5 5 rectfill } if + -9223372036854775808 -1 mod 0 eq { 20 20 5 5 rectfill } if"); + assert_eq!(c.lines().filter(|line| *line == "f").count(), 2, "{c}"); +} + +#[test] +fn copy_past_operand_limit_is_a_noop() { + let (c, _) = run("10 20 5 5 rectfill 1 40 { count copy } repeat clear 20 20 5 5 rectfill"); + assert_eq!(c.lines().filter(|line| *line == "f").count(), 2, "{c}"); +} + #[test] fn transforms_curves_and_clips() { let (c, _) = run("gsave 60 45 translate 2 2 scale newpath 0 0 10 0 360 arc clip newpath -20 -20 40 40 rectfill grestore 10 20 5 5 rectfill"); @@ -120,6 +133,18 @@ fn a_string_made_a_file_runs_as_a_program() { assert!(c.contains("0.5 g"), "{c}"); } +#[test] +fn subfiledecode_materialization_respects_the_memory_budget() { + let payload = "x".repeat(8192); + let bytes = eps(&format!("0 0 10 10 rectfill currentfile 0 (END) /SubFileDecode filter pop\n{payload}END")); + let limits = Limits { memory: 4096, ..Limits::default() }; + let converted = eps_to_pdf(&bytes, limits).unwrap(); + assert!(converted.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", converted.warnings); + assert!(converted.memory <= limits.memory + 1, "{} bytes", converted.memory); + let c = content(&converted.pdf); + assert!(c.contains("0 0 m") && c.contains("10 10 l"), "{c}"); +} + #[test] fn eexec_sections_run() { // `(1 0 0 setrgbcolor 10 20 5 5 rectfill) ` encrypted with the eexec key, as hex. @@ -137,7 +162,15 @@ fn eexec_sections_run() { #[test] fn hostile_programs_end_with_an_error_not_a_hang_or_panic() { - let limits = Limits { steps: 200_000, memory: 64 << 20 }; + let limits = Limits { + steps: 200_000, + memory: 64 << 20, + stack_overflow_noop: false, + skip_images: false, + keep_unpainted: false, + zero_division_zero: false, + fallback_font: None, + }; for body in [ "{ } loop", "/f { f } def f", @@ -289,8 +322,9 @@ fn type42_fonts_draw_their_truetype_glyphs() { /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end definefont 20 scalefont setfont 10 20 moveto (AA) show" )); - // Two boxes, filled: 1 point (50 units at 20 pt) in, the second 12 points on (600 units). - assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + // One fill contains both boxes; 1 point (50 units at 20 pt) in, the second 12 points on. + assert_eq!(c.lines().filter(|l| *l == "f").count(), 1, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 1, "{c}"); assert!(c.contains("11 20 m") && c.contains("23 20 m"), "{c}"); } @@ -395,7 +429,8 @@ fn cff_fonts_from_startdata_draw_their_glyphs() { let c = content( &eps_to_pdf(&startdata_eps(&tiny_cff(false), "/T findfont 10 scalefont setfont 10 20 moveto (AA) show"), Limits::default()).unwrap().pdf, ); - assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 1, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 1, "{c}"); // The box starts 0.5 points (50 units) in; the second glyph 6 points (600 units) on. assert!(c.contains("10.5 20 m") && c.contains("16.5 20 m"), "{c}"); } @@ -408,7 +443,8 @@ fn cid_fonts_draw_through_cmaps() { endcmap CMapName currentdict /CMap defineresource pop end end /U /One-H [/T] composefont pop /U findfont 10 scalefont setfont 10 40 moveto (AA) show"; let c = content(&eps_to_pdf(&startdata_eps(&tiny_cff(true), body), Limits::default()).unwrap().pdf); - assert_eq!(c.lines().filter(|l| *l == "f").count(), 4, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 2, "{c}"); assert!(c.contains("16.5 20 m") && c.contains("16.5 40 m"), "{c}"); // Broken font data is a warning, not a failure. let r = eps_to_pdf(&startdata_eps(b"\x01\x00\x04\x04\xff\xff\xff", "0 0 1 1 rectfill"), Limits::default()).unwrap(); @@ -488,7 +524,15 @@ fn metrics_without_two_numbers_dont_underflow() { #[test] fn work_in_proportion_to_operands_counts_against_the_limits() { - let limits = Limits { steps: 200_000, memory: 8 << 20 }; + let limits = Limits { + steps: 200_000, + memory: 8 << 20, + stack_overflow_noop: false, + skip_images: false, + keep_unpainted: false, + zero_division_zero: false, + fallback_font: None, + }; let ends = |body: &str| { let r = eps_to_pdf(&eps(&format!("10 20 1 1 rectfill {body}")), limits).expect("what was painted is kept"); let w = r.warnings.join(" "); From a917a923bf926eec70711b2a72867d248ee259e6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 08:51:18 +0000 Subject: [PATCH 5/7] fix(ps): address interpreter review findings --- crates/ps/src/read/composite.rs | 2 +- crates/ps/src/read/font.rs | 281 +++++++++++++++++++++----------- crates/ps/src/read/ops/misc.rs | 20 ++- crates/ps/src/read/source.rs | 26 ++- crates/ps/src/read/tests.rs | 77 ++++++++- 5 files changed, 297 insertions(+), 109 deletions(-) diff --git a/crates/ps/src/read/composite.rs b/crates/ps/src/read/composite.rs index 06ae3bc778..dad0785d9c 100644 --- a/crates/ps/src/read/composite.rs +++ b/crates/ps/src/read/composite.rs @@ -358,7 +358,7 @@ fn compose(ip: &mut Interp, n: &Obj, cmap: &Obj, descendants: Vec) -> R Obj::Dict(d), k => match category(ip, b"CIDFont").get(&k.key()) { Some(d @ Obj::Dict(_)) => d, - _ => Obj::Dict(crate::read::font::find_font(ip, &k)), + _ => Obj::Dict(crate::read::font::find_font(ip, &k)?), }, }); } diff --git a/crates/ps/src/read/font.rs b/crates/ps/src/read/font.rs index 931c33dad8..c8d378b9fc 100644 --- a/crates/ps/src/read/font.rs +++ b/crates/ps/src/read/font.rs @@ -14,12 +14,12 @@ fn font_matrix(d: &PDict) -> M { } /// A stand-in for a font the file doesn't carry: a Type 1 shell with StandardEncoding. -fn substitute(ip: &mut Interp, font_name: &[u8]) -> PDict { +fn substitute(ip: &mut Interp, font_name: &[u8]) -> R { if let Some(bytes) = ip.limits.fallback_font.and_then(|provider| provider(font_name)) - && let Some(d) = substitute_type42(ip, font_name, bytes) + && let Some(d) = substitute_type42(ip, font_name, bytes)? { add_fid(ip, &d); - return d; + return Ok(d); } let d = PDict::new(12); d.put_name(b"FontName", Obj::lit(font_name)); @@ -34,13 +34,13 @@ fn substitute(ip: &mut Interp, font_name: &[u8]) -> PDict { d.put_name(b"Private", Obj::Dict(PDict::new(1))); d.put_name(b"CraftPsSubstitute", Obj::Bool(true)); add_fid(ip, &d); - d + Ok(d) } -fn substitute_type42(ip: &Interp, font_name: &[u8], bytes: &[u8]) -> Option { +fn substitute_type42(ip: &mut Interp, font_name: &[u8], bytes: &[u8]) -> R> { use skrifa::MetadataProvider; - let font = skrifa::FontRef::new(bytes).ok()?; + let Ok(font) = skrifa::FontRef::new(bytes) else { return Ok(None) }; let cmap = font.charmap(); let charstrings = PDict::new(256); for glyph_name in crate::read::encodings::standard() { @@ -48,6 +48,7 @@ fn substitute_type42(ip: &Interp, font_name: &[u8], bytes: &[u8]) -> Option Option Option { @@ -116,25 +117,79 @@ fn standard_glyph_char(name: &[u8]) -> Option { b"seven" => '7', b"eight" => '8', b"nine" => '9', + b"exclamdown" => '¡', + b"cent" => '¢', + b"sterling" => '£', + b"fraction" => '⁄', + b"yen" => '¥', + b"florin" => 'ƒ', + b"section" => '§', + b"currency" => '¤', + b"quotesingle" => '\'', + b"quotedblleft" => '“', + b"guillemotleft" => '«', + b"guilsinglleft" => '‹', + b"guilsinglright" => '›', + b"fi" => '\u{fb01}', + b"fl" => '\u{fb02}', + b"endash" => '–', + b"dagger" => '†', + b"daggerdbl" => '‡', + b"periodcentered" => '·', + b"paragraph" => '¶', + b"bullet" => '•', + b"quotesinglbase" => '‚', + b"quotedblbase" => '„', + b"quotedblright" => '”', + b"guillemotright" => '»', + b"ellipsis" => '…', + b"perthousand" => '‰', + b"questiondown" => '¿', + b"grave" => '`', + b"acute" => '´', + b"circumflex" => 'ˆ', + b"tilde" => '˜', + b"macron" => '¯', + b"breve" => '˘', + b"dotaccent" => '˙', + b"dieresis" => '¨', + b"ring" => '˚', + b"cedilla" => '¸', + b"hungarumlaut" => '˝', + b"ogonek" => '˛', + b"caron" => 'ˇ', + b"emdash" => '—', + b"AE" => 'Æ', + b"ordfeminine" => 'ª', + b"Lslash" => 'Ł', + b"Oslash" => 'Ø', + b"OE" => 'Œ', + b"ordmasculine" => 'º', + b"ae" => 'æ', + b"dotlessi" => 'ı', + b"lslash" => 'ł', + b"oslash" => 'ø', + b"oe" => 'œ', + b"germandbls" => 'ß', _ => return None, }) } -fn add_fid(ip: &mut Interp, d: &PDict) { +pub(crate) fn add_fid(ip: &mut Interp, d: &PDict) { if !d.known(&Key::Name(name(b"FID"))) { d.put_name(b"FID", Obj::FontId(ip.next_fid)); ip.next_fid += 1; } } -pub fn find_font(ip: &mut Interp, key: &Obj) -> PDict { +pub fn find_font(ip: &mut Interp, key: &Obj) -> R { if let Some(Obj::Dict(d)) = ip.fontdir.get(&key.key()) { - return d; + return Ok(d); } if let Some(Obj::Dict(fonts)) = ip.resources.get_name(b"Font") && let Some(Obj::Dict(d)) = fonts.get(&key.key()) { - return d; + return Ok(d); } // An embedded Type 1 program can leave its FontName dictionary active after eexec without // issuing definefont. Resolve that in-scope font before substituting a stand-in. @@ -145,20 +200,20 @@ pub fn find_font(ip: &mut Interp, key: &Obj) -> PDict { .cloned(); if let Some(d) = active { add_fid(ip, &d); - return d; + return Ok(d); } let n = crate::read::ops::basic::text_of(key); if let Some(d) = crate::read::composite::compose_by_name(ip, &n) { - return d; + return Ok(d); } - let d = substitute(ip, &n); + let d = substitute(ip, &n)?; ip.fontdir.put(key.clone(), Obj::Dict(d.clone())); - d + Ok(d) } pub fn findfont(ip: &mut Interp) -> R { let k = ip.pop()?; - let d = find_font(ip, &k); + let d = find_font(ip, &k)?; ip.push(Obj::Dict(d)) } @@ -211,7 +266,7 @@ pub fn setfont(ip: &mut Interp) -> R { pub fn currentfont(ip: &mut Interp) -> R { let f = match ip.gs.font.clone() { Some(f) => f, - None => find_font(ip, &Obj::lit(b"Courier")), + None => find_font(ip, &Obj::lit(b"Courier"))?, }; ip.push(Obj::Dict(f)) } @@ -227,7 +282,7 @@ pub fn selectfont(ip: &mut Interp) -> R { let k = ip.pop()?; let d = match k { Obj::Dict(d) => d, - k => find_font(ip, &k), + k => find_font(ip, &k)?, }; let f = transformed(ip, &d, &m); ip.gs.font = Some(f); @@ -245,6 +300,12 @@ fn glyph_name(font: &PDict, code: u8) -> Vec { } } +/// Unicode text represented by the simple font's selected glyph names. Unknown names have no +/// honest replacement text, so callers must omit ActualText rather than infer from input bytes. +fn actual_text(font: &PDict, bytes: &[u8]) -> Option { + bytes.iter().map(|code| standard_glyph_char(&glyph_name(font, *code)).map(|c| c.to_string())).collect() +} + /// A glyph's advance from the font's Metrics or CharStrings-free estimate, in glyph space. fn estimated_width(font: &PDict, code: u8) -> (f64, f64) { let g = glyph_name(font, code); @@ -383,89 +444,107 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & s.iter().map(|c| (u32::from(*c), font.clone(), fm, crate::read::composite::Key2::Code(*c))).collect() }; let mut total = (0.0, 0.0); - let mut text_path = Vec::new(); - for (i, (code, leaf, fm, key)) in jobs.into_iter().enumerate() { - let type3 = leaf.get_name(b"FontType").and_then(|o| o.num()) == Some(3.0); - let w = match key { - crate::read::composite::Key2::Code(c) if type3 => { - ip.charge(GLYPH_STEPS)?; - if draw && ip.gs.cur.is_some() { - type3_glyph(ip, &leaf, c)? - } else { - let saved = ip.gs.nulldev; - ip.gs.nulldev = true; - if ip.gs.cur.is_none() { - ip.gs.cur = Some((0.0, 0.0)); + let mut text_paths: Vec<(Vec, crate::read::gstate::DevColor)> = Vec::new(); + let mut text_segments = 0usize; + let result = (|| -> R<(f64, f64)> { + for (i, (code, leaf, fm, key)) in jobs.into_iter().enumerate() { + let type3 = leaf.get_name(b"FontType").and_then(|o| o.num()) == Some(3.0); + let w = match key { + crate::read::composite::Key2::Code(c) if type3 => { + ip.charge(GLYPH_STEPS)?; + if draw && ip.gs.cur.is_some() { + type3_glyph(ip, &leaf, c)? + } else { + let saved = ip.gs.nulldev; + ip.gs.nulldev = true; + if ip.gs.cur.is_none() { + ip.gs.cur = Some((0.0, 0.0)); + } + let w = type3_glyph(ip, &leaf, c); + ip.gs.nulldev = saved; + w? } - let w = type3_glyph(ip, &leaf, c); - ip.gs.nulldev = saved; - w? } - } - key => { - let cid = match key { - crate::read::composite::Key2::Cid(cid) => Some(cid), - crate::read::composite::Key2::Code(_) => None, - }; - let g = match key { - crate::read::composite::Key2::Code(c) => outline(ip, &leaf, c), - crate::read::composite::Key2::Cid(cid) => cid_outline(ip, &leaf, cid), - }; - // Empty outlines (usually spaces) still advance but need little work; their - // outline cache/decoder has already charged what it actually did. - ip.charge(if g.as_ref().is_some_and(|g| g.path.is_empty()) { 1 } else { GLYPH_STEPS })?; - // Vertical writing: the glyph hangs from its vertical origin and advances down. - let em = 1.0 / font_matrix(&leaf).0[3].abs().max(1e-9); - let vmetrics = match (vertical, &g) { - (true, Some(g)) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), g.width.0, em)), - (true, None) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), em, em)), - _ => None, - }; - let origin = vmetrics.map_or((0.0, 0.0), |(_, v)| v); - match g { - Some(g) => { - if draw && !g.path.is_empty() && ip.gs.cur.is_some() { - ip.charge_path(g.path.len())?; - let m = glyph_to_device(ip, &fm, origin)?; - let path: Vec = g.path.iter().map(|s| s.map(&m)).collect(); - if ip.charpath { - if ip.gs.path.len().saturating_add(path.len()) > crate::read::gstate::MAX_PATH { - return Err(PsErr::Fatal("PostScript (path limit)".into())); + key => { + let cid = match key { + crate::read::composite::Key2::Cid(cid) => Some(cid), + crate::read::composite::Key2::Code(_) => None, + }; + let g = match key { + crate::read::composite::Key2::Code(c) => outline(ip, &leaf, c), + crate::read::composite::Key2::Cid(cid) => cid_outline(ip, &leaf, cid), + }; + // Empty outlines (usually spaces) still advance but need little work; their + // outline cache/decoder has already charged what it actually did. + ip.charge(if g.as_ref().is_some_and(|g| g.path.is_empty()) { 1 } else { GLYPH_STEPS })?; + // Vertical writing: the glyph hangs from its vertical origin and advances down. + let em = 1.0 / font_matrix(&leaf).0[3].abs().max(1e-9); + let vmetrics = match (vertical, &g) { + (true, Some(g)) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), g.width.0, em)), + (true, None) => Some(crate::read::composite::vertical_metrics(&leaf, cid.unwrap_or(0), em, em)), + _ => None, + }; + let origin = vmetrics.map_or((0.0, 0.0), |(_, v)| v); + match g { + Some(g) => { + if draw && !g.path.is_empty() && ip.gs.cur.is_some() { + ip.charge_path(g.path.len())?; + let m = glyph_to_device(ip, &fm, origin)?; + let path: Vec = g.path.iter().map(|s| s.map(&m)).collect(); + if ip.charpath { + if ip.gs.path.len().saturating_add(path.len()) > crate::read::gstate::MAX_PATH { + return Err(PsErr::Fatal("PostScript (path limit)".into())); + } + ip.gs.path.extend(path); + } else { + if text_segments.saturating_add(path.len()) > crate::read::gstate::MAX_PATH { + return Err(PsErr::Fatal("PostScript (path limit)".into())); + } + text_segments += path.len(); + if matches!(ip.gs.space, crate::read::gstate::Space::Pattern) { + // Pattern fills execute a cell program. Keep them on the normal + // path until their deferred representation is prepared. + crate::read::ops::gfx::fill_path(ip, &path, false)?; + } else { + let color = crate::read::ops::gfx::current_color(ip)?; + // Charge the fill work before staging it so completed glyphs + // can still be committed if a later glyph hits the budget. + ip.charge(path.len() as u64 / 4 + 1)?; + text_paths.push((path, color)); + } } - ip.gs.path.extend(path); - } else { - if text_path.len().saturating_add(path.len()) > crate::read::gstate::MAX_PATH { - return Err(PsErr::Fatal("PostScript (path limit)".into())); - } - text_path.extend(path); + } + match (vmetrics, leaf.get_name(b"Metrics"), key_code(code)) { + (Some((w1, _)), _, _) => w1, + (None, Some(_), Some(c)) => estimated_width(&leaf, c), + _ => g.width, } } - match (vmetrics, leaf.get_name(b"Metrics"), key_code(code)) { - (Some((w1, _)), _, _) => w1, - (None, Some(_), Some(c)) => estimated_width(&leaf, c), - _ => g.width, - } + None => match vmetrics { + Some((w1, _)) => w1, + None => estimated_width(&leaf, key_code(code).unwrap_or(0)), + }, } - None => match vmetrics { - Some((w1, _)) => w1, - None => estimated_width(&leaf, key_code(code).unwrap_or(0)), - }, } + }; + let (wx, wy) = fm.delta(w.0, w.1); + let (ex, ey) = per_char(ip, code, i)?; + let (dx, dy) = if replace { (ex, ey) } else { (wx + ex, wy + ey) }; + total = (total.0 + dx, total.1 + dy); + if draw && let Some((cx, cy)) = ip.gs.cur { + let (ddx, ddy) = ip.gs.ctm.delta(dx, dy); + ip.gs.cur = Some((cx + ddx, cy + ddy)); } - }; - let (wx, wy) = fm.delta(w.0, w.1); - let (ex, ey) = per_char(ip, code, i)?; - let (dx, dy) = if replace { (ex, ey) } else { (wx + ex, wy + ey) }; - total = (total.0 + dx, total.1 + dy); - if draw && let Some((cx, cy)) = ip.gs.cur { - let (ddx, ddy) = ip.gs.ctm.delta(dx, dy); - ip.gs.cur = Some((cx + ddx, cy + ddy)); + } + Ok(total) + })(); + let path_limit = matches!(&result, Err(PsErr::Fatal(message)) if message == "PostScript (path limit)"); + if result.is_ok() || !path_limit { + for (path, color) in text_paths { + ip.out.fill(&ip.gs.clip, &path, false, color).map_err(PsErr::Fatal)?; } } - if draw && !ip.charpath && !text_path.is_empty() { - crate::read::ops::gfx::fill_path(ip, &text_path, false)?; - } - Ok(total) + result } /// What showing one glyph costs, in steps (it takes about ten times a plain operator), and the @@ -480,8 +559,11 @@ fn key_code(code: u32) -> Option { fn show_string(ip: &mut Interp, s: &[u8], draw: bool, per_char: &mut PerGlyph<'_>) -> R<(f64, f64)> { // Whitespace-only shows advance the text position but emit no painted glyphs; avoid bloating // the content stream with an empty semantic marked-content wrapper for them. - if draw && !ip.charpath && s.iter().any(|b| !b.is_ascii_whitespace()) { - let text: String = s.iter().map(|b| char::from(*b)).collect(); + let text = ip.gs.font.as_ref().and_then(|font| actual_text(font, s)); + if draw + && !ip.charpath + && let Some(text) = text.filter(|text| text.chars().any(|c| !c.is_whitespace())) + { ip.out.begin_actual_text(&text); let result = show_glyphs(ip, s, draw, false, per_char); ip.out.end_actual_text(); @@ -644,3 +726,16 @@ pub fn setcachedevice2(ip: &mut Interp) -> R { } setcharwidth(ip) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn standard_encoding_glyph_names_have_unicode_text() { + assert_eq!(standard_glyph_char(b"emdash"), Some('—')); + assert_eq!(standard_glyph_char(b"fi"), Some('\u{fb01}')); + assert_eq!(standard_glyph_char(b"sterling"), Some('£')); + assert_eq!(standard_glyph_char(b"unmapped.custom"), None); + } +} diff --git a/crates/ps/src/read/ops/misc.rs b/crates/ps/src/read/ops/misc.rs index 3fa457d1fc..662a1540e8 100644 --- a/crates/ps/src/read/ops/misc.rs +++ b/crates/ps/src/read/ops/misc.rs @@ -126,16 +126,18 @@ pub fn closefile(ip: &mut Interp) -> R { { // Some embedded programs leave `/CharStrings dict` on the operand stack instead of // installing it with `ND` before closing eexec. Attach that completed table to its font. - if font.get_name(b"CharStrings").is_none() - && let Some(chars) = ip.o.windows(2).rev().find_map(|pair| match pair { + if font.get_name(b"CharStrings").is_none() { + ip.charge(ip.o.len() as u64)?; + if let Some(chars) = ip.o.windows(2).rev().find_map(|pair| match pair { [Obj::Name(key, _), Obj::Dict(chars)] if key.as_ref() == b"CharStrings" => Some(chars.clone()), _ => None, - }) - { - font.put_name(b"CharStrings", Obj::Dict(chars)); + }) { + font.put_name(b"CharStrings", Obj::Dict(chars)); + } } // Embedded Type 1 programs conventionally leave their completed font dictionary in // the current dictionary stack; closefile marks the end of eexec, so make it findable. + crate::read::font::add_fid(ip, &font); ip.fontdir.put(name, Obj::Dict(font)); } Ok(()) @@ -210,7 +212,13 @@ pub fn eexec(ip: &mut Interp) -> R { // A number of embedded Type 1 programs omit the usual `currentdict end` before eexec; // their encrypted prolog starts with `dup /Private ...` and expects its font dictionary. // Supply it only when the caller has not already left an operand on the stack. + let font_prolog = ip.d.last().is_some_and(|font| { + font.get_name(b"FontName").is_some() + && font.get_name(b"FontType").and_then(|value| value.num()) == Some(1.0) + && font.get_name(b"FontMatrix").is_some() + }); if ip.o.is_empty() + && font_prolog && let Some(font) = ip.d.last() { ip.push(Obj::Dict(font.clone()))?; @@ -550,7 +558,7 @@ pub fn findresource(ip: &mut Interp) -> R { let cat = ip.pop_name()?; let key = ip.pop()?; if cat == b"Font" { - let f = crate::read::font::find_font(ip, &key); + let f = crate::read::font::find_font(ip, &key)?; return ip.push(Obj::Dict(f)); } if cat == b"Category" { diff --git a/crates/ps/src/read/source.rs b/crates/ps/src/read/source.rs index e87153cbe8..db15b09a27 100644 --- a/crates/ps/src/read/source.rs +++ b/crates/ps/src/read/source.rs @@ -420,13 +420,15 @@ impl Sources { continue; } if c == b'<' && first { - // A leading `<~`. - if self.read_depth(up, depth + 1) == Some(b'~') { + // A leading `<~` is optional; `<` is itself a valid ASCII85 digit. + let next = self.read_depth(up, depth + 1); + if next == Some(b'~') { first = false; continue; } - more = false; - break; + if next.is_some() { + self.unread(up); + } } first = false; if c == b'~' { @@ -517,6 +519,12 @@ impl Sources { break; } count -= 1; + if count == 0 { + // Positive EODCount includes the Nth terminating occurrence. + produced.extend(window.drain(..)); + more = false; + break; + } } while window.len() > eod.len() { if let Some(b) = window.pop_front() { @@ -827,6 +835,11 @@ mod tests { let f = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); assert_eq!(all(&mut s, f), b"Hello World!"); assert_eq!(all(&mut s, id), b"rest"); + let id = s.add(Source::bytes(b"rest".to_vec())); + let f = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); + let value = 27u32 * 85u32.pow(4); + assert_eq!(all(&mut s, f), value.to_be_bytes()); + assert_eq!(all(&mut s, id), b"rest"); let id = s.add(Source::bytes(vec![2, b'a', b'b', b'c', 254, b'x', 128, b'!'])); let f = s.filter(id, b"RunLengthDecode", &FilterParams::default()).unwrap(); assert_eq!(all(&mut s, f), b"abcxxx"); @@ -836,6 +849,11 @@ mod tests { let f = s.filter(id, b"SubFileDecode", &p).unwrap(); assert_eq!(all(&mut s, f), b"data"); assert_eq!(all(&mut s, id), b" tail"); + let id = s.add(Source::bytes(b"aENDtailENDrest".to_vec())); + let p = FilterParams { eod_count: 1, eod_string: b"END".to_vec(), ..Default::default() }; + let f = s.filter(id, b"SubFileDecode", &p).unwrap(); + assert_eq!(all(&mut s, f), b"aEND"); + assert_eq!(all(&mut s, id), b"tailENDrest"); } fn ascii85(data: &[u8]) -> String { diff --git a/crates/ps/src/read/tests.rs b/crates/ps/src/read/tests.rs index 9836aa427f..d612eeb5e4 100644 --- a/crates/ps/src/read/tests.rs +++ b/crates/ps/src/read/tests.rs @@ -160,6 +160,25 @@ fn eexec_sections_run() { assert!(c.contains("1 0 0 rg") && c.contains("0 1 0 rg"), "the program carries on after the section: {c}"); } +#[test] +fn completed_embedded_type1_fonts_get_a_fid_when_registered() { + let plain = b"pop currentfile closefile\n"; + let mut r: u16 = 55665; + let mut hex = String::new(); + for &p in [1u8, 2, 3, 4].iter().chain(plain.iter()) { + let c = p ^ (r >> 8) as u8; + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + hex.push_str(&format!("{c:02x}")); + } + let body = format!( + "/T 6 dict dup begin /FontName /T def /FontType 1 def /FontMatrix [0.001 0 0 0.001 0 0] def /Private 1 dict def end def + T begin currentfile eexec\n{hex}\nT end + T /FID known {{ 1 0 0 setrgbcolor 10 20 5 5 rectfill }} if" + ); + let (content, _) = run(&body); + assert!(content.contains("1 0 0 rg"), "the recovered font has a FID: {content}"); +} + #[test] fn hostile_programs_end_with_an_error_not_a_hang_or_panic() { let limits = Limits { @@ -311,6 +330,54 @@ fn tiny_ttf() -> Vec { out } +fn fallback_tiny_ttf(_: &[u8]) -> Option<&'static [u8]> { + static FONT: std::sync::OnceLock> = std::sync::OnceLock::new(); + Some(FONT.get_or_init(tiny_ttf).as_slice()) +} + +#[test] +fn fallback_font_bytes_are_charged_before_copying() { + let limits = Limits { memory: 32, fallback_font: Some(fallback_tiny_ttf), ..Limits::default() }; + let error = eps_to_pdf(&eps("/Missing findfont pop 0 0 1 1 rectfill"), limits).unwrap_err(); + assert_eq!(error, "out of memory"); +} + +#[test] +fn actual_text_uses_the_selected_encoding_and_omits_unknown_names() { + let (content, _) = run("/F /Helvetica findfont def F /Encoding get 65 /B put F 10 scalefont setfont 10 20 moveto (A) show"); + assert!(content.contains("/ActualText (B)"), "{content}"); + + let (content, _) = run("/F /Helvetica findfont def F /Encoding get 65 /customGlyph put F 10 scalefont setfont 10 20 moveto (A) show"); + assert!(!content.contains("/ActualText"), "{content}"); +} + +#[test] +fn text_paint_keeps_completed_glyphs_when_a_later_glyph_hits_the_work_limit() { + let ttf = tiny_ttf(); + let sfnts: String = ttf.chunks(64).map(|c| format!("<{}>\n", c.iter().map(|b| format!("{b:02x}")).collect::())).collect(); + let body = format!( + "/T42 11 dict dup begin /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /PaintType 0 def /FontBBox [0 0 1 1] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end definefont + 20 scalefont setfont 10 20 moveto (AA) show" + ); + let bytes = eps(&body); + let complete = eps_to_pdf(&bytes, Limits::default()).unwrap(); + let mut found_prefix = false; + for steps in (complete.steps.saturating_sub(100))..complete.steps { + let limits = Limits { steps, keep_unpainted: true, ..Limits::default() }; + let Ok(converted) = eps_to_pdf(&bytes, limits) else { continue }; + if converted.warnings.iter().any(|warning| warning == "the program runs too long") { + let content = content(&converted.pdf); + if content.lines().filter(|line| *line == "f").count() == 1 { + found_prefix = true; + break; + } + } + } + assert!(found_prefix, "no partial PDF retained the first completed glyph"); +} + #[test] fn type42_fonts_draw_their_truetype_glyphs() { let ttf = tiny_ttf(); @@ -322,8 +389,8 @@ fn type42_fonts_draw_their_truetype_glyphs() { /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end definefont 20 scalefont setfont 10 20 moveto (AA) show" )); - // One fill contains both boxes; 1 point (50 units at 20 pt) in, the second 12 points on. - assert_eq!(c.lines().filter(|l| *l == "f").count(), 1, "{c}"); + // Each glyph is filled independently; 1 point (50 units at 20 pt) in, the second 12 points on. + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 1, "{c}"); assert!(c.contains("11 20 m") && c.contains("23 20 m"), "{c}"); } @@ -429,7 +496,7 @@ fn cff_fonts_from_startdata_draw_their_glyphs() { let c = content( &eps_to_pdf(&startdata_eps(&tiny_cff(false), "/T findfont 10 scalefont setfont 10 20 moveto (AA) show"), Limits::default()).unwrap().pdf, ); - assert_eq!(c.lines().filter(|l| *l == "f").count(), 1, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 1, "{c}"); // The box starts 0.5 points (50 units) in; the second glyph 6 points (600 units) on. assert!(c.contains("10.5 20 m") && c.contains("16.5 20 m"), "{c}"); @@ -443,8 +510,8 @@ fn cid_fonts_draw_through_cmaps() { endcmap CMapName currentdict /CMap defineresource pop end end /U /One-H [/T] composefont pop /U findfont 10 scalefont setfont 10 40 moveto (AA) show"; let c = content(&eps_to_pdf(&startdata_eps(&tiny_cff(true), body), Limits::default()).unwrap().pdf); - assert_eq!(c.lines().filter(|l| *l == "f").count(), 2, "{c}"); - assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 2, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "f").count(), 4, "{c}"); + assert_eq!(c.lines().filter(|l| *l == "EMC").count(), 0, "{c}"); assert!(c.contains("16.5 20 m") && c.contains("16.5 40 m"), "{c}"); // Broken font data is a warning, not a failure. let r = eps_to_pdf(&startdata_eps(b"\x01\x00\x04\x04\xff\xff\xff", "0 0 1 1 rectfill"), Limits::default()).unwrap(); From 639f904fce16c958b190bfc91c4d2582d67bd53b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 09:20:15 +0000 Subject: [PATCH 6/7] fix(ps): preserve composite glyph paint order --- crates/ps/src/read/font.rs | 5 ++++ crates/ps/src/read/tests.rs | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/ps/src/read/font.rs b/crates/ps/src/read/font.rs index c8d378b9fc..9a08452427 100644 --- a/crates/ps/src/read/font.rs +++ b/crates/ps/src/read/font.rs @@ -451,6 +451,11 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & let type3 = leaf.get_name(b"FontType").and_then(|o| o.num()) == Some(3.0); let w = match key { crate::read::composite::Key2::Code(c) if type3 => { + // Type 3 glyph procedures paint immediately. Commit any earlier staged + // outlines first so their paint order remains the same as the input string. + for (path, color) in std::mem::take(&mut text_paths) { + ip.out.fill(&ip.gs.clip, &path, false, color).map_err(PsErr::Fatal)?; + } ip.charge(GLYPH_STEPS)?; if draw && ip.gs.cur.is_some() { type3_glyph(ip, &leaf, c)? diff --git a/crates/ps/src/read/tests.rs b/crates/ps/src/read/tests.rs index d612eeb5e4..0ffe4f934c 100644 --- a/crates/ps/src/read/tests.rs +++ b/crates/ps/src/read/tests.rs @@ -126,6 +126,34 @@ fn type3_fonts_draw_their_glyphs() { assert!(c.contains("22 20 m"), "{c}"); } +#[test] +fn type3_glyph_paints_after_prior_outline_glyphs() { + let ttf = tiny_ttf(); + let sfnts: String = ttf.chunks(64).map(|c| format!("<{}>\n", c.iter().map(|b| format!("{b:02x}")).collect::())).collect(); + let (c, _) = run(&format!( + "/T42 11 dict dup begin /FontType 42 def /FontMatrix [0.001 0 0 0.001 0 0] def /PaintType 0 def /FontBBox [0 0 1 1] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end /T42 exch definefont pop + /T3 8 dict dup begin /FontType 3 def /FontMatrix [0.001 0 0 0.001 0 0] def /FontBBox [0 0 1000 700] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /BuildChar {{ pop pop 1000 0 0 0 1000 700 setcachedevice 1 setgray 0 0 1000 700 rectfill }} def end /T3 exch definefont pop + /T0 6 dict dup begin /FontType 0 def /FMapType 2 def /FontMatrix [1 0 0 1 0 0] def + /FDepVector [ /T42 findfont /T3 findfont ] def end /T0 exch definefont pop + /T0 findfont 20 scalefont setfont 10 10 moveto <00410141> show" + )); + let lines: Vec<&str> = c.lines().collect(); + let first_fill = lines.iter().position(|line| *line == "f").unwrap_or_else(|| panic!("outline fill missing: {c}")); + let white_paint = lines.iter().position(|line| *line == "1 g").unwrap_or_else(|| panic!("Type 3 white paint missing: {c}")); + let second_fill = lines + .iter() + .enumerate() + .skip(white_paint + 1) + .find(|(_, line)| **line == "f") + .map(|(index, _)| index) + .unwrap_or_else(|| panic!("Type 3 fill missing: {c}")); + assert!(first_fill < white_paint && white_paint < second_fill, "glyph paints are out of order: {c}"); +} + #[test] fn a_string_made_a_file_runs_as_a_program() { // Adobe's idiom: `(code) 0 () /SubFileDecode filter cvx exec`. @@ -179,6 +207,29 @@ fn completed_embedded_type1_fonts_get_a_fid_when_registered() { assert!(content.contains("1 0 0 rg"), "the recovered font has a FID: {content}"); } +#[test] +fn repeated_eexec_font_searches_charge_the_operand_stack_work() { + let plain = b"pop end currentfile closefile\n"; + let mut r: u16 = 55665; + let mut hex = String::new(); + for &p in [1u8, 2, 3, 4].iter().chain(plain.iter()) { + let c = p ^ (r >> 8) as u8; + r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719); + hex.push_str(&format!("{c:02x}")); + } + let mut body = format!( + "/T 6 dict dup begin /FontName /T def /FontType 1 def /FontMatrix [0.001 0 0 0.001 0 0] def /Private 1 dict def end def\nT begin {} ", + "0 ".repeat(4096) + ); + for _ in 0..16 { + body.push_str(&format!("currentfile eexec\n{hex}\n")); + } + let limits = Limits { steps: 20_000, keep_unpainted: true, ..Limits::default() }; + let converted = eps_to_pdf(&eps(&body), limits).expect("bounded recovery keeps completed output"); + assert!(converted.warnings.iter().any(|warning| warning == "the program runs too long"), "{:?}", converted.warnings); + assert!(converted.steps >= limits.steps, "stack search work was not charged: {}", converted.steps); +} + #[test] fn hostile_programs_end_with_an_error_not_a_hang_or_panic() { let limits = Limits { From 4fed25150713ffd6e38de9237084c1b935c4ecd1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 09:52:54 +0000 Subject: [PATCH 7/7] fix(ps): address interpreter review findings --- crates/ps/src/read/font.rs | 15 ++++++--- crates/ps/src/read/gstate.rs | 27 ++++++++++----- crates/ps/src/read/interp.rs | 28 +++++++++++++-- crates/ps/src/read/ops/basic.rs | 15 +++++---- crates/ps/src/read/ops/gfx.rs | 12 +++++-- crates/ps/src/read/ops/misc.rs | 35 ++++++++----------- crates/ps/src/read/source.rs | 48 ++++++++++++++++---------- crates/ps/src/read/tests.rs | 60 +++++++++++++++++++++++++++++---- 8 files changed, 167 insertions(+), 73 deletions(-) diff --git a/crates/ps/src/read/font.rs b/crates/ps/src/read/font.rs index 9a08452427..9a1af3fb63 100644 --- a/crates/ps/src/read/font.rs +++ b/crates/ps/src/read/font.rs @@ -81,7 +81,7 @@ fn standard_glyph_char(name: &[u8]) -> Option { b"dollar" => '$', b"percent" => '%', b"ampersand" => '&', - b"quoteright" => '\'', + b"quoteright" => '’', b"parenleft" => '(', b"parenright" => ')', b"asterisk" => '*', @@ -102,7 +102,7 @@ fn standard_glyph_char(name: &[u8]) -> Option { b"bracketright" => ']', b"asciicircum" => '^', b"underscore" => '_', - b"quoteleft" => '`', + b"quoteleft" => '‘', b"braceleft" => '{', b"bar" => '|', b"braceright" => '}', @@ -453,8 +453,11 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & crate::read::composite::Key2::Code(c) if type3 => { // Type 3 glyph procedures paint immediately. Commit any earlier staged // outlines first so their paint order remains the same as the input string. - for (path, color) in std::mem::take(&mut text_paths) { - ip.out.fill(&ip.gs.clip, &path, false, color).map_err(PsErr::Fatal)?; + let pending = std::mem::take(&mut text_paths); + if !ip.gs.nulldev { + for (path, color) in pending { + ip.out.fill(&ip.gs.clip, &path, false, color).map_err(PsErr::Fatal)?; + } } ip.charge(GLYPH_STEPS)?; if draw && ip.gs.cur.is_some() { @@ -544,7 +547,7 @@ fn show_glyphs(ip: &mut Interp, s: &[u8], draw: bool, replace: bool, per_char: & Ok(total) })(); let path_limit = matches!(&result, Err(PsErr::Fatal(message)) if message == "PostScript (path limit)"); - if result.is_ok() || !path_limit { + if (result.is_ok() || !path_limit) && !ip.gs.nulldev { for (path, color) in text_paths { ip.out.fill(&ip.gs.clip, &path, false, color).map_err(PsErr::Fatal)?; } @@ -741,6 +744,8 @@ mod tests { assert_eq!(standard_glyph_char(b"emdash"), Some('—')); assert_eq!(standard_glyph_char(b"fi"), Some('\u{fb01}')); assert_eq!(standard_glyph_char(b"sterling"), Some('£')); + assert_eq!(standard_glyph_char(b"quoteleft"), Some('‘')); + assert_eq!(standard_glyph_char(b"quoteright"), Some('’')); assert_eq!(standard_glyph_char(b"unmapped.custom"), None); } } diff --git a/crates/ps/src/read/gstate.rs b/crates/ps/src/read/gstate.rs index 4a9eaf06d2..2b62f91800 100644 --- a/crates/ps/src/read/gstate.rs +++ b/crates/ps/src/read/gstate.rs @@ -104,24 +104,33 @@ pub fn bounded_arc_sweep(a0: f64, a1: f64, negative: bool) -> (f64, bool) { (sign * (max - 720.0 + remainder), true) } -/// Bézier segments for an arc of the circle at (cx, cy) with radius r, from angle a0 to a1 -/// (degrees), in user space: the first point and the curves. -pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, max_segments: usize) -> ((f64, f64), Vec<[f64; 6]>, bool) { - let (a0, a1) = (a0.to_radians(), a1.to_radians()); - let start = (cx + r * a0.cos(), cy + r * a0.sin()); - let sweep = a1 - a0; +/// How many Bézier segments an arc needs, or `None` if it exceeds `max_segments`. +pub fn arc_curve_count(r: f64, a0: f64, a1: f64, max_segments: usize) -> Option { + let sweep = a1.to_radians() - a0.to_radians(); if !sweep.is_finite() || sweep == 0.0 { - return (start, Vec::new(), false); + return Some(0); } // The established EPS reader uses a 0.1-unit curve tolerance. A quarter-circle cubic has // about 0.00027253*r maximum radial error; subdivision error falls with the sixth power of // the segment angle. Estimate before allocation so huge radii stop at the path bound. let subdivisions = (r.abs() * 0.00027253 / 0.1).max(1.0).powf(1.0 / 6.0).ceil(); let required = (sweep.abs() / std::f64::consts::FRAC_PI_2 * subdivisions).ceil().max(1.0); - if !required.is_finite() || required > max_segments as f64 { + (required.is_finite() && required <= max_segments as f64).then_some(required as usize) +} + +/// Bézier segments for an arc of the circle at (cx, cy) with radius r, from angle a0 to a1 +/// (degrees), in user space: the first point and the curves. +pub fn arc_curves(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, max_segments: usize) -> ((f64, f64), Vec<[f64; 6]>, bool) { + let count = arc_curve_count(r, a0, a1, max_segments); + let (a0, a1) = (a0.to_radians(), a1.to_radians()); + let start = (cx + r * a0.cos(), cy + r * a0.sin()); + let Some(n) = count else { return (start, Vec::new(), true); + }; + let sweep = a1 - a0; + if n == 0 || !sweep.is_finite() { + return (start, Vec::new(), false); } - let n = required as usize; let step = sweep / n as f64; let k = 4.0 / 3.0 * (step / 4.0).tan(); let mut out = Vec::with_capacity(n); diff --git a/crates/ps/src/read/interp.rs b/crates/ps/src/read/interp.rs index 35e272d448..c03fad1ce7 100644 --- a/crates/ps/src/read/interp.rs +++ b/crates/ps/src/read/interp.rs @@ -228,7 +228,7 @@ impl Interp { out: Out::new(origin), limits, steps: 0, - memory: 0, + memory: SOURCE_BYTES, read_charged: 0, written_charged: 0, depth: 0, @@ -360,6 +360,29 @@ impl Interp { if self.memory > self.limits.memory { Err(PsErr::Fatal("out of memory".into())) } else { Ok(()) } } + /// Register a retained file source against the memory bound. + pub fn add_source(&mut self, source: Source) -> R { + self.alloc(SOURCE_BYTES)?; + Ok(self.srcs.add(source)) + } + + /// Consume a SubFileDecode stream whose file object is explicitly discarded with `pop`. + pub fn discard_subfile(&mut self, id: usize) -> R { + if !self.srcs.is_subfile_filter(id) { + return Ok(()); + } + let mut chunk = 0u64; + while self.srcs.read(id).is_some() { + chunk += 1; + if chunk == BYTES_PER_STEP { + self.read_charged = self.read_charged.saturating_add(chunk); + self.charge(1)?; + chunk = 0; + } + } + if self.srcs.exhausted { Err(PsErr::Fatal("the program reads too much data".into())) } else { Ok(()) } + } + // ----- dictionaries ----- pub fn lookup(&self, k: &Key) -> Option { @@ -420,8 +443,7 @@ impl Interp { Obj::Op(op) => self.call_op(op), Obj::Str(s, true) => { // A string runs as a file reading it in place. - self.alloc(SOURCE_BYTES)?; - let id = self.srcs.add(Source::string(&s)); + let id = self.add_source(Source::string(&s))?; self.push_frame(Frame::File { id }) } Obj::File(id) => self.push_frame(Frame::File { id }), diff --git a/crates/ps/src/read/ops/basic.rs b/crates/ps/src/read/ops/basic.rs index 00031cf441..887c324e97 100644 --- a/crates/ps/src/read/ops/basic.rs +++ b/crates/ps/src/read/ops/basic.rs @@ -6,7 +6,10 @@ use crate::read::obj::{Key, Obj, PArr, PDict, PStr, name}; // ----- stack ----- pub fn pop(ip: &mut Interp) -> R { - ip.pop().map(|_| ()) + match ip.pop()? { + Obj::File(id) => ip.discard_subfile(id), + _ => Ok(()), + } } pub fn exch(ip: &mut Interp) -> R { @@ -26,11 +29,10 @@ pub fn copy(ip: &mut Interp) -> R { Obj::Int(n) => { let n = usize::try_from(n).map_err(|_| PsErr::Ps("rangecheck"))?; ip.need(n)?; - // EPS readers conventionally treat a copy that would exceed the operand limit as a - // no-op. Check before cloning or charging so a hostile count cannot partially mutate - // the stack or allocate a large temporary. + // Check before cloning or charging so a hostile count cannot partially mutate the + // stack or allocate a large temporary; strict hosts still report stackoverflow. if !ip.has_operand_room(n) { - return Ok(()); + return if ip.limits.stack_overflow_noop { Ok(()) } else { err("stackoverflow") }; } let start = ip.o.len() - n; ip.charge((n / 8) as u64)?; @@ -1039,8 +1041,7 @@ pub fn anchorsearch(ip: &mut Interp) -> R { pub fn token(ip: &mut Interp) -> R { match ip.pop()? { Obj::Str(s, _) => { - ip.alloc(crate::read::interp::SOURCE_BYTES)?; - let id = ip.srcs.add(crate::read::source::Source::string(&s)); + let id = ip.add_source(crate::read::source::Source::string(&s))?; let d = &ip.d; let mut resolve = |n: &[u8]| d.iter().rev().find_map(|dict| dict.get(&Key::Name(name(n)))); match crate::read::scan::next(&mut ip.srcs, id, &mut resolve) { diff --git a/crates/ps/src/read/ops/gfx.rs b/crates/ps/src/read/ops/gfx.rs index 17178008e9..489c9fe860 100644 --- a/crates/ps/src/read/ops/gfx.rs +++ b/crates/ps/src/read/ops/gfx.rs @@ -2,8 +2,8 @@ use std::rc::Rc; -use crate::read::gstate::{Clip, DevColor, GState, M, MAX_PATH, Seg, Space, arc_curves, bbox, bounded_arc_sweep, flatten}; -use crate::read::interp::{Interp, PsErr, R, err, matrix_obj}; +use crate::read::gstate::{Clip, DevColor, GState, M, MAX_PATH, Seg, Space, arc_curve_count, arc_curves, bbox, bounded_arc_sweep, flatten}; +use crate::read::interp::{Interp, PsErr, R, SEG_BYTES, err, matrix_obj}; use crate::read::obj::{Obj, PArr, PDict}; use crate::read::pdf::StrokeParams; @@ -447,6 +447,13 @@ fn arc_common(ip: &mut Interp, negative: bool) -> R { } let start_angle = a0 % 360.0; let available = MAX_PATH.saturating_sub(ip.gs.path.len()); + let max_curves = available.saturating_sub(1); + let connector = 1 + usize::from(ip.gs.cur.is_some() && matches!(ip.gs.path.last(), Some(Seg::Close))); + ip.alloc(connector.min(available).saturating_mul(SEG_BYTES))?; + if let Some(curves) = arc_curve_count(r, start_angle, start_angle + sweep, max_curves) { + ip.charge(curves as u64)?; + ip.alloc(curves.saturating_mul(std::mem::size_of::<[f64; 6]>()).saturating_add(curves.saturating_mul(SEG_BYTES)))?; + } let (start, curves, truncated) = arc_curves(cx, cy, r, start_angle, start_angle + sweep, available.saturating_sub(1)); let m = ip.gs.ctm; let s = m.apply(start.0, start.1); @@ -455,7 +462,6 @@ fn arc_common(ip: &mut Interp, negative: bool) -> R { } else { move_dev(ip, s)? } - ip.charge(curves.len() as u64)?; for c in curves { curve_dev(ip, m.apply(c[0], c[1]), m.apply(c[2], c[3]), m.apply(c[4], c[5]))?; } diff --git a/crates/ps/src/read/ops/misc.rs b/crates/ps/src/read/ops/misc.rs index 662a1540e8..5d18398e45 100644 --- a/crates/ps/src/read/ops/misc.rs +++ b/crates/ps/src/read/ops/misc.rs @@ -187,7 +187,7 @@ pub fn file(ip: &mut Interp) -> R { b"%stdin" => ip.push(Obj::File(ip.main)), // Output files swallow what is written. b"%stdout" | b"%stderr" | b"%lineedit" | b"%statementedit" => { - let id = ip.srcs.add(Source::bytes(Vec::new())); + let id = ip.add_source(Source::bytes(Vec::new()))?; ip.push(Obj::File(id)) } // An EPS can't reach the disk. @@ -205,10 +205,13 @@ pub fn write_ops(ip: &mut Interp) -> R { pub fn eexec(ip: &mut Interp) -> R { let up = match ip.pop()? { Obj::File(id) => id, - Obj::Str(s, _) => ip.srcs.add(Source::bytes(s.bytes())), + Obj::Str(s, _) => { + ip.alloc(s.len)?; + ip.add_source(Source::bytes(s.bytes()))? + } _ => return err("typecheck"), }; - let id = ip.srcs.add(Source::eexec(up)); + let id = ip.add_source(Source::eexec(up))?; // A number of embedded Type 1 programs omit the usual `currentdict end` before eexec; // their encrypted prolog starts with `dup /Private ...` and expects its font dictionary. // Supply it only when the caller has not already left an operand on the stack. @@ -257,7 +260,10 @@ pub fn filter(ip: &mut Interp) -> R { let src = ip.pop()?; let up = match src { Obj::File(id) => id, - Obj::Str(s, _) => ip.srcs.add(Source::bytes(s.bytes())), + Obj::Str(s, _) => { + ip.alloc(s.len)?; + ip.add_source(Source::bytes(s.bytes()))? + } p @ Obj::Array(_, true) => { // A procedure source: call it until it gives an empty string. let mut data = Vec::new(); @@ -270,31 +276,18 @@ pub fn filter(ip: &mut Interp) -> R { data.extend_from_slice(&s); ip.alloc(s.len())?; } - ip.srcs.add(Source::bytes(data)) + ip.add_source(Source::bytes(data))? } _ => return err("typecheck"), }; if fname.ends_with(b"Encode") && fname != b"NullEncode" { // Encoding filters write; an EPS has nowhere to write to. - let id = ip.srcs.add(Source::bytes(Vec::new())); + let id = ip.add_source(Source::bytes(Vec::new()))?; return ip.push(Obj::File(id)); } match ip.srcs.filter(up, &fname, ¶ms) { - Ok(mut id) => { - if fname == b"SubFileDecode" { - // EPS prologs commonly use `currentfile ... SubFileDecode filter pop` to skip - // inline data. Materialize the bounded filter so the interpreter resumes after - // its end marker rather than treating the payload as PostScript source. - let mut data = Vec::new(); - while let Some(c) = ip.srcs.read(id) { - if data.len() == data.capacity() { - data.try_reserve_exact(4096).map_err(|_| PsErr::Fatal("out of memory".into()))?; - } - ip.alloc(1)?; - data.push(c); - } - id = ip.srcs.add(Source::bytes(data)); - } + Ok(source) => { + let id = ip.add_source(source)?; ip.push(Obj::File(id)) } Err(e) => { diff --git a/crates/ps/src/read/source.rs b/crates/ps/src/read/source.rs index db15b09a27..d86fc08766 100644 --- a/crates/ps/src/read/source.rs +++ b/crates/ps/src/read/source.rs @@ -35,6 +35,7 @@ pub enum Dec { eod: Vec, count: usize, left: Option, + skip_on_pop: bool, window: VecDeque, }, /// JPEG data: only images take it (whole, as it is). @@ -157,6 +158,10 @@ impl Sources { self.list.len() - 1 } + pub fn is_subfile_filter(&self, id: usize) -> bool { + self.list.get(id).is_some_and(|source| matches!(&source.kind, Kind::Filter { dec: Dec::SubFile { skip_on_pop: true, .. }, .. })) + } + pub fn read(&mut self, id: usize) -> Option { self.read_depth(id, 0) } @@ -307,8 +312,9 @@ impl Sources { Some(c) } - /// A decoding filter over `up`. Unsupported filters give `Err`. - pub fn filter(&mut self, up: usize, name: &[u8], p: &FilterParams) -> Result { + /// Build a decoding source without retaining it, so the interpreter can charge its memory + /// budget before registering the source. + pub fn filter(&mut self, up: usize, name: &[u8], p: &FilterParams) -> Result { let dec = match name { b"ASCIIHexDecode" | b"AHx" => Dec::Hex { hi: None }, b"ASCII85Decode" | b"A85" => Dec::A85 { group: [0; 5], n: 0, first: true }, @@ -330,9 +336,10 @@ impl Sources { count: p.eod_count, // A byte count; with no count and no end string, everything passes through. left: (p.eod_string.is_empty() && p.eod_count > 0).then_some(p.eod_count), + skip_on_pop: true, window: VecDeque::new(), }, - b"NullEncode" => Dec::SubFile { eod: Vec::new(), count: 0, left: None, window: VecDeque::new() }, + b"NullEncode" => Dec::SubFile { eod: Vec::new(), count: 0, left: None, skip_on_pop: false, window: VecDeque::new() }, b"ReusableStreamDecode" => { // Reads its data when made, and can be read again (`resetfile`, positions). let mut data = Vec::new(); @@ -342,7 +349,7 @@ impl Sources { return Err("reusable stream too large".into()); } } - return Ok(self.add(Source::bytes(data))); + return Ok(Source::bytes(data)); } _ => return Err(format!("unsupported filter {}", String::from_utf8_lossy(name))), }; @@ -354,7 +361,7 @@ impl Sources { let row = (colors * bpc * columns).div_ceil(8); s.predictor = Some(Predictor { kind: p.predictor, bpp: (colors * bpc).div_ceil(8).max(1), row, prev: vec![0; row], cur: Vec::new() }); } - Ok(self.add(s)) + Ok(s) } fn read_filter(&mut self, id: usize, depth: usize) -> Option { @@ -486,7 +493,7 @@ impl Sources { None => more = false, }, }, - Dec::SubFile { eod, count, left, window } => { + Dec::SubFile { eod, count, left, window, .. } => { let (eod, mut count, mut left, mut window) = (eod.clone(), *count, *left, window.clone()); if let Some(n) = left.as_mut() { if *n == 0 { @@ -824,34 +831,39 @@ mod tests { out } + fn add_filter(s: &mut Sources, up: usize, name: &[u8], params: &FilterParams) -> usize { + let source = s.filter(up, name, params).unwrap(); + s.add(source) + } + #[test] fn filters_stop_at_their_end_markers() { let mut s = Sources::default(); let id = s.add(Source::bytes(b"48656c6c6f>rest".to_vec())); - let f = s.filter(id, b"ASCIIHexDecode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"ASCIIHexDecode", &FilterParams::default()); assert_eq!(all(&mut s, f), b"Hello"); assert_eq!(all(&mut s, id), b"rest"); let id = s.add(Source::bytes(b"87cURD]i,\"Ebo80~>rest".to_vec())); - let f = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default()); assert_eq!(all(&mut s, f), b"Hello World!"); assert_eq!(all(&mut s, id), b"rest"); let id = s.add(Source::bytes(b"rest".to_vec())); - let f = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default()); let value = 27u32 * 85u32.pow(4); assert_eq!(all(&mut s, f), value.to_be_bytes()); assert_eq!(all(&mut s, id), b"rest"); let id = s.add(Source::bytes(vec![2, b'a', b'b', b'c', 254, b'x', 128, b'!'])); - let f = s.filter(id, b"RunLengthDecode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"RunLengthDecode", &FilterParams::default()); assert_eq!(all(&mut s, f), b"abcxxx"); assert_eq!(all(&mut s, id), b"!"); let id = s.add(Source::bytes(b"data%%EndBinary tail".to_vec())); let p = FilterParams { eod_string: b"%%EndBinary".to_vec(), ..Default::default() }; - let f = s.filter(id, b"SubFileDecode", &p).unwrap(); + let f = add_filter(&mut s, id, b"SubFileDecode", &p); assert_eq!(all(&mut s, f), b"data"); assert_eq!(all(&mut s, id), b" tail"); let id = s.add(Source::bytes(b"aENDtailENDrest".to_vec())); let p = FilterParams { eod_count: 1, eod_string: b"END".to_vec(), ..Default::default() }; - let f = s.filter(id, b"SubFileDecode", &p).unwrap(); + let f = add_filter(&mut s, id, b"SubFileDecode", &p); assert_eq!(all(&mut s, f), b"aEND"); assert_eq!(all(&mut s, id), b"tailENDrest"); } @@ -878,15 +890,15 @@ mod tests { z.extend_from_slice(b"tail"); let mut s = Sources::default(); let id = s.add(Source::bytes(z)); - let f = s.filter(id, b"FlateDecode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"FlateDecode", &FilterParams::default()); assert_eq!(all(&mut s, f), b"abcabcabc".repeat(2000)); assert_eq!(all(&mut s, id), b"tail"); // Flate over ASCII85 over the program: bytes pushed back return to the filter beneath. let z = miniz_oxide::deflate::compress_to_vec_zlib(b"xyz", 6); let mut s = Sources::default(); let id = s.add(Source::bytes(format!("{}~> after", ascii85(&z)).into_bytes())); - let a = s.filter(id, b"ASCII85Decode", &FilterParams::default()).unwrap(); - let f = s.filter(a, b"FlateDecode", &FilterParams::default()).unwrap(); + let a = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default()); + let f = add_filter(&mut s, a, b"FlateDecode", &FilterParams::default()); assert_eq!(all(&mut s, f), b"xyz"); assert_eq!(all(&mut s, id), b" after"); let mut enc = weezl::encode::Encoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8); @@ -894,7 +906,7 @@ mod tests { l.extend_from_slice(b"!"); let mut s = Sources::default(); let id = s.add(Source::bytes(l)); - let f = s.filter(id, b"LZWDecode", &FilterParams { early_change: true, ..Default::default() }).unwrap(); + let f = add_filter(&mut s, id, b"LZWDecode", &FilterParams { early_change: true, ..Default::default() }); assert_eq!(all(&mut s, f), b"lzw lzw lzw"); assert_eq!(all(&mut s, id), b"!"); } @@ -934,12 +946,12 @@ mod tests { ] { let mut s = Sources::default(); let id = s.add(Source::bytes(data.to_vec())); - let f = s.filter(id, name, &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, name, &FilterParams::default()); let _ = all(&mut s, f); } let mut s = Sources::default(); let id = s.add(Source::bytes(b"\xff\xd8\xff".to_vec())); - let f = s.filter(id, b"DCTDecode", &FilterParams::default()).unwrap(); + let f = add_filter(&mut s, id, b"DCTDecode", &FilterParams::default()); assert!(s.dct(f).is_some()); } } diff --git a/crates/ps/src/read/tests.rs b/crates/ps/src/read/tests.rs index 0ffe4f934c..103fc3d411 100644 --- a/crates/ps/src/read/tests.rs +++ b/crates/ps/src/read/tests.rs @@ -84,8 +84,14 @@ fn integer_division_overflow_returns_zero() { #[test] fn copy_past_operand_limit_is_a_noop() { - let (c, _) = run("10 20 5 5 rectfill 1 40 { count copy } repeat clear 20 20 5 5 rectfill"); - assert_eq!(c.lines().filter(|line| *line == "f").count(), 2, "{c}"); + let body = format!("10 20 5 5 rectfill {} 60000 copy 20 20 5 5 rectfill", "0 ".repeat(60_000)); + let compatible = eps_to_pdf(&eps(&body), Limits { stack_overflow_noop: true, ..Limits::default() }).unwrap(); + let compatible_content = content(&compatible.pdf); + assert_eq!(compatible_content.lines().filter(|line| *line == "f").count(), 2, "{compatible_content}"); + + let strict = eps_to_pdf(&eps(&body), Limits { keep_unpainted: true, ..Limits::default() }).unwrap(); + assert!(strict.warnings.iter().any(|warning| warning.contains("stackoverflow")), "{:?}", strict.warnings); + assert_eq!(content(&strict.pdf).lines().filter(|line| *line == "f").count(), 1); } #[test] @@ -97,6 +103,14 @@ fn transforms_curves_and_clips() { assert!(c.contains("80 45 m"), "{c}"); } +#[test] +fn huge_arc_checks_temporary_and_retained_memory_before_building_curves() { + let limits = Limits { memory: 4096, keep_unpainted: true, ..Limits::default() }; + let converted = eps_to_pdf(&eps("10 20 5 5 rectfill newpath 0 0 1e16 0 360 arc"), limits).unwrap(); + assert!(converted.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", converted.warnings); + assert_eq!(content(&converted.pdf).lines().filter(|line| *line == "f").count(), 1); +} + #[test] fn separation_colours_use_their_tint_transform() { let (c, _) = run("[/Separation (Spot) /DeviceCMYK { dup 0 exch 0 } ] setcolorspace 0.5 setcolor 10 20 10 10 rectfill"); @@ -154,6 +168,22 @@ fn type3_glyph_paints_after_prior_outline_glyphs() { assert!(first_fill < white_paint && white_paint < second_fill, "glyph paints are out of order: {c}"); } +#[test] +fn type3_measurement_suppresses_nested_outline_painting() { + let ttf = tiny_ttf(); + let sfnts: String = ttf.chunks(64).map(|c| format!("<{}>\n", c.iter().map(|b| format!("{b:02x}")).collect::())).collect(); + let (c, _) = run(&format!( + "/T42 11 dict dup begin /FontType 42 def /FontMatrix [0.001 0 0 0.001 0 0] def /PaintType 0 def /FontBBox [0 0 1 1] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /CharStrings 2 dict dup /.notdef 0 put dup /A 1 put def /sfnts [ {sfnts} ] def end /T42 exch definefont pop + /T3 8 dict dup begin /FontType 3 def /FontMatrix [0.001 0 0 0.001 0 0] def /FontBBox [0 0 1000 700] def + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for Encoding 65 /A put + /BuildChar {{ pop pop 1000 0 0 0 1000 700 setcachedevice /T42 findfont 20 scalefont setfont 10 10 moveto (A) show }} def end /T3 exch definefont pop + /T3 findfont 20 scalefont setfont (A) stringwidth pop pop 0 0 10 10 rectfill" + )); + assert_eq!(c.lines().filter(|line| *line == "f").count(), 1, "measurement painted nested outline text: {c}"); +} + #[test] fn a_string_made_a_file_runs_as_a_program() { // Adobe's idiom: `(code) 0 () /SubFileDecode filter cvx exec`. @@ -162,17 +192,33 @@ fn a_string_made_a_file_runs_as_a_program() { } #[test] -fn subfiledecode_materialization_respects_the_memory_budget() { +fn subfiledecode_exec_reads_the_following_program_lazily() { + let (c, converted) = run("currentfile 0 () /SubFileDecode filter exec\n0 0 10 10 rectfill"); + assert!(converted.warnings.is_empty(), "{:?}", converted.warnings); + assert_eq!(c.lines().filter(|line| *line == "f").count(), 1, "{c}"); +} + +#[test] +fn discarded_subfiledecode_skips_payload_without_materializing_it() { let payload = "x".repeat(8192); let bytes = eps(&format!("0 0 10 10 rectfill currentfile 0 (END) /SubFileDecode filter pop\n{payload}END")); let limits = Limits { memory: 4096, ..Limits::default() }; let converted = eps_to_pdf(&bytes, limits).unwrap(); - assert!(converted.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", converted.warnings); - assert!(converted.memory <= limits.memory + 1, "{} bytes", converted.memory); + assert!(!converted.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", converted.warnings); + assert!(converted.memory <= limits.memory, "{} bytes", converted.memory); let c = content(&converted.pdf); assert!(c.contains("0 0 m") && c.contains("10 10 l"), "{c}"); } +#[test] +fn empty_filter_source_records_are_charged_to_the_memory_limit() { + let limits = Limits { memory: 4096, keep_unpainted: true, ..Limits::default() }; + let converted = eps_to_pdf(&eps("10 20 1 1 rectfill 100000 { () 0 () /SubFileDecode filter pop } repeat"), limits).unwrap(); + assert!(converted.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", converted.warnings); + assert!(converted.memory > limits.memory, "{} bytes", converted.memory); + assert!(converted.steps < limits.steps, "the test hit the work limit first: {} steps", converted.steps); +} + #[test] fn eexec_sections_run() { // `(1 0 0 setrgbcolor 10 20 5 5 rectfill) ` encrypted with the eexec key, as hex. @@ -388,8 +434,8 @@ fn fallback_tiny_ttf(_: &[u8]) -> Option<&'static [u8]> { #[test] fn fallback_font_bytes_are_charged_before_copying() { - let limits = Limits { memory: 32, fallback_font: Some(fallback_tiny_ttf), ..Limits::default() }; - let error = eps_to_pdf(&eps("/Missing findfont pop 0 0 1 1 rectfill"), limits).unwrap_err(); + let limits = Limits { memory: 256, fallback_font: Some(fallback_tiny_ttf), ..Limits::default() }; + let error = eps_to_pdf(&eps("/Missing findfont pop"), limits).unwrap_err(); assert_eq!(error, "out of memory"); }