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