Update trim dimensions and cover export functionality in Blurb Photo Book Editor

This commit is contained in:
2026-10-07 17:14:49 -04:00
parent 52a497e972
commit f82c3320c4
4 changed files with 198 additions and 54 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ Maintain a Windows desktop photo-book editor that imports a photo folder, arrang
- Spine title and subtitle vertical positions are saved in inches from the bottom of the face. The title moves within the full spine; the subtitle stays in the bottom subtitle area. Resetting spine text clears custom positions and restores the centered defaults. - Spine title and subtitle vertical positions are saved in inches from the bottom of the face. The title moves within the full spine; the subtitle stays in the bottom subtitle area. Resetting spine text clears custom positions and restores the centered defaults.
- **View Tagged** includes tagged photo pages and is disabled when there are no tagged photos. Page insertion and drag reordering are available in **View All** only. - **View Tagged** includes tagged photo pages and is disabled when there are no tagged photos. Page insertion and drag reordering are available in **View All** only.
- Folder import scanning, project loading, and thumbnail preparation run in background workers. Keep image decoding and preview work off the UI thread. Create or modify `QPixmap` objects only on the GUI thread; worker results should use thread-safe image data such as `QImage`. Keep progress dialogs and worker lifetime/error handling in sync. - Folder import scanning, project loading, and thumbnail preparation run in background workers. Keep image decoding and preview work off the UI thread. Create or modify `QPixmap` objects only on the GUI thread; worker results should use thread-safe image data such as `QImage`. Keep progress dialogs and worker lifetime/error handling in sync.
- The default trim area is 9.5 × 8 inches per individual interior page. Final PDF dimensions and trim insets are set separately for the chosen Blurb configuration. Interior export renders at 300 PPI and verifies PDF page dimensions and page count. Configured covers export as a separate one-page full spread with editable face/spine dimensions and 0.125-inch starting values for the wrap/bleed margins; users must verify exact values for their selected Blurb configuration. - The default trim and PDF page size is 9.625 × 8.25 inches per individual interior page, with zero trim insets. Final PDF dimensions and trim insets can be changed for the chosen Blurb configuration. Interior export renders at 300 PPI and verifies PDF page dimensions and page count. Configured covers export as a separate one-page full spread with editable face/spine dimensions, a 0.57-inch default spine, and zero wrap/bleed margins; users must verify exact values for their selected Blurb configuration.
- Do not assume trim dimensions equal final PDF dimensions. Blurb requirements vary by format and paper; consult the current official specifications when changing export guidance. - Do not assume trim dimensions equal final PDF dimensions. Blurb requirements vary by format and paper; consult the current official specifications when changing export guidance.
## Development ## Development
+2 -2
View File
@@ -26,9 +26,9 @@ Photo cards show the date as `mm/dd/yyyy`. Dates come from supported timestamps
## Blurb print setup and export ## Blurb print setup and export
The default trim area is **9.5 × 8 inches per individual page**. In **File → Blurb print setup**, enter the final PDF page width and height and the trim offsets required for the selected Blurb book format, paper, and cover. The exported PDF has one page per book page and includes the configured bleed area. The dashed guide shows the trim area, including alternating binding and outside edges. The default trim and PDF page size is **9.625 × 8.25 inches per individual page**, with zero trim insets. In **File → Blurb print setup**, enter the final PDF page width and height and the trim offsets required for the selected Blurb book format, paper, and cover. The exported PDF has one page per book page and includes the configured bleed area. The dashed guide shows the trim area, including alternating binding and outside edges.
Export renders photo pages at 300 PPI, checks the resulting PDF page count and dimensions, and requires an even page count. It can append blank pages to meet the configured minimum. The default page limits are 20–440; set them to the limits for the selected format and paper. When cover content is assigned, export also creates a one-page full-spread companion named `<book>-cover.pdf` beside the interior PDF. Cover face defaults are 10.074 × 9 inches, with a 0.46-inch spine. Wrap/bleed margins start at 0.125 inches on each edge; these are editable starting values, so verify the exact dimensions and margins for the chosen Blurb format, paper, and page count before exporting. Export renders photo pages at 300 PPI, checks the resulting PDF page count and dimensions, and requires an even page count. It can append blank pages to meet the configured minimum. The default page limits are 20–440; set them to the limits for the selected format and paper. When cover content is assigned, export also creates a one-page full-spread companion named `<book>-cover.pdf` beside the interior PDF. Use **File → Export Cover PDF** to export only the cover. Cover face defaults are 10.074 × 9 inches, with a 0.57-inch spine and zero wrap/bleed margins. Front and spine text defaults to MV Boli; if it is unavailable, the app uses Arial or the system sans-serif font. These are starting values; verify the exact dimensions and margins for the chosen Blurb format, paper, and page count before exporting.
Blurb's requirements vary by book configuration. Check the current [PDF-to-Book specifications](https://support.blurb.com/hc/en-us/articles/207792946-PDF-to-Book-specifications-and-checklist) and [page-count limits](https://support.blurb.com/hc/en-us/articles/7607055286541-How-many-pages-can-my-book-have) before exporting. Blurb's requirements vary by book configuration. Check the current [PDF-to-Book specifications](https://support.blurb.com/hc/en-us/articles/207792946-PDF-to-Book-specifications-and-checklist) and [page-count limits](https://support.blurb.com/hc/en-us/articles/7607055286541-How-many-pages-can-my-book-have) before exporting.
+56 -44
View File
@@ -74,10 +74,10 @@ class PhotoPage:
class PrintProfile: class PrintProfile:
"""Trim dimensions plus exact final PDF dimensions supplied by Blurb.""" """Trim dimensions plus exact final PDF dimensions supplied by Blurb."""
trim_width_in: float = 9.5 trim_width_in: float = 9.625
trim_height_in: float = 8.0 trim_height_in: float = 8.25
pdf_width_in: float | None = None pdf_width_in: float | None = 9.625
pdf_height_in: float | None = None pdf_height_in: float | None = 8.25
trim_binding_in: float = 0.0 trim_binding_in: float = 0.0
trim_outside_in: float = 0.0 trim_outside_in: float = 0.0
trim_top_in: float = 0.0 trim_top_in: float = 0.0
@@ -135,10 +135,18 @@ class PrintProfile:
if not data: if not data:
return cls() return cls()
return cls( return cls(
trim_width_in=_to_float(data.get("trim_width_in"), 9.5), trim_width_in=_to_float(data.get("trim_width_in"), 9.625),
trim_height_in=_to_float(data.get("trim_height_in"), 8.0), trim_height_in=_to_float(data.get("trim_height_in"), 8.25),
pdf_width_in=_optional_float(data.get("pdf_width_in")), pdf_width_in=(
pdf_height_in=_optional_float(data.get("pdf_height_in")), _optional_float(data.get("pdf_width_in"))
if "pdf_width_in" in data
else 9.625
),
pdf_height_in=(
_optional_float(data.get("pdf_height_in"))
if "pdf_height_in" in data
else 8.25
),
trim_binding_in=_to_float( trim_binding_in=_to_float(
data.get("trim_binding_in", data.get("trim_left_in")), 0.0 data.get("trim_binding_in", data.get("trim_left_in")), 0.0
), ),
@@ -147,8 +155,8 @@ class PrintProfile:
"trim_outside_in", "trim_outside_in",
max( max(
0.0, 0.0,
_to_float(data.get("pdf_width_in"), 9.5) _to_float(data.get("pdf_width_in"), 9.625)
- _to_float(data.get("trim_width_in"), 9.5) - _to_float(data.get("trim_width_in"), 9.625)
- _to_float(data.get("trim_left_in"), 0.0), - _to_float(data.get("trim_left_in"), 0.0),
), ),
), ),
@@ -171,28 +179,28 @@ class CoverLayout:
subtitle: str = "" subtitle: str = ""
front_title: str = "" front_title: str = ""
front_subtitle: str = "" front_subtitle: str = ""
front_font_family: str = "Arial" front_font_family: str = "MV Boli"
front_title_font_size_pt: float = 36.0 front_title_font_size_pt: float = 48.0
front_subtitle_font_size_pt: float = 20.0 front_subtitle_font_size_pt: float = 24.0
front_title_x: float = 0.5 front_title_x: float = 0.6511959815448585
front_title_y: float = 0.17 front_title_y: float = 0.22590834010644723
front_subtitle_x: float = 0.5 front_subtitle_x: float = 0.7505046203361379
front_subtitle_y: float = 0.28 front_subtitle_y: float = 0.3083317165255237
title_font_family: str = "Arial" title_font_family: str = "MV Boli"
title_font_size_pt: float = 10.0 title_font_size_pt: float = 20.0
title_color: str = "#fff03f" title_color: str = "#fff03f"
subtitle_font_family: str = "Arial" subtitle_font_family: str = "MV Boli"
subtitle_font_size_pt: float = 8.0 subtitle_font_size_pt: float = 14.0
subtitle_color: str = "#fff03f" subtitle_color: str = "#fff03f"
spine_title_y_in: float | None = None spine_title_y_in: float | None = None
spine_subtitle_y_in: float | None = None spine_subtitle_y_in: float | None = 1.1093844419391201
face_width_in: float = 10.074 face_width_in: float = 10.074
face_height_in: float = 9.0 face_height_in: float = 9.0
spine_width_in: float = 0.46 spine_width_in: float = 0.57
margin_left_in: float | None = 0.125 margin_left_in: float | None = 0.0
margin_right_in: float | None = 0.125 margin_right_in: float | None = 0.0
margin_top_in: float | None = 0.125 margin_top_in: float | None = 0.0
margin_bottom_in: float | None = 0.125 margin_bottom_in: float | None = 0.0
@property @property
def has_content(self) -> bool: def has_content(self) -> bool:
@@ -368,28 +376,32 @@ class CoverLayout:
subtitle=str(data.get("subtitle", "")), subtitle=str(data.get("subtitle", "")),
front_title=str(data.get("front_title", "")), front_title=str(data.get("front_title", "")),
front_subtitle=str(data.get("front_subtitle", "")), front_subtitle=str(data.get("front_subtitle", "")),
front_font_family=str(data.get("front_font_family") or "Arial"), front_font_family=str(data.get("front_font_family") or "MV Boli"),
front_title_font_size_pt=_to_float(data.get("front_title_font_size_pt"), 36.0), front_title_font_size_pt=_to_float(data.get("front_title_font_size_pt"), 48.0),
front_subtitle_font_size_pt=_to_float(data.get("front_subtitle_font_size_pt"), 20.0), front_subtitle_font_size_pt=_to_float(data.get("front_subtitle_font_size_pt"), 24.0),
front_title_x=_to_float(data.get("front_title_x"), 0.5), front_title_x=_to_float(data.get("front_title_x"), 0.6511959815448585),
front_title_y=_to_float(data.get("front_title_y"), 0.17), front_title_y=_to_float(data.get("front_title_y"), 0.22590834010644723),
front_subtitle_x=_to_float(data.get("front_subtitle_x"), 0.5), front_subtitle_x=_to_float(data.get("front_subtitle_x"), 0.7505046203361379),
front_subtitle_y=_to_float(data.get("front_subtitle_y"), 0.28), front_subtitle_y=_to_float(data.get("front_subtitle_y"), 0.3083317165255237),
title_font_family=str(data.get("title_font_family") or "Arial"), title_font_family=str(data.get("title_font_family") or "MV Boli"),
title_font_size_pt=_to_float(data.get("title_font_size_pt"), 10.0), title_font_size_pt=_to_float(data.get("title_font_size_pt"), 20.0),
title_color=str(data.get("title_color") or "#fff03f"), title_color=str(data.get("title_color") or "#fff03f"),
subtitle_font_family=str(data.get("subtitle_font_family") or "Arial"), subtitle_font_family=str(data.get("subtitle_font_family") or "MV Boli"),
subtitle_font_size_pt=_to_float(data.get("subtitle_font_size_pt"), 8.0), subtitle_font_size_pt=_to_float(data.get("subtitle_font_size_pt"), 14.0),
subtitle_color=str(data.get("subtitle_color") or "#fff03f"), subtitle_color=str(data.get("subtitle_color") or "#fff03f"),
spine_title_y_in=_optional_float(data.get("spine_title_y_in")), spine_title_y_in=_optional_float(data.get("spine_title_y_in")),
spine_subtitle_y_in=_optional_float(data.get("spine_subtitle_y_in")), spine_subtitle_y_in=(
_optional_float(data.get("spine_subtitle_y_in"))
if "spine_subtitle_y_in" in data
else 1.1093844419391201
),
face_width_in=_to_float(data.get("face_width_in"), 10.074), face_width_in=_to_float(data.get("face_width_in"), 10.074),
face_height_in=_to_float(data.get("face_height_in"), 9.0), face_height_in=_to_float(data.get("face_height_in"), 9.0),
spine_width_in=_to_float(data.get("spine_width_in"), 0.46), spine_width_in=_to_float(data.get("spine_width_in"), 0.57),
margin_left_in=_to_float(data.get("margin_left_in"), 0.125), margin_left_in=_to_float(data.get("margin_left_in"), 0.0),
margin_right_in=_to_float(data.get("margin_right_in"), 0.125), margin_right_in=_to_float(data.get("margin_right_in"), 0.0),
margin_top_in=_to_float(data.get("margin_top_in"), 0.125), margin_top_in=_to_float(data.get("margin_top_in"), 0.0),
margin_bottom_in=_to_float(data.get("margin_bottom_in"), 0.125), margin_bottom_in=_to_float(data.get("margin_bottom_in"), 0.0),
) )
+139 -7
View File
@@ -10,7 +10,7 @@ import sys
from PIL import Image, ImageOps from PIL import Image, ImageOps
from PySide6.QtCore import QThread, Qt, Signal, QSize, QRectF, QPointF, QSettings from PySide6.QtCore import QThread, Qt, Signal, QSize, QRectF, QPointF, QSettings
from PySide6.QtGui import QAction, QActionGroup, QColor, QFont, QFontMetricsF, QImage, QPainter, QPainterPath, QPen, QPixmap from PySide6.QtGui import QAction, QActionGroup, QColor, QFont, QFontDatabase, QFontMetricsF, QImage, QPainter, QPainterPath, QPen, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
@@ -90,6 +90,18 @@ def _photo_path_key(path: str | Path) -> str:
return value.casefold() return value.casefold()
def _resolve_cover_font_families(cover: CoverLayout) -> None:
available = {family.casefold(): family for family in QFontDatabase.families()}
fallback = available.get("arial", "Sans Serif")
for attribute in (
"front_font_family",
"title_font_family",
"subtitle_font_family",
):
family = getattr(cover, attribute)
setattr(cover, attribute, available.get(family.casefold(), fallback))
class PageListWidget(QListWidget): class PageListWidget(QListWidget):
orderChanged = Signal() orderChanged = Signal()
@@ -1090,6 +1102,28 @@ class ExportWorker(QThread):
self.failed.emit(str(exc)) self.failed.emit(str(exc))
class CoverExportWorker(QThread):
progressChanged = Signal(int, int)
succeeded = Signal(str)
failed = Signal(str)
def __init__(self, project: BookProject, output_path: str) -> None:
super().__init__()
self.project = project
self.output_path = output_path
def run(self) -> None:
try:
output = export_cover_pdf(
self.project,
self.output_path,
progress=lambda done, total: self.progressChanged.emit(done, total),
)
self.succeeded.emit(str(output))
except Exception as exc:
self.failed.emit(str(exc))
class FolderImportWorker(QThread): class FolderImportWorker(QThread):
stageChanged = Signal(str, int) stageChanged = Signal(str, int)
progressChanged = Signal(int, int) progressChanged = Signal(int, int)
@@ -1247,6 +1281,7 @@ class PhotoBookWindow(QMainWindow):
self.setWindowTitle("Blurb Photo Book Editor") self.setWindowTitle("Blurb Photo Book Editor")
self.resize(1480, 920) self.resize(1480, 920)
self.project = BookProject() self.project = BookProject()
_resolve_cover_font_families(self.project.cover)
self.project_path: str | None = None self.project_path: str | None = None
self.settings = QSettings("Blurb", "Blurb Photo Book Editor") self.settings = QSettings("Blurb", "Blurb Photo Book Editor")
self._photo_date_cache: dict[str, str] = {} self._photo_date_cache: dict[str, str] = {}
@@ -1257,6 +1292,7 @@ class PhotoBookWindow(QMainWindow):
self.dirty = False self.dirty = False
self.current_page_index = -1 self.current_page_index = -1
self.worker: ExportWorker | None = None self.worker: ExportWorker | None = None
self.cover_export_worker: CoverExportWorker | None = None
self.folder_import_worker: FolderImportWorker | None = None self.folder_import_worker: FolderImportWorker | None = None
self.import_progress_dialog: QProgressDialog | None = None self.import_progress_dialog: QProgressDialog | None = None
self.project_load_worker: ProjectLoadWorker | None = None self.project_load_worker: ProjectLoadWorker | None = None
@@ -1331,12 +1367,12 @@ class PhotoBookWindow(QMainWindow):
self.front_font_combo.currentFontChanged.connect( self.front_font_combo.currentFontChanged.connect(
lambda font: self._front_font_changed(font.family()) lambda font: self._front_font_changed(font.family())
) )
self.front_title_size_spin = self._front_font_size_spin(36.0) self.front_title_size_spin = self._front_font_size_spin(48.0)
self.front_title_size_spin.setToolTip("Front cover title size") self.front_title_size_spin.setToolTip("Front cover title size")
self.front_title_size_spin.valueChanged.connect( self.front_title_size_spin.valueChanged.connect(
lambda value: self._front_font_size_changed("title", value) lambda value: self._front_font_size_changed("title", value)
) )
self.front_subtitle_size_spin = self._front_font_size_spin(20.0) self.front_subtitle_size_spin = self._front_font_size_spin(24.0)
self.front_subtitle_size_spin.setToolTip("Front cover subtitle size") self.front_subtitle_size_spin.setToolTip("Front cover subtitle size")
self.front_subtitle_size_spin.valueChanged.connect( self.front_subtitle_size_spin.valueChanged.connect(
lambda value: self._front_font_size_changed("subtitle", value) lambda value: self._front_font_size_changed("subtitle", value)
@@ -1344,7 +1380,7 @@ class PhotoBookWindow(QMainWindow):
self.cover_font_combos: dict[str, QFontComboBox] = {} self.cover_font_combos: dict[str, QFontComboBox] = {}
self.cover_font_sizes: dict[str, QDoubleSpinBox] = {} self.cover_font_sizes: dict[str, QDoubleSpinBox] = {}
self.cover_color_buttons: dict[str, QPushButton] = {} self.cover_color_buttons: dict[str, QPushButton] = {}
for role, default_size in (("title", 10.0), ("subtitle", 8.0)): for role, default_size in (("title", 20.0), ("subtitle", 14.0)):
font_combo = QFontComboBox() font_combo = QFontComboBox()
font_combo.setMinimumWidth(125) font_combo.setMinimumWidth(125)
font_combo.setMaximumWidth(185) font_combo.setMaximumWidth(185)
@@ -1373,7 +1409,7 @@ class PhotoBookWindow(QMainWindow):
self.cover_color_buttons[role] = color_button self.cover_color_buttons[role] = color_button
self.cover_face_width = self._cover_dimension_spin(10.074) self.cover_face_width = self._cover_dimension_spin(10.074)
self.cover_face_height = self._cover_dimension_spin(9.0) self.cover_face_height = self._cover_dimension_spin(9.0)
self.cover_spine_width = self._cover_dimension_spin(0.46) self.cover_spine_width = self._cover_dimension_spin(0.57)
for spin in (self.cover_face_width, self.cover_face_height, self.cover_spine_width): for spin in (self.cover_face_width, self.cover_face_height, self.cover_spine_width):
spin.valueChanged.connect(self._cover_dimensions_changed) spin.valueChanged.connect(self._cover_dimensions_changed)
self.cover_margin_edits: dict[str, QLineEdit] = {} self.cover_margin_edits: dict[str, QLineEdit] = {}
@@ -1486,7 +1522,7 @@ class PhotoBookWindow(QMainWindow):
cover_form.addRow("Wrap/bleed margins", margins_row) cover_form.addRow("Wrap/bleed margins", margins_row)
cover_layout.addLayout(cover_form) cover_layout.addLayout(cover_form)
cover_margin_hint = QLabel( cover_margin_hint = QLabel(
"Starting margins are 0.125 in on each edge. Verify the exact wrap/bleed values for your Blurb format, paper, and page count." "Default margins are 0.000 in on each edge. Verify the exact wrap/bleed values for your Blurb format, paper, and page count."
) )
cover_margin_hint.setWordWrap(True) cover_margin_hint.setWordWrap(True)
cover_margin_hint.setStyleSheet("color: #aab1bd; padding-left: 4px;") cover_margin_hint.setStyleSheet("color: #aab1bd; padding-left: 4px;")
@@ -1561,6 +1597,9 @@ class PhotoBookWindow(QMainWindow):
self.export_action.setShortcut("Ctrl+E") self.export_action.setShortcut("Ctrl+E")
self.export_action.triggered.connect(self._export_pdf) self.export_action.triggered.connect(self._export_pdf)
file_menu.addAction(self.export_action) file_menu.addAction(self.export_action)
self.export_cover_action = QAction("Export Cover PDF…", self)
self.export_cover_action.triggered.connect(self._export_cover_pdf)
file_menu.addAction(self.export_cover_action)
file_menu.addSeparator() file_menu.addSeparator()
exit_action = QAction("Exit", self) exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close) exit_action.triggered.connect(self.close)
@@ -2597,6 +2636,7 @@ class PhotoBookWindow(QMainWindow):
self._pending_empty_import = True self._pending_empty_import = True
return return
self.project = project self.project = project
_resolve_cover_font_families(self.project.cover)
self.project_path = None self.project_path = None
self._photo_date_cache = photo_dates self._photo_date_cache = photo_dates
self._photo_thumbnail_cache = { self._photo_thumbnail_cache = {
@@ -2773,6 +2813,7 @@ class PhotoBookWindow(QMainWindow):
thumbnails: dict[int, QImage], thumbnails: dict[int, QImage],
) -> None: ) -> None:
self.project = project self.project = project
_resolve_cover_font_families(self.project.cover)
self.project_path = str(Path(self.project_load_worker.path).expanduser().resolve()) self.project_path = str(Path(self.project_load_worker.path).expanduser().resolve())
self._photo_date_cache = photo_dates self._photo_date_cache = photo_dates
self._photo_thumbnail_cache = { self._photo_thumbnail_cache = {
@@ -2857,7 +2898,9 @@ class PhotoBookWindow(QMainWindow):
self._update_status() self._update_status()
def _export_pdf(self) -> None: def _export_pdf(self) -> None:
if self.worker and self.worker.isRunning(): if (self.worker and self.worker.isRunning()) or (
self.cover_export_worker and self.cover_export_worker.isRunning()
):
return return
if not self.project.pages: if not self.project.pages:
QMessageBox.information(self, "Export PDF", "Import photos or insert a page first.") QMessageBox.information(self, "Export PDF", "Import photos or insert a page first.")
@@ -2948,12 +2991,91 @@ class PhotoBookWindow(QMainWindow):
self.worker.failed.connect(self._export_failed) self.worker.failed.connect(self._export_failed)
self.worker.start() self.worker.start()
self.export_action.setEnabled(False) self.export_action.setEnabled(False)
self.export_cover_action.setEnabled(False)
total_work = len(snapshot.pages) + (1 if snapshot.cover.has_content else 0) total_work = len(snapshot.pages) + (1 if snapshot.cover.has_content else 0)
self.progress.setRange(0, total_work) self.progress.setRange(0, total_work)
self.progress.setValue(0) self.progress.setValue(0)
self.progress.setVisible(True) self.progress.setVisible(True)
self.status_label.setText("Rendering pages…") self.status_label.setText("Rendering pages…")
def _export_cover_pdf(self) -> None:
if (self.worker and self.worker.isRunning()) or (
self.cover_export_worker and self.cover_export_worker.isRunning()
):
return
if not self.project.cover.has_content:
QMessageBox.information(
self,
"Export cover PDF",
"Add a cover photo or spine text before exporting the cover.",
)
return
cover_errors = self.project.cover.validate()
if cover_errors:
QMessageBox.warning(self, "Cover setup", "\n".join(cover_errors))
return
project_path = Path(self.project_path) if self.project_path else None
output_directory = (
project_path.parent
if project_path
else Path(self.project.source_directory or Path.home())
)
base_name = project_path.stem if project_path else "blurb-book"
suggested = output_directory / f"{base_name}-cover.pdf"
output, _ = QFileDialog.getSaveFileName(
self,
"Export Blurb Cover PDF",
str(suggested),
"PDF files (*.pdf)",
)
if not output:
return
if not output.lower().endswith(".pdf"):
output += ".pdf"
destination = Path(output)
if destination.exists():
overwrite = QMessageBox.question(
self,
"Replace cover PDF",
f"The cover PDF already exists:\n{destination}\n\nReplace it?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if overwrite != QMessageBox.StandardButton.Yes:
return
snapshot = copy.deepcopy(self.project)
self.cover_export_worker = CoverExportWorker(snapshot, output)
self.cover_export_worker.progressChanged.connect(self._cover_export_progress)
self.cover_export_worker.succeeded.connect(self._cover_export_succeeded)
self.cover_export_worker.failed.connect(self._cover_export_failed)
self.cover_export_worker.start()
self.export_action.setEnabled(False)
self.export_cover_action.setEnabled(False)
self.progress.setRange(0, 0)
self.progress.setVisible(True)
self.status_label.setText("Rendering cover spread…")
def _cover_export_progress(self, done: int, total: int) -> None:
self.progress.setRange(0, total)
self.progress.setValue(done)
self.status_label.setText("Cover PDF verified and saved.")
def _cover_export_succeeded(self, path: str) -> None:
self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False)
self.status_label.setText(f"Cover PDF verified and saved: {path}")
QMessageBox.information(self, "Cover PDF ready", f"Cover PDF created and verified:\n\n{path}")
def _cover_export_failed(self, message: str) -> None:
self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False)
self.status_label.setText("Cover PDF export failed.")
QMessageBox.critical(self, "Cover PDF export failed", message)
def _export_progress(self, done: int, total: int) -> None: def _export_progress(self, done: int, total: int) -> None:
self.progress.setRange(0, total) self.progress.setRange(0, total)
self.progress.setValue(done) self.progress.setValue(done)
@@ -2964,6 +3086,7 @@ class PhotoBookWindow(QMainWindow):
def _export_succeeded(self, path: str) -> None: def _export_succeeded(self, path: str) -> None:
self.export_action.setEnabled(True) self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False) self.progress.setVisible(False)
paths = path.splitlines() paths = path.splitlines()
self.status_label.setText(f"PDF verified and saved: {paths[0]}") self.status_label.setText(f"PDF verified and saved: {paths[0]}")
@@ -2971,6 +3094,7 @@ class PhotoBookWindow(QMainWindow):
def _export_failed(self, message: str) -> None: def _export_failed(self, message: str) -> None:
self.export_action.setEnabled(True) self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False) self.progress.setVisible(False)
self.status_label.setText("PDF export failed.") self.status_label.setText("PDF export failed.")
QMessageBox.critical(self, "PDF export failed", message) QMessageBox.critical(self, "PDF export failed", message)
@@ -2996,6 +3120,14 @@ class PhotoBookWindow(QMainWindow):
QMessageBox.information(self, "PDF export running", "Wait for the PDF export to finish before closing.") QMessageBox.information(self, "PDF export running", "Wait for the PDF export to finish before closing.")
event.ignore() event.ignore()
return return
if self.cover_export_worker and self.cover_export_worker.isRunning():
QMessageBox.information(
self,
"Cover PDF export running",
"Wait for the cover PDF export to finish before closing.",
)
event.ignore()
return
if self._maybe_save(): if self._maybe_save():
event.accept() event.accept()
else: else: