Skip to content

Instantly share code, notes, and snippets.

@idbrii
Created March 1, 2026 05:28
Show Gist options
  • Select an option

  • Save idbrii/886083f04fec1039293d89c2aa756b36 to your computer and use it in GitHub Desktop.

Select an option

Save idbrii/886083f04fec1039293d89c2aa756b36 to your computer and use it in GitHub Desktop.
Preview Fonts - Preview text with installed or otf/ttf fonts.
"""ViewFont -- Preview characters in any system or custom font.
CC0 - https://creativecommons.org/public-domain/cc0/
Code mostly generated by Claude Opus 4.6, so I claim no copyright.
Similar in concept to [List Font
Families](https://stackoverflow.com/questions/39614027/list-available-font-families-in-tkinter#53717785)
but instead of displaying fonts with their name as a preview, shows custom
text. Also supports loading custom fonts.
"""
import ctypes
import os
import tkinter as tk
from tkinter import ttk, filedialog, font as tkfont
try:
from fontTools.ttLib import TTFont
except ImportError:
TTFont = False
FR_PRIVATE = 0x10
class Tooltip:
"""Simple hover tooltip for a widget."""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip = None
widget.bind("<Enter>", self._show)
widget.bind("<Leave>", self._hide)
def _show(self, event=None):
x = self.widget.winfo_rootx() + self.widget.winfo_width() // 2
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
self.tip = tk.Toplevel(self.widget)
self.tip.wm_overrideredirect(True)
self.tip.wm_geometry(f"+{x}+{y}")
label = tk.Label(
self.tip,
text=self.text,
background="#ffffe0",
relief=tk.SOLID,
borderwidth=1,
padx=4,
pady=2,
)
label.pack()
def _hide(self, event=None):
if self.tip:
self.tip.destroy()
self.tip = None
def load_font_file(path):
"""Load a .ttf/.otf file into the process and return its family name."""
path = os.path.abspath(path)
added = ctypes.windll.gdi32.AddFontResourceExW(path, FR_PRIVATE, 0)
if added == 0:
raise RuntimeError(f"Failed to load font: {path}")
tt = TTFont(path)
family = tt["name"].getDebugName(1)
tt.close()
return family
class ViewFontApp:
def __init__(self, root):
self.root = root
self.root.title("ViewFont")
self.root.geometry("800x600")
self.root.minsize(500, 400)
self.loaded_files = []
self._all_fonts = sorted(set(tkfont.families()), key=str.lower)
self._build_ui()
self._update_preview()
def _build_ui(self):
# Top bar: text input + size
top = ttk.Frame(self.root, padding=8)
top.pack(fill=tk.X)
ttk.Label(top, text="Text:").pack(side=tk.LEFT)
self.text_var = tk.StringVar(value="\u5203")
self.text_var.trace_add("write", lambda *_: self._update_preview())
text_entry = ttk.Entry(top, textvariable=self.text_var, width=30)
text_entry.pack(side=tk.LEFT, padx=(4, 16))
ttk.Label(top, text="Size:").pack(side=tk.LEFT)
self.size_var = tk.IntVar(value=72)
self.size_var.trace_add("write", lambda *_: self._update_preview())
size_spin = ttk.Spinbox(
top, from_=8, to=200, textvariable=self.size_var, width=5
)
size_spin.pack(side=tk.LEFT, padx=(4, 0))
# Font selection bar
font_bar = ttk.Frame(self.root, padding=(8, 0, 8, 8))
font_bar.pack(fill=tk.X)
ttk.Label(font_bar, text="Font:").pack(side=tk.LEFT)
self.font_var = tk.StringVar()
self.font_combo = ttk.Combobox(
font_bar, textvariable=self.font_var, values=self._all_fonts, width=40
)
self.font_combo.pack(side=tk.LEFT, padx=(4, 4))
self.font_combo.bind("<<ComboboxSelected>>", lambda *_: self._update_preview())
self.font_combo.bind("<KeyRelease>", self._filter_fonts)
if self._all_fonts:
self.font_var.set(self._all_fonts[0])
btn_next = ttk.Button(font_bar, text="\u25b6", width=3, command=self._next_font)
btn_next.pack(side=tk.LEFT, padx=(0, 8))
Tooltip(btn_next, "Next font")
btn_load = ttk.Button(font_bar, text="Load File...", command=self._load_file)
btn_load.pack(side=tk.LEFT)
if TTFont:
Tooltip(btn_load, "Load a font file (.ttf/.otf) that isn't installed.")
else:
btn_load.configure(state=tk.DISABLED)
Tooltip(btn_load, "Requires pip install fonttools")
self.show_name_var = tk.BooleanVar(value=True)
self.show_name_var.trace_add("write", lambda *_: self._update_preview())
ttk.Checkbutton(
font_bar, text="Show font name", variable=self.show_name_var
).pack(side=tk.LEFT, padx=(8, 0))
# Preview area
preview_frame = ttk.LabelFrame(self.root, text="Preview", padding=8)
preview_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0, 4))
self.preview_text = tk.Text(
preview_frame,
wrap=tk.WORD,
bg="white",
relief=tk.SUNKEN,
cursor="arrow",
state=tk.DISABLED,
)
self.preview_text.pack(fill=tk.BOTH, expand=True)
# Status bar
self.status_var = tk.StringVar(value="Ready")
status_bar = ttk.Label(
self.root, textvariable=self.status_var, relief=tk.SUNKEN, padding=(8, 2)
)
status_bar.pack(fill=tk.X, side=tk.BOTTOM)
def _filter_fonts(self, event=None):
"""Filter the font dropdown as the user types."""
typed = self.font_var.get().lower()
if typed:
filtered = [f for f in self._all_fonts if typed in f.lower()]
else:
filtered = self._all_fonts
self.font_combo["values"] = filtered
def _next_font(self):
"""Cycle to the next font in the list."""
current = self.font_var.get()
fonts = list(self.font_combo["values"])
if not fonts:
fonts = self._all_fonts
try:
idx = fonts.index(current)
idx = (idx + 1) % len(fonts)
except ValueError:
idx = 0
self.font_var.set(fonts[idx])
self._update_preview()
def _update_preview(self):
"""Redraw the preview with current text, font, and size."""
try:
size = self.size_var.get()
except (tk.TclError, ValueError):
size = 72
family = self.font_var.get()
text = self.text_var.get() or "\u5203"
self.preview_text.configure(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.tag_configure(
"name", font=(family, max(size // 4, 10)), justify=tk.CENTER
)
self.preview_text.tag_configure("body", font=(family, size), justify=tk.CENTER)
if self.show_name_var.get():
self.preview_text.insert(tk.END, family + "\n", "name")
self.preview_text.insert(tk.END, text, "body")
self.preview_text.configure(state=tk.DISABLED)
def _load_file(self):
"""Open a font file and add it to the dropdown."""
path = filedialog.askopenfilename(
title="Select Font File",
filetypes=[
("Font files", "*.ttf *.otf *.ttc *.otc"),
("All files", "*.*"),
],
)
if not path:
return
try:
family = load_font_file(path)
# Refresh tkinter font list to include the new font.
self._all_fonts = sorted(set(tkfont.families()), key=str.lower)
self.font_combo["values"] = self._all_fonts
self.font_var.set(family)
self._update_preview()
self.loaded_files.append(os.path.basename(path))
self.status_var.set(f"Loaded: {', '.join(self.loaded_files)}")
except Exception as e:
self.status_var.set(f"Error: {e}")
def main():
root = tk.Tk()
ViewFontApp(root)
root.mainloop()
if __name__ == "__main__":
main()
@idbrii
Copy link
Author

idbrii commented Mar 1, 2026

Code could probably be simpler and more elegant, but works better than most free font preview tools I found.

It took 24 Premium requests (API time spent: 7m 50s). I wonder how much processing that required.

Looks like this:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment