mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 06:36:51 -06:00
Merge PR #1515: Package dialog PDF options refactor
crates/ui-egui/src/dialogs.rs: kept the Enter-stays-local fix crates/ui-egui/src/package_dialog.rs: kept the summary wording and the fix; took the PR's pdf option value crates/ui-egui/src/i18n/*.rs: kept the reworded keys and the PR's PDF Preset rows Also (the PR's own CI failure): package.rs calls check_pdf_options and pdf_report on wasm, so those two re-exports are no longer wasm-gated (only pdf_export is).
This commit is contained in:
@@ -16,6 +16,7 @@ mod registry;
|
||||
use flatten::{rasterize_effects, shadow_coverage};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use pdf::pdf_export;
|
||||
pub(crate) use pdf::{check_pdf_options, pdf_report};
|
||||
pub(crate) use pdf::{flatten_ppi, options, print_pdf};
|
||||
pub use registry::specs;
|
||||
|
||||
|
||||
@@ -169,41 +169,21 @@ pub(crate) fn print_pdf(
|
||||
/// The PDF that File › Export PDF writes for `p`, with its warnings: the print PDF
|
||||
/// ([`print_pdf`]) with its view options, timings and transitions. File › Print prints it.
|
||||
pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::ExportReport> {
|
||||
let st = s.doc()?;
|
||||
let opts = options(p, st.doc.page_count())?;
|
||||
pdf_report(&s.doc()?.doc, &s.cache, p)
|
||||
}
|
||||
|
||||
/// [`pdf_export`] of `doc`: `file.package` writes its PDF of the packaged copy with it.
|
||||
pub(crate) fn pdf_report(doc: &designcraft_doc::Document, cache: &designcraft_compose::Cache, p: &Value) -> Result<designcraft_pdf::ExportReport> {
|
||||
let opts = options(p, doc.page_count())?;
|
||||
let ppi = flatten_ppi("file.exportPdf", p)?;
|
||||
let mut r = print_pdf(&st.doc, &s.cache, &opts, ppi)?;
|
||||
let doc: &designcraft_doc::Document = &st.doc;
|
||||
let (cat, dest) = view_options(p)?;
|
||||
let mut r = print_pdf(doc, cache, &opts, ppi)?;
|
||||
let spreads = designcraft_pdf::sheet_spreads(doc, &opts);
|
||||
// Interactive PDF view options.
|
||||
let mut cat = String::new();
|
||||
if p.get("fullScreen").and_then(Value::as_bool) == Some(true) {
|
||||
cat.push_str("/PageMode/FullScreen");
|
||||
} else if p.get("bookmarksPanel").and_then(Value::as_bool) == Some(true) {
|
||||
cat.push_str("/PageMode/UseOutlines");
|
||||
}
|
||||
match str_param(p, "pageLayout") {
|
||||
Some("single") => cat.push_str("/PageLayout/SinglePage"),
|
||||
Some("continuous") => cat.push_str("/PageLayout/OneColumn"),
|
||||
Some("twoUp") => cat.push_str("/PageLayout/TwoPageLeft"),
|
||||
Some("twoUpCover") => cat.push_str("/PageLayout/TwoPageRight"),
|
||||
Some("twoUpContinuous") => cat.push_str("/PageLayout/TwoColumnRight"),
|
||||
Some(o) => return Err(bad("file.exportPdf", format!("unknown pageLayout `{o}`"))),
|
||||
None => {}
|
||||
}
|
||||
if let Some(v) = str_param(p, "view") {
|
||||
// The first page opens with this view (pages are objects in order, so refer by index 0).
|
||||
let dest = match v {
|
||||
"fitPage" => "/Fit",
|
||||
"fitWidth" => "/FitH null",
|
||||
"actual" => "/XYZ null null 1",
|
||||
o => {
|
||||
return Err(bad("file.exportPdf", format!("unknown view `{o}` (fitPage, fitWidth, actual)")));
|
||||
}
|
||||
};
|
||||
if let Some(first) = first_page_ref(&r.bytes) {
|
||||
cat.push_str(&format!("/OpenAction[{first} 0 R{dest}]"));
|
||||
}
|
||||
let mut cat = cat;
|
||||
if let Some(dest) = dest
|
||||
&& let Some(first) = first_page_ref(&r.bytes)
|
||||
{
|
||||
cat.push_str(&format!("/OpenAction[{first} 0 R{dest}]"));
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
match designcraft_pdf::add_catalog_entries(&r.bytes, &cat) {
|
||||
@@ -213,8 +193,7 @@ pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::Expo
|
||||
}
|
||||
// Flip pages every N seconds (presentation).
|
||||
if let Some(secs) = p.get("advanceSeconds").and_then(Value::as_f64).filter(|s| *s > 0.0) {
|
||||
let n = designcraft_pdf::sheet_spreads(doc, &opts).len();
|
||||
let dur: Vec<Option<String>> = (0..n).map(|_| Some(format!("/Dur {secs:.2}"))).collect();
|
||||
let dur: Vec<Option<String>> = (0..spreads.len()).map(|_| Some(format!("/Dur {secs:.2}"))).collect();
|
||||
match designcraft_pdf::add_page_entries(&r.bytes, &dur) {
|
||||
Some(b) => r.bytes = b,
|
||||
None => r.warnings.push("page timings couldn't be set".into()),
|
||||
@@ -232,6 +211,44 @@ pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::Expo
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The catalog entries of the interactive view options in `p` (page mode and layout) and the
|
||||
/// destination the first page opens at (`view`); unknown values are refused.
|
||||
fn view_options(p: &Value) -> Result<(String, Option<&'static str>)> {
|
||||
let mut cat = String::new();
|
||||
if p.get("fullScreen").and_then(Value::as_bool) == Some(true) {
|
||||
cat.push_str("/PageMode/FullScreen");
|
||||
} else if p.get("bookmarksPanel").and_then(Value::as_bool) == Some(true) {
|
||||
cat.push_str("/PageMode/UseOutlines");
|
||||
}
|
||||
match str_param(p, "pageLayout") {
|
||||
Some("single") => cat.push_str("/PageLayout/SinglePage"),
|
||||
Some("continuous") => cat.push_str("/PageLayout/OneColumn"),
|
||||
Some("twoUp") => cat.push_str("/PageLayout/TwoPageLeft"),
|
||||
Some("twoUpCover") => cat.push_str("/PageLayout/TwoPageRight"),
|
||||
Some("twoUpContinuous") => cat.push_str("/PageLayout/TwoColumnRight"),
|
||||
Some(o) => return Err(bad("file.exportPdf", format!("unknown pageLayout `{o}`"))),
|
||||
None => {}
|
||||
}
|
||||
// The first page opens with this view (pages are objects in order, so refer by index 0).
|
||||
let dest = match str_param(p, "view") {
|
||||
None => None,
|
||||
Some("fitPage") => Some("/Fit"),
|
||||
Some("fitWidth") => Some("/FitH null"),
|
||||
Some("actual") => Some("/XYZ null null 1"),
|
||||
Some(o) => return Err(bad("file.exportPdf", format!("unknown view `{o}` (fitPage, fitWidth, actual)"))),
|
||||
};
|
||||
Ok((cat, dest))
|
||||
}
|
||||
|
||||
/// Refuses PDF options `p` that [`pdf_report`] would refuse for a document of `pages` pages,
|
||||
/// without exporting: `file.package` checks them before it writes anything.
|
||||
pub(crate) fn check_pdf_options(p: &Value, pages: usize) -> Result<()> {
|
||||
options(p, pages)?;
|
||||
flatten_ppi("file.exportPdf", p)?;
|
||||
view_options(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tagged_tests {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -203,7 +203,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Package…",
|
||||
["File"],
|
||||
None,
|
||||
"{dir: folder to create, idml?: true, pdf?: false, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report}",
|
||||
"{dir: folder to create, idml?: true, pdf?: false|true|{…options of file.exportPdf}, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report}",
|
||||
has_doc,
|
||||
package
|
||||
),
|
||||
@@ -390,6 +390,17 @@ fn drawn_faces(cs: &ComposedStory, depth: usize, out: &mut Vec<FaceRef>) {
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("file.package", "missing `dir`"))?);
|
||||
let st = s.doc()?;
|
||||
// The PDF: with the default settings, or with the options of a PDF preset, made as
|
||||
// file.exportPdf makes it. Read first, so that bad options write nothing.
|
||||
let pdf = match p.get("pdf") {
|
||||
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
|
||||
Some(Value::Bool(true)) => Some(json!({})),
|
||||
Some(o @ Value::Object(_)) => {
|
||||
super::export::check_pdf_options(o, st.doc.page_count())?;
|
||||
Some(o.clone())
|
||||
}
|
||||
Some(v) => return Err(bad("file.package", format!("`pdf` is true, false or PDF export options, not {v}"))),
|
||||
};
|
||||
let root = OutputRoot::new(dir.clone())?;
|
||||
let display_title = st.doc.title.clone();
|
||||
let title = safe_component(&display_title, "Untitled");
|
||||
@@ -441,11 +452,9 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
idml_layer_note = "\nPlaced PDF layers\nThe IDML copy links the placed PDFs, so it shows their file defaults. The DesignCraft document keeps each placement's Object Layer Options choice.\n";
|
||||
}
|
||||
}
|
||||
if p.get("pdf").and_then(Value::as_bool).unwrap_or(false) {
|
||||
if let Some(params) = pdf {
|
||||
let path = package_output_path(&dir, &title, "pdf")?;
|
||||
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)?;
|
||||
root.write(&path, &super::export::pdf_report(&packed, &s.cache, ¶ms)?.bytes)?;
|
||||
files.push(path);
|
||||
}
|
||||
// The printing instructions first, as the form gave them; then the report: fonts (missing
|
||||
|
||||
@@ -25,6 +25,58 @@ fn relocated_packages_rebind_embedded_image_links_on_open() {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// `pdf` takes file.exportPdf's options (a PDF preset's): a PDF/X-4 package PDF; `true` is the
|
||||
/// default PDF, and bad options are refused.
|
||||
#[test]
|
||||
fn package_pdf_takes_export_options() {
|
||||
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("dc-package-pdf-options-{}-{nonce}", std::process::id()));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"title": "Options", "width": 72, "height": 72})).unwrap();
|
||||
let x4 = dir.join("x4");
|
||||
s.execute("file.package", &json!({"dir": x4.to_string_lossy(), "idml": false, "pdf": {"standard": "x4"}})).unwrap();
|
||||
let has = |path: &std::path::Path, what: &[u8]| std::fs::read(path).unwrap().windows(what.len()).any(|w| w == what);
|
||||
assert!(has(&x4.join("Options.pdf"), b"GTS_PDFX"));
|
||||
let plain = dir.join("plain");
|
||||
s.execute("file.package", &json!({"dir": plain.to_string_lossy(), "idml": false, "pdf": true})).unwrap();
|
||||
assert!(!has(&plain.join("Options.pdf"), b"GTS_PDFX"));
|
||||
// Refused before anything is written: bad options, a bad flattener or view option, and a
|
||||
// `pdf` that is neither a switch nor options.
|
||||
for bad in [
|
||||
json!({"standard": "nope"}),
|
||||
json!({"flatten": "extreme"}),
|
||||
json!({"pageLayout": "spiral"}),
|
||||
json!({"view": "zoomed"}),
|
||||
json!("print"),
|
||||
json!(3),
|
||||
] {
|
||||
let out = dir.join("bad");
|
||||
assert!(s.execute("file.package", &json!({"dir": out.to_string_lossy(), "pdf": bad})).is_err(), "{bad}");
|
||||
assert!(!out.exists(), "{bad}: bad options write nothing");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// A preset that flattens transparency (Screen and Email's `flatten: low`) gives the packaged
|
||||
/// PDF the flattening `file.exportPdf` does.
|
||||
#[test]
|
||||
fn the_packaged_pdf_is_flattened_as_export_flattens_it() {
|
||||
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("dc-package-pdf-flatten-{}-{nonce}", std::process::id()));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"title": "Flat", "width": 144, "height": 144})).unwrap();
|
||||
let id = s.execute("frame.create", &json!({"rect": [10, 10, 100, 100], "content": "unassigned"})).unwrap()["id"].clone();
|
||||
s.execute("object.fill", &json!({"ids": [id], "swatch": "[Black]"})).unwrap();
|
||||
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
|
||||
let exported = super::super::base64_decode(s.execute("file.exportPdf", &json!({"flatten": "low"})).unwrap()["base64"].as_str().unwrap());
|
||||
s.execute("file.package", &json!({"dir": dir.to_string_lossy(), "idml": false, "pdf": {"flatten": "low"}})).unwrap();
|
||||
let packaged = std::fs::read(dir.join("Flat.pdf")).unwrap();
|
||||
let images = |pdf: &[u8]| pdf.windows(b"/Subtype/Image".len()).filter(|w| *w == b"/Subtype/Image").count();
|
||||
assert!(images(&exported) > 0, "the export rasterises the transparent object");
|
||||
assert_eq!(images(&packaged), images(&exported), "so does the package");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// A per-placement PDF layer choice cannot travel in an IDML copy that links its files, so the
|
||||
/// report says the copies differ instead of leaving it to be discovered.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user