1 #!/usr/bin/env python 2 3 """ 4 Convert and optimise images for display in an Acorn Electron MODE 1 variant 5 with four colours per line but eight colours available for selection on each 6 line. 7 8 Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk> 9 10 This program is free software; you can redistribute it and/or modify it under 11 the terms of the GNU General Public License as published by the Free Software 12 Foundation; either version 3 of the License, or (at your option) any later 13 version. 14 15 This program is distributed in the hope that it will be useful, but WITHOUT ANY 16 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 17 PARTICULAR PURPOSE. See the GNU General Public License for more details. 18 19 You should have received a copy of the GNU General Public License along 20 with this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 from optimiserlib import * 24 from os.path import split, splitext 25 import EXIF 26 import PIL.Image 27 import sys 28 29 # Image operations. 30 31 def get_colours(im, y): 32 33 "Get a colour distribution from image 'im' for the row 'y'." 34 35 width, height = im.size 36 c = {} 37 for x in range(0, width): 38 rgb = im.getpixel((x, y)) 39 40 # Sum the colour probabilities. 41 42 for f, value in combination(rgb): 43 if not c.has_key(value): 44 c[value] = f 45 else: 46 c[value] += f 47 48 d = [(n/width, value) for value, n in c.items()] 49 d.sort(reverse=True) 50 return d 51 52 def test(): 53 54 "Generate slices of the colour cube." 55 56 size = 512 57 for r in (0, 63, 127, 191, 255): 58 im = PIL.Image.new("RGB", (size, size)) 59 for g in range(0, size): 60 for b in range(0, size): 61 value = get_value((r, (g * 256) / size, (b * 256 / size))) 62 im.putpixel((g, b), value) 63 im.save("rgb%d.png" % r) 64 65 def test_flat(rgb): 66 67 "Generate a flat image for the colour 'rgb'." 68 69 size = 64 70 im = PIL.Image.new("RGB", (size, size)) 71 for y in range(0, size): 72 for x in range(0, size): 73 im.putpixel((x, y), get_value(rgb)) 74 im.save("rgb%02d%02d%02d.png" % rgb) 75 76 def rotate_and_scale(exif, im, width, height, rotate): 77 78 """ 79 Using the given 'exif' information, rotate and scale image 'im' given the 80 indicated 'width' and 'height' constraints and any explicit 'rotate' 81 indication. The returned image will be within the given 'width' and 82 'height', filling either or both, and preserve its original aspect ratio. 83 """ 84 85 if rotate or exif and exif["Image Orientation"].values == [6L]: 86 im = im.rotate(270) 87 88 w, h = im.size 89 if w > h: 90 height = (width * h) / w 91 else: 92 width = (height * w) / h 93 94 return im.resize((width, height)) 95 96 def count_colours(im, colours): 97 98 """ 99 Count colours on each row of image 'im', returning a tuple indicating the 100 first row with more than the given number of 'colours' together with the 101 found colours; otherwise returning None. 102 """ 103 104 width, height = im.size 105 106 for y in range(0, height): 107 l = set() 108 for x in range(0, width): 109 l.add(im.getpixel((x, y))) 110 if len(l) > colours: 111 return (y, l) 112 return None 113 114 def process_image(pim, saturate, desaturate, darken, brighten): 115 116 """ 117 Process image 'pim' using the given options: 'saturate', 'desaturate', 118 'darken', 'brighten'. 119 """ 120 121 width, height = pim.size 122 im = SimpleImage(list(pim.getdata()), pim.size) 123 124 if saturate or desaturate or darken or brighten: 125 for y in range(0, height): 126 for x in range(0, width): 127 rgb = im.getpixel((x, y)) 128 if saturate or desaturate: 129 rgb = saturate_rgb(rgb, saturate and 0.5 / saturate or 2 * desaturate) 130 if darken or brighten: 131 rgb = amplify_rgb(rgb, brighten and 0.5 / brighten or 2 * darken) 132 im.putpixel((x, y), rgb) 133 134 pim.putdata(im.getdata()) 135 136 def preview_image(pim, half_resolution_preview=False): 137 138 "Return a preview copy of image 'pim'." 139 140 width, height = pim.size 141 imp = pim.copy() 142 im = SimpleImage(list(pim.getdata()), pim.size) 143 step = half_resolution_preview and 2 or 1 144 145 for y in range(0, height): 146 for x in range(0, width): 147 rgb = im.getpixel((x, y)) 148 value = get_value(rgb) 149 im.putpixel((x, y), value) 150 if half_resolution_preview: 151 im.putpixel((x+1, y), value) 152 153 imp.putdata(im.getdata()) 154 return imp 155 156 def convert_image(pim): 157 158 "Convert image 'pim' to an appropriate output representation." 159 160 width, height = pim.size 161 im = SimpleImage(list(pim.getdata()), pim.size) 162 163 for y in range(0, height): 164 c = get_colours(im, y) 165 166 for l in get_combinations(c, 4): 167 most = [value for f, value in l] 168 for x in range(0, width): 169 rgb = im.getpixel((x, y)) 170 value = get_value(rgb, most, True) 171 if value is None: 172 break # try next combination 173 else: 174 break # use this combination 175 else: 176 most = [value for f, value in c[:4]] # use the first four 177 178 for x in range(0, width): 179 rgb = im.getpixel((x, y)) 180 value = get_value(rgb, most) 181 im.putpixel((x, y), value) 182 183 if x < width - 1: 184 rgbn = im.getpixel((x+1, y)) 185 rgbn = ( 186 clip(rgbn[0] + (rgb[0] - value[0]) / 4.0), 187 clip(rgbn[1] + (rgb[1] - value[1]) / 4.0), 188 clip(rgbn[2] + (rgb[2] - value[2]) / 4.0) 189 ) 190 im.putpixel((x+1, y), rgbn) 191 192 if y < height - 1: 193 rgbn = im.getpixel((x, y+1)) 194 rgbn = ( 195 clip(rgbn[0] + (rgb[0] - value[0]) / 2.0), 196 clip(rgbn[1] + (rgb[1] - value[1]) / 2.0), 197 clip(rgbn[2] + (rgb[2] - value[2]) / 2.0) 198 ) 199 im.putpixel((x, y+1), rgbn) 200 201 pim.putdata(im.getdata()) 202 203 def get_float(options, flag): 204 try: 205 i = options.index(flag) 206 if i+1 < len(options) and options[i+1].isdigit(): 207 return float(options[i+1]) 208 else: 209 return 1.0 210 except ValueError: 211 return 0.0 212 213 # Main program. 214 215 if __name__ == "__main__": 216 217 # Test options. 218 219 if "--test" in sys.argv: 220 test() 221 sys.exit(0) 222 elif "--test-flat" in sys.argv: 223 test_flat((120, 40, 60)) 224 sys.exit(0) 225 elif "--help" in sys.argv: 226 print >>sys.stderr, """\ 227 Usage: %s <input filename> <output filename> [ <options> ] 228 229 Options are... 230 231 -s - Saturate the input image (can be followed by a float, default 1.0) 232 -d - Desaturate the input image (can be followed by a float, default 1.0) 233 -D - Darken the input image (can be followed by a float, default 1.0) 234 -B - Brighten the input image (can be followed by a float, default 1.0) 235 236 -r - Rotate the input image clockwise 237 -p - Generate a separate preview image 238 -h - Make the preview image with half horizontal resolution (MODE 2) 239 -v - Verify the output image (loaded if -n is given) 240 -n - Generate no output image 241 """ % split(sys.argv[0])[1] 242 sys.exit(1) 243 244 width = 320 245 height = 256 246 247 input_filename, output_filename = sys.argv[1:3] 248 basename, ext = splitext(output_filename) 249 preview_filename = "".join([basename + "_preview", ext]) 250 251 options = sys.argv[3:] 252 253 # Preprocessing options that can be repeated for extra effect. 254 255 saturate = get_float(options, "-s") 256 desaturate = get_float(options, "-d") 257 darken = get_float(options, "-D") 258 brighten = get_float(options, "-B") 259 260 # General output options. 261 262 rotate = "-r" in options 263 preview = "-p" in options 264 half_resolution_preview = "-h" in options 265 verify = "-v" in options 266 no_normal_output = "-n" in options 267 make_image = not no_normal_output 268 269 # Load the input image if requested. 270 271 if make_image or preview: 272 exif = EXIF.process_file(open(input_filename)) 273 im = PIL.Image.open(input_filename).convert("RGB") 274 im = rotate_and_scale(exif, im, width, height, rotate) 275 276 process_image(im, saturate, desaturate, darken, brighten) 277 278 # Generate a preview if requested. 279 280 if preview: 281 preview_image(im, half_resolution_preview).save(preview_filename) 282 283 # Generate an output image if requested. 284 285 if make_image: 286 convert_image(im) 287 im.save(output_filename) 288 289 # Verify the output image (which may be loaded) if requested. 290 291 if verify: 292 if no_normal_output: 293 im = PIL.Image.open(output_filename).convert("RGB") 294 295 result = count_colours(im, 4) 296 if result is not None: 297 y, colours = result 298 print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) 299 300 # vim: tabstop=4 expandtab shiftwidth=4