tool/render_brand.py
Rama main · réplica de lectura del repositorio original.
# /// script
# dependencies = ["pillow"]
# ///
"""Render brand raster sources from the approved SVG artwork."""
import copy
import io
import math
from pathlib import Path
import shutil
import subprocess
import xml.etree.ElementTree as ET
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
BRAND = ROOT / "design/brand"
NS = "{http://www.w3.org/2000/svg}"
ET.register_namespace("", NS[1:-1])
def svg(name):
return ET.parse(BRAND / name).getroot()
def render(art, size, height=None):
result = subprocess.run(
["rsvg-convert", "--width", str(size), "--height", str(height or size)],
input=ET.tostring(art), capture_output=True, check=True,
)
return Image.open(io.BytesIO(result.stdout)).convert("RGBA")
def save(path, image):
image.save(ROOT / path, compress_level=9)
print(path, f"{image.width}x{image.height}")
def fitted(art, box):
art = copy.deepcopy(art)
left, top, right, bottom = render(art, 1024).getchannel("A").getbbox()
x, y, width, height = box
scale = min(width / (right - left), height / (bottom - top))
tx = x + (width - (right - left) * scale) / 2 - left * scale
ty = y + (height - (bottom - top) * scale) / 2 - top * scale
group = ET.Element(NS + "g", {"transform": f"translate({tx} {ty}) scale({scale})"})
for child in list(art):
if child.tag != NS + "defs":
art.remove(child)
group.append(child)
art.append(group)
art.set("viewBox", "0 0 1024 1024")
return art
def greyscale_shade(image):
alpha = image.getchannel("A")
shade = image.convert("RGB").convert("L")
brightest = max(value for value, count in enumerate(shade.histogram(mask=alpha)) if count)
shade = shade.point(lambda value: round(255 * .88 + .12 * value * 255 / brightest))
return Image.merge("RGBA", (shade, shade, shade, alpha))
def social_card():
card = Image.new("RGBA", (1280, 640))
pixels = card.load()
for y in range(640):
for x in range(1280):
if y >= 632:
amount = (y - 632) / 7
start, end = (124, 123, 255), (51, 214, 255)
else:
amount = min(1, math.hypot(x - 640, y - 320) / 640)
start, end = (26, 29, 74), (10, 12, 24)
pixels[x, y] = tuple(int(a + (b - a) * amount)
for a, b in zip(start, end)) + (255,)
art = fitted(svg("tsumiru-mark.svg"), (193, 210, 247, 227))
art.set("viewBox", "0 0 1280 640")
for value, box, size, weight, colour in [
("Tsumiru", (563, 246, 406, 81), 110, "700", "#eef0fb"),
("Your Suwayomi library, everywhere", (567, 367, 568, 33),
36, "400", "#9aa0c4"),
]:
text = ET.Element(NS + "text", {
"x": "0", "y": "128", "font-family": "Adwaita Sans",
"font-size": str(size), "font-weight": weight, "fill": colour,
})
text.text = value
probe = ET.Element(NS + "svg", {"viewBox": "0 0 1280 640"})
probe.append(copy.deepcopy(text))
left, top, right, bottom = render(probe, 1280, 640).getchannel("A").getbbox()
x, y, width, height = box
sx, sy = width / (right - left), height / (bottom - top)
text.set("transform", f"translate({x - left * sx} {y - top * sy}) scale({sx} {sy})")
art.append(text)
card.alpha_composite(render(art, 1280, 640))
save("design/brand/tsumiru-social-card.png", card)
def main():
if shutil.which("rsvg-convert") is None:
raise SystemExit("rsvg-convert is required; install librsvg to render brand assets.")
social_card()
mark = fitted(svg("tsumiru-mark.svg"), (192, 216, 694, 638))
light = fitted(svg("tsumiru-mark-on-light.svg"), (192, 216, 696, 640))
round_icon = svg("tsumiru-icon.svg")
fullbleed = svg("tsumiru-icon-fullbleed.svg")
for path, art, size in [
("design/brand/tsumiru-mark-transparent.png", mark, 1024),
("design/brand/tsumiru-icon-fullbleed.png", fullbleed, 1254),
("design/brand/tsumiru-org-avatar.png", round_icon, 512),
("assets/icons/launcher/tsumiru_icon.png", fullbleed, 1024),
("assets/icons/launcher/tsumiru_icon_rounded.png", round_icon, 1024),
("assets/icons/launcher/splash_icon.png", mark, 1024),
("assets/icons/dark_icon.png", mark, 512),
("assets/icons/logo_on_light.png", light, 512),
("flatpak/app.tsumiru.Tsumiru.png", round_icon, 512),
("flatpak/io.github.aaronbamblett.tsumiru.png", round_icon, 512),
]:
save(path, render(art, size))
desktop = Image.new("RGBA", (1024, 1024))
desktop.alpha_composite(render(round_icon, 826), (99, 99))
save("assets/icons/launcher/tsumiru_icon_desktop.png", desktop)
background = copy.deepcopy(fullbleed)
background.remove(background.find(NS + "g"))
save("assets/icons/launcher/tsumiru_icon_bg.png", render(background, 1024))
foreground = copy.deepcopy(fullbleed)
foreground.remove(foreground.find(NS + "rect"))
scale = 66 / 108 # Android adaptive icon safe zone: 66dp of the 108dp layer
group = foreground.find(NS + "g")
wrapper = ET.Element(NS + "g", {
"transform": f"translate(256 256) scale({scale}) translate(-256 -256)"
})
foreground.remove(group)
wrapper.append(group)
foreground.append(wrapper)
save("assets/icons/launcher/tsumiru_icon_fg.png", render(foreground, 1024))
for art, prefix, size in [(mark, "logo", 512), (light, "logo_on_light", 512)]:
for name in ("foreground", "shadow"):
isolated = copy.deepcopy(art)
isolated_group = isolated.find(NS + "g")
for child in list(isolated_group):
if child.get("id") != name:
isolated_group.remove(child)
image = render(isolated, size)
if name == "shadow":
image = greyscale_shade(image)
save(f"assets/icons/{prefix}_{name}.png", image)
silhouette = svg("tsumiru-mark.svg")
for group in silhouette.findall(NS + "g"):
group.set("fill", "#ffffff")
silhouette = fitted(silhouette, (85, 117, 864, 789))
for density, size in [("mdpi", 24), ("hdpi", 36), ("xhdpi", 48),
("xxhdpi", 72), ("xxxhdpi", 96)]:
save(f"android/app/src/main/res/drawable-{density}/ic_stat_tsumiru.png",
render(silhouette, size))
if __name__ == "__main__":
main()