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