448 lines
18 KiB
Python
448 lines
18 KiB
Python
"""Build / apply a system-app launcher-icon replacement for a Vela `vela_system.bin`.
|
||||
|
|
|
|||
|
|
Why "same length" is the whole ball game
|
|||
|
|
----------------------------------------
|
|||
|
|
ROMFS packs superblock -> inode -> name -> file data strictly back to back, and
|
|||
|
|
every `next` / `spec` pointer is an ABSOLUTE offset. If a replacement file
|
|||
|
|
changes length by even one byte, every later offset shifts and the image is
|
|||
|
|
corrupt. So a replacement must encode to EXACTLY the original byte length:
|
|||
|
|
|
|||
|
|
* source PNG == original dimensions -> encode, lengths match, done.
|
|||
|
|
* source PNG != original dimensions -> two options:
|
|||
|
|
(default) resize to the original dimensions (icon renders identically)
|
|||
|
|
--pad keep the source dimensions, zero-pad the container to the
|
|||
|
|
original length (renders smaller unless the launcher scales)
|
|||
|
|
|
|||
|
|
Usage:
|
|||
|
|
iconpack.py list <vela_system.bin>
|
|||
|
|
iconpack.py make <vela_system.bin> <pngdir> <outdir> [--pad] [--patched]
|
|||
|
|
iconpack.py apply <vela_system.bin> <patch.bin> <out.bin>
|
|||
|
|
"""
|
|||
|
|
import os, sys, json, struct, hashlib, shutil
|
|||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
from romfs import Romfs, img_header
|
|||
|
|
from lvgl_bin import rle_compress, pad_rle
|
|||
|
|
from PIL import Image
|
|||
|
|
|
|||
|
|
CF_ARGB8888, CF_I8, MAGIC = 0x10, 0x0a, 0x19
|
|||
|
|
FMT = {CF_ARGB8888: 'ARGB8888', CF_I8: 'I8'}
|
|||
|
|
RESAMPLE = Image.LANCZOS
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ----------------------------------------------------------------- encoders
|
|||
|
|
def encode_argb8888(img):
|
|||
|
|
rgba = img.convert('RGBA')
|
|||
|
|
w, h = rgba.size
|
|||
|
|
px = rgba.load()
|
|||
|
|
out = bytearray()
|
|||
|
|
for y in range(h):
|
|||
|
|
row = bytearray()
|
|||
|
|
for x in range(w):
|
|||
|
|
r, g, b, a = px[x, y]
|
|||
|
|
row += bytes((b, g, r, a))
|
|||
|
|
out += row
|
|||
|
|
return bytes(out)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def encode_i8(img, alpha_levels=16, sharpen_up=0.0):
|
|||
|
|
"""I8 encoder with an ALPHA-AWARE palette.
|
|||
|
|
|
|||
|
|
The naive version quantised RGB only and then stored one *averaged* alpha
|
|||
|
|
per palette entry. Any colour shared by an opaque body pixel and a
|
|||
|
|
semi-transparent edge pixel collapsed to a mid alpha, and fully transparent
|
|||
|
|
pixels inherited that same entry -- so the transparent margin became faintly
|
|||
|
|
visible (measured: perpetual_calendar haze 0.000 -> 1.000, i.e. a grey veil
|
|||
|
|
around the whole icon).
|
|||
|
|
|
|||
|
|
Here the palette is built over (quantised RGB, quantised alpha) PAIRS, so a
|
|||
|
|
colour can own several entries at different alphas and alpha 0 stays alpha 0.
|
|||
|
|
"""
|
|||
|
|
rgba = img.convert('RGBA')
|
|||
|
|
w, h = rgba.size
|
|||
|
|
src = rgba.load()
|
|||
|
|
|
|||
|
|
# average colour of the visible pixels: transparent pixels must not invent
|
|||
|
|
# their own colour clusters
|
|||
|
|
rs = gs = bs = n = 0
|
|||
|
|
for y in range(h):
|
|||
|
|
for x in range(w):
|
|||
|
|
r, g, b, a = src[x, y]
|
|||
|
|
if a > 0:
|
|||
|
|
rs += r; gs += g; bs += b; n += 1
|
|||
|
|
avg = (rs // n, gs // n, bs // n) if n else (0, 0, 0)
|
|||
|
|
|
|||
|
|
step = max(1, 256 // max(1, alpha_levels))
|
|||
|
|
flat = Image.new('RGB', (w, h))
|
|||
|
|
fp = flat.load()
|
|||
|
|
qa = [[0] * w for _ in range(h)]
|
|||
|
|
for y in range(h):
|
|||
|
|
for x in range(w):
|
|||
|
|
r, g, b, a = src[x, y]
|
|||
|
|
fp[x, y] = (r, g, b) if a > 0 else avg
|
|||
|
|
q = int(round(a / float(step))) * step
|
|||
|
|
qa[y][x] = 255 if q > 255 else q
|
|||
|
|
|
|||
|
|
p = flat.quantize(colors=256, method=Image.MEDIANCUT)
|
|||
|
|
pal_rgb = p.getpalette()[:768]
|
|||
|
|
idx = p.tobytes()
|
|||
|
|
|
|||
|
|
cnt = {}
|
|||
|
|
for y in range(h):
|
|||
|
|
row = y * w
|
|||
|
|
for x in range(w):
|
|||
|
|
k = (idx[row + x], qa[y][x])
|
|||
|
|
cnt[k] = cnt.get(k, 0) + 1
|
|||
|
|
keep = sorted(cnt.items(), key=lambda kv: -kv[1])[:256]
|
|||
|
|
|
|||
|
|
pal = bytearray()
|
|||
|
|
remap = {}
|
|||
|
|
by_rgb = {}
|
|||
|
|
for slot, ((pi, al), _c) in enumerate(keep):
|
|||
|
|
r = pal_rgb[pi * 3] if pi * 3 + 2 < len(pal_rgb) else 0
|
|||
|
|
g = pal_rgb[pi * 3 + 1] if pi * 3 + 2 < len(pal_rgb) else 0
|
|||
|
|
b = pal_rgb[pi * 3 + 2] if pi * 3 + 2 < len(pal_rgb) else 0
|
|||
|
|
pal += bytes((b, g, r, al))
|
|||
|
|
remap[(pi, al)] = slot
|
|||
|
|
by_rgb.setdefault(pi, []).append((al, slot))
|
|||
|
|
removed = 256 - len(keep)
|
|||
|
|
if removed > 0:
|
|||
|
|
pal += b'\x00\x00\x00\x00' * removed
|
|||
|
|
for pi in by_rgb:
|
|||
|
|
by_rgb[pi].sort()
|
|||
|
|
|
|||
|
|
out = bytearray(w * h)
|
|||
|
|
for y in range(h):
|
|||
|
|
row = y * w
|
|||
|
|
for x in range(w):
|
|||
|
|
pi = idx[row + x]
|
|||
|
|
al = qa[y][x]
|
|||
|
|
s = remap.get((pi, al))
|
|||
|
|
if s is None:
|
|||
|
|
cand = by_rgb.get(pi)
|
|||
|
|
if cand:
|
|||
|
|
s = min(cand, key=lambda t: abs(t[0] - al))[1]
|
|||
|
|
else:
|
|||
|
|
s = 0
|
|||
|
|
out[row + x] = s
|
|||
|
|
return bytes(pal) + bytes(out)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def encode_i8_palette(img, pal_bgra, alpha_tol=12):
|
|||
|
|
"""Quantise to an EXISTING 256-entry BGRA palette (as stored in an I8 blob).
|
|||
|
|
|
|||
|
|
Reusing the source icon's own palette preserves the artwork's gradient shades
|
|||
|
|
and its anti-aliased edge levels. A freshly clustered palette cannot: the
|
|||
|
|
plate gradient eats the 256 entries, the edge collapses to a few alpha steps
|
|||
|
|
and dropped pixels land on wrong entries -- measured on remote_camera as a
|
|||
|
|
jagged plate rim plus stray coloured speckles.
|
|||
|
|
|
|||
|
|
The search is constrained by alpha so that
|
|||
|
|
* fully transparent source pixels can only pick transparent entries
|
|||
|
|
(no veil, no speckles), and
|
|||
|
|
* fully opaque source pixels can only pick opaque entries
|
|||
|
|
(the body never goes translucent).
|
|||
|
|
"""
|
|||
|
|
rgba = img.convert('RGBA')
|
|||
|
|
w, h = rgba.size
|
|||
|
|
px = rgba.load()
|
|||
|
|
|
|||
|
|
ent = [(pal_bgra[i * 4 + 2], pal_bgra[i * 4 + 1],
|
|||
|
|
pal_bgra[i * 4], pal_bgra[i * 4 + 3]) for i in range(256)]
|
|||
|
|
clear = [i for i, e in enumerate(ent) if e[3] == 0]
|
|||
|
|
if not clear: # guarantee a transparent entry exists
|
|||
|
|
worst = min(range(256), key=lambda i: ent[i][3])
|
|||
|
|
ent[worst] = (0, 0, 0, 0)
|
|||
|
|
clear = [worst]
|
|||
|
|
solid = [i for i, e in enumerate(ent) if e[3] >= 248]
|
|||
|
|
by_alpha = {}
|
|||
|
|
for i, e in enumerate(ent):
|
|||
|
|
by_alpha.setdefault(e[3], []).append(i)
|
|||
|
|
|
|||
|
|
def premul(e):
|
|||
|
|
r, g, b, a = e
|
|||
|
|
return (r * a // 255, g * a // 255, b * a // 255, a)
|
|||
|
|
|
|||
|
|
P = [premul(e) for e in ent]
|
|||
|
|
cache = {}
|
|||
|
|
out = bytearray(w * h)
|
|||
|
|
for y in range(h):
|
|||
|
|
row = y * w
|
|||
|
|
for x in range(w):
|
|||
|
|
r, g, b, a = px[x, y]
|
|||
|
|
key = (r >> 2, g >> 2, b >> 2, a >> 3)
|
|||
|
|
s = cache.get(key)
|
|||
|
|
if s is None:
|
|||
|
|
if a == 0:
|
|||
|
|
cand = clear
|
|||
|
|
elif a >= 248 and solid:
|
|||
|
|
cand = solid
|
|||
|
|
else:
|
|||
|
|
cand = []
|
|||
|
|
for da in range(0, alpha_tol + 1):
|
|||
|
|
cand += by_alpha.get(a + da, ()) if a + da <= 255 else ()
|
|||
|
|
if da:
|
|||
|
|
cand += by_alpha.get(a - da, ()) if a - da >= 0 else ()
|
|||
|
|
if not cand:
|
|||
|
|
cand = range(256)
|
|||
|
|
pr, pg, pb, pa = premul((r, g, b, a))
|
|||
|
|
best, bd = cand[0], None
|
|||
|
|
for i in cand:
|
|||
|
|
q = P[i]
|
|||
|
|
d = ((pr - q[0]) ** 2 + (pg - q[1]) ** 2 + (pb - q[2]) ** 2
|
|||
|
|
+ 2 * (pa - q[3]) ** 2)
|
|||
|
|
if bd is None or d < bd:
|
|||
|
|
bd, best = d, i
|
|||
|
|
s = best
|
|||
|
|
cache[key] = s
|
|||
|
|
out[row + x] = s
|
|||
|
|
|
|||
|
|
pal = bytearray()
|
|||
|
|
for (r, g, b, a) in ent:
|
|||
|
|
pal += bytes((b, g, r, a))
|
|||
|
|
return bytes(pal) + bytes(out)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def encode(img, cf, w, h, flags=0, pad_to=None, palette=None):
|
|||
|
|
if cf == CF_ARGB8888:
|
|||
|
|
body = encode_argb8888(img)
|
|||
|
|
elif palette is not None:
|
|||
|
|
body = encode_i8_palette(img, palette)
|
|||
|
|
else:
|
|||
|
|
body = encode_i8(img)
|
|||
|
|
stride = w * (4 if cf == CF_ARGB8888 else 1)
|
|||
|
|
blob = struct.pack('<BBHHHHH', MAGIC, cf, flags, w, h, stride, 0) + body
|
|||
|
|
if pad_to is not None:
|
|||
|
|
if len(blob) > pad_to:
|
|||
|
|
raise ValueError(f'encoded {len(blob)} > slot {pad_to}')
|
|||
|
|
blob += b'\x00' * (pad_to - len(blob))
|
|||
|
|
return blob
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ----------------------------------------------------------------- helpers
|
|||
|
|
def raw_i8_strided(img, stride, colors=256):
|
|||
|
|
"""I8 像素流:1024 B 调色板(BGRA)+ stride*h 个索引(行尾按 header 的 stride 补 0)。
|
|||
|
|
|
|||
|
|
RLE 槽位专用:用 FASTOCTREE 自适应减色 + 关闭抖动 —— 颜色越少、相邻像素越容易出现
|
|||
|
|
长游程,RLE 流就越短(这是能不能塞进原 csize 的关键)。
|
|||
|
|
"""
|
|||
|
|
q = img.convert('RGBA').quantize(colors=max(2, min(256, colors)),
|
|||
|
|
method=Image.FASTOCTREE, dither=Image.NONE)
|
|||
|
|
if q.palette.mode == 'RGBA':
|
|||
|
|
pal = list(q.getpalette('RGBA'))
|
|||
|
|
else:
|
|||
|
|
rgb = q.getpalette('RGB')
|
|||
|
|
pal = [v for i in range(256) for v in (rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], 255)]
|
|||
|
|
pal = (pal + [0] * 1024)[:1024]
|
|||
|
|
pal_b = bytearray()
|
|||
|
|
for i in range(256): # 存储序:B, G, R, A
|
|||
|
|
r, g, b, a = pal[i * 4:i * 4 + 4]
|
|||
|
|
pal_b += bytes((b, g, r, a))
|
|||
|
|
idx = q.tobytes()
|
|||
|
|
w, h = img.size
|
|||
|
|
rows = bytearray()
|
|||
|
|
for y in range(h):
|
|||
|
|
rows += idx[y * w:(y + 1) * w]
|
|||
|
|
if stride > w:
|
|||
|
|
rows += b'\x00' * (stride - w)
|
|||
|
|
return bytes(pal_b) + bytes(rows)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 从大到小试:优先保真,装不下再减色
|
|||
|
|
RLE_COLOR_STEPS = (256, 192, 160, 128, 96, 80, 64, 48, 40, 32, 24, 16, 12, 8, 6, 4)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def encode_into_slot(img, stock_blob, slot_len=None, colors=None):
|
|||
|
|
"""把 img 编成与 stock_blob **逐字节等长** 的 LVGL v9 blob(沿用原 header)。
|
|||
|
|
|
|||
|
|
真实固件里这些 UI 素材有两种槽位形态,都必须支持,否则 ROMFS 后面所有偏移全平移:
|
|||
|
|
* ARGB8888 未压缩(144x144 图标那种):像素流 + 零填充到原长度
|
|||
|
|
* I8 + RLE 压缩(walkie_talkie 里几乎全是):量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize
|
|||
|
|
(颜色数从 256 逐级往下试,取第一个能塞进原 csize 的)
|
|||
|
|
返回 (blob, how);不支持 / 压缩后仍超长时返回 (None, 原因)。
|
|||
|
|
"""
|
|||
|
|
h = img_header(stock_blob)
|
|||
|
|
if h is None:
|
|||
|
|
return None, '不是 LVGL bin'
|
|||
|
|
w, ht, cf, flags = h['w'], h['h'], h['cf'], h['flags']
|
|||
|
|
if slot_len is None:
|
|||
|
|
slot_len = len(stock_blob)
|
|||
|
|
out_img = img.convert('RGBA')
|
|||
|
|
if out_img.size != (w, ht):
|
|||
|
|
# 约定:**长宽比不一致的素材直接不替换**(保持原厂图),而不是拉伸或裁切 ——
|
|||
|
|
# 拉伸会把方块变长方形;裁切会切掉按钮两端。宁可保持原厂,也不给设备塞变形图。
|
|||
|
|
pw, ph = out_img.size
|
|||
|
|
if abs(pw / ph - w / ht) / (w / ht) > 0.005:
|
|||
|
|
return None, ('长宽比不一致(源 %dx%d = %.3f,槽位 %dx%d = %.3f)-> 按约定跳过,保持原厂'
|
|||
|
|
% (pw, ph, pw / ph, w, ht, w / ht))
|
|||
|
|
out_img = out_img.resize((w, ht), RESAMPLE)
|
|||
|
|
if cf == CF_ARGB8888 and not (flags & 0x08):
|
|||
|
|
blob = stock_blob[:12] + encode_argb8888(out_img)
|
|||
|
|
if len(blob) > slot_len:
|
|||
|
|
return None, 'ARGB 编码 %d > 槽位 %d' % (len(blob), slot_len)
|
|||
|
|
return blob + b'\x00' * (slot_len - len(blob)), 'ARGB(补 %d)' % (slot_len - len(blob))
|
|||
|
|
if cf == CF_I8 and (flags & 0x08):
|
|||
|
|
method, csize, usize = struct.unpack('<III', stock_blob[12:24])
|
|||
|
|
stride = h['stride'] or w
|
|||
|
|
best = None
|
|||
|
|
for n in (colors,) if colors else RLE_COLOR_STEPS:
|
|||
|
|
raw = raw_i8_strided(out_img, stride, n)
|
|||
|
|
comp = rle_compress(raw)
|
|||
|
|
# 末尾还要补一条「重复 1 次」指令(2 B):原厂流解出来正好是 usize 字节
|
|||
|
|
# (= 1024 + stride*h + 1),少这 1 字节设备端解码器可能直接判失败。
|
|||
|
|
if len(comp) + 2 <= csize:
|
|||
|
|
best = (n, raw, comp)
|
|||
|
|
break
|
|||
|
|
if best is None:
|
|||
|
|
n_last = RLE_COLOR_STEPS[-1]
|
|||
|
|
comp = rle_compress(raw_i8_strided(out_img, stride, n_last))
|
|||
|
|
return None, 'RLE 压缩后 %d > csize %d(已减到 %d 色)' % (len(comp), csize, n_last)
|
|||
|
|
n, raw, comp = best
|
|||
|
|
comp = comp + bytes((1, raw[-1])) # 把流做到正好 usize 字节,和原厂一致
|
|||
|
|
padded = pad_rle(comp, csize)
|
|||
|
|
if padded is None:
|
|||
|
|
return None, '补齐失败'
|
|||
|
|
blob = stock_blob[:12] + struct.pack('<III', 1, csize, usize) + padded
|
|||
|
|
if len(blob) != slot_len:
|
|||
|
|
return None, '组装后 %d != 槽位 %d' % (len(blob), slot_len)
|
|||
|
|
return blob, 'RLE %d色 %d/%d' % (n, len(comp), csize)
|
|||
|
|
return None, 'cf=0x%02x flags=%d 不支持' % (cf, flags)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def targets(r):
|
|||
|
|
"""Map a flat, file-name-safe key -> inode for every launcher icon.
|
|||
|
|
|
|||
|
|
alarm/launcher.bin -> alarm
|
|||
|
|
sports/training/launcher.bin -> sports_training
|
|||
|
|
phone/contacts_launcher.bin -> phone_contacts
|
|||
|
|
"""
|
|||
|
|
out = {}
|
|||
|
|
for p, i in r.entries():
|
|||
|
|
if not p.rsplit('/', 1)[-1].endswith('launcher.bin'):
|
|||
|
|
continue
|
|||
|
|
stem = p[:-4] # drop '.bin'
|
|||
|
|
if stem.endswith('_launcher'):
|
|||
|
|
stem = stem[:-len('_launcher')]
|
|||
|
|
elif stem.endswith('/launcher'):
|
|||
|
|
stem = stem[:-len('/launcher')]
|
|||
|
|
out[stem.replace('/', '_')] = i
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe(r, i):
|
|||
|
|
h = img_header(r.read(i))
|
|||
|
|
if not h:
|
|||
|
|
return '?', '?', '?'
|
|||
|
|
return f'{h["w"]}x{h["h"]}', FMT.get(h['cf'], f'0x{h["cf"]:02x}'), h
|
|||
|
|
|
|||
|
|
|
|||
|
|
def cmd_list(image):
|
|||
|
|
r = Romfs(image)
|
|||
|
|
print(f'{os.path.basename(image)} volume={r.volume} romfs_size={r.sb_size}')
|
|||
|
|
for app, i in sorted(targets(r).items()):
|
|||
|
|
dim, fmt, _ = describe(r, i)
|
|||
|
|
print(f' {app:<26} data@0x{i.data_off:08x} len={i.size:<7} {dim} {fmt}')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def cmd_make(image, pngdir, outdir, pad=False, write_patched=False):
|
|||
|
|
r = Romfs(image)
|
|||
|
|
tg = targets(r)
|
|||
|
|
os.makedirs(outdir, exist_ok=True)
|
|||
|
|
payload_dir = os.path.join(outdir, 'payload')
|
|||
|
|
os.makedirs(payload_dir, exist_ok=True)
|
|||
|
|
patch = bytearray()
|
|||
|
|
rollback = bytearray()
|
|||
|
|
records, skipped = [], []
|
|||
|
|
for fn in sorted(os.listdir(pngdir)):
|
|||
|
|
if not fn.lower().endswith('.png'):
|
|||
|
|
continue
|
|||
|
|
app = fn[:-4]
|
|||
|
|
if app not in tg:
|
|||
|
|
skipped.append((app, 'no such app'))
|
|||
|
|
continue
|
|||
|
|
ino = tg[app]
|
|||
|
|
orig = r.read(ino)
|
|||
|
|
_, fmt, h = describe(r, ino)
|
|||
|
|
if h is None:
|
|||
|
|
skipped.append((app, f'not LVGL image (len={ino.size})'))
|
|||
|
|
continue
|
|||
|
|
src = Image.open(os.path.join(pngdir, fn)).convert('RGBA')
|
|||
|
|
src_dim = f'{src.size[0]}x{src.size[1]}'
|
|||
|
|
resized = False
|
|||
|
|
if src.size != (h['w'], h['h']):
|
|||
|
|
if pad:
|
|||
|
|
tw, th = src.size
|
|||
|
|
blob = encode(src, h['cf'], tw, th, h['flags'], pad_to=len(orig))
|
|||
|
|
else:
|
|||
|
|
src = src.resize((h['w'], h['h']), RESAMPLE)
|
|||
|
|
tw, th = src.size
|
|||
|
|
resized = True
|
|||
|
|
blob = encode(src, h['cf'], tw, th, h['flags'])
|
|||
|
|
else:
|
|||
|
|
tw, th = src.size
|
|||
|
|
blob = encode(src, h['cf'], tw, th, h['flags'])
|
|||
|
|
if len(blob) != len(orig):
|
|||
|
|
skipped.append((app, f'length {len(blob)} != {len(orig)}'))
|
|||
|
|
continue
|
|||
|
|
patch += struct.pack('<II', ino.data_off, len(blob)) + blob
|
|||
|
|
rollback += struct.pack('<II', ino.data_off, len(orig)) + orig
|
|||
|
|
open(os.path.join(payload_dir, app.replace('/', '_') + '.bin'), 'wb').write(blob)
|
|||
|
|
records.append(dict(app=app, offset=ino.data_off, length=len(blob),
|
|||
|
|
format=fmt, stock_dim=f'{h["w"]}x{h["h"]}',
|
|||
|
|
source=fn, source_dim=src_dim,
|
|||
|
|
resized=resized, padded=bool(pad and src.size != (h['w'], h['h'])),
|
|||
|
|
sha256=hashlib.sha256(blob).hexdigest()))
|
|||
|
|
|
|||
|
|
open(os.path.join(outdir, 'icons.patch'), 'wb').write(patch)
|
|||
|
|
open(os.path.join(outdir, 'icons.rollback'), 'wb').write(rollback)
|
|||
|
|
manifest = dict(image=os.path.basename(image), volume=r.volume,
|
|||
|
|
image_sha256=hashlib.sha256(r.data).hexdigest(),
|
|||
|
|
replace_count=len(records), records=records,
|
|||
|
|
skipped=[{'app': a, 'reason': b} for a, b in skipped])
|
|||
|
|
json.dump(manifest, open(os.path.join(outdir, 'manifest.json'), 'w'),
|
|||
|
|
indent=1, ensure_ascii=False)
|
|||
|
|
|
|||
|
|
print(f'{"app":<26}{"offset":>12}{"bytes":>8} {"fmt":<10}{"stock":>9}{"source":>9} note')
|
|||
|
|
for m in records:
|
|||
|
|
note = 'resized' if m['resized'] else ('padded' if m['padded'] else '')
|
|||
|
|
print(f'{m["app"]:<26}0x{m["offset"]:08x}{m["length"]:>8} {m["format"]:<10}'
|
|||
|
|
f'{m["stock_dim"]:>9}{m["source_dim"]:>9} {note}')
|
|||
|
|
for a, why in skipped:
|
|||
|
|
print(f' SKIP {a}: {why}')
|
|||
|
|
print(f'\n{len(records)} replacement(s)')
|
|||
|
|
print(f' payload -> {payload_dir}')
|
|||
|
|
print(f' patch -> {os.path.join(outdir, "icons.patch")} ({len(patch)} bytes)')
|
|||
|
|
print(f' rollback -> {os.path.join(outdir, "icons.rollback")} ({len(rollback)} bytes)')
|
|||
|
|
print(f' manifest -> {os.path.join(outdir, "manifest.json")}')
|
|||
|
|
if write_patched:
|
|||
|
|
out = os.path.join(outdir, 'vela_app_patched.bin')
|
|||
|
|
cmd_apply(image, os.path.join(outdir, 'icons.patch'), out)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def cmd_apply(image, patch, out):
|
|||
|
|
data = bytearray(open(image, 'rb').read())
|
|||
|
|
blob = open(patch, 'rb').read()
|
|||
|
|
off = n = 0
|
|||
|
|
while off < len(blob):
|
|||
|
|
addr, ln = struct.unpack('<II', blob[off:off+8])
|
|||
|
|
chunk = blob[off+8:off+8+ln]
|
|||
|
|
assert len(chunk) == ln and addr + ln <= len(data), 'corrupt patch'
|
|||
|
|
data[addr:addr+ln] = chunk
|
|||
|
|
off += 8 + ln
|
|||
|
|
n += 1
|
|||
|
|
open(out, 'wb').write(data)
|
|||
|
|
print(f'applied {n} record(s) -> {out}')
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
a = sys.argv[1:]
|
|||
|
|
if not a:
|
|||
|
|
print(__doc__)
|
|||
|
|
elif a[0] == 'list':
|
|||
|
|
cmd_list(a[1])
|
|||
|
|
elif a[0] == 'make':
|
|||
|
|
cmd_make(a[1], a[2], a[3], '--pad' in a, '--patched' in a)
|
|||
|
|
elif a[0] == 'apply':
|
|||
|
|
cmd_apply(a[1], a[2], a[3])
|
|||
|
|
else:
|
|||
|
|
print(__doc__)
|