From 9b0c6ee2a79f361da04d0c6cd27b0339bc383a50 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 00:40:26 -0500 Subject: [PATCH] Delegate EPS preview parsing to craft-eps --- Cargo.lock | 26 +++---- Cargo.toml | 16 ++--- crates/images/src/eps.rs | 143 +++++---------------------------------- 3 files changed, 39 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 288950c98..80043a3d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,7 +1020,7 @@ dependencies = [ [[package]] name = "craft-codepage" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" [[package]] name = "craft-dialogs" @@ -1043,7 +1043,7 @@ source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13af [[package]] name = "craft-document-bundle" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-zip", "serde_json", @@ -1054,7 +1054,7 @@ dependencies = [ [[package]] name = "craft-drawingml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-xml", "kurbo", @@ -1065,12 +1065,12 @@ dependencies = [ [[package]] name = "craft-eps" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" [[package]] name = "craft-epub" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-xml", "craft-zip", @@ -1108,7 +1108,7 @@ dependencies = [ [[package]] name = "craft-idml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-xml", "craft-zip", @@ -1130,7 +1130,7 @@ dependencies = [ [[package]] name = "craft-opc" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-xml", "craft-zip", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "craft-rtf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-codepage", "encoding_rs", @@ -1189,12 +1189,12 @@ dependencies = [ [[package]] name = "craft-sheet" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" [[package]] name = "craft-svg" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-xml", "flate2", @@ -1273,7 +1273,7 @@ dependencies = [ [[package]] name = "craft-xlsx" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "craft-drawingml", "craft-opc", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "craft-xml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "quick-xml 0.41.0", "thiserror 2.0.21", @@ -1294,7 +1294,7 @@ dependencies = [ [[package]] name = "craft-zip" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=3afade51630fcdb02662c761e0805bc6147bb0ac#3afade51630fcdb02662c761e0805bc6147bb0ac" +source = "git+https://github.com/storytold/craft-libs?rev=ba24fe243c90e0f95c56eb6f82424be02a3ed77f#ba24fe243c90e0f95c56eb6f82424be02a3ed77f" dependencies = [ "flate2", "thiserror 2.0.21", diff --git a/Cargo.toml b/Cargo.toml index 881e9a19d..121b553b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,14 @@ repository = "https://github.com/storytold/designcraft" [workspace.dependencies] # Shared reader and writer; publish the immutable revision before this consumer. craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false } -craft-document-bundle = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } -craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "3afade51630fcdb02662c761e0805bc6147bb0ac", version = "=0.1.0" } +craft-document-bundle = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } +craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243c90e0f95c56eb6f82424be02a3ed77f", version = "=0.1.0" } # Shared keyboard bindings: chords, overrides, conflicts, egui dispatch, the Keyboard Shortcuts dialog. craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", features = ["egui", "ui"] } # Separations, ink coverage and the ink manager; same revision as craft-pdf and the krilla patch. diff --git a/crates/images/src/eps.rs b/crates/images/src/eps.rs index 2d5c85867..63f0e6b09 100644 --- a/crates/images/src/eps.rs +++ b/crates/images/src/eps.rs @@ -91,34 +91,12 @@ pub fn eps_proxy(bytes: &[u8]) -> Option<(Vec, (f64, f64))> { /// 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 = 4; -/// The deepest `gsave`/`save` nesting read. -const MAX_SAVE_DEPTH: usize = 64; -/// PostScript tokens without comments, `{` and `}` as tokens of their own. Lazy and borrowed, so -/// a huge input costs no memory beyond itself; lines may end in `\n`, `\r\n` or `\r`. -fn ps_tokens(text: &str) -> impl Iterator { - text.split(['\n', '\r']).flat_map(|line| { - line.split('%').next().unwrap_or("").split_whitespace().flat_map(|w| w.split_inclusive(['{', '}'])).flat_map(|piece| { - let (body, brace) = match piece.char_indices().last() { - Some((at, '{' | '}')) => (piece.get(..at).unwrap_or(""), piece.get(at..)), - _ => (piece, None), - }; - [Some(body).filter(|b| !b.is_empty()), brace].into_iter().flatten() - }) - }) -} - -/// 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. +/// Rasterize rectangles read by craft-eps; unsupported PostScript keeps the placeholder. fn render_rectangles(ps: &[u8], bb: [f64; 4]) -> Option> { - let text = std::str::from_utf8(ps).ok()?; + let rectangles = craft_eps::simple_rectangles(ps)?; let (w, h) = (bb[2] - bb[0], bb[3] - bb[1]); if !(w > 0.0 && h > 0.0) { return None; @@ -126,110 +104,25 @@ fn render_rectangles(ps: &[u8], bb: [f64; 4]) -> Option> { 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 = std::collections::HashMap::new(); - let mut stack: Vec = 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; - let mut saved: Vec<[u8; 4]> = Vec::new(); - let mut tokens = ps_tokens(text); - let mut read = 0usize; - let mut next = || { - read += 1; - if read > MAX_TOKENS { None } else { Some(tokens.next()) } - }; - let unit = |v: f64| v.clamp(0.0, 1.0); - // `None` from `next`: over the token limit; `Some(None)`: the end. - while let Some(tok) = next()? { - if let Ok(v) = tok.parse::() { - if !v.is_finite() { - return None; - } - stack.push(v); - continue; + for rect in rectangles { + let (x0, x1) = ((rect.x - bb[0]) * scale, (rect.x + rect.width - bb[0]) * scale); + let (y0, y1) = ((bb[3] - (rect.y + rect.height)) * scale, (bb[3] - rect.y) * 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; } - // `/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 mut body: Vec<&str> = Vec::new(); - while body.len() < 5 { - match next()? { - Some("def") | None => break, - Some(t) => body.push(t), - } + for y in ya..yb { + for x in xa..xb { + img.put_pixel(x, y, image::Rgba(rect.color)); } - let op = match body.as_slice() { - ["{", op, "}"] | ["{", op, "}", _] => (*op).to_string(), - _ => return None, - }; - aliases.insert(name.to_string(), op); - continue; } - let op = aliases.get(tok).map_or(tok, String::as_str); - let mut pop = |n: usize| -> Option> { (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)); - } - } - } - "gsave" | "save" => { - if saved.len() >= MAX_SAVE_DEPTH { - return None; - } - saved.push(color); - } - "grestore" | "restore" => { - if let Some(c) = saved.pop() { - color = c; - } - } - "pop" => { - pop(1)?; - } - "bind" | "newpath" | "showpage" | "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()?; @@ -305,14 +198,14 @@ mod tests { assert_eq!(drawn(eps, 28, 28), [0, 0, 0, 255], "bottom right: the colour from before gsave"); assert_eq!(drawn(eps, 28, 4)[3], 0, "top right: nothing (pop removed the extra operand)"); // Unbounded nesting isn't drawn. - let deep = format!("{} 0 0 2 2 rectfill", "gsave ".repeat(MAX_SAVE_DEPTH + 1)); + let deep = format!("{} 0 0 2 2 rectfill", "gsave ".repeat(craft_eps::MAX_SIMPLE_RECTANGLE_SAVE_DEPTH + 1)); assert_eq!(render_rectangles(deep.as_bytes(), [0.0, 0.0, 4.0, 4.0]), None); } #[test] fn a_huge_token_stream_on_one_line_is_refused() { // One line of more operands than the token limit, then a rectangle. - let ps = format!("{}\n0 0 2 2 rectfill\n", "0 ".repeat(MAX_TOKENS)); + let ps = format!("{}\n0 0 2 2 rectfill\n", "0 ".repeat(craft_eps::MAX_SIMPLE_RECTANGLE_TOKENS)); assert_eq!(render_rectangles(ps.as_bytes(), [0.0, 0.0, 4.0, 4.0]), None); let ps = format!("{}\n0 0 2 2 rectfill\n", "0 ".repeat(1000)); assert!(render_rectangles(ps.as_bytes(), [0.0, 0.0, 4.0, 4.0]).is_some(), "under the limit it is drawn");