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