# HG changeset patch # User Paul Boddie # Date 1495065506 -7200 # Node ID 27660220db5a93774830afa973fa461493deddd5 # Parent 9e55bb93989ce6711284f141bbaa5afa3691b785 Added a tool for simple image conversion to the appropriate pixel data format. diff -r 9e55bb93989c -r 27660220db5a tools/makeimage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/makeimage.py Thu May 18 01:58:26 2017 +0200 @@ -0,0 +1,232 @@ +#!/usr/bin/env python + +""" +Convert images for display in an Acorn Electron MODE 2 variant with a pixel +layout of R0RGGGBB, giving 128 colours instead of the usual 8 colours. + +Copyright (C) 2015, 2017 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see . + +---- + +ImageMagick can be used to dither images to a 232 palette before conversion: + +convert in.png -ordered-dither threshold,4,8,4 out.png +""" + +from os.path import split, splitext +import EXIF +import PIL.Image +import sys + +def convert_image(im, output_filename, width, height): + + "Convert 'im' and write pixel values to 'output_filename'." + + w, h = im.size + + hpad = (width - w) / 2 + leftpad = hpad; rightpad = width - w - hpad + vpad = (height - h) / 2 + toppad = vpad; bottompad = height - h - vpad + + data = iter(im.getdata()) + + f = open(output_filename, "w") + try: + word = [] + y = 0 + + while y < height: + x = 0 + + # Top and bottom padding. + + if y < toppad or y >= height - bottompad: + + while x < width: + word.append(0) + flush_word(f, word) + x += 1 + + flush_last_word(f, word) + + # Lines with data. + + else: + while x < width: + + # Left and right padding. + + if x < leftpad or x >= width - rightpad: + word.append(0) + + # Data regions. + + else: + r, g, b = data.next() + word.insert(0, + # R<7> -> D<7> + (r & 0x80) | + # R<6> -> D<5> + ((r & 0x40) >> 1) | + # G<7:5> -> D<4:2> + ((g >> 5) << 2) | + # B<7:6> -> D<1:0> + (b >> 6)) + + flush_word(f, word) + x += 1 + + flush_last_word(f, word) + + y += 1 + + finally: + f.close() + +def make_preview(im): + imp = PIL.Image.new("RGB", im.size) + data = [] + for r, g, b in im.getdata(): + data.append((((r >> 6) << 6), ((g >> 5) << 5), (b >> 6) << 6)) + imp.putdata(data) + return imp + +def flush_last_word(f, word): + if word: + pad_word(word) + write_word(f, word) + del word[:] + +def flush_word(f, word): + if len(word) == 4: + write_word(f, word) + del word[:] + +def pad_word(word): + while len(word) < 4: + word.insert(0, 0) + +def write_word(f, word): + print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) + +def rotate_and_scale(exif, im, width, height, rotate, scale_factor): + + """ + Using the given 'exif' information, rotate and scale image 'im' given the + indicated 'width' and 'height' constraints and any explicit 'rotate' + indication. The returned image will be within the given 'width' and + 'height', filling either or both, and preserve its original aspect ratio. + """ + + if rotate or exif and exif["Image Orientation"].values == [6L]: + im = im.rotate(270) + + w, h = im.size + + # Get the relationship between the base width and the image width. + + width_scale_factor = (width / scale_factor) / w + height_scale_factor = float(height) / h + min_scale_factor = min(width_scale_factor, height_scale_factor) + + if min_scale_factor < 1: + width = int(min_scale_factor * w * scale_factor) + height = int(min_scale_factor * h) + return im.resize((width, height)) + elif scale_factor != 1: + width = int(w * scale_factor) + return im.resize((width, h)) + else: + return im + +def get_parameter(options, flag, conversion, default, missing): + + """ + From 'options', return any parameter following the given 'flag', applying + the 'conversion' which has the given 'default' if no valid parameter is + found, or returning the given 'missing' value if the flag does not appear at + all. + """ + + try: + i = options.index(flag) + try: + return conversion(options[i+1]) + except (IndexError, ValueError): + return default + except ValueError: + return missing + +# Main program. + +if __name__ == "__main__": + + # Test options. + + if "--help" in sys.argv or len(sys.argv) < 3: + basename = split(sys.argv[0])[1] + print >>sys.stderr, """\ +Usage: + +%s [ ] + +Options are... + +-W - Indicate the output width (default is 160) + +-p - Generate a preview with a filename based on the output filename + +-r - Rotate the input image clockwise explicitly + (EXIF information is used otherwise) +""" % basename + sys.exit(1) + + base_width = 320; width = 160 + base_height = height = 256 + + input_filename, output_filename = sys.argv[1:3] + options = sys.argv[3:] + + # Basic image properties. + + width = get_parameter(options, "-W", int, width, width) + rotate = "-r" in options + preview = "-p" in options + + # Determine any differing horizontal scale factor. + + scale_factor = float(width) / base_width + + # Load the input image. + + exif = EXIF.process_file(open(input_filename)) + im = PIL.Image.open(input_filename).convert("RGB") + im = rotate_and_scale(exif, im, width, height, rotate, scale_factor) + + # Generate an output image. + + convert_image(im, output_filename, width, height) + + # Generate a preview image if requested. + + if preview: + _basename, ext = splitext(input_filename) + basename, _ext = splitext(output_filename) + preview_filename = "%s_preview%s" % (basename, ext) + imp = make_preview(im) + imp.save(preview_filename) + +# vim: tabstop=4 expandtab shiftwidth=4