Files
wordcraft/crates/layout
6e73fe40e3 Tables: Word 2013 table edge, and floating tables (w:tblpPr) (#137)
* 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>
2026-10-10 07:08:20 -04:00
..