Files
www.dangerouswonder.org/dither.py

97 lines
2.9 KiB
Python

#!/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()