Skip to content

Instantly share code, notes, and snippets.

@methanoliver
Created August 27, 2022 05:42
Show Gist options
  • Save methanoliver/2668767d5332aea66a866d2b84692d1c to your computer and use it in GitHub Desktop.
Save methanoliver/2668767d5332aea66a866d2b84692d1c to your computer and use it in GitHub Desktop.
A primitive automatic loader for the simple kind of layered images
#!/usr/bin/env python3
"""
A script to produce (simple) layered images from existing non-layered ones,
by designating one image as the base, and then, for every other one,
only saving the pixels that are different, and filling the rest with #00000000.
You need an installation of Python 3 with Pillow library in it.
"""
import argparse
from PIL import Image
parser = argparse.ArgumentParser(description="""
Calculate and save differential images.
Resulting files will be named diff#<variant>.webp, which is what the diff-loader expects to see.""")
parser.add_argument('source',
metavar='SOURCE',
type=argparse.FileType('rb'),
help="Image that is considered to be the base.")
parser.add_argument('files',
metavar='FILE',
type=argparse.FileType('rb'),
nargs='+',
help='All other images.')
parser.add_argument('--overlay', '-o',
action='store_true',
help='Assume differentials need to be overlaid on the original first, '
'in case of, e.g. diff images being disembodied heads.')
parser.add_argument('--reverse', '-r',
action='store_true',
help="Do the reverse: go back from differential images to full ones.")
args = parser.parse_args()
# Output file format args.
SAVE_EXT = ".webp"
SAVE_FORMAT = {
"format": "webp",
"lossless": True,
"quality": 100,
"method": 6
}
source_image = Image.open(args.source).convert(mode="RGBA")
source_image_px = source_image.load()
for diff_file in args.files:
diff_image = Image.open(diff_file).convert(mode="RGBA")
if diff_image.size != source_image.size:
print("Image dimensions for {} do not match, skipping!".format(diff_file.name))
continue
if args.reverse:
# Reverse mode
if not diff_file.name.startswith("diff#"):
print("Image {} is not marked with 'diff#' filename tag, skipping.".format(diff_file.name))
continue
undiff_image = Image.alpha_composite(source_image, diff_image)
fn = diff_file.name.split("diff#", maxsplit=1)[1]
fn = fn.rsplit('.',maxsplit=1)[0] + SAVE_EXT
undiff_image.save(fn, **SAVE_FORMAT)
print(fn, "saved.")
else:
# Normal mode.
if args.overlay:
to_image = Image.alpha_composite(source_image, diff_image).load()
else:
to_image = diff_image.load()
difference = Image.new("RGBA", source_image.size, color=(0,0,0,0))
difference_px = difference.load()
for y in range(source_image.height):
for x in range(source_image.width):
r, g, b, a = to_image[x, y]
if a != 0 and source_image_px[x, y] != to_image[x, y]:
difference_px[x, y] = to_image[x, y]
fn = "diff#{}{}".format(diff_file.name.rsplit('.',maxsplit=1)[0], SAVE_EXT)
difference.save(fn, **SAVE_FORMAT)
print(fn, "saved.")
# Also crush and make a base file if it's not a webp.
if not args.reverse and not args.source.name.lower().endswith(".webp"):
source_image.save("base#"+args.source.name.rsplit('.',1)[0]+SAVE_EXT, **SAVE_FORMAT)
print("Done.")
# Differential image loader loads stacked images generated by
# differentiate.py from regular sprites.
#
# images/sprites/<character>/base#<expression_1>.<ext>
# images/sprites/<character>/diff#<expression_2>.<ext>
#
# diff# will be overlaid on base# and you will get
# <character> <expression_1>
# <character> <expression_2>
#
# Things like <character>/<outfit or pose>/<expression> would require
# a more complicated naming scheme, so aren't implemented, if you're
# up to something that complex, you're better using layeredimage language
# as intended.
init python hide:
# Where we keep the sprites.
SPRITE_SOURCE = "images/sprites/"
################
import collections
# Filter by extension: .png, .jpg, .webp
def is_image(fn):
return any(
fn.lower().endswith(x)
for x in ['.png', '.jpg', '.webp']
)
def is_hidden(fn):
if renpy.os.path.basename(fn).startswith("_"):
renpy.log("INFO: File '{}' was hidden".format(fn))
return True
return False
all_image_files = [x for x in renpy.list_files() if is_image(x) and not is_hidden(x)]
# Ignore differential images anywhere except images/sprites.
spritefiles = [x for x in all_image_files if x.startswith(SPRITE_SOURCE)]
# Count the individual directories, pick out those that have files tagged with "#"
# and sort those files into bins per directory.
spritedirs = collections.defaultdict(list)
for fn in spritefiles:
d, f = renpy.os.path.split(fn)
if "#" in f:
spritedirs[d].append(fn)
# And work on each directory:
for spritedir, sprites in spritedirs.items():
basefile = None
expressions = {}
# Make a dict of expressions.
# Rules are:
# * base#<expression>.<ext> is an expression.
# * base#.<ext> is a base file that is never shown alone.
# * diff#<expression>.<ext> is an expression overlaid on the nearest base file.
# * diff#.<ext> is an error and ignored.
# * More than one base# file per directory is a bug.
for sprite in sprites:
shortfn = renpy.os.path.basename(sprite).rsplit(".", 1)[0]
tag, exp = shortfn.split("#",1)
if tag == "base":
if basefile is not None:
renpy.log("ERROR: Differential image directory '{}' contains more than one base file.".format(spritedir))
basefile = sprite
if exp:
expressions[exp] = sprite
elif tag == "diff":
if exp:
expressions[exp] = sprite
else:
renpy.log("ERROR: Differential image '{}' has no expression tag.".format(sprite))
else:
renpy.log("WARNING: Unrecognized #-tag in '{}'".format(sprite))
if basefile is None:
renpy.log("ERROR: Differential image directory '{}' does not appear to contain a base file.".format(spritedir))
continue
# Determine the image name. We're assuming no spaces or other separators for now.
imagename = spritedir.replace(SPRITE_SOURCE, '')
# Build a list of attributes.
variants = []
for exp, sprite in expressions.items():
isBase = sprite == basefile
variants.append(Attribute(
"diff", exp,
# Trying to show a diff of absolutely nothing in case the base image
# is itself an expression causes an error message.
# Trying to plaster a blank alpha layer causes issues with sprite positioning.
# So we'll have to just duplicate it and show it twice.
image = sprite,
default = isBase))
renpy.image(imagename, LayeredImage([basefile] + variants, name=imagename))
renpy.log("Known images: {}".format(renpy.list_images()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment