diff --git a/ROADMAP.md b/ROADMAP.md index 97b22b499..67ba80ede 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -144,7 +144,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Preferences › Appearance of Black: 100% K on screen accurately (dark grey) or as rich black (`window.richBlack`); the printing/export side is still to come. - Spreads of up to 10 pages: Add to Previous Spread and Allow Spread to Shuffle (Pages panel; `layout.pages.toSpread`, `layout.spreadShuffle`); inserted pages go around kept spreads. - Page tool (⇧P) and `layout.pageSize`: pages of their own size (or a preset); the spine stays put and objects keep their place on their page. -- File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed as PDF through the system spooler (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document). +- File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed through the system spooler as the PDF File › Export PDF writes with the same settings (effects, flattener, print color, warnings) (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document). - Nested styles (through/up to N sentences, words, characters, letters, digits, tabs, breaks, spaces or given characters) and GREP styles (regex → character style), edited in Paragraph Style Options and composed under local formatting; `ui.dialog.open` for agents. Nested line styles are still to come; nested and GREP styles round-trip through IDML. - Table and cell styles (round-trip through IDML): cell styles (fill, insets, vertical justification, strokes, paragraph style) and table styles (region cell styles, border, alternating rows, spacing); apply from the Table panel, edits re-apply to every user; `style.cell.*`, `style.table.*`. - Style groups: paragraph and character styles shown in group folders, Move to Group (new, existing, none); `style.group` renames every use (styles, stories, table cells, footnotes, object styles). @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-20** · Print color (craft-libs#190): CMYK print PDFs blend in DeviceCMYK and carry the CMYK working space as their output intent through craft-pdf's print settings instead of byte rewrites; overprint (Overprint Fill, Stroke and Gap, overprinting text, 100% [Black]) is written as `/op` `/OP` `/OPM 1`; Knockout Group writes `/K true` instead of becoming an image; File › Print prints the PDF Export PDF writes. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 62233c7a6..7b1db1afb 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -706,6 +706,20 @@ fn first_page_ref(pdf: &[u8]) -> Option { } fn export_pdf(s: &mut Session, p: &Value) -> Result { + let r = pdf_export(s, p)?; + match str_param(p, "path") { + Some(path) => { + write_file(path, &r.bytes)?; + Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})) + } + None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})), + } +} + +/// The PDF that File › Export PDF writes for `p`, with its warnings: the flattener, placed PDFs +/// with hidden layers and soft effects as images, the PDF itself and its view options, timings +/// and transitions. File › Print prints it. +pub(crate) fn pdf_export(s: &Session, p: &Value) -> Result { let st = s.doc()?; let opts = options(p, st.doc.page_count())?; // Transparency Flattener presets: High / Medium / Low Resolution, or a ppi. @@ -803,13 +817,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result { { r.warnings.push(format!("transparency flattened: {n} region(s) rasterised")); } - match str_param(p, "path") { - Some(path) => { - write_file(path, &r.bytes)?; - Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})) - } - None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})), - } + Ok(r) } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 4f79f13ad..826bfd6b8 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -5,8 +5,6 @@ use serde_json::{Value, json}; #[cfg(not(target_arch = "wasm32"))] use super::str_param; use super::{CommandSpec, bad, cmd, has_doc}; -#[cfg(not(target_arch = "wasm32"))] -use crate::EngineError; use crate::{Result, Session}; pub fn specs() -> Vec { @@ -17,7 +15,7 @@ pub fn specs() -> Vec { "Print…", ["File"], Some("Cmd+P"), - "{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (export and preview submission without printing)} → {printer, copies, pages, command, bytes}", + "{printer?, copies?: 1, pages?: \"1-3,5\", dryRun?: bool (export and preview submission without printing), …the PDF settings of file.exportPdf (the PDF it writes is what prints)} → {printer, copies, pages, command, bytes, warnings}", has_doc, print ), @@ -58,12 +56,14 @@ fn print_with( ..craft_print::Job::default() }; let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false); - let pdf = designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(d), &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?; - let command = craft_print::command_preview(&job).map_err(|e| bad("file.print", e.to_string()))?; let pages = opts.pages.as_ref().map_or(d.page_count(), |v| v.len()); - let out = json!({"printer": job.printer, "copies": copies, "pages": pages, "command": command, "bytes": pdf.len()}); + // The PDF that Export PDF writes with these settings. + let pdf = super::export::pdf_export(s, p)?; + let command = craft_print::command_preview(&job).map_err(|e| bad("file.print", e.to_string()))?; + let out = + json!({"printer": job.printer, "copies": copies, "pages": pages, "command": command, "bytes": pdf.bytes.len(), "warnings": pdf.warnings}); if !dry { - submit(&pdf, &job).map_err(|e| bad("file.print", e.to_string()))?; + submit(&pdf.bytes, &job).map_err(|e| bad("file.print", e.to_string()))?; } Ok(out) } @@ -96,11 +96,7 @@ mod tests { let mut s = Session::new(); s.execute("file.new", &json!({"pages": 4})).unwrap(); let params = json!({"printer": "Office (east)", "copies": 2, "pages": "2-3", "marks": true, "bleed": true}); - let expected = { - let d = &s.doc().unwrap().doc; - let options = super::super::export::options(¶ms, d.page_count()).unwrap(); - designcraft_pdf::export_pdf(&super::super::interchange::with_pdf_boxes(d), &s.cache, &options).unwrap() - }; + let expected = super::super::export::pdf_export(&s, ¶ms).unwrap().bytes; let r = print_with(&mut s, ¶ms, |pdf, job| { // The production export options and full bytes reach the shared spooler boundary. assert_eq!(pdf, expected); @@ -119,4 +115,34 @@ mod tests { print_with(&mut s, &json!({"dryRun": true}), |_, _| panic!("dry runs must not spool")).unwrap(); assert!(print_with(&mut s, &json!({"pages": "9"}), |_, _| panic!("invalid ranges must not spool")).is_err()); } + + /// File › Print spools the PDF that File › Export PDF writes with the same settings: soft + /// effects as images, CMYK print color, the same warnings. + #[test] + fn print_spools_what_export_pdf_writes() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = s.execute("frame.create", &json!({"rect": [72, 72, 200, 200]})).unwrap()["id"].clone(); + s.execute("object.fill", &json!({"ids": [id], "swatch": "C=100 M=0 Y=0 K=0"})).unwrap(); + s.execute("object.dropShadow", &json!({"ids": [id], "on": true, "distance": 8, "size": 6})).unwrap(); + let params = json!({"standard": "x4"}); + // The creation date is in whole seconds: an attempt can straddle a second boundary. + let mut matched = false; + for _ in 0..3 { + let exported = s.execute("file.exportPdf", ¶ms).unwrap(); + let mut printed = Vec::new(); + let r = print_with(&mut s, ¶ms, |pdf, _| { + printed = pdf.to_vec(); + Ok("request id is Office-1".into()) + }) + .unwrap(); + assert_eq!(r["warnings"], exported["warnings"]); + assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("soft effects")), "{r}"); + if printed == super::super::file::base64_decode(exported["base64"].as_str().unwrap()) { + matched = true; + break; + } + } + assert!(matched, "File › Print spools Export PDF's bytes"); + } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9294cc04e..44b8245b2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -407,7 +407,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Preferences › Appearance of Black: 100% K on screen as rich black (InDesign's default) or accurately as dark grey (`window.richBlack`); the printing/export side is still to come. - Spreads of up to 10 pages: Add to Previous Spread and Allow Spread to Shuffle (Pages panel; `layout.pages.toSpread`, `layout.spreadShuffle`); inserted pages go around kept spreads. - Page tool (⇧P) and `layout.pageSize`: pages of their own size (or a preset); the spine stays put and objects keep their place on their page. A click selects a page (handles, its liquid page rule tagged above it); dragging a handle previews the liquid rule and springs back, Alt-drag resizes, Shift keeps proportions, with a W/H readout; `anchor` keeps a reference point in place; double-click for exact sizes. With the Page tool the Control panel shows the page (reference point, W/H, preset, orientation, Liquid Page Rule including Controlled by Parent, Show Parent Page Overlay); X/Y and Objects Move with Page are shown, but pages don't move within their spread yet. On an object-based page it selects objects and shows their pins and width/height locks on the canvas (click to toggle, with tooltips). -- File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed as PDF through the system spooler (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document). +- File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed through the system spooler as the PDF File › Export PDF writes with the same settings (effects, flattener, print color, warnings) (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document). - Nested styles (through/up to N sentences, words, characters, letters, digits, tabs, breaks, spaces or given characters) and GREP styles (regex → character style), edited in Paragraph Style Options and composed under local formatting; `ui.dialog.open` for agents. Nested line styles are still to come; nested and GREP styles round-trip through IDML. - Table and cell styles (round-trip through IDML): cell styles (fill, insets, vertical justification, strokes, paragraph style) and table styles (region cell styles, border, alternating rows, spacing); apply from the Table panel, edits re-apply to every user; `style.cell.*`, `style.table.*`. - Style groups: paragraph and character styles shown in group folders, Move to Group (new, existing, none); `style.group` renames every use (styles, stories, table cells, footnotes, object styles).