pyWebLayout/tests/test_monospace_basic.py
Duncan Tourolle ae15fe54e8
All checks were successful
Python CI / test (push) Successful in 5m17s
new style handling
2025-06-22 13:42:15 +02:00

224 lines
8.0 KiB
Python

"""
Basic mono-space font tests for predictable character width behavior.
This test focuses on the fundamental property of mono-space fonts:
every character has the same width, making layout calculations predictable.
"""
import unittest
import os
from PIL import Image
from pyWebLayout.concrete.text import Text, Line
from pyWebLayout.style.fonts import Font
from pyWebLayout.style.layout import Alignment
class TestMonospaceBasics(unittest.TestCase):
"""Basic tests for mono-space font behavior."""
def setUp(self):
"""Set up test with a mono-space font if available."""
# Try to find DejaVu Sans Mono
mono_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"/System/Library/Fonts/Monaco.ttf",
"C:/Windows/Fonts/consola.ttf"
]
self.mono_font_path = None
for path in mono_paths:
if os.path.exists(path):
self.mono_font_path = path
break
if self.mono_font_path:
self.font = Font(font_path=self.mono_font_path, font_size=12)
# Calculate reference character width
ref_char = Text("M", self.font)
self.char_width = ref_char.width
print(f"Using mono-space font: {self.mono_font_path}")
print(f"Character width: {self.char_width}px")
else:
print("No mono-space font found - tests will be skipped")
def test_character_width_consistency(self):
"""Test that all characters have the same width."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Test a variety of characters
test_chars = "AaBbCc123!@#.,:;'\"()[]{}|-_+=<>"
widths = []
for char in test_chars:
text = Text(char, self.font)
widths.append(text.width)
print(f"'{char}': {text.width}px")
# All widths should be nearly identical
min_width = min(widths)
max_width = max(widths)
variance = max_width - min_width
self.assertLessEqual(variance, 2,
f"Character width variance should be minimal, got {variance}px")
def test_predictable_string_width(self):
"""Test that string width equals character_width * length."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
test_strings = [
"A",
"AB",
"ABC",
"ABCD",
"Hello",
"Hello World",
"123456789"
]
for s in test_strings:
text = Text(s, self.font)
expected_width = len(s) * self.char_width
actual_width = text.width
# Allow small variance for font rendering
diff = abs(actual_width - expected_width)
max_allowed_diff = len(s) + 2 # Small tolerance
print(f"'{s}' ({len(s)} chars): expected {expected_width}px, "
f"actual {actual_width}px, diff {diff}px")
self.assertLessEqual(diff, max_allowed_diff,
f"String '{s}' width should be predictable")
def test_line_capacity_prediction(self):
"""Test that we can predict how many characters fit on a line."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Test with different line widths
test_widths = [100, 200, 300]
for line_width in test_widths:
# Calculate expected character capacity
expected_chars = line_width // self.char_width
# Create a line and fill it with single characters
line = Line(
spacing=(1, 1), # Minimal spacing
origin=(0, 0),
size=(line_width, 20),
font=self.font,
halign=Alignment.LEFT
)
chars_added = 0
for i in range(expected_chars + 5): # Try a few extra
result = line.add_word("X", self.font)
if result is not None: # Doesn't fit
break
chars_added += 1
print(f"Line width {line_width}px: expected ~{expected_chars} chars, "
f"actual {chars_added} chars")
# Should be reasonably close to prediction
self.assertGreaterEqual(chars_added, max(1, expected_chars - 2))
self.assertLessEqual(chars_added, expected_chars + 2)
def test_word_breaking_with_known_widths(self):
"""Test word breaking with known character widths."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Create a line that fits exactly 10 characters
line_width = self.char_width * 10
line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(line_width, 20),
font=self.font,
halign=Alignment.LEFT
)
# Try to add a word that's too long
long_word = "ABCDEFGHIJKLMNOP" # 16 characters
result = line.add_word(long_word, self.font)
# Word should be broken or rejected
if result is None:
self.fail("16-character word should not fit in 10-character line")
else:
print(f"Long word '{long_word}' result: '{result}'")
# Check that some text was added
self.assertGreater(len(line.text_objects), 0,
"Some text should be added to the line")
if line.text_objects:
added_text = line.text_objects[0].text
print(f"Added to line: '{added_text}' ({len(added_text)} chars)")
# Added text should be shorter than original
self.assertLess(len(added_text), len(long_word),
"Added text should be shorter than original word")
def test_alignment_visual_differences(self):
"""Test that different alignments produce visually different results."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Use a line width that allows for visible alignment differences
line_width = self.char_width * 20
test_words = ["Hello", "World"]
alignments = [
(Alignment.LEFT, "left"),
(Alignment.CENTER, "center"),
(Alignment.RIGHT, "right"),
(Alignment.JUSTIFY, "justify")
]
results = {}
for alignment, name in alignments:
line = Line(
spacing=(3, 8),
origin=(0, 0),
size=(line_width, 20),
font=self.font,
halign=alignment
)
# Add test words
for word in test_words:
result = line.add_word(word, self.font)
if result is not None:
break
# Render the line
line_image = line.render()
results[name] = line_image
# Save for visual inspection
output_dir = "test_output"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_path = os.path.join(output_dir, f"mono_align_{name}.png")
line_image.save(output_path)
print(f"Saved {name} alignment test to: {output_path}")
# All alignments should produce valid images
for name, image in results.items():
self.assertIsInstance(image, Image.Image)
self.assertEqual(image.size, (line_width, 20))
if __name__ == '__main__':
unittest.main(verbosity=2)