Skip to content

Resolve a percentage width on an image in a table cell against the cell (#223) - #505

Open
jakejackson1 wants to merge 2 commits into
gravitypdffrom
fix/223-image-percentage-in-table-cell
Open

jakejackson1 wants to merge 2 commits into
gravitypdffrom
fix/223-image-percentage-in-table-cell

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

What was wrong

A percentage width, min-width or max-width on an <img> in a table cell was resolved against the block containing the table, because neither <table> nor <td> pushes a block level. In a 60mm cell of a 180mm table, max-width: 20% came out as 36mm and max-width: 100% never constrained anything. The image was then the widest unbreakable thing in its cell, so TableCheckMinWidth() made it the column's minimum and pulled the other columns off the widths they were declared with.

The fix

Img::open() no longer converts such a percentage inside a table. It keeps it in the image's objattr (cell_sizing: the lengths it was given, its natural size and the percentages still to resolve) and sizes the image provisionally as though the percentage were absent. Once the column widths are known, _tableHeight() calls the new Mpdf::sizeCellImages(), which resolves the percentages against the cell's content width and rewrites the object. Every later consumer (printbuffer, _tableWrite) sees the final size. The image is resized from what it was given each time, so the repeated layouts of the shrink/autosize loop give the same result. A percentage border-radius on such an image is resolved again against its final box.

The sizing rules (default size, aspect ratio, min/max clamps, page fit) moved out of Img into Mpdf\Image\ImageSizing::fit(), so the tag and the table code use the same rules. The eight copies of "CSS value, else attribute, then convert" in Img became one length() helper.

Column minimums and shrink-to-fit

As in CSS (css-sizing-3 §5.2.2, compressible replaced elements), an image with a percentage width or max-width no longer sets its column's minimum width. Its minimum is its absolute min-width (if any) plus padding, border and margin. This is what keeps declared column widths intact.

On its own, that would break tables too wide for the page. mPDF shrinks those as a whole (shrin_k), columns go to their minimums, and a compressible image's column would collapse to almost nothing (0.1mm in a table of long URLs). So when the first measurement says the table must shrink ($check > 1) and it has such images, the table is measured again with the images keeping their provisional widths. The picture is then scaled with the rest of the table, as it was before this change. Nested tables work out both minimums when they close (nestedmiw and nestedmiw_kept), so this also covers an image inside a nested table of an overflowing table. Both extra measurements only run for tables that contain such images.

Absolute lengths, images outside tables, and height/min-height/max-height behave as before. No snapshot fixture changed.

Decisions

  • A picture given width: 100% in an auto-width column takes its natural width as its max-content contribution, as browsers do. The column's width comes from that, and the picture then fills the column.
  • I considered computing the "kept" and "compressible" minimums in a single pass of _tableColumnWidth(). Everything there that derives from miw (colspans, percentage columns, the overflow check) would have needed a second copy, so I chose the re-measure, which runs only for affected tables.
  • The descendant-selector defect mentioned at the end of the issue (td img { … } never matching) is separate: no descendant rule under a table element matches anything inside a cell, not even td span or td p. It is a CSS cascade change with its own risk, so it has its own PR (Match descendant rules naming a table or cell against the content of the cell (#223) #507).

Tests

tests/Mpdf/ImagePercentageInTableCellTest.php covers:

  • max-width, width (CSS and attribute) and min-width percentages resolving against the cell.
  • Unchanged behaviour outside tables and for absolute lengths.
  • Declared column widths being kept (the image's x position and width).
  • A table shrunk to fit.
  • A nested table.
  • Images keeping their width in overflowing tables, directly and nested.
  • A percentage radius after resizing.

The core cases fail on gravitypdf.

Part of #223; the descendant-selector half is #507, which closes it.

🤖 Generated with Claude Code

…ll (mpdf#420)

A percentage width, min-width or max-width on an <img> inside a table was
converted against the block containing the table, since neither <table> nor
<td> pushes a block level. The image came out wider than its column and, being
the column's widest unbreakable content, widened the column past the width it
was declared with.

The image now carries such a percentage until the table is laid out, and
_tableHeight() resolves it against the cell's content width. Until then a
percentage width or max-width lets the cell narrow the picture, so it no longer
raises the column's minimum. A table that has to be shrunk to fit its page is
measured again with the pictures keeping their widths, so that a picture is
scaled with the rest of the table instead of left in a cell with no room.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant