4.6 KiB · text History e74691a
#!/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 and the
# bg_* table at the top of plymouth/theme/neiam.script.
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",
]


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()