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

349 lines
13 KiB
Python

"""
Mono-space font hyphenation tests.
Tests hyphenation behavior with mono-space fonts where character widths
are predictable, making it easier to verify hyphenation logic and
line-breaking decisions.
"""
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
from pyWebLayout.abstract.inline import Word
class TestMonospaceHyphenation(unittest.TestCase):
"""Test hyphenation behavior with mono-space fonts."""
def setUp(self):
"""Set up test with mono-space font."""
# Try to find a mono-space font
mono_paths = [
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf" ,
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.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=14,
min_hyphenation_width=20
)
# Calculate character width
ref_char = Text("M", self.font)
self.char_width = ref_char.width
print(f"Using mono-space font: {os.path.basename(self.mono_font_path)}")
print(f"Character width: {self.char_width}px")
else:
print("No mono-space font found - hyphenation tests will be skipped")
def test_hyphenation_basic_functionality(self):
"""Test basic hyphenation with known words."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Test words that should hyphenate
test_words = [
"hyphenation",
"development",
"information",
"character",
"beautiful",
"computer"
]
for word_text in test_words:
word = Word(word_text, self.font)
if word.hyphenate():
parts_count = word.get_hyphenated_part_count()
print(f"\nWord: '{word_text}' -> {parts_count} parts")
# Collect all parts
parts = []
for i in range(parts_count):
part = word.get_hyphenated_part(i)
parts.append(part)
print(f" Part {i}: '{part}' ({len(part)} chars)")
# Verify that parts reconstruct the original word
reconstructed = ''.join(parts).replace('-', '')
self.assertEqual(reconstructed, word_text,
f"Hyphenated parts should reconstruct '{word_text}'")
# Test that each part has predictable width
for i, part in enumerate(parts):
text_obj = Text(part, self.font)
expected_width = len(part) * self.char_width
actual_width = text_obj.width
# Allow small variance for hyphen rendering
diff = abs(actual_width - expected_width)
max_diff = 5 # pixels tolerance for hyphen
self.assertLessEqual(diff, max_diff,
f"Part '{part}' width should be predictable")
else:
print(f"Word '{word_text}' cannot be hyphenated")
def test_hyphenation_line_fitting(self):
"""Test that hyphenation helps words fit on lines."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Create a line that's too narrow for long words
narrow_width = self.char_width * 12 # 12 characters
line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(narrow_width, 20),
font=self.font,
halign=Alignment.LEFT
)
# Test with a word that needs hyphenation
long_word = "hyphenation" # 11 characters - should barely fit or need hyphenation
result = line.add_word(long_word, self.font)
print(f"\nTesting word '{long_word}' in {narrow_width}px line:")
print(f"Line capacity: ~{narrow_width // self.char_width} characters")
if result is None:
# Word fit completely
print("Word fit completely on line")
self.assertGreater(len(line.text_objects), 0, "Line should have text")
added_text = line.text_objects[0].text
print(f"Added text: '{added_text}'")
else:
# Word was hyphenated or rejected
print(f"Word result: '{result}'")
if len(line.text_objects) > 0:
added_text = line.text_objects[0].text
print(f"Added to line: '{added_text}' ({len(added_text)} chars)")
print(f"Remaining: '{result}' ({len(result)} chars)")
# Added part should be shorter than original
self.assertLess(len(added_text), len(long_word),
"Hyphenated part should be shorter than original")
# Remaining part should be shorter than original
self.assertLess(len(result), len(long_word),
"Remaining part should be shorter than original")
else:
print("No text was added to line")
def test_hyphenation_vs_no_hyphenation(self):
"""Compare behavior with and without hyphenation enabled."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Create fonts with and without hyphenation
font_with_hyphen = Font(
font_path=self.mono_font_path,
font_size=14,
min_hyphenation_width=20
)
font_no_hyphen = Font(
font_path=self.mono_font_path,
font_size=14,
)
# Test with a word that benefits from hyphenation
test_word = "development" # 11 characters
line_width = self.char_width * 8 # 8 characters - too narrow
# Test with hyphenation enabled
line_with_hyphen = Line(
spacing=(2, 4),
origin=(0, 0),
size=(line_width, 20),
font=font_with_hyphen,
halign=Alignment.LEFT
)
result_with_hyphen = line_with_hyphen.add_word(test_word, font_with_hyphen)
# Test without hyphenation
line_no_hyphen = Line(
spacing=(2, 4),
origin=(0, 0),
size=(line_width, 20),
font=font_no_hyphen,
halign=Alignment.LEFT
)
result_no_hyphen = line_no_hyphen.add_word(test_word, font_no_hyphen)
print(f"\nTesting '{test_word}' in {line_width}px line:")
print(f"With hyphenation: {result_with_hyphen}")
print(f"Without hyphenation: {result_no_hyphen}")
# With hyphenation, we might get partial content
# Without hyphenation, word should be rejected entirely
if result_with_hyphen is None:
print("Word fit completely with hyphenation")
elif len(line_with_hyphen.text_objects) > 0:
added_with_hyphen = line_with_hyphen.text_objects[0].text
print(f"Added with hyphenation: '{added_with_hyphen}'")
if result_no_hyphen is None:
print("Word fit completely without hyphenation")
elif len(line_no_hyphen.text_objects) > 0:
added_no_hyphen = line_no_hyphen.text_objects[0].text
print(f"Added without hyphenation: '{added_no_hyphen}'")
def test_hyphenation_quality_metrics(self):
"""Test hyphenation quality with different line widths."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
test_word = "information" # 11 characters
# Test with different line widths
test_widths = [
self.char_width * 6, # Very narrow
self.char_width * 8, # Narrow
self.char_width * 10, # Medium
self.char_width * 12, # Wide enough
]
print(f"\nTesting hyphenation quality for '{test_word}':")
for width in test_widths:
capacity = width // self.char_width
line = Line(
spacing=(2, 4),
origin=(0, 0),
size=(width, 20),
font=self.font,
halign=Alignment.LEFT
)
result = line.add_word(test_word, self.font)
print(f"\nLine width: {width}px (~{capacity} chars)")
if result is None:
print(" Word fit completely")
if line.text_objects:
added = line.text_objects[0].text
print(f" Added: '{added}'")
else:
print(f" Result: '{result}'")
if line.text_objects:
added = line.text_objects[0].text
print(f" Added: '{added}' ({len(added)} chars)")
print(f" Remaining: '{result}' ({len(result)} chars)")
# Calculate hyphenation efficiency
chars_used = len(added) - added.count('-') # Don't count hyphens
efficiency = chars_used / len(test_word)
print(f" Efficiency: {efficiency:.2%}")
def test_multiple_words_with_hyphenation(self):
"""Test adding multiple words where hyphenation affects spacing."""
if not self.mono_font_path:
self.skipTest("No mono-space font available")
# Create a line that forces interesting hyphenation decisions
line_width = self.char_width * 20 # 20 characters
line = Line(
spacing=(3, 6),
origin=(0, 0),
size=(line_width, 20),
font=self.font,
halign=Alignment.JUSTIFY
)
# Test words that might need hyphenation
test_words = ["The", "development", "of", "hyphenation"]
print(f"\nAdding words to {line_width}px line (~{line_width // self.char_width} chars):")
words_added = []
for word in test_words:
result = line.add_word(word, self.font)
if result is None:
print(f" '{word}' - fit completely")
words_added.append(word)
else:
print(f" '{word}' - result: '{result}'")
if line.text_objects:
last_added = line.text_objects[-1].text
print(f" Added: '{last_added}'")
words_added.append(last_added)
break
print(f"Final line contains {len(line.text_objects)} text objects")
# Render the line to test spacing
line_image = line.render()
# 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, "mono_hyphenation_multiword.png")
line_image.save(output_path)
print(f"Saved multi-word hyphenation test to: {output_path}")
# Basic validation
self.assertIsInstance(line_image, Image.Image)
self.assertEqual(line_image.size, (line_width, 20))
def save_hyphenation_example(self, test_name: str, lines: list):
"""Save a visual example of hyphenation behavior."""
from pyWebLayout.concrete.page import Container
# Create a container for multiple lines
container = Container(
origin=(0, 0),
size=(400, len(lines) * 25),
direction='vertical',
spacing=5,
padding=(10, 10, 10, 10)
)
# Add each line to the container
for i, line in enumerate(lines):
line._origin = (0, i * 25)
container.add_child(line)
# Render the container
container_image = container.render()
# Save the image
output_dir = "test_output"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_path = os.path.join(output_dir, f"mono_hyphen_{test_name}.png")
container_image.save(output_path)
print(f"Saved hyphenation example '{test_name}' to: {output_path}")
if __name__ == '__main__':
unittest.main(verbosity=2)