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 test(): 32 33 "Generate slices of the colour cube." 34 35 size = 512 36 for r in (0, 63, 127, 191, 255): 37 im = PIL.Image.new("RGB", (size, size)) 38 for g in range(0, size): 39 for b in range(0, size): 40 value = get_value((r, (g * 256) / size, (b * 256 / size))) 41 im.putpixel((g, b), value) 42 im.save("rgb%d.png" % r) 43 44 def test_flat(rgb): 45 46 "Generate a flat image for the colour 'rgb'." 47 48 size = 64 49 im = PIL.Image.new("RGB", (size, size)) 50 for y in range(0, size): 51 for x in range(0, size): 52 im.putpixel((x, y), get_value(rgb)) 53 im.save("rgb%02d%02d%02d.png" % rgb) 54 55 def rotate_and_scale(exif, im, width, height, rotate): 56 57 """ 58 Using the given 'exif' information, rotate and scale image 'im' given the 59 indicated 'width' and 'height' constraints and any explicit 'rotate' 60 indication. The returned image will be within the given 'width' and 61 'height', filling either or both, and preserve its original aspect ratio. 62 """ 63 64 if rotate or exif and exif["Image Orientation"].values == [6L]: 65 im = im.rotate(270) 66 67 w, h = im.size 68 if w > h: 69 height = (width * h) / w 70 else: 71 width = (height * w) / h 72 73 return im.resize((width, height)) 74 75 def process_image(pim, saturate, desaturate, darken, brighten): 76 77 """ 78 Process image 'pim' using the given options: 'saturate', 'desaturate', 79 'darken', 'brighten'. 80 """ 81 82 width, height = pim.size 83 im = SimpleImage(list(pim.getdata()), pim.size) 84 85 if saturate or desaturate or darken or brighten: 86 for y in range(0, height): 87 for x in range(0, width): 88 rgb = im.getpixel((x, y)) 89 if saturate or desaturate: 90 rgb = saturate_rgb(rgb, saturate and 0.5 / saturate or 2 * desaturate) 91 if darken or brighten: 92 rgb = amplify_rgb(rgb, brighten and 0.5 / brighten or 2 * darken) 93 im.putpixel((x, y), rgb) 94 95 pim.putdata(im.getdata()) 96 97 def preview_image(pim, half_resolution_preview=False): 98 99 "Return a preview copy of image 'pim'." 100 101 width, height = pim.size 102 imp = pim.copy() 103 im = SimpleImage(list(pim.getdata()), pim.size) 104 step = half_resolution_preview and 2 or 1 105 106 for y in range(0, height): 107 for x in range(0, width): 108 rgb = im.getpixel((x, y)) 109 value = get_value(rgb) 110 im.putpixel((x, y), value) 111 if half_resolution_preview: 112 im.putpixel((x+1, y), value) 113 114 imp.putdata(im.getdata()) 115 return imp 116 117 def convert_image(pim): 118 119 "Convert image 'pim' to an appropriate output representation." 120 121 width, height = pim.size 122 im = SimpleImage(list(pim.getdata()), pim.size) 123 124 for y in range(0, height): 125 c = get_colours(im, y) 126 127 for l in get_combinations(c, 4): 128 most = [value for f, value in l] 129 for x in range(0, width): 130 rgb = im.getpixel((x, y)) 131 value = get_value(rgb, most, True) 132 if value is None: 133 break # try next combination 134 else: 135 break # use this combination 136 else: 137 most = [value for f, value in c[:4]] # use the first four 138 139 for x in range(0, width): 140 rgb = im.getpixel((x, y)) 141 value = get_value(rgb, most) 142 im.putpixel((x, y), value) 143 144 if x < width - 1: 145 rgbn = im.getpixel((x+1, y)) 146 rgbn = ( 147 clip(rgbn[0] + (rgb[0] - value[0]) / 4.0), 148 clip(rgbn[1] + (rgb[1] - value[1]) / 4.0), 149 clip(rgbn[2] + (rgb[2] - value[2]) / 4.0) 150 ) 151 im.putpixel((x+1, y), rgbn) 152 153 if y < height - 1: 154 rgbn = im.getpixel((x, y+1)) 155 rgbn = ( 156 clip(rgbn[0] + (rgb[0] - value[0]) / 2.0), 157 clip(rgbn[1] + (rgb[1] - value[1]) / 2.0), 158 clip(rgbn[2] + (rgb[2] - value[2]) / 2.0) 159 ) 160 im.putpixel((x, y+1), rgbn) 161 162 pim.putdata(im.getdata()) 163 164 def get_float(options, flag): 165 try: 166 i = options.index(flag) 167 if i+1 < len(options) and options[i+1].isdigit(): 168 return float(options[i+1]) 169 else: 170 return 1.0 171 except ValueError: 172 return 0.0 173 174 # Main program. 175 176 if __name__ == "__main__": 177 178 # Test options. 179 180 if "--test" in sys.argv: 181 test() 182 sys.exit(0) 183 elif "--test-flat" in sys.argv: 184 test_flat((120, 40, 60)) 185 sys.exit(0) 186 elif "--help" in sys.argv: 187 print >>sys.stderr, """\ 188 Usage: %s <input filename> <output filename> [ <options> ] 189 190 Options are... 191 192 -s - Saturate the input image (can be followed by a float, default 1.0) 193 -d - Desaturate the input image (can be followed by a float, default 1.0) 194 -D - Darken the input image (can be followed by a float, default 1.0) 195 -B - Brighten the input image (can be followed by a float, default 1.0) 196 197 -r - Rotate the input image clockwise 198 -p - Generate a separate preview image 199 -h - Make the preview image with half horizontal resolution (MODE 2) 200 -v - Verify the output image (loaded if -n is given) 201 -n - Generate no output image 202 """ % split(sys.argv[0])[1] 203 sys.exit(1) 204 205 width = 320 206 height = 256 207 208 input_filename, output_filename = sys.argv[1:3] 209 basename, ext = splitext(output_filename) 210 preview_filename = "".join([basename + "_preview", ext]) 211 212 options = sys.argv[3:] 213 214 # Preprocessing options that can be repeated for extra effect. 215 216 saturate = get_float(options, "-s") 217 desaturate = get_float(options, "-d") 218 darken = get_float(options, "-D") 219 brighten = get_float(options, "-B") 220 221 # General output options. 222 223 rotate = "-r" in options 224 preview = "-p" in options 225 half_resolution_preview = "-h" in options 226 verify = "-v" in options 227 no_normal_output = "-n" in options 228 make_image = not no_normal_output 229 230 # Load the input image if requested. 231 232 if make_image or preview: 233 exif = EXIF.process_file(open(input_filename)) 234 im = PIL.Image.open(input_filename).convert("RGB") 235 im = rotate_and_scale(exif, im, width, height, rotate) 236 237 process_image(im, saturate, desaturate, darken, brighten) 238 239 # Generate a preview if requested. 240 241 if preview: 242 preview_image(im, half_resolution_preview).save(preview_filename) 243 244 # Generate an output image if requested. 245 246 if make_image: 247 convert_image(im) 248 im.save(output_filename) 249 250 # Verify the output image (which may be loaded) if requested. 251 252 if verify: 253 if no_normal_output: 254 im = PIL.Image.open(output_filename).convert("RGB") 255 256 result = count_colours(im, 4) 257 if result is not None: 258 y, colours = result 259 print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) 260 261 # vim: tabstop=4 expandtab shiftwidth=4