pyWebLayout/tests/test_monospace_concepts.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

344 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Mono-space font testing concepts and demo.
This test demonstrates why mono-space fonts are valuable for testing
rendering, line-breaking, and hyphenation, even when using regular fonts.
"""
import unittest
import os
from PIL import Image
from pyWebLayout.concrete.text import Text, Line
from pyWebLayout.concrete.page import Page, Container
from pyWebLayout.style.fonts import Font
from pyWebLayout.style.layout import Alignment
class TestMonospaceConcepts(unittest.TestCase):
"""Demonstrate mono-space testing concepts."""
def setUp(self):
"""Set up test with available fonts."""
# Use the project's default font
self.regular_font = Font(font_size=12)
# Analyze character width variance
test_chars = "iIlLmMwW0O"
self.char_analysis = {}
for char in test_chars:
text = Text(char, self.regular_font)
self.char_analysis[char] = text.width
widths = list(self.char_analysis.values())
self.min_width = min(widths)
self.max_width = max(widths)
self.variance = self.max_width - self.min_width
print(f"\nFont analysis:")
print(f"Character width range: {self.min_width}-{self.max_width}px")
print(f"Variance: {self.variance}px")
# Find most uniform character (closest to average)
avg_width = sum(widths) / len(widths)
self.uniform_char = min(self.char_analysis.keys(),
key=lambda c: abs(self.char_analysis[c] - avg_width))
print(f"Most uniform character: '{self.uniform_char}' ({self.char_analysis[self.uniform_char]}px)")
def test_character_width_predictability(self):
"""Show why predictable character widths matter for testing."""
print("\n=== Character Width Predictability Demo ===")
# Compare narrow vs wide characters
narrow_word = "ill" # Narrow characters
wide_word = "WWW" # Wide characters
uniform_word = self.uniform_char * 3 # Uniform characters
narrow_text = Text(narrow_word, self.regular_font)
wide_text = Text(wide_word, self.regular_font)
uniform_text = Text(uniform_word, self.regular_font)
print(f"Same length (3 chars), different widths:")
print(f" '{narrow_word}': {narrow_text.width}px")
print(f" '{wide_word}': {wide_text.width}px")
print(f" '{uniform_word}': {uniform_text.width}px")
# Show the problem this creates for testing
width_ratio = wide_text.width / narrow_text.width
print(f" Width ratio: {width_ratio:.1f}x")
if width_ratio > 1.5:
print(" → This variance makes line capacity unpredictable!")
# With mono-space, all would be ~36px (3 chars × 12px each)
theoretical_mono = 3 * 12
print(f" With mono-space: ~{theoretical_mono}px each")
def test_line_capacity_challenges(self):
"""Show how variable character widths affect line capacity."""
print("\n=== Line Capacity Prediction Challenges ===")
line_width = 120 # Fixed width
# Test with different character types
test_cases = [
("narrow", "i" * 20), # 20 narrow chars
("wide", "W" * 8), # 8 wide chars
("mixed", "Hello World"), # Mixed realistic text
("uniform", self.uniform_char * 15) # 15 uniform chars
]
print(f"Line width: {line_width}px")
for name, test_text in test_cases:
text_obj = Text(test_text, self.regular_font)
fits = "YES" if text_obj.width <= line_width else "NO"
print(f" {name:8}: '{test_text[:15]}...' ({len(test_text)} chars)")
print(f" Width: {text_obj.width}px, Fits: {fits}")
print("\nWith mono-space fonts:")
char_width = 12 # Theoretical mono-space width
capacity = line_width // char_width
print(f" Predictable capacity: ~{capacity} characters")
print(f" Any {capacity}-character string would fit")
def test_word_breaking_complexity(self):
"""Demonstrate word breaking complexity with variable widths."""
print("\n=== Word Breaking Complexity Demo ===")
# Create a narrow line
line_width = 80
line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(line_width, 20),
font=self.regular_font,
halign=Alignment.LEFT
)
# Test different word types
test_words = [
("narrow", "illillill"), # 9 narrow chars
("wide", "WWWWW"), # 5 wide chars
("mixed", "Hello"), # 5 mixed chars
]
print(f"Line width: {line_width}px")
for word_type, word in test_words:
# Create fresh line for each test
test_line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(line_width, 20),
font=self.regular_font,
halign=Alignment.LEFT
)
word_obj = Text(word, self.regular_font)
result = test_line.add_word(word, self.regular_font)
fits = "YES" if result is None else "NO"
print(f" {word_type:6}: '{word}' ({len(word)} chars, {word_obj.width}px) → {fits}")
if result is not None and test_line.text_objects:
added = test_line.text_objects[0].text
print(f" Added: '{added}', Remaining: '{result}'")
print("\nWith mono-space fonts, word fitting would be predictable:")
char_width = 12
capacity = line_width // char_width
print(f" Any word ≤ {capacity} characters would fit")
print(f" Any word > {capacity} characters would need breaking")
def test_alignment_consistency(self):
"""Show how alignment behavior varies with character widths."""
print("\n=== Alignment Consistency Demo ===")
line_width = 150
# Test different alignments with various text
test_texts = [
"ill ill ill", # Narrow characters
"WWW WWW WWW", # Wide characters
"The cat sat", # Mixed characters
]
alignments = [
(Alignment.LEFT, "LEFT"),
(Alignment.CENTER, "CENTER"),
(Alignment.RIGHT, "RIGHT"),
(Alignment.JUSTIFY, "JUSTIFY")
]
results = {}
for align_enum, align_name in alignments:
print(f"\n{align_name} alignment:")
for text in test_texts:
line = Line(
spacing=(3, 8),
origin=(0, 0),
size=(line_width, 20),
font=self.regular_font,
halign=align_enum
)
# Add words to line
words = text.split()
for word in words:
result = line.add_word(word, self.regular_font)
if result is not None:
break
# Render and save
line_image = line.render()
# Calculate text coverage
text_obj = Text(text.replace(" ", ""), self.regular_font)
coverage = text_obj.width / line_width
print(f" '{text}': {coverage:.1%} line coverage")
# Save example
output_dir = "test_output"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
filename = f"align_{align_name.lower()}_{text.replace(' ', '_')}.png"
output_path = os.path.join(output_dir, filename)
line_image.save(output_path)
print("\nWith mono-space fonts:")
print(" - Alignment calculations would be simpler")
print(" - Spacing distribution would be more predictable")
print(" - Visual consistency would be higher")
def test_hyphenation_decision_factors(self):
"""Show factors affecting hyphenation decisions."""
print("\n=== Hyphenation Decision Factors ===")
# Test word that might benefit from hyphenation
test_word = "development" # 11 characters
word_obj = Text(test_word, self.regular_font)
print(f"Test word: '{test_word}' ({len(test_word)} chars, {word_obj.width}px)")
# Test different line widths
test_widths = [60, 80, 100, 120, 140]
for width in test_widths:
line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(width, 20),
font=self.regular_font,
halign=Alignment.LEFT
)
result = line.add_word(test_word, self.regular_font)
if result is None:
status = "FITS completely"
elif line.text_objects:
added = line.text_objects[0].text
status = f"PARTIAL: '{added}' + '{result}'"
else:
status = "REJECTED completely"
# Calculate utilization
utilization = word_obj.width / width
print(f" Width {width:3}px ({utilization:>5.1%} util): {status}")
print("\nWith mono-space fonts:")
char_width = 12
word_width_mono = len(test_word) * char_width # 132px
print(f" Word would be exactly {word_width_mono}px")
print(f" Hyphenation decisions would be based on character count")
print(f" Line capacity would be width ÷ {char_width}px per char")
def test_create_visual_comparison(self):
"""Create visual comparison showing the difference."""
print("\n=== Creating Visual Comparison ===")
# Create a page showing the problems with variable width fonts
page = Page(size=(600, 400))
# Create test content
test_text = "The quick brown fox jumps over lazy dogs with varying character widths."
# Split into words and create multiple lines with different alignments
words = test_text.split()
# Create container for demonstration
demo_container = Container(
origin=(0, 0),
size=(580, 380),
direction='vertical',
spacing=5,
padding=(10, 10, 10, 10)
)
alignments = [
(Alignment.LEFT, "Left Aligned"),
(Alignment.CENTER, "Center Aligned"),
(Alignment.RIGHT, "Right Aligned"),
(Alignment.JUSTIFY, "Justified")
]
for align_enum, title in alignments:
# Add title
from pyWebLayout.style.fonts import FontWeight
title_text = Text(title + ":", Font(font_size=14, weight=FontWeight.BOLD))
demo_container.add_child(title_text)
# Create line with this alignment
line = Line(
spacing=(3, 8),
origin=(0, 0),
size=(560, 20),
font=self.regular_font,
halign=align_enum
)
# Add as many words as fit
for word in words[:6]: # Limit to first 6 words
result = line.add_word(word, self.regular_font)
if result is not None:
break
demo_container.add_child(line)
# Add demo to page
page.add_child(demo_container)
# Render and save
page_image = page.render()
output_dir = "test_output"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_path = os.path.join(output_dir, "monospace_concepts_demo.png")
page_image.save(output_path)
print(f"Visual demonstration saved to: {output_path}")
print("This shows why mono-space fonts make testing more predictable!")
# Validation
self.assertIsInstance(page_image, Image.Image)
self.assertEqual(page_image.size, (600, 400))
if __name__ == '__main__':
# Ensure output directory exists
if not os.path.exists("test_output"):
os.makedirs("test_output")
unittest.main(verbosity=2)