fix(layout): honour horizontal padding and page origin (S2)
paragraph_layouter placed lines at page.border_size while sizing them to available_width, which subtracts both paddings. Text therefore started flush against the left border and the entire padding budget accumulated on the right, so lines broke well short of the right border. Page now describes its content box directly - content_origin, content_rect and remaining_height - and the layouters use it instead of each recomputing the geometry from border_size. The four block layouters had all been computing remaining space as size[1] - y_offset - border_size, subtracting the border but not the bottom padding, so every block type could be placed into the bottom padding; remaining_height fixes that too. Page also gains an origin, defaulting to (0, 0). That is inert for a top-level page but lets a page be positioned inside another surface, which table cells need in order to be laid out by the normal engine. Golden images regenerated: content now sits inside the padding on all sides.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Regression tests for page content geometry (spec S2).
|
||||
|
||||
Content must be laid out inside the content box - the page box less its border
|
||||
and padding - on all four sides. Horizontal padding was previously ignored on the
|
||||
left, shifting every line left by padding_left and leaving a gutter of
|
||||
padding_left + padding_right on the right, so lines appeared to break early.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from pyWebLayout.abstract.block import 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
|
||||
|
||||
|
||||
PADDING = (40, 30, 40, 20) # top, right, bottom, left - deliberately asymmetric
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def font():
|
||||
return Font(font_size=12)
|
||||
|
||||
|
||||
def filled_page(size, style, font, word_count=120):
|
||||
page = Page(size=size, style=style)
|
||||
paragraph = Paragraph(font)
|
||||
for i in range(word_count):
|
||||
paragraph.add_word(Word(f"word{i}", font))
|
||||
DocumentLayouter(page).layout_paragraph(paragraph)
|
||||
return page
|
||||
|
||||
|
||||
class TestContentBox:
|
||||
"""content_origin / content_rect describe the box content lives in."""
|
||||
|
||||
def test_content_origin_includes_border_and_padding(self):
|
||||
page = Page(size=(400, 300), style=PageStyle(border_width=2, padding=PADDING))
|
||||
assert page.content_origin == (2 + 20, 2 + 40)
|
||||
|
||||
def test_content_rect_subtracts_both_paddings(self):
|
||||
page = Page(size=(400, 300), style=PageStyle(border_width=2, padding=PADDING))
|
||||
x, y, w, h = page.content_rect
|
||||
assert (x, y) == (22, 42)
|
||||
assert w == 400 - 2 * 2 - 20 - 30
|
||||
assert h == 300 - 2 * 2 - 40 - 40
|
||||
|
||||
def test_page_origin_offsets_the_content_box(self):
|
||||
"""A page placed inside another surface reports absolute coordinates."""
|
||||
page = Page(size=(100, 50), style=PageStyle(border_width=1, padding=(5, 5, 5, 5)),
|
||||
origin=(200, 300))
|
||||
assert page.content_origin == (206, 306)
|
||||
|
||||
def test_remaining_height_respects_bottom_padding(self, font):
|
||||
style = PageStyle(border_width=2, padding=PADDING)
|
||||
page = Page(size=(400, 300), style=style)
|
||||
# Nothing laid out yet: the whole content box is available.
|
||||
assert page.remaining_height == page.content_rect[3]
|
||||
|
||||
|
||||
class TestLinePlacement:
|
||||
"""Lines must start after the left padding and end before the right padding."""
|
||||
|
||||
def test_first_line_starts_at_content_origin(self, font):
|
||||
style = PageStyle(border_width=2, padding=PADDING)
|
||||
page = filled_page((400, 300), style, font)
|
||||
|
||||
line = page.children[0]
|
||||
assert int(line.origin[0]) == page.content_origin[0]
|
||||
assert int(line.origin[1]) == page.content_origin[1]
|
||||
|
||||
def test_line_width_matches_content_width(self, font):
|
||||
style = PageStyle(border_width=2, padding=PADDING)
|
||||
page = filled_page((400, 300), style, font)
|
||||
|
||||
line = page.children[0]
|
||||
assert int(line.size[0]) == page.content_rect[2]
|
||||
|
||||
def test_no_line_extends_past_the_right_content_edge(self, font):
|
||||
style = PageStyle(border_width=2, padding=PADDING)
|
||||
page = filled_page((400, 300), style, font)
|
||||
right_edge = page.content_rect[0] + page.content_rect[2]
|
||||
|
||||
for line in page.children:
|
||||
assert int(line.origin[0]) + int(line.size[0]) <= right_edge
|
||||
|
||||
def test_ink_stays_inside_the_content_box(self, font):
|
||||
"""The rendered pixels, not just the boxes, respect the padding."""
|
||||
style = PageStyle(border_width=0, padding=PADDING,
|
||||
background_color=(255, 255, 255))
|
||||
page = filled_page((400, 300), style, font)
|
||||
image = page.render().convert("L")
|
||||
pixels = image.load()
|
||||
|
||||
inked_x = [x for x in range(400) for y in range(300) if pixels[x, y] < 128]
|
||||
assert inked_x, "the page should have text on it"
|
||||
|
||||
x0, _, w, _ = page.content_rect
|
||||
assert min(inked_x) >= x0
|
||||
assert max(inked_x) <= x0 + w
|
||||
|
||||
def test_right_gutter_is_not_double_width(self, font):
|
||||
"""
|
||||
The regression: text was shifted left by padding_left, so the right gutter
|
||||
was padding_left + padding_right wide while the left gutter was zero.
|
||||
"""
|
||||
style = PageStyle(border_width=0, padding=(10, 30, 10, 30))
|
||||
page = filled_page((400, 300), style, font, word_count=200)
|
||||
image = page.render().convert("L")
|
||||
pixels = image.load()
|
||||
inked_x = [x for x in range(400) for y in range(300) if pixels[x, y] < 128]
|
||||
|
||||
left_gutter = min(inked_x)
|
||||
right_gutter = 400 - max(inked_x)
|
||||
# Justification means the right edge is not always exactly flush, so allow
|
||||
# slack - but the two gutters must be comparable, not 0 vs 60.
|
||||
assert abs(left_gutter - right_gutter) < 25, \
|
||||
f"asymmetric gutters: left={left_gutter} right={right_gutter}"
|
||||
|
||||
|
||||
class TestBlockBottomBoundary:
|
||||
"""Blocks must not be placed into the bottom padding."""
|
||||
|
||||
def test_lines_stop_before_bottom_padding(self, font):
|
||||
style = PageStyle(border_width=2, padding=PADDING)
|
||||
page = filled_page((400, 300), style, font, word_count=500)
|
||||
bottom_edge = page.content_rect[1] + page.content_rect[3]
|
||||
|
||||
for line in page.children:
|
||||
assert int(line.origin[1]) <= bottom_edge
|
||||
Reference in New Issue
Block a user