mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 20:26:57 -06:00
Merge pull request #1927 from Silvertree2010/perf/export-speed
PDF export: ask the composition cache once per story and page
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! File › Export PDF on the `designcraft-cli perf` stress document, timed.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --release -p designcraft-cli --example pdf_export -- [--pages N] [--runs N] [--out FILE.pdf]
|
||||
//! ```
|
||||
//!
|
||||
//! Builds the synthetic document (200 pages, 1M characters in threaded stories, 500 placed
|
||||
//! images, gradients, a drop shadow per spread), opens it in a headless session and runs
|
||||
//! `file.exportPdf` on all pages: one warm-up export (composition), then `--runs` timed ones.
|
||||
//! Prints each run, the median and the minimum. `--out` keeps the last PDF, written with a fixed
|
||||
//! creation date so two builds can be compared byte for byte.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use designcraft_cli::stress;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let mut spec = stress::Spec::default();
|
||||
let mut runs = 7usize;
|
||||
let mut out: Option<String> = None;
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
let mut num = || args.next().and_then(|v| v.parse::<usize>().ok()).ok_or(format!("{a} needs a number"));
|
||||
match a.as_str() {
|
||||
"--pages" => spec.pages = num()?,
|
||||
"--runs" => runs = num()?.max(1),
|
||||
"--out" => out = Some(args.next().ok_or("--out needs a file")?),
|
||||
other => return Err(format!("unknown option `{other}`")),
|
||||
}
|
||||
}
|
||||
let doc = stress::synthetic(&spec)?;
|
||||
let bytes = designcraft_format::save(&doc).map_err(|e| e.to_string())?;
|
||||
let dir = std::env::temp_dir().join(format!("designcraft-pdf-bench-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let file = dir.join("stress.designcraft");
|
||||
let keep = out.is_some();
|
||||
let pdf = out.map_or_else(|| dir.join("stress.pdf"), std::path::PathBuf::from);
|
||||
std::fs::write(&file, bytes).map_err(|e| e.to_string())?;
|
||||
let mut s = designcraft_engine::Session::new();
|
||||
s.execute("file.open", &serde_json::json!({"path": file})).map_err(|e| e.to_string())?;
|
||||
let params = serde_json::json!({"path": pdf, "created": 1_700_000_000});
|
||||
let mut export = || -> Result<f64, String> {
|
||||
let t = Instant::now();
|
||||
s.execute("file.exportPdf", ¶ms).map_err(|e| e.to_string())?;
|
||||
Ok(t.elapsed().as_secs_f64() * 1000.0)
|
||||
};
|
||||
let warm = export()?;
|
||||
println!("{} pages, warm-up export (with composition) {warm:.1} ms", doc.page_count());
|
||||
let mut ms = Vec::with_capacity(runs);
|
||||
for i in 0..runs {
|
||||
let t = export()?;
|
||||
println!("run {}: {t:.1} ms", i + 1);
|
||||
ms.push(t);
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
let size = std::fs::metadata(&pdf).map_or(0, |m| m.len());
|
||||
println!("median {:.1} ms, min {:.1} ms, {} kB", ms[ms.len() / 2], ms[0], size / 1024);
|
||||
let _ = std::fs::remove_file(&file);
|
||||
if !keep {
|
||||
let _ = std::fs::remove_file(&pdf);
|
||||
}
|
||||
let _ = std::fs::remove_dir(&dir);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Headless DesignCraft's shared parts: the synthetic stress document of `designcraft-cli perf`,
|
||||
//! which the PDF export benchmark (`examples/pdf_export.rs`) exports too.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod stress;
|
||||
@@ -10,253 +10,18 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use designcraft_color::{Color, Gradient, GradientKind, GradientStop, Swatch, SwatchValue};
|
||||
use designcraft_compose::Cache;
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Asset, AssetId, Content, Document, Fill, Graphic, Item, ItemId, ParaFormat, Shape, SpreadRef, StoryId};
|
||||
use designcraft_geom::{Affine, Rect, Vec2, shapes};
|
||||
use designcraft_doc::{Document, SpreadRef, StoryId};
|
||||
use designcraft_geom::{Affine, Rect, Vec2};
|
||||
use designcraft_render::{Placed, RenderOptions, Renderer};
|
||||
|
||||
use designcraft_cli::stress::{Spec, synthetic};
|
||||
|
||||
/// Device pixels per point at 100% (HiDPI canvas).
|
||||
const HIDPI: f64 = 2.0;
|
||||
const W: u32 = 2880;
|
||||
const H: u32 = 1800;
|
||||
|
||||
/// Deterministic xorshift in 0..1.
|
||||
struct Rng(u64);
|
||||
impl Rng {
|
||||
fn next(&mut self) -> f64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
(self.0 % 1_000_000) as f64 / 1_000_000.0
|
||||
}
|
||||
fn pick<'a>(&mut self, v: &[&'a str]) -> &'a str {
|
||||
v[((self.next() * v.len() as f64) as usize).min(v.len() - 1)]
|
||||
}
|
||||
}
|
||||
|
||||
const WORDS: &[&str] = &[
|
||||
"the",
|
||||
"grid",
|
||||
"page",
|
||||
"margin",
|
||||
"column",
|
||||
"rhythm",
|
||||
"baseline",
|
||||
"reader",
|
||||
"story",
|
||||
"frame",
|
||||
"quiet",
|
||||
"layout",
|
||||
"type",
|
||||
"paragraph",
|
||||
"composer",
|
||||
"weighs",
|
||||
"every",
|
||||
"line",
|
||||
"break",
|
||||
"texture",
|
||||
"even",
|
||||
"hyphen",
|
||||
"image",
|
||||
"spread",
|
||||
"pulse",
|
||||
"caption",
|
||||
"colour",
|
||||
"swatch",
|
||||
"ink",
|
||||
"tint",
|
||||
"gradient",
|
||||
"style",
|
||||
"voice",
|
||||
"printer",
|
||||
"proof",
|
||||
"bleed",
|
||||
"a",
|
||||
"of",
|
||||
"and",
|
||||
"to",
|
||||
"in",
|
||||
"with",
|
||||
"for",
|
||||
"is",
|
||||
"on",
|
||||
"that",
|
||||
"by",
|
||||
"it",
|
||||
"as",
|
||||
"careful",
|
||||
"measure",
|
||||
"leading",
|
||||
"kerning",
|
||||
"tracking",
|
||||
"glyph",
|
||||
"serif",
|
||||
"italic",
|
||||
"heading",
|
||||
"running",
|
||||
"folio",
|
||||
];
|
||||
|
||||
/// `chars` characters of original filler text in paragraphs of ~400 characters.
|
||||
fn filler(r: &mut Rng, chars: usize) -> String {
|
||||
let mut s = String::with_capacity(chars + 16);
|
||||
let mut para = 0;
|
||||
while s.len() < chars {
|
||||
let w = r.pick(WORDS);
|
||||
if para == 0 {
|
||||
let mut c = w.chars();
|
||||
if let Some(f) = c.next() {
|
||||
s.extend(f.to_uppercase());
|
||||
s.push_str(c.as_str());
|
||||
}
|
||||
} else {
|
||||
s.push_str(w);
|
||||
}
|
||||
para += w.len() + 1;
|
||||
if para > 400 {
|
||||
s.push_str(".\n");
|
||||
para = 0;
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Procedural RGB image, PNG-encoded.
|
||||
fn art_png(w: u32, h: u32, seed: f32) -> Vec<u8> {
|
||||
let mut px = Vec::with_capacity((w * h * 4) as usize);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let (fx, fy) = (x as f32 / w as f32, y as f32 / h as f32);
|
||||
let v = ((fx * 9.0 + seed).sin() * (fy * 7.0 - seed).cos() * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
px.extend([(v * 255.0) as u8, ((1.0 - fy) * 200.0) as u8, ((fx * 0.6 + seed * 0.1).fract() * 255.0) as u8, 255]);
|
||||
}
|
||||
}
|
||||
designcraft_render::Rendered { width: w, height: h, pixels: px }.to_png()
|
||||
}
|
||||
|
||||
/// Parameters of the synthetic stress document.
|
||||
pub struct Spec {
|
||||
pub pages: usize,
|
||||
pub frames_per_page: usize,
|
||||
pub frames_per_story: usize,
|
||||
pub chars: usize,
|
||||
pub images: usize,
|
||||
pub assets: usize,
|
||||
}
|
||||
|
||||
impl Default for Spec {
|
||||
fn default() -> Self {
|
||||
Spec { pages: 200, frames_per_page: 10, frames_per_story: 50, chars: 1_000_000, images: 500, assets: 25 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `spec.pages`-page facing document: per page a gradient band, a 2×N grid of threaded text
|
||||
/// frames, and a strip of placed images (one with a drop shadow per spread).
|
||||
pub fn synthetic(spec: &Spec) -> Result<Document, String> {
|
||||
let mut d = Document::new(&NewDocument { pages: spec.pages, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
d.swatches.push(Swatch {
|
||||
name: "Perf Gradient".into(),
|
||||
value: SwatchValue::Gradient {
|
||||
gradient: Gradient {
|
||||
kind: GradientKind::Linear,
|
||||
stops: vec![GradientStop::new(0.0, Color::cmyk(0.6, 0.9, 0.3, 0.2)), GradientStop::new(1.0, Color::cmyk(0.0, 0.6, 0.8, 0.0))],
|
||||
},
|
||||
},
|
||||
locked: false,
|
||||
named: true,
|
||||
hidden: false,
|
||||
});
|
||||
let mut rng = Rng(0x5eed);
|
||||
let (aw, ah) = (800u32, 600u32);
|
||||
let assets: Vec<AssetId> = (0..spec.assets.max(1))
|
||||
.map(|k| {
|
||||
let id = AssetId(d.alloc());
|
||||
let data = art_png(aw, ah, k as f32 * 0.37);
|
||||
d.assets.insert(
|
||||
id,
|
||||
Arc::new(Asset {
|
||||
id,
|
||||
name: format!("art-{k}.png"),
|
||||
mime: "image/png".into(),
|
||||
data: Arc::new(data),
|
||||
pixels: Some((aw, ah)),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
id
|
||||
})
|
||||
.collect();
|
||||
let total_frames = spec.pages * spec.frames_per_page;
|
||||
let stories = total_frames.div_ceil(spec.frames_per_story.max(1));
|
||||
let per_story = spec.chars / stories.max(1);
|
||||
let rows = spec.frames_per_page.div_ceil(2);
|
||||
let (top, strip) = (48.0, 130.0);
|
||||
let row_h = (792.0 - 36.0 - top - strip - 8.0) / rows as f64;
|
||||
let mut prev: Option<ItemId> = None;
|
||||
let mut n_frame = 0usize;
|
||||
let mut placed_images = 0usize;
|
||||
for abs in 0..spec.pages {
|
||||
let Some((si, pi)) = d.page_loc(abs) else { break };
|
||||
let sr = SpreadRef::Doc(si);
|
||||
let x0 = d.spreads[si].pages[pi].x;
|
||||
// Gradient band.
|
||||
let id = ItemId(d.alloc());
|
||||
let mut band = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(Rect::new(x0 + 36.0, 20.0, x0 + 576.0, 40.0)));
|
||||
band.fill = Fill { swatch: "Perf Gradient".into(), ..Fill::none() };
|
||||
band.fill.gradient_angle = Some(rng.next() * 90.0);
|
||||
d.insert_item(sr, band, None).map_err(|e| e.to_string())?;
|
||||
// Text frames.
|
||||
for k in 0..spec.frames_per_page {
|
||||
let (c, r) = (k % 2, k / 2);
|
||||
let fx = x0 + 36.0 + c as f64 * 280.0;
|
||||
let fy = top + r as f64 * row_h;
|
||||
let rect = Rect::new(fx, fy, fx + 260.0, fy + row_h - 6.0);
|
||||
let first = n_frame.is_multiple_of(spec.frames_per_story.max(1));
|
||||
let text = if first { filler(&mut rng, per_story) } else { String::new() };
|
||||
let (fid, _) = d.add_text_frame(sr, rect, lid, &text, ParaFormat::default()).map_err(|e| e.to_string())?;
|
||||
if let (false, Some(p)) = (first, prev) {
|
||||
d.thread(p, fid).map_err(|e| e.to_string())?;
|
||||
}
|
||||
prev = Some(fid);
|
||||
n_frame += 1;
|
||||
}
|
||||
// Images: spread the requested count evenly over the pages.
|
||||
let want = (spec.images * (abs + 1)) / spec.pages - placed_images;
|
||||
for k in 0..want {
|
||||
let w = 540.0 / want as f64;
|
||||
let r = Rect::new(x0 + 36.0 + k as f64 * w, 792.0 - 36.0 - strip, x0 + 36.0 + (k + 1) as f64 * w - 8.0, 792.0 - 36.0);
|
||||
let asset = assets[(placed_images + k) % assets.len()];
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(r));
|
||||
let (nw, nh) = (aw as f64, ah as f64);
|
||||
let s = (r.width() / nw).max(r.height() / nh);
|
||||
it.content = Content::Graphic(Graphic {
|
||||
layer_options: None,
|
||||
asset,
|
||||
size: (nw, nh),
|
||||
xf: Affine::translate((r.x0 + (r.width() - nw * s) / 2.0, r.y0 + (r.height() - nh * s) / 2.0)) * Affine::scale(s),
|
||||
clip: None,
|
||||
auto_fit: designcraft_doc::Fitting::FillProportionally,
|
||||
fit_align: 4,
|
||||
crop: [0.0; 4],
|
||||
wrap: Default::default(),
|
||||
fill: Default::default(),
|
||||
});
|
||||
if k == 0 && pi == 0 {
|
||||
it.effects.drop_shadow.on = true;
|
||||
}
|
||||
d.insert_item(sr, it, None).map_err(|e| e.to_string())?;
|
||||
}
|
||||
placed_images += want;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
fn median_ms(runs: usize, mut f: impl FnMut()) -> f64 {
|
||||
let mut v: Vec<f64> = (0..runs.max(1))
|
||||
.map(|_| {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//! The synthetic stress document `designcraft-cli perf` measures, also used by the PDF export
|
||||
//! benchmark (`examples/pdf_export.rs`).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use designcraft_color::{Color, Gradient, GradientKind, GradientStop, Swatch, SwatchValue};
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Asset, AssetId, Content, Document, Fill, Graphic, Item, ItemId, ParaFormat, Shape, SpreadRef};
|
||||
use designcraft_geom::{Affine, Rect, shapes};
|
||||
|
||||
/// Deterministic xorshift in 0..1.
|
||||
struct Rng(u64);
|
||||
impl Rng {
|
||||
fn next(&mut self) -> f64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
(self.0 % 1_000_000) as f64 / 1_000_000.0
|
||||
}
|
||||
fn pick<'a>(&mut self, v: &[&'a str]) -> &'a str {
|
||||
v[((self.next() * v.len() as f64) as usize).min(v.len() - 1)]
|
||||
}
|
||||
}
|
||||
|
||||
const WORDS: &[&str] = &[
|
||||
"the",
|
||||
"grid",
|
||||
"page",
|
||||
"margin",
|
||||
"column",
|
||||
"rhythm",
|
||||
"baseline",
|
||||
"reader",
|
||||
"story",
|
||||
"frame",
|
||||
"quiet",
|
||||
"layout",
|
||||
"type",
|
||||
"paragraph",
|
||||
"composer",
|
||||
"weighs",
|
||||
"every",
|
||||
"line",
|
||||
"break",
|
||||
"texture",
|
||||
"even",
|
||||
"hyphen",
|
||||
"image",
|
||||
"spread",
|
||||
"pulse",
|
||||
"caption",
|
||||
"colour",
|
||||
"swatch",
|
||||
"ink",
|
||||
"tint",
|
||||
"gradient",
|
||||
"style",
|
||||
"voice",
|
||||
"printer",
|
||||
"proof",
|
||||
"bleed",
|
||||
"a",
|
||||
"of",
|
||||
"and",
|
||||
"to",
|
||||
"in",
|
||||
"with",
|
||||
"for",
|
||||
"is",
|
||||
"on",
|
||||
"that",
|
||||
"by",
|
||||
"it",
|
||||
"as",
|
||||
"careful",
|
||||
"measure",
|
||||
"leading",
|
||||
"kerning",
|
||||
"tracking",
|
||||
"glyph",
|
||||
"serif",
|
||||
"italic",
|
||||
"heading",
|
||||
"running",
|
||||
"folio",
|
||||
];
|
||||
|
||||
/// `chars` characters of original filler text in paragraphs of ~400 characters.
|
||||
fn filler(r: &mut Rng, chars: usize) -> String {
|
||||
let mut s = String::with_capacity(chars + 16);
|
||||
let mut para = 0;
|
||||
while s.len() < chars {
|
||||
let w = r.pick(WORDS);
|
||||
if para == 0 {
|
||||
let mut c = w.chars();
|
||||
if let Some(f) = c.next() {
|
||||
s.extend(f.to_uppercase());
|
||||
s.push_str(c.as_str());
|
||||
}
|
||||
} else {
|
||||
s.push_str(w);
|
||||
}
|
||||
para += w.len() + 1;
|
||||
if para > 400 {
|
||||
s.push_str(".\n");
|
||||
para = 0;
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Procedural RGB image, PNG-encoded.
|
||||
fn art_png(w: u32, h: u32, seed: f32) -> Vec<u8> {
|
||||
let mut px = Vec::with_capacity((w * h * 4) as usize);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let (fx, fy) = (x as f32 / w as f32, y as f32 / h as f32);
|
||||
let v = ((fx * 9.0 + seed).sin() * (fy * 7.0 - seed).cos() * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
px.extend([(v * 255.0) as u8, ((1.0 - fy) * 200.0) as u8, ((fx * 0.6 + seed * 0.1).fract() * 255.0) as u8, 255]);
|
||||
}
|
||||
}
|
||||
designcraft_render::Rendered { width: w, height: h, pixels: px }.to_png()
|
||||
}
|
||||
|
||||
/// Parameters of the synthetic stress document.
|
||||
pub struct Spec {
|
||||
pub pages: usize,
|
||||
pub frames_per_page: usize,
|
||||
pub frames_per_story: usize,
|
||||
pub chars: usize,
|
||||
pub images: usize,
|
||||
pub assets: usize,
|
||||
}
|
||||
|
||||
impl Default for Spec {
|
||||
fn default() -> Self {
|
||||
Spec { pages: 200, frames_per_page: 10, frames_per_story: 50, chars: 1_000_000, images: 500, assets: 25 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `spec.pages`-page facing document: per page a gradient band, a 2×N grid of threaded text
|
||||
/// frames, and a strip of placed images (one with a drop shadow per spread).
|
||||
pub fn synthetic(spec: &Spec) -> Result<Document, String> {
|
||||
let mut d = Document::new(&NewDocument { pages: spec.pages, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
d.swatches.push(Swatch {
|
||||
name: "Perf Gradient".into(),
|
||||
value: SwatchValue::Gradient {
|
||||
gradient: Gradient {
|
||||
kind: GradientKind::Linear,
|
||||
stops: vec![GradientStop::new(0.0, Color::cmyk(0.6, 0.9, 0.3, 0.2)), GradientStop::new(1.0, Color::cmyk(0.0, 0.6, 0.8, 0.0))],
|
||||
},
|
||||
},
|
||||
locked: false,
|
||||
named: true,
|
||||
hidden: false,
|
||||
});
|
||||
let mut rng = Rng(0x5eed);
|
||||
let (aw, ah) = (800u32, 600u32);
|
||||
let assets: Vec<AssetId> = (0..spec.assets.max(1))
|
||||
.map(|k| {
|
||||
let id = AssetId(d.alloc());
|
||||
let data = art_png(aw, ah, k as f32 * 0.37);
|
||||
d.assets.insert(
|
||||
id,
|
||||
Arc::new(Asset {
|
||||
id,
|
||||
name: format!("art-{k}.png"),
|
||||
mime: "image/png".into(),
|
||||
data: Arc::new(data),
|
||||
pixels: Some((aw, ah)),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
id
|
||||
})
|
||||
.collect();
|
||||
let total_frames = spec.pages * spec.frames_per_page;
|
||||
let stories = total_frames.div_ceil(spec.frames_per_story.max(1));
|
||||
let per_story = spec.chars / stories.max(1);
|
||||
let rows = spec.frames_per_page.div_ceil(2);
|
||||
let (top, strip) = (48.0, 130.0);
|
||||
let row_h = (792.0 - 36.0 - top - strip - 8.0) / rows as f64;
|
||||
let mut prev: Option<ItemId> = None;
|
||||
let mut n_frame = 0usize;
|
||||
let mut placed_images = 0usize;
|
||||
for abs in 0..spec.pages {
|
||||
let Some((si, pi)) = d.page_loc(abs) else { break };
|
||||
let sr = SpreadRef::Doc(si);
|
||||
let x0 = d.spreads[si].pages[pi].x;
|
||||
// Gradient band.
|
||||
let id = ItemId(d.alloc());
|
||||
let mut band = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(Rect::new(x0 + 36.0, 20.0, x0 + 576.0, 40.0)));
|
||||
band.fill = Fill { swatch: "Perf Gradient".into(), ..Fill::none() };
|
||||
band.fill.gradient_angle = Some(rng.next() * 90.0);
|
||||
d.insert_item(sr, band, None).map_err(|e| e.to_string())?;
|
||||
// Text frames.
|
||||
for k in 0..spec.frames_per_page {
|
||||
let (c, r) = (k % 2, k / 2);
|
||||
let fx = x0 + 36.0 + c as f64 * 280.0;
|
||||
let fy = top + r as f64 * row_h;
|
||||
let rect = Rect::new(fx, fy, fx + 260.0, fy + row_h - 6.0);
|
||||
let first = n_frame.is_multiple_of(spec.frames_per_story.max(1));
|
||||
let text = if first { filler(&mut rng, per_story) } else { String::new() };
|
||||
let (fid, _) = d.add_text_frame(sr, rect, lid, &text, ParaFormat::default()).map_err(|e| e.to_string())?;
|
||||
if let (false, Some(p)) = (first, prev) {
|
||||
d.thread(p, fid).map_err(|e| e.to_string())?;
|
||||
}
|
||||
prev = Some(fid);
|
||||
n_frame += 1;
|
||||
}
|
||||
// Images: spread the requested count evenly over the pages.
|
||||
let want = (spec.images * (abs + 1)) / spec.pages - placed_images;
|
||||
for k in 0..want {
|
||||
let w = 540.0 / want as f64;
|
||||
let r = Rect::new(x0 + 36.0 + k as f64 * w, 792.0 - 36.0 - strip, x0 + 36.0 + (k + 1) as f64 * w - 8.0, 792.0 - 36.0);
|
||||
let asset = assets[(placed_images + k) % assets.len()];
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(r));
|
||||
let (nw, nh) = (aw as f64, ah as f64);
|
||||
let s = (r.width() / nw).max(r.height() / nh);
|
||||
it.content = Content::Graphic(Graphic {
|
||||
layer_options: None,
|
||||
asset,
|
||||
size: (nw, nh),
|
||||
xf: Affine::translate((r.x0 + (r.width() - nw * s) / 2.0, r.y0 + (r.height() - nh * s) / 2.0)) * Affine::scale(s),
|
||||
clip: None,
|
||||
auto_fit: designcraft_doc::Fitting::FillProportionally,
|
||||
fit_align: 4,
|
||||
crop: [0.0; 4],
|
||||
wrap: Default::default(),
|
||||
fill: Default::default(),
|
||||
});
|
||||
if k == 0 && pi == 0 {
|
||||
it.effects.drop_shadow.on = true;
|
||||
}
|
||||
d.insert_item(sr, it, None).map_err(|e| e.to_string())?;
|
||||
}
|
||||
placed_images += want;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
@@ -60,6 +60,9 @@ pub(crate) struct Exporter<'a> {
|
||||
/// The document's inks with its Ink Manager settings.
|
||||
pub(crate) inks: craft_prepress::InkSet,
|
||||
pub cache: &'a Cache,
|
||||
/// Each story's composition per page it is shown on, for this export: the document doesn't
|
||||
/// change while it runs, and asking the cache checks every frame of the story again.
|
||||
composed: HashMap<(designcraft_doc::StoryId, Option<usize>), Arc<designcraft_compose::ComposedStory>>,
|
||||
pub opts: &'a PdfOptions,
|
||||
/// Current sheet's visible area (spread space) for culling.
|
||||
pub clip: Rect,
|
||||
|
||||
@@ -98,6 +98,7 @@ fn export_booklet_inner(doc: &Document, cache: &Cache, opts: &BookletOptions) ->
|
||||
inks: crate::inks::ink_set(doc),
|
||||
doc,
|
||||
cache,
|
||||
composed: HashMap::new(),
|
||||
opts: &pdf_opts,
|
||||
clip: Rect::ZERO,
|
||||
warnings,
|
||||
|
||||
@@ -198,6 +198,7 @@ mod image_content_cache_tests {
|
||||
inks: crate::inks::ink_set(doc),
|
||||
doc,
|
||||
cache,
|
||||
composed: HashMap::new(),
|
||||
opts,
|
||||
clip: Rect::ZERO,
|
||||
warnings: Vec::new(),
|
||||
|
||||
@@ -172,6 +172,7 @@ fn export_pdf_with_report_inner(doc: &Document, cache: &Cache, opts: &PdfOptions
|
||||
inks: crate::inks::ink_set(doc),
|
||||
doc,
|
||||
cache,
|
||||
composed: HashMap::new(),
|
||||
opts,
|
||||
clip: Rect::ZERO,
|
||||
warnings,
|
||||
|
||||
@@ -4,6 +4,12 @@ use super::pipeline::{item_exported, layer_exported};
|
||||
use super::*;
|
||||
|
||||
impl Exporter<'_> {
|
||||
/// Story `sid` composed for `page` ([`Cache::get`]), asked of the cache once per export.
|
||||
fn composed(&mut self, sid: designcraft_doc::StoryId, page: Option<usize>) -> Arc<designcraft_compose::ComposedStory> {
|
||||
let (doc, cache) = (self.doc, self.cache);
|
||||
self.composed.entry((sid, page)).or_insert_with(|| cache.get(doc, sid, page)).clone()
|
||||
}
|
||||
|
||||
/// The parent pages' items on `layer` shown on page `pi` of spread `si`: the parent's bases
|
||||
/// first, then the parent.
|
||||
fn parent_items(&mut self, s: &mut Surface, si: usize, pi: usize, page: &designcraft_doc::Page, layer: &designcraft_doc::Layer, first: usize) {
|
||||
@@ -325,7 +331,7 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
Content::Text(tfr) => {
|
||||
let cs = self.cache.get(doc, tfr.story, page);
|
||||
let cs = self.composed(tfr.story, page);
|
||||
if let (Some(ft), Some(pt)) = (cs.frame(it.id), &tfr.options.path) {
|
||||
// Type on a path: the glyphs as outlines along the path.
|
||||
for (si, gp) in designcraft_compose::path_glyphs(&cs, ft, &bp, pt) {
|
||||
@@ -381,7 +387,7 @@ impl Exporter<'_> {
|
||||
s.pop();
|
||||
}
|
||||
if let Content::Text(tfr) = &it.content {
|
||||
let cs = self.cache.get(doc, tfr.story, page);
|
||||
let cs = self.composed(tfr.story, page);
|
||||
if let Some(ft) = cs.frame(it.id) {
|
||||
self.frame_objects(s, ft, xf * doc.text_local(it), page);
|
||||
}
|
||||
|
||||
@@ -890,3 +890,30 @@ fn a_swatch_colours_a_greyscale_image() {
|
||||
let (s, _) = export(&rgb, Some("PANTONE Test"));
|
||||
assert!(!s.contains("PANTONE#20Test"), "a colour image keeps its colours");
|
||||
}
|
||||
|
||||
/// The export asks for each story's composition once per page it is shown on: a parent page's
|
||||
/// page number still reads its own page everywhere, and a story threaded across pages shows each
|
||||
/// frame's own lines.
|
||||
#[test]
|
||||
fn each_page_shows_its_own_composition() {
|
||||
use designcraft_doc::story::PAGE_NUMBER;
|
||||
let mut d = Document::new(&NewDocument { pages: 3, facing_pages: false, ..NewDocument::default() });
|
||||
let lid = d.default_layer();
|
||||
d.add_text_frame(SpreadRef::Parent(0), Rect::new(36.0, 700.0, 300.0, 740.0), lid, &format!("Folio{PAGE_NUMBER}"), ParaFormat::default()).unwrap();
|
||||
let (first, _) =
|
||||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 300.0, 56.0), lid, "Alpha\nBravo\nCharlie", ParaFormat::default()).unwrap();
|
||||
let mut prev = first;
|
||||
for si in 1..3 {
|
||||
let id = ItemId(d.alloc());
|
||||
let it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(36.0, 36.0, 300.0, 56.0)));
|
||||
d.insert_item(SpreadRef::Doc(si), it, None).unwrap();
|
||||
d.thread(prev, id).unwrap();
|
||||
prev = id;
|
||||
}
|
||||
let cache = Cache::new();
|
||||
let opts = PdfOptions { created: Some(0), ..PdfOptions::default() };
|
||||
let bytes = export_pdf(&d, &cache, &opts).unwrap();
|
||||
// Parent items first, then the page's own.
|
||||
assert_eq!(extract_text(&bytes), ["Folio1Alpha", "Folio2Bravo", "Folio3Charlie"]);
|
||||
assert_eq!(export_pdf(&d, &cache, &opts).unwrap(), bytes, "a warm cache exports the same bytes");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user