This commit is contained in:
parent
718027f3c8
commit
fdb3023919
@ -366,7 +366,12 @@ class Line(Box):
|
|||||||
spacing_length = self._spacing[0] * (len(self._text_objects) - 1)
|
spacing_length = self._spacing[0] * (len(self._text_objects) - 1)
|
||||||
remaining=self._size[0] - word_length - spacing_length
|
remaining=self._size[0] - word_length - spacing_length
|
||||||
fraction = remaining / text.width
|
fraction = remaining / text.width
|
||||||
spliter = max(1, round(fraction*len(word.text))) # get the split index for best spacing, use original word length
|
# Calculate split position: fraction represents what portion of the hyphenated word fits
|
||||||
|
# We need to scale this to the original word length, accounting for the hyphen
|
||||||
|
hyphenated_length = len(word.text) + 1 # +1 for hyphen
|
||||||
|
split_in_hyphenated = round(fraction * hyphenated_length)
|
||||||
|
# Map back to original word, ensuring we don't go past the word length
|
||||||
|
spliter = min(len(word.text) - 1, max(1, split_in_hyphenated))
|
||||||
split = [Text(word.text[:spliter]+"-", word.style, self._draw, line=self, source=word), Text(word.text[spliter:], word.style, self._draw, line=self, source=word)]
|
split = [Text(word.text[:spliter]+"-", word.style, self._draw, line=self, source=word), Text(word.text[spliter:], word.style, self._draw, line=self, source=word)]
|
||||||
self._text_objects.append(split[0])
|
self._text_objects.append(split[0])
|
||||||
word.add_concete(split)
|
word.add_concete(split)
|
||||||
|
|||||||
@ -98,7 +98,7 @@ class ChapterNavigator:
|
|||||||
# Create position for this heading
|
# Create position for this heading
|
||||||
position = RenderingPosition(
|
position = RenderingPosition(
|
||||||
chapter_index=current_chapter_index,
|
chapter_index=current_chapter_index,
|
||||||
block_index=0, # Heading is first block in its chapter
|
block_index=block_index, # Use actual block index
|
||||||
word_index=0,
|
word_index=0,
|
||||||
table_row=0,
|
table_row=0,
|
||||||
table_col=0,
|
table_col=0,
|
||||||
@ -233,7 +233,11 @@ class BidirectionalLayouter:
|
|||||||
current_pos = position.copy()
|
current_pos = position.copy()
|
||||||
|
|
||||||
# Start laying out blocks from the current position
|
# Start laying out blocks from the current position
|
||||||
while current_pos.chapter_index < len(self.blocks) and page.free_space()[1] > 0:
|
while current_pos.block_index < len(self.blocks) and page.free_space()[1] > 0:
|
||||||
|
# Additional bounds check to prevent IndexError
|
||||||
|
if current_pos.block_index >= len(self.blocks):
|
||||||
|
break
|
||||||
|
|
||||||
block = self.blocks[current_pos.block_index]
|
block = self.blocks[current_pos.block_index]
|
||||||
|
|
||||||
# Apply font scaling to the block
|
# Apply font scaling to the block
|
||||||
@ -246,6 +250,12 @@ class BidirectionalLayouter:
|
|||||||
# Block doesn't fit, we're done with this page
|
# Block doesn't fit, we're done with this page
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Ensure new position doesn't go beyond bounds
|
||||||
|
if new_pos.block_index >= len(self.blocks):
|
||||||
|
# We've reached the end of the document
|
||||||
|
current_pos = new_pos
|
||||||
|
break
|
||||||
|
|
||||||
current_pos = new_pos
|
current_pos = new_pos
|
||||||
|
|
||||||
return page, current_pos
|
return page, current_pos
|
||||||
|
|||||||
@ -164,7 +164,8 @@ class EreaderLayoutManager:
|
|||||||
page_size: Tuple[int, int],
|
page_size: Tuple[int, int],
|
||||||
document_id: str = "default",
|
document_id: str = "default",
|
||||||
buffer_size: int = 5,
|
buffer_size: int = 5,
|
||||||
page_style: Optional[PageStyle] = None):
|
page_style: Optional[PageStyle] = None,
|
||||||
|
bookmarks_dir: str = "bookmarks"):
|
||||||
"""
|
"""
|
||||||
Initialize the ereader layout manager.
|
Initialize the ereader layout manager.
|
||||||
|
|
||||||
@ -174,6 +175,7 @@ class EreaderLayoutManager:
|
|||||||
document_id: Unique identifier for the document (for bookmarks/position)
|
document_id: Unique identifier for the document (for bookmarks/position)
|
||||||
buffer_size: Number of pages to cache in each direction
|
buffer_size: Number of pages to cache in each direction
|
||||||
page_style: Custom page styling (uses default if None)
|
page_style: Custom page styling (uses default if None)
|
||||||
|
bookmarks_dir: Directory to store bookmark files
|
||||||
"""
|
"""
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
self.page_size = page_size
|
self.page_size = page_size
|
||||||
@ -187,7 +189,7 @@ class EreaderLayoutManager:
|
|||||||
# Initialize core components
|
# Initialize core components
|
||||||
self.renderer = BufferedPageRenderer(blocks, page_style, buffer_size, page_size)
|
self.renderer = BufferedPageRenderer(blocks, page_style, buffer_size, page_size)
|
||||||
self.chapter_navigator = ChapterNavigator(blocks)
|
self.chapter_navigator = ChapterNavigator(blocks)
|
||||||
self.bookmark_manager = BookmarkManager(document_id)
|
self.bookmark_manager = BookmarkManager(document_id, bookmarks_dir)
|
||||||
|
|
||||||
# Current state
|
# Current state
|
||||||
self.current_position = RenderingPosition()
|
self.current_position = RenderingPosition()
|
||||||
|
|||||||
@ -13,17 +13,21 @@ from pyWebLayout.concrete.text import Text, Line
|
|||||||
from pyWebLayout.abstract.inline import Word
|
from pyWebLayout.abstract.inline import Word
|
||||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
||||||
from pyWebLayout.style import Alignment
|
from pyWebLayout.style import Alignment
|
||||||
|
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests
|
||||||
|
|
||||||
class TestText(unittest.TestCase):
|
class TestText(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
# Ensure consistent font usage across tests
|
||||||
|
ensure_consistent_font_in_tests()
|
||||||
|
|
||||||
# Create a real PIL image (canvas) for testing
|
# Create a real PIL image (canvas) for testing
|
||||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||||
|
|
||||||
# Create a real ImageDraw object
|
# Create a real ImageDraw object
|
||||||
self.draw = ImageDraw.Draw(self.canvas)
|
self.draw = ImageDraw.Draw(self.canvas)
|
||||||
|
|
||||||
# Create a real Font object
|
# Create a consistent test Font object using bundled font
|
||||||
self.style = Font()
|
self.style = create_default_test_font()
|
||||||
|
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
@ -114,14 +118,17 @@ class TestText(unittest.TestCase):
|
|||||||
|
|
||||||
class TestLine(unittest.TestCase):
|
class TestLine(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
# Ensure consistent font usage across tests
|
||||||
|
ensure_consistent_font_in_tests()
|
||||||
|
|
||||||
# Create a real PIL image (canvas) for testing
|
# Create a real PIL image (canvas) for testing
|
||||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||||
|
|
||||||
# Create a real ImageDraw object
|
# Create a real ImageDraw object
|
||||||
self.draw = ImageDraw.Draw(self.canvas)
|
self.draw = ImageDraw.Draw(self.canvas)
|
||||||
|
|
||||||
# Create a real Font object
|
# Create a consistent test Font object using bundled font
|
||||||
self.style = Font()
|
self.style = create_default_test_font()
|
||||||
|
|
||||||
def test_line_init(self):
|
def test_line_init(self):
|
||||||
"""Test Line initialization with real objects"""
|
"""Test Line initialization with real objects"""
|
||||||
@ -170,7 +177,7 @@ class TestLine(unittest.TestCase):
|
|||||||
self.assertEqual(line.text_objects[0].text, "Hello")
|
self.assertEqual(line.text_objects[0].text, "Hello")
|
||||||
|
|
||||||
def test_line_add_word_until_overflow(self):
|
def test_line_add_word_until_overflow(self):
|
||||||
"""Test adding a simple word to a line"""
|
"""Test adding a word until overflow occurs with consistent font measurements"""
|
||||||
spacing = (5, 15)
|
spacing = (5, 15)
|
||||||
origin = np.array([0, 0])
|
origin = np.array([0, 0])
|
||||||
size = np.array([400, 50])
|
size = np.array([400, 50])
|
||||||
@ -197,10 +204,10 @@ class TestLine(unittest.TestCase):
|
|||||||
self.assertEqual(overflow_part.text, "dam")
|
self.assertEqual(overflow_part.text, "dam")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.assertFalse(True)
|
self.fail("Expected overflow to occur but reached max iterations")
|
||||||
|
|
||||||
def test_line_add_word_until_overflow_small(self):
|
def test_line_add_word_until_overflow_small(self):
|
||||||
"""Test adding a simple word to a line"""
|
"""Test adding small words until line is full (no overflow expected)"""
|
||||||
spacing = (5, 15)
|
spacing = (5, 15)
|
||||||
origin = np.array([0, 0])
|
origin = np.array([0, 0])
|
||||||
size = np.array([400, 50])
|
size = np.array([400, 50])
|
||||||
@ -227,10 +234,10 @@ class TestLine(unittest.TestCase):
|
|||||||
self.assertIsNone(overflow_part)
|
self.assertIsNone(overflow_part)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.assertFalse(True)
|
self.fail("Expected line to reach capacity but reached max iterations")
|
||||||
|
|
||||||
def test_line_add_word_until_overflow_long_brute(self):
|
def test_line_add_word_until_overflow_long_brute(self):
|
||||||
"""Test adding a simple word to a line"""
|
"""Test adding a simple word to a line with consistent font measurements"""
|
||||||
spacing = (5, 15)
|
spacing = (5, 15)
|
||||||
origin = np.array([0, 0])
|
origin = np.array([0, 0])
|
||||||
size = np.array([400, 50])
|
size = np.array([400, 50])
|
||||||
@ -245,6 +252,8 @@ class TestLine(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create a word to add
|
# Create a word to add
|
||||||
|
# Note: Expected overflow result depends on the specific font measurements
|
||||||
|
# With DejaVuSans bundled font, this should consistently return "A" as overflow
|
||||||
|
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
word = Word(text="AAAAAAA", style=self.style)
|
word = Word(text="AAAAAAA", style=self.style)
|
||||||
@ -254,10 +263,11 @@ class TestLine(unittest.TestCase):
|
|||||||
success, overflow_part = line.add_word(word)
|
success, overflow_part = line.add_word(word)
|
||||||
# If successful, the word should be added
|
# If successful, the word should be added
|
||||||
if overflow_part:
|
if overflow_part:
|
||||||
self.assertEqual(overflow_part.text , "AA")
|
# Updated to match DejaVuSans font measurements for consistency
|
||||||
|
self.assertEqual(overflow_part.text, "A")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.assertFalse(True)
|
self.fail("Expected overflow to occur but reached max iterations")
|
||||||
|
|
||||||
|
|
||||||
def test_line_render(self):
|
def test_line_render(self):
|
||||||
|
|||||||
@ -307,7 +307,8 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
manager = EreaderLayoutManager(
|
manager = EreaderLayoutManager(
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.page_size,
|
self.page_size,
|
||||||
self.document_id
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(manager.page_size, self.page_size)
|
self.assertEqual(manager.page_size, self.page_size)
|
||||||
@ -329,7 +330,8 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
manager = EreaderLayoutManager(
|
manager = EreaderLayoutManager(
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.page_size,
|
self.page_size,
|
||||||
self.document_id
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test initial scale
|
# Test initial scale
|
||||||
@ -354,7 +356,8 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
manager = EreaderLayoutManager(
|
manager = EreaderLayoutManager(
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.page_size,
|
self.page_size,
|
||||||
self.document_id
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
toc = manager.get_table_of_contents()
|
toc = manager.get_table_of_contents()
|
||||||
@ -371,37 +374,31 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
|
|
||||||
def test_bookmark_functionality(self):
|
def test_bookmark_functionality(self):
|
||||||
"""Test bookmark functionality"""
|
"""Test bookmark functionality"""
|
||||||
original_cwd = Path.cwd()
|
manager = EreaderLayoutManager(
|
||||||
try:
|
self.blocks,
|
||||||
import os
|
self.page_size,
|
||||||
os.chdir(self.temp_dir)
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
manager = EreaderLayoutManager(
|
)
|
||||||
self.blocks,
|
|
||||||
self.page_size,
|
# Add bookmark
|
||||||
self.document_id
|
success = manager.add_bookmark("test_bookmark")
|
||||||
)
|
self.assertTrue(success)
|
||||||
|
|
||||||
# Add bookmark
|
# List bookmarks
|
||||||
success = manager.add_bookmark("test_bookmark")
|
bookmarks = manager.list_bookmarks()
|
||||||
self.assertTrue(success)
|
self.assertEqual(len(bookmarks), 1)
|
||||||
|
self.assertEqual(bookmarks[0][0], "test_bookmark")
|
||||||
# List bookmarks
|
|
||||||
bookmarks = manager.list_bookmarks()
|
# Jump to bookmark (should work even though it's the same position)
|
||||||
self.assertEqual(len(bookmarks), 1)
|
page = manager.jump_to_bookmark("test_bookmark")
|
||||||
self.assertEqual(bookmarks[0][0], "test_bookmark")
|
self.assertIsNotNone(page)
|
||||||
|
|
||||||
# Jump to bookmark (should work even though it's the same position)
|
# Remove bookmark
|
||||||
page = manager.jump_to_bookmark("test_bookmark")
|
success = manager.remove_bookmark("test_bookmark")
|
||||||
self.assertIsNotNone(page)
|
self.assertTrue(success)
|
||||||
|
|
||||||
# Remove bookmark
|
manager.shutdown()
|
||||||
success = manager.remove_bookmark("test_bookmark")
|
|
||||||
self.assertTrue(success)
|
|
||||||
|
|
||||||
manager.shutdown()
|
|
||||||
finally:
|
|
||||||
os.chdir(original_cwd)
|
|
||||||
|
|
||||||
def test_progress_tracking(self):
|
def test_progress_tracking(self):
|
||||||
"""Test reading progress tracking"""
|
"""Test reading progress tracking"""
|
||||||
@ -413,7 +410,8 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
manager = EreaderLayoutManager(
|
manager = EreaderLayoutManager(
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.page_size,
|
self.page_size,
|
||||||
self.document_id
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initial progress should be 0
|
# Initial progress should be 0
|
||||||
@ -441,7 +439,8 @@ class TestEreaderLayoutManager(unittest.TestCase):
|
|||||||
manager = create_ereader_manager(
|
manager = create_ereader_manager(
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.page_size,
|
self.page_size,
|
||||||
self.document_id
|
self.document_id,
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsInstance(manager, EreaderLayoutManager)
|
self.assertIsInstance(manager, EreaderLayoutManager)
|
||||||
|
|||||||
@ -127,18 +127,26 @@ class TestDocumentLayouter:
|
|||||||
|
|
||||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||||
@patch('pyWebLayout.layout.document_layouter.Line')
|
@patch('pyWebLayout.layout.document_layouter.Line')
|
||||||
def test_paragraph_layouter_line_overflow(self, mock_line_class, mock_style_registry_class):
|
@patch('pyWebLayout.layout.document_layouter.Text')
|
||||||
|
def test_paragraph_layouter_line_overflow(self, mock_text_class, mock_line_class, mock_style_registry_class):
|
||||||
"""Test handling of line overflow when words don't fit."""
|
"""Test handling of line overflow when words don't fit."""
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
mock_style_registry = Mock()
|
mock_style_registry = Mock()
|
||||||
mock_style_registry_class.return_value = mock_style_registry
|
mock_style_registry_class.return_value = mock_style_registry
|
||||||
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
||||||
|
|
||||||
# Create two mock lines
|
# Create two mock lines with proper size attribute
|
||||||
mock_line1 = Mock()
|
mock_line1 = Mock()
|
||||||
|
mock_line1.size = (400, 20) # (width, height)
|
||||||
mock_line2 = Mock()
|
mock_line2 = Mock()
|
||||||
|
mock_line2.size = (400, 20) # (width, height)
|
||||||
mock_line_class.side_effect = [mock_line1, mock_line2]
|
mock_line_class.side_effect = [mock_line1, mock_line2]
|
||||||
|
|
||||||
|
# Mock Text.from_word to return mock text objects with numeric width
|
||||||
|
mock_text = Mock()
|
||||||
|
mock_text.width = 50 # Reasonable word width
|
||||||
|
mock_text_class.from_word.return_value = mock_text
|
||||||
|
|
||||||
# First line: first 2 words fit, third doesn't
|
# First line: first 2 words fit, third doesn't
|
||||||
# Second line: remaining words fit
|
# Second line: remaining words fit
|
||||||
mock_line1.add_word.side_effect = [
|
mock_line1.add_word.side_effect = [
|
||||||
|
|||||||
@ -48,6 +48,7 @@ class TestStyleObjects(unittest.TestCase):
|
|||||||
self.assertEqual(Alignment.TOP.value, "top")
|
self.assertEqual(Alignment.TOP.value, "top")
|
||||||
self.assertEqual(Alignment.BOTTOM.value, "bottom")
|
self.assertEqual(Alignment.BOTTOM.value, "bottom")
|
||||||
self.assertEqual(Alignment.JUSTIFY.value, "justify")
|
self.assertEqual(Alignment.JUSTIFY.value, "justify")
|
||||||
|
self.assertEqual(Alignment.MIDDLE.value, "middle")
|
||||||
|
|
||||||
def test_font_initialization_defaults(self):
|
def test_font_initialization_defaults(self):
|
||||||
"""Test Font initialization with default values."""
|
"""Test Font initialization with default values."""
|
||||||
|
|||||||
4
tests/utils/__init__.py
Normal file
4
tests/utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Test utilities package for pyWebLayout.
|
||||||
|
Contains helper functions and utilities for testing.
|
||||||
|
"""
|
||||||
132
tests/utils/test_font_utilities.py
Normal file
132
tests/utils/test_font_utilities.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
Tests for the test font utilities module.
|
||||||
|
|
||||||
|
These tests verify that the bundled font system works correctly for consistent testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
from tests.utils.test_fonts import (
|
||||||
|
get_bundled_font_path,
|
||||||
|
verify_bundled_font_available,
|
||||||
|
create_test_font,
|
||||||
|
create_default_test_font,
|
||||||
|
ensure_consistent_font_in_tests
|
||||||
|
)
|
||||||
|
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
|
|
||||||
|
class TestFontUtilities(unittest.TestCase):
|
||||||
|
"""Test cases for font utility functions."""
|
||||||
|
|
||||||
|
def test_get_bundled_font_path_finds_font(self):
|
||||||
|
"""Test that get_bundled_font_path finds the bundled font."""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
self.assertIsNotNone(font_path, "Bundled font path should not be None")
|
||||||
|
self.assertTrue(os.path.exists(font_path), f"Font file should exist at {font_path}")
|
||||||
|
self.assertTrue(font_path.endswith("DejaVuSans.ttf"), "Font path should end with DejaVuSans.ttf")
|
||||||
|
|
||||||
|
def test_verify_bundled_font_available(self):
|
||||||
|
"""Test that the bundled font can be verified and loaded."""
|
||||||
|
self.assertTrue(verify_bundled_font_available(), "Bundled font should be available and loadable")
|
||||||
|
|
||||||
|
def test_create_test_font_with_defaults(self):
|
||||||
|
"""Test creating a test font with default parameters."""
|
||||||
|
font = create_test_font()
|
||||||
|
self.assertIsInstance(font, Font)
|
||||||
|
self.assertEqual(font.font_size, 16)
|
||||||
|
self.assertEqual(font.colour, (0, 0, 0))
|
||||||
|
self.assertEqual(font.weight, FontWeight.NORMAL)
|
||||||
|
self.assertEqual(font.style, FontStyle.NORMAL)
|
||||||
|
self.assertEqual(font.decoration, TextDecoration.NONE)
|
||||||
|
|
||||||
|
def test_create_test_font_with_custom_parameters(self):
|
||||||
|
"""Test creating a test font with custom parameters."""
|
||||||
|
font = create_test_font(
|
||||||
|
font_size=24,
|
||||||
|
colour=(255, 0, 0),
|
||||||
|
weight=FontWeight.BOLD,
|
||||||
|
style=FontStyle.ITALIC,
|
||||||
|
decoration=TextDecoration.UNDERLINE
|
||||||
|
)
|
||||||
|
self.assertIsInstance(font, Font)
|
||||||
|
self.assertEqual(font.font_size, 24)
|
||||||
|
self.assertEqual(font.colour, (255, 0, 0))
|
||||||
|
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||||
|
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||||
|
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||||
|
|
||||||
|
def test_create_default_test_font(self):
|
||||||
|
"""Test creating a default test font."""
|
||||||
|
font = create_default_test_font()
|
||||||
|
self.assertIsInstance(font, Font)
|
||||||
|
self.assertEqual(font.font_size, 16)
|
||||||
|
self.assertEqual(font.colour, (0, 0, 0))
|
||||||
|
|
||||||
|
def test_ensure_consistent_font_in_tests_succeeds(self):
|
||||||
|
"""Test that ensure_consistent_font_in_tests runs without error when font is available."""
|
||||||
|
# This should not raise any exceptions if the font is properly available
|
||||||
|
try:
|
||||||
|
ensure_consistent_font_in_tests()
|
||||||
|
except RuntimeError:
|
||||||
|
self.fail("ensure_consistent_font_in_tests() raised RuntimeError when font should be available")
|
||||||
|
|
||||||
|
def test_bundled_font_loads_with_pil(self):
|
||||||
|
"""Test that the bundled font can be loaded directly with PIL."""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
self.assertIsNotNone(font_path)
|
||||||
|
|
||||||
|
# Test loading with different sizes
|
||||||
|
for size in [12, 16, 24, 48]:
|
||||||
|
with self.subTest(size=size):
|
||||||
|
pil_font = ImageFont.truetype(font_path, size)
|
||||||
|
self.assertIsNotNone(pil_font)
|
||||||
|
|
||||||
|
def test_font_metrics_consistency(self):
|
||||||
|
"""Test that font metrics are consistent between different Font objects using the same parameters."""
|
||||||
|
font1 = create_test_font(font_size=16)
|
||||||
|
font2 = create_test_font(font_size=16)
|
||||||
|
|
||||||
|
# Both fonts should have the same size
|
||||||
|
self.assertEqual(font1.font_size, font2.font_size)
|
||||||
|
|
||||||
|
# Test that text measurements are consistent
|
||||||
|
# This is a basic check - in real usage, text measurement consistency is what matters most
|
||||||
|
self.assertEqual(font1.font_size, font2.font_size)
|
||||||
|
|
||||||
|
def test_different_sizes_create_different_fonts(self):
|
||||||
|
"""Test that different font sizes create fonts with different metrics."""
|
||||||
|
small_font = create_test_font(font_size=12)
|
||||||
|
large_font = create_test_font(font_size=24)
|
||||||
|
|
||||||
|
self.assertNotEqual(small_font.font_size, large_font.font_size)
|
||||||
|
self.assertEqual(small_font.font_size, 12)
|
||||||
|
self.assertEqual(large_font.font_size, 24)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFontPathResolution(unittest.TestCase):
|
||||||
|
"""Test cases for font path resolution from different locations."""
|
||||||
|
|
||||||
|
def test_font_path_is_absolute(self):
|
||||||
|
"""Test that the returned font path is absolute."""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if font_path:
|
||||||
|
self.assertTrue(os.path.isabs(font_path), "Font path should be absolute")
|
||||||
|
|
||||||
|
def test_font_path_points_to_file(self):
|
||||||
|
"""Test that the font path points to a file, not a directory."""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if font_path:
|
||||||
|
self.assertTrue(os.path.isfile(font_path), "Font path should point to a file")
|
||||||
|
|
||||||
|
def test_font_file_has_correct_extension(self):
|
||||||
|
"""Test that the font file has the expected .ttf extension."""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if font_path:
|
||||||
|
self.assertTrue(font_path.lower().endswith('.ttf'), "Font file should have .ttf extension")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
164
tests/utils/test_fonts.py
Normal file
164
tests/utils/test_fonts.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
Test font utilities for ensuring consistent font usage across tests.
|
||||||
|
|
||||||
|
This module provides utilities to guarantee that all tests use the same bundled font,
|
||||||
|
preventing inconsistencies that can arise from different system fonts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
|
|
||||||
|
def get_bundled_font_path() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the path to the bundled DejaVuSans.ttf font.
|
||||||
|
|
||||||
|
This function works from test directories by finding the font relative to the
|
||||||
|
test file locations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Path to the bundled font file, or None if not found
|
||||||
|
"""
|
||||||
|
# Get the directory containing this test utility file
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Navigate up to the project root (tests/utils -> tests -> root)
|
||||||
|
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||||
|
|
||||||
|
# Path to the bundled font
|
||||||
|
bundled_font_path = os.path.join(project_root, 'pyWebLayout', 'assets', 'fonts', 'DejaVuSans.ttf')
|
||||||
|
|
||||||
|
if os.path.exists(bundled_font_path):
|
||||||
|
return bundled_font_path
|
||||||
|
|
||||||
|
# Alternative: try to find it relative to the pyWebLayout module
|
||||||
|
try:
|
||||||
|
import pyWebLayout
|
||||||
|
module_dir = os.path.dirname(pyWebLayout.__file__)
|
||||||
|
alt_font_path = os.path.join(module_dir, 'assets', 'fonts', 'DejaVuSans.ttf')
|
||||||
|
if os.path.exists(alt_font_path):
|
||||||
|
return alt_font_path
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_bundled_font_available() -> bool:
|
||||||
|
"""
|
||||||
|
Verify that the bundled font is available and can be loaded.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the bundled font is available and loadable
|
||||||
|
"""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if not font_path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to load the font with PIL to verify it's valid
|
||||||
|
test_font = ImageFont.truetype(font_path, 16)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_font(font_size: int = 16,
|
||||||
|
colour: tuple = (0, 0, 0),
|
||||||
|
weight: FontWeight = FontWeight.NORMAL,
|
||||||
|
style: FontStyle = FontStyle.NORMAL,
|
||||||
|
decoration: TextDecoration = TextDecoration.NONE,
|
||||||
|
background: Optional[tuple] = None,
|
||||||
|
language: str = "en_EN",
|
||||||
|
min_hyphenation_width: Optional[int] = None) -> Font:
|
||||||
|
"""
|
||||||
|
Create a Font object that uses the bundled font for consistent testing.
|
||||||
|
|
||||||
|
This function ensures all tests use the same font file, preventing
|
||||||
|
cross-system inconsistencies in text measurements and layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_size: Size of the font in points
|
||||||
|
colour: RGB color tuple for the text
|
||||||
|
weight: Font weight (normal or bold)
|
||||||
|
style: Font style (normal or italic)
|
||||||
|
decoration: Text decoration (none, underline, or strikethrough)
|
||||||
|
background: RGBA background color for the text
|
||||||
|
language: Language code for hyphenation and text processing
|
||||||
|
min_hyphenation_width: Minimum width in pixels for hyphenation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Font: A Font object guaranteed to use the bundled font
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the bundled font cannot be found or loaded
|
||||||
|
"""
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if not font_path:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Bundled font (DejaVuSans.ttf) not found. "
|
||||||
|
"Ensure the font exists in pyWebLayout/assets/fonts/"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not verify_bundled_font_available():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Bundled font at {font_path} cannot be loaded. "
|
||||||
|
"Font file may be corrupted or invalid."
|
||||||
|
)
|
||||||
|
|
||||||
|
return Font(
|
||||||
|
font_path=font_path,
|
||||||
|
font_size=font_size,
|
||||||
|
colour=colour,
|
||||||
|
weight=weight,
|
||||||
|
style=style,
|
||||||
|
decoration=decoration,
|
||||||
|
background=background,
|
||||||
|
language=language,
|
||||||
|
min_hyphenation_width=min_hyphenation_width
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_test_font() -> Font:
|
||||||
|
"""
|
||||||
|
Create a default Font object for testing with the bundled font.
|
||||||
|
|
||||||
|
This is equivalent to Font() but guarantees the bundled font is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Font: A default Font object using the bundled font
|
||||||
|
"""
|
||||||
|
return create_test_font()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_consistent_font_in_tests():
|
||||||
|
"""
|
||||||
|
Ensure that tests are using consistent fonts by checking availability.
|
||||||
|
|
||||||
|
This function can be called in test setup to verify the font environment
|
||||||
|
is properly configured.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the bundled font is not available
|
||||||
|
"""
|
||||||
|
if not verify_bundled_font_available():
|
||||||
|
font_path = get_bundled_font_path()
|
||||||
|
if font_path:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Bundled font found at {font_path} but cannot be loaded. "
|
||||||
|
"Font file may be corrupted."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Bundled font (DejaVuSans.ttf) not found. "
|
||||||
|
"Ensure the font exists in pyWebLayout/assets/fonts/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience aliases
|
||||||
|
get_test_font = create_default_test_font
|
||||||
|
font_factory = create_test_font
|
||||||
Loading…
x
Reference in New Issue
Block a user