doxa-maps/library/scripts/posters/make-previews.py
doxa-maps-88655c70 79bd6d2ac3
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).

- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
  public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
  is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
  before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
  to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
  (all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
  the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
  there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
  step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
  start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
  excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
  checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
  could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
  the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
  check:stale-sweep-claim's command (it was already in //scripts).

Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
2026-09-25 17:41:56 +00:00

66 lines
3 KiB
Python

#!/usr/bin/env python3
"""make-previews.py — turn the rendered 3000x3000 posters into the TWO WebP sets
the app ships.
python3 make-previews.py
-> plugin-designs/my-upg-100-list/assets/previews/<zone>.webp 560px card thumbnails
-> data/posters/<zone>.webp 1500px the download
WHY TWO SIZES
The wall card shows a small thumbnail; tapping "Poster Map" must hand the user
THE SAME POSTER at a usable size. The button used to re-render a live Mapbox
canvas through useMapPoster.js — a completely different artefact from the preview
above it, carrying none of the template. Serving the pre-rendered image makes
preview and download identical BY CONSTRUCTION, and it works on Android, where a
Node generator cannot run at all.
SIZE BUDGET (measured): 1500px q80 WebP is ~217 KB per poster, 5.9 MB for all 28 —
small enough to ship in the APK, and 5in at 300dpi / 10in at 150dpi, a real print.
The 3000px PNG masters (51 MB) stay OUT of the app; export-public.mjs sends those
to a sibling folder for desktop/CDN use.
Uses PIL rather than sharp: sharp is not installed in this repo, PIL is present.
"""
import os, sys, glob
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, 'out', 'posters')
REPO = os.path.abspath(os.path.join(HERE, '..', '..'))
# WHICH APP the previews belong to. The country-posters app renders from the SAME
# generator into the SAME out/posters, so the only thing that differs is where the
# WebPs land — parameterize rather than fork this script.
# PROFILE=my-upg-100-list (default)
# PROFILE=country-posters python3 make-previews.py
# ONLY filters by filename prefix so a country run does not re-emit the 28 zone
# posters sitting in the same folder (and vice versa).
PROFILE = os.environ.get('PROFILE', 'my-upg-100-list')
ONLY = os.environ.get('ONLY_PREFIX', '')
THUMB = os.path.join(REPO, 'plugin-designs', PROFILE, 'assets', 'previews')
FULL = os.path.join(REPO, 'data', 'posters') # a SOURCE: the build stages it into <tree>/assets/posters on request
os.makedirs(THUMB, exist_ok=True)
os.makedirs(FULL, exist_ok=True)
pngs = sorted(glob.glob(os.path.join(SRC, (ONLY + '*.png') if ONLY else '*.png')))
if not pngs:
sys.exit(f'[make-previews] no PNGs in {SRC} — run `bun run posters` then `bun run posters:shoot` first.')
tkb = fkb = 0
for p in pngs:
zone = os.path.basename(p)[:-4]
im = Image.open(p).convert('RGB')
t = im.copy(); t.thumbnail((560, 560), Image.LANCZOS)
tp = os.path.join(THUMB, zone + '.webp')
t.save(tp, 'WEBP', quality=82, method=6)
tkb += os.path.getsize(tp) / 1024
f = im.copy(); f.thumbnail((1500, 1500), Image.LANCZOS)
fp = os.path.join(FULL, zone + '.webp')
f.save(fp, 'WEBP', quality=80, method=6)
fkb += os.path.getsize(fp) / 1024
print(f'[make-previews] {len(pngs)} posters')
print(f' thumbnails -> plugin-designs/.../assets/previews {tkb/1024:5.1f} MB (inlined in the bundle)')
print(f' full size -> data/posters {fkb/1024:5.1f} MB (served + shipped in the APK)')