Initial work on grayscale PNG support for update-palette

This commit is contained in:
Kevin Caccamo 2023-11-17 15:59:12 -05:00
parent 2da59702c2
commit 8aa60e87e9
No known key found for this signature in database
GPG key ID: 483F90E1F56A8723

View file

@ -16,16 +16,25 @@
# - pypng: https://gitlab.com/drj11/pypng (retrieved Sept 10 2023) # - pypng: https://gitlab.com/drj11/pypng (retrieved Sept 10 2023)
import argparse import argparse
import array
from functools import reduce from functools import reduce
from itertools import zip_longest from itertools import cycle, zip_longest
import struct import struct
from operator import eq
import os import os
from os.path import dirname, relpath, realpath, join, normpath from os.path import dirname, relpath, realpath, join, normpath
import png import png
from sys import argv from sys import argv
import zlib import zlib
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" # Misc. utility functions
# https://docs.python.org/3.8/library/itertools.html#itertools-recipes
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
# Parse the command line arguments, and return a dict with the arguments # Parse the command line arguments, and return a dict with the arguments
def parse_args(): def parse_args():
@ -51,14 +60,6 @@ def parse_args():
return args return args
# https://docs.python.org/3.8/library/itertools.html#itertools-recipes
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
# Compare the old palette and the new palette, and return a dict with the # Compare the old palette and the new palette, and return a dict with the
# differences. # differences.
def compare_palettes(directory, new_palette): def compare_palettes(directory, new_palette):
@ -115,8 +116,38 @@ def compare_palettes(directory, new_palette):
for colour, indices in old_palette_duplicates.items(): for colour, indices in old_palette_duplicates.items():
if all(map(lambda i: i in old_to_new.keys(), indices)): if all(map(lambda i: i in old_to_new.keys(), indices)):
replaced[colour] = old_to_new[min(indices)] replaced[colour] = old_to_new[min(indices)]
else: print("replaced", replaced)
replaced[colour] = None
# Find the closest colour in the old palette for each grayscale colour
def is_grayscale(colour):
return all(map(eq, colour[1], cycle((colour[1][0],))))
# def to_grayscale(colour):
# return colour[0] if is_grayscale(colour) else colour
grayscale_colours_in_palette = dict(
map(lambda g: (g[0], g[1][0]),
filter(is_grayscale, enumerate(new_palette)))
)
def closest_grayscale(grayscale):
nonlocal grayscale_colours_in_palette
distances = sorted(map(
lambda kv: (kv[0], abs(kv[1] - grayscale)),
grayscale_colours_in_palette.items()
), key=lambda kv: kv[1])
closest = distances[0][1]
distances = sorted(
filter(lambda kv: kv[1] == closest, distances),
key=lambda kv: kv[0])
return distances[0][0]
# This is a map from grayscale to palette index
gray_map = array.array('B', map(closest_grayscale, range(256)))
# Turn it into a grayscale to colour map, in case any grayscale colours
# were changed in the new palette
gray_map = dict(
map(lambda g: (g[0], old_to_new[g[1]]),
filter(
lambda g: g[1] in old_to_new,
enumerate(gray_map))))
print(len(gray_map), gray_map)
# Replace the keys in old_to_new, which are indices, with the old palette # Replace the keys in old_to_new, which are indices, with the old palette
# colours they correspond to. This way, we have a colour-to-colour dict. # colours they correspond to. This way, we have a colour-to-colour dict.
@ -125,12 +156,12 @@ def compare_palettes(directory, new_palette):
replaced.get(old_palette[iv[0]], iv[1])), replaced.get(old_palette[iv[0]], iv[1])),
old_to_new.items() old_to_new.items()
))) )))
return old_to_new return old_to_new, gray_map
# "Stolen" from the map-color-index script # "Stolen" from the map-color-index script
# Process a directory recursively for PNG files. # Process a directory recursively for PNG files.
def process_dir(colour_map, dry_run, directory, palette): def process_dir(colour_map, gray_map, dry_run, directory, palette):
pngs_changed_count = 0 pngs_changed_count = 0
pngs_examined_count = 0 pngs_examined_count = 0
@ -140,12 +171,12 @@ def process_dir(colour_map, dry_run, directory, palette):
continue continue
png_path = os.path.join(dirpath, png_base) png_path = os.path.join(dirpath, png_base)
pngs_examined_count += 1 pngs_examined_count += 1
if process_png(colour_map, png_path, dry_run, directory): if process_png(colour_map, gray_map, png_path, dry_run, directory):
pngs_changed_count += 1 pngs_changed_count += 1
# Process a PNG file in place. # Process a PNG file in place.
def process_png(colour_map, png_path, dry, directory): def process_png(colour_map, gray_map, png_path, dry, directory):
# Read the PNG file # Read the PNG file
png_reader = png.Reader(filename=png_path) png_reader = png.Reader(filename=png_path)
@ -216,8 +247,9 @@ def process_png(colour_map, png_path, dry, directory):
return modified, new_rows return modified, new_rows
# Modify the PLTE chunk if necessary, and check if the PLTE modifications def maybe_modify_grayscale_image(rows, channels):
# affect the IDAT chunk. pass
plte_modified = False plte_modified = False
idat_modified = False idat_modified = False
width, height, rows, info = png_reader.read() width, height, rows, info = png_reader.read()
@ -232,6 +264,8 @@ def process_png(colour_map, png_path, dry, directory):
has_alpha = info.get("alpha", False) has_alpha = info.get("alpha", False)
channels = info.get("planes") channels = info.get("planes")
if is_paletted: if is_paletted:
# Modify the PLTE chunk if necessary, and check if the PLTE
# modifications affect the colours used in the IDAT chunk.
plte_modified, new_palette = maybe_modify_plte(info["palette"]) plte_modified, new_palette = maybe_modify_plte(info["palette"])
if plte_modified: if plte_modified:
idat_modified = any(map(is_paletted_colour_changed, rows)) idat_modified = any(map(is_paletted_colour_changed, rows))
@ -239,6 +273,8 @@ def process_png(colour_map, png_path, dry, directory):
new_palette = None new_palette = None
idat_modified, rows = maybe_modify_truecolour_image( idat_modified, rows = maybe_modify_truecolour_image(
rows, channels, transparent_colour) rows, channels, transparent_colour)
else:
idat_modified, rows = maybe_modify_grayscale_image(rows, channels)
# Write the modified PNG file # Write the modified PNG file
if idat_modified: if idat_modified:
@ -257,8 +293,11 @@ def process_png(colour_map, png_path, dry, directory):
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args() args = parse_args()
directory = normpath(join(dirname(realpath(argv[0])), "..")) directory = normpath(join(dirname(realpath(argv[0])), ".."))
comparison = compare_palettes(directory, args.palette) colour_map, gray_map = compare_palettes(directory, args.palette)
process_dir(comparison, directory=directory, **vars(args)) print("The rest of the script is disabled for development right now")
exit(0)
process_dir(colour_map, gray_map, directory=directory, **vars(args))
# Replace old playpal-base.lmp # Replace old playpal-base.lmp
if not args.dry_run: if not args.dry_run:
playpal_base_path = ( playpal_base_path = (