134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""
|
|
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
|