From da89f7282de919811dee5b000617f00ece2e683f Mon Sep 17 00:00:00 2001 From: Mauro Mazzieri Date: Tue, 13 Oct 2026 13:31:00 +0200 Subject: [PATCH] #1290: read Word's VML text watermark from DOCX headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Word's Design › Watermark is a VML shape with a v:textpath in the headers. The reader dropped it, so screen and PDF showed no watermark. It now maps it to the document's watermark model (text, font, fill colour, semitransparent from v:fill opacity, diagonal when turned), which the renderers already draw. Only Word's watermark shape (PowerPlusWaterMarkObject id) or text centred on the page both ways counts; other header WordArt, body WordArt and shapes inside text boxes are left alone. Writing it back to DOCX is still missing (it was already lost on save). Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 + crates/docx/src/read/mod.rs | 1 + crates/docx/src/read/story.rs | 57 ++++++++++++++++++++++++++ crates/docx/tests/fixtures.rs | 76 +++++++++++++++++++++++++++++++++++ docs/file-format-parity.md | 5 ++- docs/gaps.md | 5 ++- 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c331514cf3..af0e6fb685 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ 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-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 | @@ -273,6 +274,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 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) | | 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) | diff --git a/crates/docx/src/read/mod.rs b/crates/docx/src/read/mod.rs index 5f16f82365..f5bc4b3b90 100644 --- a/crates/docx/src/read/mod.rs +++ b/crates/docx/src/read/mod.rs @@ -636,6 +636,7 @@ impl Reader<'_> { }; let rels = self.pkg.rels(path); let mut sc = StoryCtx::default(); + sc.in_hf = true; let mut blocks = Blocks::new(); self.read_blocks(&mut sc, &root, &rels, &mut blocks, 0); self.flush_pending(&mut sc, &mut blocks); diff --git a/crates/docx/src/read/story.rs b/crates/docx/src/read/story.rs index fc6302e4be..a1a34e6b02 100644 --- a/crates/docx/src/read/story.rs +++ b/crates/docx/src/read/story.rs @@ -119,6 +119,8 @@ pub struct StoryCtx { pending: Vec, /// Text-box nesting depth. pub story_depth: usize, + /// Reading a header or footer (not a text box inside one): where Word keeps its watermark. + pub in_hf: bool, } /// Paragraph content under construction. @@ -1280,6 +1282,13 @@ impl Reader<'_> { /// Legacy VML pictures, text boxes, lines, rectangles and ovals (best effort). fn read_vml(&mut self, sc: &mut StoryCtx, pict: &El, rels: &Rels) -> Option { let shape = pict.els().find(|e| e.name.starts_with("v:") && e.local() != "shapetype")?; + if sc.in_hf + && let Some(wm) = vml_watermark(shape) + { + // The document's watermark (the first one found); drawn on every page, not as a shape. + self.doc.settings.watermark.get_or_insert(wm); + return None; + } let style = shape.attr("style").unwrap_or(""); let len = |k: &str| vml_style(style, k).and_then(|v| measure(v, 1.0)).unwrap_or(0.0).clamp(0.0, crate::units::MAX_LEN_PT); let (mut w, mut h) = (len("width"), len("height")); @@ -1641,6 +1650,54 @@ fn vml_float(shape: &El, style: &str) -> Float { f } +/// Word's text watermark (Design › Watermark): a VML shape in a header whose `v:textpath` draws +/// its `string` as text (ECMA-376 Part 4, VML `textpath`), in the textpath's `font-family`, filled +/// with the shape's `fillcolor`, semitransparent when its `v:fill` has an `opacity` below 1, and +/// diagonal when the shape is turned. Only Word's watermark shape counts (its `id`), or text +/// placed absolutely and centred both ways; other header WordArt (a logo or a title) is None. +fn vml_watermark(shape: &El) -> Option { + let tp = shape.child("v:textpath")?; + if tp.attr("on").is_some_and(|v| matches!(v, "f" | "false" | "0")) { + return None; + } + let style = shape.attr("style").unwrap_or(""); + let centred = |k: &str| vml_style(style, k).is_some_and(|v| v.eq_ignore_ascii_case("center")); + let word = shape.attr("id").is_some_and(|id| id.starts_with("PowerPlusWaterMarkObject")); + let placed = vml_style(style, "position").is_some_and(|p| p.eq_ignore_ascii_case("absolute")) + && centred("mso-position-horizontal") + && centred("mso-position-vertical"); + if !word && !placed { + return None; + } + let text: String = tp.attr("string")?.chars().take(200).collect(); + if text.trim().is_empty() { + return None; + } + let default = wordcraft_doc::Watermark::default(); + let font = tp + .attr("style") + .and_then(|s| vml_style(s, "font-family")) + .map(|f| f.trim_matches(|c: char| c == '"' || c == '\'' || c.is_whitespace()).chars().take(100).collect::()) + .filter(|f| !f.is_empty()) + .unwrap_or(default.font); + let color = shape.attr("fillcolor").and_then(vml_color).unwrap_or(default.color); + let opacity = shape.child("v:fill").and_then(|f| f.attr("opacity")).and_then(vml_fraction).unwrap_or(1.0); + Some(wordcraft_doc::Watermark { text, font, color, diagonal: vml_spin(style).deg != 0.0, semitransparent: opacity < 1.0 }) +} + +/// A VML fraction (`opacity`): a number, a percentage, or 65536ths with an `f` suffix; 0..=1. +fn vml_fraction(v: &str) -> Option { + let v = v.trim(); + let f = if let Some(n) = v.strip_suffix('f') { + n.trim().parse::().ok()? / 65_536.0 + } else if let Some(n) = v.strip_suffix('%') { + n.trim().parse::().ok()? / 100.0 + } else { + v.parse::().ok()? + }; + f.is_finite().then(|| f.clamp(0.0, 1.0)) +} + /// A VML shape's turn: `rotation` in degrees, or 65536ths of one with an `fd` suffix; `flip`: `x` /// and/or `y`. fn vml_spin(style: &str) -> wordcraft_geom::Spin { diff --git a/crates/docx/tests/fixtures.rs b/crates/docx/tests/fixtures.rs index 689f37dc48..eea3771c59 100644 --- a/crates/docx/tests/fixtures.rs +++ b/crates/docx/tests/fixtures.rs @@ -742,6 +742,82 @@ fn bare_break_directly_in_paragraph() { assert_eq!(paras(&d2)[0].text, "First\nSecond\n\u{C}Third"); } +/// Issue #1290: Word's text watermark, a VML shape with a `v:textpath` in a header or footer, +/// becomes the document's watermark (drawn on every page) instead of being dropped; other WordArt +/// is not a watermark, and hostile values don't panic. +#[test] +fn vml_textpath_in_header_is_the_watermark() { + let read = |shape: &str, in_header: bool| { + let pict = format!("Header label{shape}"); + let header = format!(r#"{}"#, if in_header { pict.as_str() } else { "" }); + let body = format!( + r#"{}Body"#, + if in_header { "" } else { pict.as_str() } + ); + let d = wordcraft_docx::read(&docx(&body, &[("rIdH", "header", "header1.xml")], &[("word/header1.xml", &header)])).unwrap(); + let hid = d.last_section.headers.default.unwrap(); + let header_objects = d.parts.get(&hid).unwrap().blocks.iter().filter_map(|b| b.as_para()).map(|p| p.objects.len()).sum::(); + let header_text = d.parts.get(&hid).unwrap().blocks.first().and_then(|b| b.as_para()).map(|p| p.text.clone()).unwrap_or_default(); + (d.settings.watermark.clone(), header_objects, header_text) + }; + + // Word's Design › Watermark shape: turned 315°, silver, semitransparent, quoted font name. + let word = r##""##; + let (wm, objects, text) = read(word, true); + let wm = wm.expect("watermark"); + assert_eq!((wm.text.as_str(), wm.font.as_str(), wm.color), ("CONFIDENTIAL", "Calibri Light", Rgb(0xC0, 0xC0, 0xC0))); + assert!(wm.diagonal && wm.semitransparent, "{wm:?}"); + // Consumed: not also drawn as a shape in the header, whose text stays. + assert_eq!((objects, text.as_str()), (0, "Header label")); + + // A page-centred, gray, opaque shape turned -40° (no `v:fill`). + let gray = r##""##; + let wm = read(gray, true).0.expect("watermark"); + assert_eq!( + (wm.text.as_str(), wm.font.as_str(), wm.color, wm.diagonal, wm.semitransparent), + ("BOZZA", "Arial", Rgb(0x80, 0x80, 0x80), true, false) + ); + + // Horizontal, centred both ways without Word's id, opacity in 65536ths. + let flat = r##""##; + let wm = read(flat, true).0.expect("watermark"); + assert_eq!((wm.color, wm.diagonal, wm.semitransparent), (Rgb(0x33, 0x66, 0x99), false, true)); + + // Not watermarks: the same shape in the body (WordArt); header WordArt that isn't centred + // and isn't Word's watermark (a logo); a textpath turned off; a watermark-like shape inside a + // header's text box. + assert_eq!(read(gray, false).0, None); + let logo = r##""##; + assert_eq!(read(logo, true).0, None); + assert_eq!(read(&word.replace(r#"{word}"## + ); + assert_eq!(read(&boxed, true).0, None); + + // A footer's watermark counts too, and the first one found wins. + let footer = format!(r#"{gray}"#); + let header = format!(r#"{word}"#); + let body = r#""#; + let only_footer = r#""#; + let parts = [("word/header1.xml", header.as_str()), ("word/footer1.xml", footer.as_str())]; + let rels = [("rIdH", "header", "header1.xml"), ("rIdF", "footer", "footer1.xml")]; + let wm = |body: &str| wordcraft_docx::read(&docx(body, &rels, &parts)).unwrap().settings.watermark.map(|w| w.text); + assert_eq!(wm(only_footer).as_deref(), Some("BOZZA")); + assert_eq!(wm(body).as_deref(), Some("CONFIDENTIAL")); + + // Hostile values: an oversized string is capped, garbage rotation/opacity/colour don't panic, + // an empty string is no watermark. + let long = "W".repeat(10_000); + let hostile = format!( + r##""## + ); + let wm = read(&hostile, true).0.expect("watermark"); + assert_eq!(wm.text.chars().count(), 200); + assert!(!wm.font.is_empty()); + assert_eq!(read(r#""#, true).0, None); +} + /// Issue #149: an absolutely positioned VML text box in a header floats where its style puts it /// (relative to the page here, behind the text at a negative z-index, no wrapping) instead of /// sitting inline in the header's flow, and it keeps that placement when saved and read back. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 200f259e22..af09a86319 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-11 · **Change:** 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 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) | +| 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 | | 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: 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) | | 2026-10-12 | minor | RTF reads 8-bit text in the document's or the font's code page (`\ansicpg`, `\fcharset`, `\cpg`) instead of always Windows-1252; a `.doc`/`.docx`/`.rtf` whose content is another of these formats opens by signature. Found on a private corpus of real Russian documents: 38 of 44 `.doc` files were RTF, all now open with their Cyrillic text | diff --git a/docs/gaps.md b/docs/gaps.md index 04a985cc91..bb8cc8d0fa 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-11 · **Change:** 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 (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) | 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 (#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: 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 | | 2026-10-11 | trivial | Tables: ordinary drags, Shift+arrows and Shift+click over cells select a rectangle of whole cells (into or out of a table, whole rows); removed from the UI/UX gaps |