← 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()