""" 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)