diff --git a/ROADMAP.md b/ROADMAP.md index 5ad4493a5..83a934727 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-10-22: Effect › Sketch › Conté Crayon: a black crayon in the dark areas and a white one in the light areas, on mid-grey paper whose texture (Brick, Burlap, Canvas, Sandstone; Scaling, Relief, Light Direction, Invert) breaks the strokes, with Foreground Level and Background Level; an original approximation, defaults unverified. Every Photoshop-style filter is now in (57/57); the Effect Gallery remains (4–6 h). - 2026-10-22: Ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range of the selected type — above it in horizontal type, right of the column in vertical type (or below/left), start/centre/end aligned, at 1–100% of the base size (default half), overhanging or squeezed to the base width. It is saved in the native file, laid out with its line growing (following lines move on), drawn on the canvas, and written in SVG and PDF as outlines where ruby is present. UI panel and type on a path are next. Headline numbers unchanged. +- 2026-10-22: PDF accessibility: titled exports set `/ViewerPreferences << /DisplayDocTitle true >>`, so viewers show the document title rather than the file name and title checks can pass (#3646). Headline numbers unchanged. - 2026-10-14: Document fonts register every explicitly supplied craft-fonts family after the existing Japanese, Arabic and Thai prefix; optional empty input remains supported. Headline scores and estimates are unchanged. diff --git a/crates/engine/src/cmd/fileio/export.rs b/crates/engine/src/cmd/fileio/export.rs index 9dce9a321..dd7307e84 100644 --- a/crates/engine/src/cmd/fileio/export.rs +++ b/crates/engine/src/cmd/fileio/export.rs @@ -19,8 +19,15 @@ pub(super) fn serialize(s: &mut Session, p: &Value) -> Result { let f = writable("document.serialize", Some(str_param(p, "format").unwrap_or("vectorcraft")), None)?; let expanded = expand_presets(s, "document.serialize", p)?; let p = with_black_output(s, &expanded); - let doc = &*source(s, f, &p, "document.serialize")?; - let enc = encode_all(doc, f.id, &p)?; + let (doc, display_doc_title) = source(s, f, &p, "document.serialize")?; + let doc = &*doc; + let mut q = p.clone(); + if f.id == "pdf" + && let Some(o) = q.as_object_mut() + { + o.insert("_displayDocTitle".into(), json!(display_doc_title)); + } + let enc = encode_all(doc, f.id, &q)?; let files = enc.named(doc, &default_name(doc, f.extensions[0])); let (main, linked) = files.split_at(enc.files.len()); // Text formats come back as text; an SVG in UTF-16 or ISO 8859-1 also as its bytes. @@ -53,8 +60,15 @@ pub(super) fn export(s: &mut Session, p: &Value) -> Result { let f = writable("document.export", str_param(p, "format"), path)?; let expanded = expand_presets(s, "document.export", p)?; let p = with_black_output(s, &expanded); - let doc = &*source(s, f, &p, "document.export")?; - let enc = encode_all(doc, f.id, &p)?; + let (doc, display_doc_title) = source(s, f, &p, "document.export")?; + let doc = &*doc; + let mut q = p.clone(); + if f.id == "pdf" + && let Some(o) = q.as_object_mut() + { + o.insert("_displayDocTitle".into(), json!(display_doc_title)); + } + let enc = encode_all(doc, f.id, &q)?; write_encoded(path, &default_name(doc, f.extensions[0]), doc, &enc, json!({ "format": f.id, "warnings": enc.warnings })) } @@ -154,13 +168,16 @@ fn expand_presets<'a>(s: &Session, cmd: &str, p: &'a Value) -> Result(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result> { +fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<(Cow<'a, Document>, bool)> { if f.id == "txt" && bool_or(p, "selectionOnly", false) { - return Ok(Cow::Owned(selection(s, cmd)?.0)); + let doc = selection(s, cmd)?.0; + let display_doc_title = super::has_own_title(&doc); + return Ok((Cow::Owned(doc), display_doc_title)); } let st = s.doc()?; let doc = export_source(st, p)?; - Ok(super::titled_for_export(&doc, st.path.as_deref(), f.id).map_or(doc, Cow::Owned)) + let display_doc_title = super::has_own_title(&doc); + Ok((super::titled_for_export(&doc, st.path.as_deref(), f.id).map_or(doc, Cow::Owned), display_doc_title)) } /// The selected objects (not on template layers) alone on one layer, with one artboard: their diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 3931defdb..77fde102b 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -871,9 +871,12 @@ pub fn file_stem(name: &str) -> String { /// document that only has the Untitled-N name New gave it is titled after its file, as its tab is /// (the title PDF, PNG and SVG metadata carry). A title of its own stays, and native files keep /// the stored title. +pub fn has_own_title(doc: &vectorcraft_doc::Document) -> bool { + !doc.title.trim().is_empty() && !doc.title.strip_prefix("Untitled-").is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) +} + pub fn titled_for_export(doc: &vectorcraft_doc::Document, path: Option<&str>, format: &str) -> Option { - let untitled = doc.title.strip_prefix("Untitled-").is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())); - let path = path.filter(|_| untitled && !matches!(format, "vectorcraft" | "template" | "ai"))?; + let path = path.filter(|_| !has_own_title(doc) && !matches!(format, "vectorcraft" | "template" | "ai"))?; let mut d = doc.clone(); d.title = file_stem(path); Some(d) diff --git a/crates/engine/src/cmd/fileio/pdf.rs b/crates/engine/src/cmd/fileio/pdf.rs index c1ecbb2e6..df512f629 100644 --- a/crates/engine/src/cmd/fileio/pdf.rs +++ b/crates/engine/src/cmd/fileio/pdf.rs @@ -243,7 +243,14 @@ pub fn options(cmd: &str, doc: &Document, p: &Value) -> Result { let pick = if p.is_object() { ArtboardPick::deserialize(p).map_err(|e| bad(cmd, format!("PDF options: {e}")))? } else { ArtboardPick::default() }; let artboards = pick.resolve(doc.artboards.len()).map_err(|e| bad(cmd, e))?; let now = vectorcraft_doc::metadata::output_time(); - Ok(PdfOptions { settings: settings(cmd, p)?, artboards, created: doc.metadata.created.or(now), modified: now, ..Default::default() }) + Ok(PdfOptions { + settings: settings(cmd, p)?, + artboards, + display_doc_title: bool_or(p, "_displayDocTitle", super::has_own_title(doc)), + created: doc.metadata.created.or(now), + modified: now, + ..Default::default() + }) } /// Why a PDF of some artboards carries no editing data. @@ -387,8 +394,15 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result { let expanded = expand_preset(s, C, p)?; let p = &*expanded; let st = s.doc()?; + let display_doc_title = super::has_own_title(&st.doc); let titled = super::titled_for_export(&st.doc, st.path.as_deref(), "pdf"); - let (bytes, warnings) = encode(C, titled.as_ref().unwrap_or(&st.doc), p)?; + let mut q = p.clone(); + if let Some(o) = q.as_object_mut() { + o.insert("_displayDocTitle".into(), json!(display_doc_title)); + } else { + q = json!({ "_displayDocTitle": display_doc_title }); + } + let (bytes, warnings) = encode(C, titled.as_ref().unwrap_or(&st.doc), &q)?; write_or_return(str_param(p, "path"), &bytes, json!({ "warnings": warnings })) } diff --git a/crates/engine/src/tests_fileinfo.rs b/crates/engine/src/tests_fileinfo.rs index cb42c223b..09d1efd17 100644 --- a/crates/engine/src/tests_fileinfo.rs +++ b/crates/engine/src/tests_fileinfo.rs @@ -310,6 +310,18 @@ fn an_untitled_document_saved_to_a_file_exports_titled_after_it() { let dir = std::env::temp_dir().join(format!("vc-pdftitle-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("Document C.vectorcraft"); + // Unsaved and untitled: viewers keep showing the file name, not "Untitled-1". + let mut untitled = Session::new(); + untitled.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); + for pdf in pdf_texts(&mut untitled) { + assert!(!pdf.contains("/DisplayDocTitle true"), "{pdf}"); + } + // An explicitly empty document title is also not a title to display. + std::sync::Arc::make_mut(&mut untitled.doc_mut().unwrap().doc).title.clear(); + for pdf in pdf_texts(&mut untitled) { + assert!(!pdf.contains("/DisplayDocTitle true"), "{pdf}"); + } + // Saved as a file, then exported: Untitled-1 was only the name New gave it. let mut s = Session::new(); s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); @@ -317,6 +329,9 @@ fn an_untitled_document_saved_to_a_file_exports_titled_after_it() { for pdf in pdf_texts(&mut s) { assert!(pdf.contains("/Title(Document C)") && xmp_title(&pdf).contains("Document C"), "{}", xmp_title(&pdf)); } + // Non-object parameters still preserve that the file-stem title was not user supplied. + let pdf = String::from_utf8_lossy(&b64(&s.execute("document.exportPdf", &Value::Null).unwrap())).into_owned(); + assert!(!pdf.contains("/DisplayDocTitle true"), "{pdf}"); // Opened from that file, in another session. let mut s = Session::new(); s.execute("document.open", &json!({"path": path.to_str().unwrap()})).unwrap(); @@ -330,6 +345,7 @@ fn an_untitled_document_saved_to_a_file_exports_titled_after_it() { s.execute("document.save", &json!({"path": dir.join("Document D.vectorcraft").to_str().unwrap()})).unwrap(); for pdf in pdf_texts(&mut s) { assert!(pdf.contains("/Title(Poster)") && xmp_title(&pdf).contains("Poster")); + assert!(pdf.contains("/ViewerPreferences<>"), "{pdf}"); } let _ = std::fs::remove_dir_all(dir); } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index f30665121..b155c49ea 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -73,6 +73,7 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha let now = vectorcraft_doc::metadata::output_time(); let created_at = opts.created.or(now); crate::pdfx::require_date(set.standard, created_at)?; + let display_doc_title = opts.display_doc_title || opts.title.as_deref().is_some_and(|title| !title.trim().is_empty()); let mut w = Writer::new(doc, set, &title, created_at, doc.color_mode == vectorcraft_doc::ColorMode::Cmyk)?; // Preserve Editing: the native document as an embedded file (`check` refused PDF/A with it). let mut warnings = set.warnings(); @@ -132,7 +133,7 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; let (layers, overprinted, invisible) = (ex.layers, ex.overprinted, ex.invisible); w.absorb(ex); - let (bytes, more) = w.finish(left_out)?; + let (bytes, more) = w.finish(left_out, display_doc_title)?; let bytes = crate::forms::finish(bytes, doc, set, layers, overprinted, invisible)?; // The writer gives the file its creation date as its modification date too. let bytes = match opts.modified.filter(|m| Some(*m) != created_at) { @@ -425,7 +426,7 @@ impl Writer { /// standard) and the warnings of its drawing. PDF/X-1a spot colours keep their CMYK /// alternates. When the writer couldn't decode one of the images, its key is added to /// `left_out` ([`leaving_out_undecodable`]) and the writer's error is returned. - pub(crate) fn finish(mut self, left_out: &mut HashSet) -> Result<(Vec, Vec), PdfError> { + pub(crate) fn finish(mut self, left_out: &mut HashSet, display_doc_title: bool) -> Result<(Vec, Vec), PdfError> { let images = std::mem::take(&mut self.images); let mut bytes = self.pdf.finish().map_err(|e| { if let write::WriteError::Serialization(k) = &e @@ -440,7 +441,7 @@ impl Writer { } let lab = !self.lab_spots.is_empty() && !self.out.standard.cmyk_only(); let bytes = if lab { crate::lab_spot::lab_alternates(bytes, &self.lab_spots) } else { bytes }; - let (bytes, more) = self.out.write_catalog(bytes)?; + let (bytes, more) = self.out.write_catalog(bytes, display_doc_title)?; self.warnings.extend(more); self.warnings.extend(self.rgb_in_cmyk.warning()); Ok((crate::pdfx::finish(bytes, self.out.standard, self.pdf13)?, self.warnings)) diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 1e2b796ad..55990cafa 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -94,6 +94,8 @@ pub struct PdfOptions { pub artboards: Option>, /// Document title for the metadata; `None` = the document's title. pub title: Option, + /// Ask PDF viewers to display the document title instead of the file name. + pub display_doc_title: bool, /// Creation date as Unix seconds (UTC); `None` = now (native) / omitted (wasm). PDF/A needs a date. pub created: Option, /// Modification date as Unix seconds (UTC); `None` = the creation date. diff --git a/crates/pdf/src/output.rs b/crates/pdf/src/output.rs index 1fb13c8ec..55ab42ced 100644 --- a/crates/pdf/src/output.rs +++ b/crates/pdf/src/output.rs @@ -350,14 +350,17 @@ impl ColorOut { /// `pdf` with the output intent and Trapped entries, and a PDF/X file's identification /// ([`crate::pdfx`]) → (the file, warnings). - pub(crate) fn write_catalog(&self, pdf: Vec) -> Result<(Vec, Vec), PdfError> { - let Some(cat) = &self.catalog else { return Ok((pdf, vec![])) }; + pub(crate) fn write_catalog(&self, pdf: Vec, display_doc_title: bool) -> Result<(Vec, Vec), PdfError> { + let cat = self.catalog.as_ref(); + if cat.is_none() && !display_doc_title { + return Ok((pdf, vec![])); + } let xref = Xref::read(&pdf).ok_or_else(|| PdfError::Write("the written PDF has no cross-reference table".into()))?; let mut patch = Patch::new(&xref); let mut warnings = vec![]; let root = xref.trailer_ref(&pdf, b"/Root").and_then(|n| xref.dict(&pdf, n)); let Some(root) = root else { return Err(PdfError::Write("the written PDF has no catalog".into())) }; - if let Some(OutputIntent { id, condition, registry, info, profile }) = &cat.intent { + if let Some(OutputIntent { id, condition, registry, info, profile }) = cat.and_then(|c| c.intent.as_ref()) { if crate::lab_spot::find(pdf.get(root.0..root.1).unwrap_or_default(), b"/OutputIntents", 0).is_some() { warnings.push("the file has an output intent already: the one asked for is left out".into()); } else { @@ -388,9 +391,12 @@ impl ColorOut { patch.replace(root.0, root.0, format!("/OutputIntents[{oi} 0 R]").into_bytes()); } } + if display_doc_title && crate::lab_spot::find(pdf.get(root.0..root.1).unwrap_or_default(), b"/ViewerPreferences", 0).is_none() { + patch.replace(root.0, root.0, b"/ViewerPreferences<>".to_vec()); + } let info = xref.trailer_ref(&pdf, b"/Info").and_then(|n| xref.dict(&pdf, n)); let mut entries = String::new(); - if let Some(trapped) = cat.trapped + if let Some(trapped) = cat.and_then(|c| c.trapped) && info.is_none_or(|(s, e)| crate::lab_spot::find(pdf.get(s..e).unwrap_or_default(), b"/Trapped", 0).is_none()) { entries.push_str(if trapped { "/Trapped/True" } else { "/Trapped/False" }); @@ -404,7 +410,7 @@ impl ColorOut { patch.trailer_entry(format!("/Info {n} 0 R").as_bytes()); } } - crate::pdfx::xmp(&pdf, &xref, root, &mut patch, self.standard, cat.trapped == Some(true))?; + crate::pdfx::xmp(&pdf, &xref, root, &mut patch, self.standard, cat.and_then(|c| c.trapped) == Some(true))?; Ok((patch.apply(&pdf, &xref)?, warnings)) } } diff --git a/crates/pdf/src/print/mod.rs b/crates/pdf/src/print/mod.rs index 2bd8f01b0..d207fac8f 100644 --- a/crates/pdf/src/print/mod.rs +++ b/crates/pdf/src/print/mod.rs @@ -426,7 +426,7 @@ fn print_inner(doc: &Document, opts: &PrintOptions, left_out: &mut HashSet", "", ""] { assert!(!s.contains(key), "unexpected empty metadata {key}"); } + assert!(!s.contains("/DisplayDocTitle true"), "the default export does not request displaying its metadata title"); d.title = "Synthetic title".into(); d.metadata.author = " Test author ".into(); d.metadata.keywords = vec!["test keyword".into()]; - let bytes = export(&d, &PdfOptions { created: Some(1_700_000_000), ..PdfOptions::uncompressed() }).unwrap(); + let bytes = export(&d, &PdfOptions { display_doc_title: true, created: Some(1_700_000_000), ..PdfOptions::uncompressed() }).unwrap(); let s = String::from_utf8_lossy(&bytes); for key in ["/Title", "/Author", "/Keywords", "/CreationDate", "", "", ""] { assert!(s.contains(key), "missing populated metadata {key}"); } assert!(s.contains("Synthetic title") && s.contains("Test author") && s.contains("test keyword")); + assert!(s.contains("/ViewerPreferences<>"), "viewers should show the document title (#3646)"); } /// A function or colour space built on itself is refused before the interpreter builds it (by diff --git a/docs/gaps.md b/docs/gaps.md index 1fa9754dc..2d5611de9 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -422,6 +422,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall - **Missing:** AccessKit is on by default, so egui widgets reach screen readers, but custom panels, the canvas and its objects aren't described and nothing has been tested with VoiceOver, Narrator or Orca; keyboard-only canvas operation; high contrast. Illustrator's own accessibility is limited, so this is mostly beyond parity. +- **Done:** PDF export writes `/ViewerPreferences << /DisplayDocTitle true >>` when File Info has a Document Title (or an explicit export title is supplied), so viewers and accessibility checkers use that title instead of the file name (#3646). - **Estimate:** 15–25 h. ## Highlighted-text Eyedropper (2026-10-21) @@ -444,6 +445,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | G19: PDF export asks viewers to display the document title only when File Info has a Document Title or an explicit export title is supplied; untitled documents keep the PDF file name in the viewer title bar (#3646); scores and estimates unchanged. | | 2026-10-22 | minor | G4: colour emoji fonts with bitmap glyphs (`sbix`: Apple Color Emoji; `CBDT`: Noto Color Emoji) draw on the canvas and export: PDF Type 3 glyphs or images, SVG images with outlined text, EPS and legacy `.ai` images, Create Outlines images; COLR and OpenType SVG fonts remain; estimate unchanged. | | 2026-10-22 | minor | G13: gradient Dither (30.0) in the Gradient panel, on the canvas, in raster exports and as images in SVG and PDF exports; G13 estimate 15–25 h → 14–24 h. | | 2026-10-22 | minor | G2: Effect › Sketch › Conté Crayon (Foreground Level, Background Level, Texture, Scaling, Relief, Light Direction, Invert; a black crayon in the dark areas and a white one in the light areas on textured mid-grey paper; ranges and defaults unverified VectorCraft choices); measured Photoshop-style filters 57/57, the Effect Gallery remains (4–6 h). | diff --git a/docs/roadmap.md b/docs/roadmap.md index 85522391f..2fd320469 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -88,7 +88,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; broader scripting and batch work remains | 18–27 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 | -| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Dock groups collapse to their tabs by a double-click on a tab (UI.8); Numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); accessibility and broader Windows/Linux packaging/QA pending | 18–28 | +| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Dock groups collapse to their tabs by a double-click on a tab (UI.8); Numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending | 18–28 | | — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed) | ⬜ (new ruler-guide Alt orientation implemented; existing-guide Escape cancellation regression-tested; broader pass remains) | 60–90 | | — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 PngSuite integrity and bounded fetching verified (#1729); real-world corpus/manual QA remain | 50–80 | | M14 follow-up | Color Picker layout (#2008) | ✅ Compact controls, centered header, stacked actions, aligned fields and screen sampling; narrow windows scroll vertically | Included above | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index dcca19a9c..26909b6fa 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -380,7 +380,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Libraries, Links, Package | 2 | 82% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles (#745), its search, groups and library export/import as `.vclibrary` files (#926), and a list that scrolls, with items and groups reordered by dragging and items dragged into and out of groups, are done; Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 | | **Feature parity** | **103** | **~75%** | | **~210–330** | | Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 | -| Hardening (big-file corpus, fuzzing, cross-platform and browser QA, accessibility, packaging) | | | Bounded control socket waits (#1726); PngSuite pinned-file integrity and download budgets verified (#1729); RPM runtime capabilities and glibc floor checked (#1275); real-distro installation/start-up QA pending | 50–80 | +| Hardening (big-file corpus, fuzzing, cross-platform and browser QA, accessibility, packaging) | | | Bounded control socket waits (#1726); PngSuite pinned-file integrity and download budgets verified (#1729); RPM runtime capabilities and glibc floor checked (#1275); titled PDF exports ask viewers to display the document title (#3646); real-distro installation/start-up QA pending | 50–80 | | **Full parity** | | **~56%** | | **~320–500** | _Inventories (2026-10-07):_ 13 menu items still stubbed (`todo(…)` in `crates/ui-egui/src/menus.rs`); every tool