Merge track 6

Conflicts: dialogs.rs dialog widths (both kept); i18n/*.rs adjacent rows (both kept).
This commit is contained in:
aiongg
2026-10-30 23:57:22 +00:00
29 changed files with 1481 additions and 99 deletions
+5
View File
@@ -388,6 +388,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.
@@ -507,6 +509,9 @@ Newest first.
- **2026-10-21** · Drop Shadow, Inner Shadow and Outer Glow have a blend mode, as InDesign's (Effects dialog › Blending › Mode; `mode` on `object.dropShadow`, `object.innerShadow`, `object.outerGlow`; IDML `BlendMode`). New shadows start Multiply and new outer glows Screen in white ([Paper]); effects saved before keep Normal, so documents look as they did. Found in the Learn Skills Daily course, chapter 14. No readiness percentage change.
- **2026-10-14** · The Pages panel labels spreads as InDesign does ("2–3", "2, 1" across a section start, "8, A"), marks each section's first page with a triangle, and scrolls to the page in view. No readiness percentage change.
- **2026-10-18** · Double-clicking a section triangle in the Pages panel opens that page’s numbering options, including from parent-page editing. No readiness percentage change.
- **2026-10-18** · TOC separators preserve right-indent tabs through the dialog and IDML, including localized `$ID/^y` values. A separator picker exposes Tab Character, Right Indent Tab, Em Space and En Space. Existing paragraph tab leaders now work with the tutorial's TOC workflow. No readiness percentage change.
- **2026-10-21** · The Package dialog's Include PDF (Print) has a PDF Preset (the Export PDF presets, the last one used first), as InDesign's Select PDF Preset; `file.package` takes `pdf: {…file.exportPdf options}`, refusing bad options before writing anything. The packaged PDF always had the default settings. Found in the Learn Skills Daily course, chapter 27. No readiness percentage change.
- **2026-10-21** · File › Package… opens InDesign's Package dialog: Summary counts the fonts, links and images and inks the package carries, with a warning where some are missing or modified, and Fonts, Links and Images and Colors and Inks list them (Show Problems Only). Create Printing Instructions asks for the contact details and instructions that go into the package's Instructions.txt, and Include IDML / Include PDF (Print) choose the copies it carries. Package… went straight to the folder picker with fixed options. Found in the Learn Skills Daily course, chapter 27. No readiness percentage change.
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
+29
View File
@@ -39,6 +39,35 @@ 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}', "^>")
}
/// 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
}
/// 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
/// the next paragraph starts in the next column, frame or page. A break inside a paragraph (from
+1
View File
@@ -16,6 +16,7 @@ mod registry;
use flatten::{rasterize_effects, shadow_coverage};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use pdf::pdf_export;
pub(crate) use pdf::{check_pdf_options, pdf_report};
pub(crate) use pdf::{flatten_ppi, options, print_pdf};
pub use registry::specs;
+52 -35
View File
@@ -169,41 +169,21 @@ pub(crate) fn print_pdf(
/// The PDF that File › Export PDF writes for `p`, with its warnings: the print PDF
/// ([`print_pdf`]) with its view options, timings and transitions. File › Print prints it.
pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::ExportReport> {
let st = s.doc()?;
let opts = options(p, st.doc.page_count())?;
pdf_report(&s.doc()?.doc, &s.cache, p)
}
/// [`pdf_export`] of `doc`: `file.package` writes its PDF of the packaged copy with it.
pub(crate) fn pdf_report(doc: &designcraft_doc::Document, cache: &designcraft_compose::Cache, p: &Value) -> Result<designcraft_pdf::ExportReport> {
let opts = options(p, doc.page_count())?;
let ppi = flatten_ppi("file.exportPdf", p)?;
let mut r = print_pdf(&st.doc, &s.cache, &opts, ppi)?;
let doc: &designcraft_doc::Document = &st.doc;
let (cat, dest) = view_options(p)?;
let mut r = print_pdf(doc, cache, &opts, ppi)?;
let spreads = designcraft_pdf::sheet_spreads(doc, &opts);
// Interactive PDF view options.
let mut cat = String::new();
if p.get("fullScreen").and_then(Value::as_bool) == Some(true) {
cat.push_str("/PageMode/FullScreen");
} else if p.get("bookmarksPanel").and_then(Value::as_bool) == Some(true) {
cat.push_str("/PageMode/UseOutlines");
}
match str_param(p, "pageLayout") {
Some("single") => cat.push_str("/PageLayout/SinglePage"),
Some("continuous") => cat.push_str("/PageLayout/OneColumn"),
Some("twoUp") => cat.push_str("/PageLayout/TwoPageLeft"),
Some("twoUpCover") => cat.push_str("/PageLayout/TwoPageRight"),
Some("twoUpContinuous") => cat.push_str("/PageLayout/TwoColumnRight"),
Some(o) => return Err(bad("file.exportPdf", format!("unknown pageLayout `{o}`"))),
None => {}
}
if let Some(v) = str_param(p, "view") {
// The first page opens with this view (pages are objects in order, so refer by index 0).
let dest = match v {
"fitPage" => "/Fit",
"fitWidth" => "/FitH null",
"actual" => "/XYZ null null 1",
o => {
return Err(bad("file.exportPdf", format!("unknown view `{o}` (fitPage, fitWidth, actual)")));
}
};
if let Some(first) = first_page_ref(&r.bytes) {
cat.push_str(&format!("/OpenAction[{first} 0 R{dest}]"));
}
let mut cat = cat;
if let Some(dest) = dest
&& let Some(first) = first_page_ref(&r.bytes)
{
cat.push_str(&format!("/OpenAction[{first} 0 R{dest}]"));
}
if !cat.is_empty() {
match designcraft_pdf::add_catalog_entries(&r.bytes, &cat) {
@@ -213,8 +193,7 @@ pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::Expo
}
// Flip pages every N seconds (presentation).
if let Some(secs) = p.get("advanceSeconds").and_then(Value::as_f64).filter(|s| *s > 0.0) {
let n = designcraft_pdf::sheet_spreads(doc, &opts).len();
let dur: Vec<Option<String>> = (0..n).map(|_| Some(format!("/Dur {secs:.2}"))).collect();
let dur: Vec<Option<String>> = (0..spreads.len()).map(|_| Some(format!("/Dur {secs:.2}"))).collect();
match designcraft_pdf::add_page_entries(&r.bytes, &dur) {
Some(b) => r.bytes = b,
None => r.warnings.push("page timings couldn't be set".into()),
@@ -232,6 +211,44 @@ pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result<designcraft_pdf::Expo
Ok(r)
}
/// The catalog entries of the interactive view options in `p` (page mode and layout) and the
/// destination the first page opens at (`view`); unknown values are refused.
fn view_options(p: &Value) -> Result<(String, Option<&'static str>)> {
let mut cat = String::new();
if p.get("fullScreen").and_then(Value::as_bool) == Some(true) {
cat.push_str("/PageMode/FullScreen");
} else if p.get("bookmarksPanel").and_then(Value::as_bool) == Some(true) {
cat.push_str("/PageMode/UseOutlines");
}
match str_param(p, "pageLayout") {
Some("single") => cat.push_str("/PageLayout/SinglePage"),
Some("continuous") => cat.push_str("/PageLayout/OneColumn"),
Some("twoUp") => cat.push_str("/PageLayout/TwoPageLeft"),
Some("twoUpCover") => cat.push_str("/PageLayout/TwoPageRight"),
Some("twoUpContinuous") => cat.push_str("/PageLayout/TwoColumnRight"),
Some(o) => return Err(bad("file.exportPdf", format!("unknown pageLayout `{o}`"))),
None => {}
}
// The first page opens with this view (pages are objects in order, so refer by index 0).
let dest = match str_param(p, "view") {
None => None,
Some("fitPage") => Some("/Fit"),
Some("fitWidth") => Some("/FitH null"),
Some("actual") => Some("/XYZ null null 1"),
Some(o) => return Err(bad("file.exportPdf", format!("unknown view `{o}` (fitPage, fitWidth, actual)"))),
};
Ok((cat, dest))
}
/// Refuses PDF options `p` that [`pdf_report`] would refuse for a document of `pages` pages,
/// without exporting: `file.package` checks them before it writes anything.
pub(crate) fn check_pdf_options(p: &Value, pages: usize) -> Result<()> {
options(p, pages)?;
flatten_ppi("file.exportPdf", p)?;
view_options(p)?;
Ok(())
}
#[cfg(test)]
mod tagged_tests {
use serde_json::json;
+18 -7
View File
@@ -203,7 +203,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Package…",
["File"],
None,
"{dir: folder to create, idml?: true, pdf?: false, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report}",
"{dir: folder to create, idml?: true, pdf?: false|true|{…options of file.exportPdf}, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report}",
has_doc,
package
),
@@ -390,6 +390,17 @@ fn drawn_faces(cs: &ComposedStory, depth: usize, out: &mut Vec<FaceRef>) {
fn package(s: &mut Session, p: &Value) -> Result<Value> {
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("file.package", "missing `dir`"))?);
let st = s.doc()?;
// The PDF: with the default settings, or with the options of a PDF preset, made as
// file.exportPdf makes it. Read first, so that bad options write nothing.
let pdf = match p.get("pdf") {
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
Some(Value::Bool(true)) => Some(json!({})),
Some(o @ Value::Object(_)) => {
super::export::check_pdf_options(o, st.doc.page_count())?;
Some(o.clone())
}
Some(v) => return Err(bad("file.package", format!("`pdf` is true, false or PDF export options, not {v}"))),
};
let root = OutputRoot::new(dir.clone())?;
let display_title = st.doc.title.clone();
let title = safe_component(&display_title, "Untitled");
@@ -441,18 +452,18 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
idml_layer_note = "\nPlaced PDF layers\nThe IDML copy links the placed PDFs, so it shows their file defaults. The DesignCraft document keeps each placement's Object Layer Options choice.\n";
}
}
if p.get("pdf").and_then(Value::as_bool).unwrap_or(false) {
if let Some(params) = pdf {
let path = package_output_path(&dir, &title, "pdf")?;
let bytes = designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(&packed), &s.cache, &Default::default())
.map_err(|e| EngineError::Other(e.to_string()))?;
root.write(&path, &bytes)?;
root.write(&path, &super::export::pdf_report(&packed, &s.cache, &params)?.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() {
+53
View File
@@ -25,6 +25,58 @@ fn relocated_packages_rebind_embedded_image_links_on_open() {
std::fs::remove_dir_all(dir).unwrap();
}
/// `pdf` takes file.exportPdf's options (a PDF preset's): a PDF/X-4 package PDF; `true` is the
/// default PDF, and bad options are refused.
#[test]
fn package_pdf_takes_export_options() {
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let dir = std::env::temp_dir().join(format!("dc-package-pdf-options-{}-{nonce}", std::process::id()));
let mut s = Session::new();
s.execute("file.new", &json!({"title": "Options", "width": 72, "height": 72})).unwrap();
let x4 = dir.join("x4");
s.execute("file.package", &json!({"dir": x4.to_string_lossy(), "idml": false, "pdf": {"standard": "x4"}})).unwrap();
let has = |path: &std::path::Path, what: &[u8]| std::fs::read(path).unwrap().windows(what.len()).any(|w| w == what);
assert!(has(&x4.join("Options.pdf"), b"GTS_PDFX"));
let plain = dir.join("plain");
s.execute("file.package", &json!({"dir": plain.to_string_lossy(), "idml": false, "pdf": true})).unwrap();
assert!(!has(&plain.join("Options.pdf"), b"GTS_PDFX"));
// Refused before anything is written: bad options, a bad flattener or view option, and a
// `pdf` that is neither a switch nor options.
for bad in [
json!({"standard": "nope"}),
json!({"flatten": "extreme"}),
json!({"pageLayout": "spiral"}),
json!({"view": "zoomed"}),
json!("print"),
json!(3),
] {
let out = dir.join("bad");
assert!(s.execute("file.package", &json!({"dir": out.to_string_lossy(), "pdf": bad})).is_err(), "{bad}");
assert!(!out.exists(), "{bad}: bad options write nothing");
}
std::fs::remove_dir_all(dir).unwrap();
}
/// A preset that flattens transparency (Screen and Email's `flatten: low`) gives the packaged
/// PDF the flattening `file.exportPdf` does.
#[test]
fn the_packaged_pdf_is_flattened_as_export_flattens_it() {
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let dir = std::env::temp_dir().join(format!("dc-package-pdf-flatten-{}-{nonce}", std::process::id()));
let mut s = Session::new();
s.execute("file.new", &json!({"title": "Flat", "width": 144, "height": 144})).unwrap();
let id = s.execute("frame.create", &json!({"rect": [10, 10, 100, 100], "content": "unassigned"})).unwrap()["id"].clone();
s.execute("object.fill", &json!({"ids": [id], "swatch": "[Black]"})).unwrap();
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
let exported = super::super::base64_decode(s.execute("file.exportPdf", &json!({"flatten": "low"})).unwrap()["base64"].as_str().unwrap());
s.execute("file.package", &json!({"dir": dir.to_string_lossy(), "idml": false, "pdf": {"flatten": "low"}})).unwrap();
let packaged = std::fs::read(dir.join("Flat.pdf")).unwrap();
let images = |pdf: &[u8]| pdf.windows(b"/Subtype/Image".len()).filter(|w| *w == b"/Subtype/Image").count();
assert!(images(&exported) > 0, "the export rasterises the transparent object");
assert_eq!(images(&packaged), images(&exported), "so does the package");
std::fs::remove_dir_all(dir).unwrap();
}
/// A per-placement PDF layer choice cannot travel in an IDML copy that links its files, so the
/// report says the copies differ instead of leaving it to be discovered.
#[test]
@@ -169,6 +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.contains("Preflight:"), "{report}");
// The packaged document links to its Links folder.
let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap();
+1 -1
View File
@@ -527,7 +527,7 @@ fn table_of_contents_definition_round_trips_through_idml_and_updates() {
s.execute(
"toc.generate",
&json!({"page": page, "title": "Inside", "titleStyle": basic, "createBookmarks": true, "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"},
]}),
)
+8 -5
View File
@@ -313,10 +313,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<TocEntry> = ts
.elements()
@@ -333,9 +335,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 })
})
+5
View File
@@ -223,6 +223,11 @@ impl<'r> Importer<'r> {
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`]).
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).
/// 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.
+17 -20
View File
@@ -858,28 +858,25 @@ pub fn digits_in(s: &str) -> Option<designcraft_doc::Digits> {
[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 {
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
}
+92
View File
@@ -4637,6 +4637,98 @@ fn cell_first_baseline_comes_from_cell_styles_table_regions_and_cells_and_round_
assert_eq!(s2.first_baseline, Some(FirstBaseline::CapHeight));
}
/// A TOC style as InDesign writes it: `$ID/`-prefixed separator (`^y`, the right indent tab) and entry style.
const TOC_STYLES: &str = r#"<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
<RootParagraphStyleGroup>
<ParagraphStyle Self="ParagraphStyle/Heading 1" Name="Heading 1"/>
<ParagraphStyleGroup Self="psg-toc" Name="TOC">
<ParagraphStyle Self="ParagraphStyle/TOC%3aTOC body text" Name="TOC:TOC body text" Capitalization="AllCaps"/>
<ParagraphStyle Self="ParagraphStyle/TOC%3aTOC Header" Name="TOC:TOC Header"/>
</ParagraphStyleGroup>
</RootParagraphStyleGroup>
<RootCharacterStyleGroup><CharacterStyle Self="CharacterStyle/TOC Page Number" Name="TOC Page Number"/></RootCharacterStyleGroup>
<TOCStyle Self="TOCStyle/$ID/DefaultTOCStyleName" TitleStyle="ParagraphStyle/TOC%3aTOC Header" Title="Table of Contents" Name="$ID/DefaultTOCStyleName">
<TOCStyleEntry Self="e1" Name="Heading 1" Level="1" PageNumberPosition="AfterEntry" Separator="$ID/^y">
<Properties><FormatStyle type="object">ParagraphStyle/$ID/TOC%3aTOC body text</FormatStyle>
<PageNumberStyle type="object">CharacterStyle/TOC Page Number</PageNumberStyle>
<SeparatorStyle type="string">$ID/[Same Style]</SeparatorStyle></Properties>
</TOCStyleEntry>
</TOCStyle></idPkg:Styles>"#;
const TOC_STORY: &str = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
<Story Self="s1" AppliedTOCStyle="TOCStyle/$ID/DefaultTOCStyleName"><ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/TOC%3aTOC body text"><CharacterStyleRange><Content>Welcome</Content></CharacterStyleRange></ParagraphStyleRange></Story></idPkg:Story>"#;
#[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.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"));
}
#[test]
fn toc_style_entries_fall_back_on_malformed_and_unsupported_values() {
let long = "x".repeat(65);
let entries = format!(
r#"<TOCStyleEntry Self="e1" Name="Heading 1" Level="abc" PageNumberPosition="Sideways" Separator="$ID/^q">
<Properties><FormatStyle type="object">ParagraphStyle/Missing</FormatStyle>
<PageNumberStyle type="object">CharacterStyle/$ID/[None]</PageNumberStyle>
<SeparatorStyle type="object">CharacterStyle/Missing</SeparatorStyle></Properties>
</TOCStyleEntry>
<TOCStyleEntry Self="e2" Name="TOC:TOC Header" Level="12" Separator="{long}"/>
<TOCStyleEntry Self="e3" Name="Heading 1" Level="2" Separator="^t"/>
<TOCStyleEntry Self="e4" Name="" Level="3"/>"#
);
let start = TOC_STYLES.find("<TOCStyleEntry").unwrap();
let end = TOC_STYLES.find("</TOCStyle>").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
// 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"));
}
/// A graphic placed in a table cell is written into the cell, not dropped.
#[test]
fn exports_graphic_in_table_cell() {
+148 -22
View File
@@ -202,6 +202,21 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.end_row();
});
}
/// 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::unique(MULTILINE_FOCUS), true));
}
/// Whether Enter in dialog `id` goes to a multi-line field this frame. The flag is read (and cleared)
/// for every dialog, but only the Printing Instructions dialog honours it.
fn take_multiline_focus(ctx: &egui::Context, id: &str) -> bool {
let set = ctx.data_mut(|m| m.remove_temp::<bool>(egui::Id::unique(MULTILINE_FOCUS))).unwrap_or(false);
set && id == crate::package_dialog::INSTRUCTIONS
}
/// Separators the Convert dialogs offer by name; anything else typed in the field is the
/// separator's own characters.
@@ -385,6 +400,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]
@@ -528,6 +555,40 @@ mod color_theme_tests {
assert_eq!(opts(&app).columns, 1, "one undo step");
}
#[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();
if app.session.tool_id() == "tocPlace" {
app.run("toc.place", json!({"x":36,"y":36})).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);
}
// 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]
fn color_theme_cancel_is_read_only_and_confirm_adds_the_preview() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
@@ -727,6 +788,41 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
const PDF_EXPORT_PRESETS: &[&str] =
&["Desktop Printing", "Commercial Printing", "Screen and Email", "PDF/X-1a:2003", "PDF/X-4", "PDF/A-2b", "Custom"];
/// The `file.exportPdf` options the Export PDF dialog's fields stand for (pages and title aside).
fn pdf_export_params(d: &Dialog) -> Value {
let mut params = json!({
"standard": d.s("standard"),
"compressImages": d.b("compressImages"),
"spreads": d.b("spreads"),
"bleed": d.b("bleed"),
"tagged": d.b("tagged"),
"marks": {
"crop": d.b("marksCrop"),
"bleed": d.b("marksBleed"),
"pageInfo": d.b("marksPageInfo"),
"weight": d.pt("marksWeight").unwrap_or(0.25),
"offset": d.pt("marksOffset").unwrap_or(6.0),
},
});
let flatten = d.s("flatten");
if !flatten.is_empty() {
params["flatten"] = json!(flatten);
}
params
}
/// The PDF presets a document can be exported with, by name (Custom aside).
pub(crate) fn pdf_presets() -> impl Iterator<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" and
/// "PDF/X-1a:2003" (High Resolution); "Custom" (and any unknown value) changes nothing.
fn apply_pdf_preset(d: &mut Dialog, preset: &str) {
@@ -2789,6 +2885,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"),
@@ -2810,6 +2908,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"numberingSection" => 560.0,
crate::effects_dialog::ID => 720.0,
"colorPicker" => 580.0,
crate::package_dialog::ID => 720.0,
crate::package_dialog::INSTRUCTIONS => 520.0,
_ => 640.0,
};
let width = preferred_width.min(available.x);
@@ -3146,6 +3246,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" => {
@@ -3604,6 +3706,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() {
@@ -3617,6 +3721,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 = take_multiline_focus(ui.ctx(), &d.id);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add(
@@ -3624,7 +3730,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);
}
@@ -4477,24 +4583,7 @@ fn confirm_part3(app: &mut DesignApp, d: Dialog) -> 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);
@@ -4812,6 +4901,22 @@ fn confirm_part6(app: &mut DesignApp, d: Dialog) -> Result<Value, String> {
}
"alert" => Ok(Value::Null),
crate::effects_dialog::ID => crate::effects_dialog::confirm(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.
@@ -8081,11 +8186,20 @@ 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)
}
/// 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<TocRow> {
@@ -8311,7 +8425,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, name);
ui.end_row();
let between = crate::rtl::label(ui, tr("Between Entry and Number:")).id;
ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).align(egui::Align2::LEFT_TOP)).labelled_by(between);
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).align(egui::Align2::LEFT_TOP)).labelled_by(between);
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() {
toc_separator_append(&mut r.separator, code);
ui.close();
}
}
})
.response
.on_hover_text(tr("Insert Special Character"));
});
let name = crate::rtl::label(ui, tr("Style:")).id;
toc_style_combo(ui, "separator", &char_styles, none, &mut r.separator_style, name);
ui.end_row();
+39
View File
@@ -1,6 +1,7 @@
//! Original Arabic interface translations, stored in logical Unicode order.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "خيارات أقل"),
("Tab Character", "محرف جدولة"),
("File", "ملف"),
("Edit", "تحرير"),
("Layout", "تخطيط"),
@@ -95,6 +96,44 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "إضافة عينة لون CMYK"),
("Added swatch", "تمت إضافة عينة اللون"),
("Eyedropper: drag it onto the page", "القطارة: اسحبها إلى الصفحة"),
("PDF Preset:", "إعداد PDF المسبق:"),
("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, {spot} spot", "الألوان والأحبار: {process} أحبار معالجة؛ {spot} أحبار مخصصة"),
("Embedded images: {embedded}", "الصور: {embedded} مضمنة"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"الروابط والصور: {found} روابط؛ {modified} معدلة، {missing} مفقودة",
),
("Fonts: {used} in use, {missing} unavailable", "الخطوط: {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", "الملخص"),
("Restart Numbering", "إعادة الترقيم"),
("Continue Numbering", "متابعة الترقيم"),
("Keep Options…", "خيارات الإبقاء…"),
+38
View File
@@ -4,6 +4,7 @@
//! the ArtCraft siblings (VectorCraft, PhotoCraft) are kept identical; see ASSETS.md for sources.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Méně možností"),
("Tab Character", "Znak tabulátoru"),
("File", "Soubor"),
("Edit", "Úpravy"),
("Layout", "Rozvržení"),
@@ -100,6 +101,43 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Přidat vzorek CMYK"),
("Added swatch", "Vzorek přidán"),
("Eyedropper: drag it onto the page", "Kapátko: přetáhněte ho na stránku"),
("PDF Preset:", "Předvolba PDF:"),
("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, {spot} spot", "Barvy a inkousty: procesní {process}; přímé {spot}"),
("Embedded images: {embedded}", "Obrazy: vloženo {embedded}"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Odkazy a obrazy: nalezeno {found}; změněno {modified}, chybí {missing}",
),
("Fonts: {used} in use, {missing} unavailable", "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"),
("Restart Numbering", "Začít číslování znovu"),
("Continue Numbering", "Pokračovat v číslování"),
("Keep Options…", "Volby zachování…"),
+1
View File
@@ -4,6 +4,7 @@
//! table is carried over and stays the fallback for keys added there after this table.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Weniger Optionen"),
("Tab Character", "Tabulatorzeichen"),
("File", "Datei"),
("Edit", "Bearbeiten"),
("Layout", "Layout"),
-3
View File
@@ -104,7 +104,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"),
(
@@ -252,7 +251,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"),
@@ -262,7 +260,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"),
+43
View File
@@ -2,6 +2,10 @@
//! using ordinary Italian desktop-publishing vocabulary.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Meno opzioni"),
("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"),
@@ -95,6 +99,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Aggiungi campione CMYK"),
("Added swatch", "Campione aggiunto"),
("Eyedropper: drag it onto the page", "Contagocce: trascinalo sulla pagina"),
("PDF Preset:", "Predefinito PDF:"),
("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, {spot} spot", "Colori e inchiostri: {process} quadricromia; {spot} tinte piatte"),
("Embedded images: {embedded}", "Immagini: {embedded} incorporate"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Collegamenti e immagini: {found} trovati; {modified} modificati, {missing} mancanti",
),
("Fonts: {used} in use, {missing} unavailable", "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"),
("Restart Numbering", "Riavvia numerazione"),
("Continue Numbering", "Continua numerazione"),
("Keep Options…", "Opzioni di separazione…"),
+1
View File
@@ -4,6 +4,7 @@
//! shared table are carried over unchanged.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "オプションを減らす"),
("Tab Character", "タブ文字"),
("File", "ファイル"),
("Edit", "編集"),
("Layout", "レイアウト"),
+38
View File
@@ -5,6 +5,7 @@
//! the code or the command table use.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "ნაკლები პარამეტრი"),
("Tab Character", "ტაბულაციის სიმბოლო"),
("File", "ფაილი"),
("Edit", "რედაქტირება"),
("Layout", "განლაგება"),
@@ -94,6 +95,43 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "CMYK ფერის ნიმუშის დამატება"),
("Added swatch", "ფერის ნიმუში დამატებულია"),
("Eyedropper: drag it onto the page", "პიპეტი: გადაიტანეთ გვერდზე"),
("PDF Preset:", "PDF-ის წინასწარი პარამეტრი:"),
("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, {spot} spot", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"),
("Embedded images: {embedded}", "გამოსახულებები: ჩაშენებულია {embedded}"),
("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის მელანი; {spot} სპოტ-საღებავი"),
("Images: {embedded} Embedded", "გამოსახულებები: ჩაშენებულია {embedded}"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"ბმულები და გამოსახულებები: ნაპოვნია {found}; შეცვლილია {modified}, აკლია {missing}",
),
("Fonts: {used} in use, {missing} unavailable", "შრიფტები: გამოყენებულია {used}, აკლია {missing}"),
("Instructions:", "ინსტრუქციები:"),
("Fax:", "ფაქსი:"),
("Address:", "მისამართი:"),
("Company:", "კომპანია:"),
("Contact:", "საკონტაქტო პირი:"),
("Process Black", "პროცესის შავი"),
("Process Yellow", "პროცესის ყვითელი"),
("Process Magenta", "პროცესის მეწამული"),
("Process Cyan", "პროცესის ცისფერი"),
("Colors and Inks", "ფერები და მელნები"),
("Links and Images", "ბმულები და გამოსახულებები"),
("Fonts", "შრიფტები"),
("Summary", "შეჯამება"),
("Restart Numbering", "ნუმერაციის თავიდან დაწყება"),
("Continue Numbering", "ნუმერაციის გაგრძელება"),
("Keep Options…", "შენარჩუნების პარამეტრები…"),
+41
View File
@@ -3,6 +3,7 @@
//! (rozkładówka, łam, strona wzorcowa, obszar informacyjny) and the sibling ArtCraft catalogs.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Mniej opcji"),
("Tab Character", "Znak tabulacji"),
("File", "Plik"),
("Edit", "Edycja"),
("Layout", "Układ"),
@@ -98,6 +99,46 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Dodaj próbkę CMYK"),
("Added swatch", "Dodano próbkę"),
("Eyedropper: drag it onto the page", "Kroplomierz: przeciągnij go na stronę"),
("PDF Preset:", "Ustawienie predefiniowane PDF:"),
("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, {spot} spot", "Kolory i farby: procesowe {process}; dodatkowe {spot}"),
("Embedded images: {embedded}", "Obrazy: osadzone {embedded}"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Łącza i obrazy: znaleziono {found}; zmienione {modified}, brakujące {missing}",
),
("Fonts: {used} in use, {missing} unavailable", "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"),
("Restart Numbering", "Zacznij numerowanie od nowa"),
("Continue Numbering", "Kontynuuj numerowanie"),
("Keep Options…", "Opcje utrzymywania…"),
+43
View File
@@ -2,6 +2,10 @@
//! using ordinary Brazilian Portuguese computing vocabulary.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Menos opções"),
("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"),
@@ -98,6 +102,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Adicionar amostra CMYK"),
("Added swatch", "Amostra adicionada"),
("Eyedropper: drag it onto the page", "Conta-gotas: arraste-o para a página"),
("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"),
("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, {spot} spot", "Cores e tintas: {process} de escala; {spot} especiais"),
("Embedded images: {embedded}", "Imagens: {embedded} incorporadas"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Vínculos e imagens: {found} encontrados; {modified} modificados, {missing} ausentes",
),
("Fonts: {used} in use, {missing} unavailable", "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"),
("Restart Numbering", "Reiniciar numeração"),
("Continue Numbering", "Continuar numeração"),
("Keep Options…", "Opções de manutenção…"),
+41
View File
@@ -4,6 +4,7 @@
//! category before the number ("страниц: 5"), see `count_label`.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Меньше параметров"),
("Tab Character", "Символ табуляции"),
("File", "Файл"),
("Edit", "Редактирование"),
("Can't Open the File", "Не удалось открыть файл"),
@@ -102,6 +103,46 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Добавить образец CMYK"),
("Added swatch", "Образец добавлен"),
("Eyedropper: drag it onto the page", "Пипетка: перетащите её на страницу"),
("PDF Preset:", "Стиль PDF:"),
("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, {spot} spot", "Цвета и краски: триадных {process}; плашечных {spot}"),
("Embedded images: {embedded}", "Изображения: внедрено {embedded}"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Связи и изображения: найдено {found}; изменено {modified}, отсутствует {missing}",
),
("Fonts: {used} in use, {missing} unavailable", "Шрифты: используется {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", "Сводка"),
("Restart Numbering", "Начать нумерацию заново"),
("Continue Numbering", "Продолжить нумерацию"),
("Keep Options…", "Параметры удержания…"),
+87
View File
@@ -152,6 +152,92 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[
("File Info…", ["Dateiinformationen …", "Informations sur le fichier…", "Información de archivo…", "ファイル情報…", "文件信息…"]),
("File Info", ["Dateiinformationen", "Informations sur le fichier", "Información de archivo", "ファイル情報", "文件信息"]),
("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} in use, {missing} unavailable",
[
"Schriften: {used} verwendet, {missing} fehlen",
"Polices : {used} utilisées, {missing} manquantes",
"Fuentes: {used} utilizadas, {missing} ausentes",
"フォント : {used} 個使用、{missing} 個見つかりません",
"字体:使用 {used} 种,缺失 {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",
"Vínculos e imágenes: {found} vínculos encontrados; {modified} modificados, {missing} ausentes",
"リンクと画像 : {found} 個のリンク、{modified} 個変更、{missing} 個見つかりません",
"链接和图像:找到 {found} 个链接;{modified} 个已修改,{missing} 个缺失",
],
),
(
"Embedded images: {embedded}",
[
"Bilder: {embedded} eingebettet",
"Images : {embedded} incorporées",
"Imágenes: {embedded} incrustadas",
"画像 : {embedded} 個埋め込み",
"图像:嵌入 {embedded} 个",
],
),
(
"Colors and Inks: {process} process, {spot} spot",
[
"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(打印)"],
),
("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", "続行", "继续"]),
(
"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", "やり直し", "重做"]),
@@ -4840,6 +4926,7 @@ pub(super) 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",
+43
View File
@@ -3,6 +3,10 @@
//! (VectorCraft, PhotoCraft, WordCraft and DeckCraft); see ASSETS.md for pinned sources.
pub(super) const TABLE: &[(&str, &str)] = &[
("Fewer Options", "Менше параметрів"),
("Tab Character", "Символ табуляції"),
("Right Indent Tab", "Табуляція правого відступу"),
("Em Space", "Кругла шпація"),
("En Space", "Півкругла шпація"),
("File", "Файл"),
("Edit", "Редагування"),
("Layout", "Макет"),
@@ -96,6 +100,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Add CMYK Swatch", "Додати зразок CMYK"),
("Added swatch", "Зразок додано"),
("Eyedropper: drag it onto the page", "Піпетка: перетягніть її на сторінку"),
("PDF Preset:", "Набір параметрів PDF:"),
("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, {spot} spot", "Кольори й фарби: тріадних {process}; плашкових {spot}"),
("Embedded images: {embedded}", "Зображення: вбудовано {embedded}"),
(
"Links and Images: {found} linked, {modified} changed, {missing} not found",
"Зв’язки й зображення: знайдено {found}; змінено {modified}, бракує {missing}",
),
("Fonts: {used} in use, {missing} unavailable", "Шрифти: використано {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", "Зведення"),
("Restart Numbering", "Почати нумерацію знову"),
("Continue Numbering", "Продовжити нумерацію"),
("Keep Options…", "Параметри утримання…"),
+1
View File
@@ -31,6 +31,7 @@ pub mod menus;
mod modal_drag;
mod new_document;
mod object_control;
pub mod package_dialog;
mod page_history;
mod panel_docking;
pub mod panels;
+2 -5
View File
@@ -247,12 +247,9 @@ fn run_ui_part1(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
}
"app.exportText" => export_bytes(app, p, "rtf", "file.exportText"),
"app.packageDialog" => {
// Without a folder, the Package dialog: what the package carries, then the folder.
let Some(dir) = p.get("dir").and_then(Value::as_str) else {
let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default();
let p = p.clone();
return Some(app.ask_save(&name, Bound::Document, move |app, dir| {
run_ui(app, "app.packageDialog", &with_path(&p, "dir", dir)).unwrap_or(Ok(Value::Null))
}));
return Some(crate::package_dialog::open(app).map(|()| Value::Null));
};
let r = app.run("file.package", json!({"dir": dir}));
if let Ok(v) = &r {
+6 -1
View File
@@ -35,7 +35,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"), "{dir?} — choose the package folder to create (file.package)"),
(
"app.packageDialog",
"Package…",
Some("Cmd+Alt+Shift+P"),
"{dir?} — the Package dialog (what the package carries, printing instructions), then the folder to create; with dir, packages into it at once (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"),
+622
View File
@@ -0,0 +1,622 @@
//! 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::app::file_dialogs::Bound;
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) -> Result<(), String> {
if app.session.active().is_none() {
return Err("no document open".into());
}
let fonts: Vec<Value> = app
.session
.execute("font.list", &json!({}))
.ok()
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.map(|f| {
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})
})
.collect();
let links: Vec<Value> = 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<Value> = 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();
// The packaged PDF's preset: the one last used for Export PDF, as InDesign suggests it.
let last = app.ui.pdf_export.preset.clone();
let preset = crate::dialogs::pdf_presets().find(|p| *p == last).unwrap_or("Desktop Printing");
let doc = app.session.active().map(|d| d.uid);
app.ui.dialog = Some(Dialog::new(
ID,
json!({"page": "summary", "doc": doc, "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false,
"pdfPreset": preset, "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()
}
/// 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} in use, {missing} unavailable")
.replace("{used}", &fonts.len().to_string())
.replace("{missing}", &missing_fonts.to_string()),
missing_fonts > 0,
),
(
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("Embedded images: {embedded}").replace("{embedded}", &count(&links, &["embedded"]).to_string()), false),
(
tr("Colors and Inks: {process} process, {spot} spot")
.replace("{process}", &PROCESS_INKS.len().to_string())
.replace("{spot}", &rows(d, "spot").len().to_string()),
false,
),
]
}
/// How a status is shown: coloured with the theme's warning or error colour.
#[derive(Clone, Copy, PartialEq, Debug)]
enum Level {
Warn,
Bad,
}
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(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 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| {
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.color(ui));
}
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)));
// 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| {
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(Level::Warn.color(ui));
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));
}
}
// The PDF's preset, as InDesign's Select PDF Preset.
ui.add_enabled_ui(d.b("pdf"), |ui| {
ui.horizontal(|ui| {
ui.add_space(24.0);
let name = crate::rtl::label(ui, tr("PDF Preset:"));
let cur = d.s("pdfPreset");
craft_ui::ComboBox::from_id_salt("package_pdf_preset")
.selected_text(crate::rtl::widget(ui, tr(&cur)))
.width(180.0)
.show_ui(ui, |ui| {
for p in crate::dialogs::pdf_presets() {
if ui.selectable_label(cur == p, crate::rtl::widget(ui, tr(p))).clicked() {
d.fields.insert("pdfPreset".into(), json!(p));
}
}
})
.response
.labelled_by(name.id);
});
});
}
}
if matches!(page.as_str(), "fonts" | "links") {
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));
}
}
});
});
}
/// 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> {
same_document(app, d)?;
let (idml, pdf) = (d.b("idml"), pdf_param(d));
if d.b("instructions") {
app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf, "doc": d.fields.get("doc")})));
return Ok(Value::Null);
}
package_to_folder(app, idml, pdf, None, d.clone())
}
/// 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 {
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) };
// 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));
}
// Enter there makes a new line: the footer mustn't take it for Continue.
if lines > 1 && resp.has_focus() {
crate::dialogs::set_multiline_focus(ui.ctx());
}
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<Value, String> {
same_document(app, d)?;
let text = instructions_text(d);
package_to_folder(app, d.b("idml"), d.fields.get("pdf").cloned().unwrap_or(json!(false)), Some(text).filter(|t| !t.is_empty()), d.clone())
}
/// Ask for the folder to create ("<title> Folder") and package the document into it once it is
/// chosen (Cancel packages nothing). Packaging that fails reopens `asking`, the dialog that asked,
/// with its options. `pdf`:
/// false, true or the PDF's `file.exportPdf` options.
pub fn package_to_folder(app: &mut DesignApp, idml: bool, pdf: Value, instructions: Option<String>, asking: Dialog) -> Result<Value, String> {
let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default();
app.ask_save(&name, Bound::Document, move |app, dir| {
let mut p = json!({"dir": dir, "idml": idml, "pdf": pdf});
if let Some(t) = instructions {
p["instructions"] = json!(t);
}
let r = app.run("file.package", p);
match &r {
Ok(v) => 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("")),
),
Err(_) if app.ui.dialog.is_none() => app.ui.dialog = Some(asking),
Err(_) => {}
}
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::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));
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.answer_dialogs(crate::app::file_dialogs::testing::answer_with(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 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} 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);
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);
h.state_mut().settle_dialogs();
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);
h.state_mut().settle_dialogs();
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, 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");
let mut app = app_with_missing_link(&dir);
let out = dir.join("Packed Folder");
let picked = out.to_string_lossy().to_string();
app.answer_dialogs(crate::app::file_dialogs::testing::answer_with(Some(picked.clone())));
open(&mut app).unwrap();
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);
// The PDF's preset: PDF/X-4.
h.get_by_value("Desktop Printing").click();
h.run_steps(2);
h.get_by_label("PDF/X-4").click();
h.run_steps(2);
h.get_by_label("Package…").click();
h.run_steps(3);
h.state_mut().settle_dialogs();
assert!(h.state().ui.dialog.is_none());
assert!(out.join("Packed.pdf").exists() && !out.join("Packed.idml").exists());
let pdf = std::fs::read(out.join("Packed.pdf")).unwrap();
assert!(pdf.windows(8).any(|w| w == b"GTS_PDFX"), "the PDF/X-4 preset's PDF");
std::fs::remove_dir_all(dir).unwrap();
}
/// 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.answer_dialogs(crate::app::file_dialogs::testing::answer_with(Some(picked.clone())));
open(&mut app).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("pdf".into(), json!(true));
crate::dialogs::confirm(&mut app).unwrap();
// The folder is chosen on a later frame; packaging into it fails then.
app.settle_dialogs();
let d = app.ui.dialog.as_ref().expect("the dialog opens again");
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();
}
/// 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.answer_dialogs(crate::app::file_dialogs::testing::answer_with(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();
}
}
+6
View File
@@ -76,6 +76,12 @@ number, separator, sorting and level controls. Toggling visibility preserves the
## 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