Files
Omar 41e4f35897 Automate WinUtil title screen generation (#4965)
* feat: add title screen generation tooling

Add local automation for capturing WinUtil in Light and Dark themes
and generating the composite title-screen image.

* ci: automate title screen updates

Add a manual Windows workflow that compiles WinUtil, generates the
Light and Dark composite, and opens an image-only pull request.

* docs: update AGENTS.md and SPEC.md with title-screen generation details

* fix: address review comments

Tested image is correct, SHA-256 hash is identical to the previously verified output.

* fix: deselect bitmap before reading capture pixels
2026-09-02 16:10:44 -05:00

181 lines
5.7 KiB
Python

"""Create WinUtil's framed diagonal Light and Dark title-screen image.
WinUtil captures include a narrow near-black DWM shadow around the actual WPF
window. The compositor detects that shadow from the Dark capture, applies the
same crop to both themes, and blends them with a supersampled diagonal mask.
Only the final composite is written; the cropped and framed theme images remain
in memory.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image, ImageDraw
BORDER_COLOR = (180, 180, 180, 255)
BORDER_WIDTH = 2
# DWM shadow pixels are nearly black. WinUtil's actual Dark theme remains above
# this threshold at the window edges used by the bounds scan.
BRIGHTNESS_THRESHOLD = 20
LARGE_SHADOW_MARGIN = 30
MASK_SCALE = 4
# These endpoints keep the split clear of the left tab labels near the top and
# the right-side action controls near the bottom of the Tweaks layout.
TOP_SPLIT = 0.16
BOTTOM_SPLIT = 0.86
def _contains_content(pixel) -> bool:
"""Return whether a pixel is brighter than the near-black DWM shadow."""
return any(channel > BRIGHTNESS_THRESHOLD for channel in pixel[:3])
def _find_content_bounds(image: Image.Image) -> tuple[int, int, int, int]:
"""Return the non-shadow bounds detected from a Dark theme capture.
Every edge coordinate is checked because Windows 11 shadows can be narrower
than ten pixels. Sampling at a fixed interval could skip the first content
pixel and trim real UI from the final image.
"""
width, height = image.size
pixels = image.load()
left = next(
(
x
for x in range(width)
if any(_contains_content(pixels[x, y]) for y in range(height))
),
0,
)
right = next(
(
x
for x in range(width - 1, -1, -1)
if any(_contains_content(pixels[x, y]) for y in range(height))
),
width - 1,
)
top = next(
(
y
for y in range(height)
if any(_contains_content(pixels[x, y]) for x in range(width))
),
0,
)
bottom = next(
(
y
for y in range(height - 1, -1, -1)
if any(_contains_content(pixels[x, y]) for x in range(width))
),
height - 1,
)
right_margin = width - right - 1
bottom_margin = height - bottom - 1
margins = (left, top, right_margin, bottom_margin)
if any(margin > LARGE_SHADOW_MARGIN for margin in margins):
print(
"Warning: unusually large shadow margins detected "
f"(left={left}, top={top}, right={right_margin}, "
f"bottom={bottom_margin}). Input images may already be cropped or "
"framed."
)
return left, top, right + 1, bottom + 1
def _apply_frame(image: Image.Image) -> Image.Image:
"""Return a copy with the title-screen border drawn inside its bounds."""
framed = image.copy()
width, height = framed.size
draw = ImageDraw.Draw(framed)
for inset in range(BORDER_WIDTH):
draw.rectangle(
[inset, inset, width - 1 - inset, height - 1 - inset],
outline=BORDER_COLOR,
)
return framed
def create_composite(
dark_path: str | Path,
light_path: str | Path,
output_path: str | Path,
) -> Path:
"""Create one framed diagonal composite from matching theme captures."""
dark_path = Path(dark_path)
light_path = Path(light_path)
output_path = Path(output_path)
with Image.open(dark_path) as dark_source:
dark_image = dark_source.convert("RGBA")
with Image.open(light_path) as light_source:
light_image = light_source.convert("RGBA")
if dark_image.size != light_image.size:
raise ValueError(
"Image dimensions do not match: "
f"Dark is {dark_image.size}, Light is {light_image.size}"
)
crop_box = _find_content_bounds(dark_image)
print(f"Content crop box (stripping DWM shadow): {crop_box}")
dark_framed = _apply_frame(dark_image.crop(crop_box))
light_framed = _apply_frame(light_image.crop(crop_box))
width, height = dark_framed.size
# Drawing the mask at four times the target resolution and downsampling it
# with Lanczos produces a smooth diagonal without softening either UI image.
mask_width = width * MASK_SCALE
mask_height = height * MASK_SCALE
mask_high_resolution = Image.new("L", (mask_width, mask_height), 0)
draw_mask = ImageDraw.Draw(mask_high_resolution)
draw_mask.polygon(
[
(0, 0),
(TOP_SPLIT * mask_width, 0),
(BOTTOM_SPLIT * mask_width, mask_height),
(0, mask_height),
],
fill=255,
)
mask = mask_high_resolution.resize(
(width, height),
resample=Image.Resampling.LANCZOS,
)
# A white mask selects Light, so Light occupies the left side and Dark the
# right. Reapplying the frame removes any mask antialiasing at the outer edge.
composite = Image.composite(light_framed, dark_framed, mask)
composite = _apply_frame(composite)
composite.save(output_path, "PNG")
print(f"Generated title-screen composite: {output_path} ({width}x{height})")
return output_path
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Create WinUtil's diagonal Light and Dark title-screen image."
)
parser.add_argument("dark", type=Path, help="Raw Dark theme PNG")
parser.add_argument("light", type=Path, help="Raw Light theme PNG")
parser.add_argument("output", type=Path, help="Destination composite PNG")
return parser.parse_args()
def main() -> None:
args = _parse_args()
create_composite(args.dark, args.light, args.output)
if __name__ == "__main__":
main()