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 R0RGGGBB, 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 23 ImageMagick can be used to dither images to a 232 palette before conversion: 24 25 convert in.png -ordered-dither threshold,4,8,4 out.png 26 """ 27 28 from os.path import split, splitext 29 import EXIF 30 import PIL.Image 31 import sys 32 33 def convert_image(im, output_filename, width, height): 34 35 "Convert 'im' and write pixel values to 'output_filename'." 36 37 w, h = im.size 38 39 hpad = (width - w) / 2 40 leftpad = hpad; rightpad = width - w - hpad 41 vpad = (height - h) / 2 42 toppad = vpad; bottompad = height - h - vpad 43 44 data = iter(im.getdata()) 45 46 f = open(output_filename, "w") 47 try: 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 word.insert(0, 80 # R<7> -> D<7> 81 (r & 0x80) | 82 # R<6> -> D<5> 83 ((r & 0x40) >> 1) | 84 # G<7:5> -> D<4:2> 85 ((g >> 5) << 2) | 86 # B<7:6> -> D<1:0> 87 (b >> 6)) 88 89 flush_word(f, word) 90 x += 1 91 92 flush_last_word(f, word) 93 94 y += 1 95 96 finally: 97 f.close() 98 99 def make_preview(im): 100 imp = PIL.Image.new("RGB", im.size) 101 data = [] 102 for r, g, b in im.getdata(): 103 data.append((((r >> 6) << 6), ((g >> 5) << 5), (b >> 6) << 6)) 104 imp.putdata(data) 105 return imp 106 107 def flush_last_word(f, word): 108 if word: 109 pad_word(word) 110 write_word(f, word) 111 del word[:] 112 113 def flush_word(f, word): 114 if len(word) == 4: 115 write_word(f, word) 116 del word[:] 117 118 def pad_word(word): 119 while len(word) < 4: 120 word.insert(0, 0) 121 122 def write_word(f, word): 123 print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) 124 125 def rotate_and_scale(exif, im, width, height, rotate, scale_factor): 126 127 """ 128 Using the given 'exif' information, rotate and scale image 'im' given the 129 indicated 'width' and 'height' constraints and any explicit 'rotate' 130 indication. The returned image will be within the given 'width' and 131 'height', filling either or both, and preserve its original aspect ratio. 132 """ 133 134 if rotate or exif and exif["Image Orientation"].values == [6L]: 135 im = im.rotate(270) 136 137 w, h = im.size 138 139 # Get the relationship between the base width and the image width. 140 141 width_scale_factor = (width / scale_factor) / w 142 height_scale_factor = float(height) / h 143 min_scale_factor = min(width_scale_factor, height_scale_factor) 144 145 if min_scale_factor < 1: 146 width = int(min_scale_factor * w * scale_factor) 147 height = int(min_scale_factor * h) 148 return im.resize((width, height)) 149 elif scale_factor != 1: 150 width = int(w * scale_factor) 151 return im.resize((width, h)) 152 else: 153 return im 154 155 def get_parameter(options, flag, conversion, default, missing): 156 157 """ 158 From 'options', return any parameter following the given 'flag', applying 159 the 'conversion' which has the given 'default' if no valid parameter is 160 found, or returning the given 'missing' value if the flag does not appear at 161 all. 162 """ 163 164 try: 165 i = options.index(flag) 166 try: 167 return conversion(options[i+1]) 168 except (IndexError, ValueError): 169 return default 170 except ValueError: 171 return missing 172 173 # Main program. 174 175 if __name__ == "__main__": 176 177 # Test options. 178 179 if "--help" in sys.argv or len(sys.argv) < 3: 180 basename = split(sys.argv[0])[1] 181 print >>sys.stderr, """\ 182 Usage: 183 184 %s <input filename> <output filename> [ <options> ] 185 186 Options are... 187 188 -W - Indicate the output width (default is 160) 189 190 -p - Generate a preview with a filename based on the output filename 191 192 -r - Rotate the input image clockwise explicitly 193 (EXIF information is used otherwise) 194 """ % basename 195 sys.exit(1) 196 197 base_width = 320; width = 160 198 base_height = height = 256 199 200 input_filename, output_filename = sys.argv[1:3] 201 options = sys.argv[3:] 202 203 # Basic image properties. 204 205 width = get_parameter(options, "-W", int, width, width) 206 rotate = "-r" in options 207 preview = "-p" in options 208 209 # Determine any differing horizontal scale factor. 210 211 scale_factor = float(width) / base_width 212 213 # Load the input image. 214 215 exif = EXIF.process_file(open(input_filename)) 216 im = PIL.Image.open(input_filename).convert("RGB") 217 im = rotate_and_scale(exif, im, width, height, rotate, scale_factor) 218 219 # Generate an output image. 220 221 convert_image(im, output_filename, width, height) 222 223 # Generate a preview image if requested. 224 225 if preview: 226 _basename, ext = splitext(input_filename) 227 basename, _ext = splitext(output_filename) 228 preview_filename = "%s_preview%s" % (basename, ext) 229 imp = make_preview(im) 230 imp.save(preview_filename) 231 232 # vim: tabstop=4 expandtab shiftwidth=4