1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/tools/makeimage.py Sat Oct 27 23:45:09 2018 +0200
1.3 @@ -0,0 +1,277 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Convert images for display in an Acorn Electron MODE 2 variant with a pixel
1.8 +layout of I0RRGGBB, giving 128 colours instead of the usual 8 colours.
1.9 +
1.10 +Copyright (C) 2015, 2017, 2018 Paul Boddie <paul@boddie.org.uk>
1.11 +
1.12 +This program is free software; you can redistribute it and/or modify it under
1.13 +the terms of the GNU General Public License as published by the Free Software
1.14 +Foundation; either version 3 of the License, or (at your option) any later
1.15 +version.
1.16 +
1.17 +This program is distributed in the hope that it will be useful, but WITHOUT ANY
1.18 +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
1.19 +PARTICULAR PURPOSE. See the GNU General Public License for more details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along
1.22 +with this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from os.path import split, splitext
1.26 +import EXIF
1.27 +import PIL.Image
1.28 +import sys
1.29 +
1.30 +def convert_image(im, output_filename, width, height, label="screendata"):
1.31 +
1.32 + "Convert 'im' and write pixel values to 'output_filename'."
1.33 +
1.34 + w, h = im.size
1.35 +
1.36 + hpad = (width - w) / 2
1.37 + leftpad = hpad; rightpad = width - w - hpad
1.38 + vpad = (height - h) / 2
1.39 + toppad = vpad; bottompad = height - h - vpad
1.40 +
1.41 + data = iter(im.getdata())
1.42 +
1.43 + f = open(output_filename, "w")
1.44 + try:
1.45 + print >>f, """\
1.46 +.section .rodata, "a"
1.47 +
1.48 +.globl %s
1.49 +.globl %s_width
1.50 +.globl %s_height
1.51 +
1.52 +%s_width:
1.53 +.word %d
1.54 +%s_height:
1.55 +.word %d
1.56 +
1.57 +%s:
1.58 +""" % (label, label, label, label, width, label, height, label)
1.59 +
1.60 + word = []
1.61 + y = 0
1.62 +
1.63 + while y < height:
1.64 + x = 0
1.65 +
1.66 + # Top and bottom padding.
1.67 +
1.68 + if y < toppad or y >= height - bottompad:
1.69 +
1.70 + while x < width:
1.71 + word.append(0)
1.72 + flush_word(f, word)
1.73 + x += 1
1.74 +
1.75 + flush_last_word(f, word)
1.76 +
1.77 + # Lines with data.
1.78 +
1.79 + else:
1.80 + while x < width:
1.81 +
1.82 + # Left and right padding.
1.83 +
1.84 + if x < leftpad or x >= width - rightpad:
1.85 + word.append(0)
1.86 +
1.87 + # Data regions.
1.88 +
1.89 + else:
1.90 + r, g, b = data.next()
1.91 + rm, gm, bm, i = get_values(r, g, b)
1.92 +
1.93 + # Encode the byte value: I0RRGGBB.
1.94 +
1.95 + word.insert(0,
1.96 + # I -> D<7>
1.97 + (i << 7) |
1.98 + # R<7:6> -> D<5:4>
1.99 + (rm >> 2) |
1.100 + # G<7:6> -> D<3:2>
1.101 + (gm >> 4) |
1.102 + # B<7:6> -> D<1:0>
1.103 + (bm >> 6))
1.104 +
1.105 + flush_word(f, word)
1.106 + x += 1
1.107 +
1.108 + flush_last_word(f, word)
1.109 +
1.110 + y += 1
1.111 +
1.112 + finally:
1.113 + f.close()
1.114 +
1.115 +def get_values(r, g, b):
1.116 +
1.117 + "Return modified values for 'r', 'g' and 'b', plus an intensity bit."
1.118 +
1.119 + rm = r & 0xc0
1.120 + gm = g & 0xc0
1.121 + bm = b & 0xc0
1.122 + rd = r - rm
1.123 + gd = g - gm
1.124 + bd = b - bm
1.125 + i = ((rd ** 2 + gd ** 2 + bd ** 2) ** 0.5) >= 32 and 1 or 0
1.126 + return rm, gm, bm, i
1.127 +
1.128 +def make_preview(im):
1.129 + imp = PIL.Image.new("RGB", im.size)
1.130 + data = []
1.131 + for r, g, b in im.getdata():
1.132 + rm, gm, bm, i = get_values(r, g, b)
1.133 + r = rm + (i * 32)
1.134 + g = gm + (i * 32)
1.135 + b = bm + (i * 32)
1.136 + data.append((r, g, b))
1.137 + imp.putdata(data)
1.138 + return imp
1.139 +
1.140 +def flush_last_word(f, word):
1.141 + if word:
1.142 + pad_word(word)
1.143 + write_word(f, word)
1.144 + del word[:]
1.145 +
1.146 +def flush_word(f, word):
1.147 + if len(word) == 4:
1.148 + write_word(f, word)
1.149 + del word[:]
1.150 +
1.151 +def pad_word(word):
1.152 + while len(word) < 4:
1.153 + word.insert(0, 0)
1.154 +
1.155 +def write_word(f, word):
1.156 + print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word)
1.157 +
1.158 +def rotate_and_scale(exif, im, base_width, base_height, rotate, width, height):
1.159 +
1.160 + """
1.161 + Using the given 'exif' information, rotate and scale image 'im' given the
1.162 + indicated 'base_width' and 'base_height' constraints and any explicit
1.163 + 'rotate' indication.
1.164 +
1.165 + The 'width' and 'height' indicate the final dimensions.
1.166 +
1.167 + The returned image will be within the given 'width' and 'height', filling
1.168 + either or both, and preserve its original aspect ratio.
1.169 + """
1.170 +
1.171 + if rotate or exif and exif["Image Orientation"].values == [6L]:
1.172 + im = im.rotate(270)
1.173 +
1.174 + w, h = im.size
1.175 +
1.176 + # Get the relationship between the base width and the image width.
1.177 +
1.178 + wsf = float(base_width) / w
1.179 +
1.180 + # Get the relationship between the base height and the image height.
1.181 +
1.182 + hsf = float(base_height) / h
1.183 +
1.184 + # Determine the maximal scaling down required to fit the image.
1.185 +
1.186 + min_scale_factor = min(wsf, hsf)
1.187 +
1.188 + # Determine the final scaling factors to yield the desired dimensions.
1.189 +
1.190 + xscale = float(width) / base_width
1.191 + yscale = float(height) / base_height
1.192 +
1.193 + if min_scale_factor < 1:
1.194 + width = int(min_scale_factor * w * xscale)
1.195 + height = int(min_scale_factor * h * yscale)
1.196 + return im.resize((width, height))
1.197 + elif scale_factor != 1:
1.198 + width = int(w * wsf)
1.199 + return im.resize((width, h))
1.200 + else:
1.201 + return im
1.202 +
1.203 +def get_parameter(options, flag, conversion, default, missing):
1.204 +
1.205 + """
1.206 + From 'options', return any parameter following the given 'flag', applying
1.207 + the 'conversion' which has the given 'default' if no valid parameter is
1.208 + found, or returning the given 'missing' value if the flag does not appear at
1.209 + all.
1.210 + """
1.211 +
1.212 + try:
1.213 + i = options.index(flag)
1.214 + try:
1.215 + return conversion(options[i+1])
1.216 + except (IndexError, ValueError):
1.217 + return default
1.218 + except ValueError:
1.219 + return missing
1.220 +
1.221 +# Main program.
1.222 +
1.223 +if __name__ == "__main__":
1.224 +
1.225 + # Test options.
1.226 +
1.227 + if "--help" in sys.argv or len(sys.argv) < 3:
1.228 + basename = split(sys.argv[0])[1]
1.229 + print >>sys.stderr, """\
1.230 +Usage:
1.231 +
1.232 +%s <input filename> <output filename> <label> [ <options> ]
1.233 +
1.234 +Options are...
1.235 +
1.236 +-W - Indicate the output width (default is 160)
1.237 +
1.238 +-H - Indicate the output height (default is 256)
1.239 +
1.240 +-p - Generate a preview with a filename based on the output filename
1.241 +
1.242 +-r - Rotate the input image clockwise explicitly
1.243 + (EXIF information is used otherwise)
1.244 +""" % basename
1.245 + sys.exit(1)
1.246 +
1.247 + base_width = 320; width = 160
1.248 + base_height = height = 256
1.249 +
1.250 + input_filename, output_filename, label = sys.argv[1:4]
1.251 + options = sys.argv[4:]
1.252 +
1.253 + # Basic image properties.
1.254 +
1.255 + width = get_parameter(options, "-W", int, width, width)
1.256 + height = get_parameter(options, "-H", int, height, height)
1.257 + rotate = "-r" in options
1.258 + preview = "-p" in options
1.259 +
1.260 + # Load the input image.
1.261 +
1.262 + exif = EXIF.process_file(open(input_filename))
1.263 + im = PIL.Image.open(input_filename).convert("RGB")
1.264 + im = rotate_and_scale(exif, im, base_width, base_height, rotate,
1.265 + width, height)
1.266 +
1.267 + # Generate an output image.
1.268 +
1.269 + convert_image(im, output_filename, width, height, label)
1.270 +
1.271 + # Generate a preview image if requested.
1.272 +
1.273 + if preview:
1.274 + _basename, ext = splitext(input_filename)
1.275 + basename, _ext = splitext(output_filename)
1.276 + preview_filename = "%s_preview%s" % (basename, ext)
1.277 + imp = make_preview(im)
1.278 + imp.save(preview_filename)
1.279 +
1.280 +# vim: tabstop=4 expandtab shiftwidth=4