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 preview_image(pim, half_resolution_preview=False): 76 77 "Return a preview copy of image 'pim'." 78 79 width, height = pim.size 80 imp = pim.copy() 81 im = SimpleImage(list(pim.getdata()), pim.size) 82 step = half_resolution_preview and 2 or 1 83 84 for y in range(0, height): 85 for x in range(0, width, step): 86 rgb = im.getpixel((x, y)) 87 value = get_value(rgb) 88 im.putpixel((x, y), value) 89 if half_resolution_preview: 90 im.putpixel((x+1, y), value) 91 92 imp.putdata(im.getdata()) 93 return imp 94 95 def convert_image(pim): 96 97 "Convert image 'pim' to an appropriate output representation." 98 99 width, height = pim.size 100 im = SimpleImage(list(pim.getdata()), pim.size) 101 102 for y in range(0, height): 103 c = get_colours(im, y) 104 105 for l in get_combinations(c, 4): 106 most = [value for f, value in l] 107 for x in range(0, width): 108 rgb = im.getpixel((x, y)) 109 value = get_value(rgb, most, True) 110 if value is None: 111 break # try next combination 112 else: 113 break # use this combination 114 else: 115 most = [value for f, value in c[:4]] # use the first four 116 117 for x in range(0, width): 118 rgb = im.getpixel((x, y)) 119 value = get_value(rgb, most) 120 im.putpixel((x, y), value) 121 122 if x < width - 1: 123 rgbn = im.getpixel((x+1, y)) 124 rgbn = ( 125 clip(rgbn[0] + (rgb[0] - value[0]) / 4.0), 126 clip(rgbn[1] + (rgb[1] - value[1]) / 4.0), 127 clip(rgbn[2] + (rgb[2] - value[2]) / 4.0) 128 ) 129 im.putpixel((x+1, y), rgbn) 130 131 if y < height - 1: 132 rgbn = im.getpixel((x, y+1)) 133 rgbn = ( 134 clip(rgbn[0] + (rgb[0] - value[0]) / 2.0), 135 clip(rgbn[1] + (rgb[1] - value[1]) / 2.0), 136 clip(rgbn[2] + (rgb[2] - value[2]) / 2.0) 137 ) 138 im.putpixel((x, y+1), rgbn) 139 140 pim.putdata(im.getdata()) 141 142 def get_float(options, flag): 143 try: 144 i = options.index(flag) 145 if i+1 < len(options) and options[i+1].isdigit(): 146 return float(options[i+1]) 147 else: 148 return 1.0 149 except ValueError: 150 return 0.0 151 152 # Main program. 153 154 if __name__ == "__main__": 155 156 # Test options. 157 158 if "--test" in sys.argv: 159 test() 160 sys.exit(0) 161 elif "--test-flat" in sys.argv: 162 test_flat((120, 40, 60)) 163 sys.exit(0) 164 elif "--help" in sys.argv: 165 print >>sys.stderr, """\ 166 Usage: %s <input filename> <output filename> [ <options> ] 167 168 Options are... 169 170 -s - Saturate the input image (can be followed by a float, default 1.0) 171 -d - Desaturate the input image (can be followed by a float, default 1.0) 172 -D - Darken the input image (can be followed by a float, default 1.0) 173 -B - Brighten the input image (can be followed by a float, default 1.0) 174 175 -r - Rotate the input image clockwise 176 -p - Generate a separate preview image 177 -h - Make the preview image with half horizontal resolution (MODE 2) 178 -v - Verify the output image (loaded if -n is given) 179 -n - Generate no output image 180 """ % split(sys.argv[0])[1] 181 sys.exit(1) 182 183 width = 320 184 height = 256 185 186 input_filename, output_filename = sys.argv[1:3] 187 basename, ext = splitext(output_filename) 188 preview_filename = "".join([basename + "_preview", ext]) 189 190 options = sys.argv[3:] 191 192 # Preprocessing options that can be repeated for extra effect. 193 194 saturate = get_float(options, "-s") 195 desaturate = get_float(options, "-d") 196 darken = get_float(options, "-D") 197 brighten = get_float(options, "-B") 198 199 # General output options. 200 201 rotate = "-r" in options 202 preview = "-p" in options 203 half_resolution_preview = "-h" in options 204 verify = "-v" in options 205 no_normal_output = "-n" in options 206 make_image = not no_normal_output 207 208 # Load the input image if requested. 209 210 if make_image or preview: 211 exif = EXIF.process_file(open(input_filename)) 212 im = PIL.Image.open(input_filename).convert("RGB") 213 im = rotate_and_scale(exif, im, width, height, rotate) 214 215 sim = SimpleImage(list(im.getdata()), im.size) 216 process_image(sim, saturate, desaturate, darken, brighten) 217 im.putdata(sim.getdata()) 218 219 # Generate a preview if requested. 220 221 if preview: 222 preview_image(im, half_resolution_preview).save(preview_filename) 223 224 # Generate an output image if requested. 225 226 if make_image: 227 convert_image(im) 228 im.save(output_filename) 229 230 # Verify the output image (which may be loaded) if requested. 231 232 if verify: 233 if no_normal_output: 234 im = PIL.Image.open(output_filename).convert("RGB") 235 236 im = SimpleImage(list(im.getdata()), im.size) 237 result = count_colours(im, 4) 238 if result is not None: 239 y, colours = result 240 print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) 241 242 # vim: tabstop=4 expandtab shiftwidth=4