Images: EPS shows through its proxy at display time; simple PostScript (QR codes) is drawn

An EPS that came in with a document (InDesign embeds its QR codes as
EPS) drew nothing: only File › Place converted EPS to a proxy. Now
`decode_rgba` draws any EPS through `eps_proxy` and `natural_size` measures
it by its bounding box in points, so screen, PNG and PDF show it while the
document keeps the EPS itself.

`eps_proxy` also draws PostScript that only fills rectangles in solid
colours (setcmykcolor, setrgbcolor, setgray, rectfill and procedures that
stand for them, as InDesign's QR codes are written), at 8 px per point so
modules stay crisp. Anything else keeps the preview or placeholder. Input is
bounded: tokens, rectangles, painted area and procedure definitions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Govinda RoyandClaude Opus 5.5 committed 2026-10-14 00:26:02 +06:00
1 parent e862e0a613
commit 677cafbf9c
2 files changed
+166 -3

No files matched your search

+144 -3
View File
@@ -1,6 +1,7 @@
//! EPS: there's no PostScript interpreter here, so a placed EPS shows (and prints) through a
//! proxy, as InDesign does on screen: the file's TIFF preview when it has one, else a placeholder
//! the size of its bounding box.
//! EPS: there's no general PostScript interpreter here, so a placed EPS shows (and prints)
//! through a proxy, as InDesign does on screen: the file's TIFF preview when it has one, else the
//! rectangles of a simple one drawn here (InDesign's QR codes), else a placeholder the size of its
//! bounding box.
/// An EPS file (plain, or DOS EPS with a binary header)?
pub fn is_eps(bytes: &[u8]) -> bool {
@@ -45,6 +46,10 @@ pub fn eps_proxy(bytes: &[u8]) -> Option<(Vec<u8>, (f64, f64))> {
{
return Some((tiff.to_vec(), (w, h)));
}
// Simple PostScript drawn here: InDesign's QR codes are coloured rectangles only.
if let Some(png) = render_rectangles(sections(bytes).0, bb) {
return Some((png, (w, h)));
}
let (pw, ph) = ((w * 2.0).round().clamp(2.0, 4000.0) as u32, (h * 2.0).round().clamp(2.0, 4000.0) as u32);
let mut img = image::RgbaImage::from_pixel(pw, ph, image::Rgba([228, 228, 228, 255]));
// Diagonals and a border mark it as a stand-in.
@@ -68,10 +73,146 @@ pub fn eps_proxy(bytes: &[u8]) -> Option<(Vec<u8>, (f64, f64))> {
Some((out, (w, h)))
}
/// Pixels per point for rendered EPS (a QR code's modules stay crisp when scaled up on screen).
const RENDER_SCALE: f64 = 8.0;
/// The most rectangles and operators read from an EPS before giving up on drawing it.
const MAX_RECTS: usize = 200_000;
const MAX_TOKENS: usize = 2_000_000;
/// The most pixels painted, in multiples of the image's area (overlapping rectangles repaint).
const MAX_PAINT_AREAS: u64 = 64;
/// PostScript that only fills rectangles in solid colours (`setcmykcolor`, `setrgbcolor`,
/// `setgray`, `rectfill`, and procedures that stand for one of them, such as InDesign's
/// `/rf { rectfill } bind def`), drawn as a PNG of the bounding box `bb`. None for anything else
/// (paths, text, images, transforms), which keeps its placeholder, or when nothing is drawn.
fn render_rectangles(ps: &[u8], bb: [f64; 4]) -> Option<Vec<u8>> {
let text = std::str::from_utf8(ps).ok()?;
let (w, h) = (bb[2] - bb[0], bb[3] - bb[1]);
if !(w > 0.0 && h > 0.0) {
return None;
}
let scale = RENDER_SCALE.min(4000.0 / w.max(h));
let (pw, ph) = ((w * scale).round().max(1.0) as u32, (h * scale).round().max(1.0) as u32);
let mut img = image::RgbaImage::from_pixel(pw, ph, image::Rgba([0, 0, 0, 0]));
let mut aliases: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut stack: Vec<f64> = Vec::new();
let mut color = [0u8, 0, 0, 255];
let mut drawn = 0usize;
let mut painted = 0u64;
let paint_budget = u64::from(pw) * u64::from(ph) * MAX_PAINT_AREAS;
// Tokens without comments; `{ … }` procedures kept whole.
let mut tokens: Vec<String> = Vec::new();
for line in text.lines() {
let line = line.split('%').next().unwrap_or("");
let spaced = line.replace('{', " { ").replace('}', " } ");
tokens.extend(spaced.split_whitespace().map(str::to_string));
if tokens.len() > MAX_TOKENS {
return None;
}
}
let unit = |v: f64| v.clamp(0.0, 1.0);
let mut i = 0;
while let Some(tok) = tokens.get(i) {
i += 1;
if let Ok(v) = tok.parse::<f64>() {
if !v.is_finite() {
return None;
}
stack.push(v);
continue;
}
// `/name { op } bind def`: name stands for op.
if let Some(name) = tok.strip_prefix('/') {
// At most `{ op } bind`: anything longer isn't an alias this reader knows.
let body: Vec<&String> = tokens.iter().skip(i).take(5).take_while(|t| *t != "def").collect();
let op = match body.as_slice() {
[open, op, close] | [open, op, close, _] if *open == "{" && *close == "}" => (*op).clone(),
_ => return None,
};
aliases.insert(name.to_string(), op);
i += body.len() + 1;
continue;
}
let op = aliases.get(tok).map_or(tok.as_str(), String::as_str);
let mut pop = |n: usize| -> Option<Vec<f64>> { (stack.len() >= n).then(|| stack.split_off(stack.len() - n)) };
match op {
"setcmykcolor" => {
let v = pop(4)?;
let k = unit(v[3]);
let ch = |c: f64| ((1.0 - unit(c)) * (1.0 - k) * 255.0).round() as u8;
color = [ch(v[0]), ch(v[1]), ch(v[2]), 255];
}
"setrgbcolor" => {
let v = pop(3)?;
let ch = |c: f64| (unit(c) * 255.0).round() as u8;
color = [ch(v[0]), ch(v[1]), ch(v[2]), 255];
}
"setgray" => {
let v = pop(1)?;
let g = (unit(v[0]) * 255.0).round() as u8;
color = [g, g, g, 255];
}
"rectfill" => {
let v = pop(4)?;
drawn += 1;
if drawn > MAX_RECTS {
return None;
}
// PostScript y runs up; the image's down. Edges round to whole pixels, so
// touching rectangles (QR code modules) meet without gaps.
let (x0, x1) = ((v[0] - bb[0]) * scale, (v[0] + v[2] - bb[0]) * scale);
let (y0, y1) = ((bb[3] - (v[1] + v[3])) * scale, (bb[3] - v[1]) * scale);
let px = |a: f64, b: f64, max: u32| {
let (lo, hi) = (a.min(b).round().max(0.0), a.max(b).round().min(max as f64));
(lo as u32, hi.max(lo) as u32)
};
let ((xa, xb), (ya, yb)) = (px(x0, x1, pw), px(y0, y1, ph));
painted = painted.saturating_add(u64::from(xb - xa) * u64::from(yb - ya));
if painted > paint_budget {
return None;
}
for y in ya..yb {
for x in xa..xb {
img.put_pixel(x, y, image::Rgba(color));
}
}
}
"bind" | "gsave" | "grestore" | "newpath" | "showpage" | "save" | "restore" | "pop" | "end" | "begin" => {}
_ => return None,
}
}
if drawn == 0 {
return None;
}
let mut out = Vec::new();
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_rectangle_eps_is_drawn_like_a_qr_code() {
// As InDesign writes QR codes: a procedure for rectfill, a colour, then modules.
let eps = b"%!PS-Adobe-3.1 EPSF-3.0\n%%BoundingBox: 0 0 4 4\n%%EndComments\n/rf { rectfill } bind def\n0 0 0 1 setcmykcolor\n0 2 2 2 rf\n2 0 2 2 rf\n%%EOF\n";
let (png, size) = eps_proxy(eps).unwrap();
assert_eq!(size, (4.0, 4.0));
let img = image::load_from_memory(&png).unwrap().to_rgba8();
assert_eq!(img.dimensions(), (32, 32), "8 pixels per point");
// PostScript y runs up: (0, 2)–(2, 4) is the image's top left.
assert_eq!(img.get_pixel(4, 4).0, [0, 0, 0, 255]);
assert_eq!(img.get_pixel(28, 28).0, [0, 0, 0, 255]);
assert_eq!(img.get_pixel(28, 4).0[3], 0, "undrawn stays transparent");
assert_eq!(img.get_pixel(4, 28).0[3], 0);
// Anything else (here a path) keeps the placeholder.
let curve = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 4 4\n0 0 moveto 4 4 lineto stroke\n%%EOF\n";
let (png, _) = eps_proxy(curve).unwrap();
let img = image::load_from_memory(&png).unwrap().to_rgba8();
assert_eq!(img.get_pixel(2, 4).0, [228, 228, 228, 255]);
}
#[test]
fn eps_bounding_box_and_proxies() {
let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 50\n%%HiResBoundingBox: 0 0 100.5 50.25\nnewpath\n%%EOF\n";
+22
View File
@@ -140,6 +140,10 @@ pub fn natural_size(bytes: &[u8]) -> Option<(f64, f64)> {
let s = svg_tree(bytes)?.size();
return Some((s.width() as f64 * PT_PER_PX, s.height() as f64 * PT_PER_PX));
}
if is_eps(bytes) {
let bb = eps::bounding_box(bytes)?;
return Some((bb[2] - bb[0], bb[3] - bb[1])).filter(|(w, h)| w.is_finite() && h.is_finite() && *w > 0.0 && *h > 0.0);
}
let (w, h) = pixel_size(bytes)?;
Some((w as f64, h as f64))
}
@@ -159,6 +163,11 @@ pub fn decode_rgba(bytes: &[u8]) -> Option<image::RgbaImage> {
if is_psd(bytes) {
return psd::decode(bytes);
}
// An EPS is drawn as its proxy (preview, simple PostScript or placeholder); the document
// keeps the EPS itself.
if is_eps(bytes) {
return decode_rgba(&eps_proxy(bytes)?.0);
}
Some(decode_to_rgba8(&image::load_from_memory(bytes).ok()?))
}
@@ -386,6 +395,19 @@ mod tests {
b
}
/// A placed EPS is measured by its bounding box in points and drawn through its proxy; the
/// bytes stay the EPS.
#[test]
fn eps_draws_through_its_proxy_at_its_bounding_box_size() {
let eps =
b"%!PS-Adobe-3.1 EPSF-3.0\n%%BoundingBox: 0 0 4 2\n%%EndComments\n/rf { rectfill } bind def\n0 0 0 1 setcmykcolor\n0 0 2 2 rf\n%%EOF\n";
assert_eq!(natural_size(eps), Some((4.0, 2.0)));
let img = decode_rgba(eps).expect("drawn");
assert_eq!(img.dimensions(), (32, 16));
assert_eq!(img.get_pixel(1, 1).0, [0, 0, 0, 255], "the rectangle");
assert_eq!(img.get_pixel(30, 1).0[3], 0, "outside it, transparent");
}
/// CMYK TIFFs read back as their ink values; RGB ones aren't CMYK.
#[test]
fn cmyk_tiff_keeps_inks() {