aboutsummaryrefslogtreecommitdiffstats
path: root/src/resources
diff options
context:
space:
mode:
Diffstat (limited to 'src/resources')
-rw-r--r--src/resources/CMakeLists.txt4
-rwxr-xr-xsrc/resources/generate-img.py3
-rwxr-xr-xsrc/resources/lv_img_conv.py193
3 files changed, 198 insertions, 2 deletions
diff --git a/src/resources/CMakeLists.txt b/src/resources/CMakeLists.txt
index 0983aaff..3834e854 100644
--- a/src/resources/CMakeLists.txt
+++ b/src/resources/CMakeLists.txt
@@ -3,8 +3,8 @@ find_program(LV_FONT_CONV "lv_font_conv" NO_CACHE REQUIRED
HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin")
message(STATUS "Using ${LV_FONT_CONV} to generate font files")
-find_program(LV_IMG_CONV "lv_img_conv" NO_CACHE REQUIRED
- HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin")
+find_program(LV_IMG_CONV "lv_img_conv.py" NO_CACHE REQUIRED
+ HINTS "${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "Using ${LV_IMG_CONV} to generate font files")
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12)
diff --git a/src/resources/generate-img.py b/src/resources/generate-img.py
index cdbfc030..518d2206 100755
--- a/src/resources/generate-img.py
+++ b/src/resources/generate-img.py
@@ -11,6 +11,9 @@ import subprocess
def gen_lvconv_line(lv_img_conv: str, dest: str, color_format: str, output_format: str, binary_format: str, sources: str):
args = [lv_img_conv, sources, '--force', '--output-file', dest, '--color-format', color_format, '--output-format', output_format, '--binary-format', binary_format]
+ if lv_img_conv.endswith(".py"):
+ # lv_img_conv is a python script, call with current python executable
+ args = [sys.executable] + args
return args
diff --git a/src/resources/lv_img_conv.py b/src/resources/lv_img_conv.py
new file mode 100755
index 00000000..04765462
--- /dev/null
+++ b/src/resources/lv_img_conv.py
@@ -0,0 +1,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())