LVGLImage.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426
  1. #!/usr/bin/env python3
  2. import os
  3. import logging
  4. import argparse
  5. import subprocess
  6. from os import path
  7. from enum import Enum
  8. from typing import List
  9. from pathlib import Path
  10. try:
  11. import png
  12. except ImportError:
  13. raise ImportError("Need pypng package, do `pip3 install pypng`")
  14. try:
  15. import lz4.block
  16. except ImportError:
  17. raise ImportError("Need lz4 package, do `pip3 install lz4`")
  18. def uint8_t(val) -> bytes:
  19. return val.to_bytes(1, byteorder='little')
  20. def uint16_t(val) -> bytes:
  21. return val.to_bytes(2, byteorder='little')
  22. def uint24_t(val) -> bytes:
  23. return val.to_bytes(3, byteorder='little')
  24. def uint32_t(val) -> bytes:
  25. try:
  26. return val.to_bytes(4, byteorder='little')
  27. except OverflowError:
  28. raise ParameterError(f"overflow: {hex(val)}")
  29. def color_pre_multiply(r, g, b, a, background):
  30. bb = background & 0xff
  31. bg = (background >> 8) & 0xff
  32. br = (background >> 16) & 0xff
  33. return ((r * a + (255 - a) * br) >> 8, (g * a + (255 - a) * bg) >> 8,
  34. (b * a + (255 - a) * bb) >> 8, a)
  35. class Error(Exception):
  36. def __str__(self):
  37. return self.__class__.__name__ + ': ' + ' '.join(self.args)
  38. class FormatError(Error):
  39. """
  40. Problem with input filename format.
  41. BIN filename does not conform to standard lvgl bin image format
  42. """
  43. class ParameterError(Error):
  44. """
  45. Parameter for LVGL image not correct
  46. """
  47. class PngQuant:
  48. """
  49. Compress PNG file to 8bit mode using `pngquant`
  50. """
  51. def __init__(self, ncolors=256, dither=True, exec_path="") -> None:
  52. executable = path.join(exec_path, "pngquant")
  53. self.cmd = (f"{executable} {'--nofs' if not dither else ''} "
  54. f"{ncolors} --force - < ")
  55. def convert(self, filename) -> bytes:
  56. if not os.path.isfile(filename):
  57. raise BaseException(f"file not found: {filename}")
  58. try:
  59. compressed = subprocess.check_output(
  60. f'{self.cmd} "{str(filename)}"',
  61. stderr=subprocess.STDOUT,
  62. shell=True)
  63. except subprocess.CalledProcessError:
  64. raise BaseException(
  65. "cannot find pngquant tool, install it via "
  66. "`sudo apt install pngquant` for debian "
  67. "or `brew install pngquant` for macintosh "
  68. "For windows, you may need to download pngquant.exe from "
  69. "https://pngquant.org/, and put it in your PATH.")
  70. return compressed
  71. class CompressMethod(Enum):
  72. NONE = 0x00
  73. RLE = 0x01
  74. LZ4 = 0x02
  75. class ColorFormat(Enum):
  76. UNKNOWN = 0x00
  77. RAW = 0x01,
  78. RAW_ALPHA = 0x02,
  79. L8 = 0x06
  80. I1 = 0x07
  81. I2 = 0x08
  82. I4 = 0x09
  83. I8 = 0x0A
  84. A1 = 0x0B
  85. A2 = 0x0C
  86. A4 = 0x0D
  87. A8 = 0x0E
  88. ARGB8888 = 0x10
  89. XRGB8888 = 0x11
  90. RGB565 = 0x12
  91. ARGB8565 = 0x13
  92. RGB565A8 = 0x14
  93. RGB888 = 0x0F
  94. @property
  95. def bpp(self) -> int:
  96. """
  97. Return bit per pixel for this cf
  98. """
  99. cf_map = {
  100. ColorFormat.L8: 8,
  101. ColorFormat.I1: 1,
  102. ColorFormat.I2: 2,
  103. ColorFormat.I4: 4,
  104. ColorFormat.I8: 8,
  105. ColorFormat.A1: 1,
  106. ColorFormat.A2: 2,
  107. ColorFormat.A4: 4,
  108. ColorFormat.A8: 8,
  109. ColorFormat.ARGB8888: 32,
  110. ColorFormat.XRGB8888: 32,
  111. ColorFormat.RGB565: 16,
  112. ColorFormat.RGB565A8: 16, # 16bpp + a8 map
  113. ColorFormat.ARGB8565: 24,
  114. ColorFormat.RGB888: 24,
  115. }
  116. return cf_map[self] if self in cf_map else 0
  117. @property
  118. def ncolors(self) -> int:
  119. """
  120. Return number of colors in palette if cf is indexed1/2/4/8.
  121. Return zero if cf is not indexed format
  122. """
  123. cf_map = {
  124. ColorFormat.I1: 2,
  125. ColorFormat.I2: 4,
  126. ColorFormat.I4: 16,
  127. ColorFormat.I8: 256,
  128. }
  129. return cf_map.get(self, 0)
  130. @property
  131. def is_indexed(self) -> bool:
  132. """
  133. Return if cf is indexed color format
  134. """
  135. return self.ncolors != 0
  136. @property
  137. def is_alpha_only(self) -> bool:
  138. return ColorFormat.A1.value <= self.value <= ColorFormat.A8.value
  139. @property
  140. def has_alpha(self) -> bool:
  141. return self.is_alpha_only or self.is_indexed or self in (
  142. ColorFormat.ARGB8888,
  143. ColorFormat.XRGB8888, # const alpha: 0xff
  144. ColorFormat.ARGB8565,
  145. ColorFormat.RGB565A8)
  146. @property
  147. def is_colormap(self) -> bool:
  148. return self in (ColorFormat.ARGB8888, ColorFormat.RGB888,
  149. ColorFormat.XRGB8888, ColorFormat.RGB565A8,
  150. ColorFormat.ARGB8565, ColorFormat.RGB565)
  151. @property
  152. def is_luma_only(self) -> bool:
  153. return self in (ColorFormat.L8, )
  154. def bit_extend(value, bpp):
  155. """
  156. Extend value from bpp to 8 bit with interpolation to reduce rounding error.
  157. """
  158. if value == 0:
  159. return 0
  160. res = value
  161. bpp_now = bpp
  162. while bpp_now < 8:
  163. res |= value << (8 - bpp_now)
  164. bpp_now += bpp
  165. return res
  166. def unpack_colors(data: bytes, cf: ColorFormat, w) -> List:
  167. """
  168. Unpack lvgl 1/2/4/8/16/32 bpp color to png color: alpha map, grey scale,
  169. or R,G,B,(A) map
  170. """
  171. ret = []
  172. bpp = cf.bpp
  173. if bpp == 8:
  174. ret = data
  175. elif bpp == 4:
  176. if cf == ColorFormat.A4:
  177. values = [x * 17 for x in range(16)]
  178. else:
  179. values = [x for x in range(16)]
  180. for p in data:
  181. for i in range(2):
  182. ret.append(values[(p >> (4 - i * 4)) & 0x0f])
  183. if len(ret) % w == 0:
  184. break
  185. elif bpp == 2:
  186. if cf == ColorFormat.A2:
  187. values = [x * 85 for x in range(4)]
  188. else: # must be ColorFormat.I2
  189. values = [x for x in range(4)]
  190. for p in data:
  191. for i in range(4):
  192. ret.append(values[(p >> (6 - i * 2)) & 0x03])
  193. if len(ret) % w == 0:
  194. break
  195. elif bpp == 1:
  196. if cf == ColorFormat.A1:
  197. values = [0, 255]
  198. else:
  199. values = [0, 1]
  200. for p in data:
  201. for i in range(8):
  202. ret.append(values[(p >> (7 - i)) & 0x01])
  203. if len(ret) % w == 0:
  204. break
  205. elif bpp == 16:
  206. # This is RGB565
  207. pixels = [(data[2 * i + 1] << 8) | data[2 * i]
  208. for i in range(len(data) // 2)]
  209. for p in pixels:
  210. ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
  211. ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
  212. ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
  213. elif bpp == 24:
  214. if cf == ColorFormat.RGB888:
  215. B = data[0::3]
  216. G = data[1::3]
  217. R = data[2::3]
  218. for r, g, b in zip(R, G, B):
  219. ret += [r, g, b]
  220. elif cf == ColorFormat.RGB565A8:
  221. alpha_size = len(data) // 3
  222. pixel_alpha = data[-alpha_size:]
  223. pixel_data = data[:-alpha_size]
  224. pixels = [(pixel_data[2 * i + 1] << 8) | pixel_data[2 * i]
  225. for i in range(len(pixel_data) // 2)]
  226. for a, p in zip(pixel_alpha, pixels):
  227. ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
  228. ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
  229. ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
  230. ret.append(a)
  231. elif cf == ColorFormat.ARGB8565:
  232. L = data[0::3]
  233. H = data[1::3]
  234. A = data[2::3]
  235. for h, l, a in zip(H, L, A):
  236. p = (h << 8) | (l)
  237. ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R
  238. ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G
  239. ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B
  240. ret.append(a) # A
  241. elif bpp == 32:
  242. B = data[0::4]
  243. G = data[1::4]
  244. R = data[2::4]
  245. A = data[3::4]
  246. for r, g, b, a in zip(R, G, B, A):
  247. ret += [r, g, b, a]
  248. else:
  249. assert 0
  250. return ret
  251. def write_c_array_file(
  252. w: int, h: int,
  253. stride: int,
  254. cf: ColorFormat,
  255. filename: str,
  256. premultiplied: bool,
  257. compress: CompressMethod,
  258. data: bytes):
  259. varname = path.basename(filename).split('.')[0]
  260. varname = varname.replace("-", "_")
  261. varname = varname.replace(".", "_")
  262. flags = "0"
  263. if compress is not CompressMethod.NONE:
  264. flags += " | LV_IMAGE_FLAGS_COMPRESSED"
  265. if premultiplied:
  266. flags += " | LV_IMAGE_FLAGS_PREMULTIPLIED"
  267. macro = "LV_ATTRIBUTE_" + varname.upper()
  268. header = f'''
  269. #if defined(LV_LVGL_H_INCLUDE_SIMPLE)
  270. #include "lvgl.h"
  271. #elif defined(LV_BUILD_TEST)
  272. #include "../lvgl.h"
  273. #else
  274. #include "lvgl/lvgl.h"
  275. #endif
  276. #ifndef LV_ATTRIBUTE_MEM_ALIGN
  277. #define LV_ATTRIBUTE_MEM_ALIGN
  278. #endif
  279. #ifndef {macro}
  280. #define {macro}
  281. #endif
  282. static const
  283. LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST {macro}
  284. uint8_t {varname}_map[] = {{
  285. '''
  286. ending = f'''
  287. }};
  288. const lv_image_dsc_t {varname} = {{
  289. .header.magic = LV_IMAGE_HEADER_MAGIC,
  290. .header.cf = LV_COLOR_FORMAT_{cf.name},
  291. .header.flags = {flags},
  292. .header.w = {w},
  293. .header.h = {h},
  294. .header.stride = {stride},
  295. .data_size = sizeof({varname}_map),
  296. .data = {varname}_map,
  297. }};
  298. '''
  299. def write_binary(f, data, stride):
  300. stride = 16 if stride == 0 else stride
  301. for i, v in enumerate(data):
  302. if i % stride == 0:
  303. f.write("\n ")
  304. f.write(f"0x{v:02x},")
  305. f.write("\n")
  306. with open(filename, "w+") as f:
  307. f.write(header)
  308. if compress != CompressMethod.NONE:
  309. write_binary(f, data, 16)
  310. else:
  311. # write palette separately
  312. ncolors = cf.ncolors
  313. if ncolors:
  314. write_binary(f, data[:ncolors * 4], 16)
  315. write_binary(f, data[ncolors * 4:], stride)
  316. f.write(ending)
  317. class LVGLImageHeader:
  318. def __init__(self,
  319. cf: ColorFormat = ColorFormat.UNKNOWN,
  320. w: int = 0,
  321. h: int = 0,
  322. stride: int = 0,
  323. align: int = 1,
  324. flags: int = 0):
  325. self.cf = cf
  326. self.flags = flags
  327. self.w = w & 0xffff
  328. self.h = h & 0xffff
  329. if w > 0xffff or h > 0xffff:
  330. raise ParameterError(f"w, h overflow: {w}x{h}")
  331. if align < 1:
  332. # stride align in bytes must be larger than 1
  333. raise ParameterError(f"Invalid stride align: {align}")
  334. self.stride = self.stride_align(align) if stride == 0 else stride
  335. def stride_align(self, align: int) -> int:
  336. stride = self.stride_default
  337. if align == 1:
  338. pass
  339. elif align > 1:
  340. stride = (stride + align - 1) // align
  341. stride *= align
  342. else:
  343. raise ParameterError(f"Invalid stride align: {align}")
  344. self.stride = stride
  345. return stride
  346. @property
  347. def stride_default(self) -> int:
  348. return (self.w * self.cf.bpp + 7) // 8
  349. @property
  350. def binary(self) -> bytearray:
  351. binary = bytearray()
  352. binary += uint8_t(0x19) # magic number for lvgl version 9
  353. binary += uint8_t(self.cf.value)
  354. binary += uint16_t(self.flags) # 16bits flags
  355. binary += uint16_t(self.w) # 16bits width
  356. binary += uint16_t(self.h) # 16bits height
  357. binary += uint16_t(self.stride) # 16bits stride
  358. binary += uint16_t(0) # 16bits reserved
  359. return binary
  360. def from_binary(self, data: bytes):
  361. if len(data) < 12:
  362. raise FormatError("invalid header length")
  363. try:
  364. self.cf = ColorFormat(data[1] & 0x1f) # color format
  365. except ValueError as exc:
  366. raise FormatError(f"invalid color format: {hex(data[0])}") from exc
  367. self.w = int.from_bytes(data[4:6], 'little')
  368. self.h = int.from_bytes(data[6:8], 'little')
  369. self.stride = int.from_bytes(data[8:10], 'little')
  370. return self
  371. class LVGLCompressData:
  372. def __init__(self,
  373. cf: ColorFormat,
  374. method: CompressMethod,
  375. raw_data: bytes = b''):
  376. self.blk_size = (cf.bpp + 7) // 8
  377. self.compress = method
  378. self.raw_data = raw_data
  379. self.raw_data_len = len(raw_data)
  380. self.compressed = self._compress(raw_data)
  381. def _compress(self, raw_data: bytes) -> bytearray:
  382. if self.compress == CompressMethod.NONE:
  383. return raw_data
  384. if self.compress == CompressMethod.RLE:
  385. # RLE compression performs on pixel unit, pad data to pixel unit
  386. pad = b'\x00' * 0
  387. if self.raw_data_len % self.blk_size:
  388. pad = b'\x00' * (self.blk_size - self.raw_data_len % self.blk_size)
  389. compressed = RLEImage().rle_compress(raw_data + pad, self.blk_size)
  390. elif self.compress == CompressMethod.LZ4:
  391. compressed = lz4.block.compress(raw_data, store_size=False)
  392. else:
  393. raise ParameterError(f"Invalid compress method: {self.compress}")
  394. self.compressed_len = len(compressed)
  395. bin = bytearray()
  396. bin += uint32_t(self.compress.value)
  397. bin += uint32_t(self.compressed_len)
  398. bin += uint32_t(self.raw_data_len)
  399. bin += compressed
  400. return bin
  401. class LVGLImage:
  402. def __init__(self,
  403. cf: ColorFormat = ColorFormat.UNKNOWN,
  404. w: int = 0,
  405. h: int = 0,
  406. data: bytes = b'') -> None:
  407. self.stride = 0 # default no valid stride value
  408. self.premultiplied = False
  409. self.rgb565_dither = False
  410. self.set_data(cf, w, h, data)
  411. def __repr__(self) -> str:
  412. return (f"'LVGL image {self.w}x{self.h}, {self.cf.name}, "
  413. f"{'Pre-multiplied, ' if self.premultiplied else ''}"
  414. f"stride: {self.stride} "
  415. f"(12+{self.data_len})Byte'")
  416. def adjust_stride(self, stride: int = 0, align: int = 1):
  417. """
  418. Stride can be set directly, or by stride alignment in bytes
  419. """
  420. if self.stride == 0:
  421. # stride can only be 0, when LVGLImage is created with empty data
  422. logging.warning("Cannot adjust stride for empty image")
  423. return
  424. if align >= 1 and stride == 0:
  425. # The header with specified stride alignment
  426. header = LVGLImageHeader(self.cf, self.w, self.h, align=align)
  427. stride = header.stride
  428. elif stride > 0:
  429. pass
  430. else:
  431. raise ParameterError(f"Invalid parameter, align:{align},"
  432. f" stride:{stride}")
  433. if self.stride == stride:
  434. return # no stride adjustment
  435. # if current image is empty, no need to do anything
  436. if self.data_len == 0:
  437. self.stride = 0
  438. return
  439. current = LVGLImageHeader(self.cf, self.w, self.h, stride=self.stride)
  440. if stride < current.stride_default:
  441. raise ParameterError(f"Stride is too small:{stride}, "
  442. f"minimal:{current.stride_default}")
  443. def change_stride(data: bytearray, h, current_stride, new_stride):
  444. data_in = data
  445. data_out = [] # stride adjusted new data
  446. if new_stride < current_stride: # remove padding byte
  447. for i in range(h):
  448. start = i * current_stride
  449. end = start + new_stride
  450. data_out.append(data_in[start:end])
  451. else: # adding more padding bytes
  452. padding = b'\x00' * (new_stride - current_stride)
  453. for i in range(h):
  454. data_out.append(data_in[i * current_stride:(i + 1) *
  455. current_stride])
  456. data_out.append(padding)
  457. return b''.join(data_out)
  458. palette_size = self.cf.ncolors * 4
  459. data_out = [self.data[:palette_size]]
  460. data_out.append(
  461. change_stride(self.data[palette_size:], self.h, current.stride,
  462. stride))
  463. # deal with alpha map for RGB565A8
  464. if self.cf == ColorFormat.RGB565A8:
  465. logging.warning("handle RGB565A8 alpha map")
  466. a8_stride = self.stride // 2
  467. a8_map = self.data[-a8_stride * self.h:]
  468. data_out.append(
  469. change_stride(a8_map, self.h, current.stride // 2,
  470. stride // 2))
  471. self.stride = stride
  472. self.data = bytearray(b''.join(data_out))
  473. def premultiply(self):
  474. """
  475. Pre-multiply image RGB data with alpha, set corresponding image header flags
  476. """
  477. if self.premultiplied:
  478. raise ParameterError("Image already pre-multiplied")
  479. if not self.cf.has_alpha:
  480. raise ParameterError(f"Image has no alpha channel: {self.cf.name}")
  481. if self.cf.is_indexed:
  482. def multiply(r, g, b, a):
  483. r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8
  484. return uint8_t(b) + uint8_t(g) + uint8_t(r) + uint8_t(a)
  485. # process the palette only.
  486. palette_size = self.cf.ncolors * 4
  487. palette = self.data[:palette_size]
  488. palette = [
  489. multiply(palette[i], palette[i + 1], palette[i + 2],
  490. palette[i + 3]) for i in range(0, len(palette), 4)
  491. ]
  492. palette = b''.join(palette)
  493. self.data = palette + self.data[palette_size:]
  494. elif self.cf is ColorFormat.ARGB8888:
  495. def multiply(b, g, r, a):
  496. r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8
  497. return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0))
  498. line_width = self.w * 4
  499. for h in range(self.h):
  500. offset = h * self.stride
  501. map = self.data[offset:offset + self.stride]
  502. processed = b''.join([
  503. multiply(map[i], map[i + 1], map[i + 2], map[i + 3])
  504. for i in range(0, line_width, 4)
  505. ])
  506. self.data[offset:offset + line_width] = processed
  507. elif self.cf is ColorFormat.RGB565A8:
  508. def multiply(data, a):
  509. r = (data >> 11) & 0x1f
  510. g = (data >> 5) & 0x3f
  511. b = (data >> 0) & 0x1f
  512. r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255
  513. return uint16_t((r << 11) | (g << 5) | (b << 0))
  514. line_width = self.w * 2
  515. for h in range(self.h):
  516. # alpha map offset for this line
  517. offset = self.h * self.stride + h * (self.stride // 2)
  518. a = self.data[offset:offset + self.stride // 2]
  519. # RGB map offset
  520. offset = h * self.stride
  521. rgb = self.data[offset:offset + self.stride]
  522. processed = b''.join([
  523. multiply((rgb[i + 1] << 8) | rgb[i], a[i // 2])
  524. for i in range(0, line_width, 2)
  525. ])
  526. self.data[offset:offset + line_width] = processed
  527. elif self.cf is ColorFormat.ARGB8565:
  528. def multiply(data, a):
  529. r = (data >> 11) & 0x1f
  530. g = (data >> 5) & 0x3f
  531. b = (data >> 0) & 0x1f
  532. r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255
  533. return uint24_t((a << 16) | (r << 11) | (g << 5) | (b << 0))
  534. line_width = self.w * 3
  535. for h in range(self.h):
  536. offset = h * self.stride
  537. map = self.data[offset:offset + self.stride]
  538. processed = b''.join([
  539. multiply((map[i + 1] << 8) | map[i], map[i + 2])
  540. for i in range(0, line_width, 3)
  541. ])
  542. self.data[offset:offset + line_width] = processed
  543. else:
  544. raise ParameterError(f"Not supported yet: {self.cf.name}")
  545. self.premultiplied = True
  546. @property
  547. def data_len(self) -> int:
  548. """
  549. Return data_len in byte of this image, excluding image header
  550. """
  551. # palette is always in ARGB format, 4Byte per color
  552. p = self.cf.ncolors * 4 if self.is_indexed and self.w * self.h else 0
  553. p += self.stride * self.h
  554. if self.cf is ColorFormat.RGB565A8:
  555. a8_stride = self.stride // 2
  556. p += a8_stride * self.h
  557. return p
  558. @property
  559. def header(self) -> bytearray:
  560. return LVGLImageHeader(self.cf, self.w, self.h)
  561. @property
  562. def is_indexed(self):
  563. return self.cf.is_indexed
  564. def set_data(self,
  565. cf: ColorFormat,
  566. w: int,
  567. h: int,
  568. data: bytes,
  569. stride: int = 0):
  570. """
  571. Directly set LVGL image parameters
  572. """
  573. if w > 0xffff or h > 0xffff:
  574. raise ParameterError(f"w, h overflow: {w}x{h}")
  575. self.cf = cf
  576. self.w = w
  577. self.h = h
  578. # if stride is 0, then it's aligned to 1byte by default,
  579. # let image header handle it
  580. self.stride = LVGLImageHeader(cf, w, h, stride, align=1).stride
  581. if self.data_len != len(data):
  582. raise ParameterError(f"{self} data length error got: {len(data)}, "
  583. f"expect: {self.data_len}, {self}")
  584. self.data = data
  585. return self
  586. def from_data(self, data: bytes):
  587. header = LVGLImageHeader().from_binary(data)
  588. return self.set_data(header.cf, header.w, header.h,
  589. data[len(header.binary):], header.stride)
  590. def from_bin(self, filename: str):
  591. """
  592. Read from existing bin file and update image parameters
  593. """
  594. if not filename.endswith(".bin"):
  595. raise FormatError("filename not ended with '.bin'")
  596. with open(filename, "rb") as f:
  597. data = f.read()
  598. return self.from_data(data)
  599. def _check_ext(self, filename: str, ext):
  600. if not filename.lower().endswith(ext):
  601. raise FormatError(f"filename not ended with {ext}")
  602. def _check_dir(self, filename: str):
  603. dir = path.dirname(filename)
  604. if dir and not path.exists(dir):
  605. logging.info(f"mkdir of {dir} for {filename}")
  606. os.makedirs(dir)
  607. def to_bin(self,
  608. filename: str,
  609. compress: CompressMethod = CompressMethod.NONE):
  610. """
  611. Write this image to file, filename should be ended with '.bin'
  612. """
  613. self._check_ext(filename, ".bin")
  614. self._check_dir(filename)
  615. with open(filename, "wb+") as f:
  616. bin = bytearray()
  617. flags = 0
  618. flags |= 0x08 if compress != CompressMethod.NONE else 0
  619. flags |= 0x01 if self.premultiplied else 0
  620. header = LVGLImageHeader(self.cf,
  621. self.w,
  622. self.h,
  623. self.stride,
  624. flags=flags)
  625. bin += header.binary
  626. compressed = LVGLCompressData(self.cf, compress, self.data)
  627. bin += compressed.compressed
  628. f.write(bin)
  629. return self
  630. def to_c_array(self,
  631. filename: str,
  632. compress: CompressMethod = CompressMethod.NONE):
  633. self._check_ext(filename, ".c")
  634. self._check_dir(filename)
  635. if compress != CompressMethod.NONE:
  636. data = LVGLCompressData(self.cf, compress, self.data).compressed
  637. else:
  638. data = self.data
  639. write_c_array_file(self.w, self.h, self.stride, self.cf, filename,
  640. self.premultiplied,
  641. compress, data)
  642. def to_png(self, filename: str):
  643. self._check_ext(filename, ".png")
  644. self._check_dir(filename)
  645. old_stride = self.stride
  646. self.adjust_stride(align=1)
  647. if self.cf.is_indexed:
  648. data = self.data
  649. # Separate lvgl bin image data to palette and bitmap
  650. # The palette is in format of [(RGBA), (RGBA)...].
  651. # LVGL palette is in format of B,G,R,A,...
  652. palette = [(data[i * 4 + 2], data[i * 4 + 1], data[i * 4 + 0],
  653. data[i * 4 + 3]) for i in range(self.cf.ncolors)]
  654. data = data[self.cf.ncolors * 4:]
  655. encoder = png.Writer(self.w,
  656. self.h,
  657. palette=palette,
  658. bitdepth=self.cf.bpp)
  659. # separate packed data to plain data
  660. data = unpack_colors(data, self.cf, self.w)
  661. elif self.cf.is_alpha_only:
  662. # separate packed data to plain data
  663. transparency = unpack_colors(self.data, self.cf, self.w)
  664. data = []
  665. for a in transparency:
  666. data += [0, 0, 0, a]
  667. encoder = png.Writer(self.w, self.h, greyscale=False, alpha=True)
  668. elif self.cf == ColorFormat.L8:
  669. # to grayscale
  670. encoder = png.Writer(self.w,
  671. self.h,
  672. bitdepth=self.cf.bpp,
  673. greyscale=True,
  674. alpha=False)
  675. data = self.data
  676. elif self.cf.is_colormap:
  677. encoder = png.Writer(self.w,
  678. self.h,
  679. alpha=self.cf.has_alpha,
  680. greyscale=False)
  681. data = unpack_colors(self.data, self.cf, self.w)
  682. else:
  683. logging.warning(f"missing logic: {self.cf.name}")
  684. return
  685. with open(filename, "wb") as f:
  686. encoder.write_array(f, data)
  687. self.adjust_stride(stride=old_stride)
  688. def from_png(self,
  689. filename: str,
  690. cf: ColorFormat = None,
  691. background: int = 0x00_00_00,
  692. rgb565_dither=False):
  693. """
  694. Create lvgl image from png file.
  695. If cf is none, used I1/2/4/8 based on palette size
  696. """
  697. self.background = background
  698. self.rgb565_dither = rgb565_dither
  699. if cf is None: # guess cf from filename
  700. # split filename string and match with ColorFormat to check
  701. # which cf to use
  702. names = str(path.basename(filename)).split(".")
  703. for c in names[1:-1]:
  704. if c in ColorFormat.__members__:
  705. cf = ColorFormat[c]
  706. break
  707. if cf is None or cf.is_indexed: # palette mode
  708. self._png_to_indexed(cf, filename)
  709. elif cf.is_alpha_only:
  710. self._png_to_alpha_only(cf, filename)
  711. elif cf.is_luma_only:
  712. self._png_to_luma_only(cf, filename)
  713. elif cf.is_colormap:
  714. self._png_to_colormap(cf, filename)
  715. else:
  716. logging.warning(f"missing logic: {cf.name}")
  717. logging.info(f"from png: {filename}, cf: {self.cf.name}")
  718. return self
  719. def _png_to_indexed(self, cf: ColorFormat, filename: str):
  720. # convert to palette mode
  721. auto_cf = cf is None
  722. # read the image data to get the metadata
  723. reader = png.Reader(filename=filename)
  724. w, h, rows, metadata = reader.read()
  725. # to preserve original palette data only convert the image if needed. For this
  726. # check if image has a palette and the requested palette size equals the existing one
  727. if not 'palette' in metadata or not auto_cf and len(metadata['palette']) != 2 ** cf.bpp:
  728. # reread and convert file
  729. reader = png.Reader(
  730. bytes=PngQuant(256 if auto_cf else cf.ncolors).convert(filename))
  731. w, h, rows, _ = reader.read()
  732. palette = reader.palette(alpha="force") # always return alpha
  733. palette_len = len(palette)
  734. if auto_cf:
  735. if palette_len <= 2:
  736. cf = ColorFormat.I1
  737. elif palette_len <= 4:
  738. cf = ColorFormat.I2
  739. elif palette_len <= 16:
  740. cf = ColorFormat.I4
  741. else:
  742. cf = ColorFormat.I8
  743. if palette_len != cf.ncolors:
  744. if not auto_cf:
  745. logging.warning(
  746. f"{path.basename(filename)} palette: {palette_len}, "
  747. f"extended to: {cf.ncolors}")
  748. palette += [(255, 255, 255, 0)] * (cf.ncolors - palette_len)
  749. # Assemble lvgl image palette from PNG palette.
  750. # PNG palette is a list of tuple(R,G,B,A)
  751. rawdata = bytearray()
  752. for (r, g, b, a) in palette:
  753. rawdata += uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0))
  754. # pack data if not in I8 format
  755. if cf == ColorFormat.I8:
  756. for e in rows:
  757. rawdata += e
  758. else:
  759. for e in png.pack_rows(rows, cf.bpp):
  760. rawdata += e
  761. self.set_data(cf, w, h, rawdata)
  762. def _png_to_alpha_only(self, cf: ColorFormat, filename: str):
  763. reader = png.Reader(str(filename))
  764. w, h, rows, info = reader.asRGBA8()
  765. if not info['alpha']:
  766. raise FormatError(f"{filename} has no alpha channel")
  767. rawdata = bytearray()
  768. if cf == ColorFormat.A8:
  769. for row in rows:
  770. A = row[3::4]
  771. for e in A:
  772. rawdata += uint8_t(e)
  773. else:
  774. shift = 8 - cf.bpp
  775. mask = 2**cf.bpp - 1
  776. rows = [[(a >> shift) & mask for a in row[3::4]] for row in rows]
  777. for row in png.pack_rows(rows, cf.bpp):
  778. rawdata += row
  779. self.set_data(cf, w, h, rawdata)
  780. def sRGB_to_linear(self, x):
  781. if x < 0.04045:
  782. return x / 12.92
  783. return pow((x + 0.055) / 1.055, 2.4)
  784. def linear_to_sRGB(self, y):
  785. if y <= 0.0031308:
  786. return 12.92 * y
  787. return 1.055 * pow(y, 1 / 2.4) - 0.055
  788. def _png_to_luma_only(self, cf: ColorFormat, filename: str):
  789. reader = png.Reader(str(filename))
  790. w, h, rows, info = reader.asRGBA8()
  791. rawdata = bytearray()
  792. for row in rows:
  793. R = row[0::4]
  794. G = row[1::4]
  795. B = row[2::4]
  796. A = row[3::4]
  797. for r, g, b, a in zip(R, G, B, A):
  798. r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
  799. r = self.sRGB_to_linear(r / 255.0)
  800. g = self.sRGB_to_linear(g / 255.0)
  801. b = self.sRGB_to_linear(b / 255.0)
  802. luma = 0.2126 * r + 0.7152 * g + 0.0722 * b
  803. rawdata += uint8_t(int(self.linear_to_sRGB(luma) * 255))
  804. self.set_data(ColorFormat.L8, w, h, rawdata)
  805. def _png_to_colormap(self, cf, filename: str):
  806. if cf == ColorFormat.ARGB8888:
  807. def pack(r, g, b, a):
  808. return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0))
  809. elif cf == ColorFormat.XRGB8888:
  810. def pack(r, g, b, a):
  811. r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
  812. return uint32_t((0xff << 24) | (r << 16) | (g << 8) | (b << 0))
  813. elif cf == ColorFormat.RGB888:
  814. def pack(r, g, b, a):
  815. r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
  816. return uint24_t((r << 16) | (g << 8) | (b << 0))
  817. elif cf == ColorFormat.RGB565:
  818. def pack(r, g, b, a):
  819. r, g, b, a = color_pre_multiply(r, g, b, a, self.background)
  820. color = (r >> 3) << 11
  821. color |= (g >> 2) << 5
  822. color |= (b >> 3) << 0
  823. return uint16_t(color)
  824. elif cf == ColorFormat.RGB565A8:
  825. def pack(r, g, b, a):
  826. color = (r >> 3) << 11
  827. color |= (g >> 2) << 5
  828. color |= (b >> 3) << 0
  829. return uint16_t(color)
  830. elif cf == ColorFormat.ARGB8565:
  831. def pack(r, g, b, a):
  832. color = (r >> 3) << 11
  833. color |= (g >> 2) << 5
  834. color |= (b >> 3) << 0
  835. return uint24_t((a << 16) | color)
  836. else:
  837. raise FormatError(f"Invalid color format: {cf.name}")
  838. reader = png.Reader(str(filename))
  839. w, h, rows, _ = reader.asRGBA8()
  840. rawdata = bytearray()
  841. alpha = bytearray()
  842. for y, row in enumerate(rows):
  843. R = row[0::4]
  844. G = row[1::4]
  845. B = row[2::4]
  846. A = row[3::4]
  847. for x, (r, g, b, a) in enumerate(zip(R, G, B, A)):
  848. if cf == ColorFormat.RGB565A8:
  849. alpha += uint8_t(a)
  850. if (
  851. self.rgb565_dither and
  852. cf in (ColorFormat.RGB565, ColorFormat.RGB565A8, ColorFormat.ARGB8565)
  853. ):
  854. treshold_id = ((y & 7) << 3) + (x & 7)
  855. r = min(r + red_thresh[treshold_id], 0xFF) & 0xF8
  856. g = min(g + green_thresh[treshold_id], 0xFF) & 0xFC
  857. b = min(b + blue_thresh[treshold_id], 0xFF) & 0xF8
  858. rawdata += pack(r, g, b, a)
  859. if cf == ColorFormat.RGB565A8:
  860. rawdata += alpha
  861. self.set_data(cf, w, h, rawdata)
  862. red_thresh = [
  863. 1, 7, 3, 5, 0, 8, 2, 6,
  864. 7, 1, 5, 3, 8, 0, 6, 2,
  865. 3, 5, 0, 8, 2, 6, 1, 7,
  866. 5, 3, 8, 0, 6, 2, 7, 1,
  867. 0, 8, 2, 6, 1, 7, 3, 5,
  868. 8, 0, 6, 2, 7, 1, 5, 3,
  869. 2, 6, 1, 7, 3, 5, 0, 8,
  870. 6, 2, 7, 1, 5, 3, 8, 0
  871. ]
  872. green_thresh = [
  873. 1, 3, 2, 2, 3, 1, 2, 2,
  874. 2, 2, 0, 4, 2, 2, 4, 0,
  875. 3, 1, 2, 2, 1, 3, 2, 2,
  876. 2, 2, 4, 0, 2, 2, 0, 4,
  877. 1, 3, 2, 2, 3, 1, 2, 2,
  878. 2, 2, 0, 4, 2, 2, 4, 0,
  879. 3, 1, 2, 2, 1, 3, 2, 2,
  880. 2, 2, 4, 0, 2, 2, 0, 4
  881. ]
  882. blue_thresh = [
  883. 5, 3, 8, 0, 6, 2, 7, 1,
  884. 3, 5, 0, 8, 2, 6, 1, 7,
  885. 8, 0, 6, 2, 7, 1, 5, 3,
  886. 0, 8, 2, 6, 1, 7, 3, 5,
  887. 6, 2, 7, 1, 5, 3, 8, 0,
  888. 2, 6, 1, 7, 3, 5, 0, 8,
  889. 7, 1, 5, 3, 8, 0, 6, 2,
  890. 1, 7, 3, 5, 0, 8, 2, 6
  891. ]
  892. class RLEHeader:
  893. def __init__(self, blksize: int, len: int):
  894. self.blksize = blksize
  895. self.len = len
  896. @property
  897. def binary(self):
  898. magic = 0x5aa521e0
  899. rle_header = self.blksize
  900. rle_header |= (self.len & 0xffffff) << 4
  901. binary = bytearray()
  902. binary.extend(uint32_t(magic))
  903. binary.extend(uint32_t(rle_header))
  904. return binary
  905. class RLEImage(LVGLImage):
  906. def __init__(self,
  907. cf: ColorFormat = ColorFormat.UNKNOWN,
  908. w: int = 0,
  909. h: int = 0,
  910. data: bytes = b'') -> None:
  911. super().__init__(cf, w, h, data)
  912. def to_rle(self, filename: str):
  913. """
  914. Compress this image to file, filename should be ended with '.rle'
  915. """
  916. self._check_ext(filename, ".rle")
  917. self._check_dir(filename)
  918. # compress image data excluding lvgl image header
  919. blksize = (self.cf.bpp + 7) // 8
  920. compressed = self.rle_compress(self.data, blksize)
  921. with open(filename, "wb+") as f:
  922. header = RLEHeader(blksize, len(self.data)).binary
  923. header.extend(self.header.binary)
  924. f.write(header)
  925. f.write(compressed)
  926. def rle_compress(self, data: bytearray, blksize: int, threshold=16):
  927. index = 0
  928. data_len = len(data)
  929. compressed_data = []
  930. memview = memoryview(data)
  931. while index < data_len:
  932. repeat_cnt = self.get_repeat_count(memview[index:], blksize)
  933. if repeat_cnt == 0:
  934. # done
  935. break
  936. elif repeat_cnt < threshold:
  937. nonrepeat_cnt = self.get_nonrepeat_count(
  938. memview[index:], blksize, threshold)
  939. ctrl_byte = uint8_t(nonrepeat_cnt | 0x80)
  940. compressed_data.append(ctrl_byte)
  941. compressed_data.append(memview[index:index +
  942. nonrepeat_cnt * blksize])
  943. index += nonrepeat_cnt * blksize
  944. else:
  945. ctrl_byte = uint8_t(repeat_cnt)
  946. compressed_data.append(ctrl_byte)
  947. compressed_data.append(memview[index:index + blksize])
  948. index += repeat_cnt * blksize
  949. return b"".join(compressed_data)
  950. def get_repeat_count(self, data: bytearray, blksize: int):
  951. if len(data) < blksize:
  952. return 0
  953. start = data[:blksize]
  954. index = 0
  955. repeat_cnt = 0
  956. value = 0
  957. while index < len(data):
  958. value = data[index:index + blksize]
  959. if value == start:
  960. repeat_cnt += 1
  961. if repeat_cnt == 127: # limit max repeat count to max value of signed char.
  962. break
  963. else:
  964. break
  965. index += blksize
  966. return repeat_cnt
  967. def get_nonrepeat_count(self, data: bytearray, blksize: int, threshold):
  968. if len(data) < blksize:
  969. return 0
  970. pre_value = data[:blksize]
  971. index = 0
  972. nonrepeat_count = 0
  973. repeat_cnt = 0
  974. while True:
  975. value = data[index:index + blksize]
  976. if value == pre_value:
  977. repeat_cnt += 1
  978. if repeat_cnt > threshold:
  979. # repeat found.
  980. break
  981. else:
  982. pre_value = value
  983. nonrepeat_count += 1 + repeat_cnt
  984. repeat_cnt = 0
  985. if nonrepeat_count >= 127: # limit max repeat count to max value of signed char.
  986. nonrepeat_count = 127
  987. break
  988. index += blksize # move to next position
  989. if index >= len(data): # data end
  990. nonrepeat_count += repeat_cnt
  991. break
  992. return nonrepeat_count
  993. class RAWImage():
  994. '''
  995. RAW image is an exception to LVGL image, it has color format of RAW or RAW_ALPHA.
  996. It has same image header as LVGL image, but the data is pure raw data from file.
  997. It does not support stride adjustment etc. features for LVGL image.
  998. It only supports convert an image to C array with RAW or RAW_ALPHA format.
  999. '''
  1000. CF_SUPPORTED = (ColorFormat.RAW, ColorFormat.RAW_ALPHA)
  1001. class NotSupported(NotImplementedError):
  1002. pass
  1003. def __init__(self,
  1004. cf: ColorFormat = ColorFormat.UNKNOWN,
  1005. data: bytes = b'') -> None:
  1006. self.cf = cf
  1007. self.data = data
  1008. def to_c_array(self,
  1009. filename: str):
  1010. # Image size is set to zero, to let PNG or JPEG decoder to handle it
  1011. # Stride is meaningless for RAW image
  1012. write_c_array_file(0, 0, 0, self.cf, filename,
  1013. False, CompressMethod.NONE, self.data)
  1014. def from_file(self,
  1015. filename: str,
  1016. cf: ColorFormat = None):
  1017. if cf not in RAWImage.CF_SUPPORTED:
  1018. raise RAWImage.NotSupported(f"Invalid color format: {cf.name}")
  1019. with open(filename, "rb") as f:
  1020. self.data = f.read()
  1021. self.cf = cf
  1022. return self
  1023. class OutputFormat(Enum):
  1024. C_ARRAY = "C"
  1025. BIN_FILE = "BIN"
  1026. PNG_FILE = "PNG" # convert to lvgl image and then to png
  1027. class PNGConverter:
  1028. def __init__(self,
  1029. files: List,
  1030. cf: ColorFormat,
  1031. ofmt: OutputFormat,
  1032. odir: str,
  1033. background: int = 0x00,
  1034. align: int = 1,
  1035. premultiply: bool = False,
  1036. compress: CompressMethod = CompressMethod.NONE,
  1037. keep_folder=True,
  1038. rgb565_dither=False) -> None:
  1039. self.files = files
  1040. self.cf = cf
  1041. self.ofmt = ofmt
  1042. self.output = odir
  1043. self.pngquant = None
  1044. self.keep_folder = keep_folder
  1045. self.align = align
  1046. self.premultiply = premultiply
  1047. self.compress = compress
  1048. self.background = background
  1049. self.rgb565_dither = rgb565_dither
  1050. def _replace_ext(self, input, ext):
  1051. if self.keep_folder:
  1052. name, _ = path.splitext(input)
  1053. else:
  1054. name, _ = path.splitext(path.basename(input))
  1055. output = name + ext
  1056. output = path.join(self.output, output)
  1057. return output
  1058. def convert(self):
  1059. output = []
  1060. for f in self.files:
  1061. if self.cf in (ColorFormat.RAW, ColorFormat.RAW_ALPHA):
  1062. # Process RAW image explicitly
  1063. img = RAWImage().from_file(f, self.cf)
  1064. img.to_c_array(self._replace_ext(f, ".c"))
  1065. else:
  1066. img = LVGLImage().from_png(f, self.cf, background=self.background, rgb565_dither=self.rgb565_dither)
  1067. img.adjust_stride(align=self.align)
  1068. if self.premultiply:
  1069. img.premultiply()
  1070. output.append((f, img))
  1071. if self.ofmt == OutputFormat.BIN_FILE:
  1072. img.to_bin(self._replace_ext(f, ".bin"),
  1073. compress=self.compress)
  1074. elif self.ofmt == OutputFormat.C_ARRAY:
  1075. img.to_c_array(self._replace_ext(f, ".c"),
  1076. compress=self.compress)
  1077. elif self.ofmt == OutputFormat.PNG_FILE:
  1078. img.to_png(self._replace_ext(f, ".png"))
  1079. return output
  1080. def main():
  1081. parser = argparse.ArgumentParser(description='LVGL PNG to bin image tool.')
  1082. parser.add_argument('--ofmt',
  1083. help="output filename format, C or BIN",
  1084. default="BIN",
  1085. choices=["C", "BIN", "PNG"])
  1086. parser.add_argument(
  1087. '--cf',
  1088. help=("bin image color format, use AUTO for automatically "
  1089. "choose from I1/2/4/8"),
  1090. default="I8",
  1091. choices=[
  1092. "L8", "I1", "I2", "I4", "I8", "A1", "A2", "A4", "A8", "ARGB8888",
  1093. "XRGB8888", "RGB565", "RGB565A8", "ARGB8565", "RGB888", "AUTO",
  1094. "RAW", "RAW_ALPHA"
  1095. ])
  1096. parser.add_argument('--rgb565dither', action='store_true',
  1097. help="use dithering to correct banding in gradients", default=False)
  1098. parser.add_argument('--premultiply', action='store_true',
  1099. help="pre-multiply color with alpha", default=False)
  1100. parser.add_argument('--compress',
  1101. help=("Binary data compress method, default to NONE"),
  1102. default="NONE",
  1103. choices=["NONE", "RLE", "LZ4"])
  1104. parser.add_argument('--align',
  1105. help="stride alignment in bytes for bin image",
  1106. default=1,
  1107. type=int,
  1108. metavar='byte',
  1109. nargs='?')
  1110. parser.add_argument('--background',
  1111. help="Background color for formats without alpha",
  1112. default=0x00_00_00,
  1113. type=lambda x: int(x, 0),
  1114. metavar='color',
  1115. nargs='?')
  1116. parser.add_argument('-o',
  1117. '--output',
  1118. default="./output",
  1119. help="Select the output folder, default to ./output")
  1120. parser.add_argument('-v', '--verbose', action='store_true')
  1121. parser.add_argument(
  1122. 'input', help="the filename or folder to be recursively converted")
  1123. args = parser.parse_args()
  1124. if path.isfile(args.input):
  1125. files = [args.input]
  1126. elif path.isdir(args.input):
  1127. files = list(Path(args.input).rglob("*.[pP][nN][gG]"))
  1128. else:
  1129. raise BaseException(f"invalid input: {args.input}")
  1130. if args.verbose:
  1131. logging.basicConfig(level=logging.INFO)
  1132. logging.info(f"options: {args.__dict__}, files:{[str(f) for f in files]}")
  1133. if args.cf == "AUTO":
  1134. cf = None
  1135. else:
  1136. cf = ColorFormat[args.cf]
  1137. ofmt = OutputFormat(args.ofmt) if cf not in (
  1138. ColorFormat.RAW, ColorFormat.RAW_ALPHA) else OutputFormat.C_ARRAY
  1139. compress = CompressMethod[args.compress]
  1140. converter = PNGConverter(files,
  1141. cf,
  1142. ofmt,
  1143. args.output,
  1144. background=args.background,
  1145. align=args.align,
  1146. premultiply=args.premultiply,
  1147. compress=compress,
  1148. keep_folder=False,
  1149. rgb565_dither=args.rgb565dither)
  1150. output = converter.convert()
  1151. for f, img in output:
  1152. logging.info(f"len: {img.data_len} for {path.basename(f)} ")
  1153. print(f"done {len(files)} files")
  1154. def test():
  1155. logging.basicConfig(level=logging.INFO)
  1156. f = "pngs/cogwheel.RGB565A8.png"
  1157. img = LVGLImage().from_png(f,
  1158. cf=ColorFormat.ARGB8565,
  1159. background=0xFF_FF_00,
  1160. rgb565_dither=True)
  1161. img.adjust_stride(align=16)
  1162. img.premultiply()
  1163. img.to_bin("output/cogwheel.ARGB8565.bin")
  1164. img.to_c_array("output/cogwheel-abc.c") # file name is used as c var name
  1165. img.to_png("output/cogwheel.ARGB8565.png.png") # convert back to png
  1166. def test_raw():
  1167. logging.basicConfig(level=logging.INFO)
  1168. f = "pngs/cogwheel.RGB565A8.png"
  1169. img = RAWImage().from_file(f,
  1170. cf=ColorFormat.RAW_ALPHA)
  1171. img.to_c_array("output/cogwheel-raw.c")
  1172. if __name__ == "__main__":
  1173. # test()
  1174. # test_raw()
  1175. main()