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@118 | 57 | %s: |
paul@69 | 58 | %s_width: |
paul@69 | 59 | .word %d |
paul@69 | 60 | %s_height: |
paul@69 | 61 | .word %d |
paul@69 | 62 | |
paul@118 | 63 | %s_image: |
paul@118 | 64 | """ % (options, label, label, label, label, label, width, label, height, label) |
paul@69 | 65 | |
paul@69 | 66 | word = [] |
paul@69 | 67 | y = 0 |
paul@69 | 68 | |
paul@69 | 69 | while y < height: |
paul@69 | 70 | x = 0 |
paul@69 | 71 | |
paul@69 | 72 | # Top and bottom padding. |
paul@69 | 73 | |
paul@69 | 74 | if y < toppad or y >= height - bottompad: |
paul@69 | 75 | |
paul@69 | 76 | while x < width: |
paul@69 | 77 | word.append(0) |
paul@73 | 78 | flush_word(f, word, bytealign) |
paul@69 | 79 | x += 1 |
paul@69 | 80 | |
paul@73 | 81 | flush_last_word(f, word, bytealign) |
paul@69 | 82 | |
paul@69 | 83 | # Lines with data. |
paul@69 | 84 | |
paul@69 | 85 | else: |
paul@69 | 86 | while x < width: |
paul@69 | 87 | |
paul@69 | 88 | # Left and right padding. |
paul@69 | 89 | |
paul@69 | 90 | if x < leftpad or x >= width - rightpad: |
paul@69 | 91 | word.append(0) |
paul@69 | 92 | |
paul@69 | 93 | # Data regions. |
paul@69 | 94 | |
paul@69 | 95 | else: |
paul@69 | 96 | r, g, b = data.next() |
paul@69 | 97 | rm, gm, bm, i = get_values(r, g, b) |
paul@69 | 98 | |
paul@69 | 99 | # Encode the byte value: I0RRGGBB. |
paul@69 | 100 | |
paul@69 | 101 | word.insert(0, |
paul@69 | 102 | # I -> D<7> |
paul@69 | 103 | (i << 7) | |
paul@69 | 104 | # R<7:6> -> D<5:4> |
paul@69 | 105 | (rm >> 2) | |
paul@69 | 106 | # G<7:6> -> D<3:2> |
paul@69 | 107 | (gm >> 4) | |
paul@69 | 108 | # B<7:6> -> D<1:0> |
paul@69 | 109 | (bm >> 6)) |
paul@69 | 110 | |
paul@73 | 111 | flush_word(f, word, bytealign) |
paul@69 | 112 | x += 1 |
paul@69 | 113 | |
paul@73 | 114 | flush_last_word(f, word, bytealign) |
paul@69 | 115 | |
paul@69 | 116 | y += 1 |
paul@69 | 117 | |
paul@69 | 118 | finally: |
paul@69 | 119 | f.close() |
paul@69 | 120 | |
paul@69 | 121 | def get_values(r, g, b): |
paul@69 | 122 | |
paul@69 | 123 | "Return modified values for 'r', 'g' and 'b', plus an intensity bit." |
paul@69 | 124 | |
paul@69 | 125 | rm = r & 0xc0 |
paul@69 | 126 | gm = g & 0xc0 |
paul@69 | 127 | bm = b & 0xc0 |
paul@134 | 128 | rd = r & 0x3f |
paul@134 | 129 | gd = g & 0x3f |
paul@134 | 130 | bd = b & 0x3f |
paul@134 | 131 | i = (rd + gd + bd) / 3 >= 0x1f and 1 or 0 |
paul@69 | 132 | return rm, gm, bm, i |
paul@69 | 133 | |
paul@69 | 134 | def make_preview(im): |
paul@69 | 135 | imp = PIL.Image.new("RGB", im.size) |
paul@69 | 136 | data = [] |
paul@69 | 137 | for r, g, b in im.getdata(): |
paul@69 | 138 | rm, gm, bm, i = get_values(r, g, b) |
paul@69 | 139 | r = rm + (i * 32) |
paul@69 | 140 | g = gm + (i * 32) |
paul@69 | 141 | b = bm + (i * 32) |
paul@69 | 142 | data.append((r, g, b)) |
paul@69 | 143 | imp.putdata(data) |
paul@69 | 144 | return imp |
paul@69 | 145 | |
paul@73 | 146 | def flush_last_word(f, word, bytealign): |
paul@69 | 147 | if word: |
paul@73 | 148 | if bytealign: |
paul@73 | 149 | write_bytes(f, word) |
paul@73 | 150 | else: |
paul@73 | 151 | pad_word(word) |
paul@73 | 152 | write_word(f, word) |
paul@69 | 153 | del word[:] |
paul@69 | 154 | |
paul@73 | 155 | def flush_word(f, word, bytealign): |
paul@69 | 156 | if len(word) == 4: |
paul@73 | 157 | if bytealign: |
paul@73 | 158 | write_bytes(f, word) |
paul@73 | 159 | else: |
paul@73 | 160 | write_word(f, word) |
paul@69 | 161 | del word[:] |
paul@69 | 162 | |
paul@69 | 163 | def pad_word(word): |
paul@69 | 164 | while len(word) < 4: |
paul@69 | 165 | word.insert(0, 0) |
paul@69 | 166 | |
paul@73 | 167 | def write_bytes(f, word): |
paul@73 | 168 | while word: |
paul@73 | 169 | print >>f, ".byte 0x%02x" % word.pop() |
paul@73 | 170 | |
paul@69 | 171 | def write_word(f, word): |
paul@69 | 172 | print >>f, ".word 0x%02x%02x%02x%02x" % tuple(word) |
paul@69 | 173 | |
paul@69 | 174 | def rotate_and_scale(exif, im, base_width, base_height, rotate, width, height): |
paul@69 | 175 | |
paul@69 | 176 | """ |
paul@69 | 177 | Using the given 'exif' information, rotate and scale image 'im' given the |
paul@69 | 178 | indicated 'base_width' and 'base_height' constraints and any explicit |
paul@69 | 179 | 'rotate' indication. |
paul@69 | 180 | |
paul@69 | 181 | The 'width' and 'height' indicate the final dimensions. |
paul@69 | 182 | |
paul@69 | 183 | The returned image will be within the given 'width' and 'height', filling |
paul@69 | 184 | either or both, and preserve its original aspect ratio. |
paul@69 | 185 | """ |
paul@69 | 186 | |
paul@69 | 187 | if rotate or exif and exif["Image Orientation"].values == [6L]: |
paul@69 | 188 | im = im.rotate(270) |
paul@69 | 189 | |
paul@69 | 190 | w, h = im.size |
paul@69 | 191 | |
paul@69 | 192 | # Get the relationship between the base width and the image width. |
paul@69 | 193 | |
paul@69 | 194 | wsf = float(base_width) / w |
paul@69 | 195 | |
paul@69 | 196 | # Get the relationship between the base height and the image height. |
paul@69 | 197 | |
paul@69 | 198 | hsf = float(base_height) / h |
paul@69 | 199 | |
paul@69 | 200 | # Determine the maximal scaling down required to fit the image. |
paul@69 | 201 | |
paul@69 | 202 | min_scale_factor = min(wsf, hsf) |
paul@69 | 203 | |
paul@69 | 204 | # Determine the final scaling factors to yield the desired dimensions. |
paul@69 | 205 | |
paul@69 | 206 | xscale = float(width) / base_width |
paul@69 | 207 | yscale = float(height) / base_height |
paul@69 | 208 | |
paul@69 | 209 | if min_scale_factor < 1: |
paul@69 | 210 | width = int(min_scale_factor * w * xscale) |
paul@69 | 211 | height = int(min_scale_factor * h * yscale) |
paul@69 | 212 | return im.resize((width, height)) |
paul@69 | 213 | elif scale_factor != 1: |
paul@69 | 214 | width = int(w * wsf) |
paul@69 | 215 | return im.resize((width, h)) |
paul@69 | 216 | else: |
paul@69 | 217 | return im |
paul@69 | 218 | |
paul@69 | 219 | def get_parameter(options, flag, conversion, default, missing): |
paul@69 | 220 | |
paul@69 | 221 | """ |
paul@69 | 222 | From 'options', return any parameter following the given 'flag', applying |
paul@69 | 223 | the 'conversion' which has the given 'default' if no valid parameter is |
paul@69 | 224 | found, or returning the given 'missing' value if the flag does not appear at |
paul@69 | 225 | all. |
paul@69 | 226 | """ |
paul@69 | 227 | |
paul@69 | 228 | try: |
paul@69 | 229 | i = options.index(flag) |
paul@69 | 230 | try: |
paul@69 | 231 | return conversion(options[i+1]) |
paul@69 | 232 | except (IndexError, ValueError): |
paul@69 | 233 | return default |
paul@69 | 234 | except ValueError: |
paul@69 | 235 | return missing |
paul@69 | 236 | |
paul@69 | 237 | # Main program. |
paul@69 | 238 | |
paul@69 | 239 | if __name__ == "__main__": |
paul@69 | 240 | |
paul@69 | 241 | # Test options. |
paul@69 | 242 | |
paul@69 | 243 | if "--help" in sys.argv or len(sys.argv) < 3: |
paul@69 | 244 | basename = split(sys.argv[0])[1] |
paul@69 | 245 | print >>sys.stderr, """\ |
paul@69 | 246 | Usage: |
paul@69 | 247 | |
paul@69 | 248 | %s <input filename> <output filename> <label> [ <options> ] |
paul@69 | 249 | |
paul@73 | 250 | Preview options: |
paul@73 | 251 | |
paul@73 | 252 | -p - Generate a preview with a filename based on the output filename |
paul@73 | 253 | |
paul@73 | 254 | Size options: |
paul@69 | 255 | |
paul@69 | 256 | -W - Indicate the output width (default is 160) |
paul@69 | 257 | |
paul@69 | 258 | -H - Indicate the output height (default is 256) |
paul@69 | 259 | |
paul@73 | 260 | -S - Employ the width and height to define the appropriate output ratio but |
paul@73 | 261 | refine the final width and height and strip away padding |
paul@73 | 262 | |
paul@73 | 263 | Transformation options: |
paul@69 | 264 | |
paul@69 | 265 | -r - Rotate the input image clockwise explicitly |
paul@69 | 266 | (EXIF information is used otherwise) |
paul@73 | 267 | |
paul@73 | 268 | Output options: |
paul@73 | 269 | |
paul@73 | 270 | -b - Use bytes instead of padded words for each line of the output |
paul@69 | 271 | """ % basename |
paul@69 | 272 | sys.exit(1) |
paul@69 | 273 | |
paul@69 | 274 | base_width = 320; width = 160 |
paul@69 | 275 | base_height = height = 256 |
paul@69 | 276 | |
paul@69 | 277 | input_filename, output_filename, label = sys.argv[1:4] |
paul@118 | 278 | options = sys.argv[3:] |
paul@69 | 279 | |
paul@69 | 280 | # Basic image properties. |
paul@69 | 281 | |
paul@69 | 282 | width = get_parameter(options, "-W", int, width, width) |
paul@69 | 283 | height = get_parameter(options, "-H", int, height, height) |
paul@73 | 284 | bytealign = "-b" in options |
paul@69 | 285 | rotate = "-r" in options |
paul@69 | 286 | preview = "-p" in options |
paul@73 | 287 | strip = "-S" in options |
paul@69 | 288 | |
paul@69 | 289 | # Load the input image. |
paul@69 | 290 | |
paul@69 | 291 | exif = EXIF.process_file(open(input_filename)) |
paul@69 | 292 | im = PIL.Image.open(input_filename).convert("RGB") |
paul@69 | 293 | im = rotate_and_scale(exif, im, base_width, base_height, rotate, |
paul@69 | 294 | width, height) |
paul@69 | 295 | |
paul@69 | 296 | # Generate an output image. |
paul@69 | 297 | |
paul@73 | 298 | convert_image(im, output_filename, width, height, strip, bytealign, options, label) |
paul@69 | 299 | |
paul@69 | 300 | # Generate a preview image if requested. |
paul@69 | 301 | |
paul@69 | 302 | if preview: |
paul@69 | 303 | _basename, ext = splitext(input_filename) |
paul@69 | 304 | basename, _ext = splitext(output_filename) |
paul@69 | 305 | preview_filename = "%s_preview%s" % (basename, ext) |
paul@69 | 306 | imp = make_preview(im) |
paul@69 | 307 | imp.save(preview_filename) |
paul@69 | 308 | |
paul@69 | 309 | # vim: tabstop=4 expandtab shiftwidth=4 |