Files
pyWebLayout/tests/concrete/test_functional_text_centering.py
T
dtourolle c5c61a3503
Python CI / test (3.10) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
Python CI / test (3.13) (push) Has been cancelled
fix(functional): centre text vertically in buttons and form fields (S14)
Both renderers placed the baseline at box_top + height/2 + descent/2. Centring
glyphs of visual height ascent+descent in a box of height H puts the baseline at
box_top + H/2 + (ascent-descent)/2; the two agree only when ascent is exactly
twice descent. DejaVu is nearer 4:1, so labels sat high against the top edge -
measured at 5px above and 11px below for a 14px button.

ButtonText also sized itself from the nominal font size, which is smaller than
the text's visual height (17px of ink for a 14px DejaVu font), leaving the
button too short to centre its label in. It now measures ascent+descent, with a
fallback for font objects that cannot report metrics.

docs/images/example_07_pressed_state.png was stale - no example writes it, the
demo emits demo_07_pressed.png at the repo root and the docs copy had been
placed by hand in November. Refreshed here; the demo should write straight to
docs/images/ so it cannot drift again.
2026-08-06 22:59:27 +02:00

125 lines
4.7 KiB
Python

"""
Regression tests for vertical centring of text in buttons and form fields.
Both placed the baseline at `top + height/2 + descent/2`. Centring text whose
visual height is ascent+descent inside a box of height H puts the baseline at
`top + H/2 + (ascent-descent)/2`; the two agree only when ascent == 2*descent.
Real fonts have a much larger ratio - DejaVu is nearer 4:1 - so the text sat
several pixels high, hugging the top edge of the button.
The button was also sized from the nominal font size rather than the text's
actual visual height, leaving it too short to centre anything in.
"""
import numpy as np
import pytest
from PIL import Image, ImageDraw
from pyWebLayout.abstract.functional import Button, FormField, FormFieldType
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
from pyWebLayout.style import Font
CANVAS = (300, 120)
PADDING = (6, 10, 6, 10) # top, right, bottom, left
@pytest.fixture
def draw_ctx():
image = Image.new("RGB", CANVAS, (255, 255, 255))
return image, ImageDraw.Draw(image)
def ink_rows(image, box):
"""
Rows within box that carry text ink.
Only the central columns are sampled: the button has rounded corners, so the
page background shows through at the extremes of every row and would read as
white text on all of them.
"""
x0, y0, x1, y1 = box
inset = (x1 - x0) // 4
pixels = image.convert("RGB").load()
rows = []
for y in range(y0, y1):
for x in range(x0 + inset, x1 - inset):
r, g, b = pixels[x, y]
# Button text is white on a blue fill; look for near-white ink.
if r > 240 and g > 240 and b > 240:
rows.append(y)
break
return rows
class TestButtonTextCentring:
@pytest.mark.parametrize("font_size", [10, 14, 20])
def test_text_is_vertically_centred(self, draw_ctx, font_size):
image, draw = draw_ctx
font = Font(font_size=font_size, colour=(255, 255, 255))
button = ButtonText(Button(label="Save Document", callback=lambda p: None),
font, draw, padding=PADDING)
button.set_origin(np.array([20, 20]))
button.render()
x0, y0 = 20, 20
x1 = x0 + int(button.size[0])
y1 = y0 + int(button.size[1])
rows = ink_rows(image, (x0, y0, x1, y1))
assert rows, "the button should have visible text"
gap_above = min(rows) - y0
gap_below = y1 - max(rows) - 1
assert abs(gap_above - gap_below) <= 2, (
f"text not centred at size {font_size}: "
f"{gap_above}px above, {gap_below}px below")
def test_button_is_tall_enough_for_its_text(self):
font = Font(font_size=14, colour=(255, 255, 255))
image = Image.new("RGB", CANVAS, (255, 255, 255))
draw = ImageDraw.Draw(image)
button = ButtonText(Button(label="Cancel", callback=lambda p: None),
font, draw, padding=PADDING)
ascent, descent = font.font.getmetrics()
assert int(button.size[1]) >= ascent + descent + PADDING[0] + PADDING[2], \
"button height must accommodate the text's visual height, not the nominal size"
def test_text_stays_inside_the_button(self, draw_ctx):
image, draw = draw_ctx
font = Font(font_size=14, colour=(255, 255, 255))
button = ButtonText(Button(label="Save Document", callback=lambda p: None),
font, draw, padding=PADDING)
button.set_origin(np.array([20, 20]))
button.render()
y0, y1 = 20, 20 + int(button.size[1])
rows = ink_rows(image, (20, y0, 20 + int(button.size[0]), y1))
assert min(rows) >= y0, "text escaped above the button"
assert max(rows) < y1, "text escaped below the button"
class TestFormFieldValueCentring:
def test_value_is_centred_in_the_input_box(self):
image = Image.new("RGB", (300, 120), (0, 0, 0))
draw = ImageDraw.Draw(image)
font = Font(font_size=12, colour=(0, 0, 0))
field = FormField(name="who", field_type=FormFieldType.TEXT, value="Hello")
renderable = FormFieldText(field, font, draw, field_height=28)
renderable.set_origin(np.array([10, 10]))
renderable.render()
field_y = 10 + font.font_size + 5
pixels = image.convert("RGB").load()
rows = [y for y in range(field_y, field_y + 28)
if any(pixels[x, y] == (0, 0, 0) for x in range(12, 200))]
assert rows, "the field value should be visible"
gap_above = min(rows) - field_y
gap_below = (field_y + 28) - max(rows) - 1
assert abs(gap_above - gap_below) <= 3, (
f"field value not centred: {gap_above}px above, {gap_below}px below")