Add cover page settings
This commit is contained in:
parent
4bfaa63aae
commit
aa02506d4c
@ -994,9 +994,6 @@ class GLWidget(UndoableInteractionMixin, QOpenGLWidget):
|
||||
if not hasattr(main_window, 'project'):
|
||||
return []
|
||||
|
||||
# Get page layout with ghosts from project
|
||||
layout_with_ghosts = main_window.project.calculate_page_layout_with_ghosts()
|
||||
|
||||
dpi = main_window.project.working_dpi
|
||||
|
||||
# Use project's page_spacing_mm setting (default is 10mm = 1cm)
|
||||
@ -1011,6 +1008,22 @@ class GLWidget(UndoableInteractionMixin, QOpenGLWidget):
|
||||
result = []
|
||||
current_y = top_margin_px # Initial top offset in pixels (not screen pixels)
|
||||
|
||||
# First, render cover if it exists
|
||||
for page in main_window.project.pages:
|
||||
if page.is_cover:
|
||||
result.append(('page', page, current_y))
|
||||
|
||||
# Calculate cover height in pixels
|
||||
page_height_mm = page.layout.size[1]
|
||||
page_height_px = page_height_mm * dpi / 25.4
|
||||
|
||||
# Move to next position (add height + spacing)
|
||||
current_y += page_height_px + spacing_px
|
||||
break # Only one cover allowed
|
||||
|
||||
# Get page layout with ghosts from project (this excludes cover)
|
||||
layout_with_ghosts = main_window.project.calculate_page_layout_with_ghosts()
|
||||
|
||||
for page_type, page_obj, logical_pos in layout_with_ghosts:
|
||||
if page_type == 'page':
|
||||
# Regular page (single or double spread)
|
||||
|
||||
@ -62,9 +62,10 @@ class PageOperationsMixin:
|
||||
|
||||
page_combo = QComboBox()
|
||||
for i, page in enumerate(self.project.pages):
|
||||
page_label = f"Page {page.page_number}"
|
||||
if page.is_double_spread:
|
||||
page_label += f" (Double Spread: {page.page_number}-{page.page_number + 1})"
|
||||
# Use display name helper
|
||||
page_label = self.project.get_page_display_name(page)
|
||||
if page.is_double_spread and not page.is_cover:
|
||||
page_label += f" (Double Spread)"
|
||||
if page.manually_sized:
|
||||
page_label += " *"
|
||||
page_combo.addItem(page_label, i)
|
||||
@ -78,6 +79,48 @@ class PageOperationsMixin:
|
||||
page_select_group.setLayout(page_select_layout)
|
||||
layout.addWidget(page_select_group)
|
||||
|
||||
# Cover settings group (only show if first page is selected)
|
||||
cover_group = QGroupBox("Cover Settings")
|
||||
cover_layout = QVBoxLayout()
|
||||
|
||||
# Cover checkbox
|
||||
cover_checkbox = QCheckBox("Designate as Cover")
|
||||
cover_checkbox.setToolTip("Mark this page as the book cover with wrap-around front/spine/back")
|
||||
cover_layout.addWidget(cover_checkbox)
|
||||
|
||||
# Paper thickness
|
||||
thickness_layout = QHBoxLayout()
|
||||
thickness_layout.addWidget(QLabel("Paper Thickness:"))
|
||||
thickness_spinbox = QDoubleSpinBox()
|
||||
thickness_spinbox.setRange(0.05, 1.0)
|
||||
thickness_spinbox.setSingleStep(0.05)
|
||||
thickness_spinbox.setValue(self.project.paper_thickness_mm)
|
||||
thickness_spinbox.setSuffix(" mm")
|
||||
thickness_spinbox.setToolTip("Thickness of paper for spine calculation")
|
||||
thickness_layout.addWidget(thickness_spinbox)
|
||||
cover_layout.addLayout(thickness_layout)
|
||||
|
||||
# Bleed margin
|
||||
bleed_layout = QHBoxLayout()
|
||||
bleed_layout.addWidget(QLabel("Bleed Margin:"))
|
||||
bleed_spinbox = QDoubleSpinBox()
|
||||
bleed_spinbox.setRange(0, 10)
|
||||
bleed_spinbox.setSingleStep(0.5)
|
||||
bleed_spinbox.setValue(self.project.cover_bleed_mm)
|
||||
bleed_spinbox.setSuffix(" mm")
|
||||
bleed_spinbox.setToolTip("Extra margin around cover for printing bleed")
|
||||
bleed_layout.addWidget(bleed_spinbox)
|
||||
cover_layout.addLayout(bleed_layout)
|
||||
|
||||
# Calculated spine width display
|
||||
spine_info_label = QLabel()
|
||||
spine_info_label.setStyleSheet("font-size: 9pt; color: #0066cc; padding: 5px;")
|
||||
spine_info_label.setWordWrap(True)
|
||||
cover_layout.addWidget(spine_info_label)
|
||||
|
||||
cover_group.setLayout(cover_layout)
|
||||
layout.addWidget(cover_group)
|
||||
|
||||
# Page size group
|
||||
size_group = QGroupBox("Page Size")
|
||||
size_layout = QVBoxLayout()
|
||||
@ -136,8 +179,21 @@ class PageOperationsMixin:
|
||||
# Function to update displayed values when page selection changes
|
||||
def on_page_changed(index):
|
||||
selected_page = self.project.pages[index]
|
||||
# Get base width (accounting for double spreads)
|
||||
if selected_page.is_double_spread:
|
||||
|
||||
# Show/hide cover settings based on page selection
|
||||
is_first_page = (index == 0)
|
||||
cover_group.setVisible(is_first_page)
|
||||
|
||||
# Update cover checkbox
|
||||
if is_first_page:
|
||||
cover_checkbox.setChecked(selected_page.is_cover)
|
||||
update_spine_info()
|
||||
|
||||
# Get base width (accounting for double spreads and covers)
|
||||
if selected_page.is_cover:
|
||||
# For covers, show the full calculated width
|
||||
display_width = selected_page.layout.size[0]
|
||||
elif selected_page.is_double_spread:
|
||||
display_width = selected_page.layout.base_width if hasattr(selected_page.layout, 'base_width') else selected_page.layout.size[0] / 2
|
||||
else:
|
||||
display_width = selected_page.layout.size[0]
|
||||
@ -145,6 +201,41 @@ class PageOperationsMixin:
|
||||
width_spinbox.setValue(display_width)
|
||||
height_spinbox.setValue(selected_page.layout.size[1])
|
||||
|
||||
# Disable size editing for covers (auto-calculated)
|
||||
if selected_page.is_cover:
|
||||
width_spinbox.setEnabled(False)
|
||||
height_spinbox.setEnabled(False)
|
||||
set_default_checkbox.setEnabled(False)
|
||||
else:
|
||||
width_spinbox.setEnabled(True)
|
||||
height_spinbox.setEnabled(True)
|
||||
set_default_checkbox.setEnabled(True)
|
||||
|
||||
def update_spine_info():
|
||||
"""Update the spine information display"""
|
||||
if cover_checkbox.isChecked():
|
||||
# Calculate spine width with current settings
|
||||
content_pages = sum(p.get_page_count() for p in self.project.pages if not p.is_cover)
|
||||
import math
|
||||
sheets = math.ceil(content_pages / 4)
|
||||
spine_width = sheets * thickness_spinbox.value() * 2
|
||||
|
||||
page_width = self.project.page_size_mm[0]
|
||||
total_width = (page_width * 2) + spine_width + (bleed_spinbox.value() * 2)
|
||||
|
||||
spine_info_label.setText(
|
||||
f"Cover Layout: Front ({page_width:.0f}mm) + Spine ({spine_width:.2f}mm) + "
|
||||
f"Back ({page_width:.0f}mm) + Bleed ({bleed_spinbox.value():.1f}mm × 2)\n"
|
||||
f"Total Width: {total_width:.1f}mm | Content Pages: {content_pages} | Sheets: {sheets}"
|
||||
)
|
||||
else:
|
||||
spine_info_label.setText("")
|
||||
|
||||
# Connect signals
|
||||
cover_checkbox.stateChanged.connect(lambda: update_spine_info())
|
||||
thickness_spinbox.valueChanged.connect(lambda: update_spine_info())
|
||||
bleed_spinbox.valueChanged.connect(lambda: update_spine_info())
|
||||
|
||||
# Connect page selection change
|
||||
page_combo.currentIndexChanged.connect(on_page_changed)
|
||||
|
||||
@ -172,10 +263,34 @@ class PageOperationsMixin:
|
||||
selected_index = page_combo.currentData()
|
||||
selected_page = self.project.pages[selected_index]
|
||||
|
||||
# Update project cover settings
|
||||
self.project.paper_thickness_mm = thickness_spinbox.value()
|
||||
self.project.cover_bleed_mm = bleed_spinbox.value()
|
||||
|
||||
# Handle cover designation (only for first page)
|
||||
if selected_index == 0:
|
||||
was_cover = selected_page.is_cover
|
||||
is_cover = cover_checkbox.isChecked()
|
||||
|
||||
if was_cover != is_cover:
|
||||
selected_page.is_cover = is_cover
|
||||
self.project.has_cover = is_cover
|
||||
|
||||
if is_cover:
|
||||
# Calculate and set cover dimensions
|
||||
self.project.update_cover_dimensions()
|
||||
print(f"Page 1 designated as cover")
|
||||
else:
|
||||
# Restore normal page size
|
||||
selected_page.layout.size = self.project.page_size_mm
|
||||
print(f"Cover removed from page 1")
|
||||
|
||||
# Get new values
|
||||
width_mm = width_spinbox.value()
|
||||
height_mm = height_spinbox.value()
|
||||
|
||||
# Don't allow manual size changes for covers
|
||||
if not selected_page.is_cover:
|
||||
# Check if size actually changed
|
||||
# For double spreads, compare with base width
|
||||
if selected_page.is_double_spread:
|
||||
@ -188,7 +303,7 @@ class PageOperationsMixin:
|
||||
selected_page.layout.base_width = width_mm
|
||||
selected_page.layout.size = (width_mm * 2, height_mm)
|
||||
selected_page.manually_sized = True
|
||||
print(f"Page {selected_page.page_number} (double spread) updated to {width_mm}×{height_mm} mm per page")
|
||||
print(f"{self.project.get_page_display_name(selected_page)} (double spread) updated to {width_mm}×{height_mm} mm per page")
|
||||
else:
|
||||
old_size = selected_page.layout.size
|
||||
size_changed = (old_size != (width_mm, height_mm))
|
||||
@ -198,7 +313,7 @@ class PageOperationsMixin:
|
||||
selected_page.layout.size = (width_mm, height_mm)
|
||||
selected_page.layout.base_width = width_mm
|
||||
selected_page.manually_sized = True
|
||||
print(f"Page {selected_page.page_number} updated to {width_mm}×{height_mm} mm")
|
||||
print(f"{self.project.get_page_display_name(selected_page)} updated to {width_mm}×{height_mm} mm")
|
||||
|
||||
# Update DPI settings
|
||||
self.project.working_dpi = working_dpi_spinbox.value()
|
||||
@ -212,7 +327,11 @@ class PageOperationsMixin:
|
||||
self.update_view()
|
||||
|
||||
# Build status message
|
||||
status_msg = f"Page {selected_page.page_number} size: {width_mm}×{height_mm} mm"
|
||||
page_name = self.project.get_page_display_name(selected_page)
|
||||
if selected_page.is_cover:
|
||||
status_msg = f"{page_name} updated"
|
||||
else:
|
||||
status_msg = f"{page_name} size: {width_mm}×{height_mm} mm"
|
||||
if set_default_checkbox.isChecked():
|
||||
status_msg += " (set as default)"
|
||||
self.show_status(status_msg, 2000)
|
||||
|
||||
@ -48,8 +48,11 @@ class PDFExporter:
|
||||
self.current_pdf_page = 1
|
||||
|
||||
try:
|
||||
# Calculate total pages for progress
|
||||
total_pages = sum(2 if page.is_double_spread else 1 for page in self.project.pages)
|
||||
# Calculate total pages for progress (cover counts as 1)
|
||||
total_pages = sum(
|
||||
1 if page.is_cover else (2 if page.is_double_spread else 1)
|
||||
for page in self.project.pages
|
||||
)
|
||||
|
||||
# Get page dimensions from project (in mm)
|
||||
page_width_mm, page_height_mm = self.project.page_size_mm
|
||||
@ -64,11 +67,18 @@ class PDFExporter:
|
||||
# Process each page
|
||||
pages_processed = 0
|
||||
for page in self.project.pages:
|
||||
# Get display name for progress
|
||||
page_name = self.project.get_page_display_name(page)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(pages_processed, total_pages,
|
||||
f"Exporting page {page.page_number}...")
|
||||
f"Exporting {page_name}...")
|
||||
|
||||
if page.is_double_spread:
|
||||
if page.is_cover:
|
||||
# Export cover as single page with wrap-around design
|
||||
self._export_cover(c, page, page_width_pt, page_height_pt)
|
||||
pages_processed += 1
|
||||
elif page.is_double_spread:
|
||||
# Ensure spread starts on even page (left page of facing pair)
|
||||
if self.current_pdf_page % 2 == 1:
|
||||
# Insert blank page
|
||||
@ -98,6 +108,70 @@ class PDFExporter:
|
||||
self.warnings.append(f"Export failed: {str(e)}")
|
||||
return False, self.warnings
|
||||
|
||||
def _export_cover(self, c: canvas.Canvas, page, page_width_pt: float,
|
||||
page_height_pt: float):
|
||||
"""
|
||||
Export a cover page to PDF.
|
||||
Cover has different dimensions (wrap-around: front + spine + back + bleed).
|
||||
"""
|
||||
# Get cover dimensions (already calculated in page.layout.size)
|
||||
cover_width_mm, cover_height_mm = page.layout.size
|
||||
|
||||
# Convert to PDF points
|
||||
cover_width_pt = cover_width_mm * self.MM_TO_POINTS
|
||||
cover_height_pt = cover_height_mm * self.MM_TO_POINTS
|
||||
|
||||
# Create a new page with cover dimensions
|
||||
c.setPageSize((cover_width_pt, cover_height_pt))
|
||||
|
||||
# Render all elements on the cover
|
||||
for element in sorted(page.layout.elements, key=lambda x: x.z_index):
|
||||
self._render_element(c, element, 0, cover_width_pt, cover_height_pt, "Cover")
|
||||
|
||||
# Draw guide lines for front/spine/back zones
|
||||
self._draw_cover_guides(c, cover_width_pt, cover_height_pt)
|
||||
|
||||
c.showPage() # Finish cover page
|
||||
self.current_pdf_page += 1
|
||||
|
||||
# Reset page size for content pages
|
||||
c.setPageSize((page_width_pt, page_height_pt))
|
||||
|
||||
def _draw_cover_guides(self, c: canvas.Canvas, cover_width_pt: float, cover_height_pt: float):
|
||||
"""Draw guide lines for cover zones (front/spine/back)"""
|
||||
from reportlab.lib.colors import lightgrey
|
||||
|
||||
# Calculate zone boundaries
|
||||
bleed_pt = self.project.cover_bleed_mm * self.MM_TO_POINTS
|
||||
page_width_pt = self.project.page_size_mm[0] * self.MM_TO_POINTS
|
||||
spine_width_pt = self.project.calculate_spine_width() * self.MM_TO_POINTS
|
||||
|
||||
# Zone boundaries (from left to right)
|
||||
# Bleed | Back | Spine | Front | Bleed
|
||||
back_start = bleed_pt
|
||||
spine_start = bleed_pt + page_width_pt
|
||||
front_start = bleed_pt + page_width_pt + spine_width_pt
|
||||
front_end = bleed_pt + page_width_pt + spine_width_pt + page_width_pt
|
||||
|
||||
# Draw dashed lines at zone boundaries
|
||||
c.saveState()
|
||||
c.setStrokeColor(lightgrey)
|
||||
c.setDash(3, 3)
|
||||
c.setLineWidth(0.5)
|
||||
|
||||
# Back/Spine boundary
|
||||
c.line(spine_start, 0, spine_start, cover_height_pt)
|
||||
|
||||
# Spine/Front boundary
|
||||
c.line(front_start, 0, front_start, cover_height_pt)
|
||||
|
||||
# Bleed boundaries (outer edges)
|
||||
if bleed_pt > 0:
|
||||
c.line(back_start, 0, back_start, cover_height_pt)
|
||||
c.line(front_end, 0, front_end, cover_height_pt)
|
||||
|
||||
c.restoreState()
|
||||
|
||||
def _export_single_page(self, c: canvas.Canvas, page, page_width_pt: float,
|
||||
page_height_pt: float):
|
||||
"""Export a single page to PDF"""
|
||||
|
||||
@ -3,6 +3,7 @@ Project and page management for pyPhotoAlbum
|
||||
"""
|
||||
|
||||
import os
|
||||
import math
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pyPhotoAlbum.page_layout import PageLayout
|
||||
from pyPhotoAlbum.commands import CommandHistory
|
||||
@ -109,6 +110,12 @@ class Project:
|
||||
self.export_dpi = 300 # Default export DPI
|
||||
self.page_spacing_mm = 10.0 # Default spacing between pages (1cm)
|
||||
|
||||
# Cover configuration
|
||||
self.has_cover = False # Whether project has a cover
|
||||
self.paper_thickness_mm = 0.2 # Paper thickness for spine calculation (default 0.2mm)
|
||||
self.cover_bleed_mm = 0.0 # Bleed margin for cover (default 0mm)
|
||||
self.binding_type = "saddle_stitch" # Binding type for spine calculation
|
||||
|
||||
# Embedded templates - templates that travel with the project
|
||||
self.embedded_templates: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
@ -122,14 +129,113 @@ class Project:
|
||||
def add_page(self, page: Page):
|
||||
"""Add a page to the project"""
|
||||
self.pages.append(page)
|
||||
# Update cover dimensions if we have a cover
|
||||
if self.has_cover and self.pages:
|
||||
self.update_cover_dimensions()
|
||||
|
||||
def remove_page(self, page: Page):
|
||||
"""Remove a page from the project"""
|
||||
self.pages.remove(page)
|
||||
# Update cover dimensions if we have a cover
|
||||
if self.has_cover and self.pages:
|
||||
self.update_cover_dimensions()
|
||||
|
||||
def calculate_spine_width(self) -> float:
|
||||
"""
|
||||
Calculate spine width based on page count and paper thickness.
|
||||
|
||||
For saddle stitch binding:
|
||||
- Each sheet = 4 pages (2 pages per side when folded)
|
||||
- Spine width = (Number of sheets × Paper thickness × 2)
|
||||
|
||||
Returns:
|
||||
Spine width in mm
|
||||
"""
|
||||
if not self.has_cover:
|
||||
return 0.0
|
||||
|
||||
# Count content pages (excluding cover)
|
||||
content_page_count = sum(
|
||||
page.get_page_count()
|
||||
for page in self.pages
|
||||
if not page.is_cover
|
||||
)
|
||||
|
||||
if self.binding_type == "saddle_stitch":
|
||||
# Calculate number of sheets (each sheet = 4 pages)
|
||||
sheets = math.ceil(content_page_count / 4)
|
||||
# Spine width = sheets × paper thickness × 2 (folded)
|
||||
spine_width = sheets * self.paper_thickness_mm * 2
|
||||
return spine_width
|
||||
|
||||
return 0.0
|
||||
|
||||
def update_cover_dimensions(self):
|
||||
"""
|
||||
Update cover page dimensions based on current page count and settings.
|
||||
Calculates: Front width + Spine width + Back width + Bleed margins
|
||||
"""
|
||||
if not self.has_cover or not self.pages:
|
||||
return
|
||||
|
||||
# Find cover page (should be first page)
|
||||
cover_page = None
|
||||
for page in self.pages:
|
||||
if page.is_cover:
|
||||
cover_page = page
|
||||
break
|
||||
|
||||
if not cover_page:
|
||||
return
|
||||
|
||||
# Get standard page dimensions
|
||||
page_width_mm, page_height_mm = self.page_size_mm
|
||||
|
||||
# Calculate spine width
|
||||
spine_width = self.calculate_spine_width()
|
||||
|
||||
# Calculate cover dimensions
|
||||
# Cover = Front + Spine + Back + Bleed on all sides
|
||||
cover_width = (page_width_mm * 2) + spine_width + (self.cover_bleed_mm * 2)
|
||||
cover_height = page_height_mm + (self.cover_bleed_mm * 2)
|
||||
|
||||
# Update cover page layout
|
||||
cover_page.layout.size = (cover_width, cover_height)
|
||||
cover_page.layout.base_width = page_width_mm # Store base width for reference
|
||||
cover_page.manually_sized = True # Mark as manually sized
|
||||
|
||||
print(f"Cover dimensions updated: {cover_width:.1f} × {cover_height:.1f} mm "
|
||||
f"(Front: {page_width_mm}, Spine: {spine_width:.2f}, Back: {page_width_mm}, "
|
||||
f"Bleed: {self.cover_bleed_mm})")
|
||||
|
||||
def get_page_display_name(self, page: Page) -> str:
|
||||
"""
|
||||
Get display name for a page.
|
||||
|
||||
Args:
|
||||
page: The page to get the display name for
|
||||
|
||||
Returns:
|
||||
Display name like "Cover", "Page 1", "Pages 1-2", etc.
|
||||
"""
|
||||
if page.is_cover:
|
||||
return "Cover"
|
||||
|
||||
# Calculate adjusted page number (excluding cover from count)
|
||||
adjusted_num = page.page_number
|
||||
if self.has_cover:
|
||||
# Subtract 1 to account for cover
|
||||
adjusted_num = page.page_number - 1
|
||||
|
||||
if page.is_double_spread:
|
||||
return f"Pages {adjusted_num}-{adjusted_num + 1}"
|
||||
else:
|
||||
return f"Page {adjusted_num}"
|
||||
|
||||
def calculate_page_layout_with_ghosts(self) -> List[Tuple[str, Any, int]]:
|
||||
"""
|
||||
Calculate page layout including ghost pages for alignment.
|
||||
Excludes cover from spread calculations.
|
||||
|
||||
Returns:
|
||||
List of tuples (page_type, page_or_ghost, logical_position)
|
||||
@ -143,6 +249,10 @@ class Project:
|
||||
current_position = 1 # Start at position 1 (right page)
|
||||
|
||||
for page in self.pages:
|
||||
# Skip cover in spread calculations
|
||||
if page.is_cover:
|
||||
# Cover is rendered separately, doesn't participate in spreads
|
||||
continue
|
||||
# Check if we need a ghost page for alignment
|
||||
# Ghost pages are needed when a single page would appear on the left
|
||||
# but should be on the right (odd positions)
|
||||
@ -198,6 +308,10 @@ class Project:
|
||||
"working_dpi": self.working_dpi,
|
||||
"export_dpi": self.export_dpi,
|
||||
"page_spacing_mm": self.page_spacing_mm,
|
||||
"has_cover": self.has_cover,
|
||||
"paper_thickness_mm": self.paper_thickness_mm,
|
||||
"cover_bleed_mm": self.cover_bleed_mm,
|
||||
"binding_type": self.binding_type,
|
||||
"embedded_templates": self.embedded_templates,
|
||||
"pages": [page.serialize() for page in self.pages],
|
||||
"history": self.history.serialize(),
|
||||
@ -215,6 +329,10 @@ class Project:
|
||||
self.working_dpi = data.get("working_dpi", 300)
|
||||
self.export_dpi = data.get("export_dpi", 300)
|
||||
self.page_spacing_mm = data.get("page_spacing_mm", 10.0)
|
||||
self.has_cover = data.get("has_cover", False)
|
||||
self.paper_thickness_mm = data.get("paper_thickness_mm", 0.2)
|
||||
self.cover_bleed_mm = data.get("cover_bleed_mm", 0.0)
|
||||
self.binding_type = data.get("binding_type", "saddle_stitch")
|
||||
|
||||
# Deserialize embedded templates
|
||||
self.embedded_templates = data.get("embedded_templates", {})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user