Session 20 — 2026-08-21 — Sheri call: photo fixer + print flow
Source: Sheri's "Troubleshooting" emails (8/11, 8/18) and the 8/21 live call (Bert screen-sharing the "Happy 95th Birthday" book for Sheri's grandmother; Sheri's mom is helping her build it).
What Sheri reported
| # | Issue | Root cause | Fix |
|---|---|---|---|
| 1 | Photos sideways in the PDF | @react-pdf ignores EXIF. Session 19 fix (144e16c) was committed 8/14 but never pushed; prod was still the 8/3 build. The size-preview modal also bypassed the normalizer entirely. |
Deployed 8/14 fix; modal now runs preparePhotos() |
| 2 | Photo renders as a black box | Transparent PNG → black in PDF. Same undeployed fix flattens to white. | Same |
| 3 | Prompt shows literal [Name] |
TributeBookPDF never substituted; only the web preview did |
Substituted at render, all PDF paths |
| 4 | Text size only scaled responses | Component already scaled prompts; the size-preview modal never passed textSize |
Modal passes coverDesign.bodyTextSize |
| 5 | "Not seeing the fixes — is it because the book is finalized?" | No. Nothing since 8/3 had shipped. | Pushed eb88c7c → Netlify auto-deploy |
| 6 | Needs per-page control of photo size, placement, orientation — simple | Feature gap | Photo Fixer (below) |
| 7 | Wants a wizard that fixes most things, with manual override | Feature gap | Photo Fixer wizard + grid |
| 8 | Ordering a physical book is not intuitive | Order card buried under "Plan Your Book" → size page → below estimator | Checklist step 6 + direct #order link + page retitled |
| 9 | Finalize process unclear | Lone green button, vague confirm, no sense of sequence | "Ready to Print?" checklist replaces Actions card |
| 10 | Relabel things for clarity | Sheri didn't name specifics | Obvious ones done (below); ask her for the rest |
What was built
Photo layout engine
books.photo_layout jsonb(migration20260821000000, applied to prod via MCP) —{ [photoUrl]: { rotate, size, align } }.src/lib/photo-layout.ts— types, defaults,preparePhotos(urls, map)which normalizes (EXIF + transparency + owner rotation) and attaches size/align.image-normalize.ts—normalizeImageUrl(url, rotate); rotation is drawn on the canvas after the decoder bakes EXIF; cache keyed by url+rotate.TributeBookPDF.tsx—photoUrlsacceptsstring | PreparedPhoto. Small/medium stay the square thumbnail row (no regression for existing books); large (50% of text width) and full-width render natural aspect, one per row, aligned left/center/right.BookData.photoLayoutcarries the map so every caller (download button, size preview, admin Lulu export) applies it.
Photo Fixer — /dashboard/[bookId]/photos
- Wizard mode (default): one photo at a time, big upright preview, progress bar, "Looks good → next". Controls: Rotate (cycles 90°), Size (Small/Medium/Large/Full), Position (Left/Center/Right), Reset to automatic.
- Grid mode: every photo with the same compact controls; "Walk through one by one" to re-enter the wizard; "Preview the book" button.
- Low-resolution badge when the short edge is under 1000px.
- Autosaves (500ms debounce) to
books.photo_layout. Preview and PDF pick it up immediately.
Finalize / ordering flow (dashboard)
- Actions card → "Ready to Print?" six-step checklist: review tributes (with
approved/pending/needs-edit counts) → check photos (count, ✓ once adjusted) →
design cover → preview → finalize (explains it closes new tributes, reopenable) →
Order Printed Copy (gold button, straight to
/sizes#order). - Finalize confirm copy rewritten. Finalized banner now points at ordering.
- Sizes page retitled "Book Size & Print Order" with a two-step intro; new
"Order a Printed Copy" heading with
id="order"and auto-scroll from the hash.
Relabels
| Before | After |
|---|---|
| Plan Your Book | Book Size & Print Order |
| Choose a Book Size | Book Size & Print Order |
| Actions | Ready to Print? |
| Reopen for Contributions | Reopen for Tributes |
| (new) | Check Photos |
Story points (for the record; billing is Bert's call)
| Increment | SP |
|---|---|
P0 fixes: [Name], size-preview parity, deploy |
2 |
| Photo layout engine (schema, normalizer rotate, PDF render) | 5 |
| Photo Fixer wizard + grid page | 5 |
| Ready-to-print checklist / finalize flow | 3 |
| Ordering entry + relabels | 2 |
| Total | 17 |
Production verification (Playwright, same day)
scripts/prod-walkthrough.cjs signs in to sayitnowbook.com with a magic link
(ATH mailbox), then: exercises the Ready to Print checklist, the Photo Fixer
(rotate, size, position, autosave, reset), the #order anchor, the preview, and
downloads the PDF (shoot); then adds a [Name] question to Bert's test book and
submits a tribute through the public form with an EXIF-orientation-6 JPEG and a
transparent PNG, downloads the PDF again, and the pages are rasterized with
pymupdf (contribute). 13/13 checks passed. Artifacts in walkthrough/prod-20260821/
(gitignored).
Bug found by the run: image-resize.ts drew uploads onto an unfilled canvas and
exported JPEG, so transparent PNGs were stored with a black background at upload
time (any upload since 7/31). Fixed in f14f0d9 (fill white first); re-verified by
pixel: pre-fix embed corner (0,0,0), post-fix (255,255,255).
Client-facing page: https://notes.ath.how/sheri-photo-fix-f810f363/
Side effect: Bert's test book ("Tribute for Test", 6d8d4479) now has one extra
question and two "Playwright QA" tributes. Harmless; delete from the dashboard if
it gets in the way.
Automatic layout rewrite (same day, after Bert: "they looked like crap on her book")
The default was a 150pt square crop for every photo (about 1.5in on a 7.5in book)
that chopped group shots. Replaced with auto (a20eba4, d6824ad): one photo
gets 66% of the text width at natural aspect (48% if portrait), two sit side by
side at 48%, three or more tile at 30%; only the explicit Small size square-crops;
heights cap to the page so the same choice adapts across book sizes; a photo row
moves to the next page as a unit (wrap={false}) instead of splitting a pair.
PreparedPhoto now carries aspect. Photo Fixer size chips: Auto (default),
Small, Medium, Large, Full width.
Verified on prod with prod-walkthrough.cjs layout (group 1800x1200 + portrait
1000x1600 fixtures) and pdf: single landscape placed 305x166pt on a 540pt page;
pair 222x148 + 221x353 side by side on one page.
Observed, not chased: the first of two back-to-back public-form submissions
("Layout QA Single") showed "Thank you" but never reached submissions; the second
landed. Possible silent failure in the contributor form submit path. Worth a look.
Real page preview in the Fixer (same day, after Bert: "chase the render")
The wizard showed a thumbnail and the page copy claimed changes "show up in the
preview." That was only true after leaving the page. Now the wizard renders the
contributor's pages through TributeBookPDF with a new interiorOnly prop (no
cover/TOC/back), at the book's size, with the in-memory layout, into a blob PDF in
an iframe; it re-renders (400ms debounce, stale runs discarded) on every control
click. First cut used an <iframe> of the blob PDF; the headless screenshot showed a
blank gray box, which is also what an iPad or plugin-less browser would show. Replaced
with in-browser rasterization via pdfjs-dist 6.2.108 (pinned; Mozilla; zero
required transitive deps; one optional Node-only package never bundled) so the
preview is a PNG per page (419152a). photos.astro now loads full submissions and passes pagePreview (book,
grouped contributors, pageSize, coverDesign, textSize). Grid cards link "See this
page". The "adapt to book size" open item is closed by this: the preview is the
page at the selected size.
Open with Sheri
- Tall photo moves to the next page, leaving page 1 mostly blank. Seen in the real
preview after Rotate + Large on a 7.5x7.5 page: the row is kept whole (
wrap={false}) so it jumps to page 2. Correct and honest, but ugly. Next: when the row does not fit the remaining space, shrink it to fit under the text (down to a floor) before moving it. - Photo size/layout must adapt to the selected book size (Bert, 8/21). Addressed in part by the auto layout (fractions of text width, page-height cap). Closed by the real page preview (below). Earlier note: today the sizes are proportional (small/medium scale with the page, large = 50% of text width, full = text width) so they do resize, but the Photo Fixer previews photos with no page context, and a choice made for 8.5x11 can be wrong on 7.5x7.5 (natural-aspect large/full photos vs. the page-height cap; square crop on landscape pages). Next increment: Fixer shows the photo inside a mock page at the book's current size, re-fits when the size changes, and the auto-fix picks a default size per book size.
- Which labels she wanted changed (she hasn't named them).
- Walk her through the Photo Fixer on her grandmother's book; confirm the sideways and black photos are right in the PDF now that the normalizer is live.
Files
src/lib/photo-layout.ts (new), src/components/PhotoFixer.tsx (new),
src/pages/dashboard/[bookId]/photos.astro (new),
supabase/migrations/20260821000000_add_books_photo_layout.sql (new),
src/lib/image-normalize.ts, src/components/TributeBookPDF.tsx,
src/components/PDFDownloadButton.tsx, src/components/LuluDownloadButton.tsx,
src/pages/dashboard/[bookId].astro, src/pages/dashboard/[bookId]/sizes.astro,
src/pages/preview/[code].astro, src/pages/admin/index.astro,
src/lib/database.types.ts.