Last active
January 31, 2026 16:49
-
-
Save LDGT123/1ff16f5b1d82bc7f66b7dc05aec357d8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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