diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 36fd3d52a..ad90d312e 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -710,9 +710,20 @@ fn first_page_ref(pdf: &[u8]) -> Option { fn export_pdf(s: &mut Session, p: &Value) -> Result { 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> { + 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 { 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 { + 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 { { 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"))] diff --git a/crates/engine/src/cmd/package.rs b/crates/engine/src/cmd/package.rs index 69b3ed379..107b859ae 100644 --- a/crates/engine/src/cmd/package.rs +++ b/crates/engine/src/cmd/package.rs @@ -389,12 +389,17 @@ fn drawn_faces(cs: &ComposedStory, depth: usize, out: &mut Vec) { fn package(s: &mut Session, p: &Value) -> Result { 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 { 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, ¶ms)?.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(); } diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 9bd18a169..121f8295b 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -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",