Files
Velix-MPV-Core/TOOLS/gen-fluent-glyphs.py
Kacper Michajłow c865008e65 mpv-osd-symbols: center fluent icons only vertically
Makes it nicer for speaker icon to not move around on different volume
levels. Also remove scaling, it was not needed.

This required to rewrite the exclamation mark drawing code, made it look
similar to old icon now.
2026-04-16 19:03:47 +02:00

204 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Import Fluent UI System Icons into mpv-osd-symbols font.
Downloads SVG icons from microsoft/fluentui-system-icons and uses FontForge
to import them into the mpv-osd-symbols font.
Usage:
./TOOLS/gen-fluent-glyphs.py
./TOOLS/gen-osd-font.sh
Requirements: fontforge
"""
import os
import subprocess
import sys
import tempfile
import urllib.parse
import urllib.request
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SFDIR = os.path.join(SCRIPT_DIR, "mpv-osd-symbols.sfdir")
REPO_SHA = "9a4a2db2df7f0067b4ef43c5ae5bfcae3759a5a0" # v1.1.320
REPO_BASE = ("https://raw.githubusercontent.com/"
f"microsoft/fluentui-system-icons/{REPO_SHA}/assets")
# Mapping: codepoint -> (fluent_icon_name, size, style, transform)
#
# See https://github.com/microsoft/fluentui-system-icons/tree/main/assets
# for available icons.
ICONS = {
0xE200: ("Line Horizontal 3", 24, "filled", None), # menu
0xE201: ("Play", 24, "filled", "flip_x"), # prev / play_backward
0xE202: ("Play", 24, "filled", None), # play / next
0xE203: ("Pause", 24, "filled", None), # pause
0xE204: ("Clock", 24, "regular", None), # clock (buffering)
0xE205: ("Rewind", 24, "filled", None), # skip_backward
0xE206: ("Fast Forward", 24, "filled", None), # skip_forward
0xE207: ("Previous", 24, "filled", None), # chapter_prev
0xE208: ("Next", 24, "filled", None), # chapter_next
0xE209: ("Chat", 24, "regular", None), # audio
0xE20A: ("Closed Caption", 24, "regular", None), # subtitle
0xE20B: ("Speaker Mute", 24, "filled", None), # mute
0xE20C: ("Speaker 0", 24, "filled", None), # volume[1]
0xE20D: ("Speaker 1", 24, "filled", None), # volume[2]
0xE20E: ("Speaker 2", 24, "filled", None), # volume[3]
0xE20F: ("Speaker 2", 24, "filled", "add_alert"),# volume[4] (>100%)
0xE210: ("Full Screen Maximize", 24, "filled", None), # fullscreen
0xE211: ("Full Screen Minimize", 24, "filled", None), # exit_fullscreen
0xE212: ("Dismiss", 24, "filled", None), # close
0xE213: ("Subtract", 24, "filled", None), # minimize
0xE214: ("Maximize", 24, "filled", None), # maximize
0xE215: ("Square Multiple", 24, "filled", None), # unmaximize
}
def icon_url(name, size, style):
snake = name.lower().replace(" ", "_")
filename = f"ic_fluent_{snake}_{size}_{style}.svg"
dir_encoded = urllib.parse.quote(name)
return f"{REPO_BASE}/{dir_encoded}/SVG/{filename}"
def download_icons(dest_dir):
paths = {}
seen: dict[str, str] = {}
for cp, (name, size, style, _transform) in ICONS.items():
url = icon_url(name, size, style)
svg_path = os.path.join(dest_dir, f"uni{cp:04X}.svg")
if url in seen:
# Reuse previously downloaded file.
import shutil
shutil.copy2(seen[url], svg_path)
paths[cp] = svg_path
print(f" U+{cp:04X} {name} (cached)")
continue
print(f" U+{cp:04X} {name} <- {url}")
try:
urllib.request.urlretrieve(url, svg_path)
seen[url] = svg_path
paths[cp] = svg_path
except urllib.error.HTTPError as e:
print(f" ERROR: {e.code} {e.reason} - check icon name/size")
return paths
def import_into_font(svg_paths):
transforms = {}
for cp, (_name, _size, _style, transform) in ICONS.items():
if transform:
transforms[cp] = transform
ff_script = f"""\
import fontforge
import psMat
import os
font = fontforge.open({SFDIR!r})
svg_paths = {svg_paths!r}
transforms = {transforms!r}
for cp, svg_path in sorted(svg_paths.items()):
if not os.path.exists(svg_path):
continue
glyph = font.createChar(cp)
glyph.clear()
glyph.importOutlines(svg_path)
bb = glyph.boundingBox() # (xmin, ymin, xmax, ymax)
if bb[0] == bb[2] or bb[1] == bb[3]:
print(f" SKIP U+{{cp:04X}} (empty)")
continue
# Center vertically within ascent (0..800)
y_off = (800 - (bb[3] + bb[1])) / 2
if y_off:
glyph.transform(psMat.translate(0, y_off))
bb = glyph.boundingBox()
transform = transforms.get(cp)
if transform == "flip_x":
cx = (bb[0] + bb[2]) / 2
glyph.transform(psMat.compose(psMat.translate(-cx, 0),
psMat.compose(psMat.scale(-1, 1),
psMat.translate(cx, 0))))
elif transform == "add_alert":
cy = (bb[1] + bb[3]) / 2
h = (bb[3] - bb[1]) * 0.75
top = cy + h / 2
bot = cy - h / 2
w = h * 0.14 # stroke width relative to height
r = w * 0.55 # dot radius
cx = bb[2] + 100 + w / 2
k = 0.5523
pen = glyph.glyphPen(replace=False)
# Stem (tapers from w at top to ~0.46*w at bottom tip)
stem_bot = bot + r * 2 + h * 0.1 # gap above dot
hw = w / 2
nhw = hw * 0.46
pen.moveTo((cx + hw, top))
pen.curveTo((cx + hw, top + hw * 0.3), (cx + hw * 0.3, top + hw),
(cx, top + hw))
pen.curveTo((cx - hw * 0.3, top + hw), (cx - hw, top + hw * 0.3),
(cx - hw, top))
pen.lineTo((cx - nhw, stem_bot))
pen.curveTo((cx - nhw, stem_bot - nhw * 0.5), (cx - nhw * 0.5, stem_bot - nhw),
(cx, stem_bot - nhw))
pen.curveTo((cx + nhw * 0.5, stem_bot - nhw), (cx + nhw, stem_bot - nhw * 0.5),
(cx + nhw, stem_bot))
pen.closePath()
# Dot (circle)
dot_cy = bot + r
pen.moveTo((cx + r, dot_cy))
pen.curveTo((cx + r, dot_cy + r * k), (cx + r * k, dot_cy + r),
(cx, dot_cy + r))
pen.curveTo((cx - r * k, dot_cy + r), (cx - r, dot_cy + r * k),
(cx - r, dot_cy))
pen.curveTo((cx - r, dot_cy - r * k), (cx - r * k, dot_cy - r),
(cx, dot_cy - r))
pen.curveTo((cx + r * k, dot_cy - r), (cx + r, dot_cy - r * k),
(cx + r, dot_cy))
pen.closePath()
pen = None
print(f" U+{{cp:04X}} imported")
font.save({SFDIR!r})
print("Saved to", {SFDIR!r})
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(ff_script)
script_path = f.name
try:
subprocess.run(["fontforge", "-lang=py", "-script", script_path],check=True)
finally:
os.unlink(script_path)
def main():
print("Downloading Fluent UI System Icons...")
with tempfile.TemporaryDirectory() as tmpdir:
svg_paths = download_icons(tmpdir)
if not svg_paths:
print("No icons downloaded.")
sys.exit(1)
print(f"\nImporting {len(svg_paths)} icons into {SFDIR}...")
import_into_font(svg_paths)
print("\nDone. Run './TOOLS/gen-osd-font.sh' to rebuild the mpv font.")
if __name__ == "__main__":
main()