From 5e9df83dd4a6b1637394ac58871b8590798fabb9 Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Thu, 22 Oct 2026 18:59:53 +0200 Subject: [PATCH] Fix #3646: display PDF document title --- ROADMAP.md | 1 + crates/pdf/src/export.rs | 5 ++++- crates/pdf/src/output.rs | 16 +++++++++++----- crates/pdf/src/tests.rs | 2 ++ docs/gaps.md | 1 + docs/roadmap.md | 2 +- docs/target-app-parity.md | 2 +- 7 files changed, 21 insertions(+), 8 deletions(-) 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/pdf/src/export.rs b/crates/pdf/src/export.rs index f30665121..64575651d 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -233,6 +233,8 @@ pub(crate) struct Writer { rgb_in_cmyk: RgbInCmyk, /// A PDF 1.3 file ([`crate::PdfSettings::pdf13`]): flat, without page groups. pdf13: bool, + /// The PDF has a document title, so viewers should display it instead of the file name. + display_doc_title: bool, /// The document's images the exporters wrote, by key, gathered from the exporters /// ([`Self::absorb`]): [`Self::finish`] finds the key of an image the writer couldn't decode. images: Vec<(String, Image)>, @@ -317,6 +319,7 @@ impl Writer { warnings, rgb_in_cmyk: RgbInCmyk::default(), pdf13: set.pdf13(), + display_doc_title: !title.trim().is_empty(), images: vec![], }) } @@ -440,7 +443,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, self.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/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/tests.rs b/crates/pdf/src/tests.rs index 945d99cd4..91f0a2dd7 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -902,6 +902,7 @@ fn empty_and_populated_file_info_preserve_key_presence() { for key in ["/Title", "/Author", "/Keywords", "", "", ""] { assert!(!s.contains(key), "unexpected empty metadata {key}"); } + assert!(!s.contains("/DisplayDocTitle true"), "an untitled document has no title to display"); d.title = "Synthetic title".into(); d.metadata.author = " Test author ".into(); d.metadata.keywords = vec!["test keyword".into()]; @@ -911,6 +912,7 @@ fn empty_and_populated_file_info_preserve_key_presence() { 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..865d9eeee 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 the exported document has a title, so viewers and accessibility checkers use the document title instead of the file name (#3646). - **Estimate:** 15–25 h. ## Highlighted-text Eyedropper (2026-10-21) 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