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