trying to fix in built templates
All checks were successful
Python CI / test (push) Successful in 57s
Lint / lint (push) Successful in 1m6s
Tests / test (3.10) (push) Successful in 44s
Tests / test (3.11) (push) Successful in 44s
Tests / test (3.9) (push) Successful in 42s

This commit is contained in:
Duncan Tourolle 2025-10-28 23:05:55 +01:00
parent 795c0c531c
commit 9ed8976885
5 changed files with 248 additions and 52 deletions

View File

@ -4,7 +4,8 @@ Template operations mixin for pyPhotoAlbum
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QInputDialog, QDialog, QVBoxLayout, QLabel, QComboBox, QInputDialog, QDialog, QVBoxLayout, QLabel, QComboBox,
QRadioButton, QButtonGroup, QPushButton, QHBoxLayout QRadioButton, QButtonGroup, QPushButton, QHBoxLayout,
QDoubleSpinBox
) )
from pyPhotoAlbum.decorators import ribbon_action, undoable_operation from pyPhotoAlbum.decorators import ribbon_action, undoable_operation
@ -186,6 +187,22 @@ class TemplateOperationsMixin:
layout.addSpacing(10) layout.addSpacing(10)
# Margin/Spacing percentage
layout.addWidget(QLabel("Margin/Spacing:"))
margin_layout = QHBoxLayout()
margin_spinbox = QDoubleSpinBox()
margin_spinbox.setRange(0.0, 10.0)
margin_spinbox.setValue(2.5)
margin_spinbox.setSuffix("%")
margin_spinbox.setDecimals(1)
margin_spinbox.setSingleStep(0.5)
margin_spinbox.setToolTip("Percentage of page size to use for margins and spacing")
margin_layout.addWidget(margin_spinbox)
margin_layout.addStretch()
layout.addLayout(margin_layout)
layout.addSpacing(10)
# Scaling selection # Scaling selection
layout.addWidget(QLabel("Scaling:")) layout.addWidget(QLabel("Scaling:"))
scale_group = QButtonGroup(dialog) scale_group = QButtonGroup(dialog)
@ -228,6 +245,7 @@ class TemplateOperationsMixin:
template_name = template_combo.currentText() template_name = template_combo.currentText()
mode_id = mode_group.checkedId() mode_id = mode_group.checkedId()
scale_id = scale_group.checkedId() scale_id = scale_group.checkedId()
margin_percent = margin_spinbox.value()
mode = "replace" if mode_id == 0 else "reflow" mode = "replace" if mode_id == 0 else "reflow"
scale_mode = ["proportional", "stretch", "center"][scale_id] scale_mode = ["proportional", "stretch", "center"][scale_id]
@ -241,7 +259,8 @@ class TemplateOperationsMixin:
template, template,
current_page, current_page,
mode=mode, mode=mode,
scale_mode=scale_mode scale_mode=scale_mode,
margin_percent=margin_percent
) )
# Update display # Update display

View File

@ -204,43 +204,56 @@ class TemplateManager:
elements: List[BaseLayoutElement], elements: List[BaseLayoutElement],
from_size: Tuple[float, float], from_size: Tuple[float, float],
to_size: Tuple[float, float], to_size: Tuple[float, float],
scale_mode: str = "proportional" scale_mode: str = "proportional",
margin_percent: float = 0.0
) -> List[BaseLayoutElement]: ) -> List[BaseLayoutElement]:
""" """
Scale template elements to fit target page size. Scale template elements to fit target page size with adjustable margins.
Args: Args:
elements: List of elements to scale elements: List of elements to scale
from_size: Original template size (width, height) in mm from_size: Original template size (width, height) in mm
to_size: Target page size (width, height) in mm to_size: Target page size (width, height) in mm
scale_mode: "proportional", "stretch", or "center" scale_mode: "proportional", "stretch", or "center"
margin_percent: Percentage of page size to use for margins (0-10%)
Returns: Returns:
List of scaled elements List of scaled elements
""" """
from_width, from_height = from_size from_width, from_height = from_size
to_width, to_height = to_size to_width, to_height = to_size
if scale_mode == "center": # Calculate target margins from percentage
# No scaling, just center elements margin_x = to_width * (margin_percent / 100.0)
offset_x = (to_width - from_width) / 2 margin_y = to_height * (margin_percent / 100.0)
offset_y = (to_height - from_height) / 2
scale_x = 1.0 # Available content area after margins
scale_y = 1.0 content_width = to_width - (2 * margin_x)
content_height = to_height - (2 * margin_y)
# Calculate scale factors based on mode
if scale_mode == "stretch":
# Stretch to fill content area independently in each dimension
scale_x = content_width / from_width
scale_y = content_height / from_height
offset_x = margin_x
offset_y = margin_y
elif scale_mode == "proportional": elif scale_mode == "proportional":
# Maintain aspect ratio # Maintain aspect ratio - scale uniformly to fit content area
scale = min(to_width / from_width, to_height / from_height) scale = min(content_width / from_width, content_height / from_height)
scale_x = scale scale_x = scale
scale_y = scale scale_y = scale
# Center the scaled content # Center the scaled content within the page
offset_x = (to_width - from_width * scale) / 2 scaled_width = from_width * scale
offset_y = (to_height - from_height * scale) / 2 scaled_height = from_height * scale
else: # "stretch" offset_x = (to_width - scaled_width) / 2
# Stretch to fit offset_y = (to_height - scaled_height) / 2
scale_x = to_width / from_width else: # "center"
scale_y = to_height / from_height # No scaling, just center on page
offset_x = 0 scale_x = 1.0
offset_y = 0 scale_y = 1.0
offset_x = (to_width - from_width) / 2
offset_y = (to_height - from_height) / 2
scaled_elements = [] scaled_elements = []
for element in elements: for element in elements:
@ -283,10 +296,11 @@ class TemplateManager:
template: Template, template: Template,
page: Page, page: Page,
mode: str = "replace", mode: str = "replace",
scale_mode: str = "proportional" scale_mode: str = "proportional",
margin_percent: float = 2.5
): ):
""" """
Apply template to an existing page. Apply template to an existing page with adjustable margins.
Args: Args:
template: Template to apply template: Template to apply
@ -294,6 +308,7 @@ class TemplateManager:
mode: "replace" to clear page and add placeholders, mode: "replace" to clear page and add placeholders,
"reflow" to keep existing content and reposition "reflow" to keep existing content and reposition
scale_mode: "proportional", "stretch", or "center" scale_mode: "proportional", "stretch", or "center"
margin_percent: Percentage of page size to use for margins (0-10%)
""" """
if mode == "replace": if mode == "replace":
# Clear existing elements # Clear existing elements
@ -304,7 +319,8 @@ class TemplateManager:
template.elements, template.elements,
template.page_size_mm, template.page_size_mm,
page.layout.size, page.layout.size,
scale_mode scale_mode,
margin_percent
) )
# Add scaled elements to page # Add scaled elements to page
@ -321,7 +337,8 @@ class TemplateManager:
template.elements, template.elements,
template.page_size_mm, template.page_size_mm,
page.layout.size, page.layout.size,
scale_mode scale_mode,
margin_percent
) )
template_placeholders = [e for e in scaled_elements if isinstance(e, PlaceholderData)] template_placeholders = [e for e in scaled_elements if isinstance(e, PlaceholderData)]

View File

@ -1,20 +1,20 @@
{ {
"name": "Grid_2x2", "name": "Grid_2x2",
"description": "Simple 2x2 grid layout with equal-sized image placeholders", "description": "Simple 2x2 grid layout with equal-sized image placeholders (square page, margins applied at use time)",
"page_size_mm": [ "page_size_mm": [
210, 210,
297 210
], ],
"elements": [ "elements": [
{ {
"type": "placeholder", "type": "placeholder",
"position": [ "position": [
5, 0,
5 0
], ],
"size": [ "size": [
100, 105,
143.5 105
], ],
"rotation": 0, "rotation": 0,
"z_index": 0, "z_index": 0,
@ -25,11 +25,11 @@
"type": "placeholder", "type": "placeholder",
"position": [ "position": [
105, 105,
5 0
], ],
"size": [ "size": [
100, 105,
143.5 105
], ],
"rotation": 0, "rotation": 0,
"z_index": 0, "z_index": 0,
@ -39,12 +39,12 @@
{ {
"type": "placeholder", "type": "placeholder",
"position": [ "position": [
5, 0,
148.5 105
], ],
"size": [ "size": [
100, 105,
143.5 105
], ],
"rotation": 0, "rotation": 0,
"z_index": 0, "z_index": 0,
@ -55,11 +55,11 @@
"type": "placeholder", "type": "placeholder",
"position": [ "position": [
105, 105,
148.5 105
], ],
"size": [ "size": [
100, 105,
143.5 105
], ],
"rotation": 0, "rotation": 0,
"z_index": 0, "z_index": 0,

View File

@ -1,20 +1,20 @@
{ {
"name": "Single_Large", "name": "Single_Large",
"description": "Single large image placeholder with title text", "description": "Single large image placeholder with title text (square page, margins applied at use time)",
"page_size_mm": [ "page_size_mm": [
210, 210,
297 210
], ],
"elements": [ "elements": [
{ {
"type": "textbox", "type": "textbox",
"position": [ "position": [
10, 0,
10 0
], ],
"size": [ "size": [
190, 210,
30 25
], ],
"rotation": 0, "rotation": 0,
"z_index": 1, "z_index": 1,
@ -33,12 +33,12 @@
{ {
"type": "placeholder", "type": "placeholder",
"position": [ "position": [
10, 0,
50 25
], ],
"size": [ "size": [
190, 210,
230 185
], ],
"rotation": 0, "rotation": 0,
"z_index": 0, "z_index": 0,

View File

@ -530,3 +530,163 @@ class TestTemplateManager:
assert scaled[0].text_content == "Test" assert scaled[0].text_content == "Test"
assert scaled[0].font_settings == font_settings assert scaled[0].font_settings == font_settings
assert scaled[0].alignment == text.alignment assert scaled[0].alignment == text.alignment
def test_grid_2x2_stretch_to_square_page(self):
"""Test Grid_2x2 template applied to square page with stretch mode"""
manager = TemplateManager()
# Create a 2x2 grid template at 210x210mm (margin-less, fills entire space)
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
# 4 cells: each 105 x 105mm (half of 210mm)
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=0, y=105, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=105, width=105, height=105))
# Apply to same size page with stretch mode and 2.5% margin
layout = PageLayout(width=210, height=210)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=2.5
)
# With 2.5% margin on 210mm page: margin = 5.25mm, content area = 199.5mm
# Template is 210mm, so scale = 199.5 / 210 = 0.95
# Each element should scale by 0.95 and be offset by margin
assert len(page.layout.elements) == 4
# Check first element (top-left)
elem = page.layout.elements[0]
scale = 199.5 / 210.0 # 0.95
expected_x = 0 * scale + 5.25 # 0 + 5.25 = 5.25
expected_y = 0 * scale + 5.25 # 0 + 5.25 = 5.25
expected_width = 105 * scale # 99.75
expected_height = 105 * scale # 99.75
assert abs(elem.position[0] - expected_x) < 0.1
assert abs(elem.position[1] - expected_y) < 0.1
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
def test_grid_2x2_stretch_to_a4_page(self):
"""Test Grid_2x2 template applied to A4 page with stretch mode"""
manager = TemplateManager()
# Create Grid_2x2 template (210x210mm, margin-less)
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=0, y=105, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=105, width=105, height=105))
# Apply to A4 page (210x297mm) with stretch mode and 2.5% margin
layout = PageLayout(width=210, height=297)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=2.5
)
# With 2.5% margin: x_margin = 5.25mm, y_margin = 7.425mm
# Content area: 199.5 x 282.15mm
# Scale: x = 199.5/210 = 0.95, y = 282.15/210 = 1.3436
assert len(page.layout.elements) == 4
# First element should stretch
elem = page.layout.elements[0]
scale_x = 199.5 / 210.0
scale_y = 282.15 / 210.0
expected_x = 0 * scale_x + 5.25 # 5.25
expected_y = 0 * scale_y + 7.425 # 7.425
expected_width = 105 * scale_x # 99.75
expected_height = 105 * scale_y # 141.075
assert abs(elem.position[0] - expected_x) < 0.1
assert abs(elem.position[1] - expected_y) < 0.1
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
def test_grid_2x2_with_different_margins(self):
"""Test Grid_2x2 template with different margin percentages"""
manager = TemplateManager()
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
# Test with 0% margin
layout = PageLayout(width=210, height=210)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=0.0
)
# With 0% margin, template fills entire page (scale = 1.0, offset = 0)
elem = page.layout.elements[0]
assert abs(elem.position[0] - 0.0) < 0.1
assert abs(elem.position[1] - 0.0) < 0.1
assert abs(elem.size[0] - 105.0) < 0.1
# Test with 5% margin
layout2 = PageLayout(width=210, height=210)
page2 = Page(layout=layout2, page_number=1)
manager.apply_template_to_page(
template, page2,
mode="replace",
scale_mode="stretch",
margin_percent=5.0
)
# With 5% margin: margin = 10.5mm, content = 189mm, scale = 189/210 = 0.9
elem2 = page2.layout.elements[0]
assert abs(elem2.position[0] - 10.5) < 0.1
assert abs(elem2.position[1] - 10.5) < 0.1
assert abs(elem2.size[0] - (105 * 0.9)) < 0.1
def test_grid_2x2_proportional_mode(self):
"""Test Grid_2x2 template with proportional scaling"""
manager = TemplateManager()
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
# Apply to rectangular page with proportional mode
layout = PageLayout(width=210, height=297)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="proportional",
margin_percent=2.5
)
# With proportional mode on 210x297 page:
# Content area: 199.5 x 282.15mm
# Template: 210 x 210mm
# Scale = min(199.5/210, 282.15/210) = 0.95 (uniform)
# Content is centered on page
elem = page.layout.elements[0]
scale = 199.5 / 210.0
# Should be scaled uniformly
expected_width = 105 * scale # 99.75
expected_height = 105 * scale # 99.75
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
# Width should equal height (uniform scaling)
assert abs(elem.size[0] - elem.size[1]) < 0.1