#!/usr/bin/env python3 """ dither.py — Retro photo processor for Dangerous Wonder Converts JPGs in photos/ to palette-reduced, ordered-dithered PNGs using the 216 web-safe color palette. Skips already-processed PNGs. Usage: python3 dither.py # process all unprocessed photos python3 dither.py --force # re-process everything from backups python3 dither.py --help # show usage """ import argparse import os import shutil import sys from PIL import Image PALETTE_BYTES = [] for r in range(0, 256, 51): for g in range(0, 256, 51): for b in range(0, 256, 51): PALETTE_BYTES.extend([r, g, b]) PALETTE_IMG = Image.new("P", (1, 1)) PALETTE_IMG.putpalette(PALETTE_BYTES + [0, 0, 0] * (256 - 216)) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PHOTOS_DIR = os.path.join(SCRIPT_DIR, "photos") BACKUP_DIR = os.path.join(SCRIPT_DIR, "photos-backup") def process_image(src_path, dst_path): img = Image.open(src_path).convert("RGB") quantized = img.quantize(palette=PALETTE_IMG, dither=Image.Dither.ORDERED) quantized.save(dst_path, "PNG", optimize=True) orig_kb = os.path.getsize(src_path) // 1024 new_kb = os.path.getsize(dst_path) // 1024 print( f" {os.path.basename(src_path)} -> {os.path.basename(dst_path)} " f"({orig_kb}KB -> {new_kb}KB)" ) def ensure_backup(src_path): os.makedirs(BACKUP_DIR, exist_ok=True) dst = os.path.join(BACKUP_DIR, os.path.basename(src_path)) if not os.path.exists(dst): shutil.copy2(src_path, dst) def main(): parser = argparse.ArgumentParser( description="Convert photos to web-safe palette with ordered dithering" ) parser.add_argument( "--force", action="store_true", help="Re-process all images from photos-backup/" ) args = parser.parse_args() if args.force: jpgs = sorted( f for f in os.listdir(BACKUP_DIR) if f.lower().endswith((".jpg", ".jpeg")) ) if not jpgs: print("No JPG files found in photos-backup/") sys.exit(1) src_dir = BACKUP_DIR print(f"Re-processing {len(jpgs)} images from backup...\n") else: jpgs = sorted( f for f in os.listdir(PHOTOS_DIR) if f.lower().endswith((".jpg", ".jpeg")) ) if not jpgs: print("No new JPG files to process in photos/") sys.exit(0) src_dir = PHOTOS_DIR print(f"Processing {len(jpgs)} new image(s)...\n") for fname in jpgs: src = os.path.join(src_dir, fname) ensure_backup(src) png_name = os.path.splitext(fname)[0] + ".png" dst = os.path.join(PHOTOS_DIR, png_name) process_image(src, dst) if src_dir == PHOTOS_DIR: os.remove(src) print(f"\nDone. Originals in photos-backup/, dithered PNGs in photos/.") if __name__ == "__main__": main()