349 lines
13 KiB
Python
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)
|