Add main entry point for Blurb Photo Book Editor

- Implemented the main function to launch the editor.
- Added error handling for missing PySide6 dependency.
- Included instructions for installing required packages.
This commit is contained in:
2026-10-05 00:11:30 -04:00
parent f62cb2efdb
commit 4d5d3a4c75
7 changed files with 2027 additions and 289 deletions
+25 -288
View File
@@ -1,309 +1,46 @@
# Blurb
# Blurb Photo Book Editor
Build a print-ready PDF from a folder of photos, sized for an Affinity Publisher
spread (9.5 x 8 in).
A Windows desktop app for arranging a folder of photos into a Blurb-ready interior PDF. Each photo is placed on its own page. The editor supports reordering, crop adjustment, page tags, and print-size setup.
**Status: starter / provenance document.** The PDF described here was produced by
a one-off PowerShell + ImageMagick procedure, not by any committed code in this
repo. Nothing in `Blurb/` is implemented yet. This README records exactly what was
done so it can be turned into a real, repeatable tool.
## Install and launch
---
## The artifact that was produced
| Property | Value |
|---|---|
| Output | `C:\tmp\bbook.pdf` |
| Pages | 98 (one photo per page) |
| File size | 59.3 MB |
| Page size | 9.5 x 8 in (684 x 576 pt) |
| Image resolution | 200 DPI (1900 x 1600 px) |
| JPEG quality | 85 |
| Page order | Chronological by date embedded in filename, 2024-01-02 -> 2024-11-01 |
| Page fill | Full-bleed, center-cropped, no white bars |
| Metadata | Stripped (GPS/EXIF removed) |
Source folder `C:\tmp\bbook` was left untouched: 98 `.jpg` files, no stray output.
---
## Source images
98 JPEGs in `C:\tmp\bbook`, from Pixel phone cameras. Three filename shapes, all
carrying a sortable timestamp:
```
PXL_20241014_213548581.PORTRAIT.jpg # main pattern
IMG20240724161556.jpg # 4 files
original_<uuid>_PXL_20240512_215016784.MP.jpg # 3 files, prefixed by a uuid
```
Suffixes in use: bare (default), `.MP` (macro), `.PORTRAIT` (portrait orientation),
`~2` / `~3` (duplicate-export suffixes).
### Orientation and aspect ratio
| Aspect ratio | Count | Note |
|---|---|---|
| 1.332 / 1.328 / 1.333 (4:3) | 94 | Landscape |
| 1.778 (16:9) | 3 | Landscape, wide |
| 0.751 | 1 | Portrait |
97 landscape, 1 portrait. This matters because the target page ratio is **1.188**,
which is narrower than 4:3 -- see Cropping below.
---
## What was done
### Step 1 - Extract the archives
Two zip archives were unpacked into the photo folder in place:
- `Photos-1-001.zip` -> 14 files
- `Photos-1-001 (6).zip` -> 13 files
27 unique files; no name collisions between the two. Archives were inspected
before extraction to confirm they held only `.jpg` and that the two sets did not
overlap. Post-extraction the folder held 98 photos: 27 from the archives plus 71
already present.
> Note: both source zips disappeared from the folder sometime after extraction.
> No photo data was lost (all 27 extracted files remain, 0 zero-byte files) and
> the archives were not in the recycle bin, but the cause was never identified.
> Worth keeping in mind before wiring an "extract then delete" step into any
> automated pipeline.
### Step 2 - Choose the page geometry
The 9.5 x 8 in spread size came from Affinity Publisher. That is a ratio of
**1.1875**, which is what drives the crop behavior.
One ambiguity worth resolving before this becomes code: a Publisher *spread* is
normally two facing pages. 9.5 x 8 in could mean either:
- one 9.5 x 8 in PDF page per photo (what was actually produced), or
- a 9.5 x 8 in spread meaning **two** 4.75 x 8 in pages, i.e. 2 photos per PDF page.
This build assumed the first. The second is very likely the real intent for a
book layout and is the single biggest thing to confirm.
### Step 3 - Measure before placing
Rather than guessing at fill behavior, every image was measured with
`magick identify` first. This surfaced that 94 photos are 4:3 while the page is
1.188, so full-bleed fill has to crop -- and that 4 outliers would be cropped far
harder than the rest:
| Photo AR | Count | Full-bleed crop required |
|---|---|---|
| ~1.33 (4:3) | 94 | ~11% off the sides |
| 1.778 (16:9) | 3 | **~33% off the sides** |
| 0.751 (portrait) | 1 | **~37% off the height** |
Cropping was accepted for uniformity, and the 4 outliers were flagged to the user
rather than silently trimmed. **Worth reviewing before print.**
### Step 4 - Render each page
For each photo, in chronological order, rendered one 1900 x 1600 page:
From PowerShell in this directory:
```powershell
magick <input.jpg> -auto-orient -resize 1900x1600^ -gravity center -extent 1900x1600 -strip -interlace Plane -quality 85 page_NNNN.jpg
```
- `-auto-orient` applies the EXIF rotation flag *before* cropping, so PORTRAIT
shots are not cropped as though they were landscape.
- `-resize WxH^` (the `^`) scales to *cover* the box, then `-extent` trims the
overflow. This is what produces full-bleed. For fit-inside instead, drop the
`^`.
- `-strip` drops EXIF including GPS. Dates survive in the page order and
filenames, so nothing meaningful was lost.
- Output was written to a numbered sequence (`page_0001.jpg` ... `page_0098.jpg`)
in a temp dir, not into the photo folder, keeping sources and build artifacts
cleanly separated.
Sort key, tried in order:
1. `PXL_(20\d{6})_(\d{6})` -> date + time
2. `IMG(20\d{6})(\d{6})` -> date + time
3. creation time as fallback
Sorting on the filename timestamp matters: filesystem mtimes were scattered across
several days in Sep/Oct 2026 (copy/export artifacts) and would have produced a
meaningless order.
### Step 5 - Assemble the PDF
The `%04d` glob sequence was tried first and **failed** -- ImageMagick did not
expand `page_%04d.jpg` and errored with `unable to open image`. Passing the 98
absolute paths as explicit arguments worked:
```powershell
magick @pages -units PixelsPerInch -density 200 -quality 85 C:\tmp\bbook.pdf
```
Total argument length stayed around 4.5 KB, far below the Windows command-line
limit, so this is safe until roughly 700+ images per invocation -- at which point
it needs chunking or a response file.
### Step 6 - Verify
**A bug worth documenting:** the first PDF came out at **26.39 x 22.22 in** --
not 9.5 x 8. Cause: `-strip` had removed the density metadata, so ImageMagick
assumed 72 DPI and treated the 1900 x 1600 px page as 1900 x 1600 *points*. Pixel
dimensions alone do not determine PDF page size.
Fix: set density explicitly at PDF-write time with `-units PixelsPerInch -density 200`.
Ghostscript is not installed, so ImageMagick cannot read or rasterize a PDF to
check it visually. Verification was therefore done by parsing the PDF bytes
directly:
```powershell
$txt = [System.Text.Encoding]::Latin1.GetString([IO.File]::ReadAllBytes($out))
([regex]::Matches($txt, '/Type\s*/Page[^s]')).Count # -> 98 page objects
[regex]::Match($txt, '/MediaBox\s*\[([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\]')
```
That confirmed 98 pages at 684 x 576 pt on every page. This byte-level check is
worth keeping -- it caught the page-size bug that a file-exists test would have
missed.
---
## Development environment
A venv is set up in this directory with everything needed to generate PDFs.
Pinned versions live in `requirements.txt`.
```powershell
cd "D:\Python Utilities\Blurb"
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe main.py
```
Always invoke as `.\.venv\Scripts\python.exe` -- plain `python` will hit the
system install, which has none of these modules.
## Use the editor
| Package | Version | Role |
|---|---|---|
| Pillow | 12.3.0 | Measure images, read EXIF orientation, cover-crop / contain resize |
| img2pdf | 0.6.3 | Embed JPEG bytes **losslessly**, no re-encode |
| reportlab | 5.0.1 | Page layout: captions, multi-image pages, margins, page numbers |
| pymupdf | 1.28.2 | Inspect/render finished PDFs -- page count, MediaBox, page-to-PNG preview |
| lxml | 6.1.3 | transitive (pikepdf) |
| pikepdf | 10.16.0 | transitive (img2pdf) |
| charset-normalizer | 3.5.2 | transitive (reportlab) |
| packaging / setuptools / wheel | pinned | build tooling |
1. Choose **File → Import photo folder**. Import scans the selected folder's top level, sorts photos oldest to newest, and prepares previews in the background. A progress dialog shows scanning and preview progress.
2. Select a page card to edit it. Drag a photo inside the page to reposition it; use the zoom slider or +/- buttons to zoom around the current frame center. Photos start centered and cropped to fill the page.
3. Drag cards to reorder pages. Use **Insert blank**, **Insert photo**, **Remove page**, or **Relink photo** as needed. The source photos are not modified.
4. Right-click a page card to **Tag page** or **Untag page**. A red badge marks tagged pages, and tags are saved in the project. **View → View Tagged** filters the list to tagged photo pages; it is disabled when no photos are tagged. In this view, insertion and reordering are disabled; switch to **View All** to use them.
5. Save the editable `.blurbproj` separately from the exported PDF.
The four direct deps divide the job cleanly: **Pillow** decides *what* each page
looks like, **img2pdf** or **reportlab** decide *how* pages are assembled, and
**pymupdf** verifies the result. pymupdf is not optional-in-practice: it is what
lets the finished PDF be inspected at all on this machine.
Photo cards show the date as `mm/dd/yyyy`. Dates come from supported timestamps in the filename first, then EXIF capture date, then filesystem creation time as a fallback.
### Verified end to end
## Blurb print setup and export
A smoke test confirmed the whole chain works, not just that the imports resolve:
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.
- Pillow read two 4000x3000 source photos and reported EXIF orientation
- Pillow cover-cropped both to 1900x1600
- img2pdf produced a 2-page PDF, verified by pymupdf as **9.50 x 8.00 in**
(page size correct: True) with 1 embedded image on page 1
- reportlab produced a captioned page, verified as 9.50x8.00 in with text
extracting correctly as `'IMG20240724161556.jpg\n1'`
- pymupdf rendered page 1 to PNG; pixel stats confirmed real photo content
(79,800 distinct colors, per-channel stddev ~50) rather than a blank page
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. The PDF contains interior pages only; cover design is not included.
That last check matters because this model cannot view images, so "the render
worked" had to be asserted from pixel statistics instead of by eye.
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.
### reportlab 5.x API break
Supported photo formats are JPEG, PNG, TIFF, WebP, and BMP. HEIC and camera RAW files are reported as unsupported. Folder import is not recursive.
`canvas.drawImage()` no longer accepts a PIL `Image` object:
## Project files
```python
with Image.open(path) as im:
c.drawImage(im, ...) # TypeError: expected str, bytes or os.PathLike,
# not JpegImageFile (reportlab 5.0.1)
```
A `.blurbproj` file stores photo paths, page order, each page's zoom and pan, page tags, and print settings. Photo files remain at their original locations; if a path is no longer available, use **Relink photo**.
Pass the **path string** instead:
## Code layout
```python
c.drawImage(path, x, y, width=..., height=...)
```
- `main.py` starts the application.
- `blurb_editor.py` contains the PySide6 interface, background import and export workers, page list, and photo canvas.
- `blurb_book.py` contains project and print-profile data, photo sorting and rendering, and PDF export.
- `requirements.txt` pins the Python dependencies.
Older reportlab tutorials pass image objects and will fail here.
### Recommended architecture
Two viable routes:
- **img2pdf for one-photo-per-page.** Embed losslessly, so the already-cropped
JPEG is stored verbatim and there is no second generation of compression.
Pillow does the crop; img2pdf just wraps it. Cheapest and highest fidelity.
- **reportlab when you need captions, page numbers, margins, or several images
composited per page.** More control, but images get re-encoded by ReportLab's
own image pipeline -- pick JPEG quality deliberately.
Likely need both: img2pdf for the bulk photo pages, reportlab for pages that
carry text. Worth benchmarking file size and visual fidelity of each before
settling.
---
## Host environment (for the original one-off build)
The 59.3 MB `bbook.pdf` was **not** produced with the venv above. It was built
with the system Python absent and ImageMagick on PATH:
| Tool | Version / state |
|---|---|
| OS | Windows 10 IoT Enterprise LTSC, build 19044 |
| ImageMagick | 7.1.2-31 Q16-HDRI x64 |
| Python | 3.12.0 (at `C:\Python\3.12.0`) |
| Ghostscript | **not installed** -- no PDF read/render via ImageMagick |
| Pillow, reportlab, img2pdf, PyMuPDF | none installed at build time (see venv above) |
Two notes on the ImageMagick install:
- `policy.xml` contains `coder` rules that *appear* to restrict output to
`{GIF,JPEG,PNG,WEBP}`, which would block PDF writing. Empirically PDF writing
works fine (confirmed by test). Don't trust the apparent policy reading --
test the actual operation.
- Its `magick -list policy` output does **not** echo the coder rules, so
`policy.xml` itself is the only place to check.
ImageMagick is no longer needed for the venv route and can be ignored going
forward. It stays useful as a cross-check on output from a different toolchain.
---
## Open questions
1. **Spread semantics.** One 9.5 x 8 in page per photo, or two 4.75 x 8 in pages?
See Step 2. Biggest open item.
2. **The 4 wide/portrait outliers.** Cropped hard (33% / 37%). Review, or special-case
them to fit-inside?
3. **Page labels.** Not implemented -- ImageMagick cannot write per-page titles here.
Page N maps to the Nth photo in chronological order. Affinity may want captions.
4. **Missing zip archives.** Cause of their disappearance is still unknown.
5. **DPI.** 200 was chosen as a print-proofing middle ground. Book production may
want 300, at roughly 2.25x the pixel count.
## Ideas for the real implementation
- **Dependency direction.** The ImageMagick route shells out per image (98 process
spawns). `Pillow` or `img2pdf` would do it in-process and much faster. Prefer a
pure-Python path; keep ImageMagick as a fallback.
- **Never write into the source folder.** Keep a dedicated build dir, as done here.
- **Make geometry a config object**, not literals: page size, DPI, quality, fill
mode (cover vs. contain), and margin should all be parameters.
- **Treat outliers as data.** Compute per-image fill loss during the measure pass
and report anything past a threshold, instead of hardcoding a warning for 4 files.
- **Dry-run mode.** Render pages and emit a manifest (source -> page -> crop
amount) before committing to a large PDF.
- **Verify after write.** Reuse the byte-level page-count / MediaBox check as an
automated assertion so a page-size regression fails loudly.
Use the project virtual environment when launching or installing dependencies. The PDF exporter uses Pillow for rendering, ReportLab for assembly, and PyMuPDF for output checks.