1 #!/usr/bin/env python 2 3 """ 4 Convert images for display in an Acorn Electron MODE 2 variant with a pixel 5 layout of I0RRGGBB, giving 128 colours instead of the usual 8 colours. 6 7 Copyright (C) 2015, 2017, 2018 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT ANY 15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 16 PARTICULAR PURPOSE. See the GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License along 19 with this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from os.path import split, splitext 23 import EXIF 24 import PIL.Image 25 import sys 26 27 def convert_image(im, output_filename, width, height, label="screendata"): 28 29 "Convert 'im' and write pixel values to 'output_filename'." 30 31 w, h = im.size 32 33 hpad = (width - w) / 2 34 leftpad = hpad; rightpad = width - w - hpad 35 vpad = (height - h) / 2 36 toppad = vpad; bottompad = height - h - vpad 37 38 data = iter(im.getdata()) 39 40 f = open(output_filename, "w") 41 try: 42 print >>f, """\ 43 .section .rodata, "a" 44 45 .globl %s 46 .globl %s_width 47 .globl %s_height 48 49 %s_width: 50 .word %d 51 %s_height: 52 .word %d 53 54 %s: 55 """ % (label, label, label, label, width, label, height, label) 56 57 word = [] 58 y = 0 59 60 while y < height: 61 x = 0 62 63 # Top and bottom padding. 64 65 if y < toppad or y >= height - bottompad: 66 67 while x < width: 68 word.append(0) 69 flush_word(f, word) 70 x += 1 71 72 flush_last_word(f, word) 73 74 # Lines with data. 75 76 else: 77 while x < width: 78 79 # Left and right padding. 80 81 if x < leftpad or x >= width - rightpad: 82 word.append(0) 83 84 # Data regions. 85 86 else: 87 r, g, b = data.next() 88 rm, gm, bm, i = get_values(r, g, b) 89 90 # Encode the byte value: I0RRGGBB. 91 92 word.insert(0, 93 # I -> D<7> 94 (i << 7) | 95 # R<7:6> -> D<5:4> 96 (rm >> 2) | 97 # G<7:6> -> D<3:2> 98 (gm >> 4) | 99 # B<7:6> -> D<1:0> 100 (bm >> 6)) 101 102 flush_word(f, word) 103 x += 1 104 105 flush_last_word(f, word) 106 107 y += 1 108 109 finally: 110 f.close() 111 112 def get_values(r, g, b): 113 114 "Return modified values for 'r', 'g' and 'b', plus an intensity bit." 115 116 rm = r & 0xc0 117 gm = g & 0xc0 118 bm = b & 0xc0 119 rd = r - rm 120 gd = g - gm 121 bd = b - bm 122 i = ((rd ** 2 + gd ** 2 + bd ** 2) ** 0.5) >= 32 and 1 or 0 123 return rm, gm, bm, i 124 125 def make_preview(im): 126 imp = PIL.Image.new("RGB", im.size) 127 data = [] 128 for r, g, b in im.getdata(): 129 rm, gm, bm, i = get_values(r, g, b) 130 r = rm + (i * 32) 131 g = gm + (i * 32) 132 b = bm + (i * 32) 133 data.append((r, g, b)) 134 imp.putdata(data) 135 return imp 136 137 def flush_last_word(f, word): 138 if word: 139 pad_word(word) 140 write_word(f, word) 141 del word[:] 142 143 def flush_word(f, word): 144 if len(word) == 4: 145 write_word(f, word) 146 del word[:] 147 148 def pad_word(word): 149 while len(word) < 4: 150 word.insert(0, 0) 151 152 def write_word(f, word): 153 print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) 154 155 def rotate_and_scale(exif, im, base_width, base_height, rotate, width, height): 156 157 """ 158 Using the given 'exif' information, rotate and scale image 'im' given the 159 indicated 'base_width' and 'base_height' constraints and any explicit 160 'rotate' indication. 161 162 The 'width' and 'height' indicate the final dimensions. 163 164 The returned image will be within the given 'width' and 'height', filling 165 either or both, and preserve its original aspect ratio. 166 """ 167 168 if rotate or exif and exif["Image Orientation"].values == [6L]: 169 im = im.rotate(270) 170 171 w, h = im.size 172 173 # Get the relationship between the base width and the image width. 174 175 wsf = float(base_width) / w 176 177 # Get the relationship between the base height and the image height. 178 179 hsf = float(base_height) / h 180 181 # Determine the maximal scaling down required to fit the image. 182 183 min_scale_factor = min(wsf, hsf) 184 185 # Determine the final scaling factors to yield the desired dimensions. 186 187 xscale = float(width) / base_width 188 yscale = float(height) / base_height 189 190 if min_scale_factor < 1: 191 width = int(min_scale_factor * w * xscale) 192 height = int(min_scale_factor * h * yscale) 193 return im.resize((width, height)) 194 elif scale_factor != 1: 195 width = int(w * wsf) 196 return im.resize((width, h)) 197 else: 198 return im 199 200 def get_parameter(options, flag, conversion, default, missing): 201 202 """ 203 From 'options', return any parameter following the given 'flag', applying 204 the 'conversion' which has the given 'default' if no valid parameter is 205 found, or returning the given 'missing' value if the flag does not appear at 206 all. 207 """ 208 209 try: 210 i = options.index(flag) 211 try: 212 return conversion(options[i+1]) 213 except (IndexError, ValueError): 214 return default 215 except ValueError: 216 return missing 217 218 # Main program. 219 220 if __name__ == "__main__": 221 222 # Test options. 223 224 if "--help" in sys.argv or len(sys.argv) < 3: 225 basename = split(sys.argv[0])[1] 226 print >>sys.stderr, """\ 227 Usage: 228 229 %s <input filename> <output filename> <label> [ <options> ] 230 231 Options are... 232 233 -W - Indicate the output width (default is 160) 234 235 -H - Indicate the output height (default is 256) 236 237 -p - Generate a preview with a filename based on the output filename 238 239 -r - Rotate the input image clockwise explicitly 240 (EXIF information is used otherwise) 241 """ % basename 242 sys.exit(1) 243 244 base_width = 320; width = 160 245 base_height = height = 256 246 247 input_filename, output_filename, label = sys.argv[1:4] 248 options = sys.argv[4:] 249 250 # Basic image properties. 251 252 width = get_parameter(options, "-W", int, width, width) 253 height = get_parameter(options, "-H", int, height, height) 254 rotate = "-r" in options 255 preview = "-p" in options 256 257 # Load the input image. 258 259 exif = EXIF.process_file(open(input_filename)) 260 im = PIL.Image.open(input_filename).convert("RGB") 261 im = rotate_and_scale(exif, im, base_width, base_height, rotate, 262 width, height) 263 264 # Generate an output image. 265 266 convert_image(im, output_filename, width, height, label) 267 268 # Generate a preview image if requested. 269 270 if preview: 271 _basename, ext = splitext(input_filename) 272 basename, _ext = splitext(output_filename) 273 preview_filename = "%s_preview%s" % (basename, ext) 274 imp = make_preview(im) 275 imp.save(preview_filename) 276 277 # vim: tabstop=4 expandtab shiftwidth=4