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
+139 -7
View File
@@ -10,7 +10,7 @@ import sys
from PIL import Image, ImageOps
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 (
QAbstractItemView,
QApplication,
@@ -90,6 +90,18 @@ def _photo_path_key(path: str | Path) -> str:
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):
orderChanged = Signal()
@@ -1090,6 +1102,28 @@ class ExportWorker(QThread):
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):
stageChanged = Signal(str, int)
progressChanged = Signal(int, int)
@@ -1247,6 +1281,7 @@ class PhotoBookWindow(QMainWindow):
self.setWindowTitle("Blurb Photo Book Editor")
self.resize(1480, 920)
self.project = BookProject()
_resolve_cover_font_families(self.project.cover)
self.project_path: str | None = None
self.settings = QSettings("Blurb", "Blurb Photo Book Editor")
self._photo_date_cache: dict[str, str] = {}
@@ -1257,6 +1292,7 @@ class PhotoBookWindow(QMainWindow):
self.dirty = False
self.current_page_index = -1
self.worker: ExportWorker | None = None
self.cover_export_worker: CoverExportWorker | None = None
self.folder_import_worker: FolderImportWorker | None = None
self.import_progress_dialog: QProgressDialog | None = None
self.project_load_worker: ProjectLoadWorker | None = None
@@ -1331,12 +1367,12 @@ class PhotoBookWindow(QMainWindow):
self.front_font_combo.currentFontChanged.connect(
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.valueChanged.connect(
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.valueChanged.connect(
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_sizes: dict[str, QDoubleSpinBox] = {}
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.setMinimumWidth(125)
font_combo.setMaximumWidth(185)
@@ -1373,7 +1409,7 @@ class PhotoBookWindow(QMainWindow):
self.cover_color_buttons[role] = color_button
self.cover_face_width = self._cover_dimension_spin(10.074)
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):
spin.valueChanged.connect(self._cover_dimensions_changed)
self.cover_margin_edits: dict[str, QLineEdit] = {}
@@ -1486,7 +1522,7 @@ class PhotoBookWindow(QMainWindow):
cover_form.addRow("Wrap/bleed margins", margins_row)
cover_layout.addLayout(cover_form)
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.setStyleSheet("color: #aab1bd; padding-left: 4px;")
@@ -1561,6 +1597,9 @@ class PhotoBookWindow(QMainWindow):
self.export_action.setShortcut("Ctrl+E")
self.export_action.triggered.connect(self._export_pdf)
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()
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close)
@@ -2597,6 +2636,7 @@ class PhotoBookWindow(QMainWindow):
self._pending_empty_import = True
return
self.project = project
_resolve_cover_font_families(self.project.cover)
self.project_path = None
self._photo_date_cache = photo_dates
self._photo_thumbnail_cache = {
@@ -2773,6 +2813,7 @@ class PhotoBookWindow(QMainWindow):
thumbnails: dict[int, QImage],
) -> None:
self.project = project
_resolve_cover_font_families(self.project.cover)
self.project_path = str(Path(self.project_load_worker.path).expanduser().resolve())
self._photo_date_cache = photo_dates
self._photo_thumbnail_cache = {
@@ -2857,7 +2898,9 @@ class PhotoBookWindow(QMainWindow):
self._update_status()
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
if not self.project.pages:
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.start()
self.export_action.setEnabled(False)
self.export_cover_action.setEnabled(False)
total_work = len(snapshot.pages) + (1 if snapshot.cover.has_content else 0)
self.progress.setRange(0, total_work)
self.progress.setValue(0)
self.progress.setVisible(True)
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:
self.progress.setRange(0, total)
self.progress.setValue(done)
@@ -2964,6 +3086,7 @@ class PhotoBookWindow(QMainWindow):
def _export_succeeded(self, path: str) -> None:
self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False)
paths = path.splitlines()
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:
self.export_action.setEnabled(True)
self.export_cover_action.setEnabled(True)
self.progress.setVisible(False)
self.status_label.setText("PDF export failed.")
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.")
event.ignore()
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():
event.accept()
else: