mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
PDF export: options dialog with remembered settings (#196)
* PDF export: Export Adobe PDF options dialog with remembered settings File > Export PDF... now opens an options dialog (General / Compression / Marks and Bleeds / Advanced) that exposes only the parameters the engine's file.exportPdf already supports: PDF standard, image compression, flattener preset, spreads, document bleed, crop/bleed/page-info marks with weight and offset, tagged PDF, page range, and the title. Choosing a preset fills the dependent fields; editing any field switches the preset to Custom. The last-used settings persist in ui.json (UiState.pdfExport) and are reused across exports and sessions. Programmatic callers that pass explicit params (path, options) still export directly without the dialog; cancelling the save picker writes nothing. Adds English and ar/pt-br strings, unit tests for the settings serde, the dialog defaults and preset mapping, the open/confirm flow (stub writer), and the menu dispatch, plus a ROADMAP note. * PDF export: rename dialog title to "Export PDF" --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
8 files changed
+567
-4
No files matched your search
+1
-1
@@ -19,7 +19,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- UI (egui): application bar + menus, Control panel (object and text modes), Tools panel with flyouts, document tabs, rulers, pasteboard, guides (margins, columns, bleed, baseline grid), frame edges, selection handles, text ports and threads, Properties / Pages / Layers dock, Swatches, Styles, Character, Paragraph, Stroke, Text Wrap panels, New Document and Preferences dialogs, Quick Apply (⌘Return), 4 brightness themes.
|
||||
- Pen tool, Direct Selection anchor/handle editing, rotate from corners, Rotate/Scale/Shear tools, Eyedropper, snapping & smart guides, Align/Distribute, Find/Change (text + GREP), Story Editor, Paragraph Style Options, Effects panel, hidden characters, native macOS menu bar, measured InDesign 2026 look (Medium Dark): Properties sections per selection state, 21 pt spinner fields with arithmetic, Contextual Task Bar, inverse (black) text selection, Pages panel with drag-and-drop reorder / parent apply, Layers panel with per-object rows.
|
||||
- Tools: Selection (click, marquee, move, Alt-duplicate, resize handles), Direct Selection, Type (draw frame, click caret, select, type), Rectangle/Ellipse/Polygon (+ frame variants), Line, Hand, Zoom.
|
||||
- PDF export (krilla): real selectable text with embedded font subsets, DeviceCMYK/RGB + spot Separations, bleed boxes, crop/bleed marks + page info, pages or spreads, PDF/A-2b (PDF/X-4 output intent pending) — `file.exportPdf`, File › Export PDF…, `designcraft-cli run --export out.pdf`.
|
||||
- PDF export (krilla): real selectable text with embedded font subsets, DeviceCMYK/RGB + spot Separations, bleed boxes, crop/bleed marks + page info, pages or spreads, PDF/A-2b (PDF/X-4 output intent pending) — `file.exportPdf`, File › Export PDF…, `designcraft-cli run --export out.pdf`. File › Export PDF… now opens an "Export PDF" options dialog (General / Compression / Marks and Bleeds / Advanced) and remembers the last-used settings across exports and sessions.
|
||||
- IDML interchange (`designcraft-idml`): export and import of swatches, styles, fonts, preferences, parent spreads, spreads/pages, frames, groups, images (embedded or linked), formatted threaded stories — opens in InDesign 2026 and round-trips InDesign-exported files (`file.exportIdml`, `file.openIdml`, File → Export IDML…, CLI `--in x.idml` / `--export x.idml`).
|
||||
- Tables (M8): tables anchored in stories (header/footer/body rows, merged cells, per-edge strokes, fills + alternating fills, insets, vertical justification, at-least/exact row heights); composed into the text column (columns scale to fit, rows break across columns/frames with repeating headers/footers, overset); rendered, exported to PDF (real text) and IDML (export + import); Table menu, Table panel, Create Table dialog, caret/typing/Tab navigation and cell selection in cells; `table.*` commands.
|
||||
- Hyperlinks (text or frames → URL / e-mail / page) and bookmarks, exported as PDF link annotations and outline.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! File › Export: Adobe PDF (Print).
|
||||
//! File › Export PDF (Print).
|
||||
|
||||
use designcraft_pdf::{Marks, PdfOptions, Standard};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -31,6 +31,22 @@ impl Dialog {
|
||||
"insertXref" => json!({"linkTo": "paragraph", "style": "", "target": "", "format": ""}),
|
||||
"findChange" => json!({"find": "", "change": "", "grep": false, "caseSensitive": false, "wholeWord": false, "scope": "document"}),
|
||||
"documentSetup" => json!({}),
|
||||
"pdfExport" => {
|
||||
json!({
|
||||
"preset": "Desktop Printing",
|
||||
"standard": "none",
|
||||
"compressImages": false,
|
||||
"flatten": "",
|
||||
"spreads": false,
|
||||
"bleed": true,
|
||||
"marksCrop": false,
|
||||
"marksBleed": false,
|
||||
"marksPageInfo": false,
|
||||
"marksWeight": "0.25",
|
||||
"marksOffset": "6",
|
||||
"tagged": true
|
||||
})
|
||||
}
|
||||
_ => json!({}),
|
||||
};
|
||||
if let Value::Object(d) = defaults {
|
||||
@@ -235,6 +251,22 @@ pub fn open_print(app: &mut DesignApp) {
|
||||
));
|
||||
}
|
||||
|
||||
/// File › Export PDF…: the "Export PDF" options dialog, seeded from the persisted settings
|
||||
/// (`UiState.pdf_export`) and the active document. `pages` is per-job (not persisted); `flatten`
|
||||
/// falls back to the global flattener preset when the persisted value is empty.
|
||||
pub fn open_pdf_export(app: &mut DesignApp) {
|
||||
let mut fields = serde_json::to_value(&app.ui.pdf_export).unwrap_or_else(|_| json!({}));
|
||||
if let Value::Object(m) = &mut fields {
|
||||
let title = app.session.active().map(|st| st.doc.title.clone()).unwrap_or_default();
|
||||
m.insert("title".into(), json!(title));
|
||||
m.insert("pages".into(), json!("all"));
|
||||
if app.ui.pdf_export.flatten.is_empty() {
|
||||
m.insert("flatten".into(), json!(app.ui.flattener));
|
||||
}
|
||||
}
|
||||
app.ui.dialog = Some(Dialog::new("pdfExport", fields));
|
||||
}
|
||||
|
||||
fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let printers: Vec<String> = d
|
||||
.fields
|
||||
@@ -284,6 +316,99 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The presets offered by the Export PDF dialog, in display order. The stored value is the
|
||||
/// English name; the caption is translated.
|
||||
const PDF_EXPORT_PRESETS: &[&str] = &["Desktop Printing", "Commercial Printing", "Screen and Email", "PDF/X-4", "PDF/A-2b", "Custom"];
|
||||
|
||||
/// Apply a preset's dependent fields. Flatten is only touched by "Screen and Email"; "Custom"
|
||||
/// (and any unknown value) changes nothing.
|
||||
fn apply_pdf_preset(d: &mut Dialog, preset: &str) {
|
||||
let (standard, compress, crop, bleed, page_info, flatten) = match preset {
|
||||
"Desktop Printing" => ("none", false, false, false, false, None),
|
||||
"Commercial Printing" => ("none", true, true, true, false, None),
|
||||
"Screen and Email" => ("none", true, false, false, false, Some("low")),
|
||||
"PDF/X-4" => ("x4", false, true, true, false, None),
|
||||
"PDF/A-2b" => ("a2b", false, false, false, false, None),
|
||||
_ => return,
|
||||
};
|
||||
d.fields.insert("standard".into(), json!(standard));
|
||||
d.fields.insert("compressImages".into(), json!(compress));
|
||||
d.fields.insert("marksCrop".into(), json!(crop));
|
||||
d.fields.insert("marksBleed".into(), json!(bleed));
|
||||
d.fields.insert("marksPageInfo".into(), json!(page_info));
|
||||
if let Some(f) = flatten {
|
||||
d.fields.insert("flatten".into(), json!(f));
|
||||
}
|
||||
}
|
||||
|
||||
/// File › Export PDF: the "Export PDF" options (General / Compression / Marks and Bleeds /
|
||||
/// Advanced). Selecting a preset fills the dependent fields; editing any other control flips the
|
||||
/// preset back to "Custom".
|
||||
fn pdf_export(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let lang = app.ui.language.as_str();
|
||||
let head = |ui: &mut egui::Ui, t: &str| {
|
||||
ui.add_space(6.0);
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, t)).font(semibold(12.0)));
|
||||
};
|
||||
// Snapshot the non-preset fields so a manual edit can be detected after drawing.
|
||||
let before = d.fields.clone();
|
||||
let mut preset_clicked = false;
|
||||
|
||||
head(ui, "General");
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Preset:"));
|
||||
let cur = d.s("preset");
|
||||
egui::ComboBox::from_id_salt("pdf_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, &cur))).width(200.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for p in PDF_EXPORT_PRESETS {
|
||||
if ui.selectable_label(cur == *p, crate::rtl::widget(ui, crate::i18n::tr(lang, p))).clicked() {
|
||||
d.fields.insert("preset".into(), json!(*p));
|
||||
preset_clicked = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
if preset_clicked {
|
||||
let p = d.s("preset");
|
||||
apply_pdf_preset(d, &p);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Page Range:"));
|
||||
text_field(ui, d, "pages", 160.0);
|
||||
});
|
||||
check(ui, d, "spreads", crate::i18n::tr(lang, "Spreads"));
|
||||
|
||||
head(ui, "Compression");
|
||||
check(ui, d, "compressImages", crate::i18n::tr(lang, "Compress images"));
|
||||
|
||||
head(ui, "Marks and Bleeds");
|
||||
check(ui, d, "bleed", crate::i18n::tr(lang, "Include document bleed"));
|
||||
check(ui, d, "marksCrop", crate::i18n::tr(lang, "Crop Marks"));
|
||||
check(ui, d, "marksBleed", crate::i18n::tr(lang, "Bleed Marks"));
|
||||
check(ui, d, "marksPageInfo", crate::i18n::tr(lang, "Page Information"));
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Weight:"));
|
||||
text_field(ui, d, "marksWeight", 60.0);
|
||||
ui.add_space(8.0);
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Offset:"));
|
||||
text_field(ui, d, "marksOffset", 60.0);
|
||||
});
|
||||
|
||||
head(ui, "Advanced");
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Standard:"));
|
||||
translated_combo(lang, ui, d, "standard", &[("none", "None"), ("x4", "PDF/X-4"), ("a2b", "PDF/A-2b")]);
|
||||
});
|
||||
check(ui, d, "tagged", crate::i18n::tr(lang, "Tagged PDF"));
|
||||
|
||||
// Any edit other than picking a preset means the settings are no longer that preset.
|
||||
if !preset_clicked && d.fields != before {
|
||||
d.fields.insert("preset".into(), json!("Custom"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The options of the selected text frame (the first selected item, or the frame of the caret).
|
||||
fn selected_frame_options(app: &DesignApp) -> Option<designcraft_doc::TextFrameOptions> {
|
||||
let st = app.session.active()?;
|
||||
@@ -1020,6 +1145,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
"preferences" => crate::i18n::tr(&app.ui.language, "Preferences"),
|
||||
"print" => crate::i18n::tr(&app.ui.language, "Print"),
|
||||
"pdfImport" => crate::i18n::tr(&app.ui.language, "Place PDF"),
|
||||
"pdfExport" => crate::i18n::tr(&app.ui.language, "Export PDF"),
|
||||
"colorSettings" => crate::i18n::tr(&app.ui.language, "Color Settings"),
|
||||
"layerOptions" => crate::i18n::tr(&app.ui.language, "Object Layer Options"),
|
||||
"keyboardShortcuts" => crate::i18n::tr(&app.ui.language, "Keyboard Shortcuts"),
|
||||
@@ -1564,6 +1690,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
text_frame_options_dialog(app, ui, &mut d);
|
||||
}
|
||||
"documentSetup" => document_setup(app, ui, &mut d),
|
||||
"pdfExport" => pdf_export(app, ui, &mut d),
|
||||
"alert" => {
|
||||
let file = d.s("file");
|
||||
if !file.is_empty() {
|
||||
@@ -1807,6 +1934,57 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
app.status(format!("Sent {} page(s) to {}", r["pages"], r["printer"].as_str().unwrap_or("the default printer")));
|
||||
Ok(r)
|
||||
}
|
||||
"pdfExport" => {
|
||||
// Remember the last-used options; `pages` and `title` are per-job and not persisted.
|
||||
app.ui.pdf_export.preset = d.s("preset");
|
||||
app.ui.pdf_export.standard = d.s("standard");
|
||||
app.ui.pdf_export.compress_images = d.b("compressImages");
|
||||
app.ui.pdf_export.flatten = d.s("flatten");
|
||||
app.ui.pdf_export.spreads = d.b("spreads");
|
||||
app.ui.pdf_export.bleed = d.b("bleed");
|
||||
app.ui.pdf_export.marks_crop = d.b("marksCrop");
|
||||
app.ui.pdf_export.marks_bleed = d.b("marksBleed");
|
||||
app.ui.pdf_export.marks_page_info = d.b("marksPageInfo");
|
||||
app.ui.pdf_export.marks_weight = d.s("marksWeight");
|
||||
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 pages = d.s("pages");
|
||||
if !pages.trim().is_empty() && pages != "all" {
|
||||
params["pages"] = json!(pages);
|
||||
}
|
||||
let title = d.s("title");
|
||||
if !title.is_empty() {
|
||||
params["title"] = json!(title);
|
||||
}
|
||||
let name = if title.is_empty() { "Export.pdf".to_string() } else { format!("{title}.pdf") };
|
||||
match app.services.pick_save.as_mut().and_then(|f| f(&name)) {
|
||||
Some(path) => {
|
||||
params["path"] = json!(path);
|
||||
crate::menus::export_pdf(app, ¶ms)
|
||||
}
|
||||
// The user cancelled the save dialog: export nothing.
|
||||
None => Ok(Value::Null),
|
||||
}
|
||||
}
|
||||
"preferences" => {
|
||||
app.run(
|
||||
"prefs.set",
|
||||
@@ -4033,6 +4211,207 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_defaults() {
|
||||
let d = Dialog::new("pdfExport", json!({}));
|
||||
let keys: Vec<&str> = d.fields.iter().map(|(k, _)| k.as_str()).collect();
|
||||
// serde_json::Map keeps keys sorted.
|
||||
let expected = [
|
||||
"bleed",
|
||||
"compressImages",
|
||||
"flatten",
|
||||
"marksBleed",
|
||||
"marksCrop",
|
||||
"marksOffset",
|
||||
"marksPageInfo",
|
||||
"marksWeight",
|
||||
"preset",
|
||||
"spreads",
|
||||
"standard",
|
||||
"tagged",
|
||||
];
|
||||
assert_eq!(keys, expected);
|
||||
assert_eq!(d.fields["preset"].as_str(), Some("Desktop Printing"));
|
||||
assert_eq!(d.fields["standard"].as_str(), Some("none"));
|
||||
assert_eq!(d.fields["compressImages"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["flatten"].as_str(), Some(""));
|
||||
assert_eq!(d.fields["spreads"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["bleed"].as_bool(), Some(true));
|
||||
assert_eq!(d.fields["marksCrop"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["marksBleed"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["marksPageInfo"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["marksWeight"].as_str(), Some("0.25"));
|
||||
assert_eq!(d.fields["marksOffset"].as_str(), Some("6"));
|
||||
assert_eq!(d.fields["tagged"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_override_keeps_override() {
|
||||
let d = Dialog::new("pdfExport", json!({"bleed": false}));
|
||||
assert_eq!(d.fields["bleed"].as_bool(), Some(false));
|
||||
assert_eq!(d.fields["preset"].as_str(), Some("Desktop Printing"));
|
||||
assert_eq!(d.fields["tagged"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_high_quality_print_keeps_flatten() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
d.fields.insert("flatten".into(), json!("medium"));
|
||||
apply_pdf_preset(&mut d, "Desktop Printing");
|
||||
assert_eq!(d.s("standard"), "none");
|
||||
assert!(!d.b("compressImages"));
|
||||
assert!(!d.b("marksCrop"));
|
||||
assert!(!d.b("marksBleed"));
|
||||
assert!(!d.b("marksPageInfo"));
|
||||
assert_eq!(d.s("flatten"), "medium", "a leave-unchanged preset must not touch flatten");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_press_quality_keeps_flatten() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
d.fields.insert("flatten".into(), json!("high"));
|
||||
apply_pdf_preset(&mut d, "Commercial Printing");
|
||||
assert_eq!(d.s("standard"), "none");
|
||||
assert!(d.b("compressImages"));
|
||||
assert!(d.b("marksCrop"));
|
||||
assert!(d.b("marksBleed"));
|
||||
assert!(!d.b("marksPageInfo"));
|
||||
assert_eq!(d.s("flatten"), "high");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_smallest_file_size_sets_flatten_low() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
apply_pdf_preset(&mut d, "Screen and Email");
|
||||
assert_eq!(d.s("standard"), "none");
|
||||
assert!(d.b("compressImages"));
|
||||
assert!(!d.b("marksCrop"));
|
||||
assert!(!d.b("marksBleed"));
|
||||
assert!(!d.b("marksPageInfo"));
|
||||
assert_eq!(d.s("flatten"), "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_pdf_x4_keeps_flatten() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
d.fields.insert("flatten".into(), json!("medium"));
|
||||
apply_pdf_preset(&mut d, "PDF/X-4");
|
||||
assert_eq!(d.s("standard"), "x4");
|
||||
assert!(!d.b("compressImages"));
|
||||
assert!(d.b("marksCrop"));
|
||||
assert!(d.b("marksBleed"));
|
||||
assert!(!d.b("marksPageInfo"));
|
||||
assert_eq!(d.s("flatten"), "medium");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_pdf_a2b_keeps_flatten() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
d.fields.insert("flatten".into(), json!("low"));
|
||||
apply_pdf_preset(&mut d, "PDF/A-2b");
|
||||
assert_eq!(d.s("standard"), "a2b");
|
||||
assert!(!d.b("compressImages"));
|
||||
assert!(!d.b("marksCrop"));
|
||||
assert!(!d.b("marksBleed"));
|
||||
assert!(!d.b("marksPageInfo"));
|
||||
assert_eq!(d.s("flatten"), "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_pdf_export_seeds_fields_from_settings() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.ui.pdf_export.standard = "x4".into();
|
||||
app.ui.pdf_export.tagged = false;
|
||||
app.ui.pdf_export.marks_crop = true;
|
||||
app.ui.pdf_export.flatten = String::new();
|
||||
app.ui.flattener = "medium".into();
|
||||
open_pdf_export(&mut app);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.id, "pdfExport");
|
||||
assert_eq!(d.s("standard"), "x4");
|
||||
assert!(!d.b("tagged"));
|
||||
assert!(d.b("marksCrop"));
|
||||
assert_eq!(d.s("pages"), "all");
|
||||
assert_eq!(d.s("flatten"), "medium", "empty flatten falls back to the global flattener");
|
||||
assert_eq!(d.s("title"), "", "no document → empty title");
|
||||
|
||||
// A non-empty persisted flatten is used verbatim, not the global fallback.
|
||||
app.ui.pdf_export.flatten = "low".into();
|
||||
app.ui.flattener = "medium".into();
|
||||
open_pdf_export(&mut app);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.s("flatten"), "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_preset_custom_changes_nothing() {
|
||||
let mut d = Dialog::new("pdfExport", json!({}));
|
||||
d.fields.insert("standard".into(), json!("x4"));
|
||||
d.fields.insert("compressImages".into(), json!(true));
|
||||
d.fields.insert("marksCrop".into(), json!(true));
|
||||
d.fields.insert("marksBleed".into(), json!(true));
|
||||
d.fields.insert("marksPageInfo".into(), json!(true));
|
||||
d.fields.insert("flatten".into(), json!("high"));
|
||||
let before = d.fields.clone();
|
||||
apply_pdf_preset(&mut d, "Custom");
|
||||
assert_eq!(d.fields, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_pdf_export_writes_file_and_persists_settings() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
let writes: Arc<Mutex<Vec<(String, Vec<u8>)>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let sink = writes.clone();
|
||||
app.services.write = Some(Box::new(move |path: &str, bytes: &[u8]| {
|
||||
sink.lock().unwrap().push((path.to_string(), bytes.to_vec()));
|
||||
Ok(())
|
||||
}));
|
||||
app.services.pick_save = Some(Box::new(|_name: &str| Some("/tmp/designcraft-test.pdf".to_string())));
|
||||
|
||||
app.ui.dialog = Some(Dialog::new("pdfExport", json!({})));
|
||||
{
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert("standard".into(), json!("x4"));
|
||||
d.fields.insert("bleed".into(), json!(false));
|
||||
d.fields.insert("marksCrop".into(), json!(true));
|
||||
d.fields.insert("tagged".into(), json!(false));
|
||||
d.fields.insert("title".into(), json!("Report"));
|
||||
}
|
||||
confirm(&mut app).unwrap();
|
||||
|
||||
let got = writes.lock().unwrap();
|
||||
assert_eq!(got.len(), 1, "confirming the dialog writes exactly one file");
|
||||
assert_eq!(got[0].0, "/tmp/designcraft-test.pdf");
|
||||
assert!(got[0].1.starts_with(b"%PDF"), "the written bytes are a PDF");
|
||||
drop(got);
|
||||
|
||||
assert_eq!(app.ui.pdf_export.standard, "x4");
|
||||
assert!(!app.ui.pdf_export.bleed);
|
||||
assert!(app.ui.pdf_export.marks_crop);
|
||||
assert!(!app.ui.pdf_export.tagged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_pdf_export_cancelled_save_writes_nothing() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
let writes: Arc<Mutex<Vec<(String, Vec<u8>)>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let sink = writes.clone();
|
||||
app.services.write = Some(Box::new(move |path: &str, bytes: &[u8]| {
|
||||
sink.lock().unwrap().push((path.to_string(), bytes.to_vec()));
|
||||
Ok(())
|
||||
}));
|
||||
app.services.pick_save = Some(Box::new(|_name: &str| None));
|
||||
|
||||
app.ui.dialog = Some(Dialog::new("pdfExport", json!({})));
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r, Value::Null);
|
||||
assert!(writes.lock().unwrap().is_empty(), "cancelling the save picker must not write");
|
||||
}
|
||||
|
||||
use designcraft_doc::{CharAttrs, CharacterStyle};
|
||||
|
||||
/// A document with character styles Base (bold) and Emphasis (italic) on the first two letters.
|
||||
|
||||
@@ -229,6 +229,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Art", ["Art", "Art", "Art", "Art", "画板"]),
|
||||
("Based On:", ["Based On:", "Based On:", "Based On:", "Based On:", "基于:"]),
|
||||
("Bleed", ["Bleed", "Bleed", "Bleed", "Bleed", "出血"]),
|
||||
("Bleed Marks", ["Beschnittmarken", "Traits de fond perdu", "Marcas de sangrado", "塗り足しマーク", "出血标记"]),
|
||||
("Body Rows", ["Body Rows", "Body Rows", "Body Rows", "Body Rows", "表体行"]),
|
||||
("Bottom", ["Bottom", "Bottom", "Bottom", "Bottom", "底部"]),
|
||||
("Break Link to Style", ["Break Link to Style", "Break Link to Style", "Break Link to Style", "Break Link to Style", "断开与样式的链接"]),
|
||||
@@ -275,6 +276,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Create Table", ["Create Table", "Create Table", "Create Table", "Create Table", "创建表"]),
|
||||
("Create Table…", ["Create Table…", "Create Table…", "Create Table…", "Create Table…", "创建表…"]),
|
||||
("Crop", ["Crop", "Crop", "Crop", "Crop", "裁剪"]),
|
||||
("Crop Marks", ["Schnittmarken", "Traits de coupe", "Marcas de recorte", "トンボ", "裁切标记"]),
|
||||
("Crop to:", ["Crop to:", "Crop to:", "Crop to:", "Crop to:", "裁剪到:"]),
|
||||
("Cursor Key:", ["Cursor Key:", "Cursor Key:", "Cursor Key:", "Cursor Key:", "光标键:"]),
|
||||
("Default", ["Default", "Default", "Default", "Default", "默认"]),
|
||||
@@ -294,6 +296,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Duplicate Spread", ["Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "复制跨页"]),
|
||||
("Email", ["Email", "Email", "Email", "Email", "电子邮件"]),
|
||||
("Embed", ["Embed", "Embed", "Embed", "Embed", "嵌入"]),
|
||||
("Export PDF", ["PDF exportieren", "Exporter en PDF", "Exportar PDF", "PDF を書き出し", "导出PDF"]),
|
||||
("Export XML…", ["Export XML…", "Export XML…", "Export XML…", "Export XML…", "导出XML…"]),
|
||||
("Fast", ["Fast", "Fast", "Fast", "Fast", "快速"]),
|
||||
(
|
||||
@@ -311,6 +314,10 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
),
|
||||
("Fit Content to Frame", ["Fit Content to Frame", "Fit Content to Frame", "Fit Content to Frame", "Fit Content to Frame", "内容适合框架"]),
|
||||
("Fitting:", ["Fitting:", "Fitting:", "Fitting:", "Fitting:", "适合:"]),
|
||||
(
|
||||
"Flattener preset",
|
||||
["Voreinstellung für Transparenzreduzierung", "Préréglage d'aplatissement", "Preajuste de acoplado", "透明分割プリセット", "拼合器预设"],
|
||||
),
|
||||
("Font Family:", ["Font Family:", "Font Family:", "Font Family:", "Font Family:", "字体系列:"]),
|
||||
("Font Size:", ["Font Size:", "Font Size:", "Font Size:", "Font Size:", "字体大小:"]),
|
||||
("Font Style:", ["Font Style:", "Font Style:", "Font Style:", "Font Style:", "字体样式:"]),
|
||||
@@ -328,6 +335,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Height", ["Height", "Height", "Height", "Height", "高度"]),
|
||||
("Height:", ["Height:", "Height:", "Height:", "Height:", "高度:"]),
|
||||
("High Quality", ["High Quality", "High Quality", "High Quality", "High Quality", "高品质"]),
|
||||
("Desktop Printing", ["Bürodruck", "Impression bureautique", "Impresión de oficina", "オフィス印刷", "办公打印"]),
|
||||
(
|
||||
"Horizontal Gridline Every:",
|
||||
["Horizontal Gridline Every:", "Horizontal Gridline Every:", "Horizontal Gridline Every:", "Horizontal Gridline Every:", "水平网格线间隔:"],
|
||||
@@ -336,6 +344,16 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Hyphenate", ["Hyphenate", "Hyphenate", "Hyphenate", "Hyphenate", "连字符"]),
|
||||
("Import Options", ["Import Options", "Import Options", "Import Options", "Import Options", "导入选项"]),
|
||||
("Import XML…", ["Import XML…", "Import XML…", "Import XML…", "Import XML…", "导入XML…"]),
|
||||
(
|
||||
"Include document bleed",
|
||||
[
|
||||
"Dokumentbeschnitt einschließen",
|
||||
"Inclure le fond perdu du document",
|
||||
"Incluir sangrado del documento",
|
||||
"ドキュメントの塗り足しを含める",
|
||||
"包含文档出血",
|
||||
],
|
||||
),
|
||||
("Increment Every:", ["Increment Every:", "Increment Every:", "Increment Every:", "Increment Every:", "递增步长:"]),
|
||||
("Ink Manager", ["Ink Manager", "Ink Manager", "Ink Manager", "Ink Manager", "油墨管理器"]),
|
||||
("Insert Column", ["Insert Column", "Insert Column", "Insert Column", "Insert Column", "插入列"]),
|
||||
@@ -355,6 +373,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Load…", ["Load…", "Load…", "Load…", "Load…", "加载…"]),
|
||||
("Make Compound", ["Make Compound", "Make Compound", "Make Compound", "Make Compound", "建立复合路径"]),
|
||||
("Margins", ["Margins", "Margins", "Margins", "Margins", "边距"]),
|
||||
("Marks and Bleeds", ["Marken und Beschnitt", "Traits et fond perdu", "Marcas y sangrado", "トンボと塗り足し", "标记和出血"]),
|
||||
("Maximum", ["Maximum", "Maximum", "Maximum", "Maximum", "最大"]),
|
||||
("Menu Customization", ["Menu Customization", "Menu Customization", "Menu Customization", "Menu Customization", "菜单自定"]),
|
||||
(
|
||||
@@ -425,6 +444,9 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("OpenType", ["OpenType", "OpenType", "OpenType", "OpenType", "OpenType"]),
|
||||
("Orientation:", ["Orientation:", "Orientation:", "Orientation:", "Orientation:", "方向:"]),
|
||||
("Outside", ["Outside", "Outside", "Outside", "Outside", "外侧"]),
|
||||
("Page Information", ["Seiteninformationen", "Informations sur la page", "Información de página", "ページ情報", "页面信息"]),
|
||||
("Page Range", ["Seitenbereich", "Plage de pages", "Intervalo de páginas", "ページ範囲", "页面范围"]),
|
||||
("Page Range:", ["Seitenbereich:", "Plage de pages :", "Intervalo de páginas:", "ページ範囲:", "页面范围:"]),
|
||||
("Pages:", ["Pages:", "Pages:", "Pages:", "Pages:", "页面:"]),
|
||||
("Paragraph Composer", ["Paragraph Composer", "Paragraph Composer", "Paragraph Composer", "Paragraph Composer", "段落排版器"]),
|
||||
(
|
||||
@@ -432,6 +454,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
["Paragraph Style Options", "Paragraph Style Options", "Paragraph Style Options", "Paragraph Style Options", "段落样式选项"],
|
||||
),
|
||||
("Paragraph Style:", ["Paragraph Style:", "Paragraph Style:", "Paragraph Style:", "Paragraph Style:", "段落样式:"]),
|
||||
("PDF/A-2b", ["PDF/A-2b", "PDF/A-2b", "PDF/A-2b", "PDF/A-2b", "PDF/A-2b"]),
|
||||
("PDF/X-4", ["PDF/X-4", "PDF/X-4", "PDF/X-4", "PDF/X-4", "PDF/X-4"]),
|
||||
("Place PDF", ["Place PDF", "Place PDF", "Place PDF", "Place PDF", "置入PDF"]),
|
||||
("Plain Text", ["Plain Text", "Plain Text", "Plain Text", "Plain Text", "纯文本"]),
|
||||
("Polygon Settings", ["Polygon Settings", "Polygon Settings", "Polygon Settings", "Polygon Settings", "多边形设置"]),
|
||||
@@ -441,6 +465,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Preferences", ["Preferences", "Preferences", "Preferences", "Preferences", "首选项"]),
|
||||
("Prefix:", ["Prefix:", "Prefix:", "Prefix:", "Prefix:", "前缀:"]),
|
||||
("Preset", ["Preset", "Preset", "Preset", "Preset", "预设"]),
|
||||
("Preset:", ["Voreinstellung:", "Préréglage :", "Preajuste:", "プリセット:", "预设:"]),
|
||||
("Commercial Printing", ["Druckerei", "Impression commerciale", "Impresión comercial", "商業印刷", "商业印刷"]),
|
||||
("Print", ["Print", "Print", "Print", "Print", "打印"]),
|
||||
("Printer:", ["Printer:", "Printer:", "Printer:", "Printer:", "打印机:"]),
|
||||
("Printing / Exporting:", ["Printing / Exporting:", "Printing / Exporting:", "Printing / Exporting:", "Printing / Exporting:", "打印/导出:"]),
|
||||
@@ -508,11 +534,14 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Single-line", ["Single-line", "Single-line", "Single-line", "Single-line", "单行"]),
|
||||
("Single-line Composer", ["Single-line Composer", "Single-line Composer", "Single-line Composer", "Single-line Composer", "单行排版器"]),
|
||||
("Size:", ["Size:", "Size:", "Size:", "Size:", "大小:"]),
|
||||
("Screen and Email", ["Bildschirm und E-Mail", "Écran et e-mail", "Pantalla y correo", "画面とメール", "屏幕和电子邮件"]),
|
||||
(
|
||||
"Space Between Footnotes:",
|
||||
["Space Between Footnotes:", "Space Between Footnotes:", "Space Between Footnotes:", "Space Between Footnotes:", "脚注间距:"],
|
||||
),
|
||||
("Speed", ["Speed", "Speed", "Speed", "Speed", "速度"]),
|
||||
("Standard", ["Standard", "Norme", "Estándar", "標準", "标准"]),
|
||||
("Standard:", ["Standard:", "Norme :", "Estándar:", "標準:", "标准:"]),
|
||||
("Star Inset:", ["Star Inset:", "Star Inset:", "Star Inset:", "Star Inset:", "星形内边距:"]),
|
||||
("Start Page #:", ["Start Page #:", "Start Page #:", "Start Page #:", "Start Page #:", "起始页码:"]),
|
||||
("Start at:", ["Start at:", "Start at:", "Start at:", "Start at:", "起始编号:"]),
|
||||
@@ -524,6 +553,7 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Suffix:", ["Suffix:", "Suffix:", "Suffix:", "Suffix:", "后缀:"]),
|
||||
("Synchronize", ["Synchronize", "Synchronize", "Synchronize", "Synchronize", "同步"]),
|
||||
("Tag:", ["Tag:", "Tag:", "Tag:", "Tag:", "标签:"]),
|
||||
("Tagged PDF", ["Markiertes PDF", "PDF balisé", "PDF etiquetado", "タグ付き PDF", "带标签的 PDF"]),
|
||||
("Text Frame", ["Text Frame", "Text Frame", "Text Frame", "Text Frame", "文本框架"]),
|
||||
("Text Frame Options", ["Text Frame Options", "Text Frame Options", "Text Frame Options", "Text Frame Options", "文本框架选项"]),
|
||||
("Text Message", ["Text Message", "Text Message", "Text Message", "Text Message", "短信"]),
|
||||
@@ -735,6 +765,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
"Community & Project Links",
|
||||
["Community & Project Links", "Community & Project Links", "Community & Project Links", "Community & Project Links", "社区与项目链接"],
|
||||
),
|
||||
("Compress images", ["Bilder komprimieren", "Compresser les images", "Comprimir imágenes", "画像を圧縮", "压缩图像"]),
|
||||
("Compression", ["Komprimierung", "Compression", "Compresión", "圧縮", "压缩"]),
|
||||
("Conditions", ["Conditions", "Conditions", "Conditions", "Conditions", "条件"]),
|
||||
("Container", ["Container", "Container", "Container", "Container", "容器"]),
|
||||
("Content", ["Content", "Content", "Content", "Content", "内容"]),
|
||||
@@ -3002,4 +3034,19 @@ mod tests {
|
||||
assert_eq!(tr("ja", en), ja);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_dialog_strings_are_translated() {
|
||||
// Translated labels in every supported language.
|
||||
assert_ne!(tr("zh", "Crop Marks"), "Crop Marks");
|
||||
assert_ne!(tr("de", "Tagged PDF"), "Tagged PDF");
|
||||
assert_ne!(tr("ar", "Include document bleed"), "Include document bleed");
|
||||
assert_ne!(tr("pt-br", "Compress images"), "Compress images");
|
||||
// English returns the key unchanged.
|
||||
assert_eq!(tr("", "Export PDF"), "Export PDF");
|
||||
// An unknown string still falls through unchanged.
|
||||
assert_eq!(tr("de", "Low Quality Print"), "Low Quality Print");
|
||||
// Fixed standard names stay identical in every language.
|
||||
assert_eq!(tr("ja", "PDF/X-4"), "PDF/X-4");
|
||||
}
|
||||
}
|
||||
@@ -776,6 +776,26 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Ruby", "روبي"),
|
||||
("Show Note Anchors in Layout View", "إظهار مراسي الملاحظات في عرض التخطيط"),
|
||||
("Spreads", "صفحات متقابلة"),
|
||||
("Export PDF", "تصدير PDF"),
|
||||
("Standard", "معيار"),
|
||||
("Standard:", "المعيار:"),
|
||||
("Compress images", "ضغط الصور"),
|
||||
("Flattener preset", "إعداد تسوية الشفافية المسبق"),
|
||||
("Include document bleed", "تضمين نزف المستند"),
|
||||
("Crop Marks", "علامات القص"),
|
||||
("Bleed Marks", "علامات النزف"),
|
||||
("Page Information", "معلومات الصفحة"),
|
||||
("Page Range", "نطاق الصفحات"),
|
||||
("Page Range:", "نطاق الصفحات:"),
|
||||
("Tagged PDF", "PDF موسوم"),
|
||||
("Desktop Printing", "طباعة مكتبية"),
|
||||
("Commercial Printing", "طباعة تجارية"),
|
||||
("Screen and Email", "الشاشة والبريد الإلكتروني"),
|
||||
("PDF/X-4", "PDF/X-4"),
|
||||
("PDF/A-2b", "PDF/A-2b"),
|
||||
("Compression", "ضغط"),
|
||||
("Marks and Bleeds", "العلامات والنزف"),
|
||||
("Preset:", "الإعداد المسبق:"),
|
||||
("Substituted Fonts", "الخطوط المستبدلة"),
|
||||
("Transformations are Totals", "عرض قيم التحويل الإجمالية"),
|
||||
("Use Black Point Compensation", "استخدام تعويض النقطة السوداء"),
|
||||
|
||||
@@ -778,6 +778,26 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Ruby", "Ruby"),
|
||||
("Show Note Anchors in Layout View", "Mostrar âncoras de notas na exibição de layout"),
|
||||
("Spreads", "Páginas duplas"),
|
||||
("Export PDF", "Exportar PDF"),
|
||||
("Standard", "Padrão"),
|
||||
("Standard:", "Padrão:"),
|
||||
("Compress images", "Compactar imagens"),
|
||||
("Flattener preset", "Predefinição de achatamento"),
|
||||
("Include document bleed", "Incluir sangria do documento"),
|
||||
("Crop Marks", "Marcas de corte"),
|
||||
("Bleed Marks", "Marcas de sangria"),
|
||||
("Page Information", "Informações da página"),
|
||||
("Page Range", "Intervalo de páginas"),
|
||||
("Page Range:", "Intervalo de páginas:"),
|
||||
("Tagged PDF", "PDF marcado"),
|
||||
("Desktop Printing", "Impressão de escritório"),
|
||||
("Commercial Printing", "Impressão comercial"),
|
||||
("Screen and Email", "Tela e e-mail"),
|
||||
("PDF/X-4", "PDF/X-4"),
|
||||
("PDF/A-2b", "PDF/A-2b"),
|
||||
("Compression", "Compactação"),
|
||||
("Marks and Bleeds", "Marcas e sangria"),
|
||||
("Preset:", "Predefinição:"),
|
||||
("Substituted Fonts", "Fontes substituídas"),
|
||||
("Transformations are Totals", "Transformações são totais"),
|
||||
("Use Black Point Compensation", "Usar compensação de ponto preto"),
|
||||
|
||||
@@ -109,6 +109,49 @@ fn default_zone() -> f64 {
|
||||
4.0
|
||||
}
|
||||
|
||||
/// File › Export PDF… options: the values persisted in `UiState.pdf_export` and used as the
|
||||
/// dialog's defaults. Serialised as `pdfExport` with camelCase field names.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct PdfExportSettings {
|
||||
/// Preset (one of the five presets, or "Custom" after a manual edit).
|
||||
pub preset: String,
|
||||
/// PDF standard (`none`, `x4`, `a2b`).
|
||||
pub standard: String,
|
||||
/// Compression › Compress images (JPEG).
|
||||
pub compress_images: bool,
|
||||
/// Transparency flattener preset (`""` = none, `high`, `medium`, `low`).
|
||||
pub flatten: String,
|
||||
pub spreads: bool,
|
||||
pub bleed: bool,
|
||||
pub marks_crop: bool,
|
||||
pub marks_bleed: bool,
|
||||
pub marks_page_info: bool,
|
||||
pub marks_weight: String,
|
||||
pub marks_offset: String,
|
||||
/// File › Export PDF › Advanced › Tagged PDF.
|
||||
pub tagged: bool,
|
||||
}
|
||||
|
||||
impl Default for PdfExportSettings {
|
||||
fn default() -> Self {
|
||||
PdfExportSettings {
|
||||
preset: "Desktop Printing".into(),
|
||||
standard: "none".into(),
|
||||
compress_images: false,
|
||||
flatten: "".into(),
|
||||
spreads: false,
|
||||
bleed: true,
|
||||
marks_crop: false,
|
||||
marks_bleed: false,
|
||||
marks_page_info: false,
|
||||
marks_weight: "0.25".into(),
|
||||
marks_offset: "6".into(),
|
||||
tagged: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persisted UI state.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
@@ -149,6 +192,8 @@ pub struct UiState {
|
||||
pub language: String,
|
||||
/// Edit › Transparency Flattener Presets: "" (none), "high", "medium" or "low" for PDF export.
|
||||
pub flattener: String,
|
||||
/// File › Export PDF… options (persisted; see `PdfExportSettings`).
|
||||
pub pdf_export: PdfExportSettings,
|
||||
/// View › Separations Preview: a process plate (0–3) and/or an ink limit (total, 0–4).
|
||||
#[serde(skip)]
|
||||
pub separation: Option<u8>,
|
||||
@@ -252,6 +297,7 @@ impl Default for UiState {
|
||||
dynamic_spelling: false,
|
||||
language: String::new(),
|
||||
flattener: String::new(),
|
||||
pdf_export: PdfExportSettings::default(),
|
||||
separation: None,
|
||||
ink_limit: None,
|
||||
display_quality: designcraft_render::DisplayQuality::High,
|
||||
@@ -1124,6 +1170,38 @@ mod tests {
|
||||
assert_eq!(ui.snap_view().zone_px, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_settings_round_trip() {
|
||||
let ui = UiState::default();
|
||||
let json = serde_json::to_string(&ui).unwrap();
|
||||
let parsed: UiState = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.pdf_export, PdfExportSettings::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_settings_defaults() {
|
||||
let settings = PdfExportSettings::default();
|
||||
assert_eq!(settings.preset, "Desktop Printing");
|
||||
assert_eq!(settings.standard, "none");
|
||||
assert!(!settings.compress_images);
|
||||
assert_eq!(settings.flatten, "");
|
||||
assert!(!settings.spreads);
|
||||
assert!(settings.bleed);
|
||||
assert!(!settings.marks_crop);
|
||||
assert!(!settings.marks_bleed);
|
||||
assert!(!settings.marks_page_info);
|
||||
assert_eq!(settings.marks_weight, "0.25");
|
||||
assert_eq!(settings.marks_offset, "6");
|
||||
assert!(settings.tagged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_settings_survive_missing_key() {
|
||||
let json = r#"{ "language": "en" }"#;
|
||||
let ui: UiState = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(ui.pdf_export, PdfExportSettings::default());
|
||||
}
|
||||
|
||||
/// A temporary file named `name` whose bytes aren't a document or a graphic.
|
||||
fn unreadable_file(name: &str) -> std::path::PathBuf {
|
||||
let path = std::env::temp_dir().join(format!("designcraft-ui-{}-{name}", std::process::id()));
|
||||
|
||||
@@ -973,7 +973,14 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some("table") { None } else { Some("table".into()) };
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"app.exportPdf" => export_pdf(app, p),
|
||||
"app.exportPdf" => {
|
||||
if p.is_null() || p.as_object().is_none_or(|m| m.is_empty()) {
|
||||
crate::dialogs::open_pdf_export(app);
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
export_pdf(app, p)
|
||||
}
|
||||
}
|
||||
"app.palette" => {
|
||||
app.ui.palette = Some(String::new());
|
||||
Ok(Value::Null)
|
||||
@@ -1531,7 +1538,7 @@ fn export_bytes(app: &mut DesignApp, p: &Value, ext: &str, cmd: &str) -> Result<
|
||||
|
||||
/// File › Export PDF…: ask for a path, export through `file.exportPdf` and write the bytes with the
|
||||
/// platform writer (a download on the web).
|
||||
fn export_pdf(app: &mut DesignApp, p: &Value) -> Result<Value, String> {
|
||||
pub(crate) fn export_pdf(app: &mut DesignApp, p: &Value) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
let path = match p.get("path").and_then(Value::as_str) {
|
||||
Some(s) => Some(s.to_string()),
|
||||
@@ -2347,6 +2354,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_export_menu_item_opens_the_dialog_or_exports() {
|
||||
// No params: the menu item opens the options dialog.
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
activate(&mut app, "app.exportPdf", &Value::Null);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some("pdfExport"));
|
||||
// Explicit options: programmatic callers still export directly, opening no dialog.
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
activate(&mut app, "app.exportPdf", &json!({"path": "/tmp/x.pdf"}));
|
||||
assert!(app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_preferences_are_a_command() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
Reference in new issue
Block a user