From f39a5f181051a3e708a5c1bcf5454aeff1a0d57c Mon Sep 17 00:00:00 2001 From: Eric Branlund Date: Tue, 19 Jan 2021 18:46:54 -0800 Subject: [PATCH] Modified to handle Hengband's conversion requirements: allow use of a separate bmp input file to specify the transparency mask (as is used for Adam Bolt's tile set) and adjust the defaults for the --terrain option so the conversion of the original tile set is easy. --- lib/xtra/graf/osx_bmp2png.py | 151 ++++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 51 deletions(-) diff --git a/lib/xtra/graf/osx_bmp2png.py b/lib/xtra/graf/osx_bmp2png.py index 49517b166..0c7afd08d 100644 --- a/lib/xtra/graf/osx_bmp2png.py +++ b/lib/xtra/graf/osx_bmp2png.py @@ -5,42 +5,56 @@ Usage python osx_bmp2png.py input_file output_file [options] Options +--mask maskfile + Reads the mask (zero for transparent pixels; non-zero for opaque pixels) + from the bmp file, maskfile. maskfile must have the same x and y + dimensions as input_file but need not match the color space or bit depth. + --mask overrides --tcoord, --terrain, and --transparent. + --tcoord hcoord vcoord Reads the color to be treated as transparent for non-terrain tiles from the position (hcoord, vcoord) in the tile set. Both coordinates are non-negative integers indexed from zero. Is overridden by - --transparent if that option is set. When not set, the default is to - read the color to be treated as transparent from (0,0). + --transparent or --mask if either of those options are set. When not + set, the default is to read the color to be treated as transparent from + (0,0). --terrain lx1 ly1 ux1 uy1 ... Sets bounding boxes for parts of the tile set that are to be treated as completely opaque (i.e. as background terrain). Each bound box is given by the coordinates for the lower corner, (lx,ly), and upper corner, (ux,uy), with ux > lx and uy > ly. If not set, the default - is to use two bounding boxes, (0,0) to (496,16) and (192,192) to - (224,208), which are the terrain tiles in Michrochasm's tile set. + is to use seven bounding boxes, (0,0) to (256,16), (64,16) to + (256,24), (216,80) to (240,88), (48,272) to (152,280), (176,272) to + (192,280), (0,632) to (256,688), and (0,712) to (256,736) which are the + terrain tiles in Hengband's old 8x8 tile set. Is overridden by the + --mask option if that option is set. --transparent red green blue Sets the color, as RGB values between 0 and 255, which will be treated as completely transparent for non-terrain tiles. If not set, uses - --tcoord or its default to get the color. + --tcoord or its default to get the color. Is overridden by the --mask + option if that option is set. Example -This is the conversion that was used to ready Microchasm's tile set for -the OS X front end: - python osx_bmp2png.py 16x16.bmp 16x16_microchasm.png +This is the conversion that was used to prepare the old tile set for the +OS X front end: + python osx_bmp2png.py 8x8.bmp 8x8.png That is equivalent to - python osx_bmp2png.py 16x16.bmp 16x16_microchasm.png --tcoord 0 0 \ - --terrain 0 0 496 16 192 192 224 208 + python osx_bmp2png.py 8x8.bmp 8x8.png -tcoord 0 0 \ + -terrain 0 0 256 16 64 16 256 24 216 80 240 88 48 272 152 280 \ + 176 272 192 280 0 632 256 688 0 712 256 736 + +This is the conversion that was used to prepare Adam Bolt's tile set for the +OS X front end: + python osx_bmp2png.py 16x16.bmp 16x16.png --mask 16x16-mask.bmp Requirements argparse, PIL (Python imaging library) or equivalent, and numpy (1.7 or later) -For Michrochasm's tile set the conversion is required because -CGImageSourceCreateWithURL() does not handle the bmp format and because -there appears to be no NSCompositingOperation which is equivalent to -'composite this image with the background treating x as transparent'. +The conversions are required because CGImageSourceCreateWithURL() does not +handle the bmp format. """ import argparse @@ -52,10 +66,12 @@ aparser.add_argument('input_file', type=str, help='the path to the input tile set') aparser.add_argument('output_file', type=str, help='the path to the converted tile set') -aparser.add_argument('--tcoord', type=int, nargs=2, default=(0,0), +aparser.add_argument('--mask', default='', + help='File name for BMP file with transparency mask') +aparser.add_argument('--tcoord', type=int, nargs=2, default=(0,32), help='Position to be read for transparent color') aparser.add_argument('--terrain', type=int, nargs='+', - default=(0,0,496,16,192,192,224,208), + default=(0,0,256,16,64,16,256,24,216,80,240,88,48,272,152,280,176,272,192,280,0,632,256,688,0,712,256,736), help='Bounding box coordinates for terrain tiles') aparser.add_argument('--transparent', type=int, nargs=3, help='RGB value to be treated as transparent') @@ -64,7 +80,7 @@ args = aparser.parse_args() tin = PIL.Image.open(args.input_file) -if len(args.terrain) % 4 != 0: +if args.mask == '' and len(args.terrain) % 4 != 0: raise IndexError('Need four values for each bounding box.') for i in range(0, len(args.terrain), 4): if (args.terrain[i] > args.terrain[i + 2] or @@ -77,54 +93,87 @@ for i in range(0, len(args.terrain), 4): palette = tin.getpalette() -if args.transparent: - trind = None - trarr = numpy.array(args.transparent) -else: - if (args.tcoord[0] < 0 or args.tcoord[0] >= tin.width or - args.tcoord[1] < 0 or args.tcoord[1] >= tin.height): - raise IndexError('Coordinates for transparent pixel are out of bounds') - if palette: - trind = tin.getpixel(tuple(args.tcoord)) +if args.mask == '': + if args.transparent: + trind = None + trarr = numpy.array(args.transparent) else: - trarr = numpy.array(tin.getpixel(args.tcoord)[0:3]) - -# Set up the mask for the portions of the tile set not containing terrain. -arrt = numpy.ones((tin.height, tin.width), dtype=numpy.dtype(bool)) -ind0, ind1 = numpy.meshgrid(numpy.linspace(0, tin.height - 1, tin.height), - numpy.linspace(0, tin.width - 1, tin.width), indexing='ij') -for i in range(0, len(args.terrain), 4): - arrt = numpy.logical_and(arrt, numpy.logical_or(numpy.logical_or( - ind1 < args.terrain[i], ind0 < args.terrain[i + 1]), - numpy.logical_or(ind1 >= args.terrain[i + 2], - ind0 >= args.terrain[i + 3]))) -del ind0, ind1 + if (args.tcoord[0] < 0 or args.tcoord[0] >= tin.width or + args.tcoord[1] < 0 or + args.tcoord[1] >= tin.height): + raise IndexError('Coordinates for transparent pixel are out of bounds') + if palette: + trind = tin.getpixel(tuple(args.tcoord)) + else: + trarr = numpy.array(tin.getpixel(args.tcoord)[0:3]) arrin = numpy.array(tin) + +# Copy over the RGB components. arrout = numpy.empty((tin.height,tin.width,4), dtype='u1') if palette: parr = numpy.reshape(numpy.array(palette), (len(palette) // 3, 3)) arrout[:,:,0:3] = parr[arrin] - if trind: +else: + arrout[:,:,0:3] = arrin[:,:,0:3] + + +# Set up the alpha channel. +if args.mask == '': + # Set up mask for portions of the data set not containing terrain. + arrt = numpy.ones((tin.height, tin.width), dtype=numpy.dtype(bool)) + ind0, ind1 = numpy.meshgrid(numpy.linspace(0, tin.height - 1, tin.height), + numpy.linspace(0, tin.width - 1, tin.width), indexing='ij') + for i in range(0, len(args.terrain), 4): + arrt = numpy.logical_and(arrt, + numpy.logical_or(numpy.logical_or( + ind1 < args.terrain[i], ind0 < args.terrain[i + 1]), + numpy.logical_or(ind1 >= args.terrain[i + 2], + ind0 >= args.terrain[i + 3]))) + del ind0, ind1 + + # Combine with where the input is equal to transparent color to get + # the transparency mask. + if palette: + if trind is not None: + arrout[:,:,3] = numpy.where( + numpy.logical_and(arrt, arrin == trind), + numpy.zeros((tin.height, tin.width), dtype='u1'), + 255 * numpy.ones((tin.height, tin.width), dtype='u1')) + else: + arrout[:,:,3] = numpy.where( + numpy.logical_and(arrt, numpy.logical_and( + arrout[:,:,0] == trarr[0], numpy.logical_and( + arrout[:,:,1] == trarr[1], arrout[:,:,2] == trarr[2]))), + numpy.zeros((tin.height, tin.width), dtype='u1'), + 255 * numpy.ones((tin.height, tin.width), dtype='u1')) + else: arrout[:,:,3] = numpy.where( - numpy.logical_and(arrt, arrin == trind), + numpy.logical_and(arrt, numpy.logical_and( + arrin[:,:,0] == trarr[0], numpy.logical_and( + arrin[:,:,1] == trarr[1], arrin[:,:,2] == trarr[2]))), + numpy.zeros((tin.height, tin.width), dtype='u1'), + 255 * numpy.ones((tin.height, tin.width), dtype='u1')) + del arrt + +else: + tmask = PIL.Image.open(args.mask) + if tmask.height != tin.height or tmask.width != tin.width: + raise RuntimeError('Dimensions of mask do not match input dimensions') + arrmask = numpy.array(tmask) + if tmask.getpalette(): + arrout[:,:,3] = numpy.where(arrmask == 0, numpy.zeros((tin.height, tin.width), dtype='u1'), 255 * numpy.ones((tin.height, tin.width), dtype='u1')) else: arrout[:,:,3] = numpy.where( - numpy.logical_and(arrt, numpy.logical_and( - arrout[:,:,0] == trarr[0], numpy.logical_and( - arrout[:,:,1] == trarr[1], arrout[:,:,2] == trarr[2]))), + numpy.logical_and(arrmask[:,:,0] == 0, + numpy.logical_and(arrmask[:,:,1] == 0, + arrmask[:,:,2] == 0)), numpy.zeros((tin.height, tin.width), dtype='u1'), 255 * numpy.ones((tin.height, tin.width), dtype='u1')) -else: - arrout[:,:,0:3] = arrin[:,:,0:3] - arrout[:,:,3] = numpy.where( - numpy.logical_and(arrt, numpy.logical_and( - arrin[:,:,0] == trarr[0], numpy.logical_and( - arrin[:,:,1] == trarr[1], arrin[:,:,2] == trarr[2]))), - numpy.zeros((tin.height, tin.width), dtype='u1'), - 255 * numpy.ones((tin.height, tin.width), dtype='u1')) + del arrmask, tmask + tout = PIL.Image.fromarray(arrout) tout.save(args.output_file) -- 2.11.0