diff --git a/ROADMAP.md b/ROADMAP.md index f38a51b..7c27c5f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -99,6 +99,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result. - EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted. - Preferences: Dictionary (user dictionary; `spelling.setWords/words`), Spelling (Dynamic Spelling: red squiggles on the canvas, also Edit ▸ Spelling), Autocorrect (word list, applied as you type), Notes, Track Changes, Story Editor Display and File Handling (recovery interval) sections. +- Colour management exposed: Edit ▸ Color Settings (`color.settings`: RGB/CMYK working spaces, intent, black-point compensation; `color.loadProfile` for ICC files; `color.convert` through the working spaces, with gamut checks) and View ▸ Proof Setup / Proof Colors (press, sRGB, legacy Mac RGB, colour-blindness simulations, simulate paper) on the canvas. - Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`). - Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export. - Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override. @@ -157,7 +158,7 @@ four agents in parallel on separate crates: | Work | Estimate | |---|---| | Open P0 (2: PDF/X-4 validation against a certified checker, PostScript interpretation for EPS) | 8 h | -| Open P1 (5: New Window, colour management, language support, UI keyboard navigation, …) | 15 h | +| Open P1 (4 partial: New Window as a separate OS window, language support, UI keyboard navigation, on-screen blending in the blend space) | 12 h | | Open P2 (37: 12 missing — spread rotation, page transitions, vertical/RTL type, math, video/audio, split window, localization, flattener, placed-file layers — and 25 partial) | 110 h | | Depth and pixel fidelity of every dialog, panel and menu against InDesign 2026 | 90 h | | Performance (incremental composition, GPU raster) and interchange hardening (IDML/PDF corpus) | 55 h | diff --git a/apps/designcraft/src/main.rs b/apps/designcraft/src/main.rs index 2606072..b89c4ca 100644 --- a/apps/designcraft/src/main.rs +++ b/apps/designcraft/src/main.rs @@ -93,6 +93,8 @@ fn services() -> Services { let d = rfd::FileDialog::new(); let d = if purpose == "swatches" { d.add_filter("Swatch Exchange (ASE)", &["ase"]) + } else if purpose == "icc" { + d.add_filter("ICC profile", &["icc", "icm"]) } else if purpose == "book" { d.add_filter("Book", &["dcbook"]) } else if purpose == "xml" { diff --git a/crates/engine/src/cmd/colorsettings.rs b/crates/engine/src/cmd/colorsettings.rs new file mode 100644 index 0000000..c62b393 --- /dev/null +++ b/crates/engine/src/cmd/colorsettings.rs @@ -0,0 +1,101 @@ +//! Edit › Color Settings: the RGB and CMYK working spaces, conversion intent and black-point +//! compensation (process-wide), and loading ICC profiles. + +use designcraft_color::cms::{self, ColorSettings, Intent}; +use serde_json::{Value, json}; + +use super::{CommandSpec, always, bad, cmd, str_param}; +use crate::Result; + +fn state() -> Value { + let st = cms::active_settings(); + json!({"rgb": st.rgb, "cmyk": st.cmyk, "intent": st.intent.id(), "bpc": st.bpc, "profiles": cms::profiles()}) +} + +fn set(p: &Value) -> Result { + let mut st: ColorSettings = cms::active_settings(); + if let Some(v) = str_param(p, "rgb") { + st.rgb = v.to_string(); + } + if let Some(v) = str_param(p, "cmyk") { + st.cmyk = v.to_string(); + } + if let Some(v) = str_param(p, "intent") { + st.intent = Intent::parse(v).ok_or_else(|| bad("color.settings", format!("unknown intent `{v}`")))?; + } + if let Some(v) = p.get("bpc").and_then(Value::as_bool) { + st.bpc = v; + } + cms::set_active(&st).map_err(|e| bad("color.settings", e.to_string()))?; + Ok(state()) +} + +pub fn specs() -> Vec { + vec![ + cmd!( + noundo "color.settings", + "Color Settings", + [], + None, + "{rgb?: working RGB profile, cmyk?: working CMYK profile, intent?: perceptual|relative|saturation|absolute, bpc?: bool} → {rgb, cmyk, intent, bpc, profiles}", + always, + |_s, p| set(p) + ), + cmd!(noundo "color.loadProfile", "Load Profile…", [], None, "{path} — an ICC profile (.icc/.icm) to use as a working space or proof target", always, |_s, p| { + let path = str_param(p, "path").ok_or_else(|| bad("color.loadProfile", "`path` required"))?; + #[cfg(not(target_arch = "wasm32"))] + { + let info = cms::load_icc_file(std::path::Path::new(path)).map_err(|e| bad("color.loadProfile", e.to_string()))?; + Ok(json!(info)) + } + #[cfg(target_arch = "wasm32")] + { + let _ = path; + Err(bad("color.loadProfile", "no file system on the web")) + } + }), + cmd!(query "color.convert", "Convert Color", [], None, "{color: {model: rgb, r, g, b} | {model: cmyk, c, m, y, k} | {model: gray, k}, to: rgb|cmyk|gray|lab} — through the working spaces", always, |_s, p| { + let c: designcraft_color::Color = serde_json::from_value(p.get("color").cloned().unwrap_or_default()).map_err(|e| bad("color.convert", e.to_string()))?; + let a = cms::active(); + let intent = a.settings().intent; + Ok(match str_param(p, "to").unwrap_or("rgb") { + "lab" => { + let l = a.lab(&c); + json!({"l": l.l, "a": l.a, "b": l.b, "outOfGamut": a.out_of_gamut(&c)}) + } + "cmyk" => json!(a.convert(&c, cms::Model::Cmyk, intent)), + "gray" => json!(a.convert(&c, cms::Model::Gray, intent)), + _ => json!(a.convert(&c, cms::Model::Rgb, intent)), + }) + }), + ] +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::Session; + + #[test] + fn color_settings_report_convert_and_validate() { + // The settings are process-wide: this test only re-applies the current ones, so tests + // rendering in parallel aren't affected. + let mut s = Session::new(); + let before = s.execute("color.settings", &json!({})).unwrap(); + assert!(before["profiles"].as_array().unwrap().len() >= 6); + let cyan = json!({"color": {"model": "cmyk", "c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}, "to": "rgb"}); + let rgb = s.execute("color.convert", &cyan).unwrap(); + assert_eq!(rgb["model"], "rgb"); + assert!(rgb["r"].as_f64().unwrap() < 0.3 && rgb["b"].as_f64().unwrap() > 0.7, "{rgb}"); + let lab = s.execute("color.convert", &json!({"color": {"model": "rgb", "r": 0.0, "g": 1.0, "b": 0.0}, "to": "lab"})).unwrap(); + assert_eq!(lab["outOfGamut"], true, "pure RGB green is outside the press gamut"); + let again = s + .execute("color.settings", &json!({"rgb": before["rgb"], "cmyk": before["cmyk"], "intent": before["intent"], "bpc": before["bpc"]})) + .unwrap(); + assert_eq!(again["cmyk"], before["cmyk"]); + assert!(s.execute("color.settings", &json!({"cmyk": "Nope"})).is_err()); + assert!(s.execute("color.settings", &json!({"intent": "sideways"})).is_err()); + assert_eq!(s.execute("color.settings", &json!({})).unwrap()["cmyk"], before["cmyk"], "a failed change leaves the settings"); + } +} diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 5b8a92f..11c419c 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -7,6 +7,7 @@ pub mod book; mod buttons; mod captions; mod changes; +mod colorsettings; mod conditions; mod datamerge; mod edit; @@ -156,6 +157,7 @@ pub fn command_specs() -> &'static [CommandSpec] { v.extend(book::specs()); v.extend(buttons::specs()); v.extend(captions::specs()); + v.extend(colorsettings::specs()); v.extend(changes::specs()); v.extend(conditions::specs()); v.extend(library::specs()); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 4777779..f905e25 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -906,6 +906,25 @@ pub fn pdf_page_box(bytes: &[u8], page: usize, kind: &str) -> Option<(f64, f64, /// Separations Preview on a rendered image: `plate` 0–3 shows that process plate (C, M, Y, K) as /// ink density in grey; with an `ink_limit` (0–4, total ink) areas over it are shown in red. /// Pixels are separated with a plain GCR (black = 1 − max(r, g, b)), so it's a preview. +/// View › Proof Colors: simulate the proof target (a press, another RGB space, colour-vision +/// deficiencies) through the active colour settings' proof table. +pub fn proof_view(img: &mut Rendered, setup: &designcraft_color::cms::ProofSetup) { + let lut = designcraft_color::cms::active().proof_lut(setup); + for px in img.pixels.chunks_exact_mut(4) { + let a = px[3]; + if a == 0 { + continue; + } + // Premultiplied → straight, proof, back. + let k = 255.0 / a as f32; + let rgb = [px[0], px[1], px[2]].map(|v| ((v as f32 * k).round()).min(255.0) as u8); + let out = lut.apply8(rgb); + for i in 0..3 { + px[i] = ((out[i] as u32 * a as u32 + 127) / 255) as u8; + } + } +} + pub fn separation_view(img: &mut Rendered, plate: Option, ink_limit: Option) { for px in img.pixels.chunks_exact_mut(4) { let a = px[3] as f32 / 255.0; @@ -941,6 +960,23 @@ mod tests { use designcraft_doc::build::NewDocument; use designcraft_doc::{Fill, ParaFormat}; + #[test] + fn proof_view_simulates_the_target() { + use designcraft_color::cms::{ProofSetup, ProofTarget}; + let red = |a: u8| Rendered { width: 1, height: 1, pixels: vec![a, 0, 0, a] }; + let mut img = red(255); + proof_view(&mut img, &ProofSetup { target: ProofTarget::Protanopia, ..Default::default() }); + assert!(img.pixels[1] > 40, "protanopia turns pure red olive: {:?}", img.pixels); + let mut img = red(255); + proof_view(&mut img, &ProofSetup { target: ProofTarget::MonitorRgb, ..Default::default() }); + assert!(img.pixels[0] > 245 && img.pixels[1] < 10, "monitor RGB is no simulation: {:?}", img.pixels); + // Transparent pixels stay transparent; premultiplication is kept. + let mut img = Rendered { width: 2, height: 1, pixels: vec![0, 0, 0, 0, 100, 0, 0, 128] }; + proof_view(&mut img, &ProofSetup { target: ProofTarget::WorkingCmyk, ..Default::default() }); + assert_eq!(&img.pixels[..4], &[0, 0, 0, 0]); + assert!(img.pixels[4] <= 128 && img.pixels[7] == 128); + } + #[test] fn renders_page_with_text_and_fill() { let mut d = Document::new(&NewDocument::default()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index def9ea2..1c1daec 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -499,6 +499,9 @@ fn render_worker_spawn(ctx: &egui::Context) -> Option "Preferences", "print" => "Print", "pdfImport" => "Place PDF", + "colorSettings" => "Color Settings", "keyboardShortcuts" => "Keyboard Shortcuts", "colorPicker" => "Color Picker", id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) { @@ -949,6 +950,48 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { }); }); } + "colorSettings" => { + let profiles = d.fields.get("profiles").and_then(Value::as_array).cloned().unwrap_or_default(); + egui::Grid::new("color_settings").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + for (key, label, kind) in [("rgb", "RGB:", "rgb"), ("cmyk", "CMYK:", "cmyk")] { + ui.label(label); + let cur = d.s(key); + egui::ComboBox::from_id_salt(("ws", key)).selected_text(&cur).width(260.0).show_ui(ui, |ui| { + for pr in profiles.iter().filter(|p| p["kind"] == kind) { + let name = pr["name"].as_str().unwrap_or(""); + if ui.selectable_label(name == cur, name).clicked() { + d.fields.insert(key.into(), json!(name)); + } + } + }); + ui.end_row(); + } + ui.label("Intent:"); + let cur = d.s("intent"); + egui::ComboBox::from_id_salt("ws_intent") + .selected_text(designcraft_color::cms::Intent::parse(&cur).map_or("", |i| i.label())) + .show_ui(ui, |ui| { + for i in designcraft_color::cms::Intent::ALL { + if ui.selectable_label(i.id() == cur, i.label()).clicked() { + d.fields.insert("intent".into(), json!(i.id())); + } + } + }); + ui.end_row(); + }); + check(ui, &mut d, "bpc", "Use Black Point Compensation"); + if ui.button("Load Profile…").clicked() + && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("icc")) + { + match app.session.execute("color.loadProfile", &json!({"path": path})) { + Ok(_) => { + let f = app.session.execute("color.settings", &json!({})).unwrap_or_default(); + d.fields.insert("profiles".into(), f["profiles"].clone()); + } + Err(e) => app.status(format!("Color Settings: {e}")), + } + } + } "keyboardShortcuts" => keyboard_shortcuts(app, ui, &mut d), "userDictionary" => user_dictionary(app, ui, &mut d), "menus" => { @@ -1281,6 +1324,12 @@ pub fn confirm(app: &mut DesignApp) -> Result { let hex = d.s("hex"); app.run("object.color", json!({"color": hex, "target": d.s("target")})) } + "colorSettings" => { + let r = app.run("color.settings", json!({"rgb": d.s("rgb"), "cmyk": d.s("cmyk"), "intent": d.s("intent"), "bpc": d.b("bpc")}))?; + app.ui.color_settings = Some(designcraft_color::cms::active_settings()); + app.canvas.shown = None; + Ok(r) + } "pdfImport" => { let crop = d.s("crop"); app.run("file.place", json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }})) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bdf47dd..ec0e459 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -103,6 +103,11 @@ pub struct UiState { /// Edit › Menus: hidden menu items (`menu/label`), and Show Full Menus. pub hidden_menu_items: Vec, pub show_full_menus: bool, + /// Edit › Color Settings (re-applied at start). + pub color_settings: Option, + /// View › Proof Colors and View › Proof Setup. + pub proof_colors: bool, + pub proof_setup: designcraft_color::cms::ProofSetup, /// Edit › Spelling › Dynamic Spelling: misspelled words underlined on the canvas. pub dynamic_spelling: bool, /// Preferences › Story Editor Display: text size (points). @@ -179,6 +184,9 @@ impl Default for UiState { hidden_menu_items: Vec::new(), show_full_menus: false, story_editor_size: 14.0, + color_settings: None, + proof_colors: false, + proof_setup: Default::default(), dynamic_spelling: false, language: String::new(), flattener: String::new(), @@ -313,6 +321,8 @@ pub struct DesignApp { pub other_pane: Option<(CanvasCache, Option)>, /// Power Zoom in progress: the zoom to return to and the canvas point it will centre on. pub power_zoom: Option<(f64, designcraft_geom::Point)>, + /// The persisted colour settings have been applied this session. + pub color_applied: bool, pub perf: Perf, pub synthetic: Vec, /// Story open in the Story Editor. @@ -348,6 +358,7 @@ impl DesignApp { focus_pane: 0, other_pane: None, power_zoom: None, + color_applied: false, perf: Perf::default(), synthetic: vec![], story_editor: None, @@ -489,6 +500,12 @@ impl DesignApp { /// Per-frame logic before layout. pub fn logic(&mut self, ctx: &egui::Context) { + if !self.color_applied { + self.color_applied = true; + if let Some(cs) = self.ui.color_settings.clone() { + let _ = designcraft_color::cms::set_active(&cs); + } + } let scale = self.ui.ui_scale.clamp(0.5, 3.0); if (ctx.zoom_factor() - scale).abs() > 1e-3 { ctx.set_zoom_factor(scale); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 2e823f0..5a65e3e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -73,6 +73,14 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.textThreads", "Show/Hide Text Threads", Some("Cmd+Alt+Y"), "{}"), ("view.hiddenCharacters", "Show/Hide Hidden Characters", Some("Cmd+Alt+I"), "{}"), ("view.taggedFrames", "Show/Hide Tagged Frames", None, "{} — XML-tagged frames outlined in their tag colour"), + ("view.proofColors", "Proof Colors", None, "{on?: bool} — simulate the proof target on screen"), + ( + "view.proofSetup", + "Proof Setup", + None, + "{target?: workingCmyk|srgb|legacyMacRgb|monitorRgb|protanopia|deuteranopia|cmyk:, intent?, simulatePaper?: bool|\"toggle\"}", + ), + ("app.colorSettings", "Color Settings…", None, "{} — the working spaces, intent and black-point compensation"), ("view.separations", "Separations Preview", None, "{plate?: cyan|magenta|yellow|black|null, inkLimit?: percent|null}"), ("view.overprintPreview", "Overprint Preview", Some("Cmd+Alt+Shift+Y"), "{} — show how overprinting inks mix"), ("view.fastDisplay", "Fast Display", Some("Cmd+Alt+Shift+Z"), "{} — placed graphics as grey boxes, no effects"), @@ -179,6 +187,7 @@ pub const MENUS: &[(&str, &[&str])] = &[ "cmd:edit.transparencyBlendSpace|Document RGB|{\"space\": \"rgb\"}", "cmd:edit.transparencyBlendSpace|Document CMYK|{\"space\": \"cmyk\"}", "<", + "ui:app.colorSettings", ">Interface Language", "ui:app.language|English|{\"lang\": \"\"}", "ui:app.language|Deutsch|{\"lang\": \"de\"}", @@ -320,6 +329,17 @@ pub const MENUS: &[(&str, &[&str])] = &[ "-", "ui:view.hiddenCharacters", "ui:view.taggedFrames", + ">Proof Setup", + "ui:view.proofSetup|Working CMYK|{\"target\": \"workingCmyk\"}", + "ui:view.proofSetup|Internet Standard RGB (sRGB)|{\"target\": \"srgb\"}", + "ui:view.proofSetup|Legacy Macintosh RGB|{\"target\": \"legacyMacRgb\"}", + "ui:view.proofSetup|Monitor RGB|{\"target\": \"monitorRgb\"}", + "ui:view.proofSetup|Color Blindness – Protanopia|{\"target\": \"protanopia\"}", + "ui:view.proofSetup|Color Blindness – Deuteranopia|{\"target\": \"deuteranopia\"}", + "-", + "ui:view.proofSetup|Simulate Paper Color|{\"simulatePaper\": \"toggle\"}", + "<", + "ui:view.proofColors", ">Separations Preview", "ui:view.separations|Off|{\"plate\": null, \"inkLimit\": null}", "ui:view.separations|Cyan|{\"plate\": \"cyan\"}", @@ -956,6 +976,34 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option flag(&mut app.ui.text_threads), "view.hiddenCharacters" => flag(&mut app.ui.hidden_characters), "view.taggedFrames" => flag(&mut app.ui.tagged_frames), + "view.proofColors" => { + app.ui.proof_colors = p.get("on").and_then(Value::as_bool).unwrap_or(!app.ui.proof_colors); + app.canvas.shown = None; + Ok(json!(app.ui.proof_colors)) + } + "view.proofSetup" => { + if let Some(t) = p.get("target").and_then(Value::as_str) { + let Some(t) = designcraft_color::cms::ProofTarget::parse(t) else { return Some(Err(format!("unknown proof target `{t}`"))) }; + app.ui.proof_setup.target = t; + app.ui.proof_colors = true; + } + if let Some(i) = p.get("intent").and_then(Value::as_str) { + let Some(i) = designcraft_color::cms::Intent::parse(i) else { return Some(Err(format!("unknown intent `{i}`"))) }; + app.ui.proof_setup.intent = i; + } + match p.get("simulatePaper") { + Some(Value::Bool(b)) => app.ui.proof_setup.simulate_paper = *b, + Some(Value::String(s)) if s == "toggle" => app.ui.proof_setup.simulate_paper = !app.ui.proof_setup.simulate_paper, + _ => {} + } + app.canvas.shown = None; + Ok(json!({"target": app.ui.proof_setup.target.id(), "simulatePaper": app.ui.proof_setup.simulate_paper, "on": app.ui.proof_colors})) + } + "app.colorSettings" => { + let f = app.session.execute("color.settings", &json!({})).unwrap_or_default(); + app.ui.dialog = Some(crate::dialogs::Dialog::new("colorSettings", f)); + Ok(Value::Null) + } "view.separations" => { if let Some(v) = p.get("plate") { app.ui.separation = v.as_str().and_then(|s| ["cyan", "magenta", "yellow", "black"].iter().position(|x| *x == s)).map(|i| i as u8); @@ -1440,6 +1488,12 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option { "changes.track" => app.session.active().is_some_and(|d| d.doc.settings.track_changes), "window.controlBar" => app.ui.control_bar, "window.split" => app.split, + "view.proofColors" => app.ui.proof_colors, + "view.proofSetup" => match (params.get("target").and_then(Value::as_str), params.get("simulatePaper")) { + (Some(t), _) => app.ui.proof_setup.target.id() == t, + (None, Some(_)) => app.ui.proof_setup.simulate_paper, + _ => return None, + }, "edit.dynamicSpelling" => app.ui.dynamic_spelling, "app.language" => app.ui.language == params.get("lang").and_then(Value::as_str).unwrap_or(""), "app.flattener" => app.ui.flattener == params.get("preset").and_then(Value::as_str).unwrap_or(""), @@ -1823,6 +1877,20 @@ mod tests { } } + #[test] + fn proof_setup_and_colors() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + assert_eq!(checked(&app, "view.proofColors", &Value::Null), Some(false)); + let r = run_ui(&mut app, "view.proofSetup", &json!({"target": "deuteranopia"})).unwrap().unwrap(); + assert_eq!(r["on"], true); + assert_eq!(checked(&app, "view.proofSetup", &json!({"target": "deuteranopia"})), Some(true)); + run_ui(&mut app, "view.proofSetup", &json!({"simulatePaper": "toggle"})).unwrap().unwrap(); + assert!(app.ui.proof_setup.simulate_paper); + run_ui(&mut app, "view.proofColors", &json!({})).unwrap().unwrap(); + assert!(!app.ui.proof_colors); + assert!(run_ui(&mut app, "view.proofSetup", &json!({"target": "nope"})).unwrap().is_err()); + } + #[test] fn panels_float_and_dock() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/parity.md b/docs/parity.md index 65c0498..9033559 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Priority | Features | Done | Partial | Missing | Parity | |---|---|---|---|---|---| | P0 | 102 | 100 | 2 | 0 | 99% | -| P1 | 97 | 92 | 5 | 0 | 97% | +| P1 | 97 | 93 | 4 | 0 | 98% | | P2 | 76 | 41 | 29 | 6 | 73% | | Area | Parity | @@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Layers | 95% | | Frames, shapes & paths | 100% | | Transform | 100% | -| Fill, stroke, colour | 96% | +| Fill, stroke, colour | 99% | | Effects & transparency | 75% | | Placing & links | 91% | | Type & text frames | 95% | @@ -153,7 +153,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print) | Fill, stroke, colour | Unnamed colour to swatch | P1 | D | | Fill, stroke, colour | Stroke panel | P0 | D | | Fill, stroke, colour | Stroke styles | P1 | D | -| Fill, stroke, colour | Color management | P1 | P | +| Fill, stroke, colour | Color management | P1 | D | | Fill, stroke, colour | Overprint Preview | P1 | D | | Fill, stroke, colour | Separations Preview | P2 | P | | Fill, stroke, colour | Eyedropper | P1 | D |