paul@22 | 1 | #!/usr/bin/env python |
paul@22 | 2 | |
paul@22 | 3 | """ |
paul@22 | 4 | Convert images for display in an Acorn Electron MODE 2 variant with a pixel |
paul@23 | 5 | layout of I0RRGGBB, giving 128 colours instead of the usual 8 colours. |
paul@22 | 6 | |
paul@22 | 7 | Copyright (C) 2015, 2017 Paul Boddie <paul@boddie.org.uk> |
paul@22 | 8 | |
paul@22 | 9 | This program is free software; you can redistribute it and/or modify it under |
paul@22 | 10 | the terms of the GNU General Public License as published by the Free Software |
paul@22 | 11 | Foundation; either version 3 of the License, or (at your option) any later |
paul@22 | 12 | version. |
paul@22 | 13 | |
paul@22 | 14 | This program is distributed in the hope that it will be useful, but WITHOUT ANY |
paul@22 | 15 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
paul@22 | 16 | PARTICULAR PURPOSE. See the GNU General Public License for more details. |
paul@22 | 17 | |
paul@22 | 18 | You should have received a copy of the GNU General Public License along |
paul@22 | 19 | with this program. If not, see <http://www.gnu.org/licenses/>. |
paul@22 | 20 | """ |
paul@22 | 21 | |
paul@22 | 22 | from os.path import split, splitext |
paul@22 | 23 | import EXIF |
paul@22 | 24 | import PIL.Image |
paul@22 | 25 | import sys |
paul@22 | 26 | |
paul@22 | 27 | def convert_image(im, output_filename, width, height): |
paul@22 | 28 | |
paul@22 | 29 | "Convert 'im' and write pixel values to 'output_filename'." |
paul@22 | 30 | |
paul@22 | 31 | w, h = im.size |
paul@22 | 32 | |
paul@22 | 33 | hpad = (width - w) / 2 |
paul@22 | 34 | leftpad = hpad; rightpad = width - w - hpad |
paul@22 | 35 | vpad = (height - h) / 2 |
paul@22 | 36 | toppad = vpad; bottompad = height - h - vpad |
paul@22 | 37 | |
paul@22 | 38 | data = iter(im.getdata()) |
paul@22 | 39 | |
paul@22 | 40 | f = open(output_filename, "w") |
paul@22 | 41 | try: |
paul@40 | 42 | print >>f, """\ |
paul@40 | 43 | .globl screendata |
paul@40 | 44 | |
paul@40 | 45 | .section .flash, "a" |
paul@40 | 46 | """ |
paul@40 | 47 | |
paul@22 | 48 | word = [] |
paul@22 | 49 | y = 0 |
paul@22 | 50 | |
paul@22 | 51 | while y < height: |
paul@22 | 52 | x = 0 |
paul@22 | 53 | |
paul@22 | 54 | # Top and bottom padding. |
paul@22 | 55 | |
paul@22 | 56 | if y < toppad or y >= height - bottompad: |
paul@22 | 57 | |
paul@22 | 58 | while x < width: |
paul@22 | 59 | word.append(0) |
paul@22 | 60 | flush_word(f, word) |
paul@22 | 61 | x += 1 |
paul@22 | 62 | |
paul@22 | 63 | flush_last_word(f, word) |
paul@22 | 64 | |
paul@22 | 65 | # Lines with data. |
paul@22 | 66 | |
paul@22 | 67 | else: |
paul@22 | 68 | while x < width: |
paul@22 | 69 | |
paul@22 | 70 | # Left and right padding. |
paul@22 | 71 | |
paul@22 | 72 | if x < leftpad or x >= width - rightpad: |
paul@22 | 73 | word.append(0) |
paul@22 | 74 | |
paul@22 | 75 | # Data regions. |
paul@22 | 76 | |
paul@22 | 77 | else: |
paul@22 | 78 | r, g, b = data.next() |
paul@23 | 79 | rm, gm, bm, i = get_values(r, g, b) |
paul@23 | 80 | |
paul@23 | 81 | # Encode the byte value: I0RRGGBB. |
paul@23 | 82 | |
paul@22 | 83 | word.insert(0, |
paul@23 | 84 | # I -> D<7> |
paul@23 | 85 | (i << 7) | |
paul@23 | 86 | # R<7:6> -> D<5:4> |
paul@23 | 87 | (rm >> 2) | |
paul@23 | 88 | # G<7:6> -> D<3:2> |
paul@23 | 89 | (gm >> 4) | |
paul@22 | 90 | # B<7:6> -> D<1:0> |
paul@23 | 91 | (bm >> 6)) |
paul@22 | 92 | |
paul@22 | 93 | flush_word(f, word) |
paul@22 | 94 | x += 1 |
paul@22 | 95 | |
paul@22 | 96 | flush_last_word(f, word) |
paul@22 | 97 | |
paul@22 | 98 | y += 1 |
paul@22 | 99 | |
paul@22 | 100 | finally: |
paul@22 | 101 | f.close() |
paul@22 | 102 | |
paul@23 | 103 | def get_values(r, g, b): |
paul@23 | 104 | |
paul@23 | 105 | "Return modified values for 'r', 'g' and 'b', plus an intensity bit." |
paul@23 | 106 | |
paul@23 | 107 | rm = r & 0xc0 |
paul@23 | 108 | gm = g & 0xc0 |
paul@23 | 109 | bm = b & 0xc0 |
paul@23 | 110 | rd = r - rm |
paul@23 | 111 | gd = g - gm |
paul@23 | 112 | bd = b - bm |
paul@23 | 113 | i = ((rd ** 2 + gd ** 2 + bd ** 2) ** 0.5) >= 32 and 1 or 0 |
paul@23 | 114 | return rm, gm, bm, i |
paul@23 | 115 | |
paul@22 | 116 | def make_preview(im): |
paul@22 | 117 | imp = PIL.Image.new("RGB", im.size) |
paul@22 | 118 | data = [] |
paul@22 | 119 | for r, g, b in im.getdata(): |
paul@23 | 120 | rm, gm, bm, i = get_values(r, g, b) |
paul@23 | 121 | r = rm + (i * 32) |
paul@23 | 122 | g = gm + (i * 32) |
paul@23 | 123 | b = bm + (i * 32) |
paul@23 | 124 | data.append((r, g, b)) |
paul@22 | 125 | imp.putdata(data) |
paul@22 | 126 | return imp |
paul@22 | 127 | |
paul@22 | 128 | def flush_last_word(f, word): |
paul@22 | 129 | if word: |
paul@22 | 130 | pad_word(word) |
paul@22 | 131 | write_word(f, word) |
paul@22 | 132 | del word[:] |
paul@22 | 133 | |
paul@22 | 134 | def flush_word(f, word): |
paul@22 | 135 | if len(word) == 4: |
paul@22 | 136 | write_word(f, word) |
paul@22 | 137 | del word[:] |
paul@22 | 138 | |
paul@22 | 139 | def pad_word(word): |
paul@22 | 140 | while len(word) < 4: |
paul@22 | 141 | word.insert(0, 0) |
paul@22 | 142 | |
paul@22 | 143 | def write_word(f, word): |
paul@22 | 144 | print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) |
paul@22 | 145 | |
paul@22 | 146 | def rotate_and_scale(exif, im, width, height, rotate, scale_factor): |
paul@22 | 147 | |
paul@22 | 148 | """ |
paul@22 | 149 | Using the given 'exif' information, rotate and scale image 'im' given the |
paul@22 | 150 | indicated 'width' and 'height' constraints and any explicit 'rotate' |
paul@22 | 151 | indication. The returned image will be within the given 'width' and |
paul@22 | 152 | 'height', filling either or both, and preserve its original aspect ratio. |
paul@22 | 153 | """ |
paul@22 | 154 | |
paul@22 | 155 | if rotate or exif and exif["Image Orientation"].values == [6L]: |
paul@22 | 156 | im = im.rotate(270) |
paul@22 | 157 | |
paul@22 | 158 | w, h = im.size |
paul@22 | 159 | |
paul@22 | 160 | # Get the relationship between the base width and the image width. |
paul@22 | 161 | |
paul@22 | 162 | width_scale_factor = (width / scale_factor) / w |
paul@22 | 163 | height_scale_factor = float(height) / h |
paul@22 | 164 | min_scale_factor = min(width_scale_factor, height_scale_factor) |
paul@22 | 165 | |
paul@22 | 166 | if min_scale_factor < 1: |
paul@22 | 167 | width = int(min_scale_factor * w * scale_factor) |
paul@22 | 168 | height = int(min_scale_factor * h) |
paul@22 | 169 | return im.resize((width, height)) |
paul@22 | 170 | elif scale_factor != 1: |
paul@22 | 171 | width = int(w * scale_factor) |
paul@22 | 172 | return im.resize((width, h)) |
paul@22 | 173 | else: |
paul@22 | 174 | return im |
paul@22 | 175 | |
paul@22 | 176 | def get_parameter(options, flag, conversion, default, missing): |
paul@22 | 177 | |
paul@22 | 178 | """ |
paul@22 | 179 | From 'options', return any parameter following the given 'flag', applying |
paul@22 | 180 | the 'conversion' which has the given 'default' if no valid parameter is |
paul@22 | 181 | found, or returning the given 'missing' value if the flag does not appear at |
paul@22 | 182 | all. |
paul@22 | 183 | """ |
paul@22 | 184 | |
paul@22 | 185 | try: |
paul@22 | 186 | i = options.index(flag) |
paul@22 | 187 | try: |
paul@22 | 188 | return conversion(options[i+1]) |
paul@22 | 189 | except (IndexError, ValueError): |
paul@22 | 190 | return default |
paul@22 | 191 | except ValueError: |
paul@22 | 192 | return missing |
paul@22 | 193 | |
paul@22 | 194 | # Main program. |
paul@22 | 195 | |
paul@22 | 196 | if __name__ == "__main__": |
paul@22 | 197 | |
paul@22 | 198 | # Test options. |
paul@22 | 199 | |
paul@22 | 200 | if "--help" in sys.argv or len(sys.argv) < 3: |
paul@22 | 201 | basename = split(sys.argv[0])[1] |
paul@22 | 202 | print >>sys.stderr, """\ |
paul@22 | 203 | Usage: |
paul@22 | 204 | |
paul@22 | 205 | %s <input filename> <output filename> [ <options> ] |
paul@22 | 206 | |
paul@22 | 207 | Options are... |
paul@22 | 208 | |
paul@22 | 209 | -W - Indicate the output width (default is 160) |
paul@22 | 210 | |
paul@22 | 211 | -p - Generate a preview with a filename based on the output filename |
paul@22 | 212 | |
paul@22 | 213 | -r - Rotate the input image clockwise explicitly |
paul@22 | 214 | (EXIF information is used otherwise) |
paul@22 | 215 | """ % basename |
paul@22 | 216 | sys.exit(1) |
paul@22 | 217 | |
paul@22 | 218 | base_width = 320; width = 160 |
paul@22 | 219 | base_height = height = 256 |
paul@22 | 220 | |
paul@22 | 221 | input_filename, output_filename = sys.argv[1:3] |
paul@22 | 222 | options = sys.argv[3:] |
paul@22 | 223 | |
paul@22 | 224 | # Basic image properties. |
paul@22 | 225 | |
paul@22 | 226 | width = get_parameter(options, "-W", int, width, width) |
paul@22 | 227 | rotate = "-r" in options |
paul@22 | 228 | preview = "-p" in options |
paul@22 | 229 | |
paul@22 | 230 | # Determine any differing horizontal scale factor. |
paul@22 | 231 | |
paul@22 | 232 | scale_factor = float(width) / base_width |
paul@22 | 233 | |
paul@22 | 234 | # Load the input image. |
paul@22 | 235 | |
paul@22 | 236 | exif = EXIF.process_file(open(input_filename)) |
paul@22 | 237 | im = PIL.Image.open(input_filename).convert("RGB") |
paul@22 | 238 | im = rotate_and_scale(exif, im, width, height, rotate, scale_factor) |
paul@22 | 239 | |
paul@22 | 240 | # Generate an output image. |
paul@22 | 241 | |
paul@22 | 242 | convert_image(im, output_filename, width, height) |
paul@22 | 243 | |
paul@22 | 244 | # Generate a preview image if requested. |
paul@22 | 245 | |
paul@22 | 246 | if preview: |
paul@22 | 247 | _basename, ext = splitext(input_filename) |
paul@22 | 248 | basename, _ext = splitext(output_filename) |
paul@22 | 249 | preview_filename = "%s_preview%s" % (basename, ext) |
paul@22 | 250 | imp = make_preview(im) |
paul@22 | 251 | imp.save(preview_filename) |
paul@22 | 252 | |
paul@22 | 253 | # vim: tabstop=4 expandtab shiftwidth=4 |