diff --git a/Cargo.lock b/Cargo.lock index e5b11f56..325951da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,6 +808,14 @@ dependencies = [ "libm", ] +[[package]] +name = "craft-print" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=d991bf6792f8ade99c51358bf3c1d902c8cd96b5#d991bf6792f8ade99c51358bf3c1d902c8cd96b5" +dependencies = [ + "tempfile", +] + [[package]] name = "craft-ui" version = "0.1.0" @@ -976,6 +984,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 0fe2ca16..072dd983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,9 @@ zip = { version = "3", default-features = false, features = ["deflate"] } # INDD/INDT → IDML conversion for File › Open (indd-utils, tracking its main branch; MIT OR Apache-2.0, no dependencies). indd = { git = "https://github.com/aiongg/indd-utils", branch = "main" } +# File › Print through the system print spooler (craft-libs). +craft-print = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" } + # ui # Shared frontend components; keep this dependency above the engine boundary. craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] } diff --git a/ROADMAP.md b/ROADMAP.md index f26bc6a1..02bd401d 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-11 · **Last updated:** 2026-10-11 · **Change:** minor (Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); macOS lists fonts that font services and managers activate) · **Target:** Adobe InDesign 2026 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); macOS lists fonts that font services and managers activate; 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. @@ -132,6 +132,7 @@ Newest first. - **2026-10-12** · Properties › Transform shows rotation, shear and X/Y scale, and Rotate 90° and Flip Horizontal/Vertical buttons, as InDesign's does. No readiness percentage change. - **2026-10-12** · Underlines and strikethroughs are drawn as one bar per stretch of text on screen, as in PDF export: a bar per letter left faint seams between the letters. No readiness percentage change. - **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. 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-11** · The View menu follows InDesign's: proofing first, then zoom, Rotate Spread, Screen Mode, Display Performance, rulers, Extras, Grids & Guides and Structure; the view items that sat at the end of the Type menu moved there. No readiness percentage change. - **2026-10-11** · Properties › Appearance's Fill and Stroke menus gain InDesign's Tint field and, for a text frame, Apply To: Frame / Text. No readiness percentage change. - **2026-10-11** · `story.replaceRange`, `story.setText`, notes, footnotes, endnotes, type on a path and table cells read `\r` and `\r\n` as paragraph returns, as `frame.create` and `text.insert` already did. No readiness percentage change. diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index a0bfc53b..dc1c4ca6 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 cfb953a1..4f79f13a 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,92 +17,60 @@ 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(&super::interchange::with_pdf_boxes(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, + 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(&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()}); + 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() { @@ -113,37 +78,45 @@ 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(&"print-scaling=auto-fit")); + assert!(!command.iter().any(|a| a.starts_with("sides="))); + } 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(&super::super::interchange::with_pdf_boxes(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_eq!(job.duplex, None); + assert_eq!(job.scaling, craft_print::Scaling::ShrinkToFit); + 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 f1cd3d71..b164f62b 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; 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`, @@ -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 ca82ea73..c94c533a 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 (Optical kerning from outlines (#181); gap 6: macOS lists fonts that font services and managers activate) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Optical kerning from outlines (#181); gap 6: macOS lists fonts that font services and managers activate; 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. @@ -124,8 +124,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 d3e4cb7f..8bee7331 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 (font managers on macOS) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (font managers on macOS; 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 @@ -20,7 +20,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 | @@ -44,7 +44,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 | @@ -55,3 +55,7 @@ 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 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/roadmap.md b/docs/roadmap.md index 17f09e55..53f84a2e 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 e081dd9d..279812da 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; 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 @@ -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 |