Skip to content

photos: send the archive JPG progressive on the LOCAL upload path - #128

Merged
oddharsh merged 1 commit into
mainfrom
claude/archive-progressive-jpeg
Jul 27, 2026
Merged

oddharsh merged 1 commit into
mainfrom
claude/archive-progressive-jpeg

Conversation

@oddharsh

@oddharsh oddharsh commented Jul 27, 2026 •

Copy link
Copy Markdown
Owner

Scope correction (see "What this does NOT cover"). This only affects the local npm run photos path. The canonical remote pipeline skips it, and cannot be made to do this. Read that section before merging.

/images/full/<stem>.jpg is the one image surface on the site with no AVIF tier and no <picture> around it. The worker streams it from R2 as bare image/jpeg, roughly 10MB, and that single file is the whole payload. Scan order is therefore the entire loading experience there — the case Kornel Lesiński argues for in his perf.now() 2018 talk, unlike a 32KB thumbnail where it's invisible.

Cameras write baseline. Of the 158 archive originals, the 112 from HIF sources already go up progressive (zenc sets ProgressiveScanMode::ProgressiveSearch unconditionally); the 46 straight-from-camera Leica JPGs did not.

The change

Phase 3 runs JPG-source originals through jpegtran -progressive -copy all on the way to R2.

This is not a re-encode. jpegtran reorders existing DCT coefficients and never decodes to pixels, so there's no generational loss. Measured on a real 9.95MB Leica original:

Scan type SOF0 baseline → SOF2 progressive
Size 9.95MB → 9.65MB (-3.05%)
Decoded pixels byte-identical (1a7ece28cea8a4f9 both)
EXIF Model, DateTimeOriginal, Orientation, ISO all preserved

The SOOC file is not touched

The conversion writes to a copy under $TMP; the source folder is never opened for writing. Verified by sha256 before and after. The bytes on disk stay exactly what the camera wrote, which is the point of a SOOC archive. Only the serving copy is rearranged.

-copy all is load-bearing: it keeps EXIF including Orientation, which gotchas 3 and 4 in CLAUDE.md are both about. Falls back to uploading the untouched original if jpegtran fails.

What this does NOT cover

Phase 3 is inside if [ "${REMOTE_RENDER_ONLY:-0}" = "1" ]; then ... else, and photo-pipeline.yml:115 sets REMOTE_RENDER_ONLY: "1". So on the canonical remote path this code does not run.

That is not fixable inside CI. Per PHOTO-PIPELINE.md, the remote input contract is to upload the source object to R2 by hand (dashboard or other remote path); the workflow then downloads it read-only, builds derived tiers, and opens an artifact PR. It never rewrites the archive object — and it must not be able to, because the release design forbids GitHub from holding a Cloudflare token with write scope. The archive bytes are therefore fixed at hand-upload time.

So the honest scope is:

  • local npm run photos — covered by this PR, progressive from here on.
  • canonical remote pipeline — not covered. Needs jpegtran -progressive -copy all run on the file before it is uploaded to R2, which is a change to the documented input contract rather than to any script here.
  • the 46 already in R2 — still baseline. Backfill needs an ARCHIVE_VERSION bump and a Purge Everything to clear caches.default (the trap from fix(images): version-scope the /images/full Cache API key (ARCHIVE_VERSION) #50), and is a workstation-only operation since CI cannot write to R2.

Follow-up worth doing: fold the jpegtran step into the input contract in PHOTO-PIPELINE.md so the remote path stops being the one that silently skips it.

Verification

  • bash -n parses clean.
  • Functional test of prep_original against a real baseline Leica file: correct temp path returned, source sha256 unchanged, output SOF2, pixels identical, EXIF intact.
  • No new dependency — jpegtran (mozjpeg) is already required for the lossless orientation step.

🤖 Generated with Claude Code

…SOOC file

/images/full/<stem>.jpg is the one image surface here with no AVIF tier and no
<picture> around it: the worker streams it from R2 as bare image/jpeg, ~10MB, and
that single file is the whole payload. Scan order is therefore the entire loading
experience, and cameras write baseline. Of the 158 archive originals, the 112 from
HIF sources already go up progressive because zenc sets ProgressiveScanMode
unconditionally; the 46 straight-from-camera JPGs did not.

phase 3 now runs those through jpegtran -progressive -copy all on the way to R2.
jpegtran reorders existing DCT coefficients and never decodes to pixels, so this
is not a re-encode and carries no generational loss. Measured on a 9.95MB Leica
original: SOF0 -> SOF2, 3.05% smaller (progressive Huffman packs better), decoded
pixels byte-identical, EXIF including Orientation and ISO preserved.

The conversion writes to a copy under $TMP and the source folder is never opened
for writing. The bytes on disk stay exactly what the camera wrote, which is the
point of keeping a SOOC archive; only the serving copy is rearranged. Falls back
to uploading the untouched original if jpegtran fails, so a bad file costs the
optimisation rather than the upload.

This only affects photos added from here on. The 46 already in R2 stay baseline
until someone backfills them, which needs an ARCHIVE_VERSION bump and a Purge
Everything to get past caches.default (PR #50) and is deliberately not bundled in.

Prompted by Kornel Lesiński's perf.now() 2018 image-optimization talk, which makes
the case that progressive scan order is worth more than it looks precisely when
one large image is the payload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Understanding-first review

This is a prompt, not a score or merge gate. Before approving, reconstruct the change:

  • My model: What do I think this PR does?
  • Mechanism: What path or invariant makes that behavior true?
  • Falsifier: What would prove my model wrong?
  • Evidence: What test, trace, dry-run, or live check did I inspect?
  • Residual uncertainty: What remains unproven?

For docs and dependency changes, a compact answer is enough. For behavior or production changes, include the causal path and one concrete counterexample. The author’s claim card belongs in the PR description; this prompt is for the reviewer’s independent reconstruction.

See UNDERSTANDING-REVIEW.md.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh fb83193 Jul 27 2026, 04:13 PM

@oddharsh
oddharsh merged commit a46b94f into main Jul 27, 2026
4 checks passed
@oddharsh oddharsh changed the title photos: send the archive JPG to R2 progressive, without touching the SOOC file photos: send the archive JPG progressive on the LOCAL upload path Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant