mirror of
https://github.com/lvgl/lvgl.git
synced 2024-11-23 01:33:59 +08:00
7f690a2f22
Signed-off-by: wxd <xaowang96@gmail.com>
1360 lines
43 KiB
Python
Executable File
1360 lines
43 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import logging
|
|
import argparse
|
|
import subprocess
|
|
from os import path
|
|
from enum import Enum
|
|
from typing import List
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import png
|
|
except ImportError:
|
|
raise ImportError("Need pypng package, do `pip3 install pypng`")
|
|
|
|
try:
|
|
import lz4.block
|
|
except ImportError:
|
|
raise ImportError("Need lz4 package, do `pip3 install lz4`")
|
|
|
|
|
|
def uint8_t(val) -> bytes:
|
|
return val.to_bytes(1, byteorder='little')
|
|
|
|
|
|
def uint16_t(val) -> bytes:
|
|
return val.to_bytes(2, byteorder='little')
|
|
|
|
|
|
def uint24_t(val) -> bytes:
|
|
return val.to_bytes(3, byteorder='little')
|
|
|
|
|
|
def uint32_t(val) -> bytes:
|
|
try:
|
|
return val.to_bytes(4, byteorder='little')
|
|
except OverflowError:
|
|
raise ParameterError(f"overflow: {hex(val)}")
|
|
|
|
|
|
def color_pre_multiply(r, g, b, a, background):
|
|
bb = background & 0xff
|
|
bg = (background >> 8) & 0xff
|
|
br = (background >> 16) & 0xff
|
|
|
|
return ((r * a + (255 - a) * br) >> 8, (g * a + (255 - a) * bg) >> 8,
|
|
(b * a + (255 - a) * bb) >> 8, a)
|
|
|
|
|
|
class Error(Exception):
|
|
|
|
def __str__(self):
|
|
return self.__class__.__name__ + ': ' + ' '.join(self.args)
|
|
|
|
|
|
class FormatError(Error):
|
|
"""
|
|
Problem with input filename format.
|
|
BIN filename does not conform to standard lvgl bin image format
|
|
"""
|
|
|
|
|
|
class ParameterError(Error):
|
|
"""
|
|
Parameter for LVGL image not correct
|
|
"""
|
|
|
|
|
|
class PngQuant:
|
|
"""
|
|
Compress PNG file to 8bit mode using `pngquant`
|
|
"""
|
|
|
|
def __init__(self, ncolors=256, dither=True, exec_path="") -> None:
|
|
executable = path.join(exec_path, "pngquant")
|
|
self.cmd = (f"{executable} {'--nofs' if not dither else ''} "
|
|
f"{ncolors} --force - < ")
|
|
|
|
def convert(self, filename) -> bytes:
|
|
if not os.path.isfile(filename):
|
|
raise BaseException(f"file not found: {filename}")
|
|
|
|
try:
|
|
compressed = subprocess.check_output(
|
|
f'{self.cmd} "{str(filename)}"',
|
|
stderr=subprocess.STDOUT,
|
|
shell=True)
|
|
except subprocess.CalledProcessError:
|
|
raise BaseException(
|
|
"cannot find pngquant tool, install it via "
|
|
"`sudo apt install pngquant` for debian "
|
|
"or `brew install pngquant` for macintosh "
|
|
"For windows, you may need to download pngquant.exe from "
|
|
"https://pngquant.org/, and put it in your PATH.")
|
|
|
|
return compressed
|
|
|
|
|
|
class CompressMethod(Enum):
|
|
NONE = 0x00
|
|
RLE = 0x01
|
|
LZ4 = 0x02
|
|
|
|
|
|
class ColorFormat(Enum):
|
|
UNKNOWN = 0x00
|
|
RAW = 0x01,
|
|
RAW_ALPHA = 0x02,
|
|
L8 = 0x06
|
|
I1 = 0x07
|
|
I2 = 0x08
|
|
I4 = 0x09
|
|
I8 = 0x0A
|
|
A1 = 0x0B
|
|
A2 = 0x0C
|
|
A4 = 0x0D
|
|
A8 = 0x0E
|
|
ARGB8888 = 0x10
|
|
XRGB8888 = 0x11
|
|
RGB565 = 0x12
|
|
ARGB8565 = 0x13
|
|
RGB565A8 = 0x14
|
|
RGB888 = 0x0F
|
|
|
|
@property
|
|
def bpp(self) -> int:
|
|
"""
|
|
Return bit per pixel for this cf
|
|
"""
|
|
cf_map = {
|
|
ColorFormat.L8: 8,
|
|
ColorFormat.I1: 1,
|
|
ColorFormat.I2: 2,
|
|
ColorFormat.I4: 4,
|
|
ColorFormat.I8: 8,
|
|
ColorFormat.A1: 1,
|
|
ColorFormat.A2: 2,
|
|
ColorFormat.A4: 4,
|
|
ColorFormat.A8: 8,
|
|
ColorFormat.ARGB8888: 32,
|
|
ColorFormat.XRGB8888: 32,
|
|
ColorFormat.RGB565: 16,
|
|
ColorFormat.RGB565A8: 16, # 16bpp + a8 map
|
|
ColorFormat.ARGB8565: 24,
|
|
ColorFormat.RGB888: 24,
|
|
}
|
|
|
|
return cf_map[self] if self in cf_map else 0
|
|
|
|
@property
|
|
def ncolors(self) -> int:
|
|
"""
|
|
Return number of colors in palette if cf is indexed1/2/4/8.
|
|
Return zero if cf is not indexed format
|
|
"""
|
|
|
|
cf_map = {
|
|
ColorFormat.I1: 2,
|
|
ColorFormat.I2: 4,
|
|
ColorFormat.I4: 16,
|
|
ColorFormat.I8: 256,
|
|
}
|
|
return cf_map.get(self, 0)
|
|
|
|
@property
|
|
def is_indexed(self) -> bool:
|
|
"""
|
|
Return if cf is indexed color format
|
|
"""
|
|
return self.ncolors != 0
|
|
|
|
@property
|
|
def is_alpha_only(self) -> bool:
|
|
return ColorFormat.A1.value <= self.value <= ColorFormat.A8.value
|
|
|
|
@property
|
|
def has_alpha(self) -> bool:
|
|
return self.is_alpha_only or self.is_indexed or self in (
|
|
ColorFormat.ARGB8888,
|
|
ColorFormat.XRGB8888, # const alpha: 0xff
|
|
ColorFormat.ARGB8565,
|
|
ColorFormat.RGB565A8)
|
|
|
|
@property
|
|
def is_colormap(self) -> bool:
|
|
return self in (ColorFormat.ARGB8888, ColorFormat.RGB888,
|
|
ColorFormat.XRGB8888, ColorFormat.RGB565A8,
|
|
ColorFormat.ARGB8565, ColorFormat.RGB565)
|
|
|
|
@property
|
|
def is_luma_only(self) -> bool:
|
|
return self in (ColorFormat.L8, )
|
|
|
|
|
|
def bit_extend(value, bpp):
|
|
"""
|
|
Extend value from bpp to 8 bit with interpolation to reduce rounding error.
|
|
"""
|
|
|
|
if value == 0:
|
|
return 0
|
|
|
|
res = value
|
|
bpp_now = bpp
|
|
while bpp_now < 8:
|
|
res |= value << (8 - bpp_now)
|
|
bpp_now += bpp
|
|
|
|
return res
|
|
|
|
|
|
def unpack_colors(data: bytes, cf: ColorFormat, w) -> List:
|
|
"""
|
|
Unpack lvgl 1/2/4/8/16/32 bpp color to png color: alpha map, grey scale,
|
|
or R,G,B,(A) map
|
|
"""
|
|
ret = []
|
|
bpp = cf.bpp
|
|
if bpp == 8:
|
|
ret = data
|
|
elif bpp == 4:
|
|
if cf == ColorFormat.A4:
|
|
values = [x * 17 for x in range(16)]
|
|
else:
|
|
values = [x for x in range(16)]
|
|
|
|
for p in data:
|
|
for i in range(2):
|
|
ret.append(values[(p >> (4 - i * 4)) & 0x0f])
|
|
if len(ret) % w == 0:
|
|
break
|
|
|
|
elif bpp == 2:
|
|
if cf == ColorFormat.A2:
|
|
values = [x * 85 for x in range(4)]
|
|
else: # must be ColorFormat.I2
|
|
values = [x for x in range(4)]
|
|
for p in data:
|
|
for i in range(4):
|
|
ret.append(values[(p >> (6 - i * 2)) & 0x03])
|
|
if len(ret) % w == 0:
|
|
break
|
|
elif bpp == 1:
|
|
if cf == ColorFormat.A1:
|
|
values = [0, 255]
|
|
else:
|
|
values = [0, 1]
|
|
for p in data:
|
|
for i in range(8):
|
|
ret.append(values[(p >> (7 - i)) & 0x01])
|
|
if len(ret) % w == 0:
|
|
break
|
|
elif bpp == 16:
|
|
# This is RGB565
|
|
pixels = [(data[2 * i + 1] << 8) | data[2 * i]
|
|
for i in range(len(data) // 2)]
|
|
|
|
for p in pixels:
|
|
ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
|
|
ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
|
|
ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
|
|
elif bpp == 24:
|
|
if cf == ColorFormat.RGB888:
|
|
B = data[0::3]
|
|
G = data[1::3]
|
|
R = data[2::3]
|
|
for r, g, b in zip(R, G, B):
|
|
ret += [r, g, b]
|
|
elif cf == ColorFormat.RGB565A8:
|
|
alpha_size = len(data) // 3
|
|
pixel_alpha = data[-alpha_size:]
|
|
pixel_data = data[:-alpha_size]
|
|
pixels = [(pixel_data[2 * i + 1] << 8) | pixel_data[2 * i]
|
|
for i in range(len(pixel_data) // 2)]
|
|
|
|
for a, p in zip(pixel_alpha, pixels):
|
|
ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
|
|
ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
|
|
ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
|
|
ret.append(a)
|
|
elif cf == ColorFormat.ARGB8565:
|
|
L = data[0::3]
|
|
H = data[1::3]
|
|
A = data[2::3]
|
|
|
|
for h, l, a in zip(H, L, A):
|
|
p = (h << 8) | (l)
|
|
ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
|
|
ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
|
|
ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
|
|
ret.append(a) # A
|
|
|
|
elif bpp == 32:
|
|
B = data[0::4]
|
|
G = data[1::4]
|
|
R = data[2::4]
|
|
A = data[3::4]
|
|
for r, g, b, a in zip(R, G, B, A):
|
|
ret += [r, g, b, a]
|
|
else:
|
|
assert 0
|
|
|
|
return ret
|
|
|
|
|
|
def write_c_array_file(
|
|
w: int, h: int,
|
|
stride: int,
|
|
cf: ColorFormat,
|
|
filename: str,
|
|
premultiplied: 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 premultiplied:
|
|
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,
|
|
cf: ColorFormat = ColorFormat.UNKNOWN,
|
|
w: int = 0,
|
|
h: int = 0,
|
|
stride: int = 0,
|
|
align: int = 1,
|
|
flags: int = 0):
|
|
self.cf = cf
|
|
self.flags = flags
|
|
self.w = w & 0xffff
|
|
self.h = h & 0xffff
|
|
if w > 0xffff or h > 0xffff:
|
|
raise ParameterError(f"w, h overflow: {w}x{h}")
|
|
if align < 1:
|
|
# stride align in bytes must be larger than 1
|
|
raise ParameterError(f"Invalid stride align: {align}")
|
|
|
|
self.stride = self.stride_align(align) if stride == 0 else stride
|
|
|
|
def stride_align(self, align: int) -> int:
|
|
stride = self.stride_default
|
|
if align == 1:
|
|
pass
|
|
elif align > 1:
|
|
stride = (stride + align - 1) // align
|
|
stride *= align
|
|
else:
|
|
raise ParameterError(f"Invalid stride align: {align}")
|
|
|
|
self.stride = stride
|
|
return stride
|
|
|
|
@property
|
|
def stride_default(self) -> int:
|
|
return (self.w * self.cf.bpp + 7) // 8
|
|
|
|
@property
|
|
def binary(self) -> bytearray:
|
|
binary = bytearray()
|
|
binary += uint8_t(0x19) # magic number for lvgl version 9
|
|
binary += uint8_t(self.cf.value)
|
|
binary += uint16_t(self.flags) # 16bits flags
|
|
|
|
binary += uint16_t(self.w) # 16bits width
|
|
binary += uint16_t(self.h) # 16bits height
|
|
binary += uint16_t(self.stride) # 16bits stride
|
|
|
|
binary += uint16_t(0) # 16bits reserved
|
|
return binary
|
|
|
|
def from_binary(self, data: bytes):
|
|
if len(data) < 12:
|
|
raise FormatError("invalid header length")
|
|
|
|
try:
|
|
self.cf = ColorFormat(data[1] & 0x1f) # color format
|
|
except ValueError as exc:
|
|
raise FormatError(f"invalid color format: {hex(data[0])}") from exc
|
|
self.w = int.from_bytes(data[4:6], 'little')
|
|
self.h = int.from_bytes(data[6:8], 'little')
|
|
self.stride = int.from_bytes(data[8:10], 'little')
|
|
return self
|
|
|
|
|
|
class LVGLCompressData:
|
|
|
|
def __init__(self,
|
|
cf: ColorFormat,
|
|
method: CompressMethod,
|
|
raw_data: bytes = b''):
|
|
self.blk_size = (cf.bpp + 7) // 8
|
|
self.compress = method
|
|
self.raw_data = raw_data
|
|
self.raw_data_len = len(raw_data)
|
|
self.compressed = self._compress(raw_data)
|
|
|
|
def _compress(self, raw_data: bytes) -> bytearray:
|
|
if self.compress == CompressMethod.NONE:
|
|
return raw_data
|
|
|
|
if self.compress == CompressMethod.RLE:
|
|
# RLE compression performs on pixel unit, pad data to pixel unit
|
|
pad = b'\x00' * 0
|
|
if self.raw_data_len % self.blk_size:
|
|
pad = b'\x00' * (self.blk_size - self.raw_data_len % self.blk_size)
|
|
compressed = RLEImage().rle_compress(raw_data + pad, self.blk_size)
|
|
elif self.compress == CompressMethod.LZ4:
|
|
compressed = lz4.block.compress(raw_data, store_size=False)
|
|
else:
|
|
raise ParameterError(f"Invalid compress method: {self.compress}")
|
|
|
|
self.compressed_len = len(compressed)
|
|
|
|
bin = bytearray()
|
|
bin += uint32_t(self.compress.value)
|
|
bin += uint32_t(self.compressed_len)
|
|
bin += uint32_t(self.raw_data_len)
|
|
bin += compressed
|
|
return bin
|
|
|
|
|
|
class LVGLImage:
|
|
|
|
def __init__(self,
|
|
cf: ColorFormat = ColorFormat.UNKNOWN,
|
|
w: int = 0,
|
|
h: int = 0,
|
|
data: bytes = b'') -> None:
|
|
self.stride = 0 # default no valid stride value
|
|
self.premultiplied = False
|
|
self.set_data(cf, w, h, data)
|
|
|
|
def __repr__(self) -> str:
|
|
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):
|
|
"""
|
|
Stride can be set directly, or by stride alignment in bytes
|
|
"""
|
|
if self.stride == 0:
|
|
# stride can only be 0, when LVGLImage is created with empty data
|
|
logging.warning("Cannot adjust stride for empty image")
|
|
return
|
|
|
|
if align >= 1 and stride == 0:
|
|
# The header with specified stride alignment
|
|
header = LVGLImageHeader(self.cf, self.w, self.h, align=align)
|
|
stride = header.stride
|
|
elif stride > 0:
|
|
pass
|
|
else:
|
|
raise ParameterError(f"Invalid parameter, align:{align},"
|
|
f" stride:{stride}")
|
|
|
|
if self.stride == stride:
|
|
return # no stride adjustment
|
|
|
|
# if current image is empty, no need to do anything
|
|
if self.data_len == 0:
|
|
self.stride = 0
|
|
return
|
|
|
|
current = LVGLImageHeader(self.cf, self.w, self.h, stride=self.stride)
|
|
|
|
if stride < current.stride_default:
|
|
raise ParameterError(f"Stride is too small:{stride}, "
|
|
f"minimal:{current.stride_default}")
|
|
|
|
def change_stride(data: bytearray, h, current_stride, new_stride):
|
|
data_in = data
|
|
data_out = [] # stride adjusted new data
|
|
if new_stride < current_stride: # remove padding byte
|
|
for i in range(h):
|
|
start = i * current_stride
|
|
end = start + new_stride
|
|
data_out.append(data_in[start:end])
|
|
else: # adding more padding bytes
|
|
padding = b'\x00' * (new_stride - current_stride)
|
|
for i in range(h):
|
|
data_out.append(data_in[i * current_stride:(i + 1) *
|
|
current_stride])
|
|
data_out.append(padding)
|
|
return b''.join(data_out)
|
|
|
|
palette_size = self.cf.ncolors * 4
|
|
data_out = [self.data[:palette_size]]
|
|
data_out.append(
|
|
change_stride(self.data[palette_size:], self.h, current.stride,
|
|
stride))
|
|
|
|
# deal with alpha map for RGB565A8
|
|
if self.cf == ColorFormat.RGB565A8:
|
|
logging.warning("handle RGB565A8 alpha map")
|
|
a8_stride = self.stride // 2
|
|
a8_map = self.data[-a8_stride * self.h:]
|
|
data_out.append(
|
|
change_stride(a8_map, self.h, current.stride // 2,
|
|
stride // 2))
|
|
|
|
self.stride = stride
|
|
self.data = bytearray(b''.join(data_out))
|
|
|
|
def premultiply(self):
|
|
"""
|
|
Pre-multiply image RGB data with alpha, set corresponding image header flags
|
|
"""
|
|
if self.premultiplied:
|
|
raise ParameterError("Image already pre-multiplied")
|
|
|
|
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.premultiplied = True
|
|
|
|
@property
|
|
def data_len(self) -> int:
|
|
"""
|
|
Return data_len in byte of this image, excluding image header
|
|
"""
|
|
|
|
# palette is always in ARGB format, 4Byte per color
|
|
p = self.cf.ncolors * 4 if self.is_indexed and self.w * self.h else 0
|
|
p += self.stride * self.h
|
|
if self.cf is ColorFormat.RGB565A8:
|
|
a8_stride = self.stride // 2
|
|
p += a8_stride * self.h
|
|
return p
|
|
|
|
@property
|
|
def header(self) -> bytearray:
|
|
return LVGLImageHeader(self.cf, self.w, self.h)
|
|
|
|
@property
|
|
def is_indexed(self):
|
|
return self.cf.is_indexed
|
|
|
|
def set_data(self,
|
|
cf: ColorFormat,
|
|
w: int,
|
|
h: int,
|
|
data: bytes,
|
|
stride: int = 0):
|
|
"""
|
|
Directly set LVGL image parameters
|
|
"""
|
|
|
|
if w > 0xffff or h > 0xffff:
|
|
raise ParameterError(f"w, h overflow: {w}x{h}")
|
|
|
|
self.cf = cf
|
|
self.w = w
|
|
self.h = h
|
|
|
|
# if stride is 0, then it's aligned to 1byte by default,
|
|
# let image header handle it
|
|
self.stride = LVGLImageHeader(cf, w, h, stride, align=1).stride
|
|
|
|
if self.data_len != len(data):
|
|
raise ParameterError(f"{self} data length error got: {len(data)}, "
|
|
f"expect: {self.data_len}, {self}")
|
|
|
|
self.data = data
|
|
|
|
return self
|
|
|
|
def from_data(self, data: bytes):
|
|
header = LVGLImageHeader().from_binary(data)
|
|
return self.set_data(header.cf, header.w, header.h,
|
|
data[len(header.binary):], header.stride)
|
|
|
|
def from_bin(self, filename: str):
|
|
"""
|
|
Read from existing bin file and update image parameters
|
|
"""
|
|
|
|
if not filename.endswith(".bin"):
|
|
raise FormatError("filename not ended with '.bin'")
|
|
|
|
with open(filename, "rb") as f:
|
|
data = f.read()
|
|
return self.from_data(data)
|
|
|
|
def _check_ext(self, filename: str, ext):
|
|
if not filename.lower().endswith(ext):
|
|
raise FormatError(f"filename not ended with {ext}")
|
|
|
|
def _check_dir(self, filename: str):
|
|
dir = path.dirname(filename)
|
|
if dir and not path.exists(dir):
|
|
logging.info(f"mkdir of {dir} for {filename}")
|
|
os.makedirs(dir)
|
|
|
|
def to_bin(self,
|
|
filename: str,
|
|
compress: CompressMethod = CompressMethod.NONE):
|
|
"""
|
|
Write this image to file, filename should be ended with '.bin'
|
|
"""
|
|
self._check_ext(filename, ".bin")
|
|
self._check_dir(filename)
|
|
|
|
with open(filename, "wb+") as f:
|
|
bin = bytearray()
|
|
flags = 0
|
|
flags |= 0x08 if compress != CompressMethod.NONE else 0
|
|
flags |= 0x01 if self.premultiplied else 0
|
|
|
|
header = LVGLImageHeader(self.cf,
|
|
self.w,
|
|
self.h,
|
|
self.stride,
|
|
flags=flags)
|
|
bin += header.binary
|
|
compressed = LVGLCompressData(self.cf, compress, self.data)
|
|
bin += compressed.compressed
|
|
|
|
f.write(bin)
|
|
|
|
return self
|
|
|
|
def to_c_array(self,
|
|
filename: str,
|
|
compress: CompressMethod = CompressMethod.NONE):
|
|
self._check_ext(filename, ".c")
|
|
self._check_dir(filename)
|
|
|
|
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.premultiplied,
|
|
compress, data)
|
|
|
|
def to_png(self, filename: str):
|
|
self._check_ext(filename, ".png")
|
|
self._check_dir(filename)
|
|
|
|
old_stride = self.stride
|
|
self.adjust_stride(align=1)
|
|
if self.cf.is_indexed:
|
|
data = self.data
|
|
# Separate lvgl bin image data to palette and bitmap
|
|
# The palette is in format of [(RGBA), (RGBA)...].
|
|
# LVGL palette is in format of B,G,R,A,...
|
|
palette = [(data[i * 4 + 2], data[i * 4 + 1], data[i * 4 + 0],
|
|
data[i * 4 + 3]) for i in range(self.cf.ncolors)]
|
|
|
|
data = data[self.cf.ncolors * 4:]
|
|
|
|
encoder = png.Writer(self.w,
|
|
self.h,
|
|
palette=palette,
|
|
bitdepth=self.cf.bpp)
|
|
# separate packed data to plain data
|
|
data = unpack_colors(data, self.cf, self.w)
|
|
elif self.cf.is_alpha_only:
|
|
# separate packed data to plain data
|
|
transparency = unpack_colors(self.data, self.cf, self.w)
|
|
data = []
|
|
for a in transparency:
|
|
data += [0, 0, 0, a]
|
|
encoder = png.Writer(self.w, self.h, greyscale=False, alpha=True)
|
|
elif self.cf == ColorFormat.L8:
|
|
# to grayscale
|
|
encoder = png.Writer(self.w,
|
|
self.h,
|
|
bitdepth=self.cf.bpp,
|
|
greyscale=True,
|
|
alpha=False)
|
|
data = self.data
|
|
elif self.cf.is_colormap:
|
|
encoder = png.Writer(self.w,
|
|
self.h,
|
|
alpha=self.cf.has_alpha,
|
|
greyscale=False)
|
|
data = unpack_colors(self.data, self.cf, self.w)
|
|
else:
|
|
logging.warning(f"missing logic: {self.cf.name}")
|
|
return
|
|
|
|
with open(filename, "wb") as f:
|
|
encoder.write_array(f, data)
|
|
|
|
self.adjust_stride(stride=old_stride)
|
|
|
|
def from_png(self,
|
|
filename: str,
|
|
cf: ColorFormat = None,
|
|
background: int = 0x00_00_00):
|
|
"""
|
|
Create lvgl image from png file.
|
|
If cf is none, used I1/2/4/8 based on palette size
|
|
"""
|
|
|
|
self.background = background
|
|
|
|
if cf is None: # guess cf from filename
|
|
# split filename string and match with ColorFormat to check
|
|
# which cf to use
|
|
names = str(path.basename(filename)).split(".")
|
|
for c in names[1:-1]:
|
|
if c in ColorFormat.__members__:
|
|
cf = ColorFormat[c]
|
|
break
|
|
|
|
if cf is None or cf.is_indexed: # palette mode
|
|
self._png_to_indexed(cf, filename)
|
|
elif cf.is_alpha_only:
|
|
self._png_to_alpha_only(cf, filename)
|
|
elif cf.is_luma_only:
|
|
self._png_to_luma_only(cf, filename)
|
|
elif cf.is_colormap:
|
|
self._png_to_colormap(cf, filename)
|
|
else:
|
|
logging.warning(f"missing logic: {cf.name}")
|
|
|
|
logging.info(f"from png: {filename}, cf: {self.cf.name}")
|
|
return self
|
|
|
|
def _png_to_indexed(self, cf: ColorFormat, filename: str):
|
|
# convert to palette mode
|
|
auto_cf = cf is None
|
|
reader = png.Reader(
|
|
bytes=PngQuant(256 if auto_cf else cf.ncolors).convert(filename))
|
|
w, h, rows, _ = reader.read()
|
|
palette = reader.palette(alpha="force") # always return alpha
|
|
|
|
palette_len = len(palette)
|
|
if auto_cf:
|
|
if palette_len <= 2:
|
|
cf = ColorFormat.I1
|
|
elif palette_len <= 4:
|
|
cf = ColorFormat.I2
|
|
elif palette_len <= 16:
|
|
cf = ColorFormat.I4
|
|
else:
|
|
cf = ColorFormat.I8
|
|
|
|
if palette_len != cf.ncolors:
|
|
if not auto_cf:
|
|
logging.warning(
|
|
f"{path.basename(filename)} palette: {palette_len}, "
|
|
f"extended to: {cf.ncolors}")
|
|
palette += [(255, 255, 255, 0)] * (cf.ncolors - palette_len)
|
|
|
|
# Assemble lvgl image palette from PNG palette.
|
|
# PNG palette is a list of tuple(R,G,B,A)
|
|
|
|
rawdata = bytearray()
|
|
for (r, g, b, a) in palette:
|
|
rawdata += uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0))
|
|
|
|
# pack data if not in I8 format
|
|
if cf == ColorFormat.I8:
|
|
for e in rows:
|
|
rawdata += e
|
|
else:
|
|
for e in png.pack_rows(rows, cf.bpp):
|
|
rawdata += e
|
|
|
|
self.set_data(cf, w, h, rawdata)
|
|
|
|
def _png_to_alpha_only(self, cf: ColorFormat, filename: str):
|
|
reader = png.Reader(str(filename))
|
|
w, h, rows, info = reader.asRGBA8()
|
|
if not info['alpha']:
|
|
raise FormatError(f"{filename} has no alpha channel")
|
|
|
|
rawdata = bytearray()
|
|
if cf == ColorFormat.A8:
|
|
for row in rows:
|
|
A = row[3::4]
|
|
for e in A:
|
|
rawdata += uint8_t(e)
|
|
else:
|
|
shift = 8 - cf.bpp
|
|
mask = 2**cf.bpp - 1
|
|
rows = [[(a >> shift) & mask for a in row[3::4]] for row in rows]
|
|
for row in png.pack_rows(rows, cf.bpp):
|
|
rawdata += row
|
|
|
|
self.set_data(cf, w, h, rawdata)
|
|
|
|
def sRGB_to_linear(self, x):
|
|
if x < 0.04045:
|
|
return x / 12.92
|
|
return pow((x + 0.055) / 1.055, 2.4)
|
|
|
|
def linear_to_sRGB(self, y):
|
|
if y <= 0.0031308:
|
|
return 12.92 * y
|
|
return 1.055 * pow(y, 1 / 2.4) - 0.055
|
|
|
|
def _png_to_luma_only(self, cf: ColorFormat, filename: str):
|
|
reader = png.Reader(str(filename))
|
|
w, h, rows, info = reader.asRGBA8()
|
|
rawdata = bytearray()
|
|
for row in rows:
|
|
R = row[0::4]
|
|
G = row[1::4]
|
|
B = row[2::4]
|
|
A = row[3::4]
|
|
for r, g, b, a in zip(R, G, B, A):
|
|
r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
|
|
r = self.sRGB_to_linear(r / 255.0)
|
|
g = self.sRGB_to_linear(g / 255.0)
|
|
b = self.sRGB_to_linear(b / 255.0)
|
|
luma = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
rawdata += uint8_t(int(self.linear_to_sRGB(luma) * 255))
|
|
|
|
self.set_data(ColorFormat.L8, w, h, rawdata)
|
|
|
|
def _png_to_colormap(self, cf, filename: str):
|
|
|
|
if cf == ColorFormat.ARGB8888:
|
|
|
|
def pack(r, g, b, a):
|
|
return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0))
|
|
elif cf == ColorFormat.XRGB8888:
|
|
|
|
def pack(r, g, b, a):
|
|
r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
|
|
return uint32_t((0xff << 24) | (r << 16) | (g << 8) | (b << 0))
|
|
elif cf == ColorFormat.RGB888:
|
|
|
|
def pack(r, g, b, a):
|
|
r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
|
|
return uint24_t((r << 16) | (g << 8) | (b << 0))
|
|
elif cf == ColorFormat.RGB565:
|
|
|
|
def pack(r, g, b, a):
|
|
r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
|
|
color = (r >> 3) << 11
|
|
color |= (g >> 2) << 5
|
|
color |= (b >> 3) << 0
|
|
return uint16_t(color)
|
|
elif cf == ColorFormat.RGB565A8:
|
|
|
|
def pack(r, g, b, a):
|
|
color = (r >> 3) << 11
|
|
color |= (g >> 2) << 5
|
|
color |= (b >> 3) << 0
|
|
return uint16_t(color)
|
|
elif cf == ColorFormat.ARGB8565:
|
|
|
|
def pack(r, g, b, a):
|
|
color = (r >> 3) << 11
|
|
color |= (g >> 2) << 5
|
|
color |= (b >> 3) << 0
|
|
return uint24_t((a << 16) | color)
|
|
else:
|
|
raise FormatError(f"Invalid color format: {cf.name}")
|
|
|
|
reader = png.Reader(str(filename))
|
|
w, h, rows, _ = reader.asRGBA8()
|
|
rawdata = bytearray()
|
|
alpha = bytearray()
|
|
for row in rows:
|
|
R = row[0::4]
|
|
G = row[1::4]
|
|
B = row[2::4]
|
|
A = row[3::4]
|
|
for r, g, b, a in zip(R, G, B, A):
|
|
if cf == ColorFormat.RGB565A8:
|
|
alpha += uint8_t(a)
|
|
rawdata += pack(r, g, b, a)
|
|
|
|
if cf == ColorFormat.RGB565A8:
|
|
rawdata += alpha
|
|
|
|
self.set_data(cf, w, h, rawdata)
|
|
|
|
|
|
class RLEHeader:
|
|
|
|
def __init__(self, blksize: int, len: int):
|
|
self.blksize = blksize
|
|
self.len = len
|
|
|
|
@property
|
|
def binary(self):
|
|
magic = 0x5aa521e0
|
|
|
|
rle_header = self.blksize
|
|
rle_header |= (self.len & 0xffffff) << 4
|
|
|
|
binary = bytearray()
|
|
binary.extend(uint32_t(magic))
|
|
binary.extend(uint32_t(rle_header))
|
|
return binary
|
|
|
|
|
|
class RLEImage(LVGLImage):
|
|
|
|
def __init__(self,
|
|
cf: ColorFormat = ColorFormat.UNKNOWN,
|
|
w: int = 0,
|
|
h: int = 0,
|
|
data: bytes = b'') -> None:
|
|
super().__init__(cf, w, h, data)
|
|
|
|
def to_rle(self, filename: str):
|
|
"""
|
|
Compress this image to file, filename should be ended with '.rle'
|
|
"""
|
|
self._check_ext(filename, ".rle")
|
|
self._check_dir(filename)
|
|
|
|
# compress image data excluding lvgl image header
|
|
blksize = (self.cf.bpp + 7) // 8
|
|
compressed = self.rle_compress(self.data, blksize)
|
|
with open(filename, "wb+") as f:
|
|
header = RLEHeader(blksize, len(self.data)).binary
|
|
header.extend(self.header.binary)
|
|
f.write(header)
|
|
f.write(compressed)
|
|
|
|
def rle_compress(self, data: bytearray, blksize: int, threshold=16):
|
|
index = 0
|
|
data_len = len(data)
|
|
compressed_data = []
|
|
memview = memoryview(data)
|
|
while index < data_len:
|
|
repeat_cnt = self.get_repeat_count(memview[index:], blksize)
|
|
if repeat_cnt == 0:
|
|
# done
|
|
break
|
|
elif repeat_cnt < threshold:
|
|
nonrepeat_cnt = self.get_nonrepeat_count(
|
|
memview[index:], blksize, threshold)
|
|
ctrl_byte = uint8_t(nonrepeat_cnt | 0x80)
|
|
compressed_data.append(ctrl_byte)
|
|
compressed_data.append(memview[index:index +
|
|
nonrepeat_cnt * blksize])
|
|
index += nonrepeat_cnt * blksize
|
|
else:
|
|
ctrl_byte = uint8_t(repeat_cnt)
|
|
compressed_data.append(ctrl_byte)
|
|
compressed_data.append(memview[index:index + blksize])
|
|
index += repeat_cnt * blksize
|
|
|
|
return b"".join(compressed_data)
|
|
|
|
def get_repeat_count(self, data: bytearray, blksize: int):
|
|
if len(data) < blksize:
|
|
return 0
|
|
|
|
start = data[:blksize]
|
|
index = 0
|
|
repeat_cnt = 0
|
|
value = 0
|
|
|
|
while index < len(data):
|
|
value = data[index:index + blksize]
|
|
|
|
if value == start:
|
|
repeat_cnt += 1
|
|
if repeat_cnt == 127: # limit max repeat count to max value of signed char.
|
|
break
|
|
else:
|
|
break
|
|
index += blksize
|
|
|
|
return repeat_cnt
|
|
|
|
def get_nonrepeat_count(self, data: bytearray, blksize: int, threshold):
|
|
if len(data) < blksize:
|
|
return 0
|
|
|
|
pre_value = data[:blksize]
|
|
|
|
index = 0
|
|
nonrepeat_count = 0
|
|
|
|
repeat_cnt = 0
|
|
while True:
|
|
value = data[index:index + blksize]
|
|
if value == pre_value:
|
|
repeat_cnt += 1
|
|
if repeat_cnt > threshold:
|
|
# repeat found.
|
|
break
|
|
else:
|
|
pre_value = value
|
|
nonrepeat_count += 1 + repeat_cnt
|
|
repeat_cnt = 0
|
|
if nonrepeat_count >= 127: # limit max repeat count to max value of signed char.
|
|
nonrepeat_count = 127
|
|
break
|
|
|
|
index += blksize # move to next position
|
|
if index >= len(data): # data end
|
|
nonrepeat_count += repeat_cnt
|
|
break
|
|
|
|
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"
|
|
PNG_FILE = "PNG" # convert to lvgl image and then to png
|
|
|
|
|
|
class PNGConverter:
|
|
|
|
def __init__(self,
|
|
files: List,
|
|
cf: ColorFormat,
|
|
ofmt: OutputFormat,
|
|
odir: str,
|
|
background: int = 0x00,
|
|
align: int = 1,
|
|
premultiply: bool = False,
|
|
compress: CompressMethod = CompressMethod.NONE,
|
|
keep_folder=True) -> None:
|
|
self.files = files
|
|
self.cf = cf
|
|
self.ofmt = ofmt
|
|
self.output = odir
|
|
self.pngquant = None
|
|
self.keep_folder = keep_folder
|
|
self.align = align
|
|
self.premultiply = premultiply
|
|
self.compress = compress
|
|
self.background = background
|
|
|
|
def _replace_ext(self, input, ext):
|
|
if self.keep_folder:
|
|
name, _ = path.splitext(input)
|
|
else:
|
|
name, _ = path.splitext(path.basename(input))
|
|
output = name + ext
|
|
output = path.join(self.output, output)
|
|
return output
|
|
|
|
def convert(self):
|
|
output = []
|
|
for f in self.files:
|
|
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.premultiply()
|
|
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"),
|
|
compress=self.compress)
|
|
elif self.ofmt == OutputFormat.PNG_FILE:
|
|
img.to_png(self._replace_ext(f, ".png"))
|
|
|
|
return output
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='LVGL PNG to bin image tool.')
|
|
parser.add_argument('--ofmt',
|
|
help="output filename format, C or BIN",
|
|
default="BIN",
|
|
choices=["C", "BIN", "PNG"])
|
|
parser.add_argument(
|
|
'--cf',
|
|
help=("bin image color format, use AUTO for automatically "
|
|
"choose from I1/2/4/8"),
|
|
default="I8",
|
|
choices=[
|
|
"L8", "I1", "I2", "I4", "I8", "A1", "A2", "A4", "A8", "ARGB8888",
|
|
"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",
|
|
choices=["NONE", "RLE", "LZ4"])
|
|
|
|
parser.add_argument('--align',
|
|
help="stride alignment in bytes for bin image",
|
|
default=1,
|
|
type=int,
|
|
metavar='byte',
|
|
nargs='?')
|
|
parser.add_argument('--background',
|
|
help="Background color for formats without alpha",
|
|
default=0x00_00_00,
|
|
type=lambda x: int(x, 0),
|
|
metavar='color',
|
|
nargs='?')
|
|
parser.add_argument('-o',
|
|
'--output',
|
|
default="./output",
|
|
help="Select the output folder, default to ./output")
|
|
parser.add_argument('-v', '--verbose', action='store_true')
|
|
parser.add_argument(
|
|
'input', help="the filename or folder to be recursively converted")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if path.isfile(args.input):
|
|
files = [args.input]
|
|
elif path.isdir(args.input):
|
|
files = list(Path(args.input).rglob("*.[pP][nN][gG]"))
|
|
else:
|
|
raise BaseException(f"invalid input: {args.input}")
|
|
|
|
if args.verbose:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
logging.info(f"options: {args.__dict__}, files:{[str(f) for f in files]}")
|
|
|
|
if args.cf == "AUTO":
|
|
cf = None
|
|
else:
|
|
cf = ColorFormat[args.cf]
|
|
|
|
ofmt = OutputFormat(args.ofmt) if cf not in (
|
|
ColorFormat.RAW, ColorFormat.RAW_ALPHA) else OutputFormat.C_ARRAY
|
|
compress = CompressMethod[args.compress]
|
|
|
|
converter = PNGConverter(files,
|
|
cf,
|
|
ofmt,
|
|
args.output,
|
|
background=args.background,
|
|
align=args.align,
|
|
premultiply=args.premultiply,
|
|
compress=compress,
|
|
keep_folder=False)
|
|
output = converter.convert()
|
|
for f, img in output:
|
|
logging.info(f"len: {img.data_len} for {path.basename(f)} ")
|
|
|
|
print(f"done {len(files)} files")
|
|
|
|
|
|
def test():
|
|
logging.basicConfig(level=logging.INFO)
|
|
f = "pngs/cogwheel.RGB565A8.png"
|
|
img = LVGLImage().from_png(f,
|
|
cf=ColorFormat.ARGB8565,
|
|
background=0xFF_FF_00)
|
|
img.adjust_stride(align=16)
|
|
img.premultiply()
|
|
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()
|