fixed bug in font rendering when zooming
All checks were successful
Python CI / test (push) Successful in 1m32s
Lint / lint (push) Successful in 1m18s
Tests / test (3.11) (push) Successful in 1m47s
Tests / test (3.12) (push) Successful in 1m48s
Tests / test (3.13) (push) Successful in 1m42s
Tests / test (3.14) (push) Successful in 1m18s

This commit is contained in:
Duncan Tourolle 2025-12-31 13:19:36 +01:00
parent 6a791b1397
commit cf27d9ebee
2 changed files with 250 additions and 15 deletions

View File

@ -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()

View File

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