build / build (push) Successful in 38m26s
- das-shell 0.1.0: the Das two-line bash prompt by default (replaces the PS1 of Void's skel .bashrc once), dprompt (Textual prompt builder with presets and live preview) and dbanner (SSH login banner editor: canvas tools, fields with values, widgets, das-banner service for cached ones, Das OS logo and tryzub pictures). Python tests in make test. - das-server 0.3.0 depends on das-shell; the server installer mentions dprompt and dbanner; the desktop image includes it. - run-qemu.sh forwards the guest's SSH to localhost:2222. - Hardware fixes: Ventoy menu icon, boot menu titles "Das OS <version>". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.4 KiB
Python
Executable File
102 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Turns an SVG into a dbanner picture: half-block cells (▀ ▄ █) in ANSI colors.
|
|
|
|
tools/art2cells.py SVG WIDTH_PX HEIGHT_PX NAME "UK title" "EN title" \
|
|
"#rrggbb=ansi_name" ... [--mirror] > srcpkgs/das-shell/files/share/banner/art/NAME.json
|
|
|
|
Each output cell holds two square pixels, so HEIGHT_PX must be even. The SVG is
|
|
drawn 8 times larger by ImageMagick, and every pixel takes the color that
|
|
covers most of its 8x8 block (or stays empty). Colors are matched to the
|
|
nearest one given on the command line; --mirror copies the left half to the
|
|
right for symmetric pictures. Needs ImageMagick and Pillow; this is a
|
|
developer tool, the package ships only the JSON.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from collections import Counter
|
|
|
|
from PIL import Image
|
|
|
|
SCALE = 8
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
mirror = "--mirror" in args
|
|
if mirror:
|
|
args.remove("--mirror")
|
|
svg, width, height, name, uk, en, *pairs = args
|
|
width, height = int(width), int(height)
|
|
palette = []
|
|
for pair in pairs:
|
|
hexcolor, ansi = pair.split("=")
|
|
palette.append((tuple(int(hexcolor[i:i + 2], 16) for i in (1, 3, 5)), ansi))
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
png = os.path.join(tmp, "art.png")
|
|
subprocess.run(["convert", "-background", "none", "-density", "600", svg,
|
|
"-resize", f"{width * SCALE}x{height * SCALE}!", png], check=True)
|
|
img = Image.open(png).convert("RGBA")
|
|
|
|
def nearest(rgb):
|
|
return min(palette, key=lambda p: sum((a - b) ** 2 for a, b in zip(p[0], rgb)))[1]
|
|
|
|
pixels = []
|
|
for py in range(height):
|
|
row = []
|
|
for px in range(width):
|
|
votes = Counter()
|
|
for y in range(py * SCALE, (py + 1) * SCALE):
|
|
for x in range(px * SCALE, (px + 1) * SCALE):
|
|
r, g, b, a = img.getpixel((x, y))
|
|
votes[nearest((r, g, b)) if a >= 128 else None] += 1
|
|
row.append(votes.most_common(1)[0][0])
|
|
if mirror:
|
|
# The source is symmetric; rounding is not, so copy the left half
|
|
for x in range(width // 2):
|
|
row[width - 1 - x] = row[x]
|
|
pixels.append(row)
|
|
|
|
styles, keys = {}, "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
def key(fg, bg):
|
|
want = {"fg": fg, "bg": bg, "bold": False}
|
|
for k, v in styles.items():
|
|
if v == want:
|
|
return k
|
|
k = keys[len(styles)]
|
|
styles[k] = want
|
|
return k
|
|
|
|
lines, smap = [], []
|
|
for cy in range(height // 2):
|
|
line, keyline = "", ""
|
|
for x in range(width):
|
|
top, bottom = pixels[2 * cy][x], pixels[2 * cy + 1][x]
|
|
if top is None and bottom is None:
|
|
ch, k = " ", " "
|
|
elif top == bottom:
|
|
ch, k = "█", key(top, None)
|
|
elif bottom is None:
|
|
ch, k = "▀", key(top, None)
|
|
elif top is None:
|
|
ch, k = "▄", key(bottom, None)
|
|
else:
|
|
ch, k = "▀", key(top, bottom)
|
|
line += ch
|
|
keyline += k
|
|
lines.append(line.rstrip())
|
|
smap.append(keyline[:len(line.rstrip())])
|
|
design = {"name": name, "title": {"uk": uk, "en": en}, "styles": styles,
|
|
"lines": lines, "style_map": smap}
|
|
json.dump(design, sys.stdout, ensure_ascii=False, indent=2)
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|