diff --git a/tests/test_concrete_page.py b/tests/test_concrete_page.py index e889307..e4e56b6 100644 --- a/tests/test_concrete_page.py +++ b/tests/test_concrete_page.py @@ -455,5 +455,302 @@ class TestPage(unittest.TestCase): self.assertEqual(large_result.size, (1920, 1080)) +class TestPageBorderMarginRendering(unittest.TestCase): + """Test cases specifically for border/margin consistency in Page rendering""" + + def setUp(self): + """Set up test fixtures for border/margin tests""" + self.page_size = (400, 300) + self.padding = (20, 15, 25, 10) # top, right, bottom, left + self.background_color = (240, 240, 240) + + def test_border_consistency_with_text_content(self): + """Test that borders/margins are consistent when rendering text content""" + from pyWebLayout.concrete.text import Text + from pyWebLayout.style.fonts import Font + + # Create page with specific padding + page = Page(size=self.page_size, background_color=self.background_color) + page._padding = self.padding + + # Add text content - let Text objects calculate their own dimensions + font = Font(font_size=14) + text1 = Text("First line of text", font) + text2 = Text("Second line of text", font) + + page.add_child(text1) + page.add_child(text2) + + # Render the page + result = page.render() + + # Extract border areas and verify consistency + border_measurements = self._extract_border_measurements(result, self.padding) + self._verify_border_consistency(border_measurements, self.padding) + + # Verify content area is correctly positioned + content_area = self._extract_content_area(result, self.padding) + self.assertIsNotNone(content_area) + + # Ensure content doesn't bleed into border areas + self._verify_no_content_in_borders(result, self.padding, self.background_color) + + def test_border_consistency_with_paragraph_content(self): + """Test borders/margins with paragraph content that may wrap""" + from pyWebLayout.abstract.block import Paragraph + from pyWebLayout.abstract.inline import Word + from pyWebLayout.style.fonts import Font + + # Create a mock paragraph with multiple words + paragraph = Paragraph() + font = Font(font_size=12) + + # Add words to create a longer paragraph + words_text = ["This", "is", "a", "longer", "paragraph", "that", "should", "wrap", "across", "multiple", "lines", "to", "test", "margin", "consistency"] + for word_text in words_text: + word = Word(word_text, font) + paragraph.add_word(word) + + # Create page with specific padding + page = Page(size=self.page_size, background_color=self.background_color) + page._padding = self.padding + + # Render paragraph on page + page.render_blocks([paragraph]) + result = page.render() + + # Extract and verify border measurements + border_measurements = self._extract_border_measurements(result, self.padding) + self._verify_border_consistency(border_measurements, self.padding) + + # Verify content positioning + self._verify_content_within_bounds(result, self.padding) + + def test_border_consistency_with_mixed_content(self): + """Test borders/margins with mixed content types""" + from pyWebLayout.concrete.text import Text + from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel + from pyWebLayout.abstract.inline import Word + from pyWebLayout.style.fonts import Font, FontWeight + + # Create page with asymmetric padding to test edge cases + asymmetric_padding = (30, 20, 15, 25) + page = Page(size=(500, 400), background_color=self.background_color) + page._padding = asymmetric_padding + + # Create mixed content + heading = Heading(HeadingLevel.H2) + heading_font = Font(font_size=18, weight=FontWeight.BOLD) + heading.add_word(Word("Test Heading", heading_font)) + + paragraph = Paragraph() + para_font = Font(font_size=12) + para_words = ["This", "paragraph", "follows", "the", "heading", "and", "tests", "mixed", "content", "rendering"] + for word_text in para_words: + paragraph.add_word(Word(word_text, para_font)) + + # Render mixed content + page.render_blocks([heading, paragraph]) + result = page.render() + + # Verify border consistency with asymmetric padding + border_measurements = self._extract_border_measurements(result, asymmetric_padding) + self._verify_border_consistency(border_measurements, asymmetric_padding) + + # Verify no content bleeds into margins + self._verify_no_content_in_borders(result, asymmetric_padding, self.background_color) + + def test_border_consistency_with_different_padding_values(self): + """Test that different padding values maintain consistent borders""" + from pyWebLayout.concrete.text import Text + from pyWebLayout.style.fonts import Font + + padding_configs = [ + (10, 10, 10, 10), # uniform + (5, 15, 5, 15), # symmetric horizontal/vertical + (20, 30, 10, 5), # asymmetric + (0, 5, 0, 5), # minimal top/bottom + ] + + font = Font(font_size=14) + + for padding in padding_configs: + with self.subTest(padding=padding): + # Create a fresh text object for each test to avoid state issues + test_text = Text("Border consistency test", font) + page = Page(size=self.page_size, background_color=self.background_color) + page._padding = padding + page.add_child(test_text) + + result = page.render() + + # Verify border measurements match expected padding + border_measurements = self._extract_border_measurements(result, padding) + self._verify_border_consistency(border_measurements, padding) + + # Verify content area calculation + expected_content_width = self.page_size[0] - padding[1] - padding[3] # width - right - left + expected_content_height = self.page_size[1] - padding[0] - padding[2] # height - top - bottom + + content_area = self._extract_content_area(result, padding) + self.assertEqual(content_area['width'], expected_content_width) + self.assertEqual(content_area['height'], expected_content_height) + + def test_border_uniformity_across_renders(self): + """Test that border areas remain uniform across multiple renders""" + from pyWebLayout.concrete.text import Text + from pyWebLayout.style.fonts import Font + + page = Page(size=self.page_size, background_color=self.background_color) + page._padding = self.padding + + font = Font(font_size=12) + text = Text("Consistency test content", font) + page.add_child(text) + + # Render multiple times + results = [] + for i in range(3): + result = page.render() + results.append(result) + border_measurements = self._extract_border_measurements(result, self.padding) + + # Store first measurement as baseline + if i == 0: + baseline_measurements = border_measurements + else: + # Compare with baseline + self._compare_border_measurements(baseline_measurements, border_measurements) + + def _extract_border_measurements(self, image, padding): + """Extract measurements of border/margin areas from rendered image""" + width, height = image.size + top_pad, right_pad, bottom_pad, left_pad = padding + + measurements = { + 'top_border': { + 'area': (0, 0, width, top_pad), + 'pixels': self._get_area_pixels(image, (0, 0, width, top_pad)) + }, + 'right_border': { + 'area': (width - right_pad, 0, width, height), + 'pixels': self._get_area_pixels(image, (width - right_pad, 0, width, height)) + }, + 'bottom_border': { + 'area': (0, height - bottom_pad, width, height), + 'pixels': self._get_area_pixels(image, (0, height - bottom_pad, width, height)) + }, + 'left_border': { + 'area': (0, 0, left_pad, height), + 'pixels': self._get_area_pixels(image, (0, 0, left_pad, height)) + } + } + + return measurements + + def _get_area_pixels(self, image, area): + """Extract pixel data from a specific area of the image""" + if area[2] <= area[0] or area[3] <= area[1]: + return [] # Invalid area + + cropped = image.crop(area) + return list(cropped.getdata()) + + def _verify_border_consistency(self, measurements, expected_padding): + """Verify that border measurements match expected padding values""" + # Get actual dimensions from the measurements instead of using self.page_size + # This allows the test to work with different page sizes + top_area = measurements['top_border']['area'] + width = top_area[2] # right coordinate of top border gives us the width + height = measurements['left_border']['area'][3] # bottom coordinate of left border gives us the height + + top_pad, right_pad, bottom_pad, left_pad = expected_padding + + # Check area dimensions + self.assertEqual(top_area, (0, 0, width, top_pad)) + + right_area = measurements['right_border']['area'] + self.assertEqual(right_area, (width - right_pad, 0, width, height)) + + bottom_area = measurements['bottom_border']['area'] + self.assertEqual(bottom_area, (0, height - bottom_pad, width, height)) + + left_area = measurements['left_border']['area'] + self.assertEqual(left_area, (0, 0, left_pad, height)) + + def _extract_content_area(self, image, padding): + """Extract the content area (area inside borders/margins)""" + width, height = image.size + top_pad, right_pad, bottom_pad, left_pad = padding + + content_area = { + 'left': left_pad, + 'top': top_pad, + 'right': width - right_pad, + 'bottom': height - bottom_pad, + 'width': width - left_pad - right_pad, + 'height': height - top_pad - bottom_pad + } + + return content_area + + def _verify_no_content_in_borders(self, image, padding, background_color): + """Verify that no content bleeds into the border/margin areas""" + measurements = self._extract_border_measurements(image, padding) + + # Check that border areas contain only background color + for border_name, border_data in measurements.items(): + pixels = border_data['pixels'] + if pixels: # Only check if area is not empty + # Most pixels should be background color (allowing for some anti-aliasing) + bg_count = sum(1 for pixel in pixels if self._is_background_color(pixel, background_color)) + total_pixels = len(pixels) + + # Allow up to 10% deviation for anti-aliasing effects + bg_ratio = bg_count / total_pixels if total_pixels > 0 else 1.0 + self.assertGreaterEqual(bg_ratio, 0.9, + f"Border area '{border_name}' contains too much non-background content. " + f"Background ratio: {bg_ratio:.2f}") + + def _is_background_color(self, pixel, background_color, tolerance=10): + """Check if a pixel is close to the background color within tolerance""" + if len(pixel) >= 3: + r_diff = abs(pixel[0] - background_color[0]) + g_diff = abs(pixel[1] - background_color[1]) + b_diff = abs(pixel[2] - background_color[2]) + return r_diff <= tolerance and g_diff <= tolerance and b_diff <= tolerance + return False + + def _verify_content_within_bounds(self, image, padding): + """Verify that content is positioned within the expected bounds""" + content_area = self._extract_content_area(image, padding) + + # Sample the content area to ensure it's not all background + if content_area['width'] > 0 and content_area['height'] > 0: + content_crop = image.crop(( + content_area['left'], + content_area['top'], + content_area['right'], + content_area['bottom'] + )) + + # Content area should have some non-background pixels + content_pixels = list(content_crop.getdata()) + non_bg_pixels = sum(1 for pixel in content_pixels + if not self._is_background_color(pixel, self.background_color)) + + # Expect at least some content in the content area + self.assertGreater(non_bg_pixels, 0, "Content area appears to be empty") + + def _compare_border_measurements(self, baseline, current): + """Compare two sets of border measurements for consistency""" + for border_name in baseline.keys(): + baseline_area = baseline[border_name]['area'] + current_area = current[border_name]['area'] + + self.assertEqual(baseline_area, current_area, + f"Border area '{border_name}' is inconsistent between renders") + + if __name__ == '__main__': unittest.main()