mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
P: place EPS through its preview or a bounding-box placeholder
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e95f562425
commit
bfbe9661e1
8 files changed
+132
-2
No files matched your search
Generated
+1
@@ -935,6 +935,7 @@ dependencies = [
|
||||
"designcraft-format",
|
||||
"designcraft-geom",
|
||||
"designcraft-idml",
|
||||
"designcraft-images",
|
||||
"designcraft-pdf",
|
||||
"designcraft-render",
|
||||
"designcraft-textimport",
|
||||
|
||||
@@ -97,6 +97,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English.
|
||||
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
|
||||
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
|
||||
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
||||
@@ -115,6 +115,7 @@ fn services() -> Services {
|
||||
"svg",
|
||||
"pdf",
|
||||
"ai",
|
||||
"eps",
|
||||
"txt",
|
||||
"docx",
|
||||
"rtf",
|
||||
@@ -132,7 +133,7 @@ fn services() -> Services {
|
||||
"ogg",
|
||||
],
|
||||
)
|
||||
.add_filter("Graphics", &["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "psd", "svg", "pdf", "ai"])
|
||||
.add_filter("Graphics", &["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "psd", "svg", "pdf", "ai", "eps"])
|
||||
.add_filter("Text (Word, RTF, plain, Excel)", &["docx", "rtf", "txt", "md", "xlsx"])
|
||||
.add_filter("Video and sound", &["mp4", "m4v", "mov", "webm", "mp3", "m4a", "wav", "ogg"])
|
||||
} else {
|
||||
|
||||
@@ -9,6 +9,7 @@ description = "DesignCraft engine: session, documents, history, selection, comma
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
designcraft-images = { workspace = true }
|
||||
designcraft-geom = { workspace = true }
|
||||
designcraft-color = { workspace = true }
|
||||
designcraft-doc = { workspace = true }
|
||||
|
||||
@@ -219,6 +219,13 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if lname.ends_with(".idml") || lname.ends_with(".designcraft") {
|
||||
return place_layout_page(s, p, &name, &bytes);
|
||||
}
|
||||
// EPS: shown and printed through its preview (or a placeholder) at its bounding box size.
|
||||
let (bytes, eps_size) = if designcraft_images::is_eps(&bytes) {
|
||||
let (proxy, size) = designcraft_images::eps_proxy(&bytes).ok_or_else(|| bad("file.place", "the EPS has no bounding box"))?;
|
||||
(proxy, Some(size))
|
||||
} else {
|
||||
(bytes, None)
|
||||
};
|
||||
// Video and sound: a media frame (Window › Interactive › Media).
|
||||
if let Some(mime) = designcraft_doc::media_mime(&name) {
|
||||
return super::media::place_media(s, p, name, mime, bytes, link);
|
||||
@@ -245,7 +252,7 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.ok_or_else(|| bad("file.place", "can't read that PDF page"))?,
|
||||
};
|
||||
// 72 ppi by default unless the file says otherwise; scale so it fits the page when huge.
|
||||
let (nw, nh) = (pw as f64, ph as f64);
|
||||
let (nw, nh) = eps_size.unwrap_or((pw as f64, ph as f64));
|
||||
// Image Import Options › Crop to: the frame shows that box of the PDF page.
|
||||
let crop_box = match str_param(p, "pdfCrop") {
|
||||
Some(k) if designcraft_render::is_pdf(&bytes) => {
|
||||
|
||||
@@ -184,4 +184,17 @@ mod tests {
|
||||
let x = read("OEBPS/content.xhtml");
|
||||
assert!(x.contains("<video src=") && x.contains("loop=") && x.contains("poster="), "{x}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_places_at_its_bounding_box() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 10 154 82\nnewpath\n%%EOF\n";
|
||||
let r = s.execute("file.place", &json!({"base64": super::super::file::base64_encode(eps), "name": "logo.eps", "x": 72, "y": 72})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let b = d.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().bounds();
|
||||
assert!((b.width() - 144.0).abs() < 1e-6 && (b.height() - 72.0).abs() < 1e-6, "{b:?}");
|
||||
// Renders (the placeholder) and exports.
|
||||
assert!(s.execute("file.exportPdf", &json!({})).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! 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.
|
||||
|
||||
/// An EPS file (plain, or DOS EPS with a binary header)?
|
||||
pub fn is_eps(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
|
||||
|| (bytes.starts_with(b"%!PS-Adobe") && String::from_utf8_lossy(&bytes[..bytes.len().min(64)]).contains("EPSF"))
|
||||
}
|
||||
|
||||
fn u32le(b: &[u8], at: usize) -> Option<usize> {
|
||||
Some(u32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?) as usize)
|
||||
}
|
||||
|
||||
/// The PostScript section and the TIFF preview (DOS EPS).
|
||||
fn sections(bytes: &[u8]) -> (&[u8], Option<&[u8]>) {
|
||||
if bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) {
|
||||
let ps = u32le(bytes, 4).zip(u32le(bytes, 8)).and_then(|(o, l)| bytes.get(o..o.checked_add(l)?)).unwrap_or(&[]);
|
||||
let tiff = u32le(bytes, 20).zip(u32le(bytes, 24)).filter(|(o, l)| *o > 0 && *l > 0).and_then(|(o, l)| bytes.get(o..o.checked_add(l)?));
|
||||
return (ps, tiff);
|
||||
}
|
||||
(bytes, None)
|
||||
}
|
||||
|
||||
/// The bounding box in points: (x0, y0, x1, y1), the high-resolution one when given.
|
||||
pub fn bounding_box(bytes: &[u8]) -> Option<[f64; 4]> {
|
||||
let (ps, _) = sections(bytes);
|
||||
let head = String::from_utf8_lossy(&ps[..ps.len().min(64 * 1024)]);
|
||||
let read = |key: &str| {
|
||||
head.lines().find_map(|l| {
|
||||
let v: Vec<f64> = l.strip_prefix(key)?.split_whitespace().filter_map(|t| t.parse().ok()).collect();
|
||||
(v.len() == 4 && v[2] > v[0] && v[3] > v[1]).then(|| [v[0], v[1], v[2], v[3]])
|
||||
})
|
||||
};
|
||||
read("%%HiResBoundingBox:").or_else(|| read("%%BoundingBox:"))
|
||||
}
|
||||
|
||||
/// The proxy for a placed EPS: (image bytes, its size in points). The TIFF preview when present,
|
||||
/// else a light grey placeholder with a cross, at 2 pixels per point.
|
||||
pub fn eps_proxy(bytes: &[u8]) -> Option<(Vec<u8>, (f64, f64))> {
|
||||
let bb = bounding_box(bytes)?;
|
||||
let (w, h) = (bb[2] - bb[0], bb[3] - bb[1]);
|
||||
if let (_, Some(tiff)) = sections(bytes)
|
||||
&& crate::pixel_size(tiff).is_some()
|
||||
{
|
||||
return Some((tiff.to_vec(), (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.
|
||||
let n = pw.max(ph);
|
||||
for i in 0..n {
|
||||
let (x, y) = ((i as f64 / n as f64 * pw as f64) as u32, (i as f64 / n as f64 * ph as f64) as u32);
|
||||
for (px, py) in [(x, y), (x, ph - 1 - y.min(ph - 1))] {
|
||||
img.put_pixel(px.min(pw - 1), py.min(ph - 1), image::Rgba([150, 150, 150, 255]));
|
||||
}
|
||||
}
|
||||
for x in 0..pw {
|
||||
img.put_pixel(x, 0, image::Rgba([150, 150, 150, 255]));
|
||||
img.put_pixel(x, ph - 1, image::Rgba([150, 150, 150, 255]));
|
||||
}
|
||||
for y in 0..ph {
|
||||
img.put_pixel(0, y, image::Rgba([150, 150, 150, 255]));
|
||||
img.put_pixel(pw - 1, y, image::Rgba([150, 150, 150, 255]));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
|
||||
Some((out, (w, h)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[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";
|
||||
assert!(is_eps(eps));
|
||||
assert_eq!(bounding_box(eps), Some([0.0, 0.0, 100.5, 50.25]));
|
||||
let (png, size) = eps_proxy(eps).unwrap();
|
||||
assert_eq!(size, (100.5, 50.25));
|
||||
assert_eq!(crate::pixel_size(&png), Some((201, 101)));
|
||||
// DOS EPS with a TIFF preview: the preview is the proxy.
|
||||
let mut tiff = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(10, 5, image::Rgba([255, 0, 0, 255])))
|
||||
.write_to(&mut std::io::Cursor::new(&mut tiff), image::ImageFormat::Tiff)
|
||||
.unwrap();
|
||||
let mut dos = vec![0xC5, 0xD0, 0xD3, 0xC6];
|
||||
let ps_off = 30usize;
|
||||
let tiff_off = ps_off + eps.len();
|
||||
for v in [ps_off, eps.len(), 0, 0, tiff_off, tiff.len()] {
|
||||
dos.extend((v as u32).to_le_bytes());
|
||||
}
|
||||
dos.extend([0xFF, 0xFF]);
|
||||
dos.extend_from_slice(eps);
|
||||
dos.extend_from_slice(&tiff);
|
||||
assert!(is_eps(&dos));
|
||||
let (p, size) = eps_proxy(&dos).unwrap();
|
||||
assert_eq!(size, (100.5, 50.25));
|
||||
assert_eq!(crate::pixel_size(&p), Some((10, 5)));
|
||||
assert!(!is_eps(b"%!PS-Adobe-3.0\nnot encapsulated"));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ use std::sync::{Arc, OnceLock};
|
||||
|
||||
pub use resvg::usvg;
|
||||
|
||||
mod eps;
|
||||
pub use eps::{bounding_box as eps_bounding_box, eps_proxy, is_eps};
|
||||
|
||||
/// CSS pixels (SVG user units) → points.
|
||||
pub const PT_PER_PX: f64 = 0.75;
|
||||
|
||||
|
||||
Reference in new issue
Block a user