Skip to main content

Fontforge Variable Font Plugin

A FontForge_plugin to create a variable font

As of October 2025, Fontforge supports legacy (maybe obsolete) multiple master formats but not yet OpenType variable fonts. This plugin adds frontend of fontmake and fonttools so that variable fonts can be created through Fontforge interface.

This module can also export to WOFF2; in this case the woff2 tool will be used as backend.

This module requires Python 3.10 or later.

Install

pip3 install fontforge-variable-font

Make sure Fontforge Python module is usable

In interactive mode of Python, run:

import fontforge

If it raises ModuleNotFoundError exception, install Fontforge first. If installed, make sure the build option set that the Python module gets also installed. If already so, Python interpreter does not recognize the module path where the required module.

export PYTHONPATH=/path/to/fontforge/python/module:$PYTHONPATH

Usage

Interactive usage

As a Fontforge plugin, fontforgeVF adds 'Variable Font' submenu to 'Tools' menu which is dedicated for plugins.

  • Variable Font
    • Open a variable font
      • By named instance...
      • By parameter...
    • Generate a variable font...
    • Design axes...
    • Named instances...
    • Delete VF info

Open a variable font

Shows a dialog to open a variable font

By named instance

Open file dialog is shown first. If a variable font is selected, then another dialog is shown to select (one or more) named instances. If a non-variable font is selected, simply opens that font.

By parameter

Like above, but the second dialog is not to select a named instance, but to specify design axis parameters.

Generate a variable font

Shows a dialog where you can set output file name and other options.

In order to build a variable font, SFD must be converted into UFO and create a designspace document. This plugin will do this first, and then required modification. The required files will be created in a temporary directory, and deleted after everything is done. So users won't see intermediate files.

Fontforge may export with postscriptIsFixedPitch flag clear when it should be set. The plugin checks if monospaced font is intended and fix the flag. Unlike Fontforge itself, only U+0020 to U+007E will be checked their width, because combining marks may have zero width even for monospaced fonts.

In a feature file, 'aalt' feature is specially treated. Fontforge may export incompatible 'aalt' feature (concretely 'script' or 'language' instructions must not be included unlike other features.) This function fix this first.

Currently available options:

  • Remove nested refs: Tell fontmake to decompose nested references into simple ones. Nested references are known to cause problems in certain environments.
  • Add 'aalt' feature: Calculate and output 'aalt' feature to UFO.

Design axes

Shows a dialog where you can set design axes.

This master

This section is needed for all masters.

For active font as one of VF masters, sets position in each design axis of VF master. Leave unset for unused axes. Registered axes can use default values which refers font properties.

  • Italic: default value is whether font.italicangle is negative. This axis is boolean: you choose the master is for italic or not. Seldom used together with slant axis.
  • Optical size: can default to font.design_size. Set in points. Must be positive.
  • Slant: can default to font.italicangle. 0 if upright, negative if oblique. This value is hardly positive (left-slanted.)
  • Width: can default using font.os2_width. 100 if normal width, less if condensed, greater if expanded. Must be positive.
  • Weight: can default to font.os2_weight. 400 if regular weight, 700 if bold. The minimum is 1 (hairline thin) and the maximum is 999 (extreme bold.)
  • Custom axes: there is a room for 3 user-defined axes. No default values.
Custom axes

This section is needed for default master (choose one master as default.)

Sets the tag for each custom axis. A tag must be up to 4-letter alphanumeric. No known axis tags use less than 4 letters; if it happens, pad with trailing space. Leave them blank if not used.

Axis order

This section is needed for default master (choose one master as default.)

Sets the order of design axes.

Axis map

This section is needed for default master (choose one master as default.)

Maps user position to design position.

Input must be comma-separated values and even number of elements. Each pair consists of user and design positions in this order.

Axis name

This section is needed for default master (choose one master as default.)

Names the design axes. For predefined axes can use default name. Custom axes must be named if used.

  • Axis name: name of axis itself.
  • Labels: comma-separated list which consists of multiple of 4 of elements. Leading and trailing spaces will be trimmed. Every group of 4 elements:
    • Axis value
    • Flags
      • 0: Neither
      • 1: OLDER_SIBLING_FONT_ATTRIBUTE
      • 2: ELIDABLE_AXIS_VALUE_NAME
      • 3: Both
    • Linked value if exist
    • Name
Localized names

This section is needed for default master (choose one master as default.)

Design axes can have translated names. Each page for each language. Set language code before you use. Choose a language from the list.

By default there is a room for 8 languages, but this will be extended if already more than 4 languages are defined.

  • Axis name: name of axis itself.
  • Labels: comma-separated list which consists of even number of elements. Leading and trailing spaces will be trimmed. Every pair of elements:
    • Axis value
    • Name

Instance list

Shows a dialog where you can set named instances. Instance list is needed for default master (choose one master as default.)

Instance

At these pages you can set PostScript name, subfamily name, and associated design positions on each axis.

By default the pages are named 'Instance 1' and so on, but will be same as subfamily name if already set.

By default there is a room for 8 instances, but this will be extended if already more than 4 instances are defined.

Localized names

Instances can have translated names. Each page (or group or pages) for each language. Choose a language from the list first. If there are already 13 instances or more, multiple pages for each language.

By default there is a room for 8 languages, but this will be extended if already more than 4 languages are defined.

Delete VF info

Deletes VF data.

Hooks

This plugin installs new/open font hooks which does:

  • sets font generation hooks to output VF if metadata exists
    • If you export a TTF or a WOFF2 when VF metadata exists, you will be asked if you intend a VF. In this case too, all the masters must be opened beforehands, however unlike the dedicated menu, VF-specific options or italic counterpart cannot be set.
    • For technical reason, first the static font gets exported as usual, then VF overwrites it. Failed attempt of exporting a VF leaves the static font.
  • loads VF-specific metadata if available
    • If you load a variable font from the ordinary 'load' menu, you will be asked if you will open additional instances and which one(s.)

Script usage

As a Python module, in addition to fontforge module, scripting to export variable fonts from SFD projects will be possible.

import fontforge
import fontforgeVF

# Open all masters
fontCL = fontforge.open('MyFont-UltraCondensed-ExtraLight.sfd')
fontCB = fontforge.open('MyFont-UltraCondensed-ExtraBold.sfd')
fontXL = fontforge.open('MyFont-UltraExpanded-ExtraLight.sfd')
fontXB = fontforge.open('MyFont-UltraExpanded-ExtraBold.sfd')

# Open an instance from an existing variable font
font1 = fontforgeVF.openVariableFont('MyFont[wdth,wght].ttf', {'wdth': 100, 'wght': 400})  # by parameters
font2 = fontforgeVF.openVariableFont('MyFont[wdth,wght].ttf', 'Regular')  # named instance
font3 = fontforgeVF.openVariableFont('MyFont[wdth,wght].ttf', 2)  # list index (instances are listed in 'fvar' table)

# Set VF-specific metadata
fontforgeVF.initPersistentDict(fontCL)
fontforgeVF.setVFValue(fontCL, "axes.wght.active", True)
fontforgeVF.setVFValue(fontCL, "axes.wght.useDefault", False)
fontforgeVF.setVFValue(fontCL, "axes.wght.value", 200)
fontforgeVF.setVFValue(fontCL, "axes.wdth.active", True)
fontforgeVF.setVFValue(fontCL, "axes.wdth.useDefault", False)
fontforgeVF.setVFValue(fontCL, "axes.wdth.value", 50)
fontforgeVF.setVFValue(fontCL, "axes.ital.active", True)
fontforgeVF.setVFValue(fontCL, "axes.ital.useDefault", False)
fontforgeVF.setVFValue(fontCL, "axes.ital.value", False)

fontforgeVF.initPersistentDict(fontCB)
fontforgeVF.setVFValue(fontCB, "axes.wght.active", True)
fontforgeVF.setVFValue(fontCB, "axes.wght.useDefault", True)
fontforgeVF.setVFValue(fontCB, "axes.wdth.active", True)
fontforgeVF.setVFValue(fontCB, "axes.wdth.useDefault", True)
fontforgeVF.setVFValue(fontCB, "axes.ital.active", True)
fontforgeVF.setVFValue(fontCB, "axes.ital.useDefault", True)

fontforgeVF.initPersistentDict(fontXL)
fontforgeVF.setVFValue(fontXL, "axes.wght.active", True)
fontforgeVF.setVFValue(fontXL, "axes.wght.useDefault", True)
fontforgeVF.setVFValue(fontXL, "axes.wdth.active", True)
fontforgeVF.setVFValue(fontXL, "axes.wdth.useDefault", True)
fontforgeVF.setVFValue(fontXL, "axes.ital.active", True)
fontforgeVF.setVFValue(fontXL, "axes.ital.useDefault", True)

fontforgeVF.initPersistentDict(fontXB)
fontforgeVF.setVFValue(fontXB, "axes.wght.active", True)
fontforgeVF.setVFValue(fontXB, "axes.wght.useDefault", False)
fontforgeVF.setVFValue(fontXB, "axes.wght.value", 800)
fontforgeVF.setVFValue(fontXB, "axes.wdth.active", True)
fontforgeVF.setVFValue(fontXB, "axes.wdth.useDefault", False)
fontforgeVF.setVFValue(fontXB, "axes.wdth.value", 200)
fontforgeVF.setVFValue(fontXB, "axes.ital.active", True)
fontforgeVF.setVFValue(fontXB, "axes.ital.useDefault", False)
fontforgeVF.setVFValue(fontXB, "axes.ital.value", False)

# Font-family-wide metadata
# Here assume fontCL as the default font
fontforgeVF.setVFValue(fontCL, "axes.wght.name", "Weight")
fontforgeVF.setVFValue(fontCL, "axes.wght.order", 1)
fontforgeVF.setVFValue(fontCL, "axes.wght.localNames.0x407", "Strichstärke")
fontforgeVF.setVFValue(fontCL, "axes.wdth.map", [(200, 200), (400, 350), (800, 800)])
fontforgeVF.setVFValue(fontCL, "axes.wdth.name", "Width")
fontforgeVF.setVFValue(fontCL, "axes.wdth.order", 0)
fontforgeVF.setVFValue(fontCL, "axes.wdth.map[0]", (50, 50))
fontforgeVF.setVFValue(fontCL, "axes.wdth.map[1]", (200, 200))
fontforgeVF.setVFValue(fontCL, "axes.wdth.localNames.0x407", "Laufweite")  # 0x407 stands for German (Germany)
fontforgeVF.setVFValue(fontCL, "axes.ital.name", "Italic")
fontforgeVF.setVFValue(fontCL, "axes.ital.order", 2)
fontforgeVF.setVFValue(fontCL, "axes.ital.localNames.0x407", "Kursiv")

fontforgeVF.setVFValue(fontCL, "axes.wght.labels.200.name", "Extra Light")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.300.name", "Light")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.400.name", "Regular")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.400.olderSibling", False)
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.400.elidable", True)
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.400.linkedValue", 700)
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.500.name", "Medium")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.600.name", "Semibold")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.700.name", "Bold")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.800.name", "Extra Bold")

fontforgeVF.setVFValue(fontCL, "axes.wght.labels.200.localNames.0x407", "Extramager")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.300.localNames.0x407", "Mager")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.400.localNames.0x407", "Standard")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.500.localNames.0x407", "Mittel")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.600.localNames.0x407", "Halbfett")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.700.localNames.0x407", "Fett")
fontforgeVF.setVFValue(fontCL, "axes.wght.labels.800.localNames.0x407", "Extrafett")

# User-defined axes (custom1 to custom3)
fontforgeVF.setVFValue(fontCL, "axes.custom1.active", True)
fontforgeVF.setVFValue(fontCL, "axes.custom1.value", 15)
fontforgeVF.setVFValue(fontCL, "axes.custom1.tag", "abc")  # needed for custom axes; will be padded with space
fontforgeVF.setVFValue(fontCL, "axes.custom1.name", "User-defined axis")
fontforgeVF.setVFValue(fontCL, "axes.custom1.order", 3)
fontforgeVF.setVFValue(fontCL, "axes.custom1.localNames.0x407", "Benutzerdefinierte Achse")

# Instances
fontforgeVF.setVFValue(fontCL, "instances[0].psName", "MyFont-ExtraLight")
fontforgeVF.setVFValue(fontCL, "instances[0].name", "ExtraLight")
fontforgeVF.setVFValue(fontCL, "instances[0].wght", 200)
fontforgeVF.setVFValue(fontCL, "instances[0].wdth", 100)
fontforgeVF.setVFValue(fontCL, "instances[0].ital", False)
fontforgeVF.setVFValue(fontCL, "instances[0].localNames.0x407", "Extramager")

# Export TTF
fontforgeVF.export(fontCL, 'MyFont.ttf')
fontforgeVF.export(fontCL, 'MyFont.ttf', 'MyFont-Italic.ttf')  # if ``ital`` axis enabled
fontforgeVF.export(fontCL, 'MyFont.ttf',
                   decomposeNestedRefs=True,
                   decomposeTransformedRefs=True,
                   addAalt=True)  # these options default to False

# Export Webfont
fontforgeVF.export(fontCL, 'MyFont.woff2')

# In case you want to drop the VF info
fontforgeVF.deleteVFInfo(fontCL)

Some example of language codes

Code Language
0x401 Arabic (Saudi Arabia)
0xc01 Arabic (Egypt)
0x403 Catalan
0x404 Chinese (Taiwan)
0x804 Chinese (Mainland)
0xc04 Chinese (Hong Kong)
0x407 German (Germany)
0x807 German (Switzerland)
0x408 Greek
0x409 English (US) (default)
0x809 English (UK)
0xc09 English (Australia)
0x1009 English (Canada)
0x1409 English (New Zealand)
0x80a Spanish (Mexico)
0xc0a Spanish (Spain, modern sort)
0x40c French (France)
0x80c French (Belgium)
0xc0c French (Canada)
0x100c French (Switzerland)
0x40d Hebrew
0x410 Italian (Italy)
0x810 Italian (Switzerland)
0x411 Japanese
0x412 Korean
0x413 Dutch
0x813 Flemish
0x416 Portuguese (Brazil)
0x816 Portuguese (Portugal)
0x417 Romansh
0x419 Russian
0x420 Urdu
0x439 Hindi

Metadata

Release files for fontforge-variable-font 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fontforge-variable-font 0.5.0
File Size Uploaded
fontforge_variable_font-0.5.0.tar.gz 39.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fontforge-variable-font 0.5.0
File Interpreter ABI Platform
fontforge_variable_font-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 73.7 kB

Release files / fontforge_variable_font-0.5.0.tar.gz

Download URL fontforge_variable_font-0.5.0.tar.gz
Size 39.7 kB
Tags Source
SHA-256 checksum
How to use checksums
259595452bfd374ff8cd1ae7bfd8a67f16119906bb4a1aa88585205c563cdf79
BLAKE2b-256 checksum
How to use checksums
b1c84d3cdeaa6d717ca2ff6b3ea3ec7e5e03ffd2148abbd8732e940254701c89
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / fontforge_variable_font-0.5.0-py3-none-any.whl

Download URL fontforge_variable_font-0.5.0-py3-none-any.whl
Size 34.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef99bf0366055a5873e044c76e600c0b7d459de70669934f3c2f6d09d2494bee
BLAKE2b-256 checksum
How to use checksums
5aeec0765d3e3194950bb78f9d58e24af7e3f7fedf7f7008fea0ab90acec0734
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page