feat(avatar): add SVG, PNG and HDR avatar exports

This commit is contained in:
2026-10-18 18:48:49 +08:00
parent 61d039beba
commit 899df27427
10 changed files with 1331 additions and 324 deletions
+276 -317
View File
@@ -1,329 +1,288 @@
"""Generate a complete Matrix character grid as square SVG and PNG avatars.
Run with: uv run python cmatrix_avatar.py --seed 42 --previews
"""
from __future__ import annotations
import argparse
from contextlib import ExitStack
from dataclasses import dataclass
import os
from pathlib import Path
import random
import secrets
from xml.sax.saxutils import escape
class CMatrixAvatar(object):
def __init__(self, cmatrix=None) -> None:
self.cmatrix = cmatrix
self.cmatrix_lenx = 0
self.cmatrix_leny = 0
if self.cmatrix != None:
assert type(cmatrix) == type("Hello"), "Wrong type"
cmatrix_lines = self.cmatrix.split("\n")
for i in cmatrix_lines:
self.cmatrix_leny += 1
if len(i) > self.cmatrix_lenx:
self.cmatrix_lenx = len(i)
self.p_black = "\33[0;30m" # print black
self.p_red = "\33[0;31m" # print red
self.p_green = "\33[0;32m" # print green
self.p_yellow = "\33[0;33m" # print yellow
self.p_blue = "\33[0;34m" # print blue
self.p_magenta = "\33[0;35m" # print magenta
self.p_cyan = "\33[0;36m" # print cyan
self.p_light_gary = "\33[0;37m" # print light gary
self.p_dark_gary = "\33[0;90m" # print dark gary
self.p_light_red = "\33[0;91m" # print light red
self.p_light_green = "\33[0;92m" # print light green
self.p_light_blue = "\33[0;94m" # print light blue
self.p_default = "\33[0m" # print default
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib import TTFont
from PIL import Image
import resvg_py
from avatar_color import gradient_level, sdr_color, validate_peak
def set_background_string(self, string=None):
if string == None:
self.background_string = [
"Never Lose a Holy Curiosity.",
"What I cannot create, I do not understand.",
"A person who never made a mistake never tried anything new.",
"Not even wrong.",
"For a successful technology, reality must take precedence over public relations, for nature cannot be fooled.",
"Know how to solve every problem that has been solved.",
"I would rather have questions that can't be answered than answers that can't be questioned.",
"We are trying to prove ourselves wrong as quickly as possible, because only in that way can we find progress.",
"Education is what remains after one has forgotten everything one has learned in school.",
"The important thing is not to stop questioning. Curiosity has its own reason for existence. One cannot help but be in awe when he contemplates the mysteries of eternity, of life, of the marvelous structure of reality. It is enough if one tries merely to comprehend a little of this mystery each day.",
"I owe a lot to my engineering training because it taught me to tolerateapproximations. Previously to that I thought one should just concentrate on exactequations all the time. Then I got the idea that in the actual world all our equations areonly approximate. We must just tend to greater and greater accuracy. In spite of theequations being approximate, they can be beautiful.",
]
elif type(string) == type("Hello"):
self.background_string = [string]
elif type(string) == type([1,2,3]):
for i in string:
assert type(i) == type("Hello"), "Wrong type"
self.background_string = string
for i in range(len(self.background_string)):
self.background_string[i] = self.background_string[i].replace(" ", "")
self.background_string = "".join(self.background_string)
tmp = ""
for i in self.background_string:
if random.randint(0,1) == 0:
tmp += i.lower()
BACKGROUND = "#0b0b0b"
BLUE = "#3b73fa"
YELLOW = "#c7a52c"
GREEN = "#00ff00"
GWB = (
" ██████╗ ██╗ ██╗██████╗ ",
"██╔════╝ ██║ ██║██╔══██╗",
"██║ ███╗██║ █╗ ██║██████╔╝",
"██║ ██║██║███╗██║██╔══██╗",
"╚██████╔╝╚███╔███╔╝██████╔╝",
" ╚═════╝ ╚══╝╚══╝ ╚═════╝ ",
)
QUOTES = (
"Never Lose a Holy Curiosity.",
"What I cannot create, I do not understand.",
"A person who never made a mistake never tried anything new.",
"Not even wrong.",
"For a successful technology, reality must take precedence over public relations, for nature cannot be fooled.",
"Know how to solve every problem that has been solved.",
"I would rather have questions that can't be answered than answers that can't be questioned.",
"We are trying to prove ourselves wrong as quickly as possible, because only in that way can we find progress.",
"Education is what remains after one has forgotten everything one has learned in school.",
"The important thing is not to stop questioning. Curiosity has its own reason for existence. One cannot help but be in awe when he contemplates the mysteries of eternity, of life, of the marvelous structure of reality. It is enough if one tries merely to comprehend a little of this mystery each day.",
"I owe a lot to my engineering training because it taught me to tolerateapproximations. Previously to that I thought one should just concentrate on exactequations all the time. Then I got the idea that in the actual world all our equations areonly approximate. We must just tend to greater and greater accuracy. In spite of theequations being approximate, they can be beautiful.",
)
@dataclass(frozen=True)
class Cell:
char: str = " "
color: str = BACKGROUND
level: float = 1.0
class CMatrixAvatar:
"""Original rain algorithm, with colors separate from characters."""
def __init__(self, seed: int | None = None, rows: int = 43, columns: int = 77):
if not len(GWB) + 2 <= rows <= 200:
raise ValueError(f"--rows must be between {len(GWB) + 2} and 200")
if not max(map(len, GWB)) + 2 <= columns <= 200:
raise ValueError(f"--columns must be between {max(map(len, GWB)) + 2} and 200")
self.rows, self.columns = rows, columns
self.seed = secrets.randbits(32) if seed is None else seed
rng = random.Random(self.seed)
text = "".join(QUOTES).replace(" ", "")
text = "".join(c.lower() if rng.randint(0, 1) == 0 else c.upper() for c in text)
self.cells = [[Cell() for _ in range(self.columns)] for _ in range(self.rows)]
for column in range(0, self.columns, 2):
length = rng.randint(0, self.rows)
start = rng.randint(0, self.rows)
for offset in range(length):
position = offset / (length - 1) if length > 1 else 1.0
self.cells[(start + offset) % self.rows][column] = Cell("+", GREEN, gradient_level(position))
# The logo is now an independent layer. Its reserved area is determined
# during layout; the complete rain grid remains available here.
text_index = 0
for row in range(self.rows):
for column in range(self.columns):
if self.cells[row][column].char == "+":
char = text[text_index % len(text)]
color = YELLOW if char.lower() in "gwb" else self.cells[row][column].color
self.cells[row][column] = Cell(char, color, self.cells[row][column].level)
text_index += 1
def find_font(requested: Path | None = None, *, for_logo: bool = False) -> Path:
if requested is not None:
if not requested.is_file():
raise ValueError(f"Font not found: {requested}")
return requested
fonts = Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts"
candidates = (
fonts / "consola.ttf", fonts / "CascadiaMono.ttf",
Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"),
Path("/System/Library/Fonts/Menlo.ttc"),
)
if not for_logo:
candidates = (fonts / "OCRAEXT.TTF",) + candidates
for candidate in candidates:
if candidate.is_file():
return candidate
raise ValueError("No supported monospace font found; pass --font /path/to/font.ttf")
def make_svg(avatar: CMatrixAvatar, font_path: Path, size: int = 2048,
logo_font_path: Path | None = None, logo_width: float = 0.72) -> str:
"""Outline all glyphs: no external font or text rendering dependency."""
if not 0.4 <= logo_width <= 0.85:
raise ValueError("--logo-width must be between 0.4 and 0.85")
with ExitStack() as stack:
font = stack.enter_context(TTFont(font_path, fontNumber=0))
cmap = font.getBestCmap()
logo_characters = set("".join(GWB).replace(" ", ""))
if logo_font_path is None:
logo_font_path = (font_path if all(ord(c) in cmap for c in logo_characters)
else find_font(for_logo=True))
logo_font = stack.enter_context(TTFont(logo_font_path, fontNumber=0))
if ord("M") not in cmap:
raise ValueError("Background font must contain Latin letters.")
cell_width = font["hmtx"][cmap[ord("M")]][0]
cell_height = cell_width * 1.8
sources = {}
for is_logo, source in ((False, font), (True, logo_font)):
glyphs, mapping = source.getGlyphSet(), source.getBestCmap()
chars = {cell.char for row in avatar.cells for cell in row
if cell.char != " " and (cell.color == BLUE) == is_logo}
if is_logo:
chars.update(logo_characters)
missing = sorted(c for c in chars if ord(c) not in mapping)
if missing:
raise ValueError(f"Font is missing characters: {''.join(missing)}")
widths = {source["hmtx"][mapping[ord(c)]][0] for c in chars}
if is_logo and len(widths) > 1:
raise ValueError("Please choose a logo font with equal character advances.")
# Display fonts such as HeavyData have varying advances. Fit their
# widest glyph to the grid, then center narrower glyphs unchanged.
advance = max(widths, default=cell_width)
if ord("█") in mapping:
block_bounds = BoundsPen(glyphs)
glyphs[mapping[ord("█")]].draw(block_bounds)
_, bottom, _, top = block_bounds.bounds
else:
tmp += i.upper()
self.background_string = tmp
# OCR A Extended has no box/block glyphs. Its Latin letters use
# its own line metrics; the logo fallback fits the SAME cells.
top, bottom = source["hhea"].ascent, source["hhea"].descent
sources[is_logo] = (glyphs, mapping, cell_width / advance,
cell_height / (top - bottom), top, source["hmtx"], BoundsPen(glyphs))
# Measure the full background before reserving the logo area.
background_glyphs = []
for row, cells in enumerate(avatar.cells):
for column, cell in enumerate(cells):
if cell.char == " ":
continue
glyphs, mapping, sx, sy, glyph_top, metrics, bounds = sources[cell.color == BLUE]
offset_x = (cell_width - metrics[mapping[ord(cell.char)]][0] * sx) / 2
transform = (sx, 0, 0, -sy, column * cell_width + offset_x,
row * cell_height + glyph_top * sy)
glyph = glyphs[mapping[ord(cell.char)]]
glyph.draw(TransformPen(bounds, transform))
cell_bounds = BoundsPen(glyphs)
glyph.draw(TransformPen(cell_bounds, transform))
gx0, gy0, gx1, gy1 = cell_bounds.bounds
occupied = (min(column * cell_width, gx0), min(row * cell_height, gy0),
max((column + 1) * cell_width, gx1), max((row + 1) * cell_height, gy1))
background_glyphs.append((cell, glyph, glyphs, transform, occupied))
def set_embed_string(self, string=None):
if string == None:
string = f"""
__________ ____________
/ _____/ \ / \______ \\
/ \ __\ \/\/ /| | _/
\ \_\ \ / | | \\
\______ /\__/\ / /_______ /
\/ \/ \/ """
# string = r"""
"""
________ __ ____________
/ _____// \ / \______ \
/ \ ___\ \/\/ / | | _/
\ \_\ \\ / | | \
\______ / \__/\ / /______ /
\/ \/ \/
"""
# Fit the COMPLETE grid and actual glyph extents, with half a cell of
# safety space on every side. Never crop or discard background cells.
drawn_bounds = [source[-1].bounds for source in sources.values() if source[-1].bounds]
bx0 = min((b[0] for b in drawn_bounds), default=0)
by0 = min((b[1] for b in drawn_bounds), default=0)
bx1 = max((b[2] for b in drawn_bounds), default=0)
by1 = max((b[3] for b in drawn_bounds), default=0)
x0, y0 = min(0, bx0), min(0, by0)
x1 = max(avatar.columns * cell_width, bx1)
y1 = max(avatar.rows * cell_height, by1)
side = max(x1 - x0, y1 - y0) + cell_width
left = (side - (x1 - x0)) / 2 - x0
top = (side - (y1 - y0)) / 2 - y0
# Build the original six-line GWB, then scale the WHOLE logo uniformly.
glyphs, mapping, sx, sy, glyph_top, metrics, _ = sources[True]
logo_pen, logo_bounds = SVGPathPen(glyphs), BoundsPen(glyphs)
for row, line in enumerate(GWB):
for column, char in enumerate(line):
if char == " ":
continue
transform = (sx, 0, 0, -sy, column * cell_width,
row * cell_height + glyph_top * sy)
glyph = glyphs[mapping[ord(char)]]
glyph.draw(TransformPen(logo_pen, transform))
glyph.draw(TransformPen(logo_bounds, transform))
lx0, ly0, lx1, ly1 = logo_bounds.bounds
logo_scale = side * logo_width / (lx1 - lx0)
logo_height = (ly1 - ly0) * logo_scale
logo_x, logo_y = (side - side * logo_width) / 2, (side - logo_height) / 2
gap = cell_width * 0.25
reserved = (logo_x - left - gap, logo_y - top - gap,
logo_x + side * logo_width - left + gap, logo_y + logo_height - top + gap)
# Leave entire intersecting cells empty. No clipped glyphs or mask over
# partial letters; all non-overlapping rain cells are retained.
pens = {}
rendered = 0
for cell, glyph, glyphs, transform, (gx0, gy0, gx1, gy1) in background_glyphs:
if gx0 < reserved[2] and gx1 > reserved[0] and gy0 < reserved[3] and gy1 > reserved[1]:
continue
pen = pens.setdefault((cell.color, cell.level), SVGPathPen(glyphs))
glyph.draw(TransformPen(pen, transform))
rendered += 1
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {side} {side}">',
'<title>GWB Matrix avatar</title>',
f'<desc>Complete {avatar.rows} x {avatar.columns} grid; seed {avatar.seed}; font {escape(font_path.name)}; logo font {escape(logo_font_path.name)}</desc>',
f'<rect width="{side}" height="{side}" fill="{BACKGROUND}"/>',
f'<g id="background" data-visible-cells="{rendered}" transform="translate({left} {top})">',
]
for (color, level), pen in pens.items():
parts.append(f'<path fill="{sdr_color(color, level)}" data-base="{color}" data-level="{level}" d="{pen.getCommands()}"/>')
parts.extend(['</g>',
f'<path id="logo" fill="{sdr_color(BLUE)}" data-base="{BLUE}" data-level="1" transform="translate({logo_x} {logo_y}) scale({logo_scale}) translate({-lx0} {-ly0})" d="{logo_pen.getCommands()}"/>',
'</svg>'])
return "\n".join(parts) + "\n"
"""
_____ _ _______
| __ \ | | | ___ \
| | \/ | | | |_/ /
| | __| |/\| | ___ \
| |_\ \ /\ / |_/ /
\____/\/ \/\____/
"""
string = """
▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄
█ █ █ ▄ █ █ ▄ █
█ ▄▄▄▄█ ██ ██ █ █▄█ █
█ █ ▄▄█ █ █
█ █ █ █ █ ▄ █
█ █▄▄█ █ ▄ █ █▄█ █
█▄▄▄▄▄▄▄█▄▄█ █▄▄█▄▄▄▄▄▄▄█
"""
string = """
░▒█▀▀█░▒█░░▒█░▒█▀▀▄
░▒█░▄▄░▒█▒█▒█░▒█▀▀▄
░▒█▄▄▀░▒▀▄▀▄▀░▒█▄▄█
"""
string = """
██████╗ ██╗ ██╗██████╗
██╔════╝ ██║ ██║██╔══██╗
██║ ███╗██║ █╗ ██║██████╔╝
██║ ██║██║███╗██║██╔══██╗
╚██████╔╝╚███╔███╔╝██████╔╝
╚═════╝ ╚══╝╚══╝ ╚═════╝ """
def export(avatar: CMatrixAvatar, output: Path, font: Path, size: int,
previews: bool = False, logo_font: Path | None = None,
logo_width: float = 0.72, hdr: bool = False,
peak_nits: float = 400) -> tuple[Path, ...]:
if output.suffix.lower() not in ("", ".png", ".svg"):
raise ValueError("Output must be a basename, .png, or .svg path.")
validate_peak(peak_nits)
png_path, svg_path = output.with_suffix(".png"), output.with_suffix(".svg")
svg = make_svg(avatar, font, size, logo_font, logo_width)
# Render exactly the same outlined SVG, not a second font drawing pipeline.
png = resvg_py.svg_to_bytes(svg_string=svg)
png_path.parent.mkdir(parents=True, exist_ok=True)
svg_path.write_text(svg, encoding="utf-8")
png_path.write_bytes(png)
if previews:
with Image.open(png_path) as image:
for small_size in (128, 64):
image.resize((small_size, small_size), Image.Resampling.LANCZOS).save(
png_path.with_name(f"{png_path.stem}-{small_size}.png"))
if hdr:
from avatar_hdr import export_hdr
hdr_path = png_path.with_name(f"{png_path.stem}-hdr.png")
export_hdr(svg, hdr_path, peak_nits)
return svg_path, png_path, hdr_path
return svg_path, png_path
"""
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██░▄▄░██░███░██░▄▄▀██
██░█▀▀██░█░█░██░▄▄▀██
██░▀▀▄██▄▀▄▀▄██░▀▀░██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀"""
"""
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░ ░░░░░ ░░░░░░░░ ░ ░░░░
▒▒ ▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒ ▒ ▒▒ ▒▒
▒ ▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒ ▒▒▒ ▒ ▒▒▒ ▒
▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓ ▓ ▓▓▓
▓ ▓▓▓ ▓ ▓ ▓ ▓ ▓ ▓▓▓▓
▓▓ ▓▓▓▓ ▓▓▓ ▓ ▓▓▓▓ ▓ ▓▓▓▓▓
███ █████ ████████ █ █ █
██████████████████████████████████████
"""
string = string[1:]
else:
assert type(string) == type("Hello")
string_lines = string.split("\n")
string_lines = [list(i) for i in string_lines]
len_x = 0
len_y = 0
for i in string_lines:
len_x += 1
if len(i) > len_y:
len_y = len(i)
for i in range(len(string_lines)):
if len(string_lines[i]) < len_y:
for j in range(len(string_lines[i]), len_y):
string_lines[i].append(" ")
self.embed_string = string_lines
self.embed_lenx = len_x
self.embed_leny = len_y
def gen_cmatrix(self, row_no, col_no):
cmatrix_horizontal = []
for i in range(col_no):
cmatrix_horizontal.append([" "] * row_no)
if i % 2 == 0:
length = random.randint(0, row_no)
first_position = random.randint(0, row_no)
for j in range(length):
tmp = j / length
if tmp >= 0 and tmp < 0.2:
tmp = 22
elif tmp >= 0.2 and tmp < 0.4:
tmp = 28
elif tmp >= 0.4 and tmp < 0.6:
tmp = 34
elif tmp >= 0.6 and tmp < 0.8:
tmp = 40
elif tmp >= 0.8 and tmp < 1.0:
tmp = 46
cmatrix_horizontal[i][(first_position+j)%row_no] = "\033[38;5;%dm+"%(tmp,)
cmatrix_vertical = []
for i in range(row_no):
cmatrix_vertical.append([" "] * col_no)
for i in range(row_no):
for j in range(col_no):
cmatrix_vertical[i][j] = cmatrix_horizontal[j][i]
# cmatrix_vertical = ["".join(i) for i in cmatrix_vertical]
# self.cmatrix = "\n".join(cmatrix_vertical)
self.cmatrix = cmatrix_vertical
self.cmatrix_lenx = row_no
self.cmatrix_leny = col_no
def mark_latters(self, latters=None):
if latters == None:
char_set = ['G', 'g', 'W', 'w', 'B', 'b']
elif type(latters) == type("Hello"):
char_set = list(latters.lower)
char_set += list(latters.upper)
elif type(latters) == type([1,2,3]):
char_set = latters.copy()
char_set = set(char_set)
char_set = list(char_set)
for i in range(self.cmatrix_lenx):
for j in range(self.cmatrix_leny):
if self.cmatrix[i][j][-1] in char_set:
self.cmatrix[i][j] = self.p_yellow + self.cmatrix[i][j][-1] #+ self.p_light_green
def substitute_colum(self, string, col):
assert type(string) == type("Hello"), "Wrong type"
cmatrix_lines = self.cmatrix.split("\n")
cmatrix_lines = [list(i) for i in cmatrix_lines]
string_len = len(string)
string = list(string)
if string_len <= len(cmatrix_lines):
for i in range(string_len):
if len(cmatrix_lines[i]) > col:
cmatrix_lines[i][col] = string[i]
else:
for j in range(len(cmatrix_lines[i]), col):
cmatrix_lines[i].append(" ")
cmatrix_lines[i].append(string[i])
else:
for i in range(len(cmatrix_lines)):
if len(cmatrix_lines[i]) > col:
cmatrix_lines[i][col] = string[i]
else:
for j in range(len(cmatrix_lines[i]), col):
cmatrix_lines[i].append(" ")
cmatrix_lines[i].append(string[i])
for i in range(len(cmatrix_lines), string_len):
cmatrix_lines.append([])
for j in range(0, col):
cmatrix_lines[i].append(" ")
cmatrix_lines[i].append(string[i])
cmatrix_lines = ["".join(i) for i in cmatrix_lines]
self.cmatrix = "\n".join(cmatrix_lines)
def substitute_all(self):
says = self.background_string
cmatrix_list = list(self.cmatrix)
count_in_says = 0
for i in range(len(cmatrix_list)):
if not (cmatrix_list[i] in [" ", "\n"]):
if count_in_says < len(says):
pass
elif count_in_says >= len(says):
count_in_says = 0
cmatrix_list[i] = says[count_in_says]
count_in_says += 1
random_numbers = [random.randint(0, 1) for _i in range(len(cmatrix_list))]
cmatrix_list = [cmatrix_list[i].upper() if random_numbers[i] == 1 else cmatrix_list[i].lower() for i in range(len(cmatrix_list))]
self.cmatrix = "".join(cmatrix_list)
def substitute_all_with_embed(self):
len_x = self.embed_lenx
len_y = self.embed_leny
embed_string_lines = self.embed_string
if len_x <= self.cmatrix_lenx and len_y <= self.cmatrix_leny:
start_x = self.cmatrix_lenx//2 - len_x//2
start_y = self.cmatrix_leny//2 - len_y//2
cmatrix_lines = self.cmatrix
for i in range(len_x):
for j in range(len_y):
cmatrix_lines[i+start_x][j+start_y] = self.p_light_blue + embed_string_lines[i][j]
background_str_count = 0
start_x = self.cmatrix_lenx//2 - len_x//2
start_y = self.cmatrix_leny//2 - len_y//2
for i in range(self.cmatrix_lenx):
for j in range(self.cmatrix_leny):
if i >= start_x and i< start_x + len_x and j >= start_y and j < start_y + len_y:
pass
elif "+" in cmatrix_lines[i][j]:
if background_str_count >= len(self.background_string):
background_str_count = 0
# print(cmatrix_lines[i][j])
cmatrix_lines[i][j] = cmatrix_lines[i][j].replace("+", self.background_string[background_str_count])
background_str_count += 1
self.cmatrix = cmatrix_lines
else:
assert False, "Wrong size"
def centered_embed(self, string=None):
len_x = 0
len_y = 0
string_lines = self.embed_string.split("\n")
for i in string_lines:
len_x += 1
if len(i) > len_y:
len_y = len(i)
string_lines = [list(i) for i in string_lines]
if len_x <= self.cmatrix_lenx and len_y <= self.cmatrix_leny:
start_x = self.cmatrix_lenx//2 - len_x//2
start_y = self.cmatrix_leny//2 - len_y//2
cmatrix_lines = self.cmatrix.split("\n")
cmatrix_lines = [list(i) for i in cmatrix_lines]
for i in range(len_x):
for j in range(len_y):
string_lines[i][j]
cmatrix_lines[i+start_x][j+start_y] = string_lines[i][j]
cmatrix_lines = ["".join(i) for i in cmatrix_lines]
self.cmatrix = "\n".join(cmatrix_lines)
else:
assert False, "Wrong size"
def show(self):
count = 0
for i in range(self.cmatrix_lenx):
for j in range(self.cmatrix_leny):
if not (" " in self.cmatrix[i][j]):
count += 1
print("Latter count:", count)
print("\n")
tmp = ["".join(i) for i in self.cmatrix]
tmp = "\n".join(tmp)
print(tmp + self.p_default)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=Path("avatar.png"), help="Output basename or PNG/SVG path; both formats are written")
parser.add_argument("--size", type=int, default=2048, help="Square PNG size (default: 2048)")
parser.add_argument("--seed", type=int, help="Seed to reproduce the same rain")
parser.add_argument("--font", type=Path, help="Monospace TTF/OTF/TTC font")
parser.add_argument("--logo-font", type=Path, help="Optional block/box font (automatic fallback if needed)")
parser.add_argument("--rows", type=int, default=43, help="Complete grid rows (default: 43)")
parser.add_argument("--columns", type=int, default=77, help="Complete grid columns (default: 77)")
parser.add_argument("--logo-width", type=float, default=0.72, help="Logo width / canvas width (default: 0.72)")
parser.add_argument("--previews", action="store_true", help="Also save 128px and 64px previews")
parser.add_argument("--hdr", action="store_true", help="Also export a true 16-bit BT.2020/PQ HDR PNG")
parser.add_argument("--peak-nits", type=float, default=400, help="Shared HDR luminance target for logo and rain peaks (100..2000; default: 400)")
args = parser.parse_args()
if not 64 <= args.size <= 4096:
parser.error("--size must be between 64 and 4096")
try:
font = find_font(args.font)
avatar = CMatrixAvatar(args.seed, args.rows, args.columns)
logo_font = find_font(args.logo_font, for_logo=True) if args.logo_font else None
paths = export(avatar, args.output, font, args.size, args.previews, logo_font,
args.logo_width, args.hdr, args.peak_nits)
except (OSError, ValueError) as exc:
parser.error(str(exc))
print(f"Seed: {avatar.seed}; font: {font.name}; PNG: {args.size} x {args.size}")
for path in paths:
print(f"Saved {path.resolve()}")
if __name__ == "__main__":
cmatrix = CMatrixAvatar()
cmatrix.set_background_string()
cmatrix.set_embed_string()
cmatrix.gen_cmatrix(43//3*2, 86//4*3+4)
# cmatrix.show()
cmatrix.substitute_all_with_embed()
# cmatrix.show()
cmatrix.mark_latters()
cmatrix.show()
cmatrix.gen_cmatrix(43//1, 86*3//2)
cmatrix.substitute_all_with_embed()
cmatrix.mark_latters()
cmatrix.show()
# print("\033[38;5;22mA \033[38;5;28mA \033[38;5;34mA \033[38;5;40mA \033[38;5;46mA \033[0m")
main()