From cf27d9ebeefb5f2d5499a1e3a20acb878adfa2ca Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Wed, 31 Dec 2025 13:19:36 +0100 Subject: [PATCH] fixed bug in font rendering when zooming --- pyPhotoAlbum/mixins/rendering.py | 36 +++-- tests/test_rendering_mixin.py | 229 +++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 15 deletions(-) diff --git a/pyPhotoAlbum/mixins/rendering.py b/pyPhotoAlbum/mixins/rendering.py index b131ee7..b6de704 100644 --- a/pyPhotoAlbum/mixins/rendering.py +++ b/pyPhotoAlbum/mixins/rendering.py @@ -254,7 +254,8 @@ class RenderingMixin: screen_h = h * renderer.zoom font_family = element.font_settings.get("family", "Arial") - font_size = int(element.font_settings.get("size", 12) * renderer.zoom) + # Use base font size without zoom - zoom is applied via painter transform + font_size = int(element.font_settings.get("size", 12)) font = QFont(font_family, font_size) painter.setFont(font) @@ -265,16 +266,17 @@ class RenderingMixin: color = QColor(int(font_color[0] * 255), int(font_color[1] * 255), int(font_color[2] * 255)) painter.setPen(QPen(color)) + # Apply zoom via painter transform so font scales consistently with page + painter.save() + painter.translate(screen_x, screen_y) + painter.scale(renderer.zoom, renderer.zoom) + if element.rotation != 0: - painter.save() - center_x = screen_x + screen_w / 2 - center_y = screen_y + screen_h / 2 - painter.translate(center_x, center_y) + painter.translate(w / 2, h / 2) painter.rotate(element.rotation) - painter.translate(-screen_w / 2, -screen_h / 2) - rect = QRectF(0, 0, screen_w, screen_h) - else: - rect = QRectF(screen_x, screen_y, screen_w, screen_h) + painter.translate(-w / 2, -h / 2) + + rect = QRectF(0, 0, w, h) alignment = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop if element.alignment == "center": @@ -288,8 +290,7 @@ class RenderingMixin: painter.drawText(rect, int(alignment | text_flags), element.text_content) - if element.rotation != 0: - painter.restore() + painter.restore() finally: painter.end() @@ -308,15 +309,20 @@ class RenderingMixin: px, py, pw, ph = ghost_data.get_page_rect() screen_x, screen_y = renderer.page_to_screen(px, py) - screen_w = pw * renderer.zoom - screen_h = ph * renderer.zoom - font = QFont("Arial", int(16 * renderer.zoom), QFont.Weight.Bold) + # Use base font size without zoom - zoom is applied via painter transform + font = QFont("Arial", 16, QFont.Weight.Bold) painter.setFont(font) painter.setPen(QColor(120, 120, 120)) - rect = QRectF(screen_x, screen_y, screen_w, screen_h) + painter.save() + painter.translate(screen_x, screen_y) + painter.scale(renderer.zoom, renderer.zoom) + + rect = QRectF(0, 0, pw, ph) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "Click to Add Page") + painter.restore() + finally: painter.end() diff --git a/tests/test_rendering_mixin.py b/tests/test_rendering_mixin.py index e5c1eb4..b58d60c 100644 --- a/tests/test_rendering_mixin.py +++ b/tests/test_rendering_mixin.py @@ -670,3 +670,232 @@ class TestRenderGhostPage: # Painter should be ended (cleanup) mock_painter.end.assert_called_once() + + +class TestFontZoomIndependence: + """Test that font size is independent of zoom level (bug fix verification)""" + + def test_font_size_not_scaled_by_zoom(self, qtbot): + """Test that font size remains constant regardless of zoom level. + + This test verifies the fix for the bug where font size was multiplied + by zoom level, causing text to scale differently than in PDF export. + """ + widget = TestRenderingWidget() + qtbot.addWidget(widget) + widget.resize(1000, 800) + + base_font_size = 24 + + # Create text element with specific font size + text_element = TextBoxData( + x=50, y=50, width=200, height=100, + text_content="Test Text", + font_settings={ + "family": "Arial", + "size": base_font_size, + "color": (0, 0, 0) + }, + alignment="left" + ) + + # Mock page with text element + page = Page(layout=PageLayout(width=210, height=297), page_number=1) + page.layout.elements = [text_element] + + # Test with different zoom levels + for zoom_level in [0.5, 1.0, 2.0, 3.0]: + mock_renderer = Mock() + mock_renderer.page_to_screen = Mock(return_value=(100, 100)) + mock_renderer.zoom = zoom_level + + widget._page_renderers = [(mock_renderer, page)] + + # Mock QPainter and QFont + with patch("pyPhotoAlbum.mixins.rendering.QPainter") as mock_painter_class: + with patch("pyPhotoAlbum.mixins.rendering.QFont") as mock_qfont_class: + mock_painter = Mock() + mock_painter_class.return_value = mock_painter + + widget._render_text_overlays() + + # QFont should be created with base font size, NOT scaled by zoom + mock_qfont_class.assert_called() + call_args = mock_qfont_class.call_args + actual_font_size = call_args[0][1] # Second positional arg is size + + assert actual_font_size == base_font_size, ( + f"Font size should be {base_font_size} at zoom {zoom_level}, " + f"got {actual_font_size}" + ) + + def test_zoom_applied_via_painter_scale(self, qtbot): + """Test that zoom is applied via painter.scale() transformation. + + This ensures text scales consistently with the page content using + the painter's transformation matrix rather than font size modification. + """ + widget = TestRenderingWidget() + qtbot.addWidget(widget) + widget.resize(1000, 800) + + # Create text element + text_element = TextBoxData( + x=50, y=50, width=200, height=100, + text_content="Test Text", + font_settings={ + "family": "Arial", + "size": 12, + "color": (0, 0, 0) + }, + alignment="left" + ) + + # Mock page with text element + page = Page(layout=PageLayout(width=210, height=297), page_number=1) + page.layout.elements = [text_element] + + zoom_level = 2.5 + mock_renderer = Mock() + mock_renderer.page_to_screen = Mock(return_value=(100, 100)) + mock_renderer.zoom = zoom_level + + widget._page_renderers = [(mock_renderer, page)] + + # Mock QPainter + with patch("pyPhotoAlbum.mixins.rendering.QPainter") as mock_painter_class: + mock_painter = Mock() + mock_painter_class.return_value = mock_painter + + widget._render_text_overlays() + + # Painter should call scale() with zoom level + mock_painter.scale.assert_called_with(zoom_level, zoom_level) + + # Painter should use save/restore for transformation + mock_painter.save.assert_called() + mock_painter.restore.assert_called() + + def test_text_rect_uses_page_coordinates(self, qtbot): + """Test that text rect uses page coordinates (not screen coordinates). + + The text rect should be in page coordinates (w, h) since the painter + applies the zoom transformation via scale(). + """ + widget = TestRenderingWidget() + qtbot.addWidget(widget) + widget.resize(1000, 800) + + element_width = 200 + element_height = 100 + + # Create text element + text_element = TextBoxData( + x=50, y=50, width=element_width, height=element_height, + text_content="Test Text", + font_settings={ + "family": "Arial", + "size": 12, + "color": (0, 0, 0) + }, + alignment="left" + ) + + # Mock page with text element + page = Page(layout=PageLayout(width=210, height=297), page_number=1) + page.layout.elements = [text_element] + + mock_renderer = Mock() + mock_renderer.page_to_screen = Mock(return_value=(100, 100)) + mock_renderer.zoom = 2.0 # Zoom that would double screen size + + widget._page_renderers = [(mock_renderer, page)] + + # Mock QPainter and QRectF + with patch("pyPhotoAlbum.mixins.rendering.QPainter") as mock_painter_class: + with patch("pyPhotoAlbum.mixins.rendering.QRectF") as mock_qrectf_class: + mock_painter = Mock() + mock_painter_class.return_value = mock_painter + + widget._render_text_overlays() + + # QRectF should be called with page coordinates (0, 0, w, h) + # not screen coordinates (0, 0, w*zoom, h*zoom) + mock_qrectf_class.assert_called() + call_args = mock_qrectf_class.call_args + rect_width = call_args[0][2] + rect_height = call_args[0][3] + + assert rect_width == element_width, ( + f"Rect width should be {element_width} (page coords), got {rect_width}" + ) + assert rect_height == element_height, ( + f"Rect height should be {element_height} (page coords), got {rect_height}" + ) + + def test_ghost_page_font_not_scaled_by_zoom(self, qtbot): + """Test that ghost page font size is not scaled by zoom level.""" + widget = TestRenderingWidget() + qtbot.addWidget(widget) + widget.resize(1000, 800) + + # Create mock ghost data + ghost_data = Mock() + ghost_data.page_size = (210, 297) + ghost_data.render = Mock() + ghost_data.get_page_rect = Mock(return_value=(0, 0, 794, 1123)) + + for zoom_level in [0.5, 1.0, 2.0]: + # Create mock renderer with varying zoom + mock_renderer = Mock() + mock_renderer.page_to_screen = Mock(return_value=(100, 100)) + mock_renderer.zoom = zoom_level + mock_renderer.begin_render = Mock() + mock_renderer.end_render = Mock() + + # Mock QPainter and QFont + with patch("pyPhotoAlbum.mixins.rendering.QPainter") as mock_painter_class: + with patch("pyPhotoAlbum.mixins.rendering.QFont") as mock_qfont_class: + mock_painter = Mock() + mock_painter_class.return_value = mock_painter + + widget._render_ghost_page(ghost_data, mock_renderer) + + # QFont should be created with base font size 16, NOT scaled + mock_qfont_class.assert_called() + call_args = mock_qfont_class.call_args + actual_font_size = call_args[0][1] # Second positional arg is size + + assert actual_font_size == 16, ( + f"Ghost page font size should be 16 at zoom {zoom_level}, " + f"got {actual_font_size}" + ) + + def test_ghost_page_zoom_applied_via_scale(self, qtbot): + """Test that ghost page zoom is applied via painter.scale().""" + widget = TestRenderingWidget() + qtbot.addWidget(widget) + widget.resize(1000, 800) + + # Create mock ghost data + ghost_data = Mock() + ghost_data.page_size = (210, 297) + ghost_data.render = Mock() + ghost_data.get_page_rect = Mock(return_value=(0, 0, 794, 1123)) + + zoom_level = 1.5 + mock_renderer = Mock() + mock_renderer.page_to_screen = Mock(return_value=(100, 100)) + mock_renderer.zoom = zoom_level + mock_renderer.begin_render = Mock() + mock_renderer.end_render = Mock() + + # Mock QPainter + with patch("pyPhotoAlbum.mixins.rendering.QPainter") as mock_painter_class: + mock_painter = Mock() + mock_painter_class.return_value = mock_painter + + widget._render_ghost_page(ghost_data, mock_renderer) + + # Painter should call scale() with zoom level + mock_painter.scale.assert_called_with(zoom_level, zoom_level)