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