From d98b2cce01309536fcaed6ece2c6b2eedccb5818 Mon Sep 17 00:00:00 2001 From: Aykut Firat Date: Sat, 17 Oct 2026 15:32:00 -0400 Subject: [PATCH 01/28] IDML: TOC styles read InDesign's $ID/-prefixed separators and Properties-child entry styles InDesign writes a TOC style entry's separator with the built-in prefix (`Separator="$ID/^y"`, the right indent tab) and its FormatStyle / PageNumberStyle / SeparatorStyle as `` children, not attributes. The importer kept the prefix as text (entries read "Welcome$ID/3") and never found the entry styles, so a regenerated TOC lost its entry paragraph style and page-number character style. - names::toc_separator_in strips `$ID/` before decoding. - toc_of reads FormatStyle, PageNumberStyle and SeparatorStyle with `prop` (attribute or Properties child). - tests: a TOC style written as InDesign writes it imports its separator, entry style, page-number style and title style. Co-Authored-By: Claude Opus 5.5 --- crates/idml/src/import.rs | 7 ++++--- crates/idml/src/names.rs | 2 ++ crates/idml/src/tests.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 5cd3746a9..cc438de6a 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -1506,9 +1506,10 @@ impl<'r> Importer<'r> { _ => TocPageNumber::After, }; let separator = e.get("Separator").map(crate::names::toc_separator_in).filter(|s| s != "\t" && s.chars().count() <= 64); - let entry_style = known(e.get("FormatStyle")).filter(|n| *n != format!("TOC Level {level}")); - let page_number_style = known_chars(e.get("PageNumberStyle")); - let separator_style = known_chars(e.get("SeparatorStyle")); + // InDesign writes these as `Properties` children; attributes are read too. + let entry_style = known(e.prop("FormatStyle").as_deref()).filter(|n| *n != format!("TOC Level {level}")); + let page_number_style = known_chars(e.prop("PageNumberStyle").as_deref()); + let separator_style = known_chars(e.prop("SeparatorStyle").as_deref()); let sort = e.get("Sort").is_some_and(|v| v.trim() == "true"); Some(TocEntry { style, level, entry_style, page_number, separator, page_number_style, separator_style, sort }) }) diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index 70d02e7c0..c242a1626 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -843,6 +843,8 @@ pub fn toc_separator_out(s: &str) -> String { } /// IDML TOC separator → text (`^t` and `^y`, the right indent tab, are tabs; other codes stay). pub fn toc_separator_in(s: &str) -> String { + // InDesign writes its built-in values with the `$ID/` prefix (`$ID/^y`, the right indent tab). + let s = s.strip_prefix("$ID/").unwrap_or(s); let mut out = String::new(); let mut it = s.chars(); while let Some(c) = it.next() { diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 3f5b7977d..785bf458f 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4189,3 +4189,35 @@ mod liquid_page_rules { } } } + +/// A TOC style as InDesign writes it: `$ID/`-prefixed separator (`^y`, the right indent tab) and entry style. +const TOC_STYLES: &str = r#" + + + + + + + + + + + ParagraphStyle/$ID/TOC%3aTOC body text + CharacterStyle/TOC Page Number + $ID/[Same Style] + +"#; + +const TOC_STORY: &str = r#" +Welcome"#; + +#[test] +fn toc_style_entries_read_id_prefixed_separators_and_styles() { + let d = import_idml(&fixture_with_table_styles(TOC_STYLES, TOC_STORY)).unwrap(); + let toc = d.toc.as_ref().expect("the story's TOC style is imported"); + let entry = toc.entries.iter().find(|e| e.style == "Heading 1").unwrap(); + assert_eq!(entry.separator, None, "`$ID/^y` is the right indent tab, the default separator, not text: {:?}", entry.separator); + assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); + assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); + assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); +} From e8ec23a88d71e4b004634e4967df0346ad91bd44 Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:51:54 +0200 Subject: [PATCH 02/28] TOC-1: preserve right-indent separators and expose their picker --- ROADMAP.md | 2 ++ crates/doc/src/story.rs | 29 ++++++++++++++++++++++ crates/engine/src/tests_idml.rs | 2 +- crates/idml/src/names.rs | 39 +++++++++++++----------------- crates/idml/src/tests.rs | 2 +- crates/ui-egui/src/dialogs.rs | 43 ++++++++++++++++++++++++++++++--- docs/ui-parity.md | 8 +++++- 7 files changed, 97 insertions(+), 28 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9bd0274ef..9a52fcd2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -346,6 +346,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **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. + Newest first. - **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change. diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index e3a0e39ee..2f9860d31 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -31,6 +31,35 @@ pub const FRAME_BREAK: char = '\u{E003}'; pub const PAGE_BREAK: char = '\u{E004}'; pub const INDENT_HERE: char = '\u{E005}'; pub const RIGHT_INDENT_TAB: char = '\u{E006}'; + +/// Encode a table-of-contents separator using InDesign's special-character notation. +pub fn toc_separator_notation(text: &str) -> String { + text.replace('^', "^^").replace('\t', "^t").replace(RIGHT_INDENT_TAB, "^y").replace('\u{2003}', "^m").replace('\u{2002}', "^>") +} + +/// Decode TOC notation, preserving unknown codes and an escaped literal caret. +pub fn toc_separator_text(notation: &str) -> String { + let mut text = String::new(); + let mut chars = notation.chars(); + while let Some(ch) = chars.next() { + if ch != '^' { + text.push(ch); + continue; + } + match chars.next() { + Some('t') => text.push('\t'), + Some('y') => text.push(RIGHT_INDENT_TAB), + Some('m') => text.push('\u{2003}'), + Some('>') => text.push('\u{2002}'), + Some('^') | None => text.push('^'), + Some(other) => { + text.push('^'); + text.push(other); + } + } + } + text +} pub const NEXT_PAGE_NUMBER: char = '\u{E007}'; pub const PREV_PAGE_NUMBER: char = '\u{E008}'; /// Anchor of a table: alone in its paragraph, whose [`ParaFormat::table`] names the table. diff --git a/crates/engine/src/tests_idml.rs b/crates/engine/src/tests_idml.rs index 0701052d0..686687781 100644 --- a/crates/engine/src/tests_idml.rs +++ b/crates/engine/src/tests_idml.rs @@ -510,7 +510,7 @@ fn table_of_contents_definition_round_trips_through_idml_and_updates() { s.execute( "toc.generate", &json!({"page": page, "title": "Inside", "titleStyle": basic, "entries": [ - {"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ "}, + {"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ \u{E006} "}, {"style": "Headline", "level": 2, "entryStyle": basic, "pageNumber": "none"}, ]}), ) diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index c242a1626..69d17c50b 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -837,30 +837,25 @@ pub fn digits_in(s: &str) -> Option { [Digits::Default, Digits::Arabic, Digits::Hindi, Digits::Farsi, Digits::Native].into_iter().find(|d| digits_out(*d) == s) } -/// A TOC separator in IDML's special-character notation: `^t` is a tab, `^^` a caret. +/// A TOC separator in IDML's special-character notation. pub fn toc_separator_out(s: &str) -> String { - s.replace('^', "^^").replace('\t', "^t") + designcraft_doc::story::toc_separator_notation(s) } -/// IDML TOC separator → text (`^t` and `^y`, the right indent tab, are tabs; other codes stay). +/// IDML may prefix a separator with its localized-string marker. pub fn toc_separator_in(s: &str) -> String { - // InDesign writes its built-in values with the `$ID/` prefix (`$ID/^y`, the right indent tab). - let s = s.strip_prefix("$ID/").unwrap_or(s); - let mut out = String::new(); - let mut it = s.chars(); - while let Some(c) = it.next() { - if c != '^' { - out.push(c); - continue; - } - match it.next() { - Some('t' | 'y') => out.push('\t'), - Some('^') => out.push('^'), - Some(o) => { - out.push('^'); - out.push(o); - } - None => out.push('^'), - } + designcraft_doc::story::toc_separator_text(s.strip_prefix("$ID/").unwrap_or(s)) +} + +#[cfg(test)] +mod toc_separator_tests { + use super::*; + + #[test] + fn localized_right_indent_tab_is_not_an_ordinary_tab() { + assert_eq!(toc_separator_in("$ID/^y"), "\u{E006}"); + assert_eq!(toc_separator_in("$ID/ ^y "), " \u{E006} "); + assert_eq!(toc_separator_in("^t"), "\t"); + assert_eq!(toc_separator_out("\u{E006}"), "^y"); + assert_eq!(toc_separator_in("^^y ^q ^"), "^y ^q ^"); } - out } diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 785bf458f..a1551b3e6 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4216,7 +4216,7 @@ fn toc_style_entries_read_id_prefixed_separators_and_styles() { let d = import_idml(&fixture_with_table_styles(TOC_STYLES, TOC_STORY)).unwrap(); let toc = d.toc.as_ref().expect("the story's TOC style is imported"); let entry = toc.entries.iter().find(|e| e.style == "Heading 1").unwrap(); - assert_eq!(entry.separator, None, "`$ID/^y` is the right indent tab, the default separator, not text: {:?}", entry.separator); + assert_eq!(entry.separator.as_deref(), Some("\u{E006}"), "right-indent tabs must not become ordinary tab stops"); assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 358416a4b..c1eff1078 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -6212,11 +6212,11 @@ struct TocRow { } fn toc_separator_shown(s: &str) -> String { - s.replace('\t', "^t").replace('\u{2003}', "^m").replace('\u{2002}', "^>") + designcraft_doc::story::toc_separator_notation(s) } fn toc_separator_typed(s: &str) -> String { - s.replace("^t", "\t").replace("^m", "\u{2003}").replace("^>", "\u{2002}") + designcraft_doc::story::toc_separator_text(s) } fn toc_rows(d: &Dialog) -> Vec { @@ -6407,7 +6407,19 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { toc_style_combo(ui, "page_number", &char_styles, none, &mut r.page_number_style); ui.end_row(); crate::rtl::label(ui, tr("Between Entry and Number:")); - ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).horizontal_align(egui::Align::Min)); + ui.horizontal(|ui| { + ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).horizontal_align(egui::Align::Min)); + ui.menu_button("▸", |ui| { + for (label, code) in [("Tab Character", "^t"), ("Right Indent Tab", "^y"), ("Em Space", "^m"), ("En Space", "^>")] { + if ui.button(tr(label)).clicked() { + r.separator.push_str(code); + ui.close(); + } + } + }) + .response + .on_hover_text(tr("Insert Special Character")); + }); crate::rtl::label(ui, tr("Style:")); toc_style_combo(ui, "separator", &char_styles, none, &mut r.separator_style); ui.end_row(); @@ -7957,6 +7969,31 @@ mod tests { assert_eq!(opts(&app), before); } + #[test] + fn toc_right_indent_separator_survives_the_dialog_and_update() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.newSample", json!({})).unwrap(); + open_toc(&mut app); + app.ui.dialog.as_mut().unwrap().fields.insert( + "included".into(), + json!([ + {"style": "Headline", "level": 1, "separator": " ^y "} + ]), + ); + confirm(&mut app).unwrap(); + let separator = format!(" {} ", designcraft_doc::story::RIGHT_INDENT_TAB); + let d = &app.session.doc().unwrap().doc; + let toc = d.toc.as_ref().unwrap(); + assert_eq!(toc.entries[0].separator.as_deref(), Some(separator.as_str())); + assert!(d.story(toc.story).unwrap().text.contains(&separator)); + app.run("toc.update", json!({})).unwrap(); + open_toc(&mut app); + assert_eq!(toc_rows(app.ui.dialog.as_ref().unwrap())[0].separator, " ^y "); + for text in ["^y", "^t^y", "^m^>", "unknown ^q and trailing ^", "a\t\u{E006}\u{2003}\u{2002}z"] { + assert_eq!(toc_separator_typed(&toc_separator_shown(text)), text); + } + } + #[test] fn toc_dialog_adds_a_style_and_ok_makes_the_table_of_contents() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 2dcf4687a..d7984e115 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (live corners work; document views row: scrollbars, Alt pans while editing text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; paragraph style override feedback and clearing) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (live corners work; document views row: scrollbars, Alt pans while editing text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; paragraph style override feedback and clearing) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS) Tools, handles, snapping, nudging, modifiers, shortcuts, panels, menus, context menus and feel, against InDesign 2026. The look was measured from the running app @@ -70,6 +70,12 @@ history (DC-1). ## Remaining effort +The Table of Contents separator field accepts `^y` (Right Indent Tab), with a special-character +picker alongside it. Literal carets use `^^`; ordinary tabs, em/en spaces and surrounding spaces +survive reopening. IDML preserves right-indent separators, including `$ID/^y`, so updating an +imported TOC keeps page numbers at the right edge and inherits paragraph tab leaders. This covers +10:54–16:08 of [Michael Bullo's TOC tutorial](https://www.youtube.com/watch?v=m4y2tm6LH_c). + Applying a paragraph or character style now removes local values equal to the applied style, so text used to capture a style follows later edits to that style. Different local formatting and named character styles stay intact. Paragraph-style application treats selection endpoints From 81913c2f482cecfcef98c6e8ce40368e58f8dee2 Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:07:02 +0200 Subject: [PATCH 03/28] TOC-1: localize separator picker and address review cleanup --- ROADMAP.md | 4 ++-- crates/doc/src/story.rs | 15 ++++++++------- crates/ui-egui/src/i18n.rs | 1 + crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/de.rs | 1 + crates/ui-egui/src/i18n/it.rs | 4 ++++ crates/ui-egui/src/i18n/ja.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 4 ++++ crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 4 ++++ 13 files changed, 30 insertions(+), 9 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9a52fcd2b..3b8d62fd4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -346,10 +346,10 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log -- **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. - Newest first. +- **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-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change. - **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change. - **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change. diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index 2f9860d31..510c2d849 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -32,6 +32,14 @@ pub const PAGE_BREAK: char = '\u{E004}'; pub const INDENT_HERE: char = '\u{E005}'; pub const RIGHT_INDENT_TAB: char = '\u{E006}'; +pub const NEXT_PAGE_NUMBER: char = '\u{E007}'; +pub const PREV_PAGE_NUMBER: char = '\u{E008}'; +/// Anchor of a table: alone in its paragraph, whose [`ParaFormat::table`] names the table. +pub const TABLE_ANCHOR: char = '\u{E009}'; +pub const FORCED_LINE_BREAK: char = '\u{2028}'; +pub const ODD_PAGE_BREAK: char = '\u{E011}'; +pub const EVEN_PAGE_BREAK: char = '\u{E012}'; + /// Encode a table-of-contents separator using InDesign's special-character notation. pub fn toc_separator_notation(text: &str) -> String { text.replace('^', "^^").replace('\t', "^t").replace(RIGHT_INDENT_TAB, "^y").replace('\u{2003}', "^m").replace('\u{2002}', "^>") @@ -60,13 +68,6 @@ pub fn toc_separator_text(notation: &str) -> String { } text } -pub const NEXT_PAGE_NUMBER: char = '\u{E007}'; -pub const PREV_PAGE_NUMBER: char = '\u{E008}'; -/// Anchor of a table: alone in its paragraph, whose [`ParaFormat::table`] names the table. -pub const TABLE_ANCHOR: char = '\u{E009}'; -pub const FORCED_LINE_BREAK: char = '\u{2028}'; -pub const ODD_PAGE_BREAK: char = '\u{E011}'; -pub const EVEN_PAGE_BREAK: char = '\u{E012}'; /// A column, frame or page break character. It ends its paragraph: the story stores it as the /// paragraph's last character, followed by the `\n` separator (also at the end of the story), and diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..a2f3f269e 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4397,6 +4397,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("OpenType font", ["OpenType-Schrift", "Police OpenType", "Fuente OpenType", "OpenType フォント", "OpenType 字体"]), ("Variable font", ["Variable Schrift", "Police variable", "Fuente variable", "バリアブルフォント", "可变字体"]), ("Color font", ["Farbschrift", "Police couleur", "Fuente de color", "カラーフォント", "彩色字体"]), + ("Tab Character", ["Tabulatorzeichen", "Caractère de tabulation", "Carácter de tabulación", "タブ文字", "制表符"]), // Table of Contents. ( "Sort Entries in Alphabetical Order", diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..5b22f124b 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -1,5 +1,6 @@ //! Original Arabic interface translations, stored in logical Unicode order. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "محرف جدولة"), ("File", "ملف"), ("Edit", "تحرير"), ("Layout", "تخطيط"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..f47746c4a 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -3,6 +3,7 @@ //! product's localization was used). Phrases shared with the MIT OR Apache-2.0 Czech catalogs of //! the ArtCraft siblings (VectorCraft, PhotoCraft) are kept identical; see ASSETS.md for sources. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Znak tabulátoru"), ("File", "Soubor"), ("Edit", "Úpravy"), ("Layout", "Rozvržení"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index efc128858..80874fe91 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -3,6 +3,7 @@ //! Brazilian Portuguese, Italian and Ukrainian tables has a row here; the German column of the shared //! table is carried over and stays the fallback for keys added there after this table. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Tabulatorzeichen"), ("File", "Datei"), ("Edit", "Bearbeiten"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..f8175c619 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1,6 +1,10 @@ //! Original Italian interface translations, written from the English source //! using ordinary Italian desktop-publishing vocabulary. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Carattere di tabulazione"), + ("Right Indent Tab", "Tabulazione rientro destro"), + ("Em Space", "Spazio em"), + ("En Space", "Spazio en"), ("File", "File"), ("Edit", "Modifica"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 5647ca940..919b10ed4 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -3,6 +3,7 @@ //! Portuguese tables has a row here; the menu titles and panel names that were already Japanese in the //! shared table are carried over unchanged. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "タブ文字"), ("File", "ファイル"), ("Edit", "編集"), ("Layout", "レイアウト"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 975da9ca1..dcd92b4d7 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -4,6 +4,7 @@ //! Portuguese, Japanese, Italian and Ukrainian tables has a row here, plus a few labels that only //! the code or the command table use. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "ტაბულაციის სიმბოლო"), ("File", "ფაილი"), ("Edit", "რედაქტირება"), ("Layout", "განლაგება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..a36518fda 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -2,6 +2,7 @@ //! are welcome. Terminology follows common Polish DTP usage //! (rozkładówka, łam, strona wzorcowa, obszar informacyjny) and the sibling ArtCraft catalogs. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Znak tabulacji"), ("File", "Plik"), ("Edit", "Edycja"), ("Layout", "Układ"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..888387797 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -1,6 +1,10 @@ //! Original Brazilian Portuguese interface translations, written from the English source //! using ordinary Brazilian Portuguese computing vocabulary. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Caractere de tabulação"), + ("Right Indent Tab", "Tabulação de recuo à direita"), + ("Em Space", "Espaço eme"), + ("En Space", "Espaço ene"), ("File", "Arquivo"), ("Edit", "Editar"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..4069930c8 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -3,6 +3,7 @@ //! The translations are our own; counted nouns are genitive plurals because the caption puts the //! category before the number ("страниц: 5"), see `count_label`. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Символ табуляции"), ("File", "Файл"), ("Edit", "Редактирование"), ("Can't Open the File", "Не удалось открыть файл"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..43030ff08 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -2,6 +2,10 @@ //! Common phrases are adapted from the MIT OR Apache-2.0 ArtCraft sibling catalogs //! (VectorCraft, PhotoCraft, WordCraft and DeckCraft); see ASSETS.md for pinned sources. pub(super) const TABLE: &[(&str, &str)] = &[ + ("Tab Character", "Символ табуляції"), + ("Right Indent Tab", "Табуляція правого відступу"), + ("Em Space", "Кругла шпація"), + ("En Space", "Півкругла шпація"), ("File", "Файл"), ("Edit", "Редагування"), ("Layout", "Макет"), From 0c5dd0ede83256dee41f1f4a329c435de627e305 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 20 Oct 2026 17:03:46 +0200 Subject: [PATCH 04/28] PR-1255: TOC style references in indented properties resolve; the picker keeps a trailing caret Co-Authored-By: Claude Opus 5.5 --- crates/idml/src/import.rs | 11 +++++++++-- crates/idml/src/tests.rs | 23 +++++++++++++++++++++++ crates/ui-egui/src/dialogs.rs | 17 ++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 76e2104f1..260911582 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -1498,10 +1498,12 @@ impl<'r> Importer<'r> { let story = top.iter().find(|e| e.local() == "Story" && e.get("AppliedTOCStyle") == Some(id))?; Some((ts, *self.story_ids.get(story.get("Self")?)?)) })?; - let known = |r: Option<&str>| r.map(|r| self.para_style_ref(r)).filter(|n| self.styles.para(n).is_some()); + // Property children are element text: formatting whitespace around a reference is dropped + // unless the reference as written names a style. + let known = |r: Option<&str>| r.map(|r| self.listed_para_style_ref(r)).filter(|n| self.styles.para(n).is_some()); // Character styles the document has; [None] is stored as no style. let known_chars = |r: Option<&str>| { - r.map(|r| self.char_style_ref(r)).filter(|n| n != designcraft_doc::NO_CHAR_STYLE && self.styles.char_style(n).is_some()) + r.map(|r| self.listed_char_style_ref(r)).filter(|n| n != designcraft_doc::NO_CHAR_STYLE && self.styles.char_style(n).is_some()) }; let entries: Vec = ts .elements() @@ -1538,6 +1540,11 @@ impl<'r> Importer<'r> { fn para_style_ref(&self, r: &str) -> String { self.para_names.get(r).cloned().unwrap_or_else(|| names::style_name_in(PARA_BUILTINS, &unescape_id(r.trim_start_matches("ParagraphStyle/")))) } + /// A paragraph style reference written as an element's text: as written when it names a + /// style, otherwise without surrounding whitespace (see [`Self::listed_char_style_ref`]). + fn listed_para_style_ref(&self, r: &str) -> String { + if self.para_names.contains_key(r) { self.para_style_ref(r) } else { self.para_style_ref(r.trim()) } + } /// A character style reference written as an element's text (nested, line and GREP styles). /// A style name may end in a space, so the reference is used as written when it names a /// style; only otherwise is surrounding whitespace dropped. diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 1c992192a..582242255 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4266,3 +4266,26 @@ fn toc_style_entries_read_id_prefixed_separators_and_styles() { assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); } + +#[test] +fn toc_style_references_in_indented_properties_resolve() { + // Property children laid out on their own lines: the whitespace around a reference isn't + // part of the style name. + let styles = TOC_STYLES + .replace( + ">ParagraphStyle/$ID/TOC%3aTOC body text<", + "> + ParagraphStyle/$ID/TOC%3aTOC body text + <", + ) + .replace( + ">CharacterStyle/TOC Page Number<", + "> + CharacterStyle/TOC Page Number + <", + ); + let d = import_idml(&fixture_with_table_styles(&styles, TOC_STORY)).unwrap(); + let entry = d.toc.as_ref().unwrap().entries.iter().find(|e| e.style == "Heading 1").unwrap(); + assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); + assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index af1e75146..69cc222ea 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -6221,6 +6221,15 @@ fn toc_separator_typed(s: &str) -> String { designcraft_doc::story::toc_separator_text(s) } +/// Appends the special-character `code` (such as `^y`) to the separator field's notation. A +/// trailing unescaped caret is a literal caret: it's escaped first, so it doesn't swallow the code. +fn toc_separator_append(field: &mut String, code: &str) { + if field.chars().rev().take_while(|&c| c == '^').count() % 2 == 1 { + field.push('^'); + } + field.push_str(code); +} + fn toc_rows(d: &Dialog) -> Vec { d.fields.get("included").cloned().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default() } @@ -6414,7 +6423,7 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.menu_button("▸", |ui| { for (label, code) in [("Tab Character", "^t"), ("Right Indent Tab", "^y"), ("Em Space", "^m"), ("En Space", "^>")] { if ui.button(tr(label)).clicked() { - r.separator.push_str(code); + toc_separator_append(&mut r.separator, code); ui.close(); } } @@ -8008,6 +8017,12 @@ mod tests { for text in ["^y", "^t^y", "^m^>", "unknown ^q and trailing ^", "a\t\u{E006}\u{2003}\u{2002}z"] { assert_eq!(toc_separator_typed(&toc_separator_shown(text)), text); } + // The picker after a literal caret: the caret stays, and the code is the code. + for (field, typed) in [("^", "^\u{E006}"), ("^^", "^\u{E006}"), ("a", "a\u{E006}"), ("^^^", "^^\u{E006}")] { + let mut f = field.to_string(); + toc_separator_append(&mut f, "^y"); + assert_eq!(toc_separator_typed(&f), typed, "{field:?} → {f:?}"); + } } #[test] From 1d59b6e739ffaa227a3b383f35e05bf8f20bab9e Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 20 Oct 2026 19:16:05 +0200 Subject: [PATCH 05/28] PR-1255: test the TOC importer's fallbacks for malformed and unsupported values Co-Authored-By: Claude Opus 5.5 --- crates/idml/src/tests.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 582242255..98934c869 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4267,6 +4267,43 @@ fn toc_style_entries_read_id_prefixed_separators_and_styles() { assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); } +#[test] +fn toc_style_entries_fall_back_on_malformed_and_unsupported_values() { + let long = "x".repeat(65); + let entries = format!( + r#" + ParagraphStyle/Missing + CharacterStyle/$ID/[None] + CharacterStyle/Missing + + + + "# + ); + let start = TOC_STYLES.find("").unwrap(); + let styles = format!("{}{entries}\n{}", &TOC_STYLES[..start], &TOC_STYLES[end..]) + .replace(r#"TitleStyle="ParagraphStyle/TOC%3aTOC Header""#, r#"TitleStyle="ParagraphStyle/Missing""#); + let d = import_idml(&fixture_with_table_styles(&styles, TOC_STORY)).unwrap(); + d.check().unwrap(); + let toc = d.toc.as_ref().expect("the TOC style is still imported"); + assert_eq!(toc.title_style, None, "a title style the document lacks is dropped"); + assert_eq!(toc.entries.len(), 3, "an entry without a style name is skipped: {:?}", toc.entries); + let e1 = &toc.entries[0]; + assert_eq!(e1.level, 1, "an unreadable level falls back to 1"); + assert_eq!(e1.page_number, designcraft_doc::TocPageNumber::After, "an unknown page-number position falls back to After"); + assert_eq!(e1.separator.as_deref(), Some("^q"), "an unknown special-character code is kept as written"); + assert_eq!( + (e1.entry_style.as_deref(), e1.page_number_style.as_deref(), e1.separator_style.as_deref()), + (None, None, None), + "undefined and [None] styles are no style" + ); + let e2 = &toc.entries[1]; + assert_eq!(e2.level, 9, "a level past 9 is clamped"); + assert_eq!(e2.separator, None, "an over-long separator is dropped"); + assert_eq!((toc.entries[2].level, toc.entries[2].separator.as_deref()), (2, None), "a plain tab is the default separator"); +} + #[test] fn toc_style_references_in_indented_properties_resolve() { // Property children laid out on their own lines: the whitespace around a reference isn't From d21459ff3ea1c807940596a76561a37a5d3f9883 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:25:29 +0200 Subject: [PATCH 06/28] =?UTF-8?q?File=20=E2=80=BA=20Package:=20InDesign's?= =?UTF-8?q?=20Package=20dialog=20and=20Printing=20Instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package… went straight to a folder picker and packaged with fixed options. As in InDesign it now opens the Package dialog: Summary counts the fonts, links and images and inks the package carries (a warning where some are missing or modified), and Fonts, Links and Images and Colors and Inks list them, with Show Problems Only. Create Printing Instructions leads to the Printing Instructions dialog (contact, company, address, phone, fax, email, instructions), whose text goes into the package's Instructions.txt; Include IDML and Include PDF (Print) choose the copies the package carries. Found in the Learn Skills Daily course, chapter 27. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 10 + crates/ui-egui/src/i18n.rs | 85 +++++ crates/ui-egui/src/i18n/ar.rs | 37 +++ crates/ui-egui/src/i18n/cs.rs | 36 +++ crates/ui-egui/src/i18n/it.rs | 38 +++ crates/ui-egui/src/i18n/ka.rs | 39 +++ crates/ui-egui/src/i18n/pl.rs | 39 +++ crates/ui-egui/src/i18n/pt_br.rs | 38 +++ crates/ui-egui/src/i18n/ru.rs | 39 +++ crates/ui-egui/src/i18n/uk.rs | 38 +++ crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/menus.rs | 24 +- crates/ui-egui/src/package_dialog.rs | 457 +++++++++++++++++++++++++++ 14 files changed, 866 insertions(+), 16 deletions(-) create mode 100644 crates/ui-egui/src/package_dialog.rs diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..b0e33a381 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **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-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 8b4043787..9182f5843 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2087,6 +2087,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "colorGroupOptions" => crate::i18n::tr(&app.ui.language, "Color Group Options"), "layerSettings" => crate::i18n::tr(&app.ui.language, "Layer Options"), crate::effects_dialog::ID => crate::i18n::tr(&app.ui.language, "Effects"), + crate::package_dialog::ID => crate::i18n::tr(&app.ui.language, "Package"), + crate::package_dialog::INSTRUCTIONS => crate::i18n::tr(&app.ui.language, "Printing Instructions"), id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) { Some(c) => crate::i18n::tr(&app.ui.language, c.label.trim_end_matches('…')), None => crate::i18n::tr(&app.ui.language, "Dialog"), @@ -2104,6 +2106,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "newDocument" => 960.0, "numberingSection" => 560.0, crate::effects_dialog::ID => 720.0, + crate::package_dialog::ID => 720.0, + crate::package_dialog::INSTRUCTIONS => 520.0, _ => 640.0, }; let width = preferred_width.min(available.x); @@ -2361,6 +2365,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "colorGroupOptions" => options::color_group_options(app, ui, &mut d), "layerSettings" => options::layer_options(app, ui, &mut d), crate::effects_dialog::ID => crate::effects_dialog::show(app, ui, &mut d), + crate::package_dialog::ID => crate::package_dialog::show(app, ui, &mut d), + crate::package_dialog::INSTRUCTIONS => crate::package_dialog::show_instructions(app, ui, &mut d), "preferences" => preferences(app, ui, &mut d, body_height), "print" => print_dialog(app, ui, &mut d), "pdfImport" => { @@ -2819,6 +2825,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "updateLink" => crate::i18n::tr(&app.ui.language, " Update ").to_string(), "findFont" => crate::i18n::tr(&app.ui.language, "Replace All").to_string(), "newDocument" => format!(" {} ", crate::i18n::tr(&app.ui.language, "Create")), + crate::package_dialog::ID => crate::i18n::tr(&app.ui.language, "Package…").to_string(), + crate::package_dialog::INSTRUCTIONS => crate::i18n::tr(&app.ui.language, "Continue").to_string(), _ => crate::i18n::tr(&app.ui.language, " OK ").to_string(), }; let cancel_label = match d.id.as_str() { @@ -3573,6 +3581,8 @@ pub fn confirm(app: &mut DesignApp) -> Result { } "alert" => Ok(Value::Null), crate::effects_dialog::ID => crate::effects_dialog::confirm(app, &d), + crate::package_dialog::ID => crate::package_dialog::confirm(app, &d), + crate::package_dialog::INSTRUCTIONS => crate::package_dialog::confirm_instructions(app, &d), "findChange" => { let result = if d.b("objectMode") { Ok(Value::Null) } else { app.run("find.change", find_change_params(&d)) }; // Confirmation consumes retained bounds before closing, even on an error. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..eccf6ca89 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -83,6 +83,91 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Print…", ["Drucken …", "Imprimer…", "Imprimir…", "プリント…", "打印…"]), ("Document Setup…", ["Dokument einrichten …", "Format de document…", "Ajustar documento…", "ドキュメント設定…", "文档设置…"]), ("Package…", ["Verpacken …", "Assemblage…", "Empaquetar…", "パッケージ…", "打包…"]), + ("Summary", ["Übersicht", "Résumé", "Resumen", "概要", "摘要"]), + ("Fonts", ["Schriften", "Polices", "Fuentes", "フォント", "字体"]), + ("Links and Images", ["Verknüpfungen und Bilder", "Liens et images", "Vínculos e imágenes", "リンクと画像", "链接和图像"]), + ("Colors and Inks", ["Farben und Druckfarben", "Couleurs et encres", "Colores y tintas", "カラーとインキ", "颜色和油墨"]), + ("Process Cyan", ["Prozess-Cyan", "Cyan quadri", "Cian de cuatricromía", "プロセスシアン", "印刷青色"]), + ("Process Magenta", ["Prozess-Magenta", "Magenta quadri", "Magenta de cuatricromía", "プロセスマゼンタ", "印刷洋红色"]), + ("Process Yellow", ["Prozess-Gelb", "Jaune quadri", "Amarillo de cuatricromía", "プロセスイエロー", "印刷黄色"]), + ("Process Black", ["Prozess-Schwarz", "Noir quadri", "Negro de cuatricromía", "プロセスブラック", "印刷黑色"]), + ("Contact:", ["Kontakt:", "Contact :", "Contacto:", "連絡先:", "联系人:"]), + ("Company:", ["Firma:", "Société :", "Empresa:", "会社名:", "公司:"]), + ("Address:", ["Adresse:", "Adresse :", "Dirección:", "住所:", "地址:"]), + ("Phone:", ["Telefon:", "Téléphone :", "Teléfono:", "電話:", "电话:"]), + ("Fax:", ["Fax:", "Fax :", "Fax:", "FAX:", "传真:"]), + ("Email:", ["E-Mail:", "E-mail :", "Correo electrónico:", "メール:", "电子邮件:"]), + ("Instructions:", ["Anweisungen:", "Instructions :", "Instrucciones:", "指示:", "说明:"]), + ( + "Fonts: {used} Fonts Used, {missing} Missing", + [ + "Schriften: {used} verwendet, {missing} fehlen", + "Polices : {used} utilisées, {missing} manquantes", + "Fuentes: {used} utilizadas, {missing} ausentes", + "フォント : {used} 個使用、{missing} 個見つかりません", + "字体:使用 {used} 种,缺失 {missing} 种", + ], + ), + ( + "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + [ + "Verknüpfungen und Bilder: {found} gefunden; {modified} geändert, {missing} fehlen", + "Liens et images : {found} liens trouvés ; {modified} modifiés, {missing} manquants", + "Vínculos e imágenes: {found} vínculos encontrados; {modified} modificados, {missing} ausentes", + "リンクと画像 : {found} 個のリンク、{modified} 個変更、{missing} 個見つかりません", + "链接和图像:找到 {found} 个链接;{modified} 个已修改,{missing} 个缺失", + ], + ), + ( + "Images: {embedded} Embedded", + [ + "Bilder: {embedded} eingebettet", + "Images : {embedded} incorporées", + "Imágenes: {embedded} incrustadas", + "画像 : {embedded} 個埋め込み", + "图像:嵌入 {embedded} 个", + ], + ), + ( + "Colors and Inks: {process} Process Inks; {spot} Spot Inks", + [ + "Farben und Druckfarben: {process} Prozessfarben; {spot} Volltonfarben", + "Couleurs et encres : {process} encres quadri ; {spot} encres d’accompagnement", + "Colores y tintas: {process} tintas de cuatricromía; {spot} tintas planas", + "カラーとインキ : プロセスインキ {process} 個、スポットインキ {spot} 個", + "颜色和油墨:{process} 种印刷色油墨;{spot} 种专色油墨", + ], + ), + ("Missing", ["Fehlt", "Manquant", "Ausente", "見つかりません", "缺失"]), + ("Modified", ["Geändert", "Modifié", "Modificado", "変更", "已修改"]), + ("Substituted", ["Ersetzt", "Remplacée", "Sustituida", "代替", "已替换"]), + ("Bundled", ["Mitgeliefert", "Fournie", "Incluida", "同梱", "内置"]), + ("Installed", ["Installiert", "Installée", "Instalada", "インストール済み", "已安装"]), + ("Source", ["Quelle", "Source", "Origen", "ソース", "来源"]), + ("Scope:", ["Bereich:", "Étendue :", "Ámbito:", "範囲:", "范围:"]), + ("Entire Publication", ["Gesamte Publikation", "Publication entière", "Toda la publicación", "出版物全体", "整个出版物"]), + ( + "Create Printing Instructions", + ["Druckanweisungen erstellen", "Créer des instructions d’impression", "Crear instrucciones de impresión", "印刷指示を作成", "创建打印说明"], + ), + ("Include IDML", ["IDML einschließen", "Inclure l’IDML", "Incluir IDML", "IDML を含める", "包括 IDML"]), + ( + "Include PDF (Print)", + ["PDF einschließen (Druck)", "Inclure le PDF (impression)", "Incluir PDF (impresión)", "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", "続行", "继续"]), + ( + "Packaged {count} files into {dir}", + [ + "{count} Dateien in {dir} verpackt", + "{count} fichiers assemblés dans {dir}", + "{count} archivos empaquetados en {dir}", + "{count} 個のファイルを {dir} にパッケージしました", + "已将 {count} 个文件打包到 {dir}", + ], + ), // Edit. ("Undo", ["Rückgängig", "Annuler", "Deshacer", "取り消し", "还原"]), ("Redo", ["Wiederholen", "Rétablir", "Rehacer", "やり直し", "重做"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..268b799d1 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,43 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Packaged {count} files into {dir}", "تم حزم {count} ملفات في {dir}"), + ("Continue", "متابعة"), + ("Printing Instructions", "تعليمات الطباعة"), + ("Show Problems Only", "إظهار المشكلات فقط"), + ("Include PDF (Print)", "تضمين PDF (للطباعة)"), + ("Include IDML", "تضمين IDML"), + ("Create Printing Instructions", "إنشاء تعليمات الطباعة"), + ("Entire Publication", "المنشور بالكامل"), + ("Scope:", "النطاق:"), + ("Source", "المصدر"), + ("Installed", "مثبت"), + ("Bundled", "مضمن مع التطبيق"), + ("Substituted", "مستبدل"), + ("Modified", "معدل"), + ("Missing", "مفقود"), + ("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", + "الروابط والصور: {found} روابط؛ {modified} معدلة، {missing} مفقودة", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "الخطوط: {used} مستخدمة، {missing} مفقودة"), + ("Instructions:", "التعليمات:"), + ("Email:", "البريد الإلكتروني:"), + ("Fax:", "الفاكس:"), + ("Phone:", "الهاتف:"), + ("Address:", "العنوان:"), + ("Company:", "الشركة:"), + ("Contact:", "جهة الاتصال:"), + ("Process Black", "أسود معالجة"), + ("Process Yellow", "أصفر معالجة"), + ("Process Magenta", "أرجواني معالجة"), + ("Process Cyan", "سماوي معالجة"), + ("Colors and Inks", "الألوان والأحبار"), + ("Links and Images", "الروابط والصور"), + ("Fonts", "الخطوط"), + ("Summary", "الملخص"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..a51121541 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,42 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Continue", "Pokračovat"), + ("Printing Instructions", "Pokyny k tisku"), + ("Show Problems Only", "Zobrazit jen problémy"), + ("Include PDF (Print)", "Zahrnout PDF (tisk)"), + ("Include IDML", "Zahrnout IDML"), + ("Create Printing Instructions", "Vytvořit pokyny k tisku"), + ("Entire Publication", "Celá publikace"), + ("Scope:", "Rozsah:"), + ("Spot", "Přímá"), + ("Process", "Procesní"), + ("Source", "Zdroj"), + ("Installed", "Nainstalováno"), + ("Bundled", "Přibaleno"), + ("Substituted", "Nahrazeno"), + ("Modified", "Změněno"), + ("Missing", "Chybí"), + ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Barvy a inkousty: procesní {process}; přímé {spot}"), + ("Images: {embedded} Embedded", "Obrazy: vloženo {embedded}"), + ( + "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Odkazy a obrazy: nalezeno {found}; změněno {modified}, chybí {missing}", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Písma: použito {used}, chybí {missing}"), + ("Instructions:", "Pokyny:"), + ("Fax:", "Fax:"), + ("Address:", "Adresa:"), + ("Company:", "Společnost:"), + ("Contact:", "Kontakt:"), + ("Process Black", "Procesní černá"), + ("Process Yellow", "Procesní žlutá"), + ("Process Magenta", "Procesní purpurová"), + ("Process Cyan", "Procesní azurová"), + ("Colors and Inks", "Barvy a inkousty"), + ("Links and Images", "Odkazy a obrazy"), + ("Fonts", "Písma"), + ("Summary", "Souhrn"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..0d0d49112 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,44 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Packaged {count} files into {dir}", "{count} file raccolti in {dir}"), + ("Continue", "Continua"), + ("Printing Instructions", "Istruzioni di stampa"), + ("Package", "Pacchetto"), + ("Show Problems Only", "Mostra solo problemi"), + ("Include PDF (Print)", "Includi PDF (stampa)"), + ("Include IDML", "Includi IDML"), + ("Create Printing Instructions", "Crea istruzioni di stampa"), + ("Entire Publication", "Intera pubblicazione"), + ("Scope:", "Ambito:"), + ("Source", "Origine"), + ("Installed", "Installato"), + ("Bundled", "Incluso"), + ("Substituted", "Sostituito"), + ("Modified", "Modificato"), + ("Missing", "Mancante"), + ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Colori e inchiostri: {process} quadricromia; {spot} tinte piatte"), + ("Images: {embedded} Embedded", "Immagini: {embedded} incorporate"), + ( + "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Collegamenti e immagini: {found} trovati; {modified} modificati, {missing} mancanti", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Font: {used} utilizzati, {missing} mancanti"), + ("Instructions:", "Istruzioni:"), + ("Email:", "E-mail:"), + ("Fax:", "Fax:"), + ("Phone:", "Telefono:"), + ("Address:", "Indirizzo:"), + ("Company:", "Società:"), + ("Contact:", "Contatto:"), + ("Process Black", "Nero quadricromia"), + ("Process Yellow", "Giallo quadricromia"), + ("Process Magenta", "Magenta quadricromia"), + ("Process Cyan", "Cyan quadricromia"), + ("Colors and Inks", "Colori e inchiostri"), + ("Links and Images", "Collegamenti e immagini"), + ("Fonts", "Font"), + ("Summary", "Riepilogo"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..142f806ac 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Packaged {count} files into {dir}", "{count} ფაილი შეიფუთა {dir}-ში"), + ("Continue", "გაგრძელება"), + ("Printing Instructions", "ბეჭდვის ინსტრუქციები"), + ("Show Problems Only", "მხოლოდ პრობლემების ჩვენება"), + ("Include PDF (Print)", "PDF-ის ჩართვა (ბეჭდვა)"), + ("Include IDML", "IDML-ის ჩართვა"), + ("Create Printing Instructions", "ბეჭდვის ინსტრუქციების შექმნა"), + ("Entire Publication", "მთელი პუბლიკაცია"), + ("Scope:", "არეალი:"), + ("Spot", "ლაქა"), + ("Process", "პროცესი"), + ("Source", "წყარო"), + ("Installed", "დაყენებული"), + ("Bundled", "ჩაშენებული"), + ("Substituted", "ჩანაცვლებული"), + ("Modified", "შეცვლილი"), + ("Missing", "აკლია"), + ("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", + "ბმულები და გამოსახულებები: ნაპოვნია {found}; შეცვლილია {modified}, აკლია {missing}", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"), + ("Instructions:", "ინსტრუქციები:"), + ("Email:", "ელფოსტა:"), + ("Fax:", "ფაქსი:"), + ("Phone:", "ტელეფონი:"), + ("Address:", "მისამართი:"), + ("Company:", "კომპანია:"), + ("Contact:", "საკონტაქტო პირი:"), + ("Process Black", "პროცესის შავი"), + ("Process Yellow", "პროცესის ყვითელი"), + ("Process Magenta", "პროცესის მეწამული"), + ("Process Cyan", "პროცესის ცისფერი"), + ("Colors and Inks", "ფერები და მელნები"), + ("Links and Images", "ბმულები და გამოსახულებები"), + ("Fonts", "შრიფტები"), + ("Summary", "შეჯამება"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..ca8c99e33 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Packaged {count} files into {dir}", "Spakowano {count} plików do {dir}"), + ("Continue", "Kontynuuj"), + ("Printing Instructions", "Instrukcje drukowania"), + ("Show Problems Only", "Pokaż tylko problemy"), + ("Include PDF (Print)", "Dołącz PDF (druk)"), + ("Include IDML", "Dołącz IDML"), + ("Create Printing Instructions", "Utwórz instrukcje drukowania"), + ("Entire Publication", "Cała publikacja"), + ("Scope:", "Zakres:"), + ("Spot", "Dodatkowy"), + ("Process", "Procesowy"), + ("Source", "Źródło"), + ("Installed", "Zainstalowana"), + ("Bundled", "Dołączona"), + ("Substituted", "Zastąpiona"), + ("Modified", "Zmieniony"), + ("Missing", "Brak"), + ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Kolory i farby: procesowe {process}; dodatkowe {spot}"), + ("Images: {embedded} Embedded", "Obrazy: osadzone {embedded}"), + ( + "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Łącza i obrazy: znaleziono {found}; zmienione {modified}, brakujące {missing}", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Czcionki: użyte {used}, brakujące {missing}"), + ("Instructions:", "Instrukcje:"), + ("Email:", "E-mail:"), + ("Fax:", "Faks:"), + ("Phone:", "Telefon:"), + ("Address:", "Adres:"), + ("Company:", "Firma:"), + ("Contact:", "Kontakt:"), + ("Process Black", "Czarny procesowy"), + ("Process Yellow", "Żółty procesowy"), + ("Process Magenta", "Magenta procesowa"), + ("Process Cyan", "Cyjan procesowy"), + ("Colors and Inks", "Kolory i farby"), + ("Links and Images", "Łącza i obrazy"), + ("Fonts", "Czcionki"), + ("Summary", "Podsumowanie"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..9c4317506 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,44 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Packaged {count} files into {dir}", "{count} arquivos empacotados em {dir}"), + ("Continue", "Continuar"), + ("Printing Instructions", "Instruções de impressão"), + ("Package", "Empacotar"), + ("Show Problems Only", "Mostrar apenas problemas"), + ("Include PDF (Print)", "Incluir PDF (impressão)"), + ("Include IDML", "Incluir IDML"), + ("Create Printing Instructions", "Criar instruções de impressão"), + ("Entire Publication", "Publicação inteira"), + ("Scope:", "Escopo:"), + ("Source", "Origem"), + ("Installed", "Instalada"), + ("Bundled", "Incluída"), + ("Substituted", "Substituída"), + ("Modified", "Modificado"), + ("Missing", "Ausente"), + ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Cores e tintas: {process} de escala; {spot} especiais"), + ("Images: {embedded} Embedded", "Imagens: {embedded} incorporadas"), + ( + "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Vínculos e imagens: {found} encontrados; {modified} modificados, {missing} ausentes", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Fontes: {used} usadas, {missing} ausentes"), + ("Instructions:", "Instruções:"), + ("Email:", "E-mail:"), + ("Fax:", "Fax:"), + ("Phone:", "Telefone:"), + ("Address:", "Endereço:"), + ("Company:", "Empresa:"), + ("Contact:", "Contato:"), + ("Process Black", "Preto de escala"), + ("Process Yellow", "Amarelo de escala"), + ("Process Magenta", "Magenta de escala"), + ("Process Cyan", "Ciano de escala"), + ("Colors and Inks", "Cores e tintas"), + ("Links and Images", "Vínculos e imagens"), + ("Fonts", "Fontes"), + ("Summary", "Resumo"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..4888924ee 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Packaged {count} files into {dir}", "Упаковано файлов: {count} в {dir}"), + ("Continue", "Продолжить"), + ("Printing Instructions", "Инструкции для печати"), + ("Show Problems Only", "Показывать только проблемы"), + ("Include PDF (Print)", "Включить PDF (печать)"), + ("Include IDML", "Включить IDML"), + ("Create Printing Instructions", "Создать инструкции для печати"), + ("Entire Publication", "Вся публикация"), + ("Scope:", "Область:"), + ("Spot", "Плашечный"), + ("Process", "Триадный"), + ("Source", "Источник"), + ("Installed", "Установлен"), + ("Bundled", "Встроенный"), + ("Substituted", "Заменен"), + ("Modified", "Изменено"), + ("Missing", "Отсутствует"), + ("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", + "Связи и изображения: найдено {found}; изменено {modified}, отсутствует {missing}", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Шрифты: используется {used}, отсутствует {missing}"), + ("Instructions:", "Инструкции:"), + ("Email:", "Эл. почта:"), + ("Fax:", "Факс:"), + ("Phone:", "Телефон:"), + ("Address:", "Адрес:"), + ("Company:", "Компания:"), + ("Contact:", "Контакт:"), + ("Process Black", "Триадный черный"), + ("Process Yellow", "Триадный желтый"), + ("Process Magenta", "Триадный пурпурный"), + ("Process Cyan", "Триадный голубой"), + ("Colors and Inks", "Цвета и краски"), + ("Links and Images", "Связи и изображения"), + ("Fonts", "Шрифты"), + ("Summary", "Сводка"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..88d72cb57 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,44 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Packaged {count} files into {dir}", "Запаковано файлів: {count} у {dir}"), + ("Continue", "Продовжити"), + ("Printing Instructions", "Інструкції для друку"), + ("Package", "Пакування"), + ("Show Problems Only", "Показувати лише проблеми"), + ("Include PDF (Print)", "Включити PDF (друк)"), + ("Include IDML", "Включити IDML"), + ("Create Printing Instructions", "Створити інструкції для друку"), + ("Entire Publication", "Уся публікація"), + ("Scope:", "Область:"), + ("Source", "Джерело"), + ("Installed", "Встановлений"), + ("Bundled", "Вбудований"), + ("Substituted", "Замінено"), + ("Modified", "Змінено"), + ("Missing", "Бракує"), + ("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", + "Зв’язки й зображення: знайдено {found}; змінено {modified}, бракує {missing}", + ), + ("Fonts: {used} Fonts Used, {missing} Missing", "Шрифти: використано {used}, бракує {missing}"), + ("Instructions:", "Інструкції:"), + ("Email:", "Ел. пошта:"), + ("Fax:", "Факс:"), + ("Phone:", "Телефон:"), + ("Address:", "Адреса:"), + ("Company:", "Компанія:"), + ("Contact:", "Контакт:"), + ("Process Black", "Тріадний чорний"), + ("Process Yellow", "Тріадний жовтий"), + ("Process Magenta", "Тріадний пурпуровий"), + ("Process Cyan", "Тріадний блакитний"), + ("Colors and Inks", "Кольори й фарби"), + ("Links and Images", "Зв’язки й зображення"), + ("Fonts", "Шрифти"), + ("Summary", "Зведення"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index cb98cac7f..758ec7988 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -21,6 +21,7 @@ pub mod menus; mod modal_drag; mod new_document; mod object_control; +pub mod package_dialog; mod panel_docking; pub mod panels; mod prefs; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 79b8b2c4e..794e58f2d 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -39,7 +39,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ None, "{page?: transparency|dropShadow|innerShadow|outerGlow|innerGlow|bevel|satin|feather|directionalFeather|gradientFeather} — the Effects dialog for the selected objects", ), - ("app.packageDialog", "Package…", Some("Cmd+Alt+Shift+P"), "{} — choose the package folder to create (file.package)"), + ( + "app.packageDialog", + "Package…", + Some("Cmd+Alt+Shift+P"), + "{} — the Package dialog (what the package carries, printing instructions), then the folder to create (file.package)", + ), ("app.saveSwatches", "Save Swatches for Exchange…", None, "{path?} — write the colour swatches as .ase"), ("app.exportPng", "Export Page as PNG…", Some("Cmd+E"), "{}"), ("app.exportIdml", "Export IDML…", None, "{path?} — InDesign Markup (IDML) package"), @@ -1145,21 +1150,8 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option export_bytes(app, p, "rtf", "file.exportText"), "app.packageDialog" => { - let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default(); - match app.services.pick_save.as_mut().and_then(|f| f(&name)) { - Some(dir) => { - let r = app.run("file.package", json!({"dir": dir})); - if let Ok(v) = &r { - app.status( - crate::i18n::tr(&app.ui.language, "Packaged {count} files into {dir}") - .replace("{count}", &v["files"].as_array().map_or(0, |f| f.len()).to_string()) - .replace("{dir}", v["dir"].as_str().unwrap_or("")), - ); - } - r - } - None => Ok(Value::Null), - } + crate::package_dialog::open(app); + Ok(Value::Null) } "app.language" => { let Some(v) = p.get("lang").and_then(Value::as_str) else { return Some(app.run("app.preferences", json!({}))) }; diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs new file mode 100644 index 000000000..6c00ca060 --- /dev/null +++ b/crates/ui-egui/src/package_dialog.rs @@ -0,0 +1,457 @@ +//! File › Package…: InDesign's Package dialog. Summary counts what the package carries (fonts, +//! links and images, inks), with a warning where something is missing or modified; Fonts, Links +//! and Images and Colors and Inks list them (Show Problems Only). Package… asks for the folder to +//! create and writes the package (`file.package`), after the Printing Instructions when Create +//! Printing Instructions is on. + +use serde_json::{Value, json}; + +use crate::DesignApp; +use crate::dialogs::Dialog; +use crate::theme::semibold; + +/// The Package dialog's id. +pub const ID: &str = "package"; +/// The Printing Instructions dialog's id. +pub const INSTRUCTIONS: &str = "printingInstructions"; + +/// The dialog's pages: (key, label). +const PAGES: [(&str, &str); 4] = [("summary", "Summary"), ("fonts", "Fonts"), ("links", "Links and Images"), ("inks", "Colors and Inks")]; + +/// The process inks every package prints with. +const PROCESS_INKS: [&str; 4] = ["Process Cyan", "Process Magenta", "Process Yellow", "Process Black"]; + +/// The Printing Instructions' fields: (key, label, lines). +const INSTRUCTION_FIELDS: [(&str, &str, usize); 7] = [ + ("contact", "Contact:", 1), + ("company", "Company:", 1), + ("address", "Address:", 3), + ("phone", "Phone:", 1), + ("fax", "Fax:", 1), + ("email", "Email:", 1), + ("instructions", "Instructions:", 4), +]; + +/// Open the Package dialog on its Summary, with the document's fonts, links and spot inks. +pub fn open(app: &mut DesignApp) { + if app.session.active().is_none() { + return; + } + let fonts: Vec = app + .session + .execute("font.list", &json!({})) + .ok() + .and_then(|v| v.as_array().cloned()) + .unwrap_or_default() + .iter() + .map(|f| { + let status = if f["missing"].as_bool().unwrap_or(false) { + "missing" + } else if f["styleMissing"].as_bool().unwrap_or(false) || f["matchStatus"] == "styleSubstitute" { + "substituted" + } else { + "ok" + }; + let name = format!("{} {}", f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or("")).trim().to_string(); + json!({"name": name, "source": f["source"].as_str().unwrap_or(""), "status": status}) + }) + .collect(); + let links: Vec = app + .session + .execute("links.list", &json!({})) + .ok() + .and_then(|v| v.as_array().cloned()) + .unwrap_or_default() + .iter() + .map(|l| { + let page = l["uses"].as_array().and_then(|u| u.iter().find_map(|u| u["page"].as_str())).unwrap_or(""); + json!({"name": l["name"].as_str().unwrap_or(""), "page": page, "status": l["status"].as_str().unwrap_or("ok")}) + }) + .collect(); + // Spot inks print on their own plates unless the Ink Manager makes them process. + let inks = app.session.execute("ink.list", &json!({})).unwrap_or_default(); + let all_process = inks["allToProcess"].as_bool().unwrap_or(false); + let spot: Vec = inks["inks"] + .as_array() + .map(|a| { + a.iter() + .filter(|i| !all_process && !i["process"].as_bool().unwrap_or(false)) + .filter_map(|i| i["name"].as_str()) + .map(|n| json!(n)) + .collect() + }) + .unwrap_or_default(); + app.ui.dialog = Some(Dialog::new( + ID, + json!({"page": "summary", "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, "problemsOnly": false}), + )); +} + +fn rows(d: &Dialog, key: &str) -> Vec { + d.fields.get(key).and_then(Value::as_array).cloned().unwrap_or_default() +} + +/// How many of `rows` have one of `statuses`. +fn count(rows: &[Value], statuses: &[&str]) -> usize { + rows.iter().filter(|r| r["status"].as_str().is_some_and(|s| statuses.contains(&s))).count() +} + +/// The Summary's lines: (text, whether it warns). +fn summary_lines(lang: &str, d: &Dialog) -> Vec<(String, bool)> { + let tr = |s: &str| crate::i18n::tr(lang, s).to_string(); + let (fonts, links) = (rows(d, "fonts"), rows(d, "links")); + let missing_fonts = count(&fonts, &["missing"]); + let (modified, missing) = (count(&links, &["modified"]), count(&links, &["missing"])); + vec![ + ( + tr("Fonts: {used} Fonts Used, {missing} Missing") + .replace("{used}", &fonts.len().to_string()) + .replace("{missing}", &missing_fonts.to_string()), + missing_fonts > 0, + ), + ( + tr("Links and Images: {found} Links Found; {modified} Modified, {missing} Missing") + .replace("{found}", &links.len().to_string()) + .replace("{modified}", &modified.to_string()) + .replace("{missing}", &missing.to_string()), + modified + missing > 0, + ), + (tr("Images: {embedded} Embedded").replace("{embedded}", &count(&links, &["embedded"]).to_string()), false), + ( + tr("Colors and Inks: {process} Process Inks; {spot} Spot Inks") + .replace("{process}", &PROCESS_INKS.len().to_string()) + .replace("{spot}", &rows(d, "spot").len().to_string()), + false, + ), + ] +} + +/// The warning sign InDesign shows beside a line with problems, and the colours of statuses. +const WARN: egui::Color32 = egui::Color32::from_rgb(0xe8, 0xb3, 0x2c); +const BAD: egui::Color32 = egui::Color32::from_rgb(0xe5, 0x4b, 0x4b); + +fn status_text(lang: &str, status: &str) -> (String, Option) { + let tr = |s: &str| crate::i18n::tr(lang, s).to_string(); + match status { + "missing" => (format!("\u{26A0} {}", tr("Missing")), Some(BAD)), + "modified" => (format!("\u{25B2} {}", tr("Modified")), Some(WARN)), + "substituted" => (tr("Substituted"), Some(WARN)), + "embedded" => (tr("Embedded"), None), + _ => (tr("OK"), None), + } +} + +/// A table: a header row and `cells` rows (each with an optional colour for its last cell). +fn table(ui: &mut egui::Ui, id: &str, header: &[String], cells: &[(Vec, Option)]) { + let t = crate::theme::Tokens::get(ui.ctx()); + egui::Frame::NONE.stroke(egui::Stroke::new(1.0, t.field_border)).inner_margin(6.0).show(ui, |ui| { + egui::ScrollArea::vertical().id_salt(id).max_height(220.0).auto_shrink([false, true]).show(ui, |ui| { + egui::Grid::new(id).num_columns(header.len()).spacing([18.0, 4.0]).striped(true).show(ui, |ui| { + for h in header { + crate::rtl::label(ui, egui::RichText::new(h).font(semibold(11.5))); + } + ui.end_row(); + for (row, color) in cells { + for (i, c) in row.iter().enumerate() { + let mut text = egui::RichText::new(c); + if i + 1 == row.len() + && let Some(color) = color + { + text = text.color(*color); + } + crate::rtl::label(ui, text); + } + ui.end_row(); + } + }); + }); + }); +} + +/// The Package dialog: the pages on the left, the page on the right. +pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { + let lang = app.ui.language.clone(); + let tr = |s: &str| crate::i18n::tr(&lang, s).to_string(); + let page = d.fields.get("page").and_then(Value::as_str).unwrap_or("summary").to_string(); + ui.horizontal_top(|ui| { + // A fixed height, as Preferences': the divider between the pages and the page fills it, + // and the dialog keeps its size from page to page. + ui.set_min_height(320.0); + ui.set_max_height(320.0); + ui.vertical(|ui| { + ui.set_width(150.0); + for (key, label) in PAGES { + if ui.selectable_label(page == key, crate::rtl::widget(ui, tr(label))).clicked() { + d.fields.insert("page".into(), json!(key)); + } + } + }); + ui.separator(); + ui.vertical(|ui| { + ui.set_min_width(460.0); + let title = PAGES.iter().find(|p| p.0 == page).map_or("Summary", |p| p.1); + crate::rtl::label(ui, egui::RichText::new(tr(title)).font(semibold(13.0))); + ui.add_space(6.0); + let problems_only = d.b("problemsOnly"); + match page.as_str() { + "fonts" => { + let fonts = rows(d, "fonts"); + let cells: Vec<_> = fonts + .iter() + .filter(|f| !problems_only || f["status"] != "ok") + .map(|f| { + let (status, color) = status_text(&lang, f["status"].as_str().unwrap_or("ok")); + let source = match f["source"].as_str().unwrap_or("") { + "bundled" => tr("Bundled"), + "installed" => tr("Installed"), + "document" => tr("Document"), + "added" => tr("Added"), + _ => String::new(), + }; + (vec![f["name"].as_str().unwrap_or("").to_string(), source, status], color) + }) + .collect(); + table(ui, "package_fonts", &[tr("Name"), tr("Source"), tr("Status")], &cells); + } + "links" => { + let links = rows(d, "links"); + let cells: Vec<_> = links + .iter() + .filter(|l| !problems_only || matches!(l["status"].as_str(), Some("missing" | "modified"))) + .map(|l| { + let (status, color) = status_text(&lang, l["status"].as_str().unwrap_or("ok")); + (vec![l["name"].as_str().unwrap_or("").to_string(), l["page"].as_str().unwrap_or("").to_string(), status], color) + }) + .collect(); + table(ui, "package_links", &[tr("Name"), tr("Page"), tr("Status")], &cells); + } + "inks" => { + let mut cells: Vec<_> = PROCESS_INKS.iter().map(|n| (vec![tr(n), tr("Process")], None)).collect(); + cells.extend(rows(d, "spot").iter().filter_map(Value::as_str).map(|n| (vec![n.to_string(), tr("Spot")], None))); + table(ui, "package_inks", &[tr("Name"), tr("Type")], &cells); + } + _ => { + ui.horizontal(|ui| { + crate::rtl::label(ui, tr("Scope:")); + crate::rtl::label(ui, tr("Entire Publication")); + }); + ui.add_space(6.0); + for (line, warn) in summary_lines(&lang, d) { + ui.horizontal(|ui| { + let sign = egui::RichText::new(if warn { "\u{26A0}" } else { " " }).color(WARN); + ui.add_sized([14.0, 16.0], egui::Label::new(sign)); + crate::rtl::label(ui, line); + }); + } + ui.add_space(10.0); + for (key, label) in [("instructions", "Create Printing Instructions"), ("idml", "Include IDML"), ("pdf", "Include PDF (Print)")] { + let mut on = d.b(key); + if ui.checkbox(&mut on, crate::rtl::widget(ui, tr(label))).changed() { + d.fields.insert(key.into(), json!(on)); + } + } + } + } + if matches!(page.as_str(), "fonts" | "links") { + ui.add_space(4.0); + let mut on = problems_only; + if ui.checkbox(&mut on, crate::rtl::widget(ui, tr("Show Problems Only"))).changed() { + d.fields.insert("problemsOnly".into(), json!(on)); + } + } + }); + }); +} + +/// Package…: the Printing Instructions first when asked for, else the folder. +pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result { + let (idml, pdf) = (d.b("idml"), d.b("pdf")); + if d.b("instructions") { + app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf}))); + return Ok(Value::Null); + } + package_to_folder(app, idml, pdf, None) +} + +/// The Printing Instructions dialog, as InDesign's: who to contact and what to do. +pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { + let lang = app.ui.language.clone(); + egui::Grid::new("printing_instructions").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + for (key, label, lines) in INSTRUCTION_FIELDS { + crate::rtl::label(ui, crate::i18n::tr(&lang, label)); + let mut text = d.s(key); + let edit = if lines > 1 { egui::TextEdit::multiline(&mut text).desired_rows(lines) } else { egui::TextEdit::singleline(&mut text) }; + if ui.add(edit.desired_width(320.0)).changed() { + d.fields.insert(key.into(), json!(text)); + } + ui.end_row(); + } + }); +} + +/// The instructions as they go into the package's Instructions.txt: the contact lines, then the +/// instructions. Empty fields are left out. +fn instructions_text(d: &Dialog) -> String { + let mut out = String::new(); + for (key, label) in + [("contact", "Contact"), ("company", "Company"), ("address", "Address"), ("phone", "Phone"), ("fax", "Fax"), ("email", "Email")] + { + let v = d.s(key); + let v = v.trim(); + if !v.is_empty() { + out += &format!("{label}: {}\n", v.replace('\n', "\n ")); + } + } + let ins = d.s("instructions"); + if !ins.trim().is_empty() { + if !out.is_empty() { + out.push('\n'); + } + out += ins.trim(); + out.push('\n'); + } + out +} + +/// Continue: the folder, with the instructions. +pub fn confirm_instructions(app: &mut DesignApp, d: &Dialog) -> Result { + let text = instructions_text(d); + package_to_folder(app, d.b("idml"), d.b("pdf"), Some(text).filter(|t| !t.is_empty())) +} + +/// Ask for the folder to create (" Folder") and package the document into it. +pub fn package_to_folder(app: &mut DesignApp, idml: bool, pdf: bool, instructions: Option<String>) -> Result<Value, String> { + let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default(); + let Some(dir) = app.services.pick_save.as_mut().and_then(|f| f(&name)) else { return Ok(Value::Null) }; + let mut p = json!({"dir": dir, "idml": idml, "pdf": pdf}); + if let Some(t) = instructions { + p["instructions"] = json!(t); + } + let r = app.run("file.package", p); + if let Ok(v) = &r { + app.status( + crate::i18n::tr(&app.ui.language, "Packaged {count} files into {dir}") + .replace("{count}", &v["files"].as_array().map_or(0, |f| f.len()).to_string()) + .replace("{dir}", v["dir"].as_str().unwrap_or("")), + ); + } + r +} + +#[cfg(test)] +mod tests { + use egui_kittest::{Harness, kittest::Queryable}; + + use super::*; + + /// A document with a text frame and an image whose file has gone, in a temporary folder. + fn app_with_missing_link(dir: &std::path::Path) -> DesignApp { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"title": "Packed"})).unwrap(); + app.run("frame.create", json!({"rect": [36, 36, 300, 200], "content": "text", "text": "Hello"})).unwrap(); + let png = designcraft_render::Rendered { width: 4, height: 3, pixels: [30, 90, 180, 255].repeat(12) }.to_png(); + let img = dir.join("gone.png"); + std::fs::write(&img, &png).unwrap(); + app.run("file.place", json!({"path": img.to_string_lossy(), "x": 40, "y": 260, "width": 40})).unwrap(); + std::fs::remove_file(&img).unwrap(); + app + } + + fn harness(app: DesignApp) -> Harness<'static, DesignApp> { + Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let key = egui::Id::new("package_test_fonts"); + if ui.data(|d| d.get_temp::<bool>(key)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(key, true)); + return; + } + crate::dialogs::show(app, ui.ctx()); + }, + app, + ) + } + + fn temp_dir(tag: &str) -> std::path::PathBuf { + 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-dialog-{tag}-{}-{nonce}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + /// The course's chapter 27: File › Package opens InDesign's Package dialog. Summary counts the + /// fonts, links (one missing, with a warning) and inks; Links and Images lists the link, and + /// Show Problems Only keeps it; Create Printing Instructions leads to Printing Instructions, + /// whose Continue asks for the folder and writes the package with the instructions. + #[test] + fn package_shows_a_summary_then_the_printing_instructions() { + let dir = temp_dir("summary"); + let mut app = app_with_missing_link(&dir); + let out = dir.join("Packed Folder"); + let picked = out.to_string_lossy().to_string(); + app.services.pick_save = Some(Box::new(move |_| Some(picked.clone()))); + crate::menus::activate(&mut app, "app.packageDialog", &Value::Null); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some(ID)); + let mut h = harness(app); + h.run_steps(3); + assert!(h.query_by_label("Scope:").is_some()); + assert!(h.query_by_label("Links and Images: 1 Links Found; 0 Modified, 1 Missing").is_some()); + assert!(h.query_by_label("Colors and Inks: 4 Process Inks; 0 Spot Inks").is_some()); + let fonts = h.state().ui.dialog.as_ref().unwrap().fields["fonts"].as_array().unwrap().len(); + assert!(h.query_by_label(&format!("Fonts: {fonts} Fonts Used, 0 Missing")).is_some()); + // Links and Images: the link, missing; Show Problems Only keeps it. + h.get_by_label("Links and Images").click(); + h.run_steps(2); + assert!(h.query_by_label("gone.png").is_some()); + assert!(h.query_by_label("\u{26A0} Missing").is_some()); + h.get_by_label("Show Problems Only").click(); + h.run_steps(2); + assert!(h.query_by_label("gone.png").is_some()); + // Back on Summary: Create Printing Instructions, then Package…. + h.get_by_label("Summary").click(); + h.run_steps(2); + h.get_by_label("Create Printing Instructions").click(); + h.run_steps(2); + h.get_by_label("Package…").click(); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.id.as_str()), Some(INSTRUCTIONS)); + for (key, value) in [("contact", "Dave"), ("email", "dave@example.com"), ("instructions", "Call me if anything is wrong.")] { + h.state_mut().ui.dialog.as_mut().unwrap().fields.insert(key.into(), json!(value)); + } + h.run_steps(2); + h.get_by_label("Continue").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap(); + assert!( + report.contains("Contact: Dave") && report.contains("Email: dave@example.com") && report.contains("Call me if anything is wrong."), + "{report}" + ); + assert!(out.join("Packed.idml").exists(), "Include IDML is on by default"); + assert!(!out.join("Packed.pdf").exists(), "Include PDF is off by default"); + 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. + #[test] + fn package_options_choose_the_idml_and_pdf() { + let dir = temp_dir("options"); + let mut app = app_with_missing_link(&dir); + let out = dir.join("Packed Folder"); + let picked = out.to_string_lossy().to_string(); + app.services.pick_save = Some(Box::new(move |_| Some(picked.clone()))); + open(&mut app); + let mut h = harness(app); + h.run_steps(3); + h.get_by_label("Include IDML").click(); + h.run_steps(2); + h.get_by_label("Include PDF (Print)").click(); + h.run_steps(2); + h.get_by_label("Package…").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert!(out.join("Packed.pdf").exists() && !out.join("Packed.idml").exists()); + std::fs::remove_dir_all(dir).unwrap(); + } +} From 4c3f24e43d3ec8aa240456a5a252956b14aa4701 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:39:43 +0200 Subject: [PATCH 07/28] Package: the packaged PDF uses a chosen PDF preset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Package dialog's Include PDF (Print) gets a PDF Preset, as InDesign's Select PDF Preset: the Export PDF presets, the last one used first. The packaged PDF always had the default settings. `file.package` takes `pdf: {…file.exportPdf options}` besides true/false, read with the same `export::options` before anything is written, so bad options leave no partial package. The Export PDF dialog's field-to-params code is shared (`pdf_export_params`, `pdf_preset_params`). Found in the Learn Skills Daily course, chapter 27. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- ROADMAP.md | 1 + crates/engine/src/cmd/package.rs | 33 +++++++++++++++-- crates/ui-egui/src/dialogs.rs | 54 ++++++++++++++++++---------- crates/ui-egui/src/i18n.rs | 1 + crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + crates/ui-egui/src/package_dialog.rs | 49 +++++++++++++++++++++---- 13 files changed, 118 insertions(+), 28 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b0e33a381..28cb3d4e9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **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-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. diff --git a/crates/engine/src/cmd/package.rs b/crates/engine/src/cmd/package.rs index f2d33ccfa..69b3ed379 100644 --- a/crates/engine/src/cmd/package.rs +++ b/crates/engine/src/cmd/package.rs @@ -202,7 +202,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 ), @@ -389,6 +389,13 @@ 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. + 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, + }; let root = OutputRoot::new(dir.clone())?; let display_title = st.doc.title.clone(); let title = safe_component(&display_title, "Untitled"); @@ -433,9 +440,9 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> { root.write(&path, &designcraft_idml::export_idml_with(&packed, &opts))?; files.push(path); } - if p.get("pdf").and_then(Value::as_bool).unwrap_or(false) { + if let Some(opts) = 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()) + 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)?; files.push(path); @@ -509,6 +516,26 @@ mod tests { 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")); + 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"); + std::fs::remove_dir_all(dir).unwrap(); + } + /// The package's IDML links its images to the files in the package's Links folder and embeds /// no copies of them. #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9182f5843..0f19550bc 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -399,6 +399,41 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { /// English name; the caption is translated. const PDF_EXPORT_PRESETS: &[&str] = &["Desktop Printing", "Commercial Printing", "Screen and Email", "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<Item = &'static str> { + 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"; "Custom" /// (and any unknown value) changes nothing. fn apply_pdf_preset(d: &mut Dialog, preset: &str) { @@ -3311,24 +3346,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> { 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.rs b/crates/ui-egui/src/i18n.rs index eccf6ca89..4eb433c18 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -155,6 +155,7 @@ 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/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 268b799d1..e1fa7080d 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("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 a51121541..2c7d8f090 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("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 0d0d49112..70e1e2e47 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("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 142f806ac..9bd18a169 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("PDF Preset:", "PDF-ის წინასწარი პარამეტრი:"), ("Packaged {count} files into {dir}", "{count} ფაილი შეიფუთა {dir}-ში"), ("Continue", "გაგრძელება"), ("Printing Instructions", "ბეჭდვის ინსტრუქციები"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index ca8c99e33..ccf7d68c1 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("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 9c4317506..7f971648a 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("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 4888924ee..b555c9c90 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("PDF Preset:", "Стиль PDF:"), ("Packaged {count} files into {dir}", "Упаковано файлов: {count} в {dir}"), ("Continue", "Продолжить"), ("Printing Instructions", "Инструкции для печати"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 88d72cb57..41e6591eb 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("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 6c00ca060..b2e5cdd75 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -81,9 +81,13 @@ pub fn open(app: &mut DesignApp) { .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"); app.ui.dialog = Some(Dialog::new( ID, - json!({"page": "summary", "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, "problemsOnly": false}), + json!({"page": "summary", "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, + "pdfPreset": preset, "problemsOnly": false}), )); } @@ -250,6 +254,24 @@ 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); + crate::rtl::label(ui, tr("PDF Preset:")); + let cur = d.s("pdfPreset"); + egui::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)); + } + } + }, + ); + }); + }); } } if matches!(page.as_str(), "fonts" | "links") { @@ -263,9 +285,14 @@ 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<Value, String> { - 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}))); return Ok(Value::Null); @@ -316,11 +343,12 @@ fn instructions_text(d: &Dialog) -> String { /// Continue: the folder, with the instructions. pub fn confirm_instructions(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> { let text = instructions_text(d); - package_to_folder(app, d.b("idml"), d.b("pdf"), Some(text).filter(|t| !t.is_empty())) + package_to_folder(app, d.b("idml"), d.fields.get("pdf").cloned().unwrap_or(json!(false)), Some(text).filter(|t| !t.is_empty())) } -/// Ask for the folder to create ("<title> Folder") and package the document into it. -pub fn package_to_folder(app: &mut DesignApp, idml: bool, pdf: bool, instructions: Option<String>) -> Result<Value, String> { +/// Ask for the folder to create ("<title> Folder") and package the document into it. `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>) -> Result<Value, String> { let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default(); let Some(dir) = app.services.pick_save.as_mut().and_then(|f| f(&name)) else { return Ok(Value::Null) }; let mut p = json!({"dir": dir, "idml": idml, "pdf": pdf}); @@ -432,8 +460,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"); @@ -448,10 +476,17 @@ 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); 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(); } } From c04fa61d17ef932a95817db628a3f2d51ff38e88 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 22 Oct 2026 00:53:21 +0200 Subject: [PATCH 08/28] Package: a failed package keeps the dialog open; the ink table's Type column; no document is an error - When `file.package` fails, the Package or Printing Instructions dialog comes back with its options and instructions, and the status line says why. Before, they were lost. - The Colors and Inks table heads its kind column with the "Type:" key (without its colon). The "Type" key is the menu's and reads Text in most languages. - `app.packageDialog` with no document open is an error rather than a silent success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/dialogs.rs | 18 +++++++++++++-- crates/ui-egui/src/menus.rs | 5 +--- crates/ui-egui/src/package_dialog.rs | 34 ++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9182f5843..41b1d0504 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3581,8 +3581,22 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> { } "alert" => Ok(Value::Null), crate::effects_dialog::ID => crate::effects_dialog::confirm(app, &d), - crate::package_dialog::ID => crate::package_dialog::confirm(app, &d), - crate::package_dialog::INSTRUCTIONS => crate::package_dialog::confirm_instructions(app, &d), + crate::package_dialog::ID | crate::package_dialog::INSTRUCTIONS => { + let r = if d.id == crate::package_dialog::ID { + crate::package_dialog::confirm(app, &d) + } else { + crate::package_dialog::confirm_instructions(app, &d) + }; + // Packaging failed: the dialog stays open with its options and instructions, and the + // status line says why. + if let Err(e) = &r + && app.ui.dialog.is_none() + { + app.status(e.clone()); + app.ui.dialog = Some(d); + } + r + } "findChange" => { let result = if d.b("objectMode") { Ok(Value::Null) } else { app.run("find.change", find_change_params(&d)) }; // Confirmation consumes retained bounds before closing, even on an error. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 794e58f2d..923ff2a86 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1149,10 +1149,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value Ok(Value::Null) } "app.exportText" => export_bytes(app, p, "rtf", "file.exportText"), - "app.packageDialog" => { - crate::package_dialog::open(app); - Ok(Value::Null) - } + "app.packageDialog" => crate::package_dialog::open(app).map(|()| Value::Null), "app.language" => { let Some(v) = p.get("lang").and_then(Value::as_str) else { return Some(app.run("app.preferences", json!({}))) }; if !crate::i18n::LANGUAGES.iter().any(|(c, _)| *c == v) { diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 6c00ca060..741de6b44 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -33,9 +33,9 @@ const INSTRUCTION_FIELDS: [(&str, &str, usize); 7] = [ ]; /// Open the Package dialog on its Summary, with the document's fonts, links and spot inks. -pub fn open(app: &mut DesignApp) { +pub fn open(app: &mut DesignApp) -> Result<(), String> { if app.session.active().is_none() { - return; + return Err("no document open".into()); } let fonts: Vec<Value> = app .session @@ -85,6 +85,7 @@ pub fn open(app: &mut DesignApp) { ID, json!({"page": "summary", "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false, "problemsOnly": false}), )); + Ok(()) } fn rows(d: &Dialog, key: &str) -> Vec<Value> { @@ -228,7 +229,9 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { "inks" => { let mut cells: Vec<_> = PROCESS_INKS.iter().map(|n| (vec![tr(n), tr("Process")], None)).collect(); cells.extend(rows(d, "spot").iter().filter_map(Value::as_str).map(|n| (vec![n.to_string(), tr("Spot")], None))); - table(ui, "package_inks", &[tr("Name"), tr("Type")], &cells); + // The ink's kind: the "Type:" key (the "Type" key is the menu's, which reads Text). + let kind = tr("Type:").trim_end_matches([':', ':']).trim_end().to_string(); + table(ui, "package_inks", &[tr("Name"), kind], &cells); } _ => { ui.horizontal(|ui| { @@ -441,7 +444,7 @@ mod tests { let out = dir.join("Packed Folder"); let picked = out.to_string_lossy().to_string(); app.services.pick_save = Some(Box::new(move |_| Some(picked.clone()))); - open(&mut app); + open(&mut app).unwrap(); let mut h = harness(app); h.run_steps(3); h.get_by_label("Include IDML").click(); @@ -454,4 +457,27 @@ mod tests { assert!(out.join("Packed.pdf").exists() && !out.join("Packed.idml").exists()); std::fs::remove_dir_all(dir).unwrap(); } + + /// Packaging that fails (here, into a folder under a file) keeps the dialog open with its + /// options, and the status line says why. Without a document there's nothing to open. + #[test] + fn a_failed_package_keeps_the_dialog_open() { + let dir = temp_dir("fails"); + let mut app = app_with_missing_link(&dir); + let blocker = dir.join("blocker"); + std::fs::write(&blocker, b"a file, not a folder").unwrap(); + let picked = blocker.join("Packed Folder").to_string_lossy().to_string(); + app.services.pick_save = Some(Box::new(move |_| Some(picked.clone()))); + open(&mut app).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("pdf".into(), json!(true)); + assert!(crate::dialogs::confirm(&mut app).is_err()); + let d = app.ui.dialog.as_ref().expect("the dialog stays open"); + assert_eq!((d.id.as_str(), d.b("pdf")), (ID, true), "with its options"); + assert!(!app.ui.status.is_empty()); + app.run("file.close", json!({})).unwrap(); + app.ui.dialog = None; + assert!(app.run("app.packageDialog", json!({})).is_err()); + assert!(app.ui.dialog.is_none()); + std::fs::remove_dir_all(dir).unwrap(); + } } From 63d3f5a19d1488e6ea9404c1f60d9d2f1bc57ff0 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 22 Oct 2026 00:59:00 +0200 Subject: [PATCH 09/28] 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> --- crates/engine/src/cmd/export.rs | 42 +++++++++++++++++---------- crates/engine/src/cmd/package.rs | 50 +++++++++++++++++++++++++------- crates/ui-egui/src/i18n/ka.rs | 4 +-- 3 files changed, 68 insertions(+), 28 deletions(-) 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<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"))] 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<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, ¶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", From 9ab1122688ad014c0e53bd27df5d8bc43555069e Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 22 Oct 2026 01:28:37 +0200 Subject: [PATCH 10/28] Package: check the PDF's view options before writing anything `export::check_pdf_options` refuses what `pdf_report` would: the export options, the flattener, and now `pageLayout` and `view`, which `pdf_report` reads up front too. `file.package` checks with it before the output folder is made, so an unknown page layout or view leaves no partial package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/engine/src/cmd/export.rs | 73 ++++++++++++++++++++------------ crates/engine/src/cmd/package.rs | 16 ++++--- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index ad90d312e..e20ebaae2 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -722,7 +722,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> { /// 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>> { +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), @@ -735,11 +735,49 @@ pub(super) fn flatten_ppi(p: &Value) -> Result<Option<f64>> { }) } +/// 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 => {} + } + 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(super) fn check_pdf_options(p: &Value, pages: usize) -> Result<()> { + options(p, pages)?; + flatten_ppi(p)?; + view_options(p)?; + Ok(()) +} + /// 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 (mut cat, dest) = view_options(p)?; 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(doc, |f| &f.0); @@ -760,33 +798,12 @@ pub(super) fn pdf_report(doc: &designcraft_doc::Document, cache: &designcraft_co None => r.warnings.push("the transparency blend space couldn't be set".into()), } } - // 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}]")); - } + // Interactive PDF view options; the first page opens with the view (pages are objects in + // order, so refer by index 0). + 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) { diff --git a/crates/engine/src/cmd/package.rs b/crates/engine/src/cmd/package.rs index 107b859ae..8c72411f9 100644 --- a/crates/engine/src/cmd/package.rs +++ b/crates/engine/src/cmd/package.rs @@ -395,8 +395,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> { 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)?; + 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}"))), @@ -534,9 +533,16 @@ 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")); - // 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)] { + // 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"); From a645127ce991773037b30db62affa1b6b7417b67 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Sat, 24 Oct 2026 13:03:00 +0200 Subject: [PATCH 11/28] i18n: keys main added to fr.rs are in the shared table here: keep one The catalogue test refuses a key in both fr.rs and the shared table (CI). The shared row keeps the French; it matches, or takes fr.rs's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/i18n/fr.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 681230d84..adf8898d7 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -105,7 +105,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Em Dash", "Tiret cadratin"), ("Em Space", "Espace cadratin"), ("Email Address:", "Adresse e-mail :"), - ("Email:", "E-mail :"), ("En Dash", "Tiret demi-cadratin"), ("En Space", "Espace demi-cadratin"), ( @@ -254,7 +253,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("PDF import cancelled: the originating document has closed", "Importation PDF annulée : le document d'origine a été fermé"), ("PRs", "PR"), ("Package", "Assemblage"), - ("Packaged {count} files into {dir}", "{count} fichiers assemblés dans {dir}"), ("Page Break", "Saut de page"), ("Page Options", "Options de page"), ("Page Tool", "Outil Page"), @@ -265,7 +263,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Parent Options…", "Options de gabarit…"), ("Pen Tool", "Outil Plume"), ("Pencil Tool", "Outil Crayon"), - ("Phone:", "Téléphone :"), ("Picas", "Picas"), ("Pin Bar Position", "Épingler la position de la barre"), ("Place with Import Options", "Importer avec les options d'importation"), From 4dbc9674fadc76dbeca858ad99bf7a813b69dc77 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Sat, 24 Oct 2026 13:04:38 +0200 Subject: [PATCH 12/28] i18n: keys main added to fr.rs are in the shared table here: keep one The catalogue test refuses a key in both fr.rs and the shared table (CI). The shared row keeps the French; it matches, or takes fr.rs's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/i18n/fr.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 681230d84..adf8898d7 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -105,7 +105,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Em Dash", "Tiret cadratin"), ("Em Space", "Espace cadratin"), ("Email Address:", "Adresse e-mail :"), - ("Email:", "E-mail :"), ("En Dash", "Tiret demi-cadratin"), ("En Space", "Espace demi-cadratin"), ( @@ -254,7 +253,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("PDF import cancelled: the originating document has closed", "Importation PDF annulée : le document d'origine a été fermé"), ("PRs", "PR"), ("Package", "Assemblage"), - ("Packaged {count} files into {dir}", "{count} fichiers assemblés dans {dir}"), ("Page Break", "Saut de page"), ("Page Options", "Options de page"), ("Page Tool", "Outil Page"), @@ -265,7 +263,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Parent Options…", "Options de gabarit…"), ("Pen Tool", "Outil Plume"), ("Pencil Tool", "Outil Crayon"), - ("Phone:", "Téléphone :"), ("Picas", "Picas"), ("Pin Bar Position", "Épingler la position de la barre"), ("Place with Import Options", "Importer avec les options d'importation"), From f756c9174766999031426f72c8acbd536e0a9dc8 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Sat, 24 Oct 2026 13:36:59 +0200 Subject: [PATCH 13/28] Package: missing styles count, same document, Enter in a field, instructions first - A style the font lacks counts as a missing font in Summary and Fonts, as file.package counts it (font_status). - The dialog remembers its document; Package... and Continue refuse when another document became active meanwhile, so the package is the one summarised. - Enter in Address or Instructions makes a new line: the dialog footer no longer takes it for Continue (dialogs::MULTILINE_FOCUS). - Instructions.txt starts with the printing instructions, then the report, as the dialog promises. Found by CodeRabbit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/engine/src/cmd/package.rs | 7 +- crates/ui-egui/src/dialogs.rs | 8 ++- crates/ui-egui/src/package_dialog.rs | 98 +++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/crates/engine/src/cmd/package.rs b/crates/engine/src/cmd/package.rs index d0ed58b03..de3846114 100644 --- a/crates/engine/src/cmd/package.rs +++ b/crates/engine/src/cmd/package.rs @@ -441,11 +441,13 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> { root.write(&path, &bytes)?; files.push(path); } - // The report: fonts (missing first), links, preflight, the user's instructions. - let mut r = format!("Package report: {display_title}\n\n"); + // The printing instructions first, as the form gave them; then the report: fonts (missing + // first), links, preflight. + let mut r = String::new(); if let Some(t) = str_param(p, "instructions").filter(|t| !t.trim().is_empty()) { r += &format!("Instructions\n{t}\n\n"); } + r += &format!("Package report: {display_title}\n\n"); r += "Fonts\n"; r += &font_lines.concat(); if !fallback_lines.is_empty() { @@ -629,6 +631,7 @@ mod tests { assert!(out.join("Links").join("logo.svg").exists()); let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap(); assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}"); + assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}"); assert!(report.contains("Preflight:"), "{report}"); // The packaged document links to its Links folder. let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap(); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index bc7d28262..e5f608972 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -137,6 +137,10 @@ fn text_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, w: f32) { } } +/// The temporary-data key a dialog sets, each frame, while one of its multi-line fields has focus: +/// Enter then makes a new line there, and the footer doesn't take it for OK. +pub(crate) const MULTILINE_FOCUS: &str = "dialog_multiline_focus"; + /// Extract once when opened. Cancel is read-only; confirmation adds exactly these colours. pub(crate) fn open_color_theme(app: &mut DesignApp, params: Value) { match app.run("color.extractTheme", params) { @@ -2850,6 +2854,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { app.ui.style_preview = d.b("preview"); style_options_preview(app, &mut d); } + // A multi-line field with focus (set by the dialog this frame) takes Enter as a new line. + let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui .add( @@ -2857,7 +2863,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { .fill(crate::theme::Tokens::get(ui.ctx()).accent_strong), ) .clicked() - || (!popup_open && ui.input(|i| i.key_pressed(egui::Key::Enter))) + || (!popup_open && !multiline && ui.input(|i| i.key_pressed(egui::Key::Enter))) { result = Some(true); } diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index cf42af11d..e064fd1f4 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -45,13 +45,7 @@ pub fn open(app: &mut DesignApp) -> Result<(), String> { .unwrap_or_default() .iter() .map(|f| { - let status = if f["missing"].as_bool().unwrap_or(false) { - "missing" - } else if f["styleMissing"].as_bool().unwrap_or(false) || f["matchStatus"] == "styleSubstitute" { - "substituted" - } else { - "ok" - }; + let status = font_status(f); let name = format!("{} {}", f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or("")).trim().to_string(); json!({"name": name, "source": f["source"].as_str().unwrap_or(""), "status": status}) }) @@ -81,13 +75,35 @@ pub fn open(app: &mut DesignApp) -> Result<(), String> { .collect() }) .unwrap_or_default(); + let doc = app.session.active().map(|d| d.uid); app.ui.dialog = Some(Dialog::new( ID, - json!({"page": "summary", "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, "problemsOnly": false}), )); Ok(()) } +/// A `font.list` entry's status here: a missing font, or a style its family lacks, is missing +/// (as `file.package` counts them); a style it stands in for is substituted. +fn font_status(f: &Value) -> &'static str { + if f["missing"].as_bool().unwrap_or(false) || f["styleMissing"].as_bool().unwrap_or(false) { + "missing" + } else if f["matchStatus"] == "styleSubstitute" { + "substituted" + } else { + "ok" + } +} + +/// The document the dialog was opened on is still the active one (automation can switch +/// documents while it is open, and the package must be of the one summarised). +fn same_document(app: &DesignApp, d: &Dialog) -> Result<(), String> { + match app.session.active() { + Some(doc) if d.fields.get("doc").and_then(Value::as_u64) == Some(doc.uid) => Ok(()), + _ => Err("the active document changed after the Package dialog opened".into()), + } +} + fn rows(d: &Dialog, key: &str) -> Vec<Value> { d.fields.get(key).and_then(Value::as_array).cloned().unwrap_or_default() } @@ -268,9 +284,10 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { /// Package…: the Printing Instructions first when asked for, else the folder. pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> { + same_document(app, d)?; let (idml, pdf) = (d.b("idml"), d.b("pdf")); if d.b("instructions") { - app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf}))); + app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf, "doc": d.fields.get("doc")}))); return Ok(Value::Null); } package_to_folder(app, idml, pdf, None) @@ -284,9 +301,14 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { crate::rtl::label(ui, crate::i18n::tr(&lang, label)); let mut text = d.s(key); let edit = if lines > 1 { egui::TextEdit::multiline(&mut text).desired_rows(lines) } else { egui::TextEdit::singleline(&mut text) }; - if ui.add(edit.desired_width(320.0)).changed() { + let resp = ui.add(edit.desired_width(320.0)); + if resp.changed() { d.fields.insert(key.into(), json!(text)); } + // Enter there makes a new line: the footer mustn't take it for Continue. + if lines > 1 && resp.has_focus() { + ui.ctx().data_mut(|m| m.insert_temp(egui::Id::new(crate::dialogs::MULTILINE_FOCUS), true)); + } ui.end_row(); } }); @@ -318,6 +340,7 @@ fn instructions_text(d: &Dialog) -> String { /// Continue: the folder, with the instructions. pub fn confirm_instructions(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> { + 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())) } @@ -481,4 +504,59 @@ mod tests { assert!(app.ui.dialog.is_none()); std::fs::remove_dir_all(dir).unwrap(); } + + /// A style the font's family lacks counts as missing, as `file.package` counts it. + #[test] + fn a_missing_font_style_is_a_missing_font() { + assert_eq!(font_status(&json!({"missing": true})), "missing"); + assert_eq!(font_status(&json!({"missing": false, "styleMissing": true})), "missing"); + assert_eq!(font_status(&json!({"missing": false, "matchStatus": "styleSubstitute"})), "substituted"); + assert_eq!(font_status(&json!({"missing": false})), "ok"); + } + + /// The package is of the document the dialog summarised: when another document became active + /// meanwhile, Package… and Continue refuse, and nothing is written. + #[test] + fn package_refuses_when_the_active_document_changed() { + let dir = temp_dir("switched"); + let mut app = app_with_missing_link(&dir); + let out = dir.join("Packed Folder"); + let picked = out.to_string_lossy().to_string(); + app.services.pick_save = Some(Box::new(move |_| Some(picked.clone()))); + open(&mut app).unwrap(); + let d = app.ui.dialog.take().unwrap(); + app.run("file.new", json!({"title": "Other"})).unwrap(); + assert!(confirm(&mut app, &d).is_err()); + let ins = Dialog::new(INSTRUCTIONS, json!({"idml": true, "pdf": false, "doc": d.fields["doc"]})); + assert!(confirm_instructions(&mut app, &ins).is_err()); + assert!(!out.exists(), "nothing packaged"); + std::fs::remove_dir_all(dir).unwrap(); + } + + /// Enter in Address or Instructions makes a new line there; in a one-line field it continues. + #[test] + fn enter_in_a_multiline_instruction_field_is_a_new_line() { + let dir = temp_dir("enter"); + let mut app = app_with_missing_link(&dir); + let doc = app.session.active().unwrap().uid; + app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": true, "pdf": false, "doc": doc}))); + let mut h = harness(app); + h.run_steps(3); + let field = |h: &Harness<'static, DesignApp>, n: usize| { + h.query_all_by(|x| x.role() == egui::accesskit::Role::TextInput || x.role() == egui::accesskit::Role::MultilineTextInput) + .nth(n) + .unwrap() + .rect() + .center() + }; + // Address is the third field. + let address = field(&h, 2); + h.event(egui::Event::PointerButton { pos: address, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE }); + h.event(egui::Event::PointerButton { pos: address, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE }); + h.run_steps(2); + h.key_press(egui::Key::Enter); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.id.as_str()), Some(INSTRUCTIONS), "still open"); + std::fs::remove_dir_all(dir).unwrap(); + } } From 83aa235cbc85a927e0f0c14ad79ace9a3f0a9053 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 15:30:31 +0100 Subject: [PATCH 14/28] TOC import: the listed style reference shared from main's split import styles module Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/idml/src/import/styles.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/idml/src/import/styles.rs b/crates/idml/src/import/styles.rs index a05e7d50b..13b140733 100644 --- a/crates/idml/src/import/styles.rs +++ b/crates/idml/src/import/styles.rs @@ -225,7 +225,7 @@ impl<'r> Importer<'r> { /// A paragraph style reference written as an element's text: as written when it names a /// style, otherwise without surrounding whitespace (see [`Self::listed_char_style_ref`]). - fn listed_para_style_ref(&self, r: &str) -> String { + pub(super) fn listed_para_style_ref(&self, r: &str) -> String { if self.para_names.contains_key(r) { self.para_style_ref(r) } else { self.para_style_ref(r.trim()) } } /// A character style reference written as an element's text (nested, line and GREP styles). From c6703e81f5769ac5dd1e05a6f8fe5c39a70e6ab8 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:25:00 +0100 Subject: [PATCH 15/28] Georgian: main's rows where the branch added the same keys again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/i18n/ka.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 8c44a18d7..46dc638d9 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -32,7 +32,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), - ("Packaged {count} files into {dir}", "{count} ფაილი შეიფუთა {dir}-ში"), ("Continue", "გაგრძელება"), ("Printing Instructions", "ბეჭდვის ინსტრუქციები"), ("Show Problems Only", "მხოლოდ პრობლემების ჩვენება"), @@ -41,8 +40,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Create Printing Instructions", "ბეჭდვის ინსტრუქციების შექმნა"), ("Entire Publication", "მთელი პუბლიკაცია"), ("Scope:", "არეალი:"), - ("Spot", "ლაქა"), - ("Process", "პროცესი"), ("Source", "წყარო"), ("Installed", "დაყენებული"), ("Bundled", "ჩაშენებული"), @@ -57,9 +54,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ), ("Fonts: {used} Fonts Used, {missing} Missing", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"), ("Instructions:", "ინსტრუქციები:"), - ("Email:", "ელფოსტა:"), ("Fax:", "ფაქსი:"), - ("Phone:", "ტელეფონი:"), ("Address:", "მისამართი:"), ("Company:", "კომპანია:"), ("Contact:", "საკონტაქტო პირი:"), From c8bc257dcd0b9baa4c931f595e645d3c10a4df5c Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:25:01 +0100 Subject: [PATCH 16/28] Georgian: main's rows where the branch added the same keys again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/i18n/ka.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 48faae5a2..6a3c28d9e 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -33,7 +33,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "კოპირება"), ("Paste", "ჩასმა"), ("PDF Preset:", "PDF-ის წინასწარი პარამეტრი:"), - ("Packaged {count} files into {dir}", "{count} ფაილი შეიფუთა {dir}-ში"), ("Continue", "გაგრძელება"), ("Printing Instructions", "ბეჭდვის ინსტრუქციები"), ("Show Problems Only", "მხოლოდ პრობლემების ჩვენება"), @@ -42,8 +41,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Create Printing Instructions", "ბეჭდვის ინსტრუქციების შექმნა"), ("Entire Publication", "მთელი პუბლიკაცია"), ("Scope:", "არეალი:"), - ("Spot", "სპოტ-საღებავი"), - ("Process", "პროცესი"), ("Source", "წყარო"), ("Installed", "დაყენებული"), ("Bundled", "ჩაშენებული"), @@ -58,9 +55,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ), ("Fonts: {used} Fonts Used, {missing} Missing", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"), ("Instructions:", "ინსტრუქციები:"), - ("Email:", "ელფოსტა:"), ("Fax:", "ფაქსი:"), - ("Phone:", "ტელეფონი:"), ("Address:", "მისამართი:"), ("Company:", "კომპანია:"), ("Contact:", "საკონტაქტო პირი:"), From 24b923dfc0de855009fa08ced9a4c85e2193a566 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:43:09 +0100 Subject: [PATCH 17/28] rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/engine/src/cmd/package/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/package/tests.rs b/crates/engine/src/cmd/package/tests.rs index 3440c68f6..fe559bfb8 100644 --- a/crates/engine/src/cmd/package/tests.rs +++ b/crates/engine/src/cmd/package/tests.rs @@ -169,7 +169,7 @@ fn package_writes_document_links_and_report() { assert!(out.join("Links").join("logo.svg").exists()); let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap(); assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}"); - assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}"); + assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}"); assert!(report.contains("Preflight:"), "{report}"); // The packaged document links to its Links folder. let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap(); From 94209bec7e77f6fdcab3e2511c1617aa0e99a7ed Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:51:14 +0100 Subject: [PATCH 18/28] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/dialogs.rs | 2 +- crates/ui-egui/src/package_dialog.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 72ea2913e..1e1972ed6 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3040,7 +3040,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { style_options_preview(app, &mut d); } // A multi-line field with focus (set by the dialog this frame) takes Enter as a new line. - let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false); + let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::unique(MULTILINE_FOCUS))).unwrap_or(false); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui .add( diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 423870451..35b041d1f 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -308,7 +308,7 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } // Enter there makes a new line: the footer mustn't take it for Continue. if lines > 1 && resp.has_focus() { - ui.ctx().data_mut(|m| m.insert_temp(egui::Id::new(crate::dialogs::MULTILINE_FOCUS), true)); + ui.ctx().data_mut(|m| m.insert_temp(egui::Id::unique(crate::dialogs::MULTILINE_FOCUS), true)); } ui.end_row(); } @@ -392,7 +392,7 @@ mod tests { fn harness(app: DesignApp) -> Harness<'static, DesignApp> { Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( |ui, app: &mut DesignApp| { - let key = egui::Id::new("package_test_fonts"); + let key = egui::Id::unique("package_test_fonts"); if ui.data(|d| d.get_temp::<bool>(key)).is_none() { crate::theme::install_fonts(ui.ctx(), ""); ui.data_mut(|d| d.insert_temp(key, true)); From e8b27c91f116515ff21e1ba03d6e88b6ff357e14 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:55:15 +0100 Subject: [PATCH 19/28] rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/engine/src/cmd/package/tests.rs | 100 ++++++++++++------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/crates/engine/src/cmd/package/tests.rs b/crates/engine/src/cmd/package/tests.rs index 124ea19c4..4d6513425 100644 --- a/crates/engine/src/cmd/package/tests.rs +++ b/crates/engine/src/cmd/package/tests.rs @@ -25,57 +25,57 @@ 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(); +/// `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 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. @@ -221,7 +221,7 @@ fn package_writes_document_links_and_report() { assert!(out.join("Links").join("logo.svg").exists()); let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap(); assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}"); - assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}"); + assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}"); assert!(report.contains("Preflight:"), "{report}"); // The packaged document links to its Links folder. let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap(); From 2a01caf0de75908da9830c0da8bf475db7ba2b13 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 22:59:47 +0100 Subject: [PATCH 20/28] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/dialogs.rs | 2 +- crates/ui-egui/src/package_dialog.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d8d3f7f5a..175dcd5f4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3075,7 +3075,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { style_options_preview(app, &mut d); } // A multi-line field with focus (set by the dialog this frame) takes Enter as a new line. - let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false); + let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::unique(MULTILINE_FOCUS))).unwrap_or(false); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui .add( diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 9583d84fe..eada9a244 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -335,7 +335,7 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } // Enter there makes a new line: the footer mustn't take it for Continue. if lines > 1 && resp.has_focus() { - ui.ctx().data_mut(|m| m.insert_temp(egui::Id::new(crate::dialogs::MULTILINE_FOCUS), true)); + ui.ctx().data_mut(|m| m.insert_temp(egui::Id::unique(crate::dialogs::MULTILINE_FOCUS), true)); } ui.end_row(); } @@ -420,7 +420,7 @@ mod tests { fn harness(app: DesignApp) -> Harness<'static, DesignApp> { Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( |ui, app: &mut DesignApp| { - let key = egui::Id::new("package_test_fonts"); + let key = egui::Id::unique("package_test_fonts"); if ui.data(|d| d.get_temp::<bool>(key)).is_none() { crate::theme::install_fonts(ui.ctx(), ""); ui.data_mut(|d| d.insert_temp(key, true)); From 50647c57204824200b0f71f24ee57de2c071ffa7 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 23:26:17 +0100 Subject: [PATCH 21/28] Printing instructions: each field named by its label, for screen readers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/package_dialog.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 35b041d1f..1919e6d9c 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -299,10 +299,11 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let lang = app.ui.language.clone(); egui::Grid::new("printing_instructions").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { for (key, label, lines) in INSTRUCTION_FIELDS { - crate::rtl::label(ui, crate::i18n::tr(&lang, label)); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, label)); let mut text = d.s(key); let edit = if lines > 1 { egui::TextEdit::multiline(&mut text).desired_rows(lines) } else { egui::TextEdit::singleline(&mut text) }; - let resp = ui.add(edit.desired_width(320.0)); + // Named by its label, for screen readers. + let resp = ui.add(edit.desired_width(320.0)).labelled_by(name.id); if resp.changed() { d.fields.insert(key.into(), json!(text)); } From 261f698aa26141d6a03a765e025789ecceb1afe5 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Thu, 29 Oct 2026 23:26:19 +0100 Subject: [PATCH 22/28] Printing instructions: each field named by its label, for screen readers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/package_dialog.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index eada9a244..2225e3d08 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -326,10 +326,11 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let lang = app.ui.language.clone(); egui::Grid::new("printing_instructions").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { for (key, label, lines) in INSTRUCTION_FIELDS { - crate::rtl::label(ui, crate::i18n::tr(&lang, label)); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, label)); let mut text = d.s(key); let edit = if lines > 1 { egui::TextEdit::multiline(&mut text).desired_rows(lines) } else { egui::TextEdit::singleline(&mut text) }; - let resp = ui.add(edit.desired_width(320.0)); + // Named by its label, for screen readers. + let resp = ui.add(edit.desired_width(320.0)).labelled_by(name.id); if resp.changed() { d.fields.insert(key.into(), json!(text)); } From cd612b1836a79153f5b6de432092ffbe924cee37 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Fri, 30 Oct 2026 00:04:27 +0100 Subject: [PATCH 23/28] Package: the PDF Preset dropdown named by its label, for screen readers; craft_ui's dropdown Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/package_dialog.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 2225e3d08..536c5e4ac 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -278,9 +278,9 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.add_enabled_ui(d.b("pdf"), |ui| { ui.horizontal(|ui| { ui.add_space(24.0); - crate::rtl::label(ui, tr("PDF Preset:")); + let name = crate::rtl::label(ui, tr("PDF Preset:")); let cur = d.s("pdfPreset"); - egui::ComboBox::from_id_salt("package_pdf_preset").selected_text(crate::rtl::widget(ui, tr(&cur))).width(180.0).show_ui( + 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() { @@ -289,7 +289,9 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } } }, - ); + ) + .response + .labelled_by(name.id); }); }); } From 559f8f590bea8bf959be6baea03195ce507c1b82 Mon Sep 17 00:00:00 2001 From: moppeeer <svlmrc@gmail.com> Date: Fri, 30 Oct 2026 15:56:16 +0100 Subject: [PATCH 24/28] rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- crates/ui-egui/src/package_dialog.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 536c5e4ac..a2eac5824 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -280,18 +280,18 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { 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| { + 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); + }) + .response + .labelled_by(name.id); }); }); } From bbce6f0323a48c5117caddcf83c2907137067484 Mon Sep 17 00:00:00 2001 From: aiongg <aiongtaigi@gmail.com> Date: Fri, 30 Oct 2026 23:27:29 +0000 Subject: [PATCH 25/28] Integration: #1514 theme tokens Status and warning colours come from the theme's warn and error colours instead of hard-coded RGB values. --- crates/ui-egui/src/package_dialog.rs | 34 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 1919e6d9c..f9ce0752d 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -144,23 +144,35 @@ fn summary_lines(lang: &str, d: &Dialog) -> Vec<(String, bool)> { ] } -/// The warning sign InDesign shows beside a line with problems, and the colours of statuses. -const WARN: egui::Color32 = egui::Color32::from_rgb(0xe8, 0xb3, 0x2c); -const BAD: egui::Color32 = egui::Color32::from_rgb(0xe5, 0x4b, 0x4b); +/// How a status is shown: coloured with the theme's warning or error colour. +#[derive(Clone, Copy, PartialEq, Debug)] +enum Level { + Warn, + Bad, +} -fn status_text(lang: &str, status: &str) -> (String, Option<egui::Color32>) { +impl Level { + fn color(self, ui: &egui::Ui) -> egui::Color32 { + match self { + Level::Warn => ui.visuals().warn_fg_color, + Level::Bad => ui.visuals().error_fg_color, + } + } +} + +fn status_text(lang: &str, status: &str) -> (String, Option<Level>) { let tr = |s: &str| crate::i18n::tr(lang, s).to_string(); match status { - "missing" => (format!("\u{26A0} {}", tr("Missing")), Some(BAD)), - "modified" => (format!("\u{25B2} {}", tr("Modified")), Some(WARN)), - "substituted" => (tr("Substituted"), Some(WARN)), + "missing" => (format!("\u{26A0} {}", tr("Missing")), Some(Level::Bad)), + "modified" => (format!("\u{25B2} {}", tr("Modified")), Some(Level::Warn)), + "substituted" => (tr("Substituted"), Some(Level::Warn)), "embedded" => (tr("Embedded"), None), _ => (tr("OK"), None), } } -/// A table: a header row and `cells` rows (each with an optional colour for its last cell). -fn table(ui: &mut egui::Ui, id: &str, header: &[String], cells: &[(Vec<String>, Option<egui::Color32>)]) { +/// A table: a header row and `cells` rows (each with an optional level colouring its last cell). +fn table(ui: &mut egui::Ui, id: &str, header: &[String], cells: &[(Vec<String>, Option<Level>)]) { let t = crate::theme::Tokens::get(ui.ctx()); egui::Frame::NONE.stroke(egui::Stroke::new(1.0, t.field_border)).inner_margin(6.0).show(ui, |ui| { egui::ScrollArea::vertical().id_salt(id).max_height(220.0).auto_shrink([false, true]).show(ui, |ui| { @@ -175,7 +187,7 @@ fn table(ui: &mut egui::Ui, id: &str, header: &[String], cells: &[(Vec<String>, if i + 1 == row.len() && let Some(color) = color { - text = text.color(*color); + text = text.color(color.color(ui)); } crate::rtl::label(ui, text); } @@ -258,7 +270,7 @@ pub fn show(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.add_space(6.0); for (line, warn) in summary_lines(&lang, d) { ui.horizontal(|ui| { - let sign = egui::RichText::new(if warn { "\u{26A0}" } else { " " }).color(WARN); + let sign = egui::RichText::new(if warn { "\u{26A0}" } else { " " }).color(Level::Warn.color(ui)); ui.add_sized([14.0, 16.0], egui::Label::new(sign)); crate::rtl::label(ui, line); }); From c77b593f80537fc7b4599d59c7018554ed2a2046 Mon Sep 17 00:00:00 2001 From: aiongg <aiongtaigi@gmail.com> Date: Fri, 30 Oct 2026 23:27:46 +0000 Subject: [PATCH 26/28] Integration: #1514 summary wording The Summary lines use neutral wording of our own (keeping the feature names); i18n keys and the test follow, translations carry over. --- crates/ui-egui/src/i18n/ar.rs | 8 ++++---- crates/ui-egui/src/i18n/cs.rs | 8 ++++---- crates/ui-egui/src/i18n/it.rs | 8 ++++---- crates/ui-egui/src/i18n/ka.rs | 8 ++++---- crates/ui-egui/src/i18n/pl.rs | 8 ++++---- crates/ui-egui/src/i18n/pt_br.rs | 8 ++++---- crates/ui-egui/src/i18n/ru.rs | 8 ++++---- crates/ui-egui/src/i18n/table.rs | 8 ++++---- crates/ui-egui/src/i18n/uk.rs | 8 ++++---- crates/ui-egui/src/package_dialog.rs | 14 +++++++------- 10 files changed, 43 insertions(+), 43 deletions(-) diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b79ea87a1..f823f4c53 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -82,13 +82,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "مستبدل"), ("Modified", "معدل"), ("Missing", "مفقود"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "الألوان والأحبار: {process} أحبار معالجة؛ {spot} أحبار مخصصة"), - ("Images: {embedded} Embedded", "الصور: {embedded} مضمنة"), + ("Colors and Inks: {process} process, {spot} spot", "الألوان والأحبار: {process} أحبار معالجة؛ {spot} أحبار مخصصة"), + ("Embedded images: {embedded}", "الصور: {embedded} مضمنة"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "الروابط والصور: {found} روابط؛ {modified} معدلة، {missing} مفقودة", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "الخطوط: {used} مستخدمة، {missing} مفقودة"), + ("Fonts: {used} in use, {missing} unavailable", "الخطوط: {used} مستخدمة، {missing} مفقودة"), ("Instructions:", "التعليمات:"), ("Email:", "البريد الإلكتروني:"), ("Fax:", "الفاكس:"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 41811c755..5feea5db4 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -88,13 +88,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Nahrazeno"), ("Modified", "Změněno"), ("Missing", "Chybí"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Barvy a inkousty: procesní {process}; přímé {spot}"), - ("Images: {embedded} Embedded", "Obrazy: vloženo {embedded}"), + ("Colors and Inks: {process} process, {spot} spot", "Barvy a inkousty: procesní {process}; přímé {spot}"), + ("Embedded images: {embedded}", "Obrazy: vloženo {embedded}"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Odkazy a obrazy: nalezeno {found}; změněno {modified}, chybí {missing}", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Písma: použito {used}, chybí {missing}"), + ("Fonts: {used} in use, {missing} unavailable", "Písma: použito {used}, chybí {missing}"), ("Instructions:", "Pokyny:"), ("Fax:", "Fax:"), ("Address:", "Adresa:"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 8c4f38912..8d97ee17d 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -85,13 +85,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Sostituito"), ("Modified", "Modificato"), ("Missing", "Mancante"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Colori e inchiostri: {process} quadricromia; {spot} tinte piatte"), - ("Images: {embedded} Embedded", "Immagini: {embedded} incorporate"), + ("Colors and Inks: {process} process, {spot} spot", "Colori e inchiostri: {process} quadricromia; {spot} tinte piatte"), + ("Embedded images: {embedded}", "Immagini: {embedded} incorporate"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Collegamenti e immagini: {found} trovati; {modified} modificati, {missing} mancanti", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Font: {used} utilizzati, {missing} mancanti"), + ("Fonts: {used} in use, {missing} unavailable", "Font: {used} utilizzati, {missing} mancanti"), ("Instructions:", "Istruzioni:"), ("Email:", "E-mail:"), ("Fax:", "Fax:"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index bdf324954..4cf1e66ff 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -80,13 +80,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "ჩანაცვლებული"), ("Modified", "შეცვლილი"), ("Missing", "აკლია"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"), - ("Images: {embedded} Embedded", "გამოსახულებები: ჩაშენებულია {embedded}"), + ("Colors and Inks: {process} process, {spot} spot", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"), + ("Embedded images: {embedded}", "გამოსახულებები: ჩაშენებულია {embedded}"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "ბმულები და გამოსახულებები: ნაპოვნია {found}; შეცვლილია {modified}, აკლია {missing}", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"), + ("Fonts: {used} in use, {missing} unavailable", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"), ("Instructions:", "ინსტრუქციები:"), ("Fax:", "ფაქსი:"), ("Address:", "მისამართი:"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 3943daffb..0d2ea135b 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -87,13 +87,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Zastąpiona"), ("Modified", "Zmieniony"), ("Missing", "Brak"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Kolory i farby: procesowe {process}; dodatkowe {spot}"), - ("Images: {embedded} Embedded", "Obrazy: osadzone {embedded}"), + ("Colors and Inks: {process} process, {spot} spot", "Kolory i farby: procesowe {process}; dodatkowe {spot}"), + ("Embedded images: {embedded}", "Obrazy: osadzone {embedded}"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Łącza i obrazy: znaleziono {found}; zmienione {modified}, brakujące {missing}", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Czcionki: użyte {used}, brakujące {missing}"), + ("Fonts: {used} in use, {missing} unavailable", "Czcionki: użyte {used}, brakujące {missing}"), ("Instructions:", "Instrukcje:"), ("Email:", "E-mail:"), ("Fax:", "Faks:"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index f87c8310d..df588c3a7 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -88,13 +88,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Substituída"), ("Modified", "Modificado"), ("Missing", "Ausente"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Cores e tintas: {process} de escala; {spot} especiais"), - ("Images: {embedded} Embedded", "Imagens: {embedded} incorporadas"), + ("Colors and Inks: {process} process, {spot} spot", "Cores e tintas: {process} de escala; {spot} especiais"), + ("Embedded images: {embedded}", "Imagens: {embedded} incorporadas"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Vínculos e imagens: {found} encontrados; {modified} modificados, {missing} ausentes", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Fontes: {used} usadas, {missing} ausentes"), + ("Fonts: {used} in use, {missing} unavailable", "Fontes: {used} usadas, {missing} ausentes"), ("Instructions:", "Instruções:"), ("Email:", "E-mail:"), ("Fax:", "Fax:"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index f6a201665..216e07f14 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -91,13 +91,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Заменен"), ("Modified", "Изменено"), ("Missing", "Отсутствует"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Цвета и краски: триадных {process}; плашечных {spot}"), - ("Images: {embedded} Embedded", "Изображения: внедрено {embedded}"), + ("Colors and Inks: {process} process, {spot} spot", "Цвета и краски: триадных {process}; плашечных {spot}"), + ("Embedded images: {embedded}", "Изображения: внедрено {embedded}"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Связи и изображения: найдено {found}; изменено {modified}, отсутствует {missing}", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Шрифты: используется {used}, отсутствует {missing}"), + ("Fonts: {used} in use, {missing} unavailable", "Шрифты: используется {used}, отсутствует {missing}"), ("Instructions:", "Инструкции:"), ("Email:", "Эл. почта:"), ("Fax:", "Факс:"), diff --git a/crates/ui-egui/src/i18n/table.rs b/crates/ui-egui/src/i18n/table.rs index 8ad790546..ace3a0e68 100644 --- a/crates/ui-egui/src/i18n/table.rs +++ b/crates/ui-egui/src/i18n/table.rs @@ -157,7 +157,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ("Email:", ["E-Mail:", "E-mail :", "Correo electrónico:", "メール:", "电子邮件:"]), ("Instructions:", ["Anweisungen:", "Instructions :", "Instrucciones:", "指示:", "说明:"]), ( - "Fonts: {used} Fonts Used, {missing} Missing", + "Fonts: {used} in use, {missing} unavailable", [ "Schriften: {used} verwendet, {missing} fehlen", "Polices : {used} utilisées, {missing} manquantes", @@ -167,7 +167,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ], ), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", [ "Verknüpfungen und Bilder: {found} gefunden; {modified} geändert, {missing} fehlen", "Liens et images : {found} liens trouvés ; {modified} modifiés, {missing} manquants", @@ -177,7 +177,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ], ), ( - "Images: {embedded} Embedded", + "Embedded images: {embedded}", [ "Bilder: {embedded} eingebettet", "Images : {embedded} incorporées", @@ -187,7 +187,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ], ), ( - "Colors and Inks: {process} Process Inks; {spot} Spot Inks", + "Colors and Inks: {process} process, {spot} spot", [ "Farben und Druckfarben: {process} Prozessfarben; {spot} Volltonfarben", "Couleurs et encres : {process} encres quadri ; {spot} encres d’accompagnement", diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index e7c83ed95..8ab4c5c3d 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -86,13 +86,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Substituted", "Замінено"), ("Modified", "Змінено"), ("Missing", "Бракує"), - ("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "Кольори й фарби: тріадних {process}; плашкових {spot}"), - ("Images: {embedded} Embedded", "Зображення: вбудовано {embedded}"), + ("Colors and Inks: {process} process, {spot} spot", "Кольори й фарби: тріадних {process}; плашкових {spot}"), + ("Embedded images: {embedded}", "Зображення: вбудовано {embedded}"), ( - "Links and Images: {found} Links Found; {modified} Modified, {missing} Missing", + "Links and Images: {found} linked, {modified} changed, {missing} not found", "Зв’язки й зображення: знайдено {found}; змінено {modified}, бракує {missing}", ), - ("Fonts: {used} Fonts Used, {missing} Missing", "Шрифти: використано {used}, бракує {missing}"), + ("Fonts: {used} in use, {missing} unavailable", "Шрифти: використано {used}, бракує {missing}"), ("Instructions:", "Інструкції:"), ("Email:", "Ел. пошта:"), ("Fax:", "Факс:"), diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index f9ce0752d..5f1e98b1d 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -122,21 +122,21 @@ fn summary_lines(lang: &str, d: &Dialog) -> Vec<(String, bool)> { let (modified, missing) = (count(&links, &["modified"]), count(&links, &["missing"])); vec![ ( - tr("Fonts: {used} Fonts Used, {missing} Missing") + tr("Fonts: {used} in use, {missing} unavailable") .replace("{used}", &fonts.len().to_string()) .replace("{missing}", &missing_fonts.to_string()), missing_fonts > 0, ), ( - tr("Links and Images: {found} Links Found; {modified} Modified, {missing} Missing") + tr("Links and Images: {found} linked, {modified} changed, {missing} not found") .replace("{found}", &links.len().to_string()) .replace("{modified}", &modified.to_string()) .replace("{missing}", &missing.to_string()), modified + missing > 0, ), - (tr("Images: {embedded} Embedded").replace("{embedded}", &count(&links, &["embedded"]).to_string()), false), + (tr("Embedded images: {embedded}").replace("{embedded}", &count(&links, &["embedded"]).to_string()), false), ( - tr("Colors and Inks: {process} Process Inks; {spot} Spot Inks") + tr("Colors and Inks: {process} process, {spot} spot") .replace("{process}", &PROCESS_INKS.len().to_string()) .replace("{spot}", &rows(d, "spot").len().to_string()), false, @@ -440,10 +440,10 @@ mod tests { let mut h = harness(app); h.run_steps(3); assert!(h.query_by_label("Scope:").is_some()); - assert!(h.query_by_label("Links and Images: 1 Links Found; 0 Modified, 1 Missing").is_some()); - assert!(h.query_by_label("Colors and Inks: 4 Process Inks; 0 Spot Inks").is_some()); + assert!(h.query_by_label("Links and Images: 1 linked, 0 changed, 1 not found").is_some()); + assert!(h.query_by_label("Colors and Inks: 4 process, 0 spot").is_some()); let fonts = h.state().ui.dialog.as_ref().unwrap().fields["fonts"].as_array().unwrap().len(); - assert!(h.query_by_label(&format!("Fonts: {fonts} Fonts Used, 0 Missing")).is_some()); + assert!(h.query_by_label(&format!("Fonts: {fonts} in use, 0 unavailable")).is_some()); // Links and Images: the link, missing; Show Problems Only keeps it. h.get_by_label("Links and Images").click(); h.run_steps(2); From eea99d1ee8cbbeaa29e6addeb2ed1ab08acb3bb7 Mon Sep 17 00:00:00 2001 From: aiongg <aiongtaigi@gmail.com> Date: Fri, 30 Oct 2026 23:28:05 +0000 Subject: [PATCH 27/28] Integration: #1514 Enter stays local Enter in a multi-line field is a new line only in the Printing Instructions dialog; the flag now uses a stable id (Id::unique never matched) and is cleared for every dialog. --- crates/ui-egui/src/dialogs.rs | 32 ++++++++++++++++++++++++---- crates/ui-egui/src/package_dialog.rs | 2 +- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index f1a443603..ddab4e5dc 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -200,9 +200,21 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.end_row(); }); } -/// The temporary-data key a dialog sets, each frame, while one of its multi-line fields has focus: -/// Enter then makes a new line there, and the footer doesn't take it for OK. -pub(crate) const MULTILINE_FOCUS: &str = "dialog_multiline_focus"; +/// The temporary-data key the Printing Instructions dialog sets, each frame, while one of its +/// multi-line fields has focus: Enter then makes a new line there, and the footer doesn't take it +/// for OK. No other dialog changes how Enter works. +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)); +} + +/// 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::<bool>(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false); + set && id == crate::package_dialog::INSTRUCTIONS +} /// Extract once when opened. Cancel is read-only; confirmation adds exactly these colours. pub(crate) fn open_color_theme(app: &mut DesignApp, params: Value) { @@ -241,6 +253,18 @@ fn color_theme(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { mod color_theme_tests { use super::*; + /// Enter in a multi-line field makes a new line in the Printing Instructions only; every other + /// dialog keeps Enter as OK, and the flag does not outlive the frame. + #[test] + fn enter_in_a_multiline_field_is_local_to_the_printing_instructions() { + let ctx = egui::Context::default(); + set_multiline_focus(&ctx); + assert!(take_multiline_focus(&ctx, crate::package_dialog::INSTRUCTIONS)); + assert!(!take_multiline_focus(&ctx, crate::package_dialog::INSTRUCTIONS), "cleared once read"); + set_multiline_focus(&ctx); + assert!(!take_multiline_focus(&ctx, "insertPages")); + } + /// A page past the end shows as the last page in the dialog, and Before Page puts the new /// page before that one rather than at the end. #[test] @@ -3277,7 +3301,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { style_options_preview(app, &mut d); } // A multi-line field with focus (set by the dialog this frame) takes Enter as a new line. - let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::unique(MULTILINE_FOCUS))).unwrap_or(false); + let multiline = take_multiline_focus(ui.ctx(), &d.id); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui .add( diff --git a/crates/ui-egui/src/package_dialog.rs b/crates/ui-egui/src/package_dialog.rs index 5f1e98b1d..04b236d76 100644 --- a/crates/ui-egui/src/package_dialog.rs +++ b/crates/ui-egui/src/package_dialog.rs @@ -321,7 +321,7 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } // Enter there makes a new line: the footer mustn't take it for Continue. if lines > 1 && resp.has_focus() { - ui.ctx().data_mut(|m| m.insert_temp(egui::Id::unique(crate::dialogs::MULTILINE_FOCUS), true)); + crate::dialogs::set_multiline_focus(ui.ctx()); } ui.end_row(); } From ea5df5980856fdd53ac5a7dbc7a4d9e0afcfdda0 Mon Sep 17 00:00:00 2001 From: aiongg <aiongtaigi@gmail.com> Date: Fri, 30 Oct 2026 23:29:17 +0000 Subject: [PATCH 28/28] Integration: ROADMAP lines for #1249, #1255, #1514, #1515 --- ROADMAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 4f950db90..5717dd3da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -386,6 +386,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-30** · File > Package opens a dialog, as the reference app's: a Summary of fonts, links and images and inks, pages for each, a Printing Instructions step, and a PDF made with the options of a preset (#1514, #1515). +- **2026-10-30** · IDML import reads TOC separators and entry styles written with the `$ID/` prefix or as `Properties` children, and the TOC separator notation (`^t`, `^y`, `^m`, `^>`) is shared by the dialog and IDML, with a special-character menu on the separator field (#1249, #1255). - **2026-10-29** · Inserting text with many returns no longer takes time that grows with their square (part of #1588): each return copied its paragraph's format with its own insert, moving every format after it. Splitting 20,000 returns into paragraphs took 15.8 s; placing a text file of them now takes 0.2 s, and one of a million returns 5.5 s. No readiness percentage change. - **2026-10-28** · Discretionary hyphens (U+00AD) break in the single-line composer with automatic hyphenation on or off, matching the paragraph composer. Unbroken soft hyphens remain invisible and zero-width, and words containing them keep only their discretionary break points (#1955). No readiness percentage change. - **2026-10-28** · Autoflow no longer sizes the rest of a story by a frame that a Start Paragraph option (next column, frame, page, or odd or even page) ends early (#1209). With a title before a chapter that starts on the next odd page, it estimated a page per few characters, added up to 2000 pages (the autoflow limit) and removed them again: placing 2000 words took 36 s in the app, now 0.4 s, with the same pages. It looks past tracked-deleted paragraphs for the option, as composing does, and asks for every frame that start options and breaks force in one round: 200 one-line chapters on their own pages take 9 ms (58 ms before). A break and the start option it meets count once, and breaks that tracked changes delete or a hidden condition hides don't count. No readiness percentage change.