InfiniTime.git

ref: e5b73212f6addcfdb5e306df63d7135e543c4f8d

src/resources/lv_img_conv.py


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env python3
import argparse
import pathlib
import sys
import decimal
from PIL import Image


def classify_pixel(value, bits):
    def round_half_up(v):
        """python3 implements "propper" "banker's rounding" by rounding to the nearest
        even number. Javascript rounds to the nearest integer.
        To have the same output as the original JavaScript implementation add a custom
        rounding function, which does "school" rounding (to the nearest integer).

        see: https://stackoverflow.com/questions/43851273/how-to-round-float-0-5-up-to-1-0-while-still-rounding-0-45-to-0-0-as-the-usual
        """
        return int(decimal.Decimal(v).quantize(decimal.Decimal('1'), rounding=decimal.ROUND_HALF_UP))
    tmp = 1 << (8 - bits)
    val = round_half_up(value / tmp) * tmp
    if val < 0:
        val = 0
    return val


def test_classify_pixel():
    # test difference between round() and round_half_up()
    assert classify_pixel(18, 5) == 16
    # school rounding 4.5 to 5, but banker's rounding 4.5 to 4
    assert classify_pixel(18, 6) == 20


def main():
    parser = argparse.ArgumentParser()

    parser.add_argument("img",
        help="Path to image to convert to C header file")
    parser.add_argument("-o", "--output-file",
        help="output file path (for single-image conversion)",
        required=True)
    parser.add_argument("-f", "--force",
        help="allow overwriting the output file",
        action="store_true")
    parser.add_argument("-i", "--image-name",
        help="name of image structure (not implemented)")
    parser.add_argument("-c", "--color-format",
        help="color format of image",
        default="CF_TRUE_COLOR_ALPHA",
        choices=[
            "CF_ALPHA_1_BIT", "CF_ALPHA_2_BIT", "CF_ALPHA_4_BIT",
            "CF_ALPHA_8_BIT", "CF_INDEXED_1_BIT", "CF_INDEXED_2_BIT", "CF_INDEXED_4_BIT",
            "CF_INDEXED_8_BIT", "CF_RAW", "CF_RAW_CHROMA", "CF_RAW_ALPHA",
            "CF_TRUE_COLOR", "CF_TRUE_COLOR_ALPHA", "CF_TRUE_COLOR_CHROMA", "CF_RGB565A8",
        ],
        required=True)
    parser.add_argument("-t", "--output-format",
        help="output format of image",
        default="bin", # default in original is 'c'
        choices=["c", "bin"])
    parser.add_argument("--binary-format",
        help="binary color format (needed if output-format is binary)",
        default="ARGB8565_RBSWAP",
        choices=["ARGB8332", "ARGB8565", "ARGB8565_RBSWAP", "ARGB8888"])
    parser.add_argument("-s", "--swap-endian",
        help="swap endian of image (not implemented)",
        action="store_true")
    parser.add_argument("-d", "--dither",
        help="enable dither (not implemented)",
        action="store_true")
    args = parser.parse_args()

    img_path = pathlib.Path(args.img)
    out = pathlib.Path(args.output_file)
    if not img_path.is_file():
        print(f"Input file is missing: '{args.img}'")
        return 1
    print(f"Beginning conversion of {args.img}")
    if out.exists():
        if args.force:
            print(f"overwriting {args.output_file}")
        else:
            pritn(f"Error: refusing to overwrite {args.output_file} without -f specified.")
            return 1
    out.touch()

    # only implemented the bare minimum, everything else is not implemented
    if args.color_format not in ["CF_INDEXED_1_BIT", "CF_TRUE_COLOR_ALPHA"]:
        raise NotImplementedError(f"argument --color-format '{args.color_format}' not implemented")
    if args.output_format != "bin":
        raise NotImplementedError(f"argument --output-format '{args.output_format}' not implemented")
    if args.binary_format not in ["ARGB8565_RBSWAP", "ARGB8888"]:
        raise NotImplementedError(f"argument --binary-format '{args.binary_format}' not implemented")
    if args.image_name:
        raise NotImplementedError(f"argument --image-name not implemented")
    if args.swap_endian:
        raise NotImplementedError(f"argument --swap-endian not implemented")
    if args.dither:
        raise NotImplementedError(f"argument --dither not implemented")

    # open image using Pillow
    img = Image.open(img_path)
    img_height = img.height
    img_width = img.width
    if args.color_format == "CF_TRUE_COLOR_ALPHA" and args.binary_format == "ARGB8888":
        buf = bytearray(img_height*img_width*4) # 4 bytes (32 bit) per pixel
        for y in range(img_height):
            for x in range(img_width):
                i = (y*img_width + x)*4 # buffer-index
                pixel = img.getpixel((x,y))
                r, g, b, a = pixel
                buf[i + 0] = r
                buf[i + 1] = g
                buf[i + 2] = b
                buf[i + 3] = a

    elif args.color_format == "CF_TRUE_COLOR_ALPHA" and args.binary_format == "ARGB8565_RBSWAP":
        buf = bytearray(img_height*img_width*3) # 3 bytes (24 bit) per pixel
        for y in range(img_height):
            for x in range(img_width):
                i = (y*img_width + x)*3 # buffer-index
                pixel = img.getpixel((x,y))
                r_act = classify_pixel(pixel[0], 5)
                g_act = classify_pixel(pixel[1], 6)
                b_act = classify_pixel(pixel[2], 5)
                a = pixel[3]
                r_act = min(r_act, 0xF8)
                g_act = min(g_act, 0xFC)
                b_act = min(b_act, 0xF8)
                c16 = ((r_act) << 8) | ((g_act) << 3) | ((b_act) >> 3) # RGR565
                buf[i + 0] = (c16 >> 8) & 0xFF
                buf[i + 1] = c16 & 0xFF
                buf[i + 2] = a

    elif args.color_format == "CF_INDEXED_1_BIT": # ignore binary format, use color format as binary format
        w = img_width >> 3
        if img_width & 0x07:
            w+=1
        max_p = w * (img_height-1) + ((img_width-1) >> 3) + 8  # +8 for the palette
        buf = bytearray(max_p+1)

        for y in range(img_height):
            for x in range(img_width):
                c, a = img.getpixel((x,y))
                p = w * y + (x >> 3) + 8  # +8 for the palette
                buf[p] |= (c & 0x1) << (7 - (x & 0x7))
        # write palette information, for indexed-1-bit we need palette with two values
        # write 8 palette bytes
        buf[0] = 0
        buf[1] = 0
        buf[2] = 0
        buf[3] = 0
        # Normally there is much math behind this, but for the current use case this is close enough
        # only needs to be more complicated if we have more than 2 colors in the palette
        buf[4] = 255
        buf[5] = 255
        buf[6] = 255
        buf[7] = 255
    else:
        # raise just to be sure
        raise NotImplementedError(f"args.color_format '{args.color_format}' with args.binary_format '{args.binary_format}' not implemented")

    # write header
    match args.color_format:
        case "CF_TRUE_COLOR_ALPHA":
            lv_cf = 5
        case "CF_INDEXED_1_BIT":
            lv_cf = 7
        case _:
            # raise just to be sure
            raise NotImplementedError(f"args.color_format '{args.color_format}' not implemented")
    header_32bit = lv_cf | (img_width << 10) | (img_height << 21)
    buf_out = bytearray(4 + len(buf))
    buf_out[0] = header_32bit & 0xFF
    buf_out[1] = (header_32bit & 0xFF00) >> 8
    buf_out[2] = (header_32bit & 0xFF0000) >> 16
    buf_out[3] = (header_32bit & 0xFF000000) >> 24
    buf_out[4:] = buf

    # write byte buffer to file
    with open(out, "wb") as f:
        f.write(buf_out)
    return 0


if __name__ == '__main__':
    if "--test" in sys.argv:
        # run small set of tests and exit
        print("running tests")
        test_classify_pixel()
        print("success!")
        sys.exit(0)
    # run normal program
    sys.exit(main())