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