Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Differentiable Variable Fonts

This is an unofficial implementation of Differentiable Variable Fonts [Kinjal+ 2025]

arxiv

@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},
}

Usage

Setup

uv sync

Load a font and apply the specified delta

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()

default

moved

Backpropagate update the axis weights

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")

0

9

19

29

Differentiably render and optimize

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

optimize_sdf_loss

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages