MultiSpaccSDK/Tools/pilbmp2nes.py

210 lines
7.7 KiB
Python

#!/usr/bin/env python3
#
# Bitmap to multi-console CHR converter using Pillow, the
# Python Imaging Library
#
# Copyright 2014-2015 Damian Yerrick
# Copying and distribution of this file, with or without
# modification, are permitted in any medium without royalty
# provided the copyright notice and this notice are preserved.
# This file is offered as-is, without any warranty.
#
from __future__ import with_statement, print_function, unicode_literals
from PIL import Image
from time import sleep
def formatTilePlanar(tile, planemap, hflip=False, little=False):
"""Turn a tile into bitplanes.
Planemap opcodes:
10 -- bit 1 then bit 0 of each tile
0,1 -- planar interleaved by rows
0;1 -- planar interlaved by planes
0,1;2,3 -- SNES/PCE format
"""
hflip = 7 if hflip else 0
if (tile.size != (8, 8)):
return None
pixels = list(tile.getdata())
pixelrows = [pixels[i:i + 8] for i in range(0, 64, 8)]
if hflip:
for row in pixelrows:
row.reverse()
out = bytearray()
planemap = [[[int(c) for c in row]
for row in plane.split(',')]
for plane in planemap.split(';')]
# format: [tile-plane number][plane-within-row number][bit number]
# we have five (!) nested loops
# outermost: separate planes
# within separate planes: pixel rows
# within pixel rows: row planes
# within row planes: pixels
# within pixels: bits
for plane in planemap:
for pxrow in pixelrows:
for rowplane in plane:
rowbits = 1
thisrow = bytearray()
for px in pxrow:
for bitnum in rowplane:
rowbits = (rowbits << 1) | ((px >> bitnum) & 1)
if rowbits >= 0x100:
thisrow.append(rowbits & 0xFF)
rowbits = 1
out.extend(thisrow[::-1] if little else thisrow)
return bytes(out)
def pilbmp2chr(im, tileWidth=8, tileHeight=8,
formatTile=lambda im: formatTilePlanar(im, "0;1")):
"""Convert a bitmap image into a list of byte strings representing tiles."""
im.load()
(w, h) = im.size
outdata = []
for mt_y in range(0, h, tileHeight):
for mt_x in range(0, w, tileWidth):
metatile = im.crop((mt_x, mt_y,
mt_x + tileWidth, mt_y + tileHeight))
for tile_y in range(0, tileHeight, 8):
for tile_x in range(0, tileWidth, 8):
tile = metatile.crop((tile_x, tile_y,
tile_x + 8, tile_y + 8))
data = formatTile(tile)
outdata.append(data)
return outdata
def parse_argv(argv):
from optparse import OptionParser
parser = OptionParser(usage="usage: %prog [options] [-i] INFILE [-o] OUTFILE")
parser.add_option("-i", "--image", dest="infilename",
help="read image from INFILE", metavar="INFILE")
parser.add_option("-o", "--output", dest="outfilename",
help="write CHR data to OUTFILE", metavar="OUTFILE")
parser.add_option("-W", "--tile-width", dest="tileWidth",
help="set width of metatiles", metavar="HEIGHT",
type="int", default=8)
parser.add_option("--packbits", dest="packbits",
help="use PackBits RLE compression",
action="store_true", default=False)
parser.add_option("-H", "--tile-height", dest="tileHeight",
help="set height of metatiles", metavar="HEIGHT",
type="int", default=8)
parser.add_option("-1", dest="planes",
help="set 1bpp mode (default: 2bpp NES)",
action="store_const", const="0", default="0;1")
parser.add_option("--planes", dest="planes",
help="set the plane map (1bpp: 0) (NES: 0;1) (GB: 0,1) (SMS:0,1,2,3) (TG16/SNES: 0,1;2,3) (MD: 3210)")
parser.add_option("--hflip", dest="hflip",
help="horizontally flip all tiles (most significant pixel on right)",
action="store_true", default=False)
parser.add_option("--little", dest="little",
help="reverse the bytes within each row-plane (needed for GBA and a few others)",
action="store_true", default=False)
parser.add_option("--add", dest="addamt",
help="value to add to each pixel",
type="int", default=0)
parser.add_option("--add0", dest="addamt0",
help="value to add to pixels of color 0 (if different)",
type="int", default=None)
(options, args) = parser.parse_args(argv[1:])
tileWidth = int(options.tileWidth)
if tileWidth <= 0:
raise ValueError("tile width '%d' must be positive" % tileWidth)
tileHeight = int(options.tileHeight)
if tileHeight <= 0:
raise ValueError("tile height '%d' must be positive" % tileHeight)
# Fill unfilled roles with positional arguments
argsreader = iter(args)
try:
infilename = options.infilename
if infilename is None:
infilename = next(argsreader)
except StopIteration:
raise ValueError("not enough filenames")
outfilename = options.outfilename
if outfilename is None:
try:
outfilename = next(argsreader)
except StopIteration:
outfilename = '-'
if outfilename == '-':
import sys
if sys.stdout.isatty():
raise ValueError("cannot write CHR to terminal")
addamt, addamt0 = options.addamt, options.addamt0
if addamt0 is None: addamt0 = addamt
return (infilename, outfilename, tileWidth, tileHeight,
options.packbits, options.planes, options.hflip, options.little,
addamt, addamt0)
argvTestingMode = True
def make_stdout_binary():
"""Ensure that sys.stdout is in binary mode, with no newline translation."""
# Recipe from
# http://code.activestate.com/recipes/65443-sending-binary-data-to-stdout-under-windows/
# via http://stackoverflow.com/a/2374507/2738262
if sys.platform == "win32":
import os, msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
def main(argv=None):
import sys
if argv is None:
argv = sys.argv
if (argvTestingMode and len(argv) < 2
and sys.stdin.isatty() and sys.stdout.isatty()):
argv.extend(input('args:').split())
try:
(infilename, outfilename, tileWidth, tileHeight,
usePackBits, planes, hflip, little,
addamt, addamt0) = parse_argv(argv)
except Exception as e:
sys.stderr.write("%s: %s\n" % (argv[0], str(e)))
sys.exit(1)
im = Image.open(infilename)
# Subpalette shift
if addamt or addamt0:
px = bytearray(im.getdata())
for i in range(len(px)):
thispixel = px[i]
px[i] = thispixel + (addamt if thispixel else addamt0)
im.putdata(px)
outdata = pilbmp2chr(im, tileWidth, tileHeight,
lambda im: formatTilePlanar(im, planes, hflip, little))
outdata = b''.join(outdata)
if usePackBits:
from packbits import PackBits
sz = len(outdata) % 0x10000
outdata = PackBits(outdata).flush().tostring()
outdata = b''.join([chr(sz >> 8), chr(sz & 0xFF), outdata])
# Write output file
outfp = None
try:
if outfilename != '-':
outfp = open(outfilename, 'wb')
else:
outfp = sys.stdout
make_stdout_binary()
outfp.write(outdata)
finally:
if outfp and outfilename != '-':
outfp.close()
if __name__=='__main__':
main()