Word draws no red squiggle under the word being typed. It appears when the word ends or the
caret leaves it by a key or a click. The session records the word after a text command that
edits the caret's paragraph, with apostrophes and hyphens counted as the spelling check counts
them, and drops it once the caret is anywhere else. The layout leaves that one range out of the
spelling squiggles, and a caret move without an edit lays out only that paragraph again. F7 and
the Spelling pane still report the word, and grammar marks are unchanged.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* track-move-options: WIP
* track-move-options: Moves options in Track Changes Options and w:doNotTrackMoves
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* track-move-options: Moved from Hidden leaves the layout; per-revision hiding
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* track-move-options: Word's mark lists, (none) moves are plain, no borders on collapsed paragraphs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* track-move-options: hr and sv translations for Moved from:/Moved to:
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
split_row already keeps widow/orphan control in each cell paragraph.
When that moves all of a cell's lines on although its first line would
fit, the other cells stayed on the page and the row was split beside
an empty cell. Word moves the whole row on instead. Observed with
synthetic documents (exact 14 pt lines, a one-line cell next to a
longer one near the bottom of the page):
long cell | room for | Word | before | now
4 lines | 2 | 2 + 2 | 2 + 2 | 2 + 2
3 lines | 2 | row moves | short cell kept | row moves
3 lines | 1 | row moves | short cell kept | row moves
4 lines | 1 | row moves | short cell kept | row moves
5 lines | 3 | 3 + 2 | 3 + 2 | 3 + 2
3, control off | 2 | 2 + 1 | 2 + 1 | 2 + 1
Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
part that inherits its lines sends it, and the picker only shows No Border checked for it.
- CellProps::valign is Option<VAlign>: a cell's own Top (Align Top, w:vAlign="top") beats a
table style part's vertical alignment, and round-trips through .docx.
- tableStyle edit rejects Part fields at the top level without "part".
- "font": null or "" makes a part's font inherited; clearing the dialog's font box sends it.
- Document ui.dialog's optional "part" in docs/control-protocol.md.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #714: table style editor formats every table style part
Modify/New Table Style: "Apply formatting to" any of the 13 ECMA-376
§17.7.6 parts (whole table, header/total row, first/last column,
odd/even row and column bands, four corner cells), each with font,
size, bold, italic, underline, colour, fill, borders (line style,
width, colour; all/none/outside/inside/each edge) and the 9-way cell
alignment, over a live preview.
- doc: TablePart, PartFormat, PartSet; TableStyleParts::part/set_part
(new parts and per-part jc/vAlign in `regions`)
- layout: generic part resolution incl. band2, corner cells, alignment
- docx: every tblStylePr type read/written with pPr jc and tcPr vAlign
- engine: table.newStyle/modifyStyle take ECMA part names, `part`,
`parts`, per-edge `borders`, `underline`, `align`/`vAlign`/`cellAlign`
- ui: dialog moved to dialogs_table_style.rs; ui.dialog takes `part`
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #887: Bulgarian strings for the table style parts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #714: Vietnamese strings for the table style parts (main's new vi catalog)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* hr: match the catalog's word for banded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: preserve row table border exceptions
* Tests: shorten the row border regression name
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A header or footer a section inherits (Same as Previous) keeps the
percentage widths of its tables relative to the section that defines it.
Measured in Word: a 100%-wide footer table defined by a portrait section
stays 451 pt wide, centred, on landscape pages that inherit it, and spans
the landscape width only when that section names the footer itself.
Paragraph alignment in the same footer follows the landscape page.
Found on a report whose landscape calculation pages drew their footer
table full width where Word draws it at the portrait width.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: text running past an exact-height row is cut off
A row with an exact height (w:trHeight w:hRule="exact") keeps that
height whatever its cells hold. Word cuts the text that doesn't fit at
the cell's edge; WordCraft drew it over the rows below, so a certificate's
identification block came out as overlapping lines.
The cell is marked as clipped in layout (Placed::Cell clip), and display
draws its lines inside that cell's clip, on screen and in PDF. Hit
testing is unchanged: the lines stay where they are in the layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: keep row clipping compatible with Rust 1.90
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layout: a table's percentage width past 100% is kept
Word keeps a preferred width over 100% of the text column (w:tblW
w:type="pct" above 5000). A cover-page footer table at 131% with the
left margin as a negative indent spans the page edge to edge in Word;
WordCraft capped it at 100%, so it ended three-quarters of the way across.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: add Fin Warman
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Table autofit measured every cell paragraph at an unbounded width through
the paragraph cache, so a long table held two layouts per cell. The cache
now keeps only the measured narrowest/widest widths (`LayoutCache::measures`,
same key and eviction as the layouts).
- The cache key formatted each cell's table-style text formatting with Debug
for every cell paragraph on every pass; the hash is now memoised per pass.
- Update Fields added the hidden ListEntry style even without a generated
list, which changed the layout environment and dropped the whole cache.
The style is now added only when a list is rebuilt.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A horizontal cell edge ran from cell edge to cell edge, so at a thick
vertical edge it started on that edge's centre line and left a notch half
the border width wide at every corner. Horizontal edges now run on over
half of each visible vertical edge, as Word draws them.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Every cell in a row gets the row's largest top and bottom cell margin, so
a cell without its own margins lines up with its neighbours.
- An exact row height covers the top border, top margin and text; the
bottom margin comes on top. Dragging a row border, or drawing a line
through an exact row, accounts for it.
- A row split across pages must keep its cells' bottom margin, border and
last paragraph's space after on the first page, and widow/orphan control
applies to the paragraphs in its cells. A row whose text fits but whose
margin doesn't no longer moves to the next page whole.
- Each cell of a split row goes on at the top of the continuation (one
cell's widow line no longer pushes its neighbours down), and the
continuation is as tall as the cell needing the most room, and at least
the rest of the row (a minimum row height holds over the pages).
- A nested table's cell the page bottom cuts through goes on from the cut
with what is left of it, and the text after it stays below it.
- A row of an exact height never splits: it moves to the next page whole.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Rulers: table column and row markers; indents in a cell measured from it (#700)
With the caret in a table, the rulers show the table, as Word's do:
- The horizontal ruler marks the column borders of the caret's row (past
the table's left edge); the vertical ruler marks the bottom of each of
the table's rows on the page. Each marker has the resize pointer, a
tooltip and a name for screen readers.
- Dragging a marker drags that border exactly like a drag on the page
(#600): a dashed guide, Escape cancels, one undo step on release
through `table.resizeDrag`. On the horizontal ruler only the column
left of the border changes and the columns right of it move along, so
the table's width changes (Word's ruler); a row marker sets the row's
height. The markers follow any drag of the table's borders, on the
ruler or on the page.
- In a cell, the indent markers and tabs are measured from the cell's
text area (its box less its margins); they were placed as if the
paragraph started at the page's margin.
- Layout: `cell_margins` (a cell's own margins, else the table's, else
Word's), so the ruler uses what the layout does.
- UI: crates/ui-egui/src/table_ruler.rs; table_resize.rs starts a drag
from a ruler marker (`grab`) and tells the ruler where a dragged border
is (`moved`), sharing that computation with the dashed guide.
- Tests: layout (cell_margins matches where each cell's text starts);
kittest with real pointer events: a column marker drag (the marker
follows, one undo step, the columns right of it keep their widths), a
row marker drag, and an 18 pt drag of the left-indent marker in a cell
indenting the paragraph 18 pt.
Progress docs updated: ui-parity (rulers 70% -> 85%), gaps (the ruler's
column markers are off the list), target-app-parity, ROADMAP. No new
commands or labels (the tooltips reuse "Table Column Width" and "Table
Row Height"). `cargo xtask ci` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rulers: Shift works the other way round on the ruler; row markers beside a vertical merge
Checked against Word (2019, Windows): on the ruler a column marker drag
changes only the column on its left and the columns right of it move
along (as before), and with Shift the neighbour makes up the difference,
so the table keeps its width; on the page Shift does the opposite. The
ruler now follows that: `ResizeDrag::plan` takes the Shift key and flips
it for a drag started on a ruler marker.
Since #669 a vertically merged cell's box spans all its rows, so a row
marker took the merge's bottom when the merge was the row's leftmost cell.
The marker now sits on the row's shortest box. (`Placed::Cell` patterns
take `..` for #669's new field.)
- Test (kittest, real pointer events): a Shift drag of a column marker
on the ruler gives the neighbour's width to the column on the left and
keeps the table's width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Read and write w:hideMark on table cells, and lay it out like Word: a cell's
empty last paragraph takes no room, spacing and all, so a row of only such
empty cells is just its borders; an ordinary empty cell keeps the row a line
tall. Measured in Word for Windows: 9 synthetic cases. NIH's Table A no
longer shows a blank row.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Layout: an inline picture wider than the text keeps its size, like Word
WordCraft shrank an inline picture, chart or shape that is wider than
its line to the line's width, height with it. Word draws it at its own
size: it runs past the margin and the page edge clips it, and the line
is as tall as the picture. In docxide-pdf's fixtures that moved the rest
of the page up (case179: a 1014 x 100 pt picture became 468 x 46 pt) and
shrank a header's 1014 pt logo strip to fit (alfies_arc).
All page outputs already clip at the page: the PDF page box, and the
page bitmaps the rasteriser and the editor draw. Text boxes don't clip
what runs past their sides yet, so a picture wider than a text box is
still fitted to the box rather than spilling over the page
(ParaEnv::fit_objects, part of the paragraph cache key).
On the 390 fixtures: case179 SSIM 70.9 -> 99.9%, alfies_arc 18.4 ->
74.8%, case178 98.0 -> 99.9%, nabl_lab 33.2 -> 36.8%; no fixture got
worse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #745: fit imported and pasted pictures to the text width
Layout no longer shrinks oversized inline pictures, so pictures that come at their pixel
size are fitted when they enter the document, as Insert › Pictures already does:
- opening HTML (also from a path), Markdown, ODT, RTF and other formats-crate imports
(io::fit_pictures, against the narrowest section text width);
- pasting HTML/RTF (paste::adopt, against our text width).
Only inline pictures wider than the text are scaled, proportionally. DOCX keeps its sizes.
Test: tests::imported_pictures_fit_the_text_width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Numeral display modes: Arabic, Hindi, Context and System digits
Display-only digit substitution (Word File > Options > Advanced >
Numeral): ASCII and Arabic-Indic digits are substituted before shaping
while clusters keep indexing the original text, so find, spelling,
copy/paste and saved files see the typed digits.
- layout: NumeralMode (arabic|hindi|context|system) with System resolved
from LC_ALL/LANG, threaded through LayoutOptions/ParaEnv with cache
invalidation; Context follows the surrounding text
- engine: view.numeral command + catalog row
- tests: layout tests_numeral, engine command test
* Options page: Numerals picker in File > Options > Display
ComboBox for the view.numeral mode (Arabic/Hindi/Context/System),
mirroring Word's File > Options > Advanced > Numeral. Display-only,
session-scoped like the other Display options.
* fmt: collapse numeral direction branch
* #501: translate the Numerals option label in every catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: abdo4312 <abdo4312@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File › Options is now a pane list (General, Display, Proofing, Save,
Language, Accessibility, Advanced, Agents), one function per pane in
crates/ui-egui/src/options.rs. Every setting shown is wired:
- General: theme, interface size (#475), mini toolbar on selection, dark page, community
button, user name and initials (comments use the initials).
- Display: tab characters, spaces, paragraph marks and hidden text shown
with Show/Hide ¶ off; rulers; print the page colour; update fields
before printing.
- Proofing: ignore words in capitals / with numbers / web and file
addresses (proof::Options, passed through layout), check spelling and
mark grammar as you type, custom dictionary add/remove (kept in ui.json).
- Save: AutoSave. Language: interface language. Accessibility: Read Aloud
speed and citations. Advanced: typing replaces selection, Insert key
overtype, overtype (text.overtype), recent documents count, measurement
units (dialogs, Layout ribbon indents and rulers).
file.options reads and sets the engine-side settings headlessly and only
opens the page when given no setting. ui.json loads leniently: a bad value
falls back to its default instead of dropping every setting.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Model: `SectionProps::doc_grid` (`w:docGrid` type, line pitch, charSpace) and the
paragraph `w:snapToGrid` flag; DOCX read and write (ECMA-376 §17.6.5).
- Layout: with a line grid, lines of snapping paragraphs take whole line pitches with
their text centred; with a character grid, East Asian characters take whole cells of
the character pitch (all text with snapToChars), Latin text stays proportional.
Hostile pitches and charSpace are clamped. Headers and footers don't snap.
- Command `layout.documentGrid` (type, linesPerPage, charsPerLine, linePitch,
charSpace), one undo step, section scope; catalog row under Layout › Page Setup.
- Page Setup dialog: Document Grid section; View › Gridlines draws the document
grid of a section that has one. UI strings in every catalog (incl. et, sr, sr-latn).
- Tests: DOCX round trip, line pitch and lines per page, character pitch placement,
command with clamping and undo.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Tables marked right to left (w:bidiVisual) now lay out mirrored: the first
column stands at the right edge, start alignment and indent are measured
from the right margin, and each cell's start margin and border sit on its
right. Merged cells, turned text and nested tables follow; clicks and the
caret follow the drawn geometry while Tab keeps the logical cell order.
Right-to-left sections (w:bidi) run their newspaper columns from the right,
with the line between columns mirrored.
The table direction is read and written in .docx (section direction already
round-tripped). New commands table.rtl and layout.sectionRtl, a "Table
direction" choice in Table Properties ("rtl" in table.properties) and a
"Section direction" choice in Page Setup, translated in every catalog.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Insert > Illustrations > Shapes > New Drawing Canvas (and Draw > Drawing Canvas)
inserts a 6" x 3" canvas in line with text. While it is selected, Pictures,
Shapes and Text Box put the new object inside it. Fit, Expand and Scale Drawing
change its frame, Shape Fill and Shape Outline colour its background and border,
and canvas.member moves, resizes or removes a member.
The canvas is the group object with a canvas style: its frame is independent of
its members, and members outside it are clipped on screen and in PDF (new
Draw::Clip / Placed::Clip). DOCX reads and writes wpc:wpc with wpc:bg,
wpc:whole and wps:wsp / pic:pic members.
A canvas turns and flips as a whole, like a group (#332): its background,
outline and clipped members are drawn inside its turn (Draw::Rotated around
Draw::Clip; Placed::Clip carries the spin, its members sit in the unturned
frame), and hit testing follows the turned frame. The canvas schema has no
transform of its own, so a turned canvas keeps its rot/flipH/flipV as an
a:xfrm in a wpc:extLst extension (urn:wordcraft:canvas-xfrm), which Word skips.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.
New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.
The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The original squash merges of these six PRs were empty. Integrated and tested together on current main.
Co-authored-by: Valentin Klein <vangel1976@googlemail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Table sizing: Height/Width boxes, real AutoFit Contents, Table Properties dialog (#44)
- Table Layout > Cell Size gets Height and Width boxes (inches) for the caret cell's row
and column; each edit runs table.rowHeight / table.columnWidth, a drag is one Undo.
- AutoFit Contents now measures every column's narrowest (longest word) and widest
(unwrapped text) content with the layout's text shaping, shares the text width the way
Word does, and writes the widths into the table grid and cell widths.
- Table Properties opens a dialog (table width, alignment, indent; row height and rule,
break across pages, header row; column width; cell width and vertical alignment). OK
applies only what changed as one table.properties step; with settings the command still
works headlessly, and without them it reports the current values.
- table.columnWidth clears the table's preferred width so the column keeps the new size;
table.rowHeight takes an optional exact rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Progress docs: Table Properties dialog and AutoFit Contents landed (#44)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table sizing: saturating column-span sum in sync_cell_widths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fix: one region_text (main's) after #287 met the landed table-style code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A chart or SmartArt diagram in a DOCX was dropped on import: it took no
room, so the text after it moved up (five of the six chart documents in
docxide-pdf's fixtures came out a page short), and nothing was drawn.
They are now an InlineObject::Graphic: it takes its room, floats and
wraps like a picture, and draws a list of neutral items (shapes, paths,
one-line texts, pictures) that layout scales to the object's box.
- Charts (read/chart.rs) are drawn from the values the chart part
caches: clustered, stacked and 100% bar and column charts, line, area,
pie, doughnut and scatter charts, with title, legend, gridlines, axis
labels and Excel number and date formats.
- SmartArt (read/diagram.rs) is drawn from the drawing Word stores
beside the diagram: preset shapes, theme colours, text and pictures.
- Theme colour handling is shared (read/drawing_color.rs).
- Each chart or diagram part is read once, only through a relationship
of its type, under a per-file budget of items.
- Tagged PDF tags them as figures with their alternative text, and the
accessibility check reports charts without one.
Not yet: charts and diagrams aren't written back when saving a DOCX
(they weren't before either), so the editor doesn't let you move or
resize them. Radar, stock, 3-D and combination charts draw their first
supported chart type only; SmartArt pictures aren't clipped to their
shape.
docxide-pdf bench (365 documents, vs main + #135): page counts right
320 -> 326 (all five chart documents and one SmartArt document), Jaccard
passes 208 -> 215, SSIM passes 76 -> 79; eight documents better, none
worse.
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word sizes the columns of a table that may follow its content (tblLayout
not fixed) from what is in them; we kept the stored grid as it was:
- A table with no preferred width anywhere (no tblW, no tcW: what
generators write) now gets its columns from their content: each its
widest line if all fit, else shared out between longest word and
widest line, as Word does (case51: a nested table no longer fills its
cell, and the document is two pages like Word's).
- In other autofit tables no column is narrower than its longest word;
the columns with room give up the difference ("ON" no longer breaks
into "O/N" in case91).
Floating tables (w:tblpPr) inside a table cell were laid out in the
text flow. They now stand at their position in the cell and take no
room; the text after them wraps around them, a table that may not
overlap goes below the ones in its way, and the cell grows to hold
them. A row whose lines all fit but whose nested floating table runs
past the page bottom splits there instead of moving to the next page.
Text now flows beside a floating object into gaps of 20pt or more
(was 36pt): Word puts a paragraph beside a floating table in a 21.3pt
gap but not in an 18.6pt one (docxide-pdf case139-148 vary just that).
docxide-pdf bench (365 documents, vs main + #135): Jaccard passes
208 -> 227, SSIM passes 76 -> 86, page counts right 320 -> 324; three
documents slightly worse (case149 -4 SSIM: Word moves the whole floating
table to the next page, we split it; two others -1 or less).
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.
- engine: table.newStyle / table.modifyStyle with documented params
(regions take borders, fill, bold, italic, colour, size, font; bandSize;
para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
style tiles redraw when a style changes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tests: shapes and pictures can be clicked, resized, dragged and nudged
Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Font list offers every weight a family installs, named as Word names them (#219)
Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.
Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Absolutely positioned VML text boxes float instead of sitting inline (#149)
A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.
The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reject All removes paragraph breaks inserted with Track Changes on (#229)
Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.
Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.
DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table Layout: Text Direction turns cell text (#226)
The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.
Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)
The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.
- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
CJK font (desktop only): a short per-platform list of files in the system font folders
(Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
collection), so egui never sees a file it can't parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a System interface theme that follows the OS appearance (#115)
The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.
Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #248: read the installed CJK UI font only when CJK text is shown
A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word's Table Grid style sets single spacing and no space after
(<w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>).
Cell text ignored it and used the document defaults (8 pt after, 1.08
lines in Word's default template), so rows came out ~60% taller than in
Word: an 80-row table took 3 pages instead of 2. A table style based on
Table Grid that adds red bold text showed no red, no bold and no borders.
- Style resolution has a table-style level (resolve_para_in,
resolve_char_in), in ECMA-376 §17.7.2 order: document defaults, table
style, paragraph style chain, character style, direct formatting. Any
property the paragraph style chain sets, Normal included, beats the
table style; the table style only replaces the defaults. (So a document
whose Normal sets its own spacing keeps tall rows, as in Word.)
- StyleSheet::table_style merges a table style's based-on chain: pPr, rPr
and the conditional parts, borders edge by edge. Layout used the first
style in the chain with any table parts and dropped the rest.
- Layout passes the table style's paragraph and run formatting, with the
cell's header row / total row / first column formatting on top, to each
cell paragraph. Conditional run formatting now sits at the table-style
level too, under the paragraph style, instead of over it.
- DOCX: the wholeTable region's pPr/rPr go to the style's own formatting
and its cell shading to a new whole-table fill; tblPr shd and tblCellMar
of a table style are read and written (as wholeTable shading and
tblCellMar).
New tables use Table Grid, so tables made in WordCraft get Word's compact
rows too.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix text boxes: type into a new box, click in and out, Escape leaves it
Insert › Text Box left the caret in the body after the box and clicks inside a
box never reached its story, so its text could not be edited.
- insert.textBox puts the caret in the new box (when anchored in a top-level
body paragraph, the only place layout shows box text today).
- Layout emits a hit-test-only Placed::TextBox area; story_at prefers a box
under the point (body text wins over boxes behind it) and ignores box lines
outside their box.
- Canvas: click enters a box, clicking outside goes back to the body (not while
editing a header/footer); double-click in a box selects a word there.
- Escape in a box puts the caret just after it (Document::text_box_anchor).
- Editing a box doesn't dim the body or show "Editing header/footer".
- Tests: engine (caret in box, typing, undo, table cell unchanged), layout
(story_at inside/outside/behind), doc (anchor lookup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix pasted text boxes sharing the original's text
A copied fragment only carried the text box's part id, so a pasted box showed
the original's story: editing either changed both, edits made after copying
leaked into the paste, and pasting into another document (or Insert › Text
from File) pointed at whatever part had that id there, e.g. a header.
- Fragment carries copies of the text box stories it shows (nested boxes too).
- insert_fragment gives every pasted box a new story of its own; a box whose
story can't be found keeps none rather than showing an unrelated part.
Nesting is depth-limited and one paste creates at most 4096 stories.
- Insert › Text from File uses Document::body_fragment, which brings the
file's text boxes along.
- Tests: doc (own story, copy-time snapshot, other document with clashing
ids, box in a table, fragment without parts, self-nested box), engine
(copy/paste/type/undo through commands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count includes text boxes, footnotes and endnotes (Word's default)
The status bar and Word Count dialog counted only the body, so words typed
into text boxes (and notes) were missing.
- Document::counted_stories / word_count_including_notes: the body plus the
text boxes, footnotes and endnotes it actually shows (nested too); orphaned
stories (deleted or cut boxes), headers, footers and comments don't count.
- Session::count_notes (default on) with Session::word_count for the status
bar; review.wordCount takes {"includeTextBoxes"?: bool} and reports it.
- Word Count dialog: "Include text boxes, footnotes and endnotes" checkbox.
- Document::word_count stays body-only (docx Words property, NUMWORDS, stats).
- Tests: doc (used vs orphaned stories, nesting, self-nested box), engine
(default on, toggle sticks, deleting a box drops its words).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* canvas: select, drag to move and resize pictures, shapes and text boxes
Click a picture or shape (or a text box's border) to select it: a frame with
eight handles. Drag it to move it, drag a handle to resize it (pictures keep
their proportions from a corner, Shift toggles), arrows nudge a floating one
(Ctrl/Alt: 1 pt), Escape cancels a drag. While a text box's text is edited its
frame shows dashed with handles. Typing or Enter with a text box selected
goes into it.
Reusable pieces, no per-kind code:
- layout: Placed::Object, one hit area for every picture/shape/text box
(replaces the text-box-only item); DocLayout::object_at / object /
text_box / find_object (page-hinted, no whole-document scans per frame).
- engine: arrange.bounds {width,height,x,y,page} (floating objects keep their
anchors on the same page; inline ones float where dropped; another page
re-anchors to the nearest line) and arrange.nudge; objects::min_size shared
with the UI.
- ui: frame.rs, a document-agnostic transform frame (handles, hit testing,
drag/resize maths, painting); objects.rs wires it to document objects.
- geom: Rect::expand. control: ui.press / ui.release for half-way drags.
Rendering: nothing is laid out or rasterised while dragging. The preview is
the object's own pixels from the cached page texture (moves, picture
resizes) or a light outline (text box/shape resizes); the drop is one
command, one relayout, one undo step.
Tests: frame maths, layout hit areas, engine move/resize/nudge across
anchors and pages, hostile values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* moving an object anchors it to the paragraph it's dropped on, so text wraps
A floating object only pushes aside text from its anchor paragraph down, and a
move kept the old anchor: drag a text box above its paragraph and the text
there ran straight under it.
- arrange.bounds now anchors the object to the paragraph under its top edge
(the last one starting at or above it on that page) and positions it
relative to that paragraph's column and top, as Word does: the text it
lands on wraps, and it moves with that text. On a page where no paragraph
starts it anchors to the nearest line, positioned on the page.
- layout: Placed::Object / ObjectHit carry the paragraph origin that column-
and paragraph-relative offsets start from.
- engine: edit_obj / edit_float change an object at a position (with_obj
uses it).
- Test: a box dropped over an earlier paragraph anchors there and the lines
beside it stop short of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* text boxes hide text that doesn't fit, as in Word
Text past a text box's bottom spilled out over the page below it.
- layout: fit_box keeps what fits in a box: whole lines that fit (a line that
only partly fits is hidden too) and other items starting inside; the first
line always stays so even a tiny box shows something. Done in layout, so the
canvas, PNG and PDF agree and hidden text can't be clicked; no renderer
changes. Reusable for anything laid out into a fixed height.
- BOX_INSET_X / BOX_INSET_Y name the text box internal margins (were 7.2/3.6
literals).
- Test: overflow hidden, every shown line inside the box, a tall box shows
everything, a minimum-size box shows its first line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* square wrap flows text on both sides of a floating object
Text only used the wider side of a floating picture, shape or text box; Word's
Square wrap fills both sides when there's room.
- para: row_spans gives the spans of a row around the objects in its way (each
at least 0.5" wide, else the line goes below); a row cut in two is two Lines
sharing top and height, the second marked `beside`. Everything per line
(alignment and justification per side, hit testing, caret, selection,
drawing, PDF) works unchanged; a row is as tall as its tallest part.
- layout: a row stays on one page; line numbers count rows, not parts.
- Exclusions that don't overlap a line horizontally no longer narrow it.
- Test: text both sides of a mid-column box (same row, left then right,
hittable), a too-narrow gap stays empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* drop a deleted or cut text box's text from the document
Deleting or cutting a text box left its story in Document::parts: never shown
or saved to .docx, but listed by document.inspect and kept in memory.
- doc: prune_text_boxes removes the text box stories no box shows (nested
ones too); free without text boxes, one paragraph pass otherwise. Session
runs it after every editing command. Undo snapshots and the clipboard hold
their own copies, so undo and cut/paste are unaffected.
- doc: reachable() walks the stories objects lead to; counted_stories (word
count) and prune_text_boxes share it, and it walks blocks directly instead of
building paragraph paths.
- Tests: orphaned and nested-orphan boxes go, shown ones (body, nested, in a
header) stay; delete then undo, cut then paste.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count's include-text-boxes setting survives a restart
The Word Count dialog's "Include text boxes, footnotes and endnotes" was a
session field, so it reset to on every launch.
- engine: Prefs, the editing preferences the front end saves between runs
(count_notes moves there; Session::prefs is the one source of truth).
- ui: SavedPrefs pairs UiState with the engine Prefs; WordApp::saved_prefs /
restore_prefs. UI fields stay at the top level of ui.json, so existing files
still load (the new `editing` section defaults).
- wordcraft app: saves and restores SavedPrefs instead of UiState alone.
- Tests: an old prefs file loads with Word's default; the setting round-trips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* text boxes and floating objects outside the body; footnotes in text boxes
Text boxes in table cells, headers, footers, footnotes and other text boxes
showed an empty frame (or nothing), and floating pictures and shapes there
weren't drawn. Footnotes inserted inside a text box were never numbered or
placed.
- layout: place_objects, one routine for the pictures, shapes and text boxes
in a paragraph's lines (floating ones drawn, every one's area, text boxes'
text), used by body paragraphs and by layout_box (cells, headers, footers,
notes, box contents). Floats are positioned against an AnchorFrame (column,
paragraph, margins, page); headers and footers pass where the page is, so
page/margin-anchored objects land right (a footer is laid out again only
when it has floating objects); elsewhere they're placed in their container.
Nested boxes show MAX_TEXT_BOX_DEPTH (doc) levels deep.
- Placed::Object / ObjectHit carry their story; DocLayout::object(&Pos),
object lookups include headers and footers, header_footer_text_box_at.
A part's lines are looked up in headers and footers too (caret, typing).
- Footnotes: numbered in reading order with text boxes where they are
(Document::objects_in_reading_order); a box's footnotes go on the page of
its line (Document::notes_in_text_box), sharing the body's reservation.
- engine: insert.textBox always puts the caret in the new box; disabled
inside a text box (as in Word).
- canvas: click into a header/footer's text boxes while editing it; leaving a
box returns to where it is; body dimming computed once per frame and used
for both the render and its cache key. objects: only body objects are
selectable, only body paragraphs' objects movable; frames off the body
show without handles.
- Tests: boxes in header (inline and page-placed), footer, cell and note show
text and take the caret; footnotes in a box numbered and placed in order;
a box in a table cell takes the caret and its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count: a selected object counts the whole document, in the dialog too
With a picture, shape or text box selected the status bar showed the
document's total but Review > Word Count counted the selection (0 words).
- engine: cmd::objects::object_selection, "the selection is exactly one
object"; review.wordCount counts the document then. The UI's
objects::selected now uses it (one definition).
- Test: an object selection counts the document; a text selection itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: bound nested text box expansion; tidy-ups
Blocker: laying out a text box lays out the boxes inside it, which could fan
out unboundedly (a box whose shapes show itself, or chains of boxes each
showing the next many times) - 30 shapes made 1.7M layout items. Footnote
numbering's walk fanned out the same way.
- doc: BoxBudget, one bound for both: a box isn't expanded inside itself,
nesting stops at MAX_TEXT_BOX_DEPTH, an outermost box expands at most 64
boxes inside it (so one hostile box can't starve the next page's header
boxes) and a pass at most 20,000.
- layout: Ctx carries a BoxBudget for the whole layout; place_objects asks it
before laying out a box's text. DocLayout::text_boxes reports how many were.
- doc: walk_objects (objects_in_reading_order, notes_in_text_box) uses it too.
- Tests (crafted documents): a box showing itself 30 times lays out once; a
five-level chain fanning out 30 ways stays within the budget, for layout and
for the walk; a box inside a box still shows its text. Weakening the budget
makes them fail (20,000 boxes / 600,001 visits).
Tidy-ups:
- para: row spans in a VecDeque (pop_front, not remove(0)).
- fit_box: a text box's text area excludes both the top and bottom margins.
- theme: Tokens::handle / handle_shadow for the frame handles (light, dark);
the drag preview's opacity is a named constant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* box budget: no overall cap; bound the .docx writer too; tests
An audit of the previous commit found:
- The 20,000-box overall cap starved ordinary long documents: one budget per
layout, spent by body boxes first, then one per page for header boxes (two
for footers with floats), so a long document's header boxes went blank on
every page. It wasn't needed: only nesting grows exponentially, and
MAX_NESTED caps that per outermost box, keeping the work linear. Removed.
- The .docx writer still expanded a shape's story only depth-limited (a box
showing itself 30 times saved as ~28,000 nested copies, ~7 MB zipped). It
now uses the same BoxBudget, and only for text box parts.
- Layout expanded a shape's story whatever its part kind (a shape pointing
at a header showed the header's text); now text box parts only, as the
walk does. The story's blocks are cloned only once the budget agrees.
- Translations of "Count words in text boxes and notes" said footnotes only;
now footnotes and endnotes, in each catalog's own terms.
- Tests: no timing assert or leftovers; a long document with 400 boxes and a
header box shows all of them on every page; notes_in_text_box follows
nested boxes once; saving a self-showing box stays small (fails at 7 MB
without the bound).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arrange: nudging or dragging an aligned float keeps it in place first
main (#136) reads w:align into Float.h_align/v_align, which overrides the offset; the PR's
nudge only changed the offset and drag only reset the anchors. Nudge now converts an
aligned axis into a column/paragraph offset that keeps the object where it is, and a drag
clears the alignment. Test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: a lookup that races another thread's load of the same family no longer falls back
FontDb::face fell back to Source Sans 3 when its load_cataloged added nothing, which happens
when another thread loaded the family between the miss and the load. word::resolve memoises
that, so the wrong font stuck for the process; it made layout tests on main flaky
(justified_lines_shrink_spaces_in_word_2013_mode, space_shrinking_is_only_for_justified_text
failed 6 of 15 runs on origin/main). Look again after loading whatever it returns. Regression
test races 8 threads on fresh databases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout tests: text box and two-sided wrap tests measure what they mean
square_wrap_flows_text_on_both_sides compared xs.last() (the end of a trailing space, which
may hang past the edge) with the edge; compare the last non-space cluster's right edge.
text_box_hides_text_that_does_not_fit compared a tall box's lines with another document's
line count; compare with its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Weeb Labs <weeblabs@VFIEVOX3.Router>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layout: picture line heights, body top below the header, effect extents
Headers with a logo or rule picture put the body in the wrong place in
Word documents, for three reasons:
- A line holding only a picture was exactly as tall as the picture. Word
keeps the run's font ascent and descent: the picture stands on the
baseline and the line is the taller of the font's line and the picture
plus the font's descent. Multiple line spacing scales the font's line
height only, never the picture (a 144pt picture at 1.15 made a 165.6pt
line; Word's is 146.4pt).
- The body started 6pt below a header that reaches past the top margin.
Word starts it right where the header ends.
- wp:effectExtent (room Word keeps around a picture for its shadow or
glow) was dropped. It is now kept on Float::effect, read and written,
and inline pictures reserve it in their line and are drawn inside it.
The reader also takes w:lineRule (and w:hRule) case-insensitively, as
Word does: `atleast` was read as multiple spacing.
Measured against the Word reference PDFs of docxide-pdf's corpus (365
documents, same open fonts for every run): mean SSIM 39.3% -> 42.1%,
documents passing SSIM 37 -> 51, mean ink overlap 19.5% -> 22.3%, page
count right 301 -> 304; 52 documents better, 6 worse (one header built
from nested tables had its own height error hidden by the old 6pt gap).
Tests: picture lines are the picture plus the font's descent under 1.15
spacing; the body starts where a tall header ends; effect extents grow
the picture's line box and the picture is drawn inside them; effect
extents round-trip through .docx; `atleast` and `EXACT` line rules read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: table rows keep their cell margins and border bands like Word
Word sizes a table row from the inside of its borders out: the cell's
text starts below the top border and the top margin, and an at-least row
height gets the cells' top and bottom margins and border widths on top
(a 30pt row with 5pt margins and 0.5pt borders steps 40.5pt). We drew
the text at the top margin and took an at-least height as the whole row,
so rows came out short and tables, and headers built from tables, ended
early: a form's header table ended 6pt above where Word starts the body.
Each row now reserves its cells' top border width above their text (the
last row also the bottom border below it), and an at-least height adds
the tallest margins plus border bands in the row. Exact heights are
unchanged.
Measured against the Word reference PDFs of docxide-pdf's corpus (365
documents, same open fonts), on top of the previous commit: mean SSIM
42.1% -> 42.8%, documents passing SSIM 51 -> 54, mean ink overlap 22.3%
-> 23.3%, mean line-break agreement 63.0% -> 63.5%; 42 documents better,
28 worse, mostly by a few points where an earlier offset had cancelled
the short rows.
Test: two 30pt at-least rows with 5pt margins and 0.5pt borders step
40.5pt (was 36.3), with the first cell's text 5.5pt below the table top.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: note the known limit of the picture-line rule
The space Word leaves below a line of only pictures is less than the
font's full descent we give it; two rules tried against docxide-pdf's
Word references (no descent, only the text's descent) each fixed some
documents and broke others. A shortcut: comment records the measured
cases so the rule isn't changed again without Word probes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layout: place floating pictures and shapes the way Word does
Pictures and shapes anchored in a .docx often landed in the wrong place:
the reader folded every margin-relative position into "margin", ignored
<wp:align>, and kept only distL as the distance from text. Floats in
headers, footers and table cells were not drawn at all.
Document model (doc):
- Anchor gains the left/right/top/bottom/inside/outside margin areas,
the anchor character and the anchor line
- Float gains h_align/v_align (FloatAlign: start, center, end, inside,
outside), which take precedence over the offsets, and separate
dist_top/dist_bottom (dist stays the left/right distance)
DOCX (docx): read and write wp:align, every relativeFrom value and
distT/distB/distL/distR.
Layout (layout):
- float_rect positions against each reference area, with alignment
- floats in headers, footers, table cells and text boxes are placed,
drawn (behind-text ones under the text) and wrapped around; headers
and footers get a page frame so page-relative floats land in page
coordinates, also when the header's height is measured for the body
top
- a paragraph's first line pushed below a top-and-bottom float is drawn
(and hit-tested) where it was laid out
- floats follow their paragraph to a new column or page, and are placed
at the paragraph's actual top (space before counted once, contextual
spacing respected)
- wrap areas use each distance as given; the old 9pt minimum is gone, so
a float with distance 0 now touches its text as in Word
- a list item's label is counted once (Ctx::next_label) and passed into
every layout of its paragraph (Ctx::para_labelled), so laying it out
again around floats doesn't advance the list counters twice; a
paragraph whose top moved is only laid out again when the page has
something to wrap around
Engine: switching an inline object to a wrapping style gives it Word's
default 9pt side distance; the position presets clear any alignment.
Approximations: inside/outside margins and alignments ignore mirrored
margins (treated as left/right); character- and line-relative positions
fall back to the column and the paragraph top.
Tests: docx fixture and round trip for alignment, reference areas and
distances; layout tests for alignment, the pushed-down first line,
floats moving to page two (with space before), header and cell floats,
header floats not changing the body top, hostile distances, and list
items counted once when laid out again (contextual spacing, page break
before, beside a float in a table cell).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: place a table's edge by the document's Word compatibility mode
Word 2013 and later (compatibilityMode 15) put a left-aligned table's
border at the margin plus its indent, moved in by half the first cell's
left border; earlier modes line the first cell's text up with the margin
instead, the border one cell margin further out. We always did the
latter, so in Word 2013+ documents (43% of docxide-pdf's Word reference
corpus) every table and its text sat a cell margin (usually 5.4pt) too
far left.
Settings::compat_mode keeps the document's mode: read from
w:compat/w:compatSetting, 12 when a document has no settings or doesn't
state one (as Word treats it), 15 for new documents, and written back
instead of a fixed 15. The legacy shift now uses the first cell's own
left margin.
Measured against docxide-pdf's corpus (365 documents, same open fonts):
mean SSIM 40.4% -> 42.1%, documents passing SSIM 40 -> 50, passing ink
overlap 142 -> 154; 71 documents better, 6 worse (tables nested in a
cell whose text Word puts at the outer cell's edge, which docxide-pdf
doesn't match either).
Tests: a mode-15 table's text sits a cell margin and half a border
inside the margin, a mode-12 table's at it; the mode round-trips
through .docx and defaults to 12 without settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: floating tables (w:tblpPr) stand at their position, text wraps
Floating tables were read as ordinary tables: laid out in the text flow
with no regard to their position, and the text after them waited below
instead of running beside them. 71 of the 382 documents in docxide-pdf's
Word reference corpus have them, typically forms anchored to the text
and the margin.
TableProps::float (TableFloat) keeps w:tblpPr and w:tblOverlap: what x
and y are measured from (column, margin or page; text, margin or page),
offsets or alignments, distances from text and whether it may overlap
other floating tables. The .docx reader and writer carry it.
A floating table at the top level of the body is placed at its position
(tblpY below the text where it stands, by default) and takes no room in
the flow; its area plus its distances from text joins the page's wrap
areas, so the following text runs beside it or below it. One that may
not overlap moves below the floating tables in its way, keeping (in Word
2013+ layout) its distance from the text's left edge; in earlier modes
an offset places the first cell's text, as an indent does. A table in
the flow starts below the floating objects in its way. Floating tables
inside table cells and headers are still laid out in their flow.
Measured against docxide-pdf's corpus (365 documents, same open fonts),
on top of the compatibility-mode commit: mean SSIM 42.1% -> 43.9%, mean
line-break agreement 63.5% -> 66.6%, documents passing SSIM 50 -> 55,
page count right 303 -> 305; 33 documents better, 4 worse by at most
3.1 points.
Tests: a full-width and a narrow floating table that may not overlap
stack, the narrow one keeping 9pt from the text edge, and the next
paragraph runs beside it; floating table positions round-trip through
.docx.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: a floating table taller than a page runs across pages
A floating table was always placed whole at its position, so one taller
than the page's body ran past the bottom margin and its rows were lost:
a Bulgarian programme whose whole body is one floating table came out as
3 pages instead of Word's 6.
Such a table now flows across pages like a table in the text (rows
splitting and header rows repeating as usual), from the floating table's
own left edge; the position is worked out by float_table_x, which the
whole-table placement shares.
In docxide-pdf's Word reference corpus that document has Word's page
count again with all its rows; nothing else changes.
Test: an 80-row floating table runs onto a second page, every row drawn
above the bottom margin from the table's left edge (was one page).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tidy floating-table code: one alignment helper, named steps, shared mappings
No change in behaviour; makes the floating-table code easier to read.
- FloatAlign::from_ooxml/ooxml (added with floating pictures) replace the
copies of the same mapping in the w:tblpPr reader and writer, and
TableFloat::dist_from_text the copies of its clamping.
- place_floating_table and float_table_x place by offset or alignment with
align_in, like floating pictures do, instead of their own copies.
- The overlap rule uses Rect::inset/intersects and names its condition
(keeps_text_distance) instead of a one-line chain of comparisons.
- below_floats names the step that starts a table in the flow below the
floating objects in its way.
- The floating-table functions move next to place_table, which calls them,
from after `mod tests;` at the end of the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Pictures and shapes anchored in a .docx often landed in the wrong place:
the reader folded every margin-relative position into "margin", ignored
<wp:align>, and kept only distL as the distance from text. Floats in
headers, footers and table cells were not drawn at all.
Document model (doc):
- Anchor gains the left/right/top/bottom/inside/outside margin areas,
the anchor character and the anchor line
- Float gains h_align/v_align (FloatAlign: start, center, end, inside,
outside), which take precedence over the offsets, and separate
dist_top/dist_bottom (dist stays the left/right distance)
DOCX (docx): read and write wp:align, every relativeFrom value and
distT/distB/distL/distR.
Layout (layout):
- float_rect positions against each reference area, with alignment
- floats in headers, footers, table cells and text boxes are placed,
drawn (behind-text ones under the text) and wrapped around; headers
and footers get a page frame so page-relative floats land in page
coordinates, also when the header's height is measured for the body
top
- a paragraph's first line pushed below a top-and-bottom float is drawn
(and hit-tested) where it was laid out
- floats follow their paragraph to a new column or page, and are placed
at the paragraph's actual top (space before counted once, contextual
spacing respected)
- wrap areas use each distance as given; the old 9pt minimum is gone, so
a float with distance 0 now touches its text as in Word
- a list item's label is counted once (Ctx::next_label) and passed into
every layout of its paragraph (Ctx::para_labelled), so laying it out
again around floats doesn't advance the list counters twice; a
paragraph whose top moved is only laid out again when the page has
something to wrap around
Engine: switching an inline object to a wrapping style gives it Word's
default 9pt side distance; the position presets clear any alignment.
Approximations: inside/outside margins and alignments ignore mirrored
margins (treated as left/right); character- and line-relative positions
fall back to the column and the paragraph top.
Tests: docx fixture and round trip for alignment, reference areas and
distances; layout tests for alignment, the pushed-down first line,
floats moving to page two (with space before), header and cell floats,
header floats not changing the body top, hostile distances, and list
items counted once when laid out again (contextual spacing, page break
before, beside a float in a table cell).
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Ignore the .superpowers/ agent scratch directory
* doc: Borders::overlay merges border sets per side (#143)
* Tables: draw style borders when w:tblBorders is empty (#143)
* docx: fixture pinning empty tblBorders/tcBorders parse contract (#143)
* Tables: border presets mask cleared sides with nil over the style (#143)
* Drop a stray tool entry from .gitignore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Clean-room foundations for WordCraft: Arc-shared document model with
paragraph/run editing, styles, lists, sections and tables; Word-style
font substitution; first-fit line breaking with tabs, lists, alignment,
pagination, tables, headers/footers and hit testing; vello_cpu page
rasteriser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>