""" 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")