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