fix(tpager): update lock-screen branding

Signed-off-by: Pixel Perfect <me@pixp.cc>
This commit is contained in:
Pixel Perfect committed 2026-08-04 21:21:40 -07:00
1 parent ad6446b312
commit fb5a9d3bd3
2 files changed
+2389 -2173

No files matched your search

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Regenerate the T-Pager lock wallpaper from the shared lock artwork.
The Pager uses a native 480x222 wallpaper so LVGL does not enlarge and crop the
shared 320x240 image. This script preserves the Pager's existing background
and replaces its left-hand artwork with a deterministic transform of the shared
wallpaper. No source artwork or font rendering is recreated here.
"""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass
from pathlib import Path
PAGER_WIDTH = 480
PAGER_HEIGHT = 222
# These are the layout values used by the original native Pager composite.
ART_SCALE = 0.9
ART_OFFSET_X = -42.0
ART_OFFSET_Y = 26.0
# Clear both the old and replacement artwork before compositing. The right
# side of the Pager image is artwork-free and supplies its background pixels.
COMPOSITE_X_END = 212
COMPOSITE_Y_START = 65
PAGER_BACKGROUND_X = 280
# The shared wallpaper has clean background margins on both sides. RGB565
# quantization varies individual pixels, so average both margins per row before
# extracting the foreground delta.
SHARED_BACKGROUND_MARGIN = 40
BACKGROUND_NOISE_THRESHOLD = 7.0
@dataclass(frozen=True)
class Rgb565Image:
width: int
height: int
pixels: list[int]
def parse_rgb565_header(path: Path) -> Rgb565Image:
text = path.read_text(encoding="utf-8")
width_match = re.search(r"#define\s+\w+_W\s+(\d+)", text)
height_match = re.search(r"#define\s+\w+_H\s+(\d+)", text)
if not width_match or not height_match:
raise ValueError(f"could not find dimensions in {path}")
width = int(width_match.group(1))
height = int(height_match.group(1))
body = text[text.index("{") : text.rindex("}")]
pixels = [int(value, 16) for value in re.findall(r"0x([0-9A-Fa-f]{4})", body)]
if len(pixels) != width * height:
raise ValueError(
f"{path} contains {len(pixels)} pixels; expected {width * height}"
)
return Rgb565Image(width, height, pixels)
def rgb565_to_rgb(pixel: int) -> tuple[float, float, float]:
return (
((pixel >> 11) & 0x1F) * 255.0 / 31.0,
((pixel >> 5) & 0x3F) * 255.0 / 63.0,
(pixel & 0x1F) * 255.0 / 31.0,
)
def rgb_to_rgb565(rgb: tuple[float, float, float]) -> int:
red = round(max(0.0, min(255.0, rgb[0])) * 31.0 / 255.0)
green = round(max(0.0, min(255.0, rgb[1])) * 63.0 / 255.0)
blue = round(max(0.0, min(255.0, rgb[2])) * 31.0 / 255.0)
return (red << 11) | (green << 5) | blue
def shared_foreground_delta(
image: Rgb565Image,
) -> list[tuple[float, float, float]]:
rgb = [rgb565_to_rgb(pixel) for pixel in image.pixels]
margins = list(range(SHARED_BACKGROUND_MARGIN)) + list(
range(image.width - SHARED_BACKGROUND_MARGIN, image.width)
)
row_background: list[tuple[float, float, float]] = []
for y in range(image.height):
samples = [rgb[y * image.width + x] for x in margins]
row_background.append(
tuple(sum(sample[channel] for sample in samples) / len(samples) for channel in range(3))
)
delta: list[tuple[float, float, float]] = []
for y in range(image.height):
background = row_background[y]
for x in range(image.width):
pixel = rgb[y * image.width + x]
difference = tuple(pixel[channel] - background[channel] for channel in range(3))
if max(abs(channel) for channel in difference) < BACKGROUND_NOISE_THRESHOLD:
difference = (0.0, 0.0, 0.0)
delta.append(difference)
return delta
def bilinear_delta(
delta: list[tuple[float, float, float]],
width: int,
height: int,
x: float,
y: float,
) -> tuple[float, float, float]:
if x < 0.0 or x > width - 1 or y < 0.0 or y > height - 1:
return (0.0, 0.0, 0.0)
x0 = int(x)
y0 = int(y)
x1 = min(width - 1, x0 + 1)
y1 = min(height - 1, y0 + 1)
fx = x - x0
fy = y - y0
samples = (
delta[y0 * width + x0],
delta[y0 * width + x1],
delta[y1 * width + x0],
delta[y1 * width + x1],
)
return tuple(
(samples[0][channel] * (1.0 - fx) + samples[1][channel] * fx) * (1.0 - fy)
+ (samples[2][channel] * (1.0 - fx) + samples[3][channel] * fx) * fy
for channel in range(3)
)
def compose(shared: Rgb565Image, pager: Rgb565Image) -> list[int]:
if pager.width != PAGER_WIDTH or pager.height != PAGER_HEIGHT:
raise ValueError(
f"Pager template is {pager.width}x{pager.height}; expected "
f"{PAGER_WIDTH}x{PAGER_HEIGHT}"
)
shared_delta = shared_foreground_delta(shared)
pager_rgb = [rgb565_to_rgb(pixel) for pixel in pager.pixels]
output = list(pager.pixels)
clean_width = pager.width - PAGER_BACKGROUND_X
for y in range(COMPOSITE_Y_START, pager.height):
for x in range(COMPOSITE_X_END):
background = pager_rgb[
y * pager.width + PAGER_BACKGROUND_X + (x % clean_width)
]
source_x = (x - ART_OFFSET_X + 0.5) / ART_SCALE - 0.5
source_y = (y - ART_OFFSET_Y + 0.5) / ART_SCALE - 0.5
foreground = bilinear_delta(
shared_delta,
shared.width,
shared.height,
source_x,
source_y,
)
output[y * pager.width + x] = rgb_to_rgb565(
tuple(background[channel] + foreground[channel] for channel in range(3))
)
return output
def render_header(pixels: list[int]) -> str:
lines = [
"// Auto-generated by scripts/build/gen-lockscreen-wallpaper-pager.py.",
"// Native 480x222 T-LoRa Pager wallpaper derived from the shared",
"// WADAMESH RGB565 artwork while preserving the Pager-specific layout.",
"#pragma once",
"#include <stdint.h>",
"",
f"#define LOCKSCREEN_WALLPAPER_PAGER_W {PAGER_WIDTH}",
f"#define LOCKSCREEN_WALLPAPER_PAGER_H {PAGER_HEIGHT}",
"",
f"static const uint16_t lockscreen_wallpaper_pager_rgb565[{len(pixels)}] = {{",
]
for start in range(0, len(pixels), 16):
values = ",".join(f"0x{pixel:04x}" for pixel in pixels[start : start + 16])
lines.append(f" {values},")
lines.extend(("};", ""))
return "\n".join(lines)
def main() -> None:
repo_root = Path(__file__).resolve().parents[2]
default_assets = repo_root / "src" / "ui-touch" / "assets"
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--shared",
type=Path,
default=default_assets / "lockscreen_wallpaper_rgb565.h",
)
parser.add_argument(
"--pager",
type=Path,
default=default_assets / "lockscreen_wallpaper_pager_rgb565.h",
)
parser.add_argument(
"--check",
action="store_true",
help="fail instead of writing when the generated Pager header is stale",
)
args = parser.parse_args()
shared = parse_rgb565_header(args.shared)
pager = parse_rgb565_header(args.pager)
generated = render_header(compose(shared, pager))
current = args.pager.read_text(encoding="utf-8")
if args.check:
if current != generated:
raise SystemExit(f"{args.pager} is stale; regenerate it with this script")
return
args.pager.write_text(generated, encoding="utf-8")
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff