From 9f7da3f6abcce29186af9a8660158e60419d069d Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 11 Oct 2026 07:29:54 +0200 Subject: [PATCH 1/3] M6: Adopt craft-print for CUPS and Windows PDF printing --- Cargo.lock | 9 ++ Cargo.toml | 1 + ROADMAP.md | 4 +- crates/engine/Cargo.toml | 1 + crates/engine/src/cmd/print.rs | 148 +++++++++++++-------------------- docs/file-format-parity.md | 6 +- docs/gaps.md | 8 +- docs/hardware-parity.md | 9 +- docs/roadmap.md | 4 +- docs/target-app-parity.md | 10 +-- 10 files changed, 92 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ec5bff..2c71f6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -796,6 +796,14 @@ dependencies = [ "libm", ] +[[package]] +name = "craft-print" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=c22bb1b0ce6316f84973cb69675fe7331c5886cc#c22bb1b0ce6316f84973cb69675fe7331c5886cc" +dependencies = [ + "tempfile", +] + [[package]] name = "craft-ui" version = "0.1.0" @@ -956,6 +964,7 @@ name = "designcraft-engine" version = "0.6.0" dependencies = [ "caseless", + "craft-print", "designcraft-color", "designcraft-compose", "designcraft-doc", diff --git a/Cargo.toml b/Cargo.toml index 35a1ed7..87d27ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ rust-version = "1.95" repository = "https://github.com/storytold/designcraft" [workspace.dependencies] +craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "c22bb1b0ce6316f84973cb69675fe7331c5886cc" } # internal designcraft-geom = { path = "crates/geom" } designcraft-color = { path = "crates/color" } diff --git a/ROADMAP.md b/ROADMAP.md index 9a0f71e..8aa7c03 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: pre-alpha** · next: alpha, ~0% and ~15–25 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, full re-measure against InDesign 2026 21.6; restructured to the craftrules progress-docs standard) · **Target:** Adobe InDesign 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. @@ -129,6 +129,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows keeps sheet size/orientation and corrected duplex binding edges. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows regression tests are present, with execution pending. PostScript, production print controls and physical output validation remain open. No readiness percentage change. + - **2026-10-10 (UI.4–UI.5):** Panel tabs can move into other groups, split the dock, float, resize and return to their saved locations through shared docking transactions. Layouts persist in custom workspaces; UI and control commands share the same validated action path. Shared pane geometry and widget-state theme application retain all five palettes and local fonts. Split panes send keyboard input only to the focused canvas and scope contextual toolbar IDs per pane. No readiness percentage change. - **2026-10-10** · Version 0.6.0; releases add a Linux riscv64 tar.gz (cross-compiled, CLI smoke-tested under QEMU). diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 8b4604f..62578ba 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -9,6 +9,7 @@ description = "DesignCraft engine: session, documents, history, selection, comma workspace = true [dependencies] +craft-print = { workspace = true } designcraft-images = { workspace = true } designcraft-geom = { workspace = true } designcraft-color = { workspace = true } diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 724feb2..20dc26f 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -1,7 +1,4 @@ -//! File › Print: the pages go to a printer through the system print spooler (`lpr`), as PDF. - -#[cfg(not(target_arch = "wasm32"))] -use std::io::Write; +//! File › Print: the pages go to a printer through the system print spooler (craft-print), as PDF. use serde_json::{Value, json}; @@ -20,91 +17,55 @@ pub fn specs() -> Vec { "Print…", ["File"], Some("Cmd+P"), - "{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (only return the spool command)} → {printer, copies, pages, command}", + "{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (export and preview submission without printing)} → {printer, copies, pages, command, bytes}", has_doc, print ), ] } -#[cfg(not(target_arch = "wasm32"))] fn printers() -> Value { - let run = |args: &[&str]| { - std::process::Command::new("lpstat").args(args).output().ok().map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default() - }; - let names: Vec = run(&["-e"]).lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect(); - // "system default destination: NAME" - let default = run(&["-d"]).rsplit(':').next().map(str::trim).filter(|s| !s.is_empty() && !s.contains("no system default")).map(str::to_string); + let printers = craft_print::printers(); + let default = printers.iter().find(|p| p.default).map(|p| p.name.clone()); + let names: Vec<_> = printers.into_iter().map(|p| p.name).collect(); json!({"printers": names, "default": default}) } -#[cfg(target_arch = "wasm32")] -fn printers() -> Value { - json!({"printers": [], "default": null}) -} - fn print(s: &mut Session, p: &Value) -> Result { - let d = &s.doc()?.doc; - let opts = super::export::options(p, d.page_count())?; #[cfg(target_arch = "wasm32")] { - // Even a dry run cannot construct a native spool command in the browser. - let _ = opts; + super::export::options(p, s.doc()?.doc.page_count())?; Err(bad("file.print", "printing isn't available on the web: export a PDF and print it")) } #[cfg(not(target_arch = "wasm32"))] - { - let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999); - let printer = str_param(p, "printer").map(str::to_string); - let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false); - let pdf = designcraft_pdf::export_pdf(d, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?; - let mut file = secure_print_file()?; - let file_path = file.path().to_path_buf(); - let mut cmd: Vec = vec!["lpr".into()]; - if let Some(pr) = &printer { - cmd.extend(["-P".into(), pr.clone()]); - } - if copies > 1 { - cmd.extend(["-#".into(), copies.to_string()]); - } - cmd.extend(["-T".into(), d.title.clone(), file_path.to_string_lossy().to_string()]); - let pages = opts.pages.as_ref().map_or(d.page_count(), |v| v.len()); - let out = json!({"printer": printer, "copies": copies, "pages": pages, "command": cmd, "bytes": pdf.len()}); - if dry { - return Ok(out); - } - file.as_file_mut().write_all(&pdf).map_err(|e| EngineError::Other(format!("{}: {e}", file.path().display())))?; - file.as_file_mut().flush().map_err(|e| EngineError::Other(format!("{}: {e}", file.path().display())))?; - let st = std::process::Command::new(&cmd[0]).args(&cmd[1..]).output().map_err(|e| bad("file.print", format!("lpr: {e}")))?; - if !st.status.success() { - return Err(bad("file.print", String::from_utf8_lossy(&st.stderr).trim().to_string())); - } - Ok(out) - } + print_with(s, p, craft_print::submit) } #[cfg(not(target_arch = "wasm32"))] -fn secure_print_file() -> Result { - let file = tempfile::Builder::new() - .prefix("designcraft-print-") - .suffix(".pdf") - .tempfile() - .map_err(|e| EngineError::Other(format!("temporary print file: {e}")))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - file.as_file() - .set_permissions(std::fs::Permissions::from_mode(0o600)) - .map_err(|e| EngineError::Other(format!("{}: {e}", file.path().display())))?; +fn print_with( + s: &mut Session, + p: &Value, + submit: impl FnOnce(&[u8], &craft_print::Job) -> std::result::Result, +) -> Result { + let d = &s.doc()?.doc; + let opts = super::export::options(p, d.page_count())?; + let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999) as u32; + let job = + craft_print::Job { printer: str_param(p, "printer").map(str::to_string), copies, title: d.title.clone(), ..craft_print::Job::default() }; + let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false); + let pdf = designcraft_pdf::export_pdf(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()}); + if !dry { + submit(&pdf, &job).map_err(|e| bad("file.print", e.to_string()))?; } - Ok(file) + Ok(out) } #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use serde_json::json; - - use crate::Session; + use super::*; #[test] fn print_builds_the_spool_command() { @@ -112,37 +73,42 @@ mod tests { s.execute("file.new", &json!({"pages": 4})).unwrap(); let r = s.execute("file.print", &json!({"printer": "Office", "copies": 2, "pages": "2-3", "dryRun": true})).unwrap(); assert_eq!(r["pages"], 2); - let cmd: Vec<&str> = r["command"].as_array().unwrap().iter().filter_map(|v| v.as_str()).collect(); - assert_eq!(&cmd[..5], ["lpr", "-P", "Office", "-#", "2"]); - assert!(cmd.last().unwrap().ends_with(".pdf")); - assert!(!std::path::Path::new(cmd.last().unwrap()).exists()); + let command: Vec<&str> = r["command"].as_array().unwrap().iter().filter_map(Value::as_str).collect(); + if craft_print::backend() == Some(craft_print::Backend::Cups) { + assert_eq!(&command[..5], ["lp", "-d", "Office", "-n", "2"]); + assert!(command.contains(&"fit-to-page=false")); + } else { + assert_eq!(command[0], "powershell"); + } assert!(r["bytes"].as_u64().unwrap() > 500); assert!(s.execute("file.print", &json!({"pages": "9", "dryRun": true})).is_err()); assert!(s.execute("file.printers", &json!({})).unwrap()["printers"].is_array()); } - #[cfg(not(target_arch = "wasm32"))] #[test] - fn print_uses_distinct_ephemeral_files() { - let first = super::secure_print_file().unwrap(); - let second = super::secure_print_file().unwrap(); - let first_path = first.path().to_path_buf(); - let second_path = second.path().to_path_buf(); - assert_ne!(first_path, second_path); - drop((first, second)); - assert!(!first_path.exists()); - assert!(!second_path.exists()); - } - - #[cfg(all(not(target_arch = "wasm32"), unix))] - #[test] - fn print_file_is_private_and_removed_on_drop() { - use std::os::unix::fs::PermissionsExt; - - let file = super::secure_print_file().unwrap(); - let path = file.path().to_path_buf(); - assert_eq!(file.as_file().metadata().unwrap().permissions().mode() & 0o777, 0o600); - drop(file); - assert!(!path.exists()); + fn print_submits_the_selected_export_and_reports_refusal() { + 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(d, &s.cache, &options).unwrap() + }; + 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); + assert!(pdf.starts_with(b"%PDF-")); + assert_eq!(job.printer.as_deref(), Some("Office (east)")); + assert_eq!(job.copies, 2); + assert!(!job.title.is_empty()); + Ok("request id is Office-1".into()) + }) + .unwrap(); + assert_eq!(r["pages"], 2); + let refusal = print_with(&mut s, ¶ms, |_, _| Err(craft_print::Error::Spool("printer offline".into()))).unwrap_err(); + assert!(refusal.to_string().contains("printer offline")); + 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()); } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 9077757..4bc9a40 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first version: every format InDesign 2026 declares, measured against the code) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) Every format InDesign 2026 reads or writes, with DesignCraft's support, fidelity and tests. The InDesign list comes from its bundle's `Info.plist` (`CFBundleDocumentTypes`, @@ -46,7 +46,7 @@ score below). | EPS export | `.eps` | ✓ | ✗ | | <1% | | Text Only / RTF / Tagged Text | `.txt`, `.rtf` | ✓ | ✓ (`file.exportText` txt, rtf, tagged) | | 1% | | XML | `.xml` | ✓ | ✓ export and import, DTD validate | depth unmeasured | 1% | -| Print / PostScript | | ✓ printers, PPDs, separations | ~ PDF to `lpr` (no Windows, no PostScript) | | 3% | +| Print / PostScript | | ✓ printers, PPDs, separations | ~ PDF to CUPS/Windows (craft-print; no PostScript) | | 3% | | Package | folder | ✓ fonts, links, IDML, PDF, report | ✓ (#340: IDML inside links to absolute paths) | | 2% | ## Placed and imported content @@ -97,7 +97,7 @@ for real work** (estimated). INDD alone moves this dimension by up to 30 points. | INDD import via a clean-room converter (owner decision, #114) | 20–60 | | IDML real-file corpus, metric, fixes (#193, #338, #339, #340) | 60–100 | | PDF/X-1a and X-3 (transparency flattener), PDF presets and `.joboptions`, external validator | 40–70 | -| Print path (Windows, PostScript level 3 output or system print APIs) | 25–45 | +| Print fidelity, PostScript level 3 output and print presets | 25–45 | | ICML story import, IDMS snippets, INDT templates (once INDD reads) | 15–25 | | HTML / fixed-layout EPUB with live text | 20–35 | | JPEG export dialog, SVG export, EPS export | 10–20 | diff --git a/docs/gaps.md b/docs/gaps.md index b8be726..f9a40d4 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DesignCraft falls short of InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, first version: gaps from the full re-measure and the issue tracker) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) Every known shortfall against InDesign 2026, one entry each, ranked by how much it stops an InDesign professional from doing real work. This is the work list: agents pick from the top. @@ -117,8 +117,10 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 40–70 h. **Doc:** [file-format-parity.md](file-format-parity.md) ### 10. Printing (H, F) -- **Missing:** a real print path: `file.print` sends PDF to `lpr`, so Windows can't print; no - PostScript, printer PPDs, separations to device, print presets, or trapping. +- **Missing:** PostScript, a printer-driver options UI, separations to device, print presets, or trapping. +- **Have:** `file.print` exports selected pages/spreads with marks and bleed, then submits through + craft-print: CUPS on macOS/Linux and the Windows PDF raster spooler. Physical printer fidelity + remains a hardware check. - **Estimate:** 25–45 h. **Doc:** [hardware-parity.md](hardware-parity.md) ### 11. Paragraph and character style editing reliability (F) diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index 1b75d1d..98fd44d 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware and platform parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first version) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) What InDesign does with hardware, per platform, against DesignCraft. Layout is not a GPU-hungry workload, so this dimension carries little weight, but a start that fails on a @@ -19,7 +19,7 @@ graphics driver loses the user entirely. Overall numbers: [target-app-parity.md] | HiDPI / Retina | ✓ | ✓ | ✓ | ✓ | ✓ | | UI scaling | ✓ | ✓ (menus and dialogs usable at larger scales, #281) | ✓ | ✓ | ✓ | | Multi-monitor, floating document windows | ✓ | ~ one window; New Window for the same document | ~ | ~ | ✗ | -| Printing | system print with PPDs, PostScript, separations | PDF to `lpr` | ✗ (no `lpr`) | PDF to `lpr` (CUPS) | browser download | +| Printing | system print with PPDs, PostScript, separations | PDF to CUPS (`craft-print`) | Windows PDF raster spooler (`craft-print`) | PDF to CUPS (`craft-print`) | browser download | | Colour-managed display (monitor profile) | ✓ | ✗ (working CMYK profile simulated; monitor profile not applied) | ✗ | ✗ | ✗ | | Touch / pen | Touch workspace (Windows), gestures | trackpad pinch/scroll | ~ | ~ | ~ | | Multi-core composition | single-threaded composer per story (documented by Adobe as mostly single-threaded) | parallel story composition (rayon), 6.7× on the sample | ✓ | ✓ | single-threaded | @@ -43,7 +43,7 @@ the developers don't use daily. | Work | Opus 5.5 h | |---|---| | Start-up robustness on every GPU: confirm #297 with reporters, Intel Metal fix, CPU-only present fallback | 15–30 | -| Printing on Windows (system print API) and print presets | 15–30 (counted in file-format-parity.md) | +| Printing fidelity, PostScript and print presets | 15–30 (counted in file-format-parity.md) | | Monitor profile in display colour management | 5–10 | | Windows and Linux runtime smoke tests in CI | 20–40 | | Multi-window document views | 15–30 | @@ -54,3 +54,6 @@ the developers don't use daily. | Date | Change | Summary | |---|---|---| | 2026-10-10 | major | Created from the code, InDesign 2026's GPU Performance features and the issue tracker | + +Windows submission now uses shared craft-print. It preserves PDF sheet size/orientation and +duplex binding edges; printer-driver options UI and physical output validation remain open. diff --git a/docs/roadmap.md b/docs/roadmap.md index e738da9..d80fe7a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DesignCraft roadmap (detail) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, milestones re-assessed; beta plan from the full re-measure) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, milestones re-assessed; beta plan from the full re-measure) · **Target:** Adobe InDesign 2026 (21.6) Forward-looking plan. The one-page summary is [ROADMAP.md](../ROADMAP.md); numbers are in [target-app-parity.md](target-app-parity.md); the work list is [gaps.md](gaps.md). Hours are @@ -88,7 +88,7 @@ that is ~28 points and **420–750 h** (~120–200 wall-clock hours with four to | Start-up robustness, fonts | 5, 6 | 25–50 | | CI, Windows/Linux runtime tests | 7, 14 | 35–70 | | IME | 8 | 10–20 | -| PDF/X-1a/X-3, presets, print on Windows | 9, 10 | 65–110 | +| PDF/X-1a/X-3, presets, physical print validation | 9, 10 | 65–110 | | Styles, tables, missing menu items (the most-used half) | 11, 12, 16 | 60–100 | | Long documents (Books) | 15 | 30–50 | | **Total** | | **420–750** | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8044ed5..ba66f89 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DesignCraft parity with Adobe InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, full re-measure against InDesign 2026 21.6; replaces the 2026-10-04 estimate in ROADMAP.md) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) The authoritative assessment of how close DesignCraft is to Adobe InDesign. [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) lists every known shortfall one at a time; the feature-by-feature @@ -66,7 +66,7 @@ they receive, setting type exactly, and basic editing that never fails. | Performance | 5% | 65% | 40–80 | 300k characters compose in ~38 ms warm; typing repaints in < 1 ms (damage regions); no incremental composition; nothing benchmarked against InDesign; no large real documents | [gaps.md](gaps.md) | | Localization | 3% | 45% | 120–180 | 9 translations beyond English; Japanese 95%, most others 83–85%, pt-BR 61%; Hindi, Indonesian, Korean, Vietnamese none | [localization-parity.md](localization-parity.md) | | Platforms | 3% | 55% | 40–80 | macOS is where it's tested; Windows and Linux users hit basic failures; web and FreeBSD builds exist | [hardware-parity.md](hardware-parity.md) | -| Hardware | 2% | 45% | 20–40 | Canvas rasterizes on the CPU (vello_cpu, multithreaded); GPU only presents; printing is `lpr` (no Windows printing, no PostScript) | [hardware-parity.md](hardware-parity.md) | +| Hardware | 2% | 45% | 20–40 | Canvas rasterizes on the CPU (vello_cpu, multithreaded); GPU only presents; printing uses shared CUPS/Windows PDF spoolers (no PostScript; physical output needs validation) | [hardware-parity.md](hardware-parity.md) | | Ecosystem (scripting, plug-ins) | 2% | 15% | 150–250 | Own command/script/MCP API is strong; no ExtendScript/UXP DOM, so InDesign scripts don't run; no plug-ins | [gaps.md](gaps.md) | | AI features | 0% (not weighted) | 15% | 60–120 | Agent control through MCP goes beyond InDesign; no generative features (InDesign 2026: Generative Expand, Text to Image, Auto Style) | [gaps.md](gaps.md) | | **Weighted sum** | 100% | **47.2% → ~47%** (range 42–52%) | **1,150–1,950** | | | @@ -117,7 +117,7 @@ twice. | Tables | 5% | 55% | anchored objects in cells, Table Options submenus | | Layers | 3% | 75% | | | Effects and transparency | 3% | 50% | fidelity unmeasured | -| Output: PDF export, print, package | 8% | 60% | PDF/X-4 strong; no X-1a, presets, Windows printing | +| Output: PDF export, print, package | 8% | 60% | PDF/X-4 strong; no X-1a or presets; physical print validation pending | | View, navigation, undo, save | 3% | 70% | | | **Weighted depth** | 100% | **60.7%** | | @@ -152,7 +152,7 @@ features), exchange edge cases, and everything excluded above. | Undo / redo | 5% | 85% | | | Save and reopen | 5% | 85% | `.designcraft` round trip, recovery | | Export PDF / PNG | 10% | 80% | | -| Print | 5% | 40% | PDF to `lpr` only, nothing on Windows | +| Print | 5% | 40% | PDF to CUPS/Windows via craft-print; physical output validation pending | | **Weighted depth** | 100% | **72.0%** | | | Discount | Factor | Evidence | @@ -217,7 +217,7 @@ table is the one summed. | Tables | 100% | 55% | 25–40 | anchored objects in cells not drawn (#163); Table ▸ Table Options submenus, Diagonal Lines, Convert Rows, Edit Header/Footer | | Long documents | 100% | 45% | 30–50 | Books are shallow (sync, book pagination, book PDF/TOC/index); Table of Contents Styles | | Interactivity & digital | 100% | 40% | 30–50 | HTML export basic (#314); fixed-layout EPUB is page images; no animation, timing, media playback | -| Output & production | 92% | 55% | 40–70 | PDF/X-4 only (no X-1a/X-3), no PDF/print presets or `.joboptions`, print via `lpr` (no Windows), no trapping; Output Preview panel | +| Output & production | 92% | 55% | 40–70 | PDF/X-4 only (no X-1a/X-3), no PDF/print presets or `.joboptions`, print via craft-print (CUPS/Windows), no trapping; Output Preview panel | | XML & automation | 100% | 35% | (ecosystem dimension) | no ExtendScript/UXP; XML structure depth | | View & navigation | 100% | 65% | 10–20 | Navigator, display badges, Show/Hide extras (content grabber, live corners) | | Undo, history, saving | 100% | 70% | 5–10 | Undo/Redo name the action; File ▸ Open Recent | From 3ec5cef6a2f5c705820180443671cec9c4d4315f Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 11 Oct 2026 22:14:24 +0200 Subject: [PATCH 2/3] M6: Adopt merged craft-print with scaling and queue duplex defaults --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- ROADMAP.md | 4 ++-- crates/engine/src/cmd/print.rs | 14 +++++++++++--- docs/file-format-parity.md | 2 +- docs/gaps.md | 2 +- docs/hardware-parity.md | 7 ++++--- docs/target-app-parity.md | 2 +- 8 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c71f6b..cc457b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,7 +799,7 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c22bb1b0ce6316f84973cb69675fe7331c5886cc#c22bb1b0ce6316f84973cb69675fe7331c5886cc" +source = "git+https://github.com/storytold/craft-libs?rev=32bcac12db98113d18d8f56bc47fb6ddabe4d54f#32bcac12db98113d18d8f56bc47fb6ddabe4d54f" dependencies = [ "tempfile", ] @@ -1481,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2655,7 +2655,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2738,7 +2738,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3847,7 +3847,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4259,10 +4259,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4497,7 +4497,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5234,7 +5234,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 87d27ca..950b0b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.95" repository = "https://github.com/storytold/designcraft" [workspace.dependencies] -craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "c22bb1b0ce6316f84973cb69675fe7331c5886cc" } +craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "32bcac12db98113d18d8f56bc47fb6ddabe4d54f" } # internal designcraft-geom = { path = "crates/geom" } designcraft-color = { path = "crates/color" } diff --git a/ROADMAP.md b/ROADMAP.md index 8aa7c03..82a42cf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: pre-alpha** · next: alpha, ~0% and ~15–25 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. @@ -129,7 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. -- **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows keeps sheet size/orientation and corrected duplex binding edges. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows regression tests are present, with execution pending. PostScript, production print controls and physical output validation remain open. No readiness percentage change. +- **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows selects matching or default driver paper and shrinks oversized sheets without enlarging smaller ones; queue duplex defaults are preserved. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows CI passes, including paused-queue submission and cleanup. PostScript, production print controls and physical output validation remain open. No readiness percentage change. - **2026-10-10 (UI.4–UI.5):** Panel tabs can move into other groups, split the dock, float, resize and return to their saved locations through shared docking transactions. Layouts persist in custom workspaces; UI and control commands share the same validated action path. Shared pane geometry and widget-state theme application retain all five palettes and local fonts. Split panes send keyboard input only to the focused canvas and scope contextual toolbar IDs per pane. No readiness percentage change. - **2026-10-10** · Version 0.6.0; releases add a Linux riscv64 tar.gz (cross-compiled, CLI diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 20dc26f..659b520 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -50,8 +50,13 @@ fn print_with( let d = &s.doc()?.doc; let opts = super::export::options(p, d.page_count())?; let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999) as u32; - let job = - craft_print::Job { printer: str_param(p, "printer").map(str::to_string), copies, title: d.title.clone(), ..craft_print::Job::default() }; + let job = craft_print::Job { + printer: str_param(p, "printer").map(str::to_string), + copies, + scaling: craft_print::Scaling::ShrinkToFit, + title: d.title.clone(), + ..craft_print::Job::default() + }; let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false); let pdf = designcraft_pdf::export_pdf(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()))?; @@ -76,7 +81,8 @@ mod tests { let command: Vec<&str> = r["command"].as_array().unwrap().iter().filter_map(Value::as_str).collect(); if craft_print::backend() == Some(craft_print::Backend::Cups) { assert_eq!(&command[..5], ["lp", "-d", "Office", "-n", "2"]); - assert!(command.contains(&"fit-to-page=false")); + assert!(command.contains(&"print-scaling=auto-fit")); + assert!(!command.iter().any(|a| a.starts_with("sides="))); } else { assert_eq!(command[0], "powershell"); } @@ -101,6 +107,8 @@ mod tests { assert!(pdf.starts_with(b"%PDF-")); assert_eq!(job.printer.as_deref(), Some("Office (east)")); assert_eq!(job.copies, 2); + assert_eq!(job.duplex, None); + assert_eq!(job.scaling, craft_print::Scaling::ShrinkToFit); assert!(!job.title.is_empty()); Ok("request id is Office-1".into()) }) diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 4bc9a40..fc8fbc3 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6) Every format InDesign 2026 reads or writes, with DesignCraft's support, fidelity and tests. The InDesign list comes from its bundle's `Info.plist` (`CFBundleDocumentTypes`, diff --git a/docs/gaps.md b/docs/gaps.md index f9a40d4..6f92c55 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DesignCraft falls short of InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6) Every known shortfall against InDesign 2026, one entry each, ranked by how much it stops an InDesign professional from doing real work. This is the work list: agents pick from the top. diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index 98fd44d..fb92c58 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware and platform parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6) What InDesign does with hardware, per platform, against DesignCraft. Layout is not a GPU-hungry workload, so this dimension carries little weight, but a start that fails on a @@ -55,5 +55,6 @@ the developers don't use daily. |---|---|---| | 2026-10-10 | major | Created from the code, InDesign 2026's GPU Performance features and the issue tracker | -Windows submission now uses shared craft-print. It preserves PDF sheet size/orientation and -duplex binding edges; printer-driver options UI and physical output validation remain open. +Windows submission now uses shared craft-print. It selects matching or default driver paper, +shrinks oversized sheets to the printable area without enlarging smaller ones, and preserves +the queue's duplex default; printer-driver options UI and physical output validation remain open. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ba66f89..74d8e78 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DesignCraft parity with Adobe InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; native Windows validation pending) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) The authoritative assessment of how close DesignCraft is to Adobe InDesign. [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) lists every known shortfall one at a time; the feature-by-feature From 957890223a0c84070278557bac529d6da848ebe6 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Mon, 12 Oct 2026 01:20:53 +0200 Subject: [PATCH 3/3] M6: Pin craft-print with native driver capability validation --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc457b2..7e5ebb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,7 +799,7 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=32bcac12db98113d18d8f56bc47fb6ddabe4d54f#32bcac12db98113d18d8f56bc47fb6ddabe4d54f" +source = "git+https://github.com/storytold/craft-libs?rev=d991bf6792f8ade99c51358bf3c1d902c8cd96b5#d991bf6792f8ade99c51358bf3c1d902c8cd96b5" dependencies = [ "tempfile", ] @@ -1481,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3847,7 +3847,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4259,10 +4259,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5234,7 +5234,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 950b0b4..4fcbfd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.95" repository = "https://github.com/storytold/designcraft" [workspace.dependencies] -craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "32bcac12db98113d18d8f56bc47fb6ddabe4d54f" } +craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" } # internal designcraft-geom = { path = "crates/geom" } designcraft-color = { path = "crates/color" }