Skip to content

Enhancement: High-resolution image masters + WebP rendition delivery - #488

Open
baltinerdist wants to merge 3 commits into
amtgard:masterfrom
baltinerdist:feature/high-res-images
Open

Enhancement: High-resolution image masters + WebP rendition delivery#488
baltinerdist wants to merge 3 commits into
amtgard:masterfrom
baltinerdist:feature/high-res-images

Conversation

@baltinerdist

Copy link
Copy Markdown
Contributor

Summary

Replaces the upload-time downscale that destroyed image quality with a store-high-res-master / derive-optimized-renditions pipeline for player photos and player/park/kingdom (and shared unit/event) heraldry. Storage is cheap and one-time; delivery bandwidth is the recurring cost — so we stop conflating them.

Design spec is included in the branch: docs/superpowers/specs/2026-07-07-image-pipeline-masters-renditions-design.md.

What changed

Storage — high-res masters. Uploads are kept at up to 3000px (downscale-only, never upscale), PNG when transparent else JPEG q92, with a 6 MB post-encode reject ceiling that fires before touching disk — a too-large re-upload can't destroy the existing image. Client-side clampImageIfHuge replaces the ~340 KB squeeze, passing original bytes through untouched unless the longest edge exceeds 3000px.

Delivery — rendition set. Each upload also generates thumb (256px) + display (1024px) WebP renditions served as static files. A size-aware resolve_media_ext serves the right one per surface (nav/list/map → thumb, hero → display, lightbox → master) and falls back to the master when a rendition is missing, so un-backfilled images never break. heraldry-rendition-backfill.php derives renditions for existing images idempotently.

Transport / infra. nginx client_max_body_size 8M + webp added to the static cache regex; PHP upload_max_filesize/post_max_size raised to 8M in all three Dockerfiles.

Correctness / polish.

  • Remove paths now sweep all master + rendition variants (shared Common::unlink_image_set) — previously "removed" images kept being served from leftover renditions.
  • Over-ceiling rejections propagate to the user instead of reporting false success.
  • Lightbox enlarges the master, not the downscaled rendition.
  • Native alert() in the heraldry flows replaced with the in-page feedback pattern; stale "340 KB / Max 1 MB" upload hints corrected (banner hints intentionally left — banners are a separate pipeline).

Verification

  • Functional harness against the real GD pipeline: 15/15 (no-upscale honored, 5000×4000 clamps to 3000, an 11.6 MB image rejects while the existing file survives, PNG→JPEG re-upload sweeps stale files, resolve_media_ext resolves renditions and falls back to master).
  • Two adversarial QA passes (server core + client/wiring); all findings fixed and re-verified.
  • php -l clean on all changed PHP/templates; node --check clean on all changed JS. WebP confirmed available in the PHP 8.1 containers.

Known follow-ups

  • Scroll generator (separate branch): its builder reads the same model fields this PR re-sizes; at merge time it must request Size => 'master' for print-quality scrolls. Being handled on that branch.
  • A few immediate-redirect-on-success create paths carry the reject signal in the response but don't display it pre-redirect (would require altering the redirect); the common edit paths surface it.
  • Run heraldry-rendition-backfill.php at deploy time to generate renditions for existing images.

🤖 Generated with Claude Code

baltinerdist and others added 3 commits July 7, 2026 15:00
Design for storing high-quality image masters and deriving
size-optimized WebP renditions for player photos and player/park/
kingdom heraldry, replacing the current upload-time downscale that
destroys quality. Two-phase rollout; static serving preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Replace the upload-time downscale that destroyed image quality with a
store-high-res-master / derive-optimized-renditions pipeline for player
photos and player/park/kingdom (and shared unit/event) heraldry.

Storage: uploads are kept at up to 3000px (downscale-only, never upscale),
PNG when transparent else JPEG q92, with a 6MB post-encode reject ceiling
that fires before touching disk (a too-large re-upload can't destroy the
existing image). Client-side clampImageIfHuge replaces the ~340KB squeeze,
passing original bytes through untouched unless the longest edge >3000px.

Delivery: each upload also generates thumb (256px) + display (1024px) WebP
renditions served as static files; a size-aware resolve_media_ext serves
the right one per surface (nav/list/map = thumb, hero = display, lightbox =
master) and falls back to the master when a rendition is missing, so
un-backfilled images never break. heraldry-rendition-backfill.php derives
renditions for existing images idempotently.

Infra: nginx client_max_body_size 8M + webp cache header; PHP
upload_max_filesize/post_max_size raised to 8M in all Dockerfiles.

Also: remove paths now sweep all master+rendition variants (shared
Common::unlink_image_set); over-ceiling rejections propagate to the user
instead of reporting false success; lightbox enlarges the master not the
rendition; native alert() in heraldry flows replaced with the in-page
feedback pattern; stale upload-size UI hints corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
# Conflicts:
#	orkui/controller/controller.Admin.php
#	orkui/controller/controller.EventAjax.php
#	orkui/controller/controller.Kingdom.php
#	orkui/controller/controller.ParkAjax.php
#	orkui/model/model.Kingdom.php
#	system/lib/ork3/class.Event.php
#	system/lib/ork3/class.Kingdom.php
#	system/lib/ork3/class.Park.php
@baltinerdist

Copy link
Copy Markdown
Contributor Author

Branch brought current with master

Synced up to master @ 6ab028a3 (merge commit 53e4c754). This branch was 332 commits behind; now 0 behind / 3 ahead.

Why 8 files conflicted so heavily

Both sides independently ran the same PSR-12 reformat on the big shared files, so the conflicts were enormous but mostly noise. Running git diff -w upstream/master HEAD per file cancelled the formatting and exposed the real deltas — and in every case the pattern was the same: upstream extracted controller code into new domain/model classes while this branch edited the old inline copies.

Resolution was uniform: take upstream's new structure, then re-apply this branch's image-pipeline delta into the new home rather than back into the controller.

File Resolution
orkui/model/model.Kingdom.php Upstream's 3 new method blocks + the branch's 'Size' => 'thumb'/'display' args on both GetHeraldryUrl calls.
orkui/controller/controller.EventAjax.php Upstream (heraldry remove now delegates to EventPlanning->remove_heraldry). Branch's rendition cleanup ported into Heraldry::RemoveEventHeraldry() as Common::unlink_image_set($base). Without this the merge would have silently dropped rendition-file cleanup on event heraldry removal.
orkui/controller/controller.ParkAjax.php Upstream's removal of inline ghettocache busts (moved into Report::bustKingdomParkAverageCaches()); kept the branch's $r['Warning']warning JSON propagation.
orkui/controller/controller.Kingdom.php Upstream (both blocks now call Model_KingdomProfile). Branch's rendition logic ported into class.KingdomProfile.php — event heraldry and player avatar/heraldry now request Common::resolve_media_ext(..., 'thumb') with the ?v= mtime cache-buster instead of resolve_image_ext().
system/lib/ork3/class.Event.php Upstream wholesale; re-applied 'Size' => 'display' on both GetHeraldryUrl calls in GetEventDetails.
system/lib/ork3/class.Kingdom.php Upstream (QualTest configs, 'Parks' => array() guard, memcache flush); re-applied the branch's $heraldry_result capture + both "Heraldry was not saved" Warning blocks (create + edit).
system/lib/ork3/class.Park.php Upstream's Report::bustKingdomParkAverageCaches() refactor (that bust code predates the branch); re-applied the branch's $heraldry_result capture + Warning block in CreatePark.
orkui/controller/controller.Admin.php 35 hunks, all upstream's Model_AdminDashboard extraction vs the branch's old inline copies. Upstream wholesale, then the branch's 7 real edits re-applied: 2× 465000IMAGE_UPLOAD_MAX_BYTES, 2× $r = capture from SetHeraldry/SetImage, $r['Value'] on create-player, $r['Warning'] on edit-kingdom and create-park.

Worth noting this moved the image-pipeline logic out of controllers and into the domain layer, in line with the ongoing migration.

Verification

  • No conflict markers; class.Authorization.php not staged, identical to upstream, no bypass.
  • php -l clean on all 302 changed .php/.tpl across both merge parents.
  • Line-level cross-check (whitespace/brace-normalized): every branch-added line from the 48 branch-touched files is present in the merged tree, except the 5 EventAjax rendition-unlink lines deliberately relocated to class.Heraldry.php. Reverse direction: every upstream-added line is present except the 5 resolve_image_ext lines in class.KingdomProfile.php intentionally replaced with rendition-aware versions.
  • Independently re-confirmed post-push: Common::unlink_image_set present at 5 sites in class.Heraldry.php; class.KingdomProfile.php has 3 resolve_media_ext calls and zero leftover resolve_image_ext. Master/rendition distinction intact.
  • Merge recomputed from its parents with rerere disabled and diffed against what was pushed: exactly the 8 conflicted files plus the 2 deliberate ports, nothing unexplained. (Two other branches in this batch were hit by a stray rerere line from an unrelated recorded resolution — this one is clean.)

For a human

  1. Heraldry::RemoveEventHeraldry — upstream's two @unlink($base.'.jpg'/'.png') calls were swapped for Common::unlink_image_set($base). That's a superset (also removes _thumb.webp/.jpg, _display.webp/.jpg) but drops the @ error suppression the upstream author used. The sibling methods this branch wrote use unlink_image_set unsuppressed, so it's internally consistent — but it is a behavior change to an upstream-authored method.
  2. 🚨 Post-deploy memcache flush recommended. The kingdom roster and events-tab payloads are cached in GhettoCache; existing entries will keep serving the old master-image URLs until they expire.
  3. ParkAjax cache busting — upstream's Park::CreatePark now does its own. The branch's warning propagation was kept and its duplicate bust lines dropped; if the PR description claims the bust as this branch's work, note it was pre-existing merge-base code that upstream refactored.

PR now shows MERGEABLE / CLEAN.

🤖 Generated with Claude Code

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