CommonPIC32

Annotated tools/makeimage.py

151:5def8be211f4
2020-04-13 Paul Boddie Added a diagram of the output routing from pins to the socket.
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