Update trim dimensions and cover export functionality in Blurb Photo Book Editor
This commit is contained in:
+139
-7
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user