Files
subhobhai943andClaude Opus 4.8 cf7bf59481 feat(brand): official SUB-OS logo -- add "OS" to the dragon mark
Take the circular SUB dragon logo and lock up an "OS" wordmark beneath
the "SUB" letters, in the dragon's accent blue (#08B3FA) with the same
oblique cut and black keyline, so the mark reads "SUB-OS" as one unit.
Store the artwork in assets/ as the canonical source and regenerate the
boot splash asset (init/logo_data.c) from it; the "OS" stays legible at
the 160x160 boot resolution. Point mklogo.py's example at the new source.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
2026-08-30 21:04:00 +05:30

155 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate the SUB-OS boot logo asset from a source image.
Emits init/logo_data.c: a 4-bit palette-indexed bitmap plus the ARGB palette
and an ASCII-art rendition used before the framebuffer is up.
python3 scripts/mklogo.py assets/logo_SUB_OS.png
Re-run this whenever the source artwork changes; init/logo_data.c is generated
and should not be hand-edited.
"""
import sys
import os
from PIL import Image
SIZE = 160 # Bitmap edge length in pixels
COLORS = 16 # Palette entries, one nibble per pixel
BACKGROUND = (11, 19, 43) # 0xFF0B132B, the desktop wallpaper's top gradient stop
ASCII_W = 46 # Text-mode logo width in characters
ASCII_RAMP = " .:-=+*#%@"
def build_bitmap(path):
img = Image.open(path).convert("RGBA")
# Composite onto the wallpaper colour so the logo's transparent surround
# blends into the splash instead of turning black.
canvas = Image.new("RGB", img.size, BACKGROUND)
canvas.paste(img, (0, 0), img)
canvas = canvas.resize((SIZE, SIZE), Image.LANCZOS)
# Dithering would scatter isolated palette indices through flat areas and
# look like noise at this size, so quantize with it disabled.
quant = canvas.quantize(colors=COLORS, dither=Image.NONE)
raw_palette = quant.getpalette()[: COLORS * 3]
palette = [tuple(raw_palette[i * 3 : i * 3 + 3]) for i in range(COLORS)]
indices = list(quant.getdata())
return palette, indices
def build_ascii(path):
img = Image.open(path).convert("RGBA")
canvas = Image.new("RGB", img.size, BACKGROUND)
canvas.paste(img, (0, 0), img)
# Console cells are about twice as tall as they are wide.
height = max(1, ASCII_W // 2)
small = canvas.convert("L").resize((ASCII_W, height), Image.LANCZOS)
pixels = list(small.getdata())
lo, hi = min(pixels), max(pixels)
span = max(1, hi - lo)
rows = []
for y in range(height):
row = []
for x in range(ASCII_W):
level = (pixels[y * ASCII_W + x] - lo) * (len(ASCII_RAMP) - 1) // span
row.append(ASCII_RAMP[level])
rows.append("".join(row).rstrip())
while rows and not rows[0]:
rows.pop(0)
while rows and not rows[-1]:
rows.pop()
return rows
def transparent_mask(palette):
"""Bitmask of palette entries close enough to the background to skip.
The source art has a transparent surround, which the compositing step turns
into the background colour. Quantization then spreads that flat area across
several near-identical entries, and drawing them would stamp a visible
rectangle over whatever the logo sits on. Marking them lets the renderer
skip those pixels so only the logo itself is painted.
"""
mask = 0
br, bg, bb = BACKGROUND
for i, (r, g, b) in enumerate(palette):
if abs(r - br) + abs(g - bg) + abs(b - bb) <= 12:
mask |= 1 << i
return mask
def emit(path, palette, indices, ascii_rows, out_path):
packed = []
for i in range(0, len(indices), 2):
hi = indices[i] & 0x0F
lo = indices[i + 1] & 0x0F if i + 1 < len(indices) else 0
packed.append((hi << 4) | lo)
lines = []
lines.append("// Generated by scripts/mklogo.py -- do not edit by hand.")
lines.append("// Source artwork: %s" % os.path.basename(path))
lines.append("")
lines.append('#include <init/bootlogo.h>')
lines.append("")
lines.append("const int boot_logo_width = %d;" % SIZE)
lines.append("const int boot_logo_height = %d;" % SIZE)
lines.append("")
lines.append("// Palette entries that are background rather than artwork; the renderer")
lines.append("// skips these so the logo composites onto whatever is behind it.")
lines.append("const uint16_t boot_logo_transparent_mask = 0x%04X;"
% transparent_mask(palette))
lines.append("")
lines.append("// 16-entry ARGB palette; index 0 is the wallpaper background.")
lines.append("const uint32_t boot_logo_palette[%d] = {" % COLORS)
for r, g, b in palette:
lines.append(" 0xFF%02X%02X%02X," % (r, g, b))
lines.append("};")
lines.append("")
lines.append("// %dx%d pixels, two 4-bit indices per byte, row major." % (SIZE, SIZE))
lines.append("const uint8_t boot_logo_bitmap[%d] = {" % len(packed))
for i in range(0, len(packed), 16):
chunk = ", ".join("0x%02X" % b for b in packed[i : i + 16])
lines.append(" %s," % chunk)
lines.append("};")
lines.append("")
lines.append("// ASCII rendition for the text console, before the framebuffer exists.")
lines.append("const char* const boot_logo_ascii[] = {")
for row in ascii_rows:
lines.append(' "%s",' % row.replace("\\", "\\\\").replace('"', '\\"'))
lines.append(" NULL")
lines.append("};")
lines.append("")
with open(out_path, "w") as fh:
fh.write("\n".join(lines))
return len(packed), len(ascii_rows)
def main():
if len(sys.argv) < 2:
print(__doc__)
return 1
src = os.path.expanduser(sys.argv[1])
out = sys.argv[2] if len(sys.argv) > 2 else "init/logo_data.c"
palette, indices = build_bitmap(src)
ascii_rows = build_ascii(src)
nbytes, nrows = emit(src, palette, indices, ascii_rows, out)
print("%s: %dx%d bitmap, %d palette entries, %d bytes packed, %d ASCII rows"
% (out, SIZE, SIZE, len(palette), nbytes, nrows))
return 0
if __name__ == "__main__":
sys.exit(main())