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 71 width_scale_factor = float(width) / w 72 height_scale_factor = float(height) / h 73 scale_factor = min(width_scale_factor, height_scale_factor) 74 75 if scale_factor < 1: 76 width = int(scale_factor * w) 77 height = int(scale_factor * h) 78 return im.resize((width, height)) 79 else: 80 return im 81 82 def get_parameter(options, flag, conversion, default, missing): 83 84 """ 85 From 'options', return any parameter following the given 'flag', applying 86 the 'conversion' which has the given 'default' if no valid parameter is 87 found, or returning the given 'missing' value if the flag does not appear at 88 all. 89 """ 90 91 try: 92 i = options.index(flag) 93 try: 94 return conversion(options[i+1]) 95 except (IndexError, ValueError): 96 return default 97 except ValueError: 98 return missing 99 100 # Main program. 101 102 if __name__ == "__main__": 103 104 # Test options. 105 106 if "--test" in sys.argv: 107 test() 108 sys.exit(0) 109 elif "--test-flat" in sys.argv: 110 test_flat((120, 40, 60)) 111 sys.exit(0) 112 elif "--help" in sys.argv or len(sys.argv) < 3: 113 basename = split(sys.argv[0])[1] 114 print >>sys.stderr, """\ 115 Usage: 116 117 %s <input filename> <output filename> [ <options> ] 118 119 %s -v <filename> [ -C <number of colours> ] 120 121 Options are... 122 123 -W - Indicate the output width (default is 320) 124 -C - Number of colours per scanline (default is 4) 125 -A - Produce an output image with the same aspect ratio as the input 126 (useful for previewing) 127 128 -s - Saturate the input image (optional float, 1.0 if unspecified) 129 -d - Desaturate the input image (optional float, 1.0 if unspecified) 130 -D - Darken the input image (optional float, 1.0 if unspecified) 131 -B - Brighten the input image (optional float, 1.0 if unspecified) 132 133 -l - Use colours producing the least error 134 (slower but useful for fewer than 4 colours) 135 136 -r - Rotate the input image clockwise explicitly 137 (EXIF information is used otherwise) 138 -p - Generate a separate preview image 139 -h - Make the preview image with half horizontal resolution (MODE 2) 140 -v - Verify the output image (loaded if -n is given) 141 -n - Generate no output image 142 143 Specifying -v instead of input filename permits the verification of 144 previously-generated images. Doing so causes all other options except for -C 145 to be ignored. 146 """ % (basename, basename) 147 sys.exit(1) 148 149 base_width = width = 320 150 base_height = height = 256 151 152 input_filename, output_filename = sys.argv[1:3] 153 basename, ext = splitext(output_filename) 154 preview_filename = "".join([basename + "_preview", ext]) 155 156 verify_only = input_filename == "-v" 157 options = sys.argv[3:] 158 159 # Basic image properties. 160 161 width = get_parameter(options, "-W", int, base_width, base_width) 162 number_of_colours = get_parameter(options, "-C", int, 4, 4) 163 preserve_aspect_ratio = "-A" in options 164 165 # Determine whether the height will need adjusting before conversion. 166 167 scale_factor = float(width) / base_width 168 height = int(base_height * scale_factor) 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) 196 image_width, image_height = im.size 197 198 # Scale images to the appropriate height. 199 200 if scale_factor != 1: 201 im = im.resize((image_width, int(image_height / scale_factor))) 202 image_width, image_height = im.size 203 204 process_image(im, saturate, desaturate, darken, brighten) 205 206 # Generate a preview if requested. 207 208 if preview: 209 imp = im.copy() 210 if half_resolution_preview: 211 imp = imp.resize((image_width / 2, image_height)) 212 convert_image(imp, 8) 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 convert_image(im, number_of_colours, least_error) 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 result = count_colours(im, number_of_colours) 242 if result is not None: 243 y, colours = result 244 print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) 245 246 # vim: tabstop=4 expandtab shiftwidth=4