97 lines
2.9 KiB
Python
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()
|