diff --git a/ROADMAP.md b/ROADMAP.md index df416f74e..4f950db90 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -491,6 +491,7 @@ Newest first. - **2026-10-20** · Type tool: a drag that starts in an empty frame draws a new text frame over it, as in InDesign; a click still turns the frame into a text frame. No readiness percentage change. - **2026-10-20** · Measures in centimetres show four decimals, as InDesign's fields do (a 1p gutter reads 0.4233 cm, not 0.423 cm). No readiness percentage change. - **2026-10-18** · TOC separators preserve right-indent tabs through the dialog and IDML, including localized `$ID/^y` values. A separator picker exposes Tab Character, Right Indent Tab, Em Space and En Space. Existing paragraph tab leaders now work with the tutorial's TOC workflow. No readiness percentage change. +- **2026-10-21** · The Package dialog's Include PDF (Print) has a PDF Preset (the Export PDF presets, the last one used first), as InDesign's Select PDF Preset; `file.package` takes `pdf: {…file.exportPdf options}`, refusing bad options before writing anything. The packaged PDF always had the default settings. Found in the Learn Skills Daily course, chapter 27. No readiness percentage change. - **2026-10-21** · File › Package… opens InDesign's Package dialog: Summary counts the fonts, links and images and inks the package carries, with a warning where some are missing or modified, and Fonts, Links and Images and Colors and Inks list them (Show Problems Only). Create Printing Instructions asks for the contact details and instructions that go into the package's Instructions.txt, and Include IDML / Include PDF (Print) choose the copies it carries. Package… went straight to the folder picker with fixed options. Found in the Learn Skills Daily course, chapter 27. No readiness percentage change. - **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change. - **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change. diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index a4ee73ce4..270fcfc4c 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -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; diff --git a/crates/engine/src/cmd/export/pdf.rs b/crates/engine/src/cmd/export/pdf.rs index 903639ff9..94cd85ce2 100644 --- a/crates/engine/src/cmd/export/pdf.rs +++ b/crates/engine/src/cmd/export/pdf.rs @@ -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 { - 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 { + 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 0.0) { - let n = designcraft_pdf::sheet_spreads(doc, &opts).len(); - let dur: Vec> = (0..n).map(|_| Some(format!("/Dur {secs:.2}"))).collect(); + let dur: Vec> = (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 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; diff --git a/crates/engine/src/cmd/package.rs b/crates/engine/src/cmd/package.rs index f9e58dcec..7b2141c54 100644 --- a/crates/engine/src/cmd/package.rs +++ b/crates/engine/src/cmd/package.rs @@ -203,7 +203,7 @@ pub fn specs() -> Vec { "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) { 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, 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 { 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 diff --git a/crates/engine/src/cmd/package/tests.rs b/crates/engine/src/cmd/package/tests.rs index fe559bfb8..4d6513425 100644 --- a/crates/engine/src/cmd/package/tests.rs +++ b/crates/engine/src/cmd/package/tests.rs @@ -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] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index ddab4e5dc..0bc872503 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -206,13 +206,13 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { const MULTILINE_FOCUS: &str = "dialog_multiline_focus"; pub(crate) fn set_multiline_focus(ctx: &egui::Context) { - ctx.data_mut(|m| m.insert_temp(egui::Id::new(MULTILINE_FOCUS), true)); + ctx.data_mut(|m| m.insert_temp(egui::Id::unique(MULTILINE_FOCUS), true)); } /// Whether Enter in dialog `id` goes to a multi-line field this frame. The flag is read (and cleared) /// for every dialog, but only the Printing Instructions dialog honours it. fn take_multiline_focus(ctx: &egui::Context, id: &str) -> bool { - let set = ctx.data_mut(|m| m.remove_temp::(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false); + let set = ctx.data_mut(|m| m.remove_temp::(egui::Id::unique(MULTILINE_FOCUS))).unwrap_or(false); set && id == crate::package_dialog::INSTRUCTIONS } @@ -582,6 +582,41 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { const PDF_EXPORT_PRESETS: &[&str] = &["Desktop Printing", "Commercial Printing", "Screen and Email", "PDF/X-1a:2003", "PDF/X-4", "PDF/A-2b", "Custom"]; +/// The `file.exportPdf` options the Export PDF dialog's fields stand for (pages and title aside). +fn pdf_export_params(d: &Dialog) -> Value { + let mut params = json!({ + "standard": d.s("standard"), + "compressImages": d.b("compressImages"), + "spreads": d.b("spreads"), + "bleed": d.b("bleed"), + "tagged": d.b("tagged"), + "marks": { + "crop": d.b("marksCrop"), + "bleed": d.b("marksBleed"), + "pageInfo": d.b("marksPageInfo"), + "weight": d.pt("marksWeight").unwrap_or(0.25), + "offset": d.pt("marksOffset").unwrap_or(6.0), + }, + }); + let flatten = d.s("flatten"); + if !flatten.is_empty() { + params["flatten"] = json!(flatten); + } + params +} + +/// The PDF presets a document can be exported with, by name (Custom aside). +pub(crate) fn pdf_presets() -> impl Iterator { + PDF_EXPORT_PRESETS.iter().copied().filter(|p| *p != "Custom") +} + +/// The `file.exportPdf` options of PDF preset `preset`, as the Export PDF dialog sets them. +pub(crate) fn pdf_preset_params(preset: &str) -> Value { + let mut d = Dialog::new("pdfExport", json!({})); + apply_pdf_preset(&mut d, preset); + pdf_export_params(&d) +} + /// Apply a preset's dependent fields. Flatten is only touched by "Screen and Email" and /// "PDF/X-1a:2003" (High Resolution); "Custom" (and any unknown value) changes nothing. fn apply_pdf_preset(d: &mut Dialog, preset: &str) { @@ -4110,24 +4145,7 @@ fn confirm_part3(app: &mut DesignApp, d: Dialog) -> Result { app.ui.pdf_export.marks_offset = d.s("marksOffset"); app.ui.pdf_export.tagged = d.b("tagged"); - let mut params = json!({ - "standard": d.s("standard"), - "compressImages": d.b("compressImages"), - "spreads": d.b("spreads"), - "bleed": d.b("bleed"), - "tagged": d.b("tagged"), - "marks": { - "crop": d.b("marksCrop"), - "bleed": d.b("marksBleed"), - "pageInfo": d.b("marksPageInfo"), - "weight": d.pt("marksWeight").unwrap_or(0.25), - "offset": d.pt("marksOffset").unwrap_or(6.0), - }, - }); - let flatten = d.s("flatten"); - if !flatten.is_empty() { - params["flatten"] = json!(flatten); - } + let mut params = pdf_export_params(&d); let pages = d.s("pages"); if !pages.trim().is_empty() && pages != "all" { params["pages"] = json!(pages); diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index f823f4c53..9ecf4f0e7 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -67,6 +67,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "قسم جديد"), ("Clear Frame Fitting Options", "مسح خيارات ملاءمة الإطار"), ("Cannot be edited", "غير قابل للتحرير"), + ("PDF Preset:", "إعداد PDF المسبق:"), ("Packaged {count} files into {dir}", "تم حزم {count} ملفات في {dir}"), ("Continue", "متابعة"), ("Printing Instructions", "تعليمات الطباعة"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 5feea5db4..523c1e3fc 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -72,6 +72,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Nový oddíl"), ("Clear Frame Fitting Options", "Vymazat volby přizpůsobení rámečku"), ("Cannot be edited", "Nelze upravit"), + ("PDF Preset:", "Předvolba PDF:"), ("Continue", "Pokračovat"), ("Printing Instructions", "Pokyny k tisku"), ("Show Problems Only", "Zobrazit jen problémy"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 8d97ee17d..ab2d6d2aa 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -69,6 +69,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Nuova sezione"), ("Clear Frame Fitting Options", "Cancella opzioni di adattamento cornice"), ("Cannot be edited", "Non modificabile"), + ("PDF Preset:", "Predefinito PDF:"), ("Packaged {count} files into {dir}", "{count} file raccolti in {dir}"), ("Continue", "Continua"), ("Printing Instructions", "Istruzioni di stampa"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 4cf1e66ff..f99c905f1 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -66,6 +66,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "ახალი სექცია"), ("Clear Frame Fitting Options", "ჩარჩოს მორგების პარამეტრების გასუფთავება"), ("Cannot be edited", "რედაქტირება შეუძლებელია"), + ("PDF Preset:", "PDF-ის წინასწარი პარამეტრი:"), ("Continue", "გაგრძელება"), ("Printing Instructions", "ბეჭდვის ინსტრუქციები"), ("Show Problems Only", "მხოლოდ პრობლემების ჩვენება"), @@ -82,6 +83,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Missing", "აკლია"), ("Colors and Inks: {process} process, {spot} spot", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"), ("Embedded images: {embedded}", "გამოსახულებები: ჩაშენებულია {embedded}"), + ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის მელანი; {spot} სპოტ-საღებავი"), + ("Images: {embedded} Embedded", "გამოსახულებები: ჩაშენებულია {embedded}"), ( "Links and Images: {found} linked, {modified} changed, {missing} not found", "ბმულები და გამოსახულებები: ნაპოვნია {found}; შეცვლილია {modified}, აკლია {missing}", diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 0d2ea135b..88931634c 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -70,6 +70,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Nowa sekcja"), ("Clear Frame Fitting Options", "Wyczyść opcje dopasowania ramki"), ("Cannot be edited", "Nie można edytować"), + ("PDF Preset:", "Ustawienie predefiniowane PDF:"), ("Packaged {count} files into {dir}", "Spakowano {count} plików do {dir}"), ("Continue", "Kontynuuj"), ("Printing Instructions", "Instrukcje drukowania"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index df588c3a7..8b2796224 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -72,6 +72,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Nova seção"), ("Clear Frame Fitting Options", "Limpar opções de ajuste do quadro"), ("Cannot be edited", "Não editável"), + ("PDF Preset:", "Predefinição de PDF:"), ("Packaged {count} files into {dir}", "{count} arquivos empacotados em {dir}"), ("Continue", "Continuar"), ("Printing Instructions", "Instruções de impressão"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 216e07f14..d0ebb87a5 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -74,6 +74,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Новый раздел"), ("Clear Frame Fitting Options", "Очистить параметры подгонки фрейма"), ("Cannot be edited", "Нельзя изменить"), + ("PDF Preset:", "Стиль PDF:"), ("Packaged {count} files into {dir}", "Упаковано файлов: {count} в {dir}"), ("Continue", "Продолжить"), ("Printing Instructions", "Инструкции для печати"), diff --git a/crates/ui-egui/src/i18n/table.rs b/crates/ui-egui/src/i18n/table.rs index ace3a0e68..ddefcf301 100644 --- a/crates/ui-egui/src/i18n/table.rs +++ b/crates/ui-egui/src/i18n/table.rs @@ -213,6 +213,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ "Include PDF (Print)", ["PDF einschließen (Druck)", "Inclure le PDF (impression)", "Incluir PDF (impresión)", "PDF を含める(印刷)", "包括 PDF(打印)"], ), + ("PDF Preset:", ["PDF-Vorgabe:", "Paramètre prédéfini PDF :", "Ajuste preestablecido de PDF:", "PDF プリセット:", "PDF 预设:"]), ("Show Problems Only", ["Nur Probleme anzeigen", "Afficher uniquement les problèmes", "Mostrar solo problemas", "問題のみ表示", "仅显示问题"]), ("Printing Instructions", ["Druckanweisungen", "Instructions d’impression", "Instrucciones de impresión", "印刷指示", "打印说明"]), ("Continue", ["Weiter", "Continuer", "Continuar", "続行", "继续"]), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 8ab4c5c3d..aaba9d927 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -70,6 +70,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Section", "Новий розділ"), ("Clear Frame Fitting Options", "Очистити параметри припасування фрейму"), ("Cannot be edited", "Не можна змінити"), + ("PDF Preset:", "Набір параметрів PDF:"), ("Packaged {count} files into {dir}", "Запаковано файлів: {count} у {dir}"), ("Continue", "Продовжити"), ("Printing Instructions", "Інструкції для друку"), diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 04b236d76..c176d1b82 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -76,10 +76,14 @@ pub fn open(app: &mut DesignApp) -> Result<(), String> { .collect() }) .unwrap_or_default(); + // The packaged PDF's preset: the one last used for Export PDF, as InDesign suggests it. + let last = app.ui.pdf_export.preset.clone(); + let preset = crate::dialogs::pdf_presets().find(|p| *p == last).unwrap_or("Desktop Printing"); let doc = app.session.active().map(|d| d.uid); app.ui.dialog = Some(Dialog::new( ID, - json!({"page": "summary", "doc": doc, "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, "problemsOnly": false}), + json!({"page": "summary", "doc": doc, "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, + "pdfPreset": preset, "problemsOnly": false}), )); Ok(()) } @@ -282,6 +286,26 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { d.fields.insert(key.into(), json!(on)); } } + // The PDF's preset, as InDesign's Select PDF Preset. + ui.add_enabled_ui(d.b("pdf"), |ui| { + ui.horizontal(|ui| { + ui.add_space(24.0); + let name = crate::rtl::label(ui, tr("PDF Preset:")); + let cur = d.s("pdfPreset"); + craft_ui::ComboBox::from_id_salt("package_pdf_preset") + .selected_text(crate::rtl::widget(ui, tr(&cur))) + .width(180.0) + .show_ui(ui, |ui| { + for p in crate::dialogs::pdf_presets() { + if ui.selectable_label(cur == p, crate::rtl::widget(ui, tr(p))).clicked() { + d.fields.insert("pdfPreset".into(), json!(p)); + } + } + }) + .response + .labelled_by(name.id); + }); + }); } } if matches!(page.as_str(), "fonts" | "links") { @@ -295,10 +319,15 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { }); } +/// The `pdf` of `file.package`: false, or the options of the chosen PDF preset. +fn pdf_param(d: &Dialog) -> Value { + if d.b("pdf") { crate::dialogs::pdf_preset_params(&d.s("pdfPreset")) } else { json!(false) } +} + /// Package…: the Printing Instructions first when asked for, else the folder. pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result { same_document(app, d)?; - let (idml, pdf) = (d.b("idml"), d.b("pdf")); + let (idml, pdf) = (d.b("idml"), pdf_param(d)); if d.b("instructions") { app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf, "doc": d.fields.get("doc")}))); return Ok(Value::Null); @@ -356,13 +385,14 @@ fn instructions_text(d: &Dialog) -> String { pub fn confirm_instructions(app: &mut DesignApp, d: &Dialog) -> Result { same_document(app, d)?; let text = instructions_text(d); - package_to_folder(app, d.b("idml"), d.b("pdf"), Some(text).filter(|t| !t.is_empty()), d.clone()) + package_to_folder(app, d.b("idml"), d.fields.get("pdf").cloned().unwrap_or(json!(false)), Some(text).filter(|t| !t.is_empty()), d.clone()) } /// Ask for the folder to create (" Folder") and package the document into it once it is /// chosen (Cancel packages nothing). Packaging that fails reopens `asking`, the dialog that asked, -/// with its options. -pub fn package_to_folder(app: &mut DesignApp, idml: bool, pdf: bool, instructions: Option<String>, asking: Dialog) -> Result<Value, String> { +/// with its options. `pdf`: +/// false, true or the PDF's `file.exportPdf` options. +pub fn package_to_folder(app: &mut DesignApp, idml: bool, pdf: Value, instructions: Option<String>, asking: Dialog) -> Result<Value, String> { let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default(); app.ask_save(&name, Bound::Document, move |app, dir| { let mut p = json!({"dir": dir, "idml": idml, "pdf": pdf}); @@ -479,8 +509,8 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } - /// Without instructions, Package… goes straight to the folder; Include PDF adds the PDF and - /// Include IDML off leaves the IDML out. + /// Without instructions, Package… goes straight to the folder; Include PDF adds the PDF, made + /// with the PDF preset chosen, and Include IDML off leaves the IDML out. #[test] fn package_options_choose_the_idml_and_pdf() { let dir = temp_dir("options"); @@ -495,11 +525,18 @@ mod tests { h.run_steps(2); h.get_by_label("Include PDF (Print)").click(); h.run_steps(2); + // The PDF's preset: PDF/X-4. + h.get_by_value("Desktop Printing").click(); + h.run_steps(2); + h.get_by_label("PDF/X-4").click(); + h.run_steps(2); h.get_by_label("Package…").click(); h.run_steps(3); h.state_mut().settle_dialogs(); assert!(h.state().ui.dialog.is_none()); assert!(out.join("Packed.pdf").exists() && !out.join("Packed.idml").exists()); + let pdf = std::fs::read(out.join("Packed.pdf")).unwrap(); + assert!(pdf.windows(8).any(|w| w == b"GTS_PDFX"), "the PDF/X-4 preset's PDF"); std::fs::remove_dir_all(dir).unwrap(); }