diff --git a/dither.py b/dither.py new file mode 100644 index 0000000..59ca6cc --- /dev/null +++ b/dither.py @@ -0,0 +1,96 @@ +#!/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()