Skip to content

Commit

Permalink
Merge pull request #10 from Gadgetoid/raw-image-output
Browse files Browse the repository at this point in the history
Support raw, headerless image output
  • Loading branch information
Gadgetoid committed Mar 11, 2020
2 parents ea07218 + 9c0b185 commit 5209c73
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 32 deletions.
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -27,6 +27,8 @@ python3 setup.py develop

All image assets are handled by Pillow so most image formats will work, be careful with lossy formats since they may add unwanted colours to your palette and leave you with oversized assets.

By default images will be packed into bits depending on the palette size. A 16-colour palette would use 4-bits-per-pixel. Use `--raw` to output raw pixel data, suitable for loading directly into a `blit::Surface`.

Supported formats:

* 8bit PNG .png
Expand All @@ -36,7 +38,8 @@ Options:

* `palette` - Image or palette file (Adobe .act, Pro Motion NG .pal, GIMP .gpl) containing the asset colour palette
* `transparent` - Transparent colour (if palette isn't an RGBA image), should be either hex (FFFFFF) or R,G,B (255,255,255)
* `packed` - (Defaults to true) will pack the output asset into bits depending on the palette size. A 16-colour palette would use 4-bits-per-pixel.
* `raw` - Output raw pixel data, suitable for loading into a `blit::Surface`
* `raw_format` - Format for raw data, either RGB or RGBA
* `strict` - Only allow colours that are present in the palette image/file

## Packing Map Assets
Expand Down
5 changes: 4 additions & 1 deletion src/README.md
Expand Up @@ -27,6 +27,8 @@ python3 setup.py develop

All image assets are handled by Pillow so most image formats will work, be careful with lossy formats since they may add unwanted colours to your palette and leave you with oversized assets.

By default images will be packed into bits depending on the palette size. A 16-colour palette would use 4-bits-per-pixel. Use `--raw` to output raw pixel data, suitable for loading directly into a `blit::Surface`.

Supported formats:

* 8bit PNG .png
Expand All @@ -36,7 +38,8 @@ Options:

* `palette` - Image or palette file (Adobe .act, Pro Motion NG .pal, GIMP .gpl) containing the asset colour palette
* `transparent` - Transparent colour (if palette isn't an RGBA image), should be either hex (FFFFFF) or R,G,B (255,255,255)
* `packed` - (Defaults to true) will pack the output asset into bits depending on the palette size. A 16-colour palette would use 4-bits-per-pixel.
* `raw` - Output raw pixel data, suitable for loading into a `blit::Surface`
* `raw_format` - Format for raw data, either RGB or RGBA
* `strict` - Only allow colours that are present in the palette image/file

## Packing Map Assets
Expand Down
4 changes: 2 additions & 2 deletions src/tests/test_image_cli.py
Expand Up @@ -50,7 +50,7 @@ def test_image_png_cli_unpacked(parsers, test_input_file):

image = image.ImageAsset(subparser)

args = parser.parse_args(['image', '--input_file', test_input_file.name, '--packed', 'no', '--output_format', 'c_header'])
args = parser.parse_args(['image', '--input_file', test_input_file.name, '--raw', '--raw_format', 'RGBA', '--output_format', 'c_header'])

image.run(args)

Expand All @@ -62,7 +62,7 @@ def test_image_png_cli_packed(parsers, test_input_file):

image = image.ImageAsset(subparser)

args = parser.parse_args(['image', '--input_file', test_input_file.name, '--packed', '--output_format', 'c_header'])
args = parser.parse_args(['image', '--input_file', test_input_file.name, '--output_format', 'c_header'])

image.run(args)

Expand Down
62 changes: 34 additions & 28 deletions src/ttblit/asset/image.py
Expand Up @@ -20,28 +20,28 @@ def __init__(self, parser):
self.options.update({
'palette': (Palette, Palette()),
'transparent': Colour,
'packed': (str, 'yes'),
'raw': (bool, False),
'raw_format': (str, 'RGBA'),
'strict': (bool, False)
})

AssetBuilder.__init__(self, parser)

self.palette = None
self.transparent = None
self.packed = True
self.raw = False
self.raw_format = 'RGBA'
self.strict = False

self.parser.add_argument('--palette', type=type_palette, default=None, help='Image or palette file of colours to use')
self.parser.add_argument('--transparent', type=Colour, help='Transparent colour')
self.parser.add_argument('--packed', type=str, nargs='?', default='yes', choices=('yes', 'no'), help='Pack into bits depending on palette colour count')
self.parser.add_argument('--raw', action='store_true', help='Output just raw binary data')
self.parser.add_argument('--raw_format', type=str, help='Raw output format', choices=('RGB', 'RGBA'), default='RGBA')
self.parser.add_argument('--strict', action='store_true', help='Reject colours not in the palette')

def prepare(self, args):
AssetBuilder.prepare(self, args)

if type(self.packed) is not bool:
self.packed = self.packed == 'yes'

if self.transparent is not None:
r, g, b = self.transparent
p = self.palette.set_transparent_colour(r, g, b)
Expand All @@ -53,10 +53,11 @@ def prepare(self, args):
def quantize_image(self, input_data):
if self.strict and len(self.palette) == 0:
raise TypeError("Attempting to enforce strict colours with an empty palette, did you really want to do this?")
# Since we already have bytes, we need to pass PIL an io.BytesIO object
image = Image.open(io.BytesIO(input_data)).convert('RGBA')

image = self.load_image(input_data)
w, h = image.size
output_image = Image.new('P', (w, h))

for y in range(h):
for x in range(w):
r, g, b, a = image.getpixel((x, y))
Expand All @@ -67,33 +68,38 @@ def quantize_image(self, input_data):

return output_image

def load_image(self, input_data):
# Since we already have bytes, we need to pass PIL an io.BytesIO object
return Image.open(io.BytesIO(input_data)).convert('RGBA')

def to_binary(self, input_data):
image = self.quantize_image(input_data)
if self.raw:
return self.load_image(input_data).tobytes()

else:
image = self.quantize_image(input_data)

# TODO Image format needs rewriting to support more than 255 palette entries, this is a bug!
# This fix allows `test_image_png_cli_strict_palette_pal` to pass.
self.palette.entries = self.palette.entries[:255]
# TODO Image format needs rewriting to support more than 255 palette entries, this is a bug!
# This fix allows `test_image_png_cli_strict_palette_pal` to pass.
self.palette.entries = self.palette.entries[:255]

palette_data = self.palette.tobytes()
palette_data = self.palette.tobytes()

if self.packed:
bit_length = self.palette.bit_length()
image_data = BitArray().join(BitArray(uint=x, length=bit_length) for x in image.tobytes()).tobytes()
else:
image_data = image.tobytes()

palette_size = struct.pack('<B', len(self.palette))
palette_size = struct.pack('<B', len(self.palette))

payload_size = struct.pack('<H', len(image_data))
image_size = struct.pack('<HH', *image.size)
payload_size = struct.pack('<H', len(image_data))
image_size = struct.pack('<HH', *image.size)

data = bytes('SPRITEPK' if self.packed else 'SPRITERW', encoding='utf-8')
data += payload_size
data += image_size
data += bytes([0x10, 0x00, 0x10, 0x00]) # Rows/cols deprecated
data += b'\x02' # Pixel format
data += palette_size
data += palette_data
data += image_data
data = bytes('SPRITEPK', encoding='utf-8')
data += payload_size
data += image_size
data += bytes([0x10, 0x00, 0x10, 0x00]) # Rows/cols deprecated
data += b'\x02' # Pixel format
data += palette_size
data += palette_data
data += image_data

return data
return data

0 comments on commit 5209c73

Please sign in to comment.