mirror of
https://gitlab.com/octospacc/MultiSpaccSDK
synced 2025-06-05 22:09:21 +02:00
Update scripts and NES compatibility, add PNG to NES CHR convert
This commit is contained in:
209
Tools/pilbmp2nes.py
Normal file
209
Tools/pilbmp2nes.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/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()
|
Reference in New Issue
Block a user