diff --git a/scripts/LVGLImage.py b/scripts/LVGLImage.py index b0e7232ced2..cad8f70c503 100755 --- a/scripts/LVGLImage.py +++ b/scripts/LVGLImage.py @@ -104,6 +104,8 @@ class CompressMethod(Enum): class ColorFormat(Enum): UNKNOWN = 0x00 + RAW = 0x01, + RAW_ALPHA = 0x02, L8 = 0x06 I1 = 0x07 I2 = 0x08 @@ -126,7 +128,6 @@ def bpp(self) -> int: Return bit per pixel for this cf """ cf_map = { - ColorFormat.UNKNOWN: 0x00, ColorFormat.L8: 8, ColorFormat.I1: 1, ColorFormat.I2: 2, @@ -144,7 +145,7 @@ def bpp(self) -> int: ColorFormat.RGB888: 24, } - return cf_map[self] + return cf_map[self] if self in cf_map else 0 @property def ncolors(self) -> int: @@ -174,7 +175,7 @@ def is_alpha_only(self) -> bool: @property def has_alpha(self) -> bool: - return self.is_alpha_only or self in ( + return self.is_alpha_only or self.is_indexed or self in ( ColorFormat.ARGB8888, ColorFormat.XRGB8888, # const alpha: 0xff ColorFormat.ARGB8565, @@ -302,6 +303,88 @@ def unpack_colors(data: bytes, cf: ColorFormat, w) -> List: return ret +def write_c_array_file( + w: int, h: int, + stride: int, + cf: ColorFormat, + filename: str, + premulitplied: bool, + compress: CompressMethod, + data: bytes): + varname = path.basename(filename).split('.')[0] + varname = varname.replace("-", "_") + varname = varname.replace(".", "_") + + flags = "0" + if compress is not CompressMethod.NONE: + flags += " | LV_IMAGE_FLAGS_COMPRESSED" + if premulitplied: + flags += " | LV_IMAGE_FLAGS_PREMULTIPLIED" + + macro = "LV_ATTRIBUTE_" + varname.upper() + header = f''' +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) +#include "lvgl.h" +#elif defined(LV_BUILD_TEST) +#include "../lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef {macro} +#define {macro} +#endif + +static const +LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST {macro} +uint8_t {varname}_map[] = {{ +''' + + ending = f''' +}}; + +const lv_image_dsc_t {varname} = {{ + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_{cf.name}, + .header.flags = {flags}, + .header.w = {w}, + .header.h = {h}, + .header.stride = {stride}, + .data_size = sizeof({varname}_map), + .data = {varname}_map, +}}; + +''' + + def write_binary(f, data, stride): + stride = 16 if stride == 0 else stride + for i, v in enumerate(data): + if i % stride == 0: + f.write("\n ") + f.write(f"0x{v:02x},") + f.write("\n") + + with open(filename, "w+") as f: + f.write(header) + + if compress != CompressMethod.NONE: + write_binary(f, data, 16) + else: + # write palette separately + ncolors = cf.ncolors + if ncolors: + write_binary(f, data[:ncolors * 4], 16) + + write_binary(f, data[ncolors * 4:], stride) + + f.write(ending) + + class LVGLImageHeader: def __init__(self, @@ -412,12 +495,14 @@ def __init__(self, h: int = 0, data: bytes = b'') -> None: self.stride = 0 # default no valid stride value + self.premulitplied = False self.set_data(cf, w, h, data) def __repr__(self) -> str: - return ( - f"'LVGL image {self.w}x{self.h}, {self.cf.name}, stride: {self.stride}" - f" (12+{self.data_len})Byte'") + return (f"'LVGL image {self.w}x{self.h}, {self.cf.name}, " + f"{'Pre-multiplied, ' if self.premultiplied else ''}" + f"stride: {self.stride} " + f"(12+{self.data_len})Byte'") def adjust_stride(self, stride: int = 0, align: int = 1): """ @@ -484,7 +569,98 @@ def change_stride(data: bytearray, h, current_stride, new_stride): stride // 2)) self.stride = stride - self.data = b''.join(data_out) + self.data = bytearray(b''.join(data_out)) + + def premulitply(self): + """ + Pre-multiply image RGB data with alpha, set corresponding image header flags + """ + if self.premulitplied: + raise ParameterError("Image already pre-mulitplied") + + if not self.cf.has_alpha: + raise ParameterError(f"Image has no alpha channel: {self.cf.name}") + + if self.cf.is_indexed: + + def multiply(r, g, b, a): + r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8 + return uint8_t(b) + uint8_t(g) + uint8_t(r) + uint8_t(a) + + # process the palette only. + palette_size = self.cf.ncolors * 4 + palette = self.data[:palette_size] + palette = [ + multiply(palette[i], palette[i + 1], palette[i + 2], + palette[i + 3]) for i in range(0, len(palette), 4) + ] + palette = b''.join(palette) + self.data = palette + self.data[palette_size:] + elif self.cf is ColorFormat.ARGB8888: + + def multiply(b, g, r, a): + r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8 + return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0)) + + line_width = self.w * 4 + for h in range(self.h): + offset = h * self.stride + map = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply(map[i], map[i + 1], map[i + 2], map[i + 3]) + for i in range(0, line_width, 4) + ]) + self.data[offset:offset + line_width] = processed + elif self.cf is ColorFormat.RGB565A8: + + def multiply(data, a): + r = (data >> 11) & 0x1f + g = (data >> 5) & 0x3f + b = (data >> 0) & 0x1f + + r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255 + return uint16_t((r << 11) | (g << 5) | (b << 0)) + + line_width = self.w * 2 + for h in range(self.h): + # alpha map offset for this line + offset = self.h * self.stride + h * (self.stride // 2) + a = self.data[offset:offset + self.stride // 2] + + # RGB map offset + offset = h * self.stride + rgb = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply((rgb[i + 1] << 8) | rgb[i], a[i // 2]) + for i in range(0, line_width, 2) + ]) + self.data[offset:offset + line_width] = processed + elif self.cf is ColorFormat.ARGB8565: + + def multiply(data, a): + r = (data >> 11) & 0x1f + g = (data >> 5) & 0x3f + b = (data >> 0) & 0x1f + + r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255 + return uint24_t((a << 16) | (r << 11) | (g << 5) | (b << 0)) + + line_width = self.w * 3 + for h in range(self.h): + offset = h * self.stride + map = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply((map[i + 1] << 8) | map[i], map[i + 2]) + for i in range(0, line_width, 3) + ]) + self.data[offset:offset + line_width] = processed + else: + raise ParameterError(f"Not supported yet: {self.cf.name}") + + self.premulitplied = True @property def data_len(self) -> int: @@ -577,6 +753,7 @@ def to_bin(self, bin = bytearray() flags = 0 flags |= 0x08 if compress != CompressMethod.NONE else 0 + flags |= 0x01 if self.premulitplied else 0 header = LVGLImageHeader(self.cf, self.w, @@ -597,78 +774,13 @@ def to_c_array(self, self._check_ext(filename, ".c") self._check_dir(filename) - varname = path.basename(filename).split('.')[0] - varname = varname.replace("-", "_") - varname = varname.replace(".", "_") - - flags = "0" - if compress is not CompressMethod.NONE: - flags += " | LV_IMAGE_FLAGS_COMPRESSED" - - compressed = LVGLCompressData(self.cf, compress, self.data) - macro = "LV_ATTRIBUTE_" + varname.upper() - header = f''' -#if defined(LV_LVGL_H_INCLUDE_SIMPLE) -#include "lvgl.h" -#elif defined(LV_BUILD_TEST) -#include "../lvgl.h" -#else -#include "lvgl/lvgl.h" -#endif - - -#ifndef LV_ATTRIBUTE_MEM_ALIGN -#define LV_ATTRIBUTE_MEM_ALIGN -#endif - -#ifndef {macro} -#define {macro} -#endif - -static const -LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST {macro} -uint8_t {varname}_map[] = {{ -''' - - ending = f''' -}}; - -const lv_image_dsc_t {varname} = {{ - .header.magic = LV_IMAGE_HEADER_MAGIC, - .header.cf = LV_COLOR_FORMAT_{self.cf.name}, - .header.flags = {flags}, - .header.w = {self.w}, - .header.h = {self.h}, - .header.stride = {self.stride}, - .data_size = sizeof({varname}_map), - .data = {varname}_map, -}}; - -''' - - def write_binary(f, data, stride): - for i, v in enumerate(data): - if i % stride == 0: - f.write("\n ") - f.write(f"0x{v:02x},") - f.write("\n") - - with open(filename, "w+") as f: - f.write(header) - - if compress is not CompressMethod.NONE: - write_binary(f, compressed.compressed, 16) - else: - # write palette separately - ncolors = self.cf.ncolors - if ncolors: - write_binary(f, self.data[:ncolors * 4], 16) - - write_binary(f, self.data[ncolors * 4:], self.stride) - - f.write(ending) - - return self + if compress != CompressMethod.NONE: + data = LVGLCompressData(self.cf, compress, self.data).compressed + else: + data = self.data + write_c_array_file(self.w, self.h, self.stride, self.cf, filename, + self.premulitplied, + compress, data) def to_png(self, filename: str): self._check_ext(filename, ".png") @@ -819,7 +931,7 @@ def _png_to_alpha_only(self, cf: ColorFormat, filename: str): rawdata += row self.set_data(cf, w, h, rawdata) - + def sRGB_to_linear(self, x): if x < 0.04045: return x / 12.92 @@ -1035,10 +1147,46 @@ def get_nonrepeat_count(self, data: bytearray, blksize: int, threshold): return nonrepeat_count +class RAWImage(): + ''' + RAW image is an exception to LVGL image, it has color format of RAW or RAW_ALPHA. + It has same image header as LVGL image, but the data is pure raw data from file. + It does not support stride adjustment etc. features for LVGL image. + It only supports convert an image to C array with RAW or RAW_ALPHA format. + ''' + CF_SUPPORTED = (ColorFormat.RAW, ColorFormat.RAW_ALPHA) + + class NotSupported(NotImplementedError): + pass + + def __init__(self, + cf: ColorFormat = ColorFormat.UNKNOWN, + data: bytes = b'') -> None: + self.cf = cf + self.data = data + + def to_c_array(self, + filename: str): + # Image size is set to zero, to let PNG or JPEG decoder to handle it + # Stride is meaningless for RAW image + write_c_array_file(0, 0, 0, self.cf, filename, + False, CompressMethod.NONE, self.data) + + def from_file(self, + filename: str, + cf: ColorFormat = None): + if cf not in RAWImage.CF_SUPPORTED: + raise RAWImage.NotSupported(f"Invalid color format: {cf.name}") + + with open(filename, "rb") as f: + self.data = f.read() + self.cf = cf + return self + + class OutputFormat(Enum): C_ARRAY = "C" BIN_FILE = "BIN" - RAW_DATA = "RAW" # option of not writing any file PNG_FILE = "PNG" # convert to lvgl image and then to png @@ -1051,6 +1199,7 @@ def __init__(self, odir: str, background: int = 0x00, align: int = 1, + premultiply: bool = False, compress: CompressMethod = CompressMethod.NONE, keep_folder=True) -> None: self.files = files @@ -1060,6 +1209,7 @@ def __init__(self, self.pngquant = None self.keep_folder = keep_folder self.align = align + self.premultiply = premultiply self.compress = compress self.background = background @@ -1075,17 +1225,24 @@ def _replace_ext(self, input, ext): def convert(self): output = [] for f in self.files: - img = LVGLImage().from_png(f, self.cf, background=self.background) - img.adjust_stride(align=self.align) - output.append((f, img)) - if self.ofmt == OutputFormat.BIN_FILE: - img.to_bin(self._replace_ext(f, ".bin"), - compress=self.compress) - elif self.ofmt == OutputFormat.C_ARRAY: - img.to_c_array(self._replace_ext(f, ".c"), + if self.cf in (ColorFormat.RAW, ColorFormat.RAW_ALPHA): + # Process RAW image explicitly + img = RAWImage().from_file(f, self.cf) + img.to_c_array(self._replace_ext(f, ".c")) + else: + img = LVGLImage().from_png(f, self.cf, background=self.background) + img.adjust_stride(align=self.align) + if self.premultiply: + img.premulitply() + output.append((f, img)) + if self.ofmt == OutputFormat.BIN_FILE: + img.to_bin(self._replace_ext(f, ".bin"), compress=self.compress) - elif self.ofmt == OutputFormat.PNG_FILE: - img.to_png(self._replace_ext(f, ".png")) + elif self.ofmt == OutputFormat.C_ARRAY: + img.to_c_array(self._replace_ext(f, ".c"), + compress=self.compress) + elif self.ofmt == OutputFormat.PNG_FILE: + img.to_png(self._replace_ext(f, ".png")) return output @@ -1103,9 +1260,13 @@ def main(): default="I8", choices=[ "L8", "I1", "I2", "I4", "I8", "A1", "A2", "A4", "A8", "ARGB8888", - "XRGB8888", "RGB565", "RGB565A8", "ARGB8565", "RGB888", "AUTO" + "XRGB8888", "RGB565", "RGB565A8", "ARGB8565", "RGB888", "AUTO", + "RAW", "RAW_ALPHA" ]) + parser.add_argument('--premultiply', action='store_true', + help="pre-multiply color with alpha", default=False) + parser.add_argument('--compress', help=("Binary data compress method, default to NONE"), default="NONE", @@ -1150,7 +1311,8 @@ def main(): else: cf = ColorFormat[args.cf] - ofmt = OutputFormat(args.ofmt) + ofmt = OutputFormat(args.ofmt) if cf not in ( + ColorFormat.RAW, ColorFormat.RAW_ALPHA) else OutputFormat.C_ARRAY compress = CompressMethod[args.compress] converter = PNGConverter(files, @@ -1159,6 +1321,7 @@ def main(): args.output, background=args.background, align=args.align, + premultiply=args.premultiply, compress=compress, keep_folder=False) output = converter.convert() @@ -1175,11 +1338,21 @@ def test(): cf=ColorFormat.ARGB8565, background=0xFF_FF_00) img.adjust_stride(align=16) + img.premulitply() img.to_bin("output/cogwheel.ARGB8565.bin") img.to_c_array("output/cogwheel-abc.c") # file name is used as c var name img.to_png("output/cogwheel.ARGB8565.png.png") # convert back to png +def test_raw(): + logging.basicConfig(level=logging.INFO) + f = "pngs/cogwheel.RGB565A8.png" + img = RAWImage().from_file(f, + cf=ColorFormat.RAW_ALPHA) + img.to_c_array("output/cogwheel-raw.c") + + if __name__ == "__main__": # test() + # test_raw() main() diff --git a/tests/main.py b/tests/main.py index 8eb02e1f141..a2e5235e99b 100755 --- a/tests/main.py +++ b/tests/main.py @@ -145,7 +145,7 @@ def generate_code_coverage_report(): def generate_test_images(): - invalids = (ColorFormat.UNKNOWN,) + invalids = (ColorFormat.UNKNOWN,ColorFormat.RAW,ColorFormat.RAW_ALPHA) formats = [i for i in ColorFormat if i not in invalids] png_path = os.path.join(lvgl_test_dir, "test_images/pngs") pngs = list(Path(png_path).rglob("*.[pP][nN][gG]"))