Many improvements and stability fixes
Lint / lint (push) Successful in 1m47s
Tests / test (3.11) (push) Successful in 57s
Tests / test (3.12) (push) Successful in 55s
Tests / test (3.13) (push) Successful in 56s
Tests / test (3.14) (push) Successful in 1m3s
Python CI / test (push) Failing after 6m1s
Lint / lint (push) Successful in 1m47s
Tests / test (3.11) (push) Successful in 57s
Tests / test (3.12) (push) Successful in 55s
Tests / test (3.13) (push) Successful in 56s
Tests / test (3.14) (push) Successful in 1m3s
Python CI / test (push) Failing after 6m1s
This commit is contained in:
@@ -809,13 +809,14 @@ class TestExportPdf:
|
||||
page = Page(layout=layout, page_number=1)
|
||||
window.project.pages = [page]
|
||||
|
||||
window.project.export_dpi = 150
|
||||
mock_file_dialog.return_value = ("/path/to/output.pdf", "")
|
||||
window.gl_widget.export_pdf_async.return_value = True
|
||||
|
||||
window.export_pdf()
|
||||
|
||||
# Verify export was called
|
||||
window.gl_widget.export_pdf_async.assert_called_once_with(window.project, "/path/to/output.pdf", export_dpi=300)
|
||||
# Verify export was called with project's export_dpi, not a hardcoded value
|
||||
window.gl_widget.export_pdf_async.assert_called_once_with(window.project, "/path/to/output.pdf", export_dpi=150)
|
||||
assert "PDF export started" in window._status_message
|
||||
|
||||
@patch("pyPhotoAlbum.mixins.operations.file_ops.QFileDialog.getSaveFileName")
|
||||
@@ -833,8 +834,10 @@ class TestExportPdf:
|
||||
|
||||
window.export_pdf()
|
||||
|
||||
# Verify .pdf was added
|
||||
window.gl_widget.export_pdf_async.assert_called_once_with(window.project, "/path/to/output.pdf", export_dpi=300)
|
||||
# Verify .pdf was added and project's export_dpi is used
|
||||
window.gl_widget.export_pdf_async.assert_called_once_with(
|
||||
window.project, "/path/to/output.pdf", export_dpi=window.project.export_dpi
|
||||
)
|
||||
|
||||
@patch("pyPhotoAlbum.mixins.operations.file_ops.QFileDialog.getSaveFileName")
|
||||
def test_export_pdf_failed_to_start(self, mock_file_dialog, qtbot):
|
||||
|
||||
+20
-19
@@ -75,7 +75,7 @@ class TestFrameDefinition:
|
||||
frame_type=FrameType.FULL,
|
||||
)
|
||||
assert frame.description == ""
|
||||
assert frame.assets == {}
|
||||
assert frame.asset_path is None
|
||||
assert frame.colorizable is True
|
||||
assert frame.default_thickness == 5.0
|
||||
|
||||
@@ -144,19 +144,20 @@ class TestFrameManager:
|
||||
assert isinstance(names, list)
|
||||
assert "simple_line" in names
|
||||
assert "double_line" in names
|
||||
assert "leafy_corners" in names
|
||||
assert "geometric_corners" in names
|
||||
|
||||
def test_bundled_frames_exist(self, frame_manager):
|
||||
"""Test that expected bundled frames exist"""
|
||||
expected_frames = [
|
||||
"simple_line",
|
||||
"double_line",
|
||||
"rounded_modern",
|
||||
"geometric_corners",
|
||||
"leafy_corners",
|
||||
"ornate_flourish",
|
||||
"victorian",
|
||||
"art_nouveau",
|
||||
"floral_corner",
|
||||
"floral_flourish",
|
||||
"ornate_corner",
|
||||
"simple_corner",
|
||||
"corner_decoration",
|
||||
"corner_ornament",
|
||||
]
|
||||
for name in expected_frames:
|
||||
frame = frame_manager.get_frame(name)
|
||||
@@ -164,15 +165,15 @@ class TestFrameManager:
|
||||
|
||||
def test_modern_frames_are_full_type(self, frame_manager):
|
||||
"""Test that modern frames are FULL type"""
|
||||
modern_frames = ["simple_line", "double_line", "rounded_modern"]
|
||||
modern_frames = ["simple_line", "double_line"]
|
||||
for name in modern_frames:
|
||||
frame = frame_manager.get_frame(name)
|
||||
assert frame is not None
|
||||
assert frame.frame_type == FrameType.FULL
|
||||
|
||||
def test_leafy_corners_is_corners_type(self, frame_manager):
|
||||
"""Test that leafy_corners is CORNERS type"""
|
||||
frame = frame_manager.get_frame("leafy_corners")
|
||||
def test_geometric_corners_is_corners_type(self, frame_manager):
|
||||
"""Test that geometric_corners is CORNERS type"""
|
||||
frame = frame_manager.get_frame("geometric_corners")
|
||||
assert frame is not None
|
||||
assert frame.frame_type == FrameType.CORNERS
|
||||
|
||||
@@ -207,7 +208,7 @@ class TestFrameCategories:
|
||||
def test_modern_category_not_empty(self, frame_manager):
|
||||
"""Test MODERN category has frames"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.MODERN)
|
||||
assert len(frames) >= 3 # simple_line, double_line, rounded_modern
|
||||
assert len(frames) >= 2 # simple_line, double_line
|
||||
|
||||
def test_vintage_category_not_empty(self, frame_manager):
|
||||
"""Test VINTAGE category has frames"""
|
||||
@@ -238,9 +239,9 @@ class TestFrameDescriptions:
|
||||
frame = frame_manager.get_frame("simple_line")
|
||||
assert frame.description != ""
|
||||
|
||||
def test_leafy_corners_has_description(self, frame_manager):
|
||||
"""Test leafy_corners has a description"""
|
||||
frame = frame_manager.get_frame("leafy_corners")
|
||||
def test_floral_corner_has_description(self, frame_manager):
|
||||
"""Test floral_corner has a description"""
|
||||
frame = frame_manager.get_frame("floral_corner")
|
||||
assert frame.description != ""
|
||||
|
||||
def test_all_frames_have_descriptions(self, frame_manager):
|
||||
@@ -268,11 +269,11 @@ class TestFrameThickness:
|
||||
|
||||
def test_vintage_frames_are_thicker(self, frame_manager):
|
||||
"""Test vintage frames have thicker default"""
|
||||
leafy = frame_manager.get_frame("leafy_corners")
|
||||
victorian = frame_manager.get_frame("victorian")
|
||||
floral = frame_manager.get_frame("floral_corner")
|
||||
ornate = frame_manager.get_frame("ornate_corner")
|
||||
|
||||
assert leafy.default_thickness >= 8.0
|
||||
assert victorian.default_thickness >= 10.0
|
||||
assert floral.default_thickness >= 8.0
|
||||
assert ornate.default_thickness >= 8.0
|
||||
|
||||
def test_all_thicknesses_positive(self, frame_manager):
|
||||
"""Test all frames have positive thickness"""
|
||||
|
||||
@@ -110,7 +110,8 @@ class TestPageSetupDialog:
|
||||
# Size editing should be disabled for covers
|
||||
assert not dialog.width_spinbox.isEnabled()
|
||||
assert not dialog.height_spinbox.isEnabled()
|
||||
assert not dialog.set_default_checkbox.isEnabled()
|
||||
assert not dialog.scope_non_manual.isEnabled()
|
||||
assert not dialog.scope_all_pages.isEnabled()
|
||||
|
||||
def test_dialog_double_spread_width_calculation(self, qtbot):
|
||||
"""Test double spread shows per-page width, not total width"""
|
||||
@@ -182,7 +183,7 @@ class TestPageSetupDialog:
|
||||
dialog.height_spinbox.setValue(280)
|
||||
dialog.working_dpi_spinbox.setValue(150)
|
||||
dialog.export_dpi_spinbox.setValue(600)
|
||||
dialog.set_default_checkbox.setChecked(True)
|
||||
dialog.scope_all_pages.setChecked(True)
|
||||
dialog.cover_checkbox.setChecked(True)
|
||||
dialog.thickness_spinbox.setValue(0.15)
|
||||
dialog.bleed_spinbox.setValue(5.0)
|
||||
@@ -199,7 +200,7 @@ class TestPageSetupDialog:
|
||||
assert values["height_mm"] == 280
|
||||
assert values["working_dpi"] == 150
|
||||
assert values["export_dpi"] == 600
|
||||
assert values["set_as_default"] is True
|
||||
assert values["apply_scope"] == 2
|
||||
|
||||
def test_dialog_page_change_updates_values(self, qtbot):
|
||||
"""Test changing selected page updates displayed values"""
|
||||
@@ -552,7 +553,7 @@ class TestPageSetupIntegration:
|
||||
"height_mm": 280,
|
||||
"working_dpi": 150,
|
||||
"export_dpi": 600,
|
||||
"set_as_default": True,
|
||||
"apply_scope": 1,
|
||||
}
|
||||
|
||||
# Access the unwrapped function to test business logic directly
|
||||
@@ -586,7 +587,7 @@ class TestPageSetupIntegration:
|
||||
assert window.project.cover_bleed_mm == 5.0
|
||||
assert window.project.working_dpi == 150
|
||||
assert window.project.export_dpi == 600
|
||||
assert window.project.page_size_mm == (200, 280) # set_as_default=True
|
||||
assert window.project.page_size_mm == (200, 280) # apply_scope=1
|
||||
|
||||
# Check page size updated
|
||||
assert window.project.pages[0].layout.size == (200, 280)
|
||||
@@ -644,7 +645,7 @@ class TestPageSetupIntegration:
|
||||
"height_mm": 297,
|
||||
"working_dpi": 96,
|
||||
"export_dpi": 300,
|
||||
"set_as_default": False,
|
||||
"apply_scope": 0,
|
||||
}
|
||||
|
||||
# Get the undecorated method
|
||||
@@ -716,7 +717,7 @@ class TestPageSetupIntegration:
|
||||
"height_mm": 280, # New height
|
||||
"working_dpi": 96,
|
||||
"export_dpi": 300,
|
||||
"set_as_default": False,
|
||||
"apply_scope": 0,
|
||||
}
|
||||
|
||||
from pyPhotoAlbum.mixins.operations import page_ops
|
||||
@@ -731,3 +732,147 @@ class TestPageSetupIntegration:
|
||||
assert window.project.pages[0].layout.base_width == 200
|
||||
assert window.project.pages[0].layout.size == (400, 280) # Double width
|
||||
assert window.project.pages[0].manually_sized is True
|
||||
|
||||
def test_set_as_default_updates_double_spread_pages(self, qtbot):
|
||||
"""Test set_as_default updates existing double spread pages with doubled width"""
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
from pyPhotoAlbum.mixins.base import ApplicationStateMixin
|
||||
from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin
|
||||
|
||||
class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._project = Project(name="Test")
|
||||
self._project.page_size_mm = (210, 297)
|
||||
self._project.working_dpi = 96
|
||||
self._project.export_dpi = 300
|
||||
self._project.paper_thickness_mm = 0.1
|
||||
self._project.cover_bleed_mm = 3.0
|
||||
|
||||
# Normal page being edited
|
||||
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
||||
# Double spread page (not manually sized - should be updated)
|
||||
page2 = Page(layout=PageLayout(width=420, height=297), page_number=2)
|
||||
page2.is_double_spread = True
|
||||
page2.layout.base_width = 210
|
||||
page2.layout.is_facing_page = True
|
||||
# Manually sized page (should NOT be updated)
|
||||
page3 = Page(layout=PageLayout(width=150, height=200), page_number=3)
|
||||
page3.manually_sized = True
|
||||
self._project.pages = [page1, page2, page3]
|
||||
|
||||
self._gl_widget = Mock()
|
||||
self._gl_widget._page_renderers = []
|
||||
self._status_bar = Mock()
|
||||
self._update_view_called = False
|
||||
|
||||
def _get_most_visible_page_index(self):
|
||||
return 0
|
||||
|
||||
def update_view(self):
|
||||
self._update_view_called = True
|
||||
|
||||
def show_status(self, message, timeout=0):
|
||||
pass
|
||||
|
||||
window = TestWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
values = {
|
||||
"selected_index": 0,
|
||||
"selected_page": window.project.pages[0],
|
||||
"is_cover": False,
|
||||
"paper_thickness_mm": 0.1,
|
||||
"cover_bleed_mm": 3.0,
|
||||
"width_mm": 200,
|
||||
"height_mm": 280,
|
||||
"working_dpi": 96,
|
||||
"export_dpi": 300,
|
||||
"apply_scope": 1,
|
||||
}
|
||||
|
||||
from pyPhotoAlbum.mixins.operations import page_ops
|
||||
|
||||
undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
|
||||
while hasattr(undecorated_page_setup, "__wrapped__"):
|
||||
undecorated_page_setup = undecorated_page_setup.__wrapped__
|
||||
|
||||
undecorated_page_setup(window, values)
|
||||
|
||||
# Project default updated
|
||||
assert window.project.page_size_mm == (200, 280)
|
||||
# Double spread page updated with doubled width
|
||||
assert window.project.pages[1].layout.base_width == 200
|
||||
assert window.project.pages[1].layout.size == (400, 280)
|
||||
# Manually sized page NOT updated
|
||||
assert window.project.pages[2].layout.size == (150, 200)
|
||||
|
||||
def test_apply_scope_all_pages_overrides_manual_sizing(self, qtbot):
|
||||
"""Test apply_scope=2 updates all pages including manually sized ones"""
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
from pyPhotoAlbum.mixins.base import ApplicationStateMixin
|
||||
from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin
|
||||
|
||||
class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._project = Project(name="Test")
|
||||
self._project.page_size_mm = (210, 297)
|
||||
self._project.working_dpi = 96
|
||||
self._project.export_dpi = 300
|
||||
self._project.paper_thickness_mm = 0.1
|
||||
self._project.cover_bleed_mm = 3.0
|
||||
|
||||
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
||||
page2 = Page(layout=PageLayout(width=420, height=297), page_number=2)
|
||||
page2.is_double_spread = True
|
||||
page2.layout.base_width = 210
|
||||
page2.layout.is_facing_page = True
|
||||
page3 = Page(layout=PageLayout(width=150, height=200), page_number=3)
|
||||
page3.manually_sized = True
|
||||
self._project.pages = [page1, page2, page3]
|
||||
|
||||
self._gl_widget = Mock()
|
||||
self._gl_widget._page_renderers = []
|
||||
self._status_bar = Mock()
|
||||
self._update_view_called = False
|
||||
|
||||
def _get_most_visible_page_index(self):
|
||||
return 0
|
||||
|
||||
def update_view(self):
|
||||
self._update_view_called = True
|
||||
|
||||
def show_status(self, message, timeout=0):
|
||||
pass
|
||||
|
||||
window = TestWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
values = {
|
||||
"selected_index": 0,
|
||||
"selected_page": window.project.pages[0],
|
||||
"is_cover": False,
|
||||
"paper_thickness_mm": 0.1,
|
||||
"cover_bleed_mm": 3.0,
|
||||
"width_mm": 200,
|
||||
"height_mm": 280,
|
||||
"working_dpi": 96,
|
||||
"export_dpi": 300,
|
||||
"apply_scope": 2,
|
||||
}
|
||||
|
||||
from pyPhotoAlbum.mixins.operations import page_ops
|
||||
|
||||
undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
|
||||
while hasattr(undecorated_page_setup, "__wrapped__"):
|
||||
undecorated_page_setup = undecorated_page_setup.__wrapped__
|
||||
|
||||
undecorated_page_setup(window, values)
|
||||
|
||||
# Project default updated
|
||||
assert window.project.page_size_mm == (200, 280)
|
||||
# Double spread updated
|
||||
assert window.project.pages[1].layout.size == (400, 280)
|
||||
# Manually sized page IS updated when scope=2
|
||||
assert window.project.pages[2].layout.size == (200, 280)
|
||||
|
||||
@@ -53,7 +53,9 @@ class TestPageSetupDialogWithMocks:
|
||||
dialog.cover_checkbox = Mock()
|
||||
dialog.width_spinbox = Mock()
|
||||
dialog.height_spinbox = Mock()
|
||||
dialog.set_default_checkbox = Mock()
|
||||
dialog.scope_non_manual = Mock()
|
||||
dialog.scope_all_pages = Mock()
|
||||
dialog.scope_page_only = Mock()
|
||||
|
||||
# Mock the update spine info method
|
||||
dialog._update_spine_info = Mock()
|
||||
@@ -187,8 +189,8 @@ class TestPageSetupDialogWithMocks:
|
||||
dialog.export_dpi_spinbox = Mock()
|
||||
dialog.export_dpi_spinbox.value.return_value = 600
|
||||
|
||||
dialog.set_default_checkbox = Mock()
|
||||
dialog.set_default_checkbox.isChecked.return_value = True
|
||||
dialog._apply_scope_group = Mock()
|
||||
dialog._apply_scope_group.checkedId.return_value = 2
|
||||
|
||||
# Get values
|
||||
values = dialog.get_values()
|
||||
@@ -203,7 +205,7 @@ class TestPageSetupDialogWithMocks:
|
||||
assert values["height_mm"] == 280.0
|
||||
assert values["working_dpi"] == 150
|
||||
assert values["export_dpi"] == 600
|
||||
assert values["set_as_default"] is True
|
||||
assert values["apply_scope"] == 2
|
||||
|
||||
def test_cover_page_width_display(self):
|
||||
"""Test cover page shows full width, not base width"""
|
||||
@@ -224,7 +226,9 @@ class TestPageSetupDialogWithMocks:
|
||||
dialog.cover_checkbox = Mock()
|
||||
dialog.width_spinbox = Mock()
|
||||
dialog.height_spinbox = Mock()
|
||||
dialog.set_default_checkbox = Mock()
|
||||
dialog.scope_non_manual = Mock()
|
||||
dialog.scope_all_pages = Mock()
|
||||
dialog.scope_page_only = Mock()
|
||||
dialog._update_spine_info = Mock()
|
||||
|
||||
# Call _on_page_changed for cover page
|
||||
@@ -238,7 +242,8 @@ class TestPageSetupDialogWithMocks:
|
||||
# Verify widgets were disabled for cover
|
||||
dialog.width_spinbox.setEnabled.assert_called_with(False)
|
||||
dialog.height_spinbox.setEnabled.assert_called_with(False)
|
||||
dialog.set_default_checkbox.setEnabled.assert_called_with(False)
|
||||
dialog.scope_non_manual.setEnabled.assert_called_with(False)
|
||||
dialog.scope_all_pages.setEnabled.assert_called_with(False)
|
||||
|
||||
# Note: Additional widget state tests are covered in test_page_setup_dialog.py
|
||||
# using qtbot which properly handles Qt widget initialization
|
||||
|
||||
@@ -965,6 +965,222 @@ def test_pdf_exporter_image_downsampling():
|
||||
os.remove(img_path)
|
||||
|
||||
|
||||
def test_pdf_exporter_bleed_expands_page_size():
|
||||
"""Test that setting page_bleed_mm expands the PDF page dimensions"""
|
||||
import pdfplumber
|
||||
|
||||
MM_TO_POINTS = 2.834645669
|
||||
page_width_mm = 210.0
|
||||
page_height_mm = 297.0
|
||||
bleed_mm = 3.0
|
||||
|
||||
# Export WITHOUT bleed
|
||||
project_no_bleed = Project("No Bleed")
|
||||
project_no_bleed.page_size_mm = (page_width_mm, page_height_mm)
|
||||
project_no_bleed.page_bleed_mm = 0.0
|
||||
page_no_bleed = Page(page_number=1, is_double_spread=False)
|
||||
project_no_bleed.add_page(page_no_bleed)
|
||||
|
||||
# Export WITH bleed
|
||||
project_bleed = Project("With Bleed")
|
||||
project_bleed.page_size_mm = (page_width_mm, page_height_mm)
|
||||
project_bleed.page_bleed_mm = bleed_mm
|
||||
page_bleed = Page(page_number=1, is_double_spread=False)
|
||||
project_bleed.add_page(page_bleed)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f1:
|
||||
path_no_bleed = f1.name
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f2:
|
||||
path_bleed = f2.name
|
||||
|
||||
try:
|
||||
exporter_no_bleed = PDFExporter(project_no_bleed)
|
||||
success, _ = exporter_no_bleed.export(path_no_bleed)
|
||||
assert success
|
||||
|
||||
exporter_bleed = PDFExporter(project_bleed)
|
||||
success, _ = exporter_bleed.export(path_bleed)
|
||||
assert success
|
||||
|
||||
with pdfplumber.open(path_no_bleed) as pdf:
|
||||
w_no_bleed = pdf.pages[0].width
|
||||
h_no_bleed = pdf.pages[0].height
|
||||
|
||||
with pdfplumber.open(path_bleed) as pdf:
|
||||
w_bleed = pdf.pages[0].width
|
||||
h_bleed = pdf.pages[0].height
|
||||
|
||||
bleed_pt = bleed_mm * MM_TO_POINTS
|
||||
expected_w = page_width_mm * MM_TO_POINTS + 2 * bleed_pt
|
||||
expected_h = page_height_mm * MM_TO_POINTS + 2 * bleed_pt
|
||||
|
||||
assert abs(w_no_bleed - page_width_mm * MM_TO_POINTS) < 1.0, (
|
||||
f"No-bleed page width mismatch: {w_no_bleed:.2f} vs {page_width_mm * MM_TO_POINTS:.2f}"
|
||||
)
|
||||
assert abs(w_bleed - expected_w) < 1.0, (
|
||||
f"Bleed page width mismatch: {w_bleed:.2f} vs {expected_w:.2f}"
|
||||
)
|
||||
assert abs(h_bleed - expected_h) < 1.0, (
|
||||
f"Bleed page height mismatch: {h_bleed:.2f} vs {expected_h:.2f}"
|
||||
)
|
||||
assert w_bleed > w_no_bleed, "Bleed page should be wider than non-bleed page"
|
||||
assert h_bleed > h_no_bleed, "Bleed page should be taller than non-bleed page"
|
||||
|
||||
finally:
|
||||
if os.path.exists(path_no_bleed):
|
||||
os.remove(path_no_bleed)
|
||||
if os.path.exists(path_bleed):
|
||||
os.remove(path_bleed)
|
||||
|
||||
|
||||
def test_pdf_exporter_spread_bleed_expands_and_offsets():
|
||||
"""Test that double spread pages each get bleed on all 4 sides, same as single pages"""
|
||||
import pdfplumber
|
||||
from PIL import Image as PILImage
|
||||
|
||||
MM_TO_POINTS = 2.834645669
|
||||
page_width_mm = 210.0
|
||||
page_height_mm = 297.0
|
||||
bleed_mm = 3.0
|
||||
bleed_pt = bleed_mm * MM_TO_POINTS
|
||||
dpi = 96
|
||||
|
||||
img = PILImage.new("RGB", (200, 200), color="red")
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as img_f:
|
||||
img_path = img_f.name
|
||||
img.save(img_path)
|
||||
|
||||
try:
|
||||
project = Project("Spread Bleed")
|
||||
project.page_size_mm = (page_width_mm, page_height_mm)
|
||||
project.working_dpi = dpi
|
||||
project.page_bleed_mm = bleed_mm
|
||||
|
||||
spread = Page(page_number=1, is_double_spread=True)
|
||||
page_w_px = page_width_mm * dpi / 25.4
|
||||
page_h_px = page_height_mm * dpi / 25.4
|
||||
center_px = page_w_px
|
||||
|
||||
# Full-page element on left page (x=0 to center)
|
||||
left_img = ImageData(image_path=img_path, x=0, y=0, width=page_w_px, height=page_h_px)
|
||||
# Full-page element on right page (x=center to 2*center)
|
||||
right_img = ImageData(image_path=img_path, x=center_px, y=0, width=page_w_px, height=page_h_px)
|
||||
spread.layout.add_element(left_img)
|
||||
spread.layout.add_element(right_img)
|
||||
project.add_page(spread)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as pdf_f:
|
||||
pdf_path = pdf_f.name
|
||||
|
||||
try:
|
||||
exporter = PDFExporter(project)
|
||||
success, warnings = exporter.export(pdf_path)
|
||||
assert success, f"Export failed: {warnings}"
|
||||
|
||||
expected_page_w = page_width_mm * MM_TO_POINTS + 2 * bleed_pt
|
||||
expected_page_h = page_height_mm * MM_TO_POINTS + 2 * bleed_pt
|
||||
expected_img_x1 = page_width_mm * MM_TO_POINTS + bleed_pt
|
||||
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
spread_pages = [pg for pg in pdf.pages if pg.images]
|
||||
assert len(spread_pages) == 2, f"Expected 2 pages with images, got {len(spread_pages)}"
|
||||
|
||||
for pg in spread_pages:
|
||||
# Page size includes bleed on all sides
|
||||
assert abs(pg.width - expected_page_w) < 1.0, (
|
||||
f"Page width {pg.width:.2f} != expected {expected_page_w:.2f}"
|
||||
)
|
||||
assert abs(pg.height - expected_page_h) < 1.0, (
|
||||
f"Page height {pg.height:.2f} != expected {expected_page_h:.2f}"
|
||||
)
|
||||
# Image starts at bleed_pt from left (3mm inner padding)
|
||||
img_obj = pg.images[0]
|
||||
assert abs(img_obj["x0"] - bleed_pt) < 1.0, (
|
||||
f"Image x0={img_obj['x0']:.2f} != bleed_pt={bleed_pt:.2f}"
|
||||
)
|
||||
# Image ends bleed_pt from right edge (3mm outer padding)
|
||||
assert abs(img_obj["x1"] - expected_img_x1) < 1.0, (
|
||||
f"Image x1={img_obj['x1']:.2f} != {expected_img_x1:.2f}"
|
||||
)
|
||||
|
||||
finally:
|
||||
if os.path.exists(pdf_path):
|
||||
os.remove(pdf_path)
|
||||
finally:
|
||||
if os.path.exists(img_path):
|
||||
os.remove(img_path)
|
||||
|
||||
|
||||
def test_pdf_exporter_bleed_offsets_content():
|
||||
"""Test that bleed shifts image content by bleed_pt so it appears at correct position"""
|
||||
import pdfplumber
|
||||
from PIL import Image as PILImage
|
||||
|
||||
MM_TO_POINTS = 2.834645669
|
||||
page_width_mm = 210.0
|
||||
page_height_mm = 297.0
|
||||
bleed_mm = 3.0
|
||||
bleed_pt = bleed_mm * MM_TO_POINTS
|
||||
|
||||
# Create a solid-color test image
|
||||
test_img = PILImage.new("RGB", (200, 200), color="red")
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as img_f:
|
||||
img_path = img_f.name
|
||||
test_img.save(img_path)
|
||||
|
||||
try:
|
||||
# Place image at page position (50mm, 50mm)
|
||||
element_x_mm = 50.0
|
||||
element_y_mm = 50.0
|
||||
element_w_mm = 40.0
|
||||
element_h_mm = 40.0
|
||||
dpi = 96
|
||||
element_x_px = element_x_mm * dpi / 25.4
|
||||
element_y_px = element_y_mm * dpi / 25.4
|
||||
element_w_px = element_w_mm * dpi / 25.4
|
||||
element_h_px = element_h_mm * dpi / 25.4
|
||||
|
||||
project = Project("Bleed Offset")
|
||||
project.page_size_mm = (page_width_mm, page_height_mm)
|
||||
project.working_dpi = dpi
|
||||
project.page_bleed_mm = bleed_mm
|
||||
|
||||
page = Page(page_number=1, is_double_spread=False)
|
||||
image = ImageData(image_path=img_path, x=element_x_px, y=element_y_px,
|
||||
width=element_w_px, height=element_h_px)
|
||||
page.layout.add_element(image)
|
||||
project.add_page(page)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as pdf_f:
|
||||
pdf_path = pdf_f.name
|
||||
|
||||
try:
|
||||
exporter = PDFExporter(project)
|
||||
success, warnings = exporter.export(pdf_path)
|
||||
assert success, f"Export failed: {warnings}"
|
||||
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
pg = pdf.pages[0]
|
||||
images = pg.images
|
||||
assert len(images) > 0, "No images found in PDF"
|
||||
|
||||
img_obj = images[0]
|
||||
# x0 should be element_x_pt + bleed_pt from left edge of expanded page
|
||||
expected_x0 = element_x_mm * MM_TO_POINTS + bleed_pt
|
||||
actual_x0 = img_obj["x0"]
|
||||
assert abs(actual_x0 - expected_x0) < 2.0, (
|
||||
f"Image X offset mismatch: expected {expected_x0:.2f}, got {actual_x0:.2f}"
|
||||
)
|
||||
|
||||
finally:
|
||||
if os.path.exists(pdf_path):
|
||||
os.remove(pdf_path)
|
||||
|
||||
finally:
|
||||
if os.path.exists(img_path):
|
||||
os.remove(img_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running PDF export tests...\n")
|
||||
|
||||
@@ -984,6 +1200,8 @@ if __name__ == "__main__":
|
||||
test_pdf_exporter_varying_aspect_ratios()
|
||||
test_pdf_exporter_rotated_image()
|
||||
test_pdf_exporter_image_downsampling()
|
||||
test_pdf_exporter_bleed_expands_page_size()
|
||||
test_pdf_exporter_bleed_offsets_content()
|
||||
|
||||
print("\n✓ All tests passed!")
|
||||
|
||||
|
||||
@@ -48,6 +48,52 @@ class TestProject:
|
||||
assert project.working_dpi == 300
|
||||
assert project.page_size_mm == (140, 140) # Default 14cm x 14cm square
|
||||
|
||||
def test_print_guide_defaults(self):
|
||||
"""Test that print guide settings have correct defaults"""
|
||||
project = Project()
|
||||
|
||||
assert project.page_bleed_mm == 0.0
|
||||
assert project.page_safe_area_mm == 5.0
|
||||
assert project.show_print_guides is False
|
||||
|
||||
def test_print_guide_serialization_roundtrip(self):
|
||||
"""Test that print guide settings are serialized and deserialized correctly"""
|
||||
project = Project()
|
||||
project.page_bleed_mm = 3.0
|
||||
project.page_safe_area_mm = 8.0
|
||||
project.show_print_guides = True
|
||||
|
||||
data = project.serialize()
|
||||
|
||||
assert data["page_bleed_mm"] == 3.0
|
||||
assert data["page_safe_area_mm"] == 8.0
|
||||
assert data["show_print_guides"] is True
|
||||
|
||||
# Deserialize into a new project
|
||||
restored = Project()
|
||||
restored.deserialize(data)
|
||||
|
||||
assert restored.page_bleed_mm == 3.0
|
||||
assert restored.page_safe_area_mm == 8.0
|
||||
assert restored.show_print_guides is True
|
||||
|
||||
def test_print_guide_deserialization_defaults(self):
|
||||
"""Test that missing print guide fields in old data fall back to defaults"""
|
||||
project = Project()
|
||||
data = project.serialize()
|
||||
|
||||
# Simulate old project file without these keys
|
||||
del data["page_bleed_mm"]
|
||||
del data["page_safe_area_mm"]
|
||||
del data["show_print_guides"]
|
||||
|
||||
restored = Project()
|
||||
restored.deserialize(data)
|
||||
|
||||
assert restored.page_bleed_mm == 0.0
|
||||
assert restored.page_safe_area_mm == 5.0
|
||||
assert restored.show_print_guides is False
|
||||
|
||||
def test_initialization_with_name(self):
|
||||
"""Test Project initialization with custom name"""
|
||||
project = Project(name="My Album")
|
||||
|
||||
@@ -27,6 +27,7 @@ class TestViewWindow(ViewOperationsMixin, QMainWindow):
|
||||
self.project.snap_to_edges = True
|
||||
self.project.snap_to_guides = True
|
||||
self.project.show_snap_lines = True
|
||||
self.project.show_print_guides = False
|
||||
self.project.grid_size_mm = 10.0
|
||||
self.project.snap_threshold_mm = 5.0
|
||||
self._update_view_called = False
|
||||
@@ -125,6 +126,44 @@ class TestZoomOperations:
|
||||
assert not window._update_view_called
|
||||
|
||||
|
||||
class TestPrintGuidesToggle:
|
||||
"""Test print guides toggle operation"""
|
||||
|
||||
def test_toggle_print_guides_show(self, qtbot):
|
||||
window = TestViewWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
window.project.show_print_guides = False
|
||||
|
||||
window.toggle_print_guides()
|
||||
|
||||
assert window.project.show_print_guides is True
|
||||
assert "visible" in window._status_message.lower()
|
||||
assert window._update_view_called
|
||||
|
||||
def test_toggle_print_guides_hide(self, qtbot):
|
||||
window = TestViewWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
window.project.show_print_guides = True
|
||||
|
||||
window.toggle_print_guides()
|
||||
|
||||
assert window.project.show_print_guides is False
|
||||
assert "hidden" in window._status_message.lower()
|
||||
assert window._update_view_called
|
||||
|
||||
def test_toggle_print_guides_no_project(self, qtbot):
|
||||
window = TestViewWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
window.project = None
|
||||
|
||||
window.toggle_print_guides()
|
||||
|
||||
assert not window._update_view_called
|
||||
|
||||
|
||||
class TestSnappingToggles:
|
||||
"""Test snapping toggle operations"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user