224 lines
8.0 KiB
Python
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)
|