From 9ed89768856338682be8939aed3715ad91581790 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 28 Oct 2025 23:05:55 +0100 Subject: [PATCH] trying to fix in built templates --- .../mixins/operations/template_ops.py | 23 ++- pyPhotoAlbum/template_manager.py | 65 ++++--- pyPhotoAlbum/templates/Grid_2x2.json | 32 ++-- pyPhotoAlbum/templates/Single_Large.json | 20 +-- tests/test_template_manager.py | 160 ++++++++++++++++++ 5 files changed, 248 insertions(+), 52 deletions(-) diff --git a/pyPhotoAlbum/mixins/operations/template_ops.py b/pyPhotoAlbum/mixins/operations/template_ops.py index 6df9467..700bafe 100644 --- a/pyPhotoAlbum/mixins/operations/template_ops.py +++ b/pyPhotoAlbum/mixins/operations/template_ops.py @@ -4,7 +4,8 @@ Template operations mixin for pyPhotoAlbum from PyQt6.QtWidgets import ( QInputDialog, QDialog, QVBoxLayout, QLabel, QComboBox, - QRadioButton, QButtonGroup, QPushButton, QHBoxLayout + QRadioButton, QButtonGroup, QPushButton, QHBoxLayout, + QDoubleSpinBox ) from pyPhotoAlbum.decorators import ribbon_action, undoable_operation @@ -186,6 +187,22 @@ class TemplateOperationsMixin: 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 layout.addWidget(QLabel("Scaling:")) scale_group = QButtonGroup(dialog) @@ -228,6 +245,7 @@ class TemplateOperationsMixin: template_name = template_combo.currentText() mode_id = mode_group.checkedId() scale_id = scale_group.checkedId() + margin_percent = margin_spinbox.value() mode = "replace" if mode_id == 0 else "reflow" scale_mode = ["proportional", "stretch", "center"][scale_id] @@ -241,7 +259,8 @@ class TemplateOperationsMixin: template, current_page, mode=mode, - scale_mode=scale_mode + scale_mode=scale_mode, + margin_percent=margin_percent ) # Update display diff --git a/pyPhotoAlbum/template_manager.py b/pyPhotoAlbum/template_manager.py index fb920a0..bc1d7df 100644 --- a/pyPhotoAlbum/template_manager.py +++ b/pyPhotoAlbum/template_manager.py @@ -204,43 +204,56 @@ class TemplateManager: elements: List[BaseLayoutElement], from_size: Tuple[float, float], to_size: Tuple[float, float], - scale_mode: str = "proportional" + scale_mode: str = "proportional", + margin_percent: float = 0.0 ) -> List[BaseLayoutElement]: """ - Scale template elements to fit target page size. + Scale template elements to fit target page size with adjustable margins. Args: elements: List of elements to scale from_size: Original template size (width, height) in mm to_size: Target page size (width, height) in mm scale_mode: "proportional", "stretch", or "center" + margin_percent: Percentage of page size to use for margins (0-10%) Returns: List of scaled elements """ from_width, from_height = from_size to_width, to_height = to_size - - if scale_mode == "center": - # No scaling, just center elements - offset_x = (to_width - from_width) / 2 - offset_y = (to_height - from_height) / 2 - scale_x = 1.0 - scale_y = 1.0 + + # Calculate target margins from percentage + margin_x = to_width * (margin_percent / 100.0) + margin_y = to_height * (margin_percent / 100.0) + + # Available content area after margins + 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": - # Maintain aspect ratio - scale = min(to_width / from_width, to_height / from_height) + # Maintain aspect ratio - scale uniformly to fit content area + scale = min(content_width / from_width, content_height / from_height) scale_x = scale scale_y = scale - # Center the scaled content - offset_x = (to_width - from_width * scale) / 2 - offset_y = (to_height - from_height * scale) / 2 - else: # "stretch" - # Stretch to fit - scale_x = to_width / from_width - scale_y = to_height / from_height - offset_x = 0 - offset_y = 0 + # Center the scaled content within the page + scaled_width = from_width * scale + scaled_height = from_height * scale + offset_x = (to_width - scaled_width) / 2 + offset_y = (to_height - scaled_height) / 2 + else: # "center" + # No scaling, just center on page + scale_x = 1.0 + scale_y = 1.0 + offset_x = (to_width - from_width) / 2 + offset_y = (to_height - from_height) / 2 scaled_elements = [] for element in elements: @@ -283,10 +296,11 @@ class TemplateManager: template: Template, page: Page, 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: template: Template to apply @@ -294,6 +308,7 @@ class TemplateManager: mode: "replace" to clear page and add placeholders, "reflow" to keep existing content and reposition scale_mode: "proportional", "stretch", or "center" + margin_percent: Percentage of page size to use for margins (0-10%) """ if mode == "replace": # Clear existing elements @@ -304,7 +319,8 @@ class TemplateManager: template.elements, template.page_size_mm, page.layout.size, - scale_mode + scale_mode, + margin_percent ) # Add scaled elements to page @@ -321,7 +337,8 @@ class TemplateManager: template.elements, template.page_size_mm, page.layout.size, - scale_mode + scale_mode, + margin_percent ) template_placeholders = [e for e in scaled_elements if isinstance(e, PlaceholderData)] diff --git a/pyPhotoAlbum/templates/Grid_2x2.json b/pyPhotoAlbum/templates/Grid_2x2.json index d40a84c..23321e5 100644 --- a/pyPhotoAlbum/templates/Grid_2x2.json +++ b/pyPhotoAlbum/templates/Grid_2x2.json @@ -1,20 +1,20 @@ { "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": [ 210, - 297 + 210 ], "elements": [ { "type": "placeholder", "position": [ - 5, - 5 + 0, + 0 ], "size": [ - 100, - 143.5 + 105, + 105 ], "rotation": 0, "z_index": 0, @@ -25,11 +25,11 @@ "type": "placeholder", "position": [ 105, - 5 + 0 ], "size": [ - 100, - 143.5 + 105, + 105 ], "rotation": 0, "z_index": 0, @@ -39,12 +39,12 @@ { "type": "placeholder", "position": [ - 5, - 148.5 + 0, + 105 ], "size": [ - 100, - 143.5 + 105, + 105 ], "rotation": 0, "z_index": 0, @@ -55,11 +55,11 @@ "type": "placeholder", "position": [ 105, - 148.5 + 105 ], "size": [ - 100, - 143.5 + 105, + 105 ], "rotation": 0, "z_index": 0, diff --git a/pyPhotoAlbum/templates/Single_Large.json b/pyPhotoAlbum/templates/Single_Large.json index 36ece36..1106a9a 100644 --- a/pyPhotoAlbum/templates/Single_Large.json +++ b/pyPhotoAlbum/templates/Single_Large.json @@ -1,20 +1,20 @@ { "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": [ 210, - 297 + 210 ], "elements": [ { "type": "textbox", "position": [ - 10, - 10 + 0, + 0 ], "size": [ - 190, - 30 + 210, + 25 ], "rotation": 0, "z_index": 1, @@ -33,12 +33,12 @@ { "type": "placeholder", "position": [ - 10, - 50 + 0, + 25 ], "size": [ - 190, - 230 + 210, + 185 ], "rotation": 0, "z_index": 0, diff --git a/tests/test_template_manager.py b/tests/test_template_manager.py index 0123875..e370197 100644 --- a/tests/test_template_manager.py +++ b/tests/test_template_manager.py @@ -530,3 +530,163 @@ class TestTemplateManager: assert scaled[0].text_content == "Test" assert scaled[0].font_settings == font_settings 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