create a dither script for adding retro look to photos
This commit is contained in:
96
dither.py
Normal file
96
dither.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user