mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
Merge remote-tracking branch 'origin/main' into codex/jpeg-baseline-encoder
This commit is contained in:
commit
1a950613f9
33 files changed
+11827
-13
No files matched your search
Generated
+12
@@ -2362,6 +2362,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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
+26
-10
@@ -27,6 +27,27 @@ pub struct Sections<'a> {
|
||||
pub metafile: Option<&'a [u8]>,
|
||||
}
|
||||
|
||||
fn word(bytes: &[u8], at: usize) -> Result<usize, Error> {
|
||||
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<Sections<'_>, Error> {
|
||||
if !bytes.starts_with(&DOS_MAGIC) {
|
||||
return Ok(Sections { postscript: bytes, tiff: None, metafile: None });
|
||||
}
|
||||
let word = |at: usize| -> Result<usize, Error> {
|
||||
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<Option<&[u8]>, 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<Option<&[u8]>, 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
|
||||
|
||||
+13
-1
@@ -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).
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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, 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. 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.
|
||||
@@ -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<String> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<u8>,
|
||||
pub matrix: [f64; 6],
|
||||
pub bbox: [f64; 4],
|
||||
/// Glyph names (name-keyed) by glyph index.
|
||||
pub names: Vec<Vec<u8>>,
|
||||
/// 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<Vec<u32>>,
|
||||
pub ros: Option<(Vec<u8>, Vec<u8>, i64)>,
|
||||
/// CID → glyph index, made on first lookup.
|
||||
by_cid: std::cell::OnceCell<std::collections::HashMap<u32, u32>>,
|
||||
charstrings: Vec<(usize, usize)>,
|
||||
privates: Vec<Private>,
|
||||
fd_select: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FontSet {
|
||||
pub data: Vec<u8>,
|
||||
gsubrs: Vec<(usize, usize)>,
|
||||
pub fonts: Vec<Font>,
|
||||
}
|
||||
|
||||
fn u16_at(d: &[u8], i: usize) -> Option<usize> {
|
||||
Some(usize::from(u16::from_be_bytes([*d.get(i)?, *d.get(i + 1)?])))
|
||||
}
|
||||
|
||||
fn offset(d: &[u8], i: usize, size: usize) -> Option<usize> {
|
||||
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<f64>)>,
|
||||
}
|
||||
|
||||
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<f64> {
|
||||
self.get(op)?.first().copied()
|
||||
}
|
||||
}
|
||||
|
||||
fn dict(d: &[u8]) -> Dict {
|
||||
let mut out = Dict::default();
|
||||
let mut ops: Vec<f64> = 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<u32> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8>) -> Option<FontSet> {
|
||||
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<Vec<u8>> = 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<Seg>,
|
||||
x: f64,
|
||||
y: f64,
|
||||
open: bool,
|
||||
stack: Vec<f64>,
|
||||
hints: usize,
|
||||
width: Option<f64>,
|
||||
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<Glyph> {
|
||||
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<u32> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<n>`, which only has to be consistent within one font (they are its own glyph names).
|
||||
pub fn standard(sid: usize) -> Option<String> {
|
||||
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}"))
|
||||
}
|
||||
@@ -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<u8>, Vec<u8>)>,
|
||||
ranges: Vec<(Vec<u8>, Vec<u8>, u32)>,
|
||||
parent: Option<usize>,
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<u32> {
|
||||
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<usize> {
|
||||
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<Vec<Obj>> {
|
||||
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<Vec<u8>> {
|
||||
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<usize> {
|
||||
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<crate::read::cff::FontSet>, 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<Obj>) -> R<PDict> {
|
||||
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<PDict> {
|
||||
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<f64> = 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<Vec<(u32, PDict, M, Key2)>> {
|
||||
let fm = font_matrix(font);
|
||||
let desc: Vec<PDict> = 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<Glyph> {
|
||||
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<PStr> = a.items().into_iter().filter_map(|o| if let Obj::Str(s, _) = o { Some(s) } else { None }).collect();
|
||||
gid_from(&strs.iter().collect::<Vec<_>>(), 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));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
//! 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, PStr, 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]) -> R<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 Ok(d);
|
||||
}
|
||||
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);
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
fn substitute_type42(ip: &mut Interp, font_name: &[u8], bytes: &[u8]) -> R<Option<PDict>> {
|
||||
use skrifa::MetadataProvider;
|
||||
|
||||
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() {
|
||||
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())));
|
||||
}
|
||||
ip.alloc(bytes.len().saturating_add(bytes.len() % 2))?;
|
||||
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));
|
||||
Ok(Some(d))
|
||||
}
|
||||
|
||||
fn standard_glyph_char(name: &[u8]) -> Option<char> {
|
||||
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',
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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) -> R<PDict> {
|
||||
if let Some(Obj::Dict(d)) = ip.fontdir.get(&key.key()) {
|
||||
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 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.
|
||||
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 Ok(d);
|
||||
}
|
||||
let n = crate::read::ops::basic::text_of(key);
|
||||
if let Some(d) = crate::read::composite::compose_by_name(ip, &n) {
|
||||
return Ok(d);
|
||||
}
|
||||
let d = substitute(ip, &n)?;
|
||||
ip.fontdir.put(key.clone(), Obj::Dict(d.clone()));
|
||||
Ok(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<u8> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
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);
|
||||
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<f64> = 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<std::rc::Rc<crate::read::type1::Glyph>> {
|
||||
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<std::rc::Rc<crate::read::type1::Glyph>> {
|
||||
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<M> {
|
||||
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);
|
||||
let mut text_paths: Vec<(Vec<crate::read::gstate::Seg>, 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 => {
|
||||
// Type 3 glyph procedures paint immediately. Commit any earlier staged
|
||||
// outlines first so their paint order remains the same as the input string.
|
||||
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() {
|
||||
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),
|
||||
};
|
||||
// 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<crate::read::gstate::Seg> = 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
})();
|
||||
let path_limit = matches!(&result, Err(PsErr::Fatal(message)) if message == "PostScript (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)?;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
u8::try_from(code).ok()
|
||||
}
|
||||
|
||||
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.
|
||||
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();
|
||||
result
|
||||
} else {
|
||||
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<f64> = 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)
|
||||
}
|
||||
|
||||
#[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"quoteleft"), Some('‘'));
|
||||
assert_eq!(standard_glyph_char(b"quoteright"), Some('’'));
|
||||
assert_eq!(standard_glyph_char(b"unmapped.custom"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
//! 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<M> {
|
||||
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 = 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;
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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<usize> {
|
||||
let sweep = a1.to_radians() - a0.to_radians();
|
||||
if !sweep.is_finite() || sweep == 0.0 {
|
||||
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);
|
||||
(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 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, false)
|
||||
}
|
||||
|
||||
#[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<Vec<u8>>,
|
||||
alt: Box<Space>,
|
||||
tint: Obj,
|
||||
},
|
||||
Indexed {
|
||||
base: Box<Space>,
|
||||
hival: usize,
|
||||
lookup: Vec<u8>,
|
||||
},
|
||||
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<f64> {
|
||||
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<Seg>,
|
||||
pub eo: bool,
|
||||
pub parent: Option<Rc<Clip>>,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
pub fn clip_chain(c: &Option<Rc<Clip>>) -> Vec<Rc<Clip>> {
|
||||
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<Seg>,
|
||||
/// 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<f64>,
|
||||
/// The pattern dictionary when the space is Pattern.
|
||||
pub pattern: Option<PDict>,
|
||||
pub line_width: f64,
|
||||
pub cap: i64,
|
||||
pub join: i64,
|
||||
pub miter: f64,
|
||||
pub dash: (Vec<f64>, f64),
|
||||
pub flat: f64,
|
||||
pub font: Option<PDict>,
|
||||
pub clip: Option<Rc<Clip>>,
|
||||
pub overprint: bool,
|
||||
pub stroke_adjust: bool,
|
||||
/// Clips saved by `clipsave`.
|
||||
pub clip_saves: Vec<Option<Rc<Clip>>>,
|
||||
/// The colour space under an uncoloured pattern (`[/Pattern /DeviceRGB]`).
|
||||
pub pattern_base: Option<Box<Space>>,
|
||||
/// 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<Seg> {
|
||||
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, 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, 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]
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
//! 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<T = ()> = Result<T, PsErr>;
|
||||
|
||||
pub fn err<T>(name: &'static str) -> R<T> {
|
||||
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.
|
||||
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<FallbackFont>,
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory a path segment and a graphics state take.
|
||||
pub const SEG_BYTES: usize = std::mem::size_of::<crate::read::gstate::Seg>();
|
||||
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;
|
||||
|
||||
// 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;
|
||||
const MAX_NAME_DEPTH: usize = 48;
|
||||
const MAX_CALL_DEPTH: usize = 48;
|
||||
|
||||
pub struct Interp {
|
||||
pub o: Vec<Obj>,
|
||||
pub d: Vec<PDict>,
|
||||
pub e: Vec<Frame>,
|
||||
pub srcs: Sources,
|
||||
pub gs: GState,
|
||||
pub gstack: Vec<GState>,
|
||||
/// Graphics-state depths that `save` objects restore to.
|
||||
pub saves: Vec<usize>,
|
||||
pub gstates: Vec<GState>,
|
||||
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<String>,
|
||||
/// Tiling pattern cells already written, by pattern and colour.
|
||||
pub cells: std::collections::HashMap<(crate::read::obj::Key, Vec<u64>), usize>,
|
||||
/// CMaps by index (CMap dictionaries refer to them).
|
||||
pub cmaps: Vec<crate::read::composite::CMap>,
|
||||
/// CFF font sets read by StartData.
|
||||
pub cff: Vec<std::rc::Rc<crate::read::cff::FontSet>>,
|
||||
/// Type 42 fonts' TrueType data by FID.
|
||||
pub sfnts: std::collections::HashMap<usize, std::rc::Rc<Vec<u8>>>,
|
||||
/// `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<u8>), std::rc::Rc<crate::read::type1::Glyph>>,
|
||||
/// The last names executed (for error reports).
|
||||
pub recent: std::collections::VecDeque<crate::read::obj::Name>,
|
||||
}
|
||||
|
||||
impl Interp {
|
||||
pub fn new(program: Vec<u8>, 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: SOURCE_BYTES,
|
||||
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 {
|
||||
if self.limits.stack_overflow_noop {
|
||||
return Ok(());
|
||||
}
|
||||
return err("stackoverflow");
|
||||
}
|
||||
self.o.push(o);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> R<Obj> {
|
||||
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(()) }
|
||||
}
|
||||
|
||||
/// 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<f64> {
|
||||
match self.pop()? {
|
||||
o @ (Obj::Int(_) | Obj::Real(_)) => o.num().ok_or(PsErr::Ps("typecheck")),
|
||||
_ => err("typecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_int(&mut self) -> R<i64> {
|
||||
match self.pop()? {
|
||||
Obj::Int(i) => Ok(i),
|
||||
Obj::Real(r) if r.is_finite() => Ok(r as i64),
|
||||
_ => err("typecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_usize(&mut self) -> R<usize> {
|
||||
usize::try_from(self.pop_int()?).map_err(|_| PsErr::Ps("rangecheck"))
|
||||
}
|
||||
|
||||
pub fn pop_bool(&mut self) -> R<bool> {
|
||||
match self.pop()? {
|
||||
Obj::Bool(b) => Ok(b),
|
||||
_ => err("typecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_dict(&mut self) -> R<PDict> {
|
||||
match self.pop()? {
|
||||
Obj::Dict(d) => Ok(d),
|
||||
_ => err("typecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_arr(&mut self) -> R<PArr> {
|
||||
match self.pop()? {
|
||||
Obj::Array(a, _) => Ok(a),
|
||||
_ => err("typecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_str(&mut self) -> R<PStr> {
|
||||
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<Vec<u8>> {
|
||||
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<crate::read::gstate::M> {
|
||||
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(()) }
|
||||
}
|
||||
|
||||
/// Register a retained file source against the memory bound.
|
||||
pub fn add_source(&mut self, source: Source) -> R<usize> {
|
||||
self.alloc(SOURCE_BYTES)?;
|
||||
Ok(self.srcs.add(source))
|
||||
}
|
||||
|
||||
/// Whether a file ID is still held by an operand or execution frame.
|
||||
pub fn file_is_live(&self, id: usize) -> bool {
|
||||
self.o.iter().any(|o| matches!(o, Obj::File(file) if *file == id))
|
||||
|| self.e.iter().any(|frame| matches!(frame, Frame::File { id: file } if *file == id))
|
||||
}
|
||||
|
||||
/// 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<Obj> {
|
||||
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<PDict> {
|
||||
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.
|
||||
let id = self.add_source(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<String> = self.recent.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect();
|
||||
let stack: Vec<String> = self
|
||||
.o
|
||||
.iter()
|
||||
.rev()
|
||||
.take(6)
|
||||
.map(|o| {
|
||||
String::from_utf8_lossy(&crate::read::ops::basic::text_of(o)).chars().take(24).collect::<String>()
|
||||
+ ":"
|
||||
+ &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<crate::read::gstate::M> {
|
||||
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)
|
||||
}
|
||||
@@ -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::{FallbackFont, Limits};
|
||||
|
||||
/// A converted EPS.
|
||||
#[derive(Debug)]
|
||||
pub struct Converted {
|
||||
/// A one-page PDF of the bounding box.
|
||||
pub pdf: Vec<u8>,
|
||||
/// 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<String>,
|
||||
/// 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<std::sync::Arc<Vec<u8>>>>;
|
||||
|
||||
fn cache() -> &'static std::sync::Mutex<Cache> {
|
||||
static CACHE: std::sync::OnceLock<std::sync::Mutex<Cache>> = 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<std::sync::Arc<Vec<u8>>> {
|
||||
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,
|
||||
/// or when it fails before painting and `limits.keep_unpainted` is false.
|
||||
pub fn eps_to_pdf(bytes: &[u8], limits: Limits) -> Result<Converted, String> {
|
||||
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 && !limits.keep_unpainted {
|
||||
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;
|
||||
@@ -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<RefCell<Vec<u8>>>,
|
||||
pub off: usize,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
impl PStr {
|
||||
pub fn new(bytes: Vec<u8>) -> 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<u8> {
|
||||
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<u8> {
|
||||
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<PStr> {
|
||||
(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<PStr> {
|
||||
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<RefCell<Vec<Obj>>>,
|
||||
pub off: usize,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
impl PArr {
|
||||
pub fn new(v: Vec<Obj>) -> Self {
|
||||
let len = v.len();
|
||||
PArr { buf: Rc::new(RefCell::new(v)), off: 0, len }
|
||||
}
|
||||
|
||||
pub fn get(&self, i: usize) -> Option<Obj> {
|
||||
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<Obj> {
|
||||
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<PArr> {
|
||||
(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<Key, (Obj, Obj)>,
|
||||
pub cap: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PDict(pub Rc<RefCell<DictInner>>);
|
||||
|
||||
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<Obj> {
|
||||
self.0.borrow().map.get(k).map(|(_, v)| v.clone())
|
||||
}
|
||||
|
||||
pub fn get_name(&self, n: &[u8]) -> Option<Obj> {
|
||||
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<f64> {
|
||||
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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,374 @@
|
||||
//! 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<u8>, 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<Obj>,
|
||||
decode: Option<Vec<f64>>,
|
||||
interpolate: bool,
|
||||
}
|
||||
|
||||
fn dict_params(ip: &mut Interp, d: &PDict, comps: usize) -> R<Params> {
|
||||
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<u8>, 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<u8>> = vec![Vec::new(); comps];
|
||||
let sources: Vec<Obj> = 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<M> {
|
||||
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<u8>, bpc: usize) -> Vec<u8> {
|
||||
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)?;
|
||||
if ip.limits.skip_images {
|
||||
ip.warnings.push("image".into());
|
||||
return Ok(());
|
||||
}
|
||||
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::<Vec<_>>());
|
||||
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<Vec<u8>, 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<f64> = 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<u8>, 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)?;
|
||||
if ip.limits.skip_images {
|
||||
ip.warnings.push("image".into());
|
||||
return Ok(());
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//! 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<DevColor>);
|
||||
|
||||
/// 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<f64>,
|
||||
bits: Option<(Vec<u8>, usize)>,
|
||||
pos: usize,
|
||||
bpc: usize,
|
||||
bpcomp: usize,
|
||||
bpflag: usize,
|
||||
decode: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
fn raw(&mut self, n: usize) -> Option<u64> {
|
||||
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<f64> {
|
||||
let v = self.nums.get(self.pos).copied();
|
||||
self.pos += 1;
|
||||
v
|
||||
}
|
||||
|
||||
fn decoded(&mut self, bits: usize, range: usize) -> Option<f64> {
|
||||
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<u8> {
|
||||
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<Vec<f64>> {
|
||||
let b = self.bpcomp;
|
||||
(0..n).map(|i| self.decoded(b, 2 + i)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Option<Shading>> {
|
||||
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<f64> = 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<DevColor> {
|
||||
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<DevColor> = 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<usize> = 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<Patch> = 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::<Vec<_>>(),
|
||||
ci.iter().filter_map(|i| pc.get(*i).copied()).collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
_ => (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<f64> = 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 }))
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
//! 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, Kind, 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<usize> {
|
||||
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<u8> = 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)?;
|
||||
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() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
// 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(())
|
||||
}
|
||||
|
||||
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.add_source(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.alloc(s.len)?;
|
||||
ip.add_source(Source::bytes(s.bytes()))?
|
||||
}
|
||||
_ => return err("typecheck"),
|
||||
};
|
||||
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.
|
||||
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()))?;
|
||||
}
|
||||
// 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.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();
|
||||
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.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.add_source(Source::bytes(Vec::new()))?;
|
||||
return ip.push(Obj::File(id));
|
||||
}
|
||||
// Reserve the retained source record before constructing any filter-owned buffers. The
|
||||
// filter separately checks its predictor/reusable-stream storage against the remaining cap.
|
||||
ip.alloc(crate::read::interp::SOURCE_BYTES)?;
|
||||
let max_owned = ip.limits.memory.saturating_sub(ip.memory);
|
||||
match ip.srcs.filter(up, &fname, ¶ms, max_owned) {
|
||||
Ok((source, owned_bytes)) => {
|
||||
ip.alloc(owned_bytes)?;
|
||||
let id = ip.srcs.add(source);
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
//! 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,
|
||||
// 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,
|
||||
// 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<OpId> {
|
||||
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);
|
||||
}
|
||||
@@ -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<f64>,
|
||||
c1: Vec<f64>,
|
||||
n: f64,
|
||||
domain: [f64; 2],
|
||||
},
|
||||
Stitch {
|
||||
fns: Vec<Func>,
|
||||
bounds: Vec<f64>,
|
||||
encode: Vec<f64>,
|
||||
domain: [f64; 2],
|
||||
},
|
||||
Sampled {
|
||||
size: Vec<usize>,
|
||||
domain: Vec<f64>,
|
||||
encode: Vec<f64>,
|
||||
decode: Vec<f64>,
|
||||
samples: Vec<f64>,
|
||||
outs: usize,
|
||||
},
|
||||
Proc(Obj),
|
||||
/// One function per output.
|
||||
Each(Vec<Func>),
|
||||
}
|
||||
|
||||
fn nums(o: Option<Obj>) -> Option<Vec<f64>> {
|
||||
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<Func> {
|
||||
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::<R<Vec<_>>>().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::<R<Vec<_>>>()?,
|
||||
_ => 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<usize> = 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<Vec<f64>> {
|
||||
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<u8>) {
|
||||
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<Option<Shading>> {
|
||||
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<DevColor> {
|
||||
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<bool> {
|
||||
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::<Vec<_>>());
|
||||
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<Vec<f64>>) -> R<usize> {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
//! 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<ImgSpace>, hival: u8, lookup: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Image {
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
pub bpc: u8,
|
||||
/// None for a stencil mask (`imagemask`).
|
||||
pub space: Option<ImgSpace>,
|
||||
pub decode: Option<Vec<f32>>,
|
||||
pub data: Vec<u8>,
|
||||
/// `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<u8>)>,
|
||||
}
|
||||
|
||||
/// 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<u8>,
|
||||
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<u8>,
|
||||
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<Rc<Clip>>,
|
||||
state: PaintState,
|
||||
saved: Vec<PaintState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct PaintState {
|
||||
fill: Option<DevColor>,
|
||||
stroke: Option<DevColor>,
|
||||
}
|
||||
|
||||
pub struct StrokeParams<'a> {
|
||||
pub width: f64,
|
||||
pub cap: i64,
|
||||
pub join: i64,
|
||||
pub miter: f64,
|
||||
pub dash: &'a (Vec<f64>, f64),
|
||||
}
|
||||
|
||||
pub struct Out {
|
||||
c: Content,
|
||||
images: Vec<Image>,
|
||||
shadings: Vec<Shading>,
|
||||
tilings: Vec<Tiling>,
|
||||
origin: (f64, f64),
|
||||
applied: Vec<Rc<Clip>>,
|
||||
state: PaintState,
|
||||
saved: Vec<PaintState>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Rc<Clip>>) -> 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<Rc<Clip>>, 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<Rc<Clip>>, 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<Seg> = 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<Rc<Clip>>, cm: &M, img: Image, fill: Option<DevColor>) -> 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<usize, String> {
|
||||
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<Rc<Clip>>, 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<Rc<Clip>>, 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<u8> {
|
||||
let mut pdf = Pdf::new();
|
||||
// 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 = || {
|
||||
let r = Ref::new(next);
|
||||
next += 1;
|
||||
r
|
||||
};
|
||||
let image_refs: Vec<(Ref, Option<Ref>)> = 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<Ref> = 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<f32> = (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>),
|
||||
}
|
||||
|
||||
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<Obj>) -> Result<Option<Obj>, String> {
|
||||
let mut stack: Vec<Vec<Obj>> = 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<Option<Token>, 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<u8>) -> Result<Vec<u8>, String> {
|
||||
let mut t: Vec<u8> = 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<Vec<u8>, 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<Vec<u8>, String> {
|
||||
let mut out = Vec::new();
|
||||
let mut hi: Option<u8> = 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<Vec<u8>, 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<Obj> {
|
||||
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::<i64>() {
|
||||
Ok(i) if i32::try_from(i).is_ok() => Obj::Int(i),
|
||||
_ => Obj::Real(s.parse::<f64>().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<Obj> {
|
||||
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<String> = 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,986 @@
|
||||
//! 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<u8>,
|
||||
},
|
||||
A85 {
|
||||
group: [u8; 5],
|
||||
n: usize,
|
||||
first: bool,
|
||||
},
|
||||
RunLength,
|
||||
Lzw {
|
||||
dec: Box<weezl::decode::Decoder>,
|
||||
pending: Vec<u8>,
|
||||
},
|
||||
Flate {
|
||||
state: Box<miniz_oxide::inflate::stream::InflateState>,
|
||||
pending: Vec<u8>,
|
||||
},
|
||||
SubFile {
|
||||
eod: Vec<u8>,
|
||||
count: usize,
|
||||
left: Option<usize>,
|
||||
skip_on_pop: bool,
|
||||
window: VecDeque<u8>,
|
||||
},
|
||||
/// JPEG data: only images take it (whole, as it is).
|
||||
Dct,
|
||||
}
|
||||
|
||||
pub enum Kind {
|
||||
Bytes(Rc<Vec<u8>>),
|
||||
/// A window onto a string's storage, read in place (`token`, executing a string).
|
||||
Shared {
|
||||
buf: Rc<std::cell::RefCell<Vec<u8>>>,
|
||||
off: usize,
|
||||
len: usize,
|
||||
},
|
||||
/// Decrypting `eexec` over another file. `hex` is decided by the first four bytes.
|
||||
Eexec {
|
||||
up: usize,
|
||||
r: u16,
|
||||
hex: Option<bool>,
|
||||
skipped: u8,
|
||||
},
|
||||
Filter {
|
||||
up: usize,
|
||||
dec: Dec,
|
||||
out: VecDeque<u8>,
|
||||
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<Rc<Vec<u8>>>,
|
||||
/// The last byte given, and bytes pushed back (read again first; the last pushed first).
|
||||
pub last: Option<u8>,
|
||||
pub back: Vec<u8>,
|
||||
/// A PNG or TIFF predictor under Flate or LZW.
|
||||
pub predictor: Option<Predictor>,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn bytes(data: Vec<u8>) -> Self {
|
||||
Self::shared(Rc::new(data))
|
||||
}
|
||||
|
||||
pub fn shared(data: Rc<Vec<u8>>) -> 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<u8> {
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Predictor {
|
||||
kind: i64,
|
||||
bpp: usize,
|
||||
row: usize,
|
||||
prev: Vec<u8>,
|
||||
cur: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The sources, read through one another (`eexec` over the program, filters over both).
|
||||
pub struct Sources {
|
||||
pub list: Vec<Source>,
|
||||
/// 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 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<u8> {
|
||||
self.read_depth(id, 0)
|
||||
}
|
||||
|
||||
fn read_depth(&mut self, id: usize, depth: usize) -> Option<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<bool> {
|
||||
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<u8> {
|
||||
let c = self.read(id)?;
|
||||
self.unread(id);
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// 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, max_owned: usize) -> Result<(Source, usize), String> {
|
||||
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),
|
||||
skip_on_pop: true,
|
||||
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();
|
||||
while let Some(c) = self.read(up) {
|
||||
let next = data.len().saturating_add(1);
|
||||
if next > MAX_DECODED || next > max_owned {
|
||||
return Err("out of memory".into());
|
||||
}
|
||||
if next > data.capacity() {
|
||||
let target = next.saturating_add(4095).min(max_owned).min(MAX_DECODED);
|
||||
data.try_reserve_exact(target.saturating_sub(data.len())).map_err(|_| "out of memory")?;
|
||||
}
|
||||
data.push(c);
|
||||
}
|
||||
// The capacity is retained by the reusable byte source, so charge it rather
|
||||
// than only the initialized bytes.
|
||||
let bytes = data.capacity();
|
||||
return Ok((Source::bytes(data), bytes));
|
||||
}
|
||||
_ => 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);
|
||||
if row > max_owned {
|
||||
return Err("out of memory".into());
|
||||
}
|
||||
let mut prev = Vec::new();
|
||||
prev.try_reserve_exact(row).map_err(|_| "out of memory")?;
|
||||
prev.resize(row, 0);
|
||||
s.predictor = Some(Predictor { kind: p.predictor, bpp: (colors * bpc).div_ceil(8).max(1), row, prev, cur: Vec::new() });
|
||||
return Ok((s, row));
|
||||
}
|
||||
Ok((s, 0))
|
||||
}
|
||||
|
||||
fn read_filter(&mut self, id: usize, depth: usize) -> Option<u8> {
|
||||
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.
|
||||
#[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;
|
||||
let mut produced: Vec<u8> = 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 `<~` is optional; `<` is itself a valid ASCII85 digit.
|
||||
let next = self.read_depth(up, depth + 1);
|
||||
if next == Some(b'~') {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if next.is_some() {
|
||||
self.unread(up);
|
||||
}
|
||||
}
|
||||
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;
|
||||
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() {
|
||||
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<Rc<Vec<u8>>> {
|
||||
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<usize> {
|
||||
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<u8> {
|
||||
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<u8> = 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<usize> {
|
||||
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<u8> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(c) = s.read(id) {
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn add_filter(s: &mut Sources, up: usize, name: &[u8], params: &FilterParams) -> usize {
|
||||
let (source, _) = s.filter(up, name, params, usize::MAX).unwrap();
|
||||
s.add(source)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_owned_buffers_are_preflighted_against_memory_budget() {
|
||||
let mut sources = Sources::default();
|
||||
let id = sources.add(Source::bytes(Vec::new()));
|
||||
let predictor = FilterParams { predictor: 12, colors: 32, bpc: 16, columns: 1 << 20, ..Default::default() };
|
||||
assert!(matches!(sources.filter(id, b"FlateDecode", &predictor, 4096), Err(e) if e == "out of memory"));
|
||||
|
||||
let id = sources.add(Source::bytes(vec![b'x'; 10]));
|
||||
assert!(matches!(sources.filter(id, b"ReusableStreamDecode", &FilterParams::default(), 9), Err(e) if e == "out of memory"));
|
||||
let id = sources.add(Source::bytes(vec![b'x'; 10]));
|
||||
let (_, owned) = sources.filter(id, b"ReusableStreamDecode", &FilterParams::default(), 10).unwrap();
|
||||
assert_eq!(owned, 10);
|
||||
}
|
||||
|
||||
#[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 = 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 = 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 = 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 = 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 = 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 = add_filter(&mut s, id, b"SubFileDecode", &p);
|
||||
assert_eq!(all(&mut s, f), b"aEND");
|
||||
assert_eq!(all(&mut s, id), b"tailENDrest");
|
||||
}
|
||||
|
||||
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 = 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 = 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);
|
||||
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 = 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"!");
|
||||
}
|
||||
|
||||
#[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::<String>().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<u8> = (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 = 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 = add_filter(&mut s, id, b"DCTDecode", &FilterParams::default());
|
||||
assert!(s.dct(f).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
//! 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<u8> {
|
||||
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 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 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]
|
||||
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 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");
|
||||
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] <f0> 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 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::<String>())).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 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::<String>())).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`.
|
||||
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 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, "{} bytes", converted.memory);
|
||||
let c = content(&converted.pdf);
|
||||
assert!(c.contains("0 0 m") && c.contains("10 10 l"), "{c}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_does_not_drain_a_subfiledecode_stream_with_another_reference() {
|
||||
let (c, converted) = run("currentfile 0 () /SubFileDecode filter 1 copy pop exec\n1 0 0 setrgbcolor 0 0 10 10 rectfill");
|
||||
assert!(converted.warnings.is_empty(), "{:?}", converted.warnings);
|
||||
assert!(c.contains("1 0 0 rg") && c.contains("0 0 m") && c.contains("10 10 l"), "{c}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predictor_and_reusable_stream_buffers_are_charged_before_retention() {
|
||||
let limits = Limits { memory: 4096, keep_unpainted: true, ..Limits::default() };
|
||||
let predictor = eps_to_pdf(
|
||||
&eps("10 20 1 1 rectfill () << /Predictor 12 /Colors 32 /BitsPerComponent 16 /Columns 1048576 >> /FlateDecode filter pop"),
|
||||
limits,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(predictor.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", predictor.warnings);
|
||||
assert!(predictor.memory <= limits.memory, "{} bytes", predictor.memory);
|
||||
|
||||
let payload = "x".repeat(2500);
|
||||
let reusable = eps_to_pdf(&eps(&format!("10 20 1 1 rectfill ({payload}) /ReusableStreamDecode filter pop")), limits).unwrap();
|
||||
assert!(reusable.warnings.iter().any(|warning| warning == "out of memory"), "{:?}", reusable.warnings);
|
||||
}
|
||||
|
||||
#[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.
|
||||
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 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 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 {
|
||||
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",
|
||||
"/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<u8> {
|
||||
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<u8>); 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
|
||||
}
|
||||
|
||||
fn fallback_tiny_ttf(_: &[u8]) -> Option<&'static [u8]> {
|
||||
static FONT: std::sync::OnceLock<Vec<u8>> = 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: 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");
|
||||
}
|
||||
|
||||
#[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::<String>())).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();
|
||||
// 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::<String>())).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"
|
||||
));
|
||||
// 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}");
|
||||
}
|
||||
|
||||
/// A CFF INDEX of `items` (4-byte offsets).
|
||||
fn cff_index(items: &[Vec<u8>]) -> Vec<u8> {
|
||||
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<u8> {
|
||||
[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<u8> {
|
||||
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<u8> {
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
|
||||
#[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> <ff> 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_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();
|
||||
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> <ff> 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,
|
||||
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(" ");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Seg>,
|
||||
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<u8> {
|
||||
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<Vec<u8>> {
|
||||
match o {
|
||||
Obj::Str(s, _) => Some(s.bytes()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct Font {
|
||||
charstrings: PDict,
|
||||
subrs: Vec<Vec<u8>>,
|
||||
len_iv: i64,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
fn of(font: &PDict) -> Option<Font> {
|
||||
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<u8> {
|
||||
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<Vec<u8>> {
|
||||
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<Seg>,
|
||||
x: f64,
|
||||
y: f64,
|
||||
open: bool,
|
||||
width: (f64, f64),
|
||||
sb: (f64, f64),
|
||||
stack: Vec<f64>,
|
||||
ps: Vec<f64>,
|
||||
flex: Option<Vec<(f64, f64)>>,
|
||||
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<Vec<u8>>) -> 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<f64> = 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<Glyph> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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", &[]);
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<u8>> {
|
||||
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<Seg>,
|
||||
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<Glyph> {
|
||||
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) })
|
||||
}
|
||||
Reference in new issue
Block a user