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 def test(): 30 31 "Generate slices of the colour cube." 32 33 size = 512 34 for r in (0, 63, 127, 191, 255): 35 im = PIL.Image.new("RGB", (size, size)) 36 for g in range(0, size): 37 for b in range(0, size): 38 value = get_value((r, (g * 256) / size, (b * 256 / size))) 39 im.putpixel((g, b), value) 40 im.save("rgb%d.png" % r) 41 42 def test_flat(rgb): 43 44 "Generate a flat image for the colour 'rgb'." 45 46 size = 64 47 im = PIL.Image.new("RGB", (size, size)) 48 for y in range(0, size): 49 for x in range(0, size): 50 im.putpixel((x, y), get_value(rgb)) 51 im.save("rgb%02d%02d%02d.png" % rgb) 52 53 def rotate_and_scale(exif, im, width, height, rotate): 54 55 """ 56 Using the given 'exif' information, rotate and scale image 'im' given the 57 indicated 'width' and 'height' constraints and any explicit 'rotate' 58 indication. The returned image will be within the given 'width' and 59 'height', filling either or both, and preserve its original aspect ratio. 60 """ 61 62 if rotate or exif and exif["Image Orientation"].values == [6L]: 63 im = im.rotate(270) 64 65 w, h = im.size 66 if w > h: 67 height = (width * h) / w 68 else: 69 width = (height * w) / h 70 71 return im.resize((width, height)) 72 73 def get_parameter(options, flag, conversion, default, missing): 74 75 """ 76 From 'options', return any parameter following the given 'flag', applying 77 the 'conversion' which has the given 'default' if no valid parameter is 78 found, or returning the given 'missing' value if the flag does not appear at 79 all. 80 """ 81 82 try: 83 i = options.index(flag) 84 try: 85 return conversion(options[i+1]) 86 except (IndexError, ValueError): 87 return default 88 except ValueError: 89 return missing 90 91 # Main program. 92 93 if __name__ == "__main__": 94 95 # Test options. 96 97 if "--test" in sys.argv: 98 test() 99 sys.exit(0) 100 elif "--test-flat" in sys.argv: 101 test_flat((120, 40, 60)) 102 sys.exit(0) 103 elif "--help" in sys.argv or len(sys.argv) < 3: 104 basename = split(sys.argv[0])[1] 105 print >>sys.stderr, """\ 106 Usage: 107 108 %s <input filename> <output filename> [ <options> ] 109 110 %s -v <filename> [ -C <number of colours> ] 111 112 Options are... 113 114 -W - Indicate the output width (default is 320) 115 -C - Number of colours per scanline (default is 4) 116 -A - Produce an output image with the same aspect ratio as the input 117 (useful for previewing) 118 119 -s - Saturate the input image (optional float, 1.0 if unspecified) 120 -d - Desaturate the input image (optional float, 1.0 if unspecified) 121 -D - Darken the input image (optional float, 1.0 if unspecified) 122 -B - Brighten the input image (optional float, 1.0 if unspecified) 123 124 -l - Use colours producing the least error 125 (slower but useful for fewer than 4 colours) 126 127 -r - Rotate the input image clockwise explicitly 128 (EXIF information is used otherwise) 129 -p - Generate a separate preview image 130 -h - Make the preview image with half horizontal resolution (MODE 2) 131 -v - Verify the output image (loaded if -n is given) 132 -n - Generate no output image 133 134 Specifying -v instead of input filename permits the verification of 135 previously-generated images. Doing so causes all other options except for -C 136 to be ignored. 137 """ % (basename, basename) 138 sys.exit(1) 139 140 base_width = width = 320 141 base_height = height = 256 142 143 input_filename, output_filename = sys.argv[1:3] 144 basename, ext = splitext(output_filename) 145 preview_filename = "".join([basename + "_preview", ext]) 146 147 verify_only = input_filename == "-v" 148 options = sys.argv[3:] 149 150 # Basic image properties. 151 152 width = get_parameter(options, "-W", int, base_width, base_width) 153 number_of_colours = get_parameter(options, "-C", int, 4, 4) 154 preserve_aspect_ratio = "-A" in options 155 156 scale_factor = float(width) / base_width 157 height = int(base_height * scale_factor) 158 159 # Preprocessing options that employ parameters. 160 161 saturate = get_parameter(options, "-s", float, 1.0, 0.0) 162 desaturate = get_parameter(options, "-d", float, 1.0, 0.0) 163 darken = get_parameter(options, "-D", float, 1.0, 0.0) 164 brighten = get_parameter(options, "-B", float, 1.0, 0.0) 165 166 # General output options. 167 168 no_normal_output = "-n" in options or verify_only 169 verify = "-v" in options or verify_only 170 171 rotate = "-r" in options and not verify_only 172 preview = "-p" in options and not verify_only 173 174 half_resolution_preview = "-h" in options 175 least_error = "-l" in options 176 177 make_image = not no_normal_output 178 179 # Load the input image if requested. 180 181 if make_image or preview: 182 exif = EXIF.process_file(open(input_filename)) 183 im = PIL.Image.open(input_filename).convert("RGB") 184 im = rotate_and_scale(exif, im, width, height, rotate) 185 image_width, image_height = im.size 186 187 # Scale images to the appropriate height. 188 189 if scale_factor != 1: 190 im = im.resize((image_width, int(image_height / scale_factor))) 191 192 sim = SimpleImage(list(im.getdata()), im.size) 193 process_image(sim, saturate, desaturate, darken, brighten) 194 im.putdata(sim.getdata()) 195 196 # Generate a preview if requested. 197 198 if preview: 199 imp = im.copy() 200 if half_resolution_preview: 201 imp = imp.resize((image_width / 2, image_height)) 202 sim = SimpleImage(list(imp.getdata()), imp.size) 203 convert_image(sim, 8) 204 imp.putdata(sim.getdata()) 205 if half_resolution_preview: 206 imp = imp.resize((image_width, image_height)) 207 208 # Scale images to a height determined by the aspect ratio. 209 210 if preserve_aspect_ratio and scale_factor != 1: 211 imp = imp.resize((image_width, image_height)) 212 213 imp.save(preview_filename) 214 215 # Generate an output image if requested. 216 217 if make_image: 218 sim = SimpleImage(list(im.getdata()), im.size) 219 convert_image(sim, number_of_colours, least_error) 220 im.putdata(sim.getdata()) 221 222 # Scale images to a height determined by the aspect ratio. 223 224 if preserve_aspect_ratio and scale_factor != 1: 225 im = im.resize((image_width, image_height)) 226 227 im.save(output_filename) 228 229 # Verify the output image (which may be loaded) if requested. 230 231 if verify: 232 if no_normal_output: 233 im = PIL.Image.open(output_filename).convert("RGB") 234 235 im = SimpleImage(list(im.getdata()), im.size) 236 result = count_colours(im, number_of_colours) 237 if result is not None: 238 y, colours = result 239 print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) 240 241 # vim: tabstop=4 expandtab shiftwidth=4