irongit

Git hosting and a container registry in one Rust binary (axum + Astro)

Logo: silver ingot (transparent PNG source); generator accepts transparent sources

huncholanehuncholaneauthored
parent 0d24f1bcommit 68235305f8c97cd898b0c4f5fc0209122374362dBrowse files

10 files changed, +40 -11

+1-1backend/src/web/ui.rs
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
44 use maud::{Markup, PreEscaped, html};
55 use sha2::{Digest, Sha256};
66
7-/// The stacked-bars mark (frontend/public/logo-128.png, built by
7+/// The ingot mark (frontend/public/logo-128.png, built by
88 /// scripts/gen-brand.py), sharp up to about 40 px on 3x screens.
99 pub fn logo(size: u32) -> Markup {
1010 html! {
+0-0brand/logo-source.jpg

Binary file changed.

+0-0brand/logo-source.png

Binary file changed.

+0-0frontend/public/apple-touch-icon.png

Binary file changed.

+0-0frontend/public/favicon-32.png

Binary file changed.

+0-0frontend/public/favicon.ico

Binary file changed.

+0-0frontend/public/icon-192.png

Binary file changed.

+0-0frontend/public/logo-128.png

Binary file changed.

+0-0frontend/public/logo.png

Binary file changed.

+39-10scripts/gen-brand.py
@@ -1,10 +1,14 @@
11 #!/usr/bin/env python3
2-"""Builds the irongit icons from brand/logo-source.jpg (the stacked bars on a
3-flat green background).
2+"""Builds the irongit icons from the logo in brand/.
43
5-The background is keyed out by how green each pixel is; edge pixels are
6-unmixed (P = a*F + (1-a)*G solved for a and F) so the outline keeps its real
7-colour instead of a green fringe. Output goes to frontend/public:
4+brand/logo-source.png is used as is when it already has a transparent
5+background; near-opaque pixels are snapped to fully opaque so the mark never
6+lets a background tint through. A source on a flat green screen (PNG without
7+alpha, or brand/logo-source.jpg) is keyed instead: the green is removed by how
8+green each pixel is, and edge pixels are unmixed (P = a*F + (1-a)*G solved for
9+a and F) so outlines keep their real colour instead of a green fringe.
10+
11+Output goes to frontend/public:
812
913 logo.png 512 px, transparent, for pages and Open Graph
1014 logo-128.png the nav and auth pages (crisp up to 3x at ~40 px)
@@ -23,7 +27,7 @@ import numpy as np
2327 from PIL import Image
2428
2529 ROOT = Path(__file__).resolve().parent.parent
26-SOURCE = ROOT / "brand" / "logo-source.jpg"
30+BRAND = ROOT / "brand"
2731 OUT = ROOT / "frontend" / "public"
2832 SURFACE = (11, 15, 22) # --color-surface, dark theme
2933
@@ -53,9 +57,24 @@ def key_out_green(path: Path) -> Image.Image:
5357 fg[..., 1] = np.minimum(fg[..., 1], spill_cap)
5458
5559 rgba = np.dstack([fg, alpha * 255]).round().astype(np.uint8)
56- image = Image.fromarray(rgba, "RGBA")
60+ print(f"keyed out background {bg.round().astype(int).tolist()}")
61+ return Image.fromarray(rgba, "RGBA")
62+
63+
64+def load_transparent(path: Path) -> Image.Image:
65+ rgba = np.asarray(Image.open(path).convert("RGBA")).copy()
66+ alpha = rgba[..., 3]
67+ # Background removers leave the body at 250-254; make it solid.
68+ alpha[alpha >= 245] = 255
69+ rgba[..., 3] = alpha
70+ rgba[alpha == 0, :3] = 0
71+ print(f"transparent source, {int((alpha == 255).sum())} opaque pixels")
72+ return Image.fromarray(rgba, "RGBA")
5773
58- # Crop to the ink and square it up with transparent padding.
74+
75+def squared(image: Image.Image) -> Image.Image:
76+ """Crops to the ink and squares it up with transparent padding."""
77+ alpha = np.asarray(image)[..., 3] / 255.0
5978 ys, xs = np.where(alpha > 0.5)
6079 x0, x1, y0, y1 = xs.min(), xs.max() + 1, ys.min(), ys.max() + 1
6180 image = image.crop((x0, y0, x1, y1))
@@ -63,7 +82,7 @@ def key_out_green(path: Path) -> Image.Image:
6382 pad = round(side * 0.02)
6483 square = Image.new("RGBA", (side + 2 * pad, side + 2 * pad), (0, 0, 0, 0))
6584 square.paste(image, ((square.width - image.width) // 2, (square.height - image.height) // 2), image)
66- print(f"background {bg.round().astype(int).tolist()}, artwork {x1 - x0}x{y1 - y0}, square {square.width}px")
85+ print(f"artwork {x1 - x0}x{y1 - y0}, square {square.width}px")
6786 return square
6887
6988
@@ -82,8 +101,18 @@ def resized(image: Image.Image, size: int) -> Image.Image:
82101 return despill(image.resize((size, size), Image.Resampling.LANCZOS))
83102
84103
104+def load_master() -> Image.Image:
105+ png, jpg = BRAND / "logo-source.png", BRAND / "logo-source.jpg"
106+ if png.exists():
107+ image = Image.open(png)
108+ if image.mode in ("RGBA", "LA") and np.asarray(image.convert("RGBA"))[0, 0, 3] == 0:
109+ return squared(load_transparent(png))
110+ return squared(key_out_green(png))
111+ return squared(key_out_green(jpg))
112+
113+
85114 def main() -> int:
86- master = key_out_green(SOURCE)
115+ master = load_master()
87116 OUT.mkdir(parents=True, exist_ok=True)
88117 resized(master, 512).save(OUT / "logo.png", optimize=True)
89118 resized(master, 128).save(OUT / "logo-128.png", optimize=True)