""" Regression tests for the page draw/canvas lifecycle (spec S3). add_child invalidates the canvas but left _draw pointing at it, and the draw property only rebuilt when _draw was None. Callers therefore received a context bound to a discarded image while page._canvas stayed None - which is how images inside table cells ended up as grey placeholders: table_layouter passed canvas=None through to the cell renderer. Fixing that alone would make layout allocate a full-page canvas per line, since layout measures text through the page. Measurement now goes through a dedicated scratch context. """ import pytest from PIL import Image from pyWebLayout.abstract.block import Image as AbstractImage, Paragraph from pyWebLayout.abstract.inline import Word from pyWebLayout.concrete.page import Page from pyWebLayout.layout.document_layouter import DocumentLayouter from pyWebLayout.style import Font from pyWebLayout.style.page_style import PageStyle @pytest.fixture def font(): return Font(font_size=12) @pytest.fixture def page(): return Page(size=(400, 600), style=PageStyle()) def paragraph_of(font, count=40): paragraph = Paragraph(font) for i in range(count): paragraph.add_word(Word(f"word{i}", font)) return paragraph class TestDrawIsNeverStale: def test_draw_matches_canvas_after_add_child(self, page, font): page.draw # force canvas creation DocumentLayouter(page).layout_paragraph(paragraph_of(font)) assert page.draw.im is page._canvas.im, \ "draw must be bound to the page's current canvas" def test_canvas_is_present_after_layout(self, page, font): DocumentLayouter(page).layout_paragraph(paragraph_of(font)) page.draw assert page._canvas is not None def test_repeated_draw_access_is_stable(self, page): first = page.draw assert page.draw is first, "draw must not be rebuilt while the canvas stands" class TestMeasurementDoesNotAllocateCanvases: def test_layout_allocates_no_page_canvas(self, page, font, monkeypatch): calls = [] original = Page._create_canvas def counting(self): calls.append(1) return original(self) monkeypatch.setattr(Page, "_create_canvas", counting) DocumentLayouter(page).layout_paragraph(paragraph_of(font, 400)) assert calls == [], \ f"layout allocated {len(calls)} full-page canvases; it should allocate none" def test_measurement_context_is_tiny_and_matches_canvas_mode(self, page): scratch = page.measurement_draw assert scratch.im.size == (1, 1) assert scratch.mode == Page._CANVAS_MODE def test_measurement_context_is_stable(self, page, font): first = page.measurement_draw DocumentLayouter(page).layout_paragraph(paragraph_of(font)) assert page.measurement_draw is first, \ "the scratch context must survive canvas invalidation" class TestRenderIsRepeatable: def test_two_renders_are_identical(self, page, font): DocumentLayouter(page).layout_paragraph(paragraph_of(font)) first = page.render().copy() second = page.render().copy() assert first.tobytes() == second.tobytes() class TestImageInCellGetsARealCanvas: """The concrete symptom: table images degraded to placeholders.""" @pytest.fixture def image_path(self, tmp_path): path = tmp_path / "swatch.png" Image.new("RGB", (40, 30), (10, 200, 10)).save(path) return str(path) def test_table_after_paragraph_receives_a_canvas(self, page, font, image_path): from pyWebLayout.abstract.block import Table, TableCell, TableRow from pyWebLayout.layout.document_layouter import table_layouter layouter = DocumentLayouter(page) layouter.layout_paragraph(paragraph_of(font, 10)) table = Table() row = TableRow() cell = TableCell() cell.add_block(AbstractImage(image_path)) row.add_cell(cell) table.add_row(row) # The canvas is invalidated by the preceding add_child; the table must # still be handed a real one. assert table_layouter(table, page) or True # placement may fail on space assert page._canvas is not None, \ "table layout must not run against a None canvas"