Package the PDF as file.exportPdf makes it; refuse a bad pdf

file.package now writes its PDF through export's `pdf_report`, so a preset's
transparency flattening, the effects and hidden-layer rasterising, the blend
space, the view options and the transitions all reach the packaged PDF.
A `pdf` that is neither a switch nor export options is refused, as is a bad
flattener, before anything is written. Georgian: the printing term for spot
inks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer
2026-10-22 00:59:00 +02:00
co-authored by Claude Opus 5.5
parent 2c43330c43
commit 63d3f5a19d
3 changed files with 68 additions and 28 deletions
+27 -15
View File
@@ -710,9 +710,20 @@ fn first_page_ref(pdf: &[u8]) -> Option<usize> {
fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let opts = options(p, st.doc.page_count())?;
// Transparency Flattener presets: High / Medium / Low Resolution, or a ppi.
let ppi = match p.get("flatten") {
let r = pdf_report(&st.doc, &s.cache, p)?;
match str_param(p, "path") {
Some(path) => {
write_file(path, &r.bytes)?;
Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings}))
}
None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})),
}
}
/// The Transparency Flattener's resolution `p.flatten` asks for: High, Medium or Low Resolution,
/// or a ppi; `None` when the PDF isn't flattened.
pub(super) fn flatten_ppi(p: &Value) -> Result<Option<f64>> {
Ok(match p.get("flatten") {
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
Some(Value::String(s)) if s == "high" => Some(300.0),
Some(Value::String(s)) if s == "medium" => Some(150.0),
@@ -721,17 +732,24 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
Some(v) => Some(
v.as_f64().filter(|v| (36.0..=1200.0).contains(v)).ok_or_else(|| bad("file.exportPdf", "`flatten`: high|medium|low or 36–1200 ppi"))?,
),
};
let flattened = ppi.map(|ppi| flatten(&st.doc, &s.cache, ppi));
})
}
/// The PDF of `doc` as `file.exportPdf` makes it from options `p`: flattened when asked, placed
/// PDFs with hidden layers and soft effects as images, the blend space, the view options and the
/// page transitions. `file.package` writes its PDF with it too.
pub(super) 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 flattened = flatten_ppi(p)?.map(|ppi| flatten(doc, cache, ppi));
// Placed PDFs with hidden layers go out as images showing just their visible layers.
let base: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0);
let base: &designcraft_doc::Document = flattened.as_ref().map_or(doc, |f| &f.0);
let (layered, layer_count) = rasterize_layered(base)?;
let base2: &designcraft_doc::Document = layered.as_ref().unwrap_or(base);
let (fx_doc, fx_count) = rasterize_effects(base2, &s.cache);
let (fx_doc, fx_count) = rasterize_effects(base2, 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 r = designcraft_pdf::export_pdf_with_report(doc, cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let mut r = r;
// Transparency Blend Space: the page group of pages with transparency.
let spreads = designcraft_pdf::sheet_spreads(doc, &opts);
@@ -805,13 +823,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
{
r.warnings.push(format!("transparency flattened: {n} region(s) rasterised"));
}
match str_param(p, "path") {
Some(path) => {
write_file(path, &r.bytes)?;
Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings}))
}
None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})),
}
Ok(r)
}
#[cfg(not(target_arch = "wasm32"))]
+39 -11
View File
@@ -389,12 +389,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 (as
// file.exportPdf's). Read first, so that bad options write nothing.
// 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") {
Some(o @ Value::Object(_)) => Some(super::export::options(o, st.doc.page_count())?),
Some(Value::Bool(true)) => Some(Default::default()),
_ => None,
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
Some(Value::Bool(true)) => Some(json!({})),
Some(o @ Value::Object(_)) => {
super::export::options(o, st.doc.page_count())?;
super::export::flatten_ppi(o)?;
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();
@@ -440,11 +445,9 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
root.write(&path, &designcraft_idml::export_idml_with(&packed, &opts))?;
files.push(path);
}
if let Some(opts) = pdf {
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, &opts)
.map_err(|e| EngineError::Other(e.to_string()))?;
root.write(&path, &bytes)?;
root.write(&path, &super::export::pdf_report(&packed, &s.cache, &params)?.bytes)?;
files.push(path);
}
// The report: fonts (missing first), links, preflight, the user's instructions.
@@ -531,8 +534,33 @@ mod tests {
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"));
assert!(s.execute("file.package", &json!({"dir": dir.join("bad").to_string_lossy(), "pdf": {"standard": "nope"}})).is_err());
assert!(!dir.join("bad").exists(), "bad options write nothing");
// Refused before anything is written: bad options, a bad flattener and a `pdf` that is
// neither a switch nor options.
for bad in [json!({"standard": "nope"}), json!({"flatten": "extreme"}), 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();
}
+2 -2
View File
@@ -41,7 +41,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Create Printing Instructions", "ბეჭდვის ინსტრუქციების შექმნა"),
("Entire Publication", "მთელი პუბლიკაცია"),
("Scope:", "არეალი:"),
("Spot", "ლაქა"),
("Spot", "სპოტ-საღებავი"),
("Process", "პროცესი"),
("Source", "წყარო"),
("Installed", "დაყენებული"),
@@ -49,7 +49,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Substituted", "ჩანაცვლებული"),
("Modified", "შეცვლილი"),
("Missing", "აკლია"),
("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"),
("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის მელანი; {spot} სპოტ-საღებავი"),
("Images: {embedded} Embedded", "გამოსახულებები: ჩაშენებულია {embedded}"),
(
"Links and Images: {found} Links Found; {modified} Modified, {missing} Missing",