← back to Greenland China

scripts/extract-colors.py

82 lines

#!/usr/bin/env python3
"""
Dominant-hex extractor for the Greenland staging catalog (offline / read-only).
Downloads each SKU's cover image from Greenland's CDN, shrinks it to a single
representative pixel, and writes {id: "#rrggbb"} to staging/greenland-colors.json
so the viewer can offer Light->Dark / Dark->Light / Color-Wheel sorts + a per-card
color dot (Steve's standing sort-skill pattern, sips/PIL variant).

Resumable: re-running skips ids already in the cache. Safe to Ctrl-C; it flushes
every 25 rows.
"""
import json, os, io, sys, urllib.request, urllib.parse
from PIL import Image

HERE = os.path.dirname(os.path.abspath(__file__))
STAGING = os.path.join(HERE, "..", "staging")
SRC = os.path.join(STAGING, "greenland.jsonl")
OUT = os.path.join(STAGING, "greenland-colors.json")
REFERER = "http://m.greenlandwallcoverings.com/"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"

def load_rows():
    rows = []
    with open(SRC, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                rows.append(json.loads(line))
            except Exception:
                pass
    return rows

def safe_url(url):
    # re-encode the query so filenames with spaces/parens (e.g. "G0162 (2)_x.jpg") are valid
    sp = urllib.parse.urlsplit(url)
    q = urllib.parse.parse_qsl(sp.query, keep_blank_values=True)
    return urllib.parse.urlunsplit((sp.scheme, sp.netloc, sp.path, urllib.parse.urlencode(q), sp.fragment))

def dominant_hex(url):
    url = safe_url(url)
    req = urllib.request.Request(url, headers={"User-Agent": UA, "Referer": REFERER})
    with urllib.request.urlopen(req, timeout=20) as r:
        raw = r.read()
    im = Image.open(io.BytesIO(raw)).convert("RGB")
    # median-ish representative: shrink to 16x16 then to 1x1 (box filter = average,
    # the two-step avoids one hot pixel dominating on busy patterns)
    im = im.resize((16, 16)).resize((1, 1))
    r, g, b = im.getpixel((0, 0))
    return "#%02x%02x%02x" % (r, g, b)

def main():
    rows = load_rows()
    cache = {}
    if os.path.exists(OUT):
        try:
            cache = json.load(open(OUT))
        except Exception:
            cache = {}
    todo = [r for r in rows if not cache.get(str(r.get("id"))) and (r.get("cover_image") or (r.get("images") or [None])[0])]
    print(f"{len(rows)} rows · {len(cache)} cached · {len(todo)} to fetch", flush=True)
    done = 0
    for r in todo:
        rid = str(r.get("id"))
        url = r.get("cover_image") or (r.get("images") or [None])[0]
        try:
            cache[rid] = dominant_hex(url)
        except Exception as e:
            cache[rid] = None
            print(f"  id={rid} FAILED: {e}", flush=True)
        done += 1
        if done % 25 == 0:
            json.dump(cache, open(OUT, "w"))
            print(f"  {done}/{len(todo)} …", flush=True)
    json.dump(cache, open(OUT, "w"), indent=0)
    ok = sum(1 for v in cache.values() if v)
    print(f"DONE · {ok}/{len(cache)} colors resolved → {OUT}", flush=True)

if __name__ == "__main__":
    main()