mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge #362 update: placed PDFs cropped to their bounding box render only when drawn or exported
This commit is contained in:
+14
-3
@@ -505,7 +505,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]>,
|
||||
}
|
||||
@@ -513,11 +514,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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,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"))]
|
||||
@@ -505,7 +506,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;
|
||||
@@ -559,6 +560,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.
|
||||
|
||||
@@ -75,15 +75,29 @@ pub fn import_report(bytes: &[u8], dir: Option<&std::path::Path>) -> Result<(Doc
|
||||
|
||||
/// 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);
|
||||
@@ -119,6 +133,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;
|
||||
|
||||
|
||||
@@ -1954,7 +1954,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()];
|
||||
|
||||
@@ -470,3 +470,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:?}");
|
||||
}
|
||||
|
||||
@@ -768,7 +768,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 };
|
||||
@@ -1137,6 +1137,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