99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageChops
|
|
|
|
|
|
ICON_SIZES = (16, 20, 24, 32, 40, 48, 64, 128, 256)
|
|
|
|
|
|
def remove_green_background(image: Image.Image) -> Image.Image:
|
|
rgba = image.convert("RGBA")
|
|
pixels = rgba.load()
|
|
width, height = rgba.size
|
|
|
|
for y in range(height):
|
|
for x in range(width):
|
|
r, g, b, a = pixels[x, y]
|
|
green_score = g - max(r, b)
|
|
is_key = g > 90 and green_score > 35
|
|
is_fringe = g > 32 and g > r * 1.35 and g > b * 1.20
|
|
if is_key or is_fringe:
|
|
edge = max(0, min(255, (green_score - 18) * 5))
|
|
alpha = max(0, 255 - edge)
|
|
if alpha < 56:
|
|
pixels[x, y] = (0, 0, 0, 0)
|
|
else:
|
|
despilled_g = min(g, max(r, b))
|
|
pixels[x, y] = (r, despilled_g, b, min(a, alpha))
|
|
|
|
return rgba
|
|
|
|
|
|
def crop_to_alpha(image: Image.Image, padding_ratio: float) -> Image.Image:
|
|
alpha = image.getchannel("A")
|
|
bbox = alpha.getbbox()
|
|
if not bbox:
|
|
raise ValueError("source image has no visible pixels after background removal")
|
|
|
|
cropped = image.crop(bbox)
|
|
side = max(cropped.size)
|
|
padding = round(side * padding_ratio)
|
|
canvas_side = side + padding * 2
|
|
canvas = Image.new("RGBA", (canvas_side, canvas_side), (0, 0, 0, 0))
|
|
canvas.alpha_composite(cropped, ((canvas_side - cropped.width) // 2, (canvas_side - cropped.height) // 2))
|
|
return canvas
|
|
|
|
|
|
def trim_transparent_padding(image: Image.Image) -> Image.Image:
|
|
empty = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
|
bbox = ImageChops.difference(image, empty).getbbox()
|
|
if bbox is None:
|
|
return image
|
|
return image.crop(bbox)
|
|
|
|
|
|
def build_icons(source: Path, out_dir: Path, padding_ratio: float) -> None:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
source_image = Image.open(source)
|
|
transparent = remove_green_background(source_image)
|
|
master = crop_to_alpha(transparent, padding_ratio)
|
|
master = trim_transparent_padding(master)
|
|
|
|
master_path = out_dir / "tray-icon.png"
|
|
master.save(master_path)
|
|
|
|
resized_images: list[Image.Image] = []
|
|
for size in ICON_SIZES:
|
|
resized = master.resize((size, size), Image.Resampling.LANCZOS)
|
|
resized.save(out_dir / f"tray-icon-{size}.png")
|
|
resized_images.append(resized)
|
|
|
|
ico_path = out_dir / "tray.ico"
|
|
resized_images[-1].save(
|
|
ico_path,
|
|
format="ICO",
|
|
sizes=[(size, size) for size in ICON_SIZES],
|
|
append_images=resized_images[:-1],
|
|
)
|
|
|
|
print(f"wrote {master_path}")
|
|
print(f"wrote {ico_path}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Build u-desktop tray icon assets from a generated source image.")
|
|
parser.add_argument("--source", type=Path, required=True, help="Generated source PNG on a green chroma-key background.")
|
|
parser.add_argument("--out-dir", type=Path, default=Path("assets/icons"), help="Directory for PNG sizes and tray.ico.")
|
|
parser.add_argument("--padding", type=float, default=0.04, help="Transparent padding ratio around the cropped icon.")
|
|
args = parser.parse_args()
|
|
|
|
build_icons(args.source, args.out_dir, args.padding)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|