Welcome to yet another brief! I'm trying to keep this up. Not all briefs will be the same size, this one is quite short - but cool!
So, you know 7-segment displays?
Well, they cannot display every character of the alphabet. So I made a segmented display that could. Inspired by some of the drafts from here I created a...
Just adding two lines in the middle made it able to display A-Z, 0-9, and punctuation.
Sure, K, R, and X look a little weird, but it can make legible sentences.
This is the character grid.
By the way, Claude made the script to make these SVGs, I've linked it to download. It requires Python 3 to run.
#!/usr/bin/env python3
"""9-segment display SVG generator.
A 9-segment display is like a 7-segment display with two vertical
segments in the middle (h = upper-middle, i = lower-middle).
Segment layout:
aaaa
f b
f h b
gggg
e i c
e c
dddd
Usage:
./9seg a b e # SVG with segments a, b, e lit
./9seg --guide # SVG guide showing segment names
./9seg -t "Hello world" # SVG text using characters.toml
./9seg -c red -t "Hi" # Custom active segment color
./9seg -c "#00ff00" a b c # Custom color for single display
"""
import os
import sys
# Segment geometry: each segment is a polygon defined by points
# Display box is roughly 60w x 100h, with some padding
SEGMENTS = {
"a": [(12, 4), (48, 4), (44, 8), (16, 8)], # top horizontal
"b": [(50, 6), (50, 46), (46, 42), (46, 10)], # top-right vertical
"c": [(50, 54), (50, 94), (46, 90), (46, 58)], # bottom-right vertical
"d": [(12, 96), (48, 96), (44, 92), (16, 92)], # bottom horizontal
"e": [(10, 54), (10, 94), (14, 90), (14, 58)], # bottom-left vertical
"f": [(10, 6), (10, 46), (14, 42), (14, 10)], # top-left vertical
"g": [(12, 48), (48, 48), (46, 52), (14, 52)], # middle horizontal
"h": [(28, 10), (32, 10), (32, 46), (28, 46)], # upper-middle vertical
"i": [(28, 54), (32, 54), (32, 90), (28, 90)], # lower-middle vertical
}
# Label positions for --guide mode (x, y) roughly centered on each segment
LABEL_POS = {
"a": (30, 8),
"b": (51, 28),
"c": (51, 76),
"d": (30, 98),
"e": (5, 76),
"f": (5, 28),
"g": (30, 52),
"h": (30, 28),
"i": (30, 76),
}
DEFAULT_LIT_COLOR = "#ff3300"
DIM_COLOR = "#e0e0e0"
GUIDE_COLORS = [
"#e63946", "#457b9d", "#2a9d8f", "#e9c46a",
"#f4a261", "#264653", "#6a4c93", "#1982c4", "#8ac926",
]
CHAR_WIDTH = 60
CHAR_HEIGHT = 100
CHAR_GAP = -2
SPACE_WIDTH = 30
def points_str(pts):
return " ".join(f"{x},{y}" for x, y in pts)
def load_characters(path):
"""Load characters.toml, return case-insensitive {char: [segments]} mapping."""
try:
import tomllib
except ImportError:
tomllib = None
if tomllib:
with open(path, "rb") as f:
raw = tomllib.load(f)
else:
# Fallback parser for flat "key" = "value" TOML
raw = {}
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip().strip('"').strip("'")
v = v.strip().strip('"').strip("'")
raw[k] = v
valid_segs = set(SEGMENTS.keys())
chars = {}
seen = {} # lowercase -> original key (for error messages)
for key, value in raw.items():
if len(key) != 1:
print(f"Error: characters.toml keys must be single characters, got '{key}'", file=sys.stderr)
sys.exit(1)
lower = key.lower()
if lower in seen:
print(
f"Error: characters.toml has duplicate (case-insensitive) entries: '{seen[lower]}' and '{key}'",
file=sys.stderr,
)
sys.exit(1)
seen[lower] = key
segments = value.split() if value.strip() else []
for seg in segments:
if seg not in valid_segs:
print(f"Error: unknown segment '{seg}' in mapping for '{key}'", file=sys.stderr)
sys.exit(1)
chars[lower] = segments
return chars
def make_svg(lit_segments, lit_color=DEFAULT_LIT_COLOR):
lit = set(lit_segments)
polys = []
for name, pts in SEGMENTS.items():
color = lit_color if name in lit else DIM_COLOR
polys.append(
f' <polygon points="{points_str(pts)}" fill="{color}" />'
)
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 100" width="240" height="400">
{chr(10).join(polys)}
</svg>"""
def make_guide():
polys = []
labels = []
for idx, (name, pts) in enumerate(SEGMENTS.items()):
color = GUIDE_COLORS[idx % len(GUIDE_COLORS)]
polys.append(
f' <polygon points="{points_str(pts)}" fill="{color}" opacity="0.85" />'
)
lx, ly = LABEL_POS[name]
labels.append(
f' <text x="{lx}" y="{ly}" text-anchor="middle" '
f'dominant-baseline="central" font-family="monospace" '
f'font-size="7" font-weight="bold" fill="white" '
f'stroke="black" stroke-width="0.3">{name}</text>'
)
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 100" width="240" height="400">
{chr(10).join(polys)}
{chr(10).join(labels)}
</svg>"""
def make_text_svg(text, chars, lit_color=DEFAULT_LIT_COLOR):
"""Generate combined SVG for a text string."""
groups = []
x = 0
for ch in text:
if ch == " ":
x += SPACE_WIDTH
continue
lower = ch.lower()
if lower not in chars:
print(f"Error: no mapping for '{ch}' in characters.toml", file=sys.stderr)
sys.exit(1)
lit = set(chars[lower])
polys = []
for name, pts in SEGMENTS.items():
color = lit_color if name in lit else DIM_COLOR
polys.append(f' <polygon points="{points_str(pts)}" fill="{color}" />')
groups.append(f' <g transform="translate({x}, 0)">\n' + "\n".join(polys) + "\n </g>")
x += CHAR_WIDTH + CHAR_GAP
# Remove trailing gap after last non-space character
if groups:
x -= CHAR_GAP
total_width = max(x, 1)
pixel_height = 400
pixel_width = int(pixel_height * total_width / CHAR_HEIGHT)
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_width} {CHAR_HEIGHT}" width="{pixel_width}" height="{pixel_height}">
{chr(10).join(groups)}
</svg>"""
def main():
args = sys.argv[1:]
if not args:
print(__doc__.strip())
sys.exit(1)
if "--guide" in args:
print(make_guide())
sys.exit(0)
# Parse -c / --color (must come first if present)
lit_color = DEFAULT_LIT_COLOR
if args and args[0] in ("-c", "--color"):
if len(args) < 2:
print("Error: -c/--color requires a color value", file=sys.stderr)
sys.exit(1)
lit_color = args[1]
args = args[2:]
if not args:
print("Error: no segments or text specified", file=sys.stderr)
sys.exit(1)
# Parse -t / --text
if args[0] in ("-t", "--text"):
if len(args) < 2:
print("Error: -t/--text requires a string argument", file=sys.stderr)
sys.exit(1)
text = args[1]
# Find characters.toml next to this script
script_dir = os.path.dirname(os.path.realpath(__file__))
toml_path = os.path.join(script_dir, "characters.toml")
if not os.path.exists(toml_path):
print(f"Error: {toml_path} not found", file=sys.stderr)
sys.exit(1)
chars = load_characters(toml_path)
print(make_text_svg(text, chars, lit_color))
sys.exit(0)
# Original mode: segment names as arguments
valid = set(SEGMENTS.keys())
for seg in args:
if seg not in valid:
print(f"Error: unknown segment '{seg}'. Valid: {', '.join(sorted(valid))}", file=sys.stderr)
sys.exit(1)
print(make_svg(args, lit_color))
if __name__ == "__main__":
main()
Hope you enjoyed!
