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