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