Table Layout

Table layout is organized around a shared Cellmap. Table normalization prepares the frame tree; table sizing populates column geometry; cells publish row heights; rows and row groups consume the resulting positions and dimensions; rendering then selects separate- or collapsed-border behavior.

[!NOTE] This page describes source-defined behavior. No successful or failed PDF rendering record was retrieved, so visual output and runtime reachability are not observed here.

flowchart TD
    n1["Table normalization"] -->|normalized frames| n2["Cellmap population"]
    n2 -->|table geometry| n3["Column sizing and x positions"]
    n3 -->|used columns| n4["Cell reflow"]
    n4 -->|row heights| n5["Row and row-group reflow"]
    n5 -->|pagination boundary| n6["Split mutation"]
    n5 -->|laid-out frames| n7["Cell and row-group rendering"]
    n6 -->|updated geometry| n7

Build cell and row geometry

The owned table layout stages use these handoffs:

  • Table::normalise handles direct cell children. When a child has display set to table-cell, it creates anonymous tbody and tr frames, decorates them, and appends the cell to the anonymous row. A row's normalise method takes the opposite path: children whose display is not table-cell are moved after the parent table instead of remaining inside the row. See pdf/src/FrameDecorator/Table.php and pdf/src/FrameDecorator/TableRow.php.
  • TableReflower::get_min_max_width calls table normalization, adds the table frame to the Cellmap, and classifies columns as absolute, percentage, or auto. Width totals include table margins, padding, borders, and non-collapsed border spacing. See pdf/src/FrameReflower/Table.php.
  • TableReflower::_assign_widths resolves the table width from preferred, minimum, maximum, containing-block, margin, padding, and border values. It distributes available width across auto, absolute, and percentage columns. An over-constrained table assigns minimum widths; a locked Cellmap returns without reassigning columns. See pdf/src/FrameReflower/Table.php.
  • TableCell::position obtains the cell position from the parent table's Cellmap and assigns it to the cell frame. See pdf/src/Positioner/TableCell.php.
  • TableCell::reflow consumes the used widths of the columns spanned by the cell. It subtracts horizontal margin, padding, and border space before storing the remaining value as the cell content width. See pdf/src/FrameReflower/TableCell.php.
  • After cell content reflow, TableCell::reflow uses the greater of styled height and content height, divides that height across the rows spanned by the cell, adds top and bottom spacing when applicable, and publishes each resulting row height with set_row_height. TableRow::reflow later reads the row and frame dimensions from the Cellmap. See pdf/src/FrameReflower/TableCell.php and pdf/src/FrameReflower/TableRow.php.
  • TableCell::set_cell_height removes vertical margin, padding, and border space from the assigned height. If space remains beyond the content height, baseline and top alignment leave the line boxes in place; middle and bottom alignment move line-box children vertically. See pdf/src/FrameDecorator/TableCell.php.
  • TableRow::position retains the containing block's x coordinate. Its y coordinate is the containing block's y coordinate for the first row, or the previous sibling's y position plus its margin height for later rows. During reflow, TableRow ultimately resets its position from the parent table's Cellmap. See pdf/src/Positioner/TableRow.php and pdf/src/FrameReflower/TableRow.php.

Handle row groups and page boundaries

flowchart TD
    g1["TableRowGroup::reflow"] -->|set containing block| r1["TableRow::reflow"]
    r1 -->|reflowed row| b1["page->check_page_break($child)"]
    b1 -->|split boundary| s1["TableRowGroup::split"]
    s1 -->|remove child and following rows| c1["Cellmap"]
    s1 -->|conditional copies| h1["Table::_headers"]

TableRowGroup::reflow assigns the group's containing block to each child row and reflows rows in source order. It checks for a page break after each child row, returns when the page is full, and otherwise obtains the group's width, height, and position from the table's Cellmap.

Page-full checks are not all at the same boundary:

  • TableRow::reflow returns before positioning when the page is full, before each child cell when the page becomes full, and again after child reflow before publishing Cellmap-derived dimensions.
  • TableCell::reflow breaks its child-content loop when the page is full, so later content children are not reflowed by that loop.
  • TableRowGroup::reflow checks page fullness before each row and after the row-group loop. Its check_page_break($child) call occurs after the child row has already reflowed.
  • Table reflow first calls check_forced_page_break($frame) and then returns if the page is full. It starts table reflow, assigns widths and Cellmap x positions, then iterates over children: only non-nested tables use the page-full break before a child and the post-child check_page_break($child); nested tables still visit the child-reflow path. After the loop, both paths calculate table height, handle collapsed-border styling, end table reflow, and attach an in-flow table to its parent block when applicable.

Consequently, a full page can prevent later cell, row, row-group, positioning, or child-reflow work. For row groups specifically, the page-break check is post-row rather than a pre-row eligibility check.

Splitting and registered headers

TableRowGroup::split mutates the parent table's Cellmap:

  • It removes the split child and every following row.
  • If the split occurs at the first child, it also removes the row group from the Cellmap before delegating the frame split.
  • For a later split, it updates the existing row-group entry to the previous remaining row before delegating.

Table normalization stores table-header-group children in the table's _headers collection and table-footer-group children in _footers. When a table with registered headers splits at a child that is neither a header nor immediately after a header, the split branch deep-copies each registered header and inserts the copies before the split child. Header or first-non-header cases avoid that duplication and move the table to the next page.

[!WARNING] The inspected source establishes footer-group registration but does not establish a corresponding repeated-footer path. Do not assume that _footers causes footer rows to be copied across pages.

Render cell and row-group branches

The renderer dispatch sends table-cell frames to the table-cell renderer and table-row-group, table-header-group, and table-footer-group frames to the row-group renderer. See pdf/src/Renderer.php.

In the separate-border branch, the implementation can paint the cell background and background image, then render the border and outline before returning. In the collapsed-border branch, it derives properties for outer and shared edges, queues border calls, and paints the background using the resolved widths. These secondary operations support the compact branch comparison below.

Branch Guard or dependency Primary consequence
Empty cell Trimmed node value is empty and empty_cells is hide Return before painting
Separate borders Parent table's border_collapse is not collapse Render the cell's separate-border box
Collapsed borders The cell must have a spanned-cell entry in the Cellmap Render from Cellmap-resolved shared edges
Collapsed borders without a spanned entry Cellmap::get_spanned_cells returns null Return without rendering

In collapsed-border mode, TableRowGroup::reflow sets the row-group border style to none because the cells use the borders. The row-group renderer still invokes its border and outline rendering operations; the effective collapsed style comes from the preceding reflow adjustment. See pdf/src/FrameReflower/TableRowGroup.php and pdf/src/Renderer/TableRowGroup.php.

[!WARNING] The collapsed-border cell renderer contains an explicit TODO for outline support. Treat outlines as unsupported in that branch.

Updated