From 0820eaebb71bfd2047f32d198f8616a3d4e994df Mon Sep 17 00:00:00 2001 From: Mauro Mazzieri Date: Tue, 13 Oct 2026 14:32:14 +0200 Subject: [PATCH] #1290: write the watermark to DOCX as a VML text shape in the headers The DOCX writer dropped settings.watermark, so a watermark read from Word (or set in WordCraft) was lost on save. Every header part now starts its first paragraph with a VML shape (ECMA-376 Part 4) whose v:textpath carries the text, font, fill colour, semitransparency (v:fill opacity) and diagonal turn, centred on the margins behind the text, with the reader's PowerPlusWaterMarkObject id so it reads back as the watermark, not a shape. The shape type is our own: a straight path the text follows, fitted to the box. Slots the pages show with no header of their own or linked from an earlier section (default; first page with a title page; even pages with even/odd headers) get a header holding only the watermark; linked sections stay linked. A blank watermark isn't written. Check Compatibility no longer warns about watermarks. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 6 +- crates/docx/src/write/mod.rs | 20 ++++- crates/docx/src/write/story.rs | 44 +++++++++- crates/docx/src/write/watermark.rs | 55 +++++++++++++ crates/docx/tests/roundtrip.rs | 128 ++++++++++++++++++++++++++++- crates/engine/src/cmd/tools.rs | 3 - docs/file-format-parity.md | 5 +- docs/gaps.md | 5 +- 8 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 crates/docx/src/write/watermark.rs diff --git a/ROADMAP.md b/ROADMAP.md index af0e6fb68..2b1b611a8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: Word's text watermark read from and written to DOCX, #1290; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -135,7 +135,8 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| -| 2026-10-13 | DOCX files with Word's text watermark (Design › Watermark, a VML text shape in the headers) show it on every page on screen and in the PDF (#1290); it isn't written back to DOCX yet | +| 2026-10-13 | Saving to DOCX keeps the watermark: it is written as Word's own text-watermark shape in every header, and a document without headers gains one to hold it (#1290) | +| 2026-10-13 | DOCX files with Word's text watermark (Design › Watermark, a VML text shape in the headers) show it on every page on screen and in the PDF (#1290) | | 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word | | 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them | | 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started | @@ -274,6 +275,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Progress log: the watermark written to DOCX (#1290) | | 2026-10-13 | trivial | Progress log: Word's text watermark read from DOCX (#1290) | | 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) | | 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) | diff --git a/crates/docx/src/write/mod.rs b/crates/docx/src/write/mod.rs index bb6fb98bb..47c904331 100644 --- a/crates/docx/src/write/mod.rs +++ b/crates/docx/src/write/mod.rs @@ -5,6 +5,7 @@ mod math; mod props; mod sdt; mod story; +mod watermark; mod wordart; use std::collections::{BTreeMap, HashMap}; @@ -81,6 +82,13 @@ pub(crate) struct Writer<'d> { endnotes: Vec, /// Header/footer part id → (file name, is footer). hf: Vec<(u32, String, bool)>, + /// The headers each slot has so far in body order, with sections' empty slots linked to the + /// ones before (as layout shows them): where a watermark needs a header of its own. + linked_headers: wordcraft_doc::section::HeaderSet, + /// The part id standing for a header written only to hold the watermark (no such part). + wm_header: u32, + /// The watermark shape to put at the start of the next top-level paragraph (a header's first). + pending_watermark: Option, /// Paragraphs that need a `w14:paraId` (last paragraph of each comment), by address. para_ids: HashMap, /// A note reference mark to put at the start of the next paragraph. @@ -147,6 +155,9 @@ pub(crate) fn write_parts(doc: &Document, flavor: Flavor) -> Result Result watermark::run(wm, done, &doc.last_section), + _ => None, + }; wr.blocks(&mut w, &blocks, &mut prels, false, 0); + // Always taken: the story ends with a top-level paragraph. + wr.pending_watermark = None; w.close(tag); push_part( &mut entries, @@ -589,7 +607,7 @@ impl Writer<'_> { } fn hf_rel(&mut self, part: u32, footer: bool, rels: &mut PartRels) -> Option { - let kind = self.doc.parts.get(&part)?.kind; + let kind = if part == self.wm_header && !footer { PartKind::Header } else { self.doc.parts.get(&part)?.kind }; if !matches!(kind, PartKind::Header | PartKind::Footer) { return None; } diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs index 8d87d6166..653a760d6 100644 --- a/crates/docx/src/write/story.rs +++ b/crates/docx/src/write/story.rs @@ -74,7 +74,16 @@ impl Writer<'_> { self.move_ranges(w, &[]); } if bl.last().is_none_or(|b| matches!(**b, Block::Table(_))) { - w.raw(""); + // A header with no paragraph of its own before: the watermark goes in this one. + if depth == 0 + && let Some(run) = self.pending_watermark.take() + { + w.raw(""); + w.raw(&run); + w.raw(""); + } else { + w.raw(""); + } } } @@ -207,6 +216,12 @@ impl Writer<'_> { w.empty(mark, &[]); w.raw(""); } + // A header's watermark, in its first top-level paragraph (not a table cell or text box). + if depth == 0 + && let Some(run) = self.pending_watermark.take() + { + w.raw(&run); + } // Taken before the content so a nested story (a text box in an entry) can't claim them. if std::mem::take(&mut self.toc_begin_here) && let Some((instr, locked, props)) = self.toc_field.take() @@ -1196,9 +1211,34 @@ impl Writer<'_> { w.close("w:tbl"); } + /// The section's headers, plus the header holding only the watermark in each slot the + /// pages show (first page with a title page, even pages with even/odd headers) that has no + /// header of its own or linked from an earlier section. Sections are written in body order. + fn watermark_headers(&mut self, s: &SectionProps) -> wordcraft_doc::section::HeaderSet { + let mut set = s.headers; + let linked = &mut self.linked_headers; + if self.doc.settings.watermark.as_ref().and_then(super::watermark::text).is_some() { + let id = self.wm_header; + if set.default.or(linked.default).is_none() { + set.default = Some(id); + } + if s.title_page && set.first.or(linked.first).is_none() { + set.first = Some(id); + } + if self.doc.settings.even_odd_headers && set.even.or(linked.even).is_none() { + set.even = Some(id); + } + } + linked.default = set.default.or(linked.default); + linked.first = set.first.or(linked.first); + linked.even = set.even.or(linked.even); + set + } + pub fn sectpr(&mut self, w: &mut W, s: &SectionProps, rels: &mut PartRels) { w.open("w:sectPr", &[]); - for (set, footer) in [(&s.headers, false), (&s.footers, true)] { + let headers = self.watermark_headers(s); + for (set, footer) in [(&headers, false), (&s.footers, true)] { for (kind, id) in [("default", set.default), ("first", set.first), ("even", set.even)] { let Some(id) = id else { continue }; if let Some(rid) = self.hf_rel(id, footer, rels) { diff --git a/crates/docx/src/write/watermark.rs b/crates/docx/src/write/watermark.rs new file mode 100644 index 000000000..a45166d0d --- /dev/null +++ b/crates/docx/src/write/watermark.rs @@ -0,0 +1,55 @@ +//! The document's text watermark as a VML text shape in the headers (ECMA-376 Part 4, VML: +//! `shape`, `shapetype`, `path`, `textpath`, `fill`), which is how a WordprocessingML consumer +//! draws text behind every page. The reader (`read::story::vml_watermark`) maps it back. + +use wordcraft_doc::{SectionProps, Watermark}; + +use crate::xml::esc; + +/// The shape type the watermark shape uses: a straight path across the middle of the box that +/// the `v:textpath` text follows, stretched to fit the shape (`fitshape`). +const SHAPETYPE_ID: &str = "_x0000_t136"; + +/// The watermark text as written: what the reader accepts (non-blank, at most 200 characters). +pub(super) fn text(wm: &Watermark) -> Option { + let t: String = wm.text.chars().take(200).collect(); + (!t.trim().is_empty()).then_some(t) +} + +/// A `w:r` holding the watermark shape (and its shape type), for the first paragraph of header +/// part number `n`. Sized like the renderers draw it: half the page width across, or half the +/// page diagonal when diagonal; the height follows the text's length. +pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option { + let text = text(wm)?; + let (pw, ph) = (page.page_w.clamp(72.0, 4000.0), page.page_h.clamp(72.0, 4000.0)); + let across = if wm.diagonal { (pw * pw + ph * ph).sqrt() } else { pw }; + let width = (across * 0.5).clamp(36.0, 2000.0); + let chars = text.chars().count().clamp(1, 200) as f32; + let height = (width / (0.55 * chars)).clamp(12.0, 400.0); + // A font name can't break out of the textpath's CSS-like style. + let font: String = wm.font.chars().filter(|c| !matches!(c, '"' | '\'' | ';' | ':')).take(100).collect(); + let font = if font.trim().is_empty() { Watermark::default().font } else { font }; + let rotation = if wm.diagonal { "rotation:315;" } else { "" }; + let style = format!( + "position:absolute;margin-left:0;margin-top:0;width:{width:.1}pt;height:{height:.1}pt;{rotation}z-index:-251654144;\ + mso-position-horizontal:center;mso-position-horizontal-relative:margin;\ + mso-position-vertical:center;mso-position-vertical-relative:margin" + ); + let fill = if wm.semitransparent { r#""# } else { "" }; + Some(format!( + concat!( + r#""#, + r#""#, + r#""#, + r##""##, + r#"{fill}"# + ), + st = SHAPETYPE_ID, + n = n, + style = esc(&style), + color = wm.color.hex(), + fill = fill, + tstyle = esc(&format!("font-family:\"{font}\";font-size:1pt")), + text = esc(&text), + )) +} diff --git a/crates/docx/tests/roundtrip.rs b/crates/docx/tests/roundtrip.rs index 791cf203f..8ff4a2151 100644 --- a/crates/docx/tests/roundtrip.rs +++ b/crates/docx/tests/roundtrip.rs @@ -1234,10 +1234,134 @@ fn settings_core_theme_round_trip() { let r = rt(&d); assert_eq!(r.settings, d.settings); assert_eq!(r.core, d.core); - // Watermarks aren't stored in DOCX by this crate (yet). + // #1290: the watermark is written as a VML text shape in the headers and read back. let mut w = d.clone(); w.settings.watermark = Some(Watermark::default()); - assert_eq!(rt(&w).settings.watermark, None); + assert_eq!(rt(&w).settings.watermark, w.settings.watermark); +} + +fn part_xml(bytes: &[u8], name: &str) -> String { + let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap(); + let mut xml = String::new(); + std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut xml).unwrap(); + xml +} + +/// Header parts in the document, by id. +fn header_ids(doc: &Document) -> Vec { + doc.parts.iter().filter(|(_, p)| p.kind == PartKind::Header).map(|(id, _)| *id).collect() +} + +/// #1290: a watermark round-trips through Word's VML text shape: text, font, colour, diagonal and +/// semitransparent, defaults or not. +#[test] +fn watermark_roundtrips() { + for wm in [ + Watermark::default(), + Watermark { + text: "Riservato & ".into(), + font: "Georgia".into(), + color: Rgb(0x80, 0x10, 0x20), + diagonal: false, + semitransparent: false, + }, + ] { + let mut d = doc_with(vec![Paragraph::with_text("Body", CharProps::default())]); + d.settings.watermark = Some(wm.clone()); + let r = rt(&d); + assert_eq!(r.settings.watermark, Some(wm)); + assert_eq!(paras(&r).len(), 1, "the body gains nothing"); + } +} + +/// #1290: a document without headers gains one default header holding only the watermark (an +/// empty paragraph once read back); saving again neither adds another nor duplicates the shape. +#[test] +fn watermark_without_header_gains_one() { + let mut d = doc_with(vec![Paragraph::with_text("Body", CharProps::default())]); + d.settings.watermark = Some(Watermark::default()); + let bytes = wordcraft_docx::write(&d).unwrap(); + let hdr = part_xml(&bytes, "word/header1.xml"); + assert_eq!(hdr.matches(""#)); + assert!(part_xml(&bytes, "word/document.xml").contains(r#" Vec { .pure(), CommandSpec::new("file.compatibility", "Check Compatibility", "File › Info", |s, _| { let mut issues = Vec::new(); - if s.doc.settings.watermark.is_some() { - issues.push("Watermarks are kept in WordCraft documents but not yet written to .docx."); - } if s.doc .para_paths(StoryRef::Body) .iter() diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index af09a8631..2de036dad 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with Microsoft Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** trivial (Word's text watermark read from DOCX headers, #1290; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** trivial (Word's text watermark written back to DOCX headers, #1290; previously trivial: Word's text watermark read from DOCX headers, #1290; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the share of a typical real-world file's content and formatting that survives (read: shows correctly; @@ -64,7 +64,7 @@ Measured by grepping `crates/docx/src/read` and `src/write` for the OOXML elemen | DrawingML shapes and text boxes (`wps:`) | ✅ | ✅ | Preset geometries subset (callouts and flowchart boxes drawn as the nearest shape, #611); fill and outline from the shape or from the theme through its style references (`a:fillRef`/`a:lnRef`, #611), scheme, preset, system, scRGB and HSL colours; rotation and flips (`a:xfrm` `rot`/`flipH`/`flipV`, pictures and groups too, #332), the effect extent covering the rotated bounds; `wps:bodyPr` `vert` and `anchor`, internal margins (`lIns`/`tIns`/`rIns`/`bIns`) and Resize shape to fit text (`a:spAutoFit`: the text is never cut, #743), linked text boxes (`wps:txbx/@id`, `wps:linkedTxbx` `id`/`seq`, #369); text warps (`a:prstTxWarp` with adjust values, all 41 presets kept; unknown ones dropped), connectors (`straightConnector1`, `bentConnector2`–`5`, `curvedConnector2`–`5` read; 3-segment forms written) with glued ends (`wps:cNvCnPr` `a:stCxn`/`a:endCxn`; the glued shapes keep their drawing ids) and arrowheads (`a:headEnd`/`a:tailEnd`, drawn as triangles) (#416) | | Freeform shapes (`a:custGeom`: `a:moveTo`, `a:lnTo`, Bézier curves flattened) and WordCraft ink | ✅ | ✅ | Ink is written as a freeform `wps:wsp` in a `wp:anchor` (round caps, alpha for highlighter) and recognised again by its drawing name (#307). It is saved as a custom-geometry shape, not as Word's own ink (InkML in `w14:contentPart`), so Word shows WordCraft ink as a freeform shape it can move and recolour but not erase with its ink eraser; arcs drawn straight | | Group shapes (`wpg:`), drawing canvas (`wpc:`) | ✅ | ✅ | Groups (#267) and canvases (#344) with pictures, shapes, freeforms (custom geometry, #743) and text boxes; nested groups flattened. The canvas schema has no transform, so a turned or flipped canvas keeps its turn in our own `wpc:extLst` extension (`urn:wordcraft:canvas-xfrm`; Word shows it unturned). Legacy VML canvases (`v:group editas="canvas"`) aren't read as canvases | -| VML (`w:pict`, `v:shape`, `v:textbox`) | 🟡 | ❌ | Pictures, text boxes, lines (with arrowheads), rectangles and ovals, best effort (#242, #611). Word's text watermark (a `v:textpath` shape in a header or footer, recognised by Word's `PowerPlusWaterMarkObject` id or by being centred on the page both ways) becomes the document's watermark: text, font, fill colour, semitransparent (`v:fill` `opacity`) and diagonal (any turn); drawn with WordCraft's own size and angle, on every page even when only some sections' headers (or only the first-page header) have it, and not yet written back to DOCX (#1290). Other VML WordArt (`v:textpath` in the body, or a header logo) is still dropped | +| VML (`w:pict`, `v:shape`, `v:textbox`) | 🟡 | 🟡 | Pictures, text boxes, lines (with arrowheads), rectangles and ovals, best effort (#242, #611). Word's text watermark (a `v:textpath` shape in a header or footer, recognised by Word's `PowerPlusWaterMarkObject` id or by being centred on the page both ways) becomes the document's watermark: text, font, fill colour, semitransparent (`v:fill` `opacity`) and diagonal (any turn); drawn with WordCraft's own size and angle, on every page even when only some sections' headers (or only the first-page header) have it (#1290). Saving writes the watermark back the same way: a `v:textpath` shape centred on the margins, behind the text, at the start of every header's first paragraph; a section whose pages would show no header gets a header holding only the watermark (default, and first-page or even-page ones when those are on), while sections linked to an earlier header stay linked. Other VML is written only for OLE object pictures. Other VML WordArt (`v:textpath` in the body, or a header logo) is still dropped | | Alt text of drawings (`wp:docPr`/`cNvPr` `descr` and `title`, decorative mark `adec:decorative` from [MS-ODRAWXML]) and tables (`w:tblCaption`, `w:tblDescription`) | ✅ | ✅ | Pictures, shapes, text boxes, groups and their members, charts and SmartArt; OLE objects get theirs written into the kept XML (#386) | | Charts (`c:chart`), SmartArt (`dgm`) | ✅ | ✅ | Drawn from the chart's cached data and SmartArt's stored drawing (#292), with data labels (`c:showVal`). Charts made in WordCraft (Insert › Chart, #409) are written as a `c:chartSpace` part with their data as literals (`c:strLit`/`c:numLit`, no embedded workbook, so Word shows them and offers to create a workbook on Edit Data) and read back as editable; a chart part holding anything more (any chart from Word, or one re-saved by Word) stays read-only. Saved back as read, with every part they reach (chart, embedded workbook, colours and style; diagram data, layout, quick style, colours and drawing) and their content types; a moved or resized one keeps its new frame, and a pasted copy gets its own parts (#319). SmartArt made in WordCraft (#497) is written from its model: data (`dgm:dataModel` points and connections), our own layout, quick-style and colour definitions under the built-in layout identifiers, and a `dsp:drawing` with every shape placed; it reads back editable only when its data part is exactly what WordCraft writes | | OLE objects (`w:object`: embedded or linked files, ActiveX) | 🟡 | ✅ | Shown as their picture (VML or DrawingML); saved back whole with the embedding and picture parts, at the size, rotation and flips set in WordCraft (#319, #332). Can't be opened or edited | @@ -163,6 +163,7 @@ raise readiness estimates. No Word-derived files are fixtures. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | DOCX: the document's watermark is written as a VML text shape in every header, adding a header where pages had none (#1290) | | 2026-10-13 | trivial | DOCX: Word's VML text watermark in headers is read as the document's watermark (#1290) | | 2026-10-12 | trivial | Paste keeps the formatting of HTML and RTF on the system clipboard; Paste Special lists them and pictures (#687, #833) | | 2026-10-12 | trivial | ODT page size, orientation and margins read back from `styles.xml` on open, so they survive a save and reopen (#810) | diff --git a/docs/gaps.md b/docs/gaps.md index bb8cc8d0f..afe2764fa 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where WordCraft falls short of Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (a DOCX field report fixed: Word's text watermark, #1290; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (Word's text watermark kept on DOCX save, #1290; previously trivial: a DOCX field report fixed: Word's text watermark, #1290; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -15,7 +15,7 @@ Native save reliability: synthetic cases on macOS reproduced loss through the pr | # | Gap | B | Evidence | Impact | Hours | Doc | |---|---|---|---|---|---|---| -| 1 | **DOCX never tested on real-world files.** No corpus of files from Word, Google Docs, LibreOffice and Pages has been opened, rendered and round-tripped and compared with Word page by page | B | No corpus in the repo; `crates/docx/tests` are synthetic fixtures (including omitted/empty paragraph defaults and imported TOC range preservation); "DOCX fidelity corpus: not started" since 2026-10-06. Field reports fixed one by one: a cover page's theme-coloured shapes (#611), freeforms inside groups and text-box margins and Resize to fit (#743), Word's text watermark in headers (#1290) | Everyone: the files people receive are the files that break | 30–50 | [file-format-parity.md](file-format-parity.md) | +| 1 | **DOCX never tested on real-world files.** No corpus of files from Word, Google Docs, LibreOffice and Pages has been opened, rendered and round-tripped and compared with Word page by page | B | No corpus in the repo; `crates/docx/tests` are synthetic fixtures (including omitted/empty paragraph defaults and imported TOC range preservation); "DOCX fidelity corpus: not started" since 2026-10-06. Field reports fixed one by one: a cover page's theme-coloured shapes (#611), freeforms inside groups and text-box margins and Resize to fit (#743), Word's text watermark in headers, read and written (#1290) | Everyone: the files people receive are the files that break | 30–50 | [file-format-parity.md](file-format-parity.md) | | 2 | **Remaining native ink forms are dropped on open and lost on save, silently**; OLE objects show only their picture | B | Drawing-level native ink is preserved with its optional picture fallback; direct run-level content parts and ink nested in groups/canvases remain unsupported. Drawing canvases are read and written (#344); group shapes (`wpg`) read and written (#267). Charts and SmartArt are drawn (#292) and, with OLE objects, written back with all their parts on save (#319) | Documents with diagrams drawn in Word or pen notes lose content without warning | 4–8 to preserve remaining native ink forms; 45–70 to render and edit charts and SmartArt (#13) | [file-format-parity.md](file-format-parity.md) | | 3 | **Crashes and freezes in the field**: startup crash on Intel UHD Graphics (#170) (lag on GPUs without a DirectX 12 driver fixed, #316; 32-bit Windows exited at launch because DirectX 12 graphics pipeline creation failed, now OpenGL), freezes (#77, #59 Thai fonts, #31 font selector), no writes without admin on Windows 11 (#218), broken shape tool (#142), images can't be moved (#82); memory growth on every font change fixed (#848: the font list no longer loads each family it previews) | B | 62 open issues, ~15 of them crashes, hangs or data-path failures | Anyone hit loses work or trust; the never-crash standard covers panics, not hangs or GPU driver faults | 20–30 | [target-app-parity.md](target-app-parity.md) (Stability) | | 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options ignored; no page-by-page comparison harness | B | [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) | @@ -73,6 +73,7 @@ The same gaps grouped the way the parity documents are, for agents working in on | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Row 1: the watermark is saved to DOCX headers (#1290) | | 2026-10-13 | trivial | Row 1: Word's text watermark in DOCX headers is drawn (#1290) | | 2026-10-11 | trivial | Table column and row markers on the rulers landed (#700): the ruler's column markers are off the UI/UX gaps | | 2026-10-11 | trivial | UI/UX gaps: the table context menu landed, its items following the selection as in Word (Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…); objects' menus and Border Styles remain |