This is an unofficial implementation of Differentiable Variable Fonts [Kinjal+ 2025]
@misc{parikh2025differentiablevariablefonts,
title={Differentiable Variable Fonts},
author={Kinjal Parikh and Danny M. Kaufman and David I. W. Levin and Alec Jacobson},
year={2025},
eprint={2510.07638},
archivePrefix={arXiv},
primaryClass={cs.GR},
url={https://arxiv.org/abs/2510.07638},
}
uv sync
import torch
from matplotlib import pyplot as plt
from dvfonts.io.cmap import read_cmap
from dvfonts.io.gvar_dense import read_dense_gvar_for_glyph
from dvfonts.io.load_font import load_font, read_font_meta, read_glyph_outline
from dvfonts.variation.apply_gvar import apply_gvar_torch
# load font and metadata
font_path = "fonts/AfacadFlux.ttf"
font = load_font(font_path)
cmap = read_cmap(font)
meta = read_font_meta(font, font_path)
print("UPEM:", meta.units_per_em)
print("Axes:")
for a in meta.axes:
print(" ", a.tag, a.min_value, a.default_value, a.max_value, "-", a.name)
# test a glyph
c = "B"
glyph_name = cmap.glyph_name(c)
print("\nGlyph:", glyph_name)
outline = read_glyph_outline(font, glyph_name)
if outline is None:
print(" (no outline)")
else:
print(" points:", len(outline.coordinates), "contours:", len(outline.end_pts))
print(" first point:", outline.coordinates[0], "flag:", outline.flags[0])
dense_gv = read_dense_gvar_for_glyph(font, outline)
print("tuples:", len(dense_gv.tuples))
if not dense_gv.tuples:
raise SystemExit("No gvar tuples for this glyph (try another character).")
print("points in outline:", len(outline.coordinates))
print("dense deltas len:", len(dense_gv.tuples[0].deltas))
base_xy = torch.tensor(outline.coordinates, dtype=torch.float32)
print("axes:", [a.tag for a in meta.axes])
default_loc = {a.tag: a.default_value for a in meta.axes}
loc2 = dict(default_loc)
if "wght" in loc2:
loc2["wght"] = min(
next(a.max_value for a in meta.axes if a.tag == "wght"),
loc2["wght"] + 300,
)
elif meta.axes:
a0 = meta.axes[0]
loc2[a0.tag] = (a0.min_value + a0.max_value) * 0.5
res1 = apply_gvar_torch(
axes=meta.axes, base_xy=base_xy, dense_gvar=dense_gv, location=default_loc
)
res2 = apply_gvar_torch(
axes=meta.axes, base_xy=base_xy, dense_gvar=dense_gv, location=loc2
)
coords1 = [(int(round(x)), int(round(y))) for x, y in res1.var_xy.tolist()]
coords2 = [(int(round(x)), int(round(y))) for x, y in res2.var_xy.tolist()]
out1 = outline.with_coordinates(coords1)
out2 = outline.with_coordinates(coords2)
fig1, ax1 = out1.plot(
title=f"{c} @ default", show_points=False, show_control_points=False
)
fig2, ax2 = out2.plot(
title=f"{c} @ moved axis", show_points=False, show_control_points=False
)
print("location1:", default_loc)
print("location2:", loc2)
plt.show()import torch
from torch.optim import Adam
from dvfonts.io.cmap import read_cmap
from dvfonts.io.gvar_dense import read_dense_gvar_for_glyph
from dvfonts.io.load_font import load_font, read_font_meta, read_glyph_outline
from dvfonts.variation.apply_gvar import apply_gvar_torch
# load font and metadata
font_path = "fonts/AfacadFlux.ttf"
font = load_font(font_path)
cmap = read_cmap(font)
meta = read_font_meta(font, font_path)
for a in meta.axes:
print(" ", a.tag, a.min_value, a.default_value, a.max_value, "-", a.name)
c = "B"
glyph_name = cmap.glyph_name(c)
outline = read_glyph_outline(font, glyph_name)
dense_gv = read_dense_gvar_for_glyph(font, outline)
base_xy = torch.tensor(outline.coordinates, dtype=torch.float32)
loc_weight = torch.tensor(300.0, requires_grad=True)
optimizer = Adam([loc_weight], lr=9.5)
loc = {"wght": loc_weight}
for step in range(30):
result = apply_gvar_torch(
axes=meta.axes, base_xy=base_xy, dense_gvar=dense_gv, location=loc
)
loss = -result.var_xy.sum()
loss.backward()
print(loc_weight.grad)
optimizer.step()
optimizer.zero_grad()
if step == 0 or (step + 1) % 10 == 0:
print(f"Step {step}: loss={loss.item():.4f}, loc={loc['wght'].item():.1f}")
coords = [(int(round(x)), int(round(y))) for x, y in result.var_xy.tolist()]
out = outline.with_coordinates(coords)
fig, ax1 = out.plot(
title=f"{c} @ wght={loc['wght'].item():.1f}",
show_points=False,
show_control_points=False,
)
fig.savefig(f"asset/optimized_glyph_{c}_{step}.png")
Combined with a differentiable renderer, the path from a axis weight to the bitmap image gets differentiable.
uv run python test_optimize_sdf_loss.py






