fix(page): separate measurement context from the render canvas (S3)

add_child invalidated the canvas but left _draw bound to it, and the draw
property only rebuilt when _draw was None. Callers therefore got a context
pointing at a discarded image while page._canvas stayed None. table_layouter
reads page._canvas directly, so every image inside a table cell laid out after
any other content silently degraded to a grey [Image: WxH] placeholder.

The property now rebuilds when either half is missing. On its own that would
make layout allocate a full-page RGBA canvas per line, because layout measures
text through the page - so measurement moves to page.measurement_draw, a 1x1
scratch context that is never invalidated. Its mode matches the render canvas
so that Text's width cache does not hold two entries per word.

Children built against the scratch context are re-bound to the live canvas by
render_children, which already synchronised _draw and _canvas; that behaviour
was incidental and is now load-bearing and documented as such.

Regenerating the examples shows table images rendering as images rather than
placeholders. The empty header row in the second table of example 05 is
unrelated and pre-existing - row height ignores cell padding, so text is clipped
as padding grows - recorded as evidence under S6.
This commit is contained in:
2026-08-06 22:37:24 +02:00
parent 284d521125
commit 202dacf350
5 changed files with 186 additions and 10 deletions
+8 -7
View File
@@ -165,7 +165,7 @@ def paragraph_layouter(paragraph: Paragraph,
# Create a temporary Text object to calculate word width
if word:
temp_text = Text.from_word(word, page.draw)
temp_text = Text.from_word(word, page.measurement_draw)
temp_text.width
else:
pass
@@ -174,7 +174,7 @@ def paragraph_layouter(paragraph: Paragraph,
spacing=word_spacing_constraints,
origin=(x_cursor, y_cursor),
size=(page.available_width, baseline_spacing),
draw=page.draw,
draw=page.measurement_draw,
font=font,
halign=text_align
)
@@ -225,7 +225,7 @@ def paragraph_layouter(paragraph: Paragraph,
return False, i, overflow_text
# Check if the word will fit on the new line before adding it
temp_text = Text.from_word(word, page.draw)
temp_text = Text.from_word(word, page.measurement_draw)
if temp_text.width > current_line.size[0]:
# Word is too wide for the line, we need to hyphenate it
if len(word.text) >= 6:
@@ -234,13 +234,13 @@ def paragraph_layouter(paragraph: Paragraph,
(Text(
pair[0],
word.style,
page.draw,
page.measurement_draw,
line=current_line,
source=word),
Text(
pair[1],
word.style,
page.draw,
page.measurement_draw,
line=current_line,
source=word)) for pair in word.possible_hyphenation()]
if len(splits) > 0:
@@ -455,7 +455,7 @@ def button_layouter(button: Button,
available_height = page.remaining_height
# Create ButtonText renderable
button_text = ButtonText(button, font, page.draw, padding=padding)
button_text = ButtonText(button, font, page.measurement_draw, padding=padding)
# Check if button fits on current page
button_height = button_text.size[1]
@@ -505,7 +505,8 @@ def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = Non
available_height = page.remaining_height
# Create FormFieldText renderable
field_text = FormFieldText(field, font, page.draw, field_height=field_height)
field_text = FormFieldText(field, font, page.measurement_draw,
field_height=field_height)
# Check if field fits on current page
total_field_height = field_text.size[1]