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, strip, bytealign, options, label="screendata"): 28 29 "Convert 'im' and write pixel values to 'output_filename'." 30 31 w, h = im.size 32 33 if strip: 34 leftpad = rightpad = toppad = bottompad = 0 35 width = w; height = h 36 else: 37 hpad = (width - w) / 2 38 leftpad = hpad; rightpad = width - w - hpad 39 vpad = (height - h) / 2 40 toppad = vpad; bottompad = height - h - vpad 41 42 data = iter(im.getdata()) 43 44 f = open(output_filename, "w") 45 try: 46 print >>f, """\ 47 .section .rodata, "a" 48 49 /* Options: 50 %s 51 */ 52 53 .globl %s 54 .globl %s_width 55 .globl %s_height 56 57 %s_width: 58 .word %d 59 %s_height: 60 .word %d 61 62 %s: 63 """ % (options, label, label, label, label, width, label, height, label) 64 65 word = [] 66 y = 0 67 68 while y < height: 69 x = 0 70 71 # Top and bottom padding. 72 73 if y < toppad or y >= height - bottompad: 74 75 while x < width: 76 word.append(0) 77 flush_word(f, word, bytealign) 78 x += 1 79 80 flush_last_word(f, word, bytealign) 81 82 # Lines with data. 83 84 else: 85 while x < width: 86 87 # Left and right padding. 88 89 if x < leftpad or x >= width - rightpad: 90 word.append(0) 91 92 # Data regions. 93 94 else: 95 r, g, b = data.next() 96 rm, gm, bm, i = get_values(r, g, b) 97 98 # Encode the byte value: I0RRGGBB. 99 100 word.insert(0, 101 # I -> D<7> 102 (i << 7) | 103 # R<7:6> -> D<5:4> 104 (rm >> 2) | 105 # G<7:6> -> D<3:2> 106 (gm >> 4) | 107 # B<7:6> -> D<1:0> 108 (bm >> 6)) 109 110 flush_word(f, word, bytealign) 111 x += 1 112 113 flush_last_word(f, word, bytealign) 114 115 y += 1 116 117 finally: 118 f.close() 119 120 def get_values(r, g, b): 121 122 "Return modified values for 'r', 'g' and 'b', plus an intensity bit." 123 124 rm = r & 0xc0 125 gm = g & 0xc0 126 bm = b & 0xc0 127 rd = r - rm 128 gd = g - gm 129 bd = b - bm 130 i = ((rd ** 2 + gd ** 2 + bd ** 2) ** 0.5) >= 32 and 1 or 0 131 return rm, gm, bm, i 132 133 def make_preview(im): 134 imp = PIL.Image.new("RGB", im.size) 135 data = [] 136 for r, g, b in im.getdata(): 137 rm, gm, bm, i = get_values(r, g, b) 138 r = rm + (i * 32) 139 g = gm + (i * 32) 140 b = bm + (i * 32) 141 data.append((r, g, b)) 142 imp.putdata(data) 143 return imp 144 145 def flush_last_word(f, word, bytealign): 146 if word: 147 if bytealign: 148 write_bytes(f, word) 149 else: 150 pad_word(word) 151 write_word(f, word) 152 del word[:] 153 154 def flush_word(f, word, bytealign): 155 if len(word) == 4: 156 if bytealign: 157 write_bytes(f, word) 158 else: 159 write_word(f, word) 160 del word[:] 161 162 def pad_word(word): 163 while len(word) < 4: 164 word.insert(0, 0) 165 166 def write_bytes(f, word): 167 while word: 168 print >>f, ".byte 0x%02x" % word.pop() 169 170 def write_word(f, word): 171 print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) 172 173 def rotate_and_scale(exif, im, base_width, base_height, rotate, width, height): 174 175 """ 176 Using the given 'exif' information, rotate and scale image 'im' given the 177 indicated 'base_width' and 'base_height' constraints and any explicit 178 'rotate' indication. 179 180 The 'width' and 'height' indicate the final dimensions. 181 182 The returned image will be within the given 'width' and 'height', filling 183 either or both, and preserve its original aspect ratio. 184 """ 185 186 if rotate or exif and exif["Image Orientation"].values == [6L]: 187 im = im.rotate(270) 188 189 w, h = im.size 190 191 # Get the relationship between the base width and the image width. 192 193 wsf = float(base_width) / w 194 195 # Get the relationship between the base height and the image height. 196 197 hsf = float(base_height) / h 198 199 # Determine the maximal scaling down required to fit the image. 200 201 min_scale_factor = min(wsf, hsf) 202 203 # Determine the final scaling factors to yield the desired dimensions. 204 205 xscale = float(width) / base_width 206 yscale = float(height) / base_height 207 208 if min_scale_factor < 1: 209 width = int(min_scale_factor * w * xscale) 210 height = int(min_scale_factor * h * yscale) 211 return im.resize((width, height)) 212 elif scale_factor != 1: 213 width = int(w * wsf) 214 return im.resize((width, h)) 215 else: 216 return im 217 218 def get_parameter(options, flag, conversion, default, missing): 219 220 """ 221 From 'options', return any parameter following the given 'flag', applying 222 the 'conversion' which has the given 'default' if no valid parameter is 223 found, or returning the given 'missing' value if the flag does not appear at 224 all. 225 """ 226 227 try: 228 i = options.index(flag) 229 try: 230 return conversion(options[i+1]) 231 except (IndexError, ValueError): 232 return default 233 except ValueError: 234 return missing 235 236 # Main program. 237 238 if __name__ == "__main__": 239 240 # Test options. 241 242 if "--help" in sys.argv or len(sys.argv) < 3: 243 basename = split(sys.argv[0])[1] 244 print >>sys.stderr, """\ 245 Usage: 246 247 %s <input filename> <output filename> <label> [ <options> ] 248 249 Preview options: 250 251 -p - Generate a preview with a filename based on the output filename 252 253 Size options: 254 255 -W - Indicate the output width (default is 160) 256 257 -H - Indicate the output height (default is 256) 258 259 -S - Employ the width and height to define the appropriate output ratio but 260 refine the final width and height and strip away padding 261 262 Transformation options: 263 264 -r - Rotate the input image clockwise explicitly 265 (EXIF information is used otherwise) 266 267 Output options: 268 269 -b - Use bytes instead of padded words for each line of the output 270 """ % basename 271 sys.exit(1) 272 273 base_width = 320; width = 160 274 base_height = height = 256 275 276 input_filename, output_filename, label = sys.argv[1:4] 277 options = sys.argv[4:] 278 279 # Basic image properties. 280 281 width = get_parameter(options, "-W", int, width, width) 282 height = get_parameter(options, "-H", int, height, height) 283 bytealign = "-b" in options 284 rotate = "-r" in options 285 preview = "-p" in options 286 strip = "-S" in options 287 288 # Load the input image. 289 290 exif = EXIF.process_file(open(input_filename)) 291 im = PIL.Image.open(input_filename).convert("RGB") 292 im = rotate_and_scale(exif, im, base_width, base_height, rotate, 293 width, height) 294 295 # Generate an output image. 296 297 convert_image(im, output_filename, width, height, strip, bytealign, options, label) 298 299 # Generate a preview image if requested. 300 301 if preview: 302 _basename, ext = splitext(input_filename) 303 basename, _ext = splitext(output_filename) 304 preview_filename = "%s_preview%s" % (basename, ext) 305 imp = make_preview(im) 306 imp.save(preview_filename) 307 308 # vim: tabstop=4 expandtab shiftwidth=4