Skip to content

Instantly share code, notes, and snippets.

@LDGT123
Last active January 31, 2026 16:49
Show Gist options
  • Select an option

  • Save LDGT123/1ff16f5b1d82bc7f66b7dc05aec357d8 to your computer and use it in GitHub Desktop.

Select an option

Save LDGT123/1ff16f5b1d82bc7f66b7dc05aec357d8 to your computer and use it in GitHub Desktop.
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
from svgpathtools import parse_path, Line, QuadraticBezier, CubicBezier, Arc
from fractions import Fraction
import re
import os
# Format numbers as LaTeX fractions
def fmt(x):
f = Fraction(x).limit_denominator()
if f.denominator == 1:
return str(f.numerator)
return f"\\frac{{{f.numerator}}}{{{f.denominator}}}"
# Convert a segment to LaTeX parametric form
def segment_to_latex(seg, s0, s1):
if s1 - s0 == 1:
if s0 == 0:
u = "s"
else:
u = f"(s-{fmt(s0)})"
else:
u = f"(s-{fmt(s0)})/{fmt(s1 - s0)}"
if isinstance(seg, Line):
x0, y0 = seg.start.real, seg.start.imag
x1, y1 = seg.end.real, seg.end.imag
x_expr = f"{fmt(x0)}+({fmt(x1-x0)})*{u}"
y_expr = f"{fmt(y0)}+({fmt(y1-y0)})*{u}"
elif isinstance(seg, QuadraticBezier):
x0, y0 = seg.start.real, seg.start.imag
cx, cy = seg.control.real, seg.control.imag
x1, y1 = seg.end.real, seg.end.imag
x_expr = f"(1-{u})*(1-{u})*{fmt(x0)} + 2*(1-{u})*{u}*{fmt(cx)} + {u}*{u}*{fmt(x1)}"
y_expr = f"(1-{u})*(1-{u})*{fmt(y0)} + 2*(1-{u})*{u}*{fmt(cy)} + {u}*{u}*{fmt(y1)}"
elif isinstance(seg, CubicBezier):
x0, y0 = seg.start.real, seg.start.imag
c1x, c1y = seg.control1.real, seg.control1.imag
c2x, c2y = seg.control2.real, seg.control2.imag
x1, y1 = seg.end.real, seg.end.imag
x_expr = (
f"(1-{u})*(1-{u})*(1-{u})*{fmt(x0)}"
f"+3*(1-{u})*(1-{u})*{u}*{fmt(c1x)}"
f"+3*(1-{u})*{u}*{u}*{fmt(c2x)}"
f"+{u}*{u}*{u}*{fmt(x1)}"
)
y_expr = (
f"(1-{u})*(1-{u})*(1-{u})*{fmt(y0)}"
f"+3*(1-{u})*(1-{u})*{u}*{fmt(c1y)}"
f"+3*(1-{u})*{u}*{u}*{fmt(c2y)}"
f"+{u}*{u}*{u}*{fmt(y1)}"
)
else:
x0, y0 = seg.start.real, seg.start.imag
x1, y1 = seg.end.real, seg.end.imag
x_expr = f"{fmt(x0)}+({fmt(x1-x0)})*{u}"
y_expr = f"{fmt(y0)}+({fmt(y1-y0)})*{u}"
mask = f"\\left\\{{{fmt(s0)}<s,0\\right\\}}\\cdot\\left\\{{s<{fmt(s1)},0\\right\\}}"
return f"({x_expr})\\cdot{mask}", f"({y_expr})\\cdot{mask}"
# Normalize glyph to height 2, centered at origin
def normalize_path(path, scale, font_center):
new_segments = []
xs = []
for seg in path:
xs.append(seg.start.real)
xs.append(path[-1].end.real)
minx = min(xs)
maxx = max(xs)
glyph_center_x = (minx + maxx) / 2
def norm(z):
x = (z.real - glyph_center_x) * scale
y = (z.imag - font_center) * scale
return complex(x, y)
for seg in path:
if isinstance(seg, Line):
new_segments.append(Line(norm(seg.start), norm(seg.end)))
elif isinstance(seg, QuadraticBezier):
new_segments.append(QuadraticBezier(norm(seg.start), norm(seg.control), norm(seg.end)))
elif isinstance(seg, CubicBezier):
new_segments.append(CubicBezier(norm(seg.start), norm(seg.control1), norm(seg.control2), norm(seg.end)))
elif isinstance(seg, Arc):
new_segments.append(Arc(norm(seg.start), seg.radius * scale, seg.rotation, seg.large_arc, seg.sweep, norm(seg.end)))
else:
new_segments.append(Line(norm(seg.start), norm(seg.end)))
return new_segments
# Convert glyph to LaTeX X(s), Y(s)
def glyph_to_latex(path_string, scale, font_center):
path = parse_path(path_string)
path = normalize_path(path, scale, font_center)
n = len(path)
X_parts = []
Y_parts = []
for i, seg in enumerate(path):
s0 = i / n
s1 = (i + 1) / n
xlatex, ylatex = segment_to_latex(seg, s0, s1)
X_parts.append(xlatex)
Y_parts.append(ylatex)
return "+".join(X_parts), "+".join(Y_parts)
# Map local X(s), Y(s) into global t-space
def map_to_global_t(Xs, Ys, index):
k = index
if k == 0:
Xg = Xs.replace("s", "t")
Yg = Ys.replace("s", "t")
else:
Xg = Xs.replace("s", f"(t-{k})")
Yg = Ys.replace("s", f"(t-{k})")
mask = f"\\left\\{{{k}<t,0\\right\\}}\\cdot\\left\\{{t<{k+1},0\\right\\}}"
return f"({Xg})\\cdot{mask}", f"({Yg})\\cdot{mask}"
# Clean the expression
def simplify(expr):
# Remove +0 or -0
expr = re.sub(r"\+0(?![\d/])", "", expr)
expr = re.sub(r"-0(?![\d/])", "", expr)
# Remove *1 or 1*
expr = re.sub(r"\*1(?![\d/])", "", expr)
expr = re.sub(r"1\*(?![\d/])", "", expr)
# Collapse -- into +
expr = expr.replace("--", "+")
# Collapse +- into -
expr = expr.replace("+-", "-")
# Remove parentheses around pure numbers
expr = re.sub(r"\((\d+/\d+|\d+)\)", r"\1", expr)
return expr
# MAIN
def find_font_file(font_name):
fonts_dir = "C:/Windows/Fonts"
font_name_lower = font_name.lower()
for file in os.listdir(fonts_dir):
if file.lower().endswith((".ttf", ".otf")):
try:
path = os.path.join(fonts_dir, file)
f = TTFont(path)
names = f["name"].names
for record in names:
try:
name_str = record.toUnicode().lower()
if font_name_lower == name_str:
return path
except:
pass
except:
pass
return None
font_path = find_font_file(input("Enter the font you want to use: "))
if not font_path:
print("Font not found.")
exit()
font = TTFont(font_path)
glyph_set = font.getGlyphSet()
# Find center of font to normalize correctly
head = font["head"]
hhea = font["hhea"]
ascender = hhea.ascent
descender = hhea.descent
font_height = ascender - descender
scale = 2.0 / font_height
font_center = (ascender + descender) / 2
chars = input("Enter the characters you want the LaTeX for: ")
global_X_parts = []
global_Y_parts = []
for idx, ch in enumerate(chars):
glyph_name = font.getBestCmap().get(ord(ch))
if glyph_name:
glyph = glyph_set[glyph_name]
pen = SVGPathPen(glyph_set)
glyph.draw(pen)
path = pen.getCommands()
Xs, Ys = glyph_to_latex(path, scale, font_center)
Xs = simplify(Xs)
Ys = simplify(Ys)
Xg, Yg = map_to_global_t(Xs, Ys, idx)
global_X_parts.append(Xg)
global_Y_parts.append(Yg)
X_t = "+".join(global_X_parts)
Y_t = "+".join(global_Y_parts)
with open("expression.txt", "w", encoding="utf-8") as f:
f.write("X(t) = " + X_t)
f.write("\nY(t) = " + Y_t)
print(f"\nSaved output to file")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment