Merge remote-tracking branch 'origin/main' into codex/jpeg-baseline-encoder

This commit is contained in:
Dylan M. Taylor committed 2026-11-14 11:13:33 +00:00
commit 1a950613f9
33 files changed
+11827 -13

No files matched your search

Generated
+12
View File
@@ -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"
+5
View File
@@ -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"] }
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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).
+2 -2
View File
@@ -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};
+18
View File
@@ -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");
+27
View File
@@ -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"]
+50
View File
@@ -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.
+41
View File
@@ -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
}
}
}
+9
View File
@@ -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;
+604
View File
@@ -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()
}
}
+252
View File
@@ -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}"))
}
+549
View File
@@ -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));
}
}
+318
View File
@@ -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"));
}
}
+751
View File
@@ -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);
}
}
+399
View File
@@ -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));
}
}
+747
View File
@@ -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)
}
+118
View File
@@ -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;
+301
View File
@@ -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
+374
View File
@@ -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)
}
+262
View File
@@ -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 }))
}
+789
View File
@@ -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, &params, 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))
}
+228
View File
@@ -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);
}
+389
View File
@@ -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)
}
+629
View File
@@ -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);
}
}
}
+375
View File
@@ -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());
}
}
+986
View File
@@ -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());
}
}
+755
View File
@@ -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);
}
}
+357
View File
@@ -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", &[]);
}
}
+81
View File
@@ -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) })
}