Skip to content

feat(images): add image variant generator library (BE-2)#473

Merged
Zheaoli merged 2 commits into
mainfrom
feat/image-variant-generator
May 30, 2026
Merged

feat(images): add image variant generator library (BE-2)#473
Zheaoli merged 2 commits into
mainfrom
feat/image-variant-generator

Conversation

@Zheaoli
Copy link
Copy Markdown
Collaborator

@Zheaoli Zheaoli commented May 30, 2026

BE-2 — image variant generator library

Part of the image-performance overhaul (parent task #1 in #picimpact). This is the pure generation engine the preprocessing queue (BE-3) will drive. No wiring into the request path here — that comes with BE-3.

What

New server/lib/image-variants.ts:

  • VARIANT_TIER_WIDTHS [320,480,640,800,1080,1280,1920,2560] — kept in sync with the planned next.config imageSizes(320,480)/deviceSizes(640..2560), so every width the custom image loader requests maps onto a generated tier.
  • generateImageVariants(input) — sharp generation of avif + webp at every tier ≤ source width (never upscales), ascending, plus the base64 thumbhash placeholder. EXIF auto-orientation (.rotate()), a 100 MP decompression-bomb guard (limitInputPixels), and failOn: 'none' for resilience on slightly malformed inputs.
  • computeImageKey(input) — content-addressed base key (sha256, 32 hex) → variants are safe to serve immutable and never need invalidation.
  • buildVariantKey(key, width, fmt) — single source of truth for the {key}_{w}.{fmt} naming the next/image loader (FE-3) also uses.
  • putVariantObject(...) — S3/R2 upload primitive that stamps Cache-Control: public, max-age=31536000, immutable at write time.

Scope boundaries

  • Pure except the explicit putVariantObject primitive — reads no external state, produces in-memory buffers. This lets BE-3 advance ready_max_width after each successfully-uploaded tier (ascending = monotonic watermark).
  • OpenList variant upload is intentionally deferred to BE-3 (it owns the OpenList fs/put specifics); BE-2 covers the S3/R2 common path.

Verification

  • tsc --noEmit: no errors in the new file.
  • eslint --fix: clean (conforms to repo no-semicolons/single-quote style).
  • No runtime/integration test yet — there is no test harness in the repo and no storage creds here; end-to-end exercise happens when BE-3 wires it into the queue. Library is written to be unit-testable (pure functions + injected client).

Interface lock (with @Picimpact-Design FE-3)

buildVariantKey naming + tier ladder + thumbhashblurhash column are the agreed contract.

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
picimpact Ready Ready Preview, Comment May 30, 2026 11:19am

Zheaoli and others added 2 commits May 30, 2026 19:12
BE-2: pure server-side library for the image preprocessing pipeline.

server/lib/image-variants.ts provides:
- VARIANT_TIER_WIDTHS [320..2560] kept in sync with next.config
  imageSizes/deviceSizes so every loader-requested width maps to a
  generated tier.
- generateImageVariants(): sharp-based generation of avif + webp at every
  tier <= source width (never upscales), ascending, plus the base64
  thumbhash placeholder. EXIF auto-orientation; 100 MP decompression-bomb
  guard; failOn:'none' for resilience on slightly malformed inputs.
- computeImageKey(): content-addressed base key (sha256) so variants are
  safe to serve with an immutable cache header and never need invalidation.
- buildVariantKey(): single source of truth for the `{key}_{w}.{fmt}`
  naming convention the custom next/image loader also uses.
- putVariantObject(): S3/R2 upload primitive that stamps the immutable
  Cache-Control header at write time.

Pure and side-effect-free except the explicit upload primitive: the
preprocessing queue (BE-3) orchestrates fetch -> generate -> upload ->
advance ready_max_width. OpenList variant upload is handled in BE-3
(it owns the OpenList put specifics).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sharp's .metadata() returns the stored (pre-rotation) width/height, but
variants are generated via .rotate() (EXIF auto-orientation). For
orientation 5-8 (rotate 90/270/transpose/transverse — common on phone
photos) the displayed dimensions are swapped, so the previous code
returned width/height that disagreed with both the generated pixels and
the browser-reported dimensions of existing rows, which would reintroduce
the portrait-as-landscape gallery layout bug.

Read metadata.orientation and swap width/height for orientation 5-8.
Zero extra decode. Verified against sharp 0.34.5 with a synthetic
orientation=6 image (200x100 stored -> 100x200 displayed): swap-based
dims now match the actual .rotate() output.

Addresses BE-2 review feedback from @Picimpact-Review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Zheaoli Zheaoli force-pushed the feat/image-variant-generator branch from 7d2a1f4 to 4bb480e Compare May 30, 2026 11:12
@Zheaoli Zheaoli merged commit 2170442 into main May 30, 2026
5 of 6 checks passed
@Zheaoli Zheaoli deleted the feat/image-variant-generator branch May 30, 2026 11:15
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