#!/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())