Word 2013 and later fit more text on a justified line by shrinking its
spaces by up to 20% of their width, so long justified paragraphs broke
differently in WordCraft (a one-page test document's summary took 8 lines instead of
Word's 7). Lines that only fit with shrunk spaces now get them, the
last line of a paragraph included; lines with tabs never shrink.
The behaviour depends on the document's compatibility mode, which is
now read from settings.xml (w:compatSetting compatibilityMode), kept in
Settings::compat_mode and written back, so saving an older file no
longer silently upgrades it to mode 15. A .docx that names no mode is
treated as Word 2007 (12); new documents are 15.
Co-authored-by: ssamstudio <267867645+ssamstudio@users.noreply.github.com>
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>
* Character border (format.border): w:bdr run property, toggle command, boxed rendering
* Character border: Home > Font ribbon button and icon
* Character border: address review (plain notes), translate ribbon label, fix flaky layout test and clippy nonminimal_bool
* Tests: merge the fixture imports after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Spanish and Brazilian Portuguese labels for Character Border
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: WordCraft agent <agent@localhost>
Co-authored-by: 999sian <999sian@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
* PDF: fields and note numbers put their shown text in the text layer
A field, a note reference or an equation is shaped as one cluster that
stands for the object's placeholder character (U+FFFC) in the paragraph
text. The display list took a glyph run's text from the paragraph, so a
footer's "Page 1 of 5" went into the PDF's text layer as "Page  of ":
copying, search and screen readers got the placeholder. 76 of the 365
PDFs written from docxide-pdf's Word reference corpus had it; Word's
have none.
ParaLayout::shown keeps, by cluster index, the text such a cluster
draws (filled by Builder::shape_atomic), and the display list takes that
text for those clusters. The drawing doesn't change.
Now none of the 365 PDFs carries the placeholder; mean line-break
agreement with Word 62.9% -> 63.4% (the scorer reads the text layer).
Test: a footer's PAGE and NUMPAGES fields read "Page 1 of 2" and
"Page 2 of 2" in the exported PDF's text (was "Page  of ").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: a REF field's result is extractable PDF text (from #112 by @LloydNicholson)
Co-Authored-By: LloydNicholson <LloydNicholson@users.noreply.github.com>
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>
* 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>
* Typing parity with Word: lists, AutoFormat and AutoCorrect as you type
Typed side by side with Word (same keystrokes, results read back) and matched it:
- Enter after a list no longer gets stuck: the paragraph that leaves a list goes back to
Normal, and later Enters make blank paragraphs (the `num: 0` marker counted as a list item,
so every Enter re-ran "leave the list" and did nothing; the list indent also stuck to every
following paragraph).
- Enter on an empty nested item moves it up a level before leaving the list.
- Second Backspace at the start of an un-numbered list paragraph returns it to Normal
instead of joining it to the paragraph above.
- "1. " always starts numbering at 1; "a. " and "I. " start lettered/roman lists; "- " makes
a dash bullet.
- Undo right after an AutoFormat/AutoCorrect change undoes only that change.
- AutoCorrect: sentence capitals (with abbreviation/initial exceptions), runs on Enter too,
"word -- word" en dash, "wait..." ellipsis, 1/2 1/4 3/4, ordinal superscripts, web
addresses become links.
- Border lines: Enter after ---, ===, ***, ___, ~~~, ### draws a bottom border.
- Layout: no line break right after a slash ("and/or", URLs), as in Word.
- ui_shot: UI_SHOT_FULL=1 prints whole replies for scripted checks.
Regression tests in crates/engine/src/tests_typing.rs and the layout tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing parity: record Word's observed typing rules, two more tests
docs/typing-parity.md lists what Word does for each key sequence (lists, Backspace, Tab,
AutoCorrect, AutoFormat, slash line breaks) with the test that pins it, the known gaps, and how
to compare without taking over the shared machine. CLAUDE.md/AGENTS.md point at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tall_first_page_header_pushes_body_down compared page 1's first line with
the last paragraph on the last page. Where Aptos falls back to Liberation
Sans (Linux without Carlito), the last page is nearly full and that
paragraph sits below page 1's first line, so the test failed although
layout was correct. Compare with the first paragraph on page 2 instead,
which is what the test means: later pages don't reserve the tall
first-page header's space.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- body on a titlePg first page now starts below the first-page header
- decode tiff pictures
- fade images through an opacity layer; vello_cpu panics on image sampler alpha
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>