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 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): 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 .globl screendata 44 45 .section .flash, "a" 46 """ 47 48 word = [] 49 y = 0 50 51 while y < height: 52 x = 0 53 54 # Top and bottom padding. 55 56 if y < toppad or y >= height - bottompad: 57 58 while x < width: 59 word.append(0) 60 flush_word(f, word) 61 x += 1 62 63 flush_last_word(f, word) 64 65 # Lines with data. 66 67 else: 68 while x < width: 69 70 # Left and right padding. 71 72 if x < leftpad or x >= width - rightpad: 73 word.append(0) 74 75 # Data regions. 76 77 else: 78 r, g, b = data.next() 79 rm, gm, bm, i = get_values(r, g, b) 80 81 # Encode the byte value: I0RRGGBB. 82 83 word.insert(0, 84 # I -> D<7> 85 (i << 7) | 86 # R<7:6> -> D<5:4> 87 (rm >> 2) | 88 # G<7:6> -> D<3:2> 89 (gm >> 4) | 90 # B<7:6> -> D<1:0> 91 (bm >> 6)) 92 93 flush_word(f, word) 94 x += 1 95 96 flush_last_word(f, word) 97 98 y += 1 99 100 finally: 101 f.close() 102 103 def get_values(r, g, b): 104 105 "Return modified values for 'r', 'g' and 'b', plus an intensity bit." 106 107 rm = r & 0xc0 108 gm = g & 0xc0 109 bm = b & 0xc0 110 rd = r - rm 111 gd = g - gm 112 bd = b - bm 113 i = ((rd ** 2 + gd ** 2 + bd ** 2) ** 0.5) >= 32 and 1 or 0 114 return rm, gm, bm, i 115 116 def make_preview(im): 117 imp = PIL.Image.new("RGB", im.size) 118 data = [] 119 for r, g, b in im.getdata(): 120 rm, gm, bm, i = get_values(r, g, b) 121 r = rm + (i * 32) 122 g = gm + (i * 32) 123 b = bm + (i * 32) 124 data.append((r, g, b)) 125 imp.putdata(data) 126 return imp 127 128 def flush_last_word(f, word): 129 if word: 130 pad_word(word) 131 write_word(f, word) 132 del word[:] 133 134 def flush_word(f, word): 135 if len(word) == 4: 136 write_word(f, word) 137 del word[:] 138 139 def pad_word(word): 140 while len(word) < 4: 141 word.insert(0, 0) 142 143 def write_word(f, word): 144 print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) 145 146 def rotate_and_scale(exif, im, width, height, rotate, scale_factor): 147 148 """ 149 Using the given 'exif' information, rotate and scale image 'im' given the 150 indicated 'width' and 'height' constraints and any explicit 'rotate' 151 indication. The returned image will be within the given 'width' and 152 'height', filling either or both, and preserve its original aspect ratio. 153 """ 154 155 if rotate or exif and exif["Image Orientation"].values == [6L]: 156 im = im.rotate(270) 157 158 w, h = im.size 159 160 # Get the relationship between the base width and the image width. 161 162 width_scale_factor = (width / scale_factor) / w 163 height_scale_factor = float(height) / h 164 min_scale_factor = min(width_scale_factor, height_scale_factor) 165 166 if min_scale_factor < 1: 167 width = int(min_scale_factor * w * scale_factor) 168 height = int(min_scale_factor * h) 169 return im.resize((width, height)) 170 elif scale_factor != 1: 171 width = int(w * scale_factor) 172 return im.resize((width, h)) 173 else: 174 return im 175 176 def get_parameter(options, flag, conversion, default, missing): 177 178 """ 179 From 'options', return any parameter following the given 'flag', applying 180 the 'conversion' which has the given 'default' if no valid parameter is 181 found, or returning the given 'missing' value if the flag does not appear at 182 all. 183 """ 184 185 try: 186 i = options.index(flag) 187 try: 188 return conversion(options[i+1]) 189 except (IndexError, ValueError): 190 return default 191 except ValueError: 192 return missing 193 194 # Main program. 195 196 if __name__ == "__main__": 197 198 # Test options. 199 200 if "--help" in sys.argv or len(sys.argv) < 3: 201 basename = split(sys.argv[0])[1] 202 print >>sys.stderr, """\ 203 Usage: 204 205 %s <input filename> <output filename> [ <options> ] 206 207 Options are... 208 209 -W - Indicate the output width (default is 160) 210 211 -p - Generate a preview with a filename based on the output filename 212 213 -r - Rotate the input image clockwise explicitly 214 (EXIF information is used otherwise) 215 """ % basename 216 sys.exit(1) 217 218 base_width = 320; width = 160 219 base_height = height = 256 220 221 input_filename, output_filename = sys.argv[1:3] 222 options = sys.argv[3:] 223 224 # Basic image properties. 225 226 width = get_parameter(options, "-W", int, width, width) 227 rotate = "-r" in options 228 preview = "-p" in options 229 230 # Determine any differing horizontal scale factor. 231 232 scale_factor = float(width) / base_width 233 234 # Load the input image. 235 236 exif = EXIF.process_file(open(input_filename)) 237 im = PIL.Image.open(input_filename).convert("RGB") 238 im = rotate_and_scale(exif, im, width, height, rotate, scale_factor) 239 240 # Generate an output image. 241 242 convert_image(im, output_filename, width, height) 243 244 # Generate a preview image if requested. 245 246 if preview: 247 _basename, ext = splitext(input_filename) 248 basename, _ext = splitext(output_filename) 249 preview_filename = "%s_preview%s" % (basename, ext) 250 imp = make_preview(im) 251 imp.save(preview_filename) 252 253 # vim: tabstop=4 expandtab shiftwidth=4