Git hosting and a container registry in one Rust binary (axum + Astro)
Logo: stacked bars with the green keyed out; despill after resizing so edges stay clean
10 files changed, +19 -5
+1-1backend/src/web/ui.rs
| @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; | ||
| 4 | 4 | use maud::{Markup, PreEscaped, html}; |
| 5 | 5 | use sha2::{Digest, Sha256}; |
| 6 | 6 | |
| 7 | -/// The vault-door mark (frontend/public/logo-128.png, built by | |
| 7 | +/// The stacked-bars mark (frontend/public/logo-128.png, built by | |
| 8 | 8 | /// scripts/gen-brand.py), sharp up to about 40 px on 3x screens. |
| 9 | 9 | pub fn logo(size: u32) -> Markup { |
| 10 | 10 | html! { |
+0-0brand/logo-source.jpg
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.
+0-0scripts/__pycache__/gen-brand.cpython-313.pyc
Binary file changed.
+18-4scripts/gen-brand.py
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | #!/usr/bin/env python3 |
| 2 | -"""Builds the irongit icons from brand/logo-source.jpg (the vault door on a | |
| 2 | +"""Builds the irongit icons from brand/logo-source.jpg (the stacked bars on a | |
| 3 | 3 | flat green background). |
| 4 | 4 | |
| 5 | 5 | The background is keyed out by how green each pixel is; edge pixels are |
| @@ -47,7 +47,8 @@ def key_out_green(path: Path) -> Image.Image: | ||
| 47 | 47 | a = alpha[..., None] |
| 48 | 48 | fg = np.where(a > 0.02, (rgb - (1.0 - a) * bg) / np.maximum(a, 1e-3), 0.0) |
| 49 | 49 | fg = np.clip(fg, 0, 255) |
| 50 | - # The artwork is neutral; remove any green spill left in the edge colours. | |
| 50 | + # The artwork is neutral greys and gold (red above green); remove any green | |
| 51 | + # spill left in the edge colours. | |
| 51 | 52 | spill_cap = np.maximum(fg[..., 0], fg[..., 2]) + 3 |
| 52 | 53 | fg[..., 1] = np.minimum(fg[..., 1], spill_cap) |
| 53 | 54 | |
| @@ -66,8 +67,19 @@ def key_out_green(path: Path) -> Image.Image: | ||
| 66 | 67 | return square |
| 67 | 68 | |
| 68 | 69 | |
| 70 | +def despill(image: Image.Image) -> Image.Image: | |
| 71 | + """Caps green at the larger of red and blue. Neutral greys and gold | |
| 72 | + (red above green) are untouched; only spill and resampling tint goes.""" | |
| 73 | + px = np.asarray(image).copy() | |
| 74 | + cap = np.maximum(px[..., 0], px[..., 2]).astype(np.int16) + 3 | |
| 75 | + px[..., 1] = np.minimum(px[..., 1], np.clip(cap, 0, 255)).astype(np.uint8) | |
| 76 | + return Image.fromarray(px, image.mode) | |
| 77 | + | |
| 78 | + | |
| 69 | 79 | def resized(image: Image.Image, size: int) -> Image.Image: |
| 70 | - return image.resize((size, size), Image.Resampling.LANCZOS) | |
| 80 | + # Lanczos overshoots on the faint edge pixels beside the gold rims and | |
| 81 | + # tints them olive, so the spill cap runs again after every resize. | |
| 82 | + return despill(image.resize((size, size), Image.Resampling.LANCZOS)) | |
| 71 | 83 | |
| 72 | 84 | |
| 73 | 85 | def main() -> int: |
| @@ -83,7 +95,9 @@ def main() -> int: | ||
| 83 | 95 | touch.paste(inner, (14, 14), inner) |
| 84 | 96 | touch.convert("RGB").save(OUT / "apple-touch-icon.png", optimize=True) |
| 85 | 97 | |
| 86 | - resized(master, 256).save(OUT / "favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)]) | |
| 98 | + # Each favicon size is resized (and despilled) here rather than by the ICO writer. | |
| 99 | + icons = [resized(master, n) for n in (48, 32, 16)] | |
| 100 | + icons[0].save(OUT / "favicon.ico", sizes=[(48, 48), (32, 32), (16, 16)], append_images=icons[1:]) | |
| 87 | 101 | for name in ["logo.png", "logo-128.png", "icon-192.png", "favicon-32.png", "apple-touch-icon.png", "favicon.ico"]: |
| 88 | 102 | print(f" {name:22} {(OUT / name).stat().st_size:>7} bytes") |
| 89 | 103 | return 0 |