#!/usr/bin/env python3
"""Stand-in branding images until the Blender renders land.
Every image lives at the path its Blender render will eventually
overwrite, so swapping in real art is a file copy and nothing else.
Existing files are left alone unless --force is given; that way a
regeneration never clobbers a render that has already been dropped in.
tools/standins.py # write any missing stand-ins
tools/standins.py --force # rewrite all of them
tools/standins.py --check # verify every image exists at its size
--check uses only the standard library so CI can run it without numpy.
"""
import argparse
import struct
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
BACKGROUND = (0x65, 0x1D, 0x1D)
FOREGROUND = (0xFE, 0xF3, 0xC7)
# Keep in step with `backgrounds` in sddm/theme/theme.conf, the bg_*
# table at the top of plymouth/theme/neiam.script and `backgrounds` in
# splash/theme/contents/splash/Splash.qml. Plasma picks among the
# wallpaper's images by itself.
RESOLUTIONS = [
(1280, 800),
(1366, 768),
(1920, 1080),
(1920, 1200),
(2560, 1440),
(2560, 1600),
(3840, 2160),
]
TARGETS = [
ROOT / "plymouth/theme/background-{w}x{h}.png",
ROOT / "sddm/theme/backgrounds/{w}x{h}.png",
# Also the splash screen's background, which loads it from the
# installed wallpaper package.
ROOT / "wallpaper/theme/contents/images/{w}x{h}.png",
]
def expected():
for pattern in TARGETS:
for w, h in RESOLUTIONS:
yield Path(str(pattern).format(w=w, h=h)), (w, h)
def png_size(path):
with open(path, "rb") as f:
head = f.read(24)
if head[:8] != b"\x89PNG\r\n\x1a\n" or head[12:16] != b"IHDR":
return None
return struct.unpack(">II", head[16:24])
def check():
bad = 0
for path, size in expected():
rel = path.relative_to(ROOT)
if not path.exists():
print(f"missing: {rel}")
bad += 1
elif (actual := png_size(path)) != size:
print(f"wrong size: {rel} is {actual}, want {size}")
bad += 1
return bad
def moire(w, h):
import numpy as np
# Coordinates are in units of the image height, so every aspect
# ratio shows the same pattern at the same apparent scale; the
# 2x supersample keeps the thin rings from aliasing at 1366x768.
ss = 2
ys, xs = np.mgrid[0 : h * ss, 0 : w * ss].astype(np.float64)
xs = (xs + 0.5) / (h * ss)
ys = (ys + 0.5) / (h * ss)
px = 1.0 / h # one output pixel, in those units
# Two ring families whose centres sit a little apart: where their
# rings interleave the light doubles up, where they coincide it
# thins out, and that beat is the moire.
focus = np.array([0.66 * w / h, 0.46])
offset = np.array([0.035, 0.018])
period = 1 / 110
half_width = 0.9 * px
def rings(centre):
r = np.hypot(xs - centre[0], ys - centre[1])
d = np.abs(r - period * np.round(r / period))
return np.clip(1 - (d - half_width) / px, 0, 1)
a = rings(focus - offset / 2)
b = rings(focus + offset / 2)
light = 1 - (1 - a * 0.85) * (1 - b * 0.85)
# Fade the whole field out to the background tone away from the
# focus, with a smoothstep so there is no visible edge.
r = np.hypot(xs - focus[0], ys - focus[1]) / 0.95
t = np.clip(1 - r, 0, 1)
# Peaks at 60% so text laid over the focus still reads.
light *= 0.6 * t * t * (3 - 2 * t)
light = light.reshape(h, ss, w, ss).mean(axis=(1, 3))
bg = np.array(BACKGROUND, dtype=np.float64)
fg = np.array(FOREGROUND, dtype=np.float64)
rgb = bg + (fg - bg) * light[..., None]
return np.round(rgb).astype(np.uint8)
def generate(force):
from PIL import Image
cache = {}
for path, (w, h) in expected():
if path.exists() and not force:
continue
if (w, h) not in cache:
cache[(w, h)] = Image.fromarray(moire(w, h), "RGB")
path.parent.mkdir(parents=True, exist_ok=True)
cache[(w, h)].save(path, optimize=True)
print(f"wrote {path.relative_to(ROOT)}")
# A 1px swatch the plymouth script scales into its progress bar and
# password underline; plymouth can only draw images.
swatch = ROOT / "plymouth/theme/swatch.png"
if force or not swatch.exists():
Image.new("RGB", (1, 1), FOREGROUND).save(swatch)
print(f"wrote {swatch.relative_to(ROOT)}")
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--force", action="store_true", help="rewrite existing images")
mode.add_argument("--check", action="store_true", help="only verify images")
args = parser.parse_args()
if args.check:
sys.exit(1 if check() else 0)
generate(args.force)
if __name__ == "__main__":
main()
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.8 KiB · text
History
0fc5cce