mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Import: placed PDFs cropped to their bounding box render only when drawn or exported
IDML import found every Bounding Box crop by rendering the PDF page at 2048 pixels, once per placement, so opening a 17-page document that places one Illustrator file 16 times took about 54 s of CPU. Import now measures the box only for a graphic without GraphicBounds; drawing finds it through designcraft_render::shown_box (cached per file, page and crop) and PDF export through with_pdf_boxes.
This commit is contained in:
+14
-3
@@ -453,7 +453,8 @@ pub struct Asset {
|
||||
pub pdf_crop: PdfCrop,
|
||||
/// Where the `pdf_crop` box sits on the page as rendered (its visible crop box, rotated as it
|
||||
/// displays): left, top, right, bottom as fractions of the page's width and height. Graphic
|
||||
/// space (0,0)-(w,h) spans this box. `None`: the whole page.
|
||||
/// space (0,0)-(w,h) spans this box. `None`: the whole page, or a Bounding Box crop not
|
||||
/// measured yet (`designcraft_render::shown_box` measures it when the graphic is drawn).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pdf_box: Option<[f64; 4]>,
|
||||
}
|
||||
@@ -461,11 +462,21 @@ pub struct Asset {
|
||||
impl Asset {
|
||||
/// The part of the page a placed PDF shows, when [`Asset::pdf_box`] holds a usable box.
|
||||
pub fn shown_box(&self) -> Option<[f64; 4]> {
|
||||
let b = self.pdf_box.filter(|_| self.mime == "application/pdf")?;
|
||||
self.shown(self.pdf_box?)
|
||||
}
|
||||
|
||||
/// `b` as the part of the page this asset shows: a usable box of a placed PDF.
|
||||
pub fn shown(&self, b: [f64; 4]) -> Option<[f64; 4]> {
|
||||
// A box under 1/10000 of the page, or far outside it, would blow the page up to sizes
|
||||
// nothing can draw.
|
||||
let usable = b.iter().all(|v| v.is_finite() && v.abs() <= 1e3) && b[2] - b[0] >= 1e-4 && b[3] - b[1] >= 1e-4;
|
||||
usable.then_some(b)
|
||||
(usable && self.mime == "application/pdf").then_some(b)
|
||||
}
|
||||
|
||||
/// A Bounding Box crop ([`PdfCrop::ContentVisible`], [`PdfCrop::ContentAll`]) whose box isn't
|
||||
/// stored yet: finding it needs the page rendered.
|
||||
pub fn content_box_pending(&self) -> bool {
|
||||
self.pdf_box.is_none() && matches!(self.pdf_crop, PdfCrop::ContentVisible | PdfCrop::ContentAll) && self.mime == "application/pdf"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
// The chapter's own fonts, for this export (skipped font files are logged).
|
||||
let _fonts = super::file::load_document_fonts(&mut d, path).0;
|
||||
let cache = designcraft_compose::Cache::new();
|
||||
let r = designcraft_pdf::export_pdf_with_report(&d, &cache, &designcraft_pdf::PdfOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let r = designcraft_pdf::export_pdf_with_report(&super::interchange::with_pdf_boxes(&d), &cache, &designcraft_pdf::PdfOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let n = designcraft_render::pdf_page_count(&r.bytes).unwrap_or(0);
|
||||
let sizes: Vec<(f32, f32)> = (0..n).filter_map(|i| designcraft_render::pdf_page_size(&r.bytes, i)).map(|(w, h)| (w as f32, h as f32)).collect();
|
||||
parts.push((r.bytes, sizes));
|
||||
|
||||
@@ -40,7 +40,8 @@ fn print_booklet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let opts = designcraft_pdf::BookletOptions { kind, space_between: p.get("spaceBetween").and_then(Value::as_f64).unwrap_or(0.0), title: None };
|
||||
let st = s.doc()?;
|
||||
let r = designcraft_pdf::export_booklet(&st.doc, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let r = designcraft_pdf::export_booklet(&super::interchange::with_pdf_boxes(&st.doc), &s.cache, &opts)
|
||||
.map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -460,7 +461,7 @@ fn rasterize_layered(d: &designcraft_doc::Document) -> (Option<designcraft_doc::
|
||||
continue;
|
||||
}
|
||||
// The whole page at 300 ppi (the graphic may show only a box of it).
|
||||
let shown = a.shown_box();
|
||||
let shown = designcraft_render::shown_box(&a);
|
||||
let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]);
|
||||
let page_side = (g.size.0 / (r - l)).max(g.size.1 / (b - t));
|
||||
let side = (page_side * 300.0 / 72.0).clamp(64.0, 8000.0) as u32;
|
||||
@@ -514,6 +515,8 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base2: &designcraft_doc::Document = layered.as_ref().unwrap_or(base);
|
||||
let (fx_doc, fx_count) = rasterize_effects(base2, &s.cache);
|
||||
let doc: &designcraft_doc::Document = fx_doc.as_ref().unwrap_or(base2);
|
||||
let boxed = super::interchange::with_pdf_boxes(doc);
|
||||
let doc: &designcraft_doc::Document = &boxed;
|
||||
let r = designcraft_pdf::export_pdf_with_report(doc, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let mut r = r;
|
||||
// Transparency Blend Space: the page group of pages with transparency.
|
||||
|
||||
@@ -68,15 +68,29 @@ pub fn import(bytes: &[u8], dir: Option<&std::path::Path>) -> Result<Document> {
|
||||
|
||||
/// Placed PDFs read from IDML name their crop (`PDFCrop`): find where each box sits on its page,
|
||||
/// which the IDML reader can't (it doesn't parse PDFs). The box spans the graphic's
|
||||
/// `GraphicBounds`; a graphic without bounds takes the box's size.
|
||||
/// `GraphicBounds`; a graphic without bounds takes the box's size. Bounding Box crops render the
|
||||
/// page to find the box, so they wait for drawing or export ([`designcraft_render::shown_box`],
|
||||
/// [`with_pdf_boxes`]) unless a graphic needs the box's size now.
|
||||
pub(crate) fn resolve_pdf_crops(d: &mut Document) {
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use designcraft_doc::{AssetId, Content, Item, PdfCrop};
|
||||
let mut no_bounds: HashSet<AssetId> = HashSet::new();
|
||||
for sp in d.spreads.iter().chain(&d.parents) {
|
||||
for it in &sp.items {
|
||||
it.walk(&mut |i| {
|
||||
if let Content::Graphic(g) = &i.content
|
||||
&& !(g.size.0 > 0.0 && g.size.1 > 0.0)
|
||||
{
|
||||
no_bounds.insert(g.asset);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut sizes: HashMap<AssetId, (f64, f64)> = HashMap::new();
|
||||
for a in d.assets.values_mut() {
|
||||
if a.pdf_crop == PdfCrop::Crop || a.pdf_box.is_some() {
|
||||
if a.pdf_crop == PdfCrop::Crop || a.pdf_box.is_some() || (a.content_box_pending() && !no_bounds.contains(&a.id)) {
|
||||
continue;
|
||||
}
|
||||
let a = Arc::make_mut(a);
|
||||
@@ -112,6 +126,22 @@ pub(crate) fn resolve_pdf_crops(d: &mut Document) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `d` with the boxes of its Bounding Box crops measured ([`resolve_pdf_crops`] leaves them for
|
||||
/// later), for writers that don't render: PDF export places the page by the box.
|
||||
pub(crate) fn with_pdf_boxes(d: &Document) -> std::borrow::Cow<'_, Document> {
|
||||
if !d.assets.values().any(|a| a.content_box_pending()) {
|
||||
return std::borrow::Cow::Borrowed(d);
|
||||
}
|
||||
let mut out = d.clone();
|
||||
for a in out.assets.values_mut() {
|
||||
if a.content_box_pending() {
|
||||
// No usable box: the whole page.
|
||||
std::sync::Arc::make_mut(a).pdf_box = designcraft_render::shown_box(a);
|
||||
}
|
||||
}
|
||||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_LINK_PATH_BYTES: usize = 128 * 1024;
|
||||
|
||||
|
||||
@@ -1612,7 +1612,7 @@ fn clipping_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let Some(asset) = d.assets.get(&g.asset) else { continue };
|
||||
let Some(px) = designcraft_render::decode_pixmap_page(&asset.data, asset.page) else { continue };
|
||||
// A placed PDF cropped to a box: trace that box, which graphic space spans.
|
||||
let px = match asset.shown_box() {
|
||||
let px = match designcraft_render::shown_box(asset) {
|
||||
Some(frac) => designcraft_render::crop_pixmap(&px, frac).unwrap_or(px),
|
||||
None => px,
|
||||
};
|
||||
|
||||
@@ -401,7 +401,8 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
if p.get("pdf").and_then(Value::as_bool).unwrap_or(false) {
|
||||
let path = package_output_path(&dir, &title, "pdf")?;
|
||||
let bytes = designcraft_pdf::export_pdf(&packed, &s.cache, &Default::default()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let bytes = designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(&packed), &s.cache, &Default::default())
|
||||
.map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
root.write(&path, &bytes)?;
|
||||
files.push(path);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999);
|
||||
let printer = str_param(p, "printer").map(str::to_string);
|
||||
let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
|
||||
let pdf = designcraft_pdf::export_pdf(d, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let pdf =
|
||||
designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(d), &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let mut file = secure_print_file()?;
|
||||
let file_path = file.path().to_path_buf();
|
||||
let mut cmd: Vec<String> = vec!["lpr".into()];
|
||||
|
||||
@@ -357,3 +357,32 @@ fn idml_placed_pdf_cropped_to_its_media_box_gets_media_box_bounds() {
|
||||
let b = a.shown_box().unwrap_or([0.0, 0.0, 1.0, 1.0]);
|
||||
assert!(b.iter().zip([0.0, 0.0, 1.0, 1.0]).all(|(x, y)| (x - y).abs() < 1e-6), "{b:?}");
|
||||
}
|
||||
|
||||
/// A Bounding Box crop needs the page rendered to find its box. IDML import leaves that until
|
||||
/// the graphic is drawn or exported (its `GraphicBounds` give its size), so opening a document
|
||||
/// doesn't render every placed PDF; drawing and PDF export find the same box placing it did.
|
||||
#[test]
|
||||
fn idml_bounding_box_crop_is_measured_when_drawn_not_on_import() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [100, 200, 300, 260], "content": "unassigned"})).unwrap();
|
||||
s.execute("object.fill", &json!({"swatch": "[Black]"})).unwrap();
|
||||
let b64 = s.execute("file.exportPdf", &json!({})).unwrap()["base64"].as_str().unwrap().to_string();
|
||||
let mut t = Session::new();
|
||||
t.execute("file.new", &json!({})).unwrap();
|
||||
t.execute("file.place", &json!({"base64": b64, "name": "a.pdf", "pdfCrop": "contentVisible", "width": 200, "x": 20, "y": 30})).unwrap();
|
||||
let placed = t.doc().unwrap().doc.assets.values().next().unwrap().shown_box().expect("placing measures the box");
|
||||
assert!(placed[0] > 0.1 && placed[2] < 0.6, "the drawn rectangle, not the page: {placed:?}");
|
||||
|
||||
let bytes = designcraft_idml::export_idml(&t.doc().unwrap().doc);
|
||||
let back = crate::cmd::interchange::import(&bytes, None).unwrap();
|
||||
let a = back.assets.values().next().unwrap();
|
||||
assert_eq!(a.pdf_crop, designcraft_doc::PdfCrop::ContentVisible);
|
||||
assert!(a.content_box_pending(), "import didn't render the page: {:?}", a.pdf_box);
|
||||
let close = |b: [f64; 4]| b.iter().zip(placed).all(|(x, y)| (x - y).abs() < 1e-9);
|
||||
let drawn = designcraft_render::shown_box(a).expect("drawing measures the box");
|
||||
assert!(close(drawn), "{drawn:?} vs {placed:?}");
|
||||
let boxed = crate::cmd::interchange::with_pdf_boxes(&back);
|
||||
let exported = boxed.assets.values().next().unwrap().shown_box().expect("PDF export gets the box");
|
||||
assert!(close(exported), "{exported:?} vs {placed:?}");
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ impl Renderer {
|
||||
let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone());
|
||||
let data = if !hidden.is_empty() && is_pdf(&asset.data) { pdf_layers::layered(&asset.data, &hidden) } else { asset.data.clone() };
|
||||
// A placed PDF cropped to a box: the whole page, placed so that the box spans graphic space.
|
||||
let [l, t, r, b] = asset.shown_box().unwrap_or([0.0, 0.0, 1.0, 1.0]);
|
||||
let [l, t, r, b] = shown_box(asset).unwrap_or([0.0, 0.0, 1.0, 1.0]);
|
||||
let (page_w, page_h) = (g.size.0 / (r - l), g.size.1 / (b - t));
|
||||
let page = Rect::new(-l * page_w, -t * page_h, (1.0 - l) * page_w, (1.0 - t) * page_h);
|
||||
let Some(pm) = images::mip(&data, asset.page, page_w, on_screen) else { return };
|
||||
@@ -1121,6 +1121,39 @@ pub fn pdf_crop_box(bytes: &[u8], page: usize, crop: designcraft_doc::PdfCrop) -
|
||||
(frac.iter().all(|v| v.is_finite()) && size.0 > 0.0 && size.1 > 0.0).then_some((frac, size))
|
||||
}
|
||||
|
||||
/// The part of the page a placed PDF shows ([`designcraft_doc::Asset::shown_box`]). A Bounding
|
||||
/// Box crop that import left unmeasured is measured here, once per file, page and crop: that
|
||||
/// renders the page, which only drawing or exporting the graphic needs.
|
||||
pub fn shown_box(asset: &designcraft_doc::Asset) -> Option<[f64; 4]> {
|
||||
if !asset.content_box_pending() {
|
||||
return asset.shown_box();
|
||||
}
|
||||
asset.shown(content_box(&asset.data, asset.page, asset.pdf_crop)?)
|
||||
}
|
||||
|
||||
type ContentBoxes = HashMap<(usize, u32, designcraft_doc::PdfCrop), (std::sync::Weak<Vec<u8>>, Option<[f64; 4]>)>;
|
||||
|
||||
/// [`pdf_crop_box`]'s box, cached by the file's allocation. The weak reference keeps that address
|
||||
/// from naming another file while the entry lives.
|
||||
fn content_box(bytes: &Arc<Vec<u8>>, page: u32, crop: designcraft_doc::PdfCrop) -> Option<[f64; 4]> {
|
||||
static CACHE: std::sync::Mutex<Option<ContentBoxes>> = std::sync::Mutex::new(None);
|
||||
let key = (Arc::as_ptr(bytes) as usize, page, crop);
|
||||
let cached = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref().and_then(|c| c.get(&key)).and_then(|(file, b)| {
|
||||
let same = file.upgrade().is_some_and(|f| Arc::ptr_eq(&f, bytes));
|
||||
same.then_some(*b)
|
||||
});
|
||||
if let Some(b) = cached {
|
||||
return b;
|
||||
}
|
||||
// Render outside the lock: other graphics keep drawing meanwhile.
|
||||
let b = pdf_crop_box(bytes, page as usize, crop).map(|(frac, _)| frac);
|
||||
let mut cache = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let cache = cache.get_or_insert_with(HashMap::new);
|
||||
cache.retain(|_, (file, _)| file.strong_count() > 0);
|
||||
cache.insert(key, (Arc::downgrade(bytes), b));
|
||||
b
|
||||
}
|
||||
|
||||
/// The bounds of the non-transparent pixels of PDF page `page`, as fractions of the page.
|
||||
fn content_bounds(bytes: &[u8], page: usize) -> Option<[f64; 4]> {
|
||||
let pm = render_pdf_page(bytes, page, 2048)?;
|
||||
|
||||
Reference in New Issue
Block a user