A variable font stores the default design and, separately, deltas that move outline
points and metrics in regions of the design space set up by
fvar. There are two containers
for deltas. The tuple variation store groups deltas by region and packs them
tightly; gvar (TrueType outlines) and cvar (hinting values)
use it. The item variation store groups deltas by the item they adjust, so one
value can be looked up quickly; HVAR, VVAR, MVAR,
GDEF (for GPOS and JSTF), BASE,
COLR and CFF2 use it. All coordinates here are normalised,
stored as F2DOT14; how a region gives a scalar is on the
fvar page.
Tuple variation store
A tuple is a position in the design space: one F2DOT14
coordinate per axis. A tuple variation store is a header, a list of tuple variation
headers (one per region), and a block of serialised data.
| Type | Name | Description |
|---|---|---|
uint16 | tupleVariationCount | Low 12 bits (0x0FFF): number of regions, 1 to 4095. Bit 15 (0x8000) SHARED_POINT_NUMBERS: the data starts with point numbers shared by the regions. Bits 12–14 reserved. |
Offset16 | dataOffset | Where the serialised data starts, from the start of this store (in cvar, from the start of the table). |
| TupleVariationHeader × count | tupleVariationHeaders | Variable-length headers, below. |
In cvar the store is preceded by uint16 majorVersion (1)
and uint16 minorVersion (0); in gvar there is one store per
glyph.
TupleVariationHeader
| Type | Name | Description |
|---|---|---|
uint16 | variationDataSize | Bytes of serialised data for this region (its private point numbers and its deltas). |
uint16 | tupleIndex | Flags and a shared-tuple index, below. |
| Tuple | peakTuple | Present if EMBEDDED_PEAK_TUPLE; always present in cvar. |
| Tuple | intermediateStartTuple | Present if INTERMEDIATE_REGION. |
| Tuple | intermediateEndTuple | Present if INTERMEDIATE_REGION. |
| Mask | Name | Meaning |
|---|---|---|
0x8000 | EMBEDDED_PEAK_TUPLE | The peak tuple follows; the index bits are ignored. |
0x4000 | INTERMEDIATE_REGION | Start and end tuples follow, for a region that does not run from the peak to 0. |
0x2000 | PRIVATE_POINT_NUMBERS | This region’s data starts with its own point numbers instead of using the shared ones. |
0x1000 | reserved | 0. |
0x0FFF | TUPLE_INDEX_MASK | Index of the peak tuple in gvar’s shared tuples, when not embedded. |
A header is 4 bytes, plus 2 × axisCount for an embedded peak, plus
4 × axisCount for intermediate start and end. Without intermediate
tuples, the region runs from the peak to 0 on each axis.
Packed point numbers
Which points (or cvt entries) the deltas belong to. First a count:
- a first byte of 0: every point, including the four phantom points (or every
cvtentry), with no list following; - 1 to 127: that many points;
- high bit set: the count is the two bytes as a
uint16with the high bit cleared, so81 22is 290.
Then runs, each a control byte and its values: bit 7 (0x80,
POINTS_ARE_WORDS) means uint16 values, otherwise
uint8; the low 7 bits (0x7F) are the number of values
minus 1. Each value is the difference from the previous point number (the very first
from 0), so numbers only increase. A repeated number gets its deltas added
together.
Packed deltas
One delta per point number, or in gvar two: all the x deltas, then all
the y deltas. There is no count; reading stops when enough have been read. Runs start
with a control byte:
| Mask | Name | Meaning |
|---|---|---|
0x80 | DELTAS_ARE_ZERO | The run is all zeros and has no data. |
0x40 | DELTAS_ARE_WORDS | int16 values; otherwise int8. |
0x3F | DELTA_RUN_COUNT_MASK | Number of deltas in the run minus 1. |
So 03 0A 97 00 C6 87 41 10 22 FB 34 is four bytes (10, −105, 0, −58),
eight zeros, and two words (4130, −1228).
gvar
Glyph variations for glyf outlines only (CFF2 keeps its deltas inside
its charstrings).
| Offset | Type | Name | Description |
|---|---|---|---|
0 | uint16 | majorVersion | 1. |
2 | uint16 | minorVersion | 0. |
4 | uint16 | axisCount | Must equal fvar’s. |
6 | uint16 | sharedTupleCount | Number of shared peak tuples. |
8 | Offset32 | sharedTuplesOffset | From the start of gvar. |
12 | uint16 | glyphCount | Must equal the font’s glyph count. |
14 | uint16 | flags | Bit 0: offsets below are 32-bit; clear, 16-bit offsets stored halved (multiply by 2). |
16 | Offset32 | glyphVariationDataArrayOffset | From the start of gvar. |
20 | Offset × (glyphCount + 1) | glyphVariationDataOffsets | From the start of that array, like loca; a glyph whose two offsets are equal has no variations. |
- Point numbers count the glyph’s outline points, then four phantom points: left side bearing, right side bearing (the advance), top side bearing and bottom side bearing. For the first two only x deltas are used, for the last two only y.
- In a composite glyph, the numbers before the phantom points are components, in
order. A component’s deltas move its x and y offset, only if it is placed by offset
(
ARGS_ARE_XY_VALUES); they never change its scale or matrix. Components are processed from the most deeply nested up, and a component withUSE_MY_METRICSsupplies the composite’s phantom points. - Because metrics need the whole outline processed this way, fonts should also carry
HVAR(andVVARfor vertical text).
Inferred deltas
When a region lists only some points of a simple glyph, the others get deltas worked out per contour, separately for x and y, from the original (default) coordinates:
- A contour with no listed points is not moved.
- For each unlisted point, find the nearest listed points before and after it in the contour, wrapping round the contour’s ends.
- If those two have the same coordinate: use their delta if they agree, otherwise 0.
- If the point’s coordinate lies outside the two coordinates, take the delta of the neighbour nearer to it in that direction.
- Otherwise interpolate: t = (target − ref) / (comp − ref), delta = (1 − t) × refDelta + t × compDelta.
A contour with a single listed point therefore moves rigidly with it. Composite
glyphs and cvar never infer deltas.
cvar
Deltas for the cvt
control values, so TrueType hinting follows the design: version 1.0, then one tuple
variation store as above, whose “point numbers” are cvt indexes, with one
delta each, embedded peak tuples always and no shared tuples.
Item variation store
| Structure | Type | Name | Description |
|---|---|---|---|
| ItemVariationStore | uint16 | format | 1. |
Offset32 | variationRegionListOffset | From the start of the store. | |
uint16 | itemVariationDataCount | Number of data subtables. | |
Offset32 × count | itemVariationDataOffsets | From the start of the store; NULL means no variation for that outer index. | |
| VariationRegionList | uint16 | axisCount | Must equal fvar’s. |
uint16 | regionCount | Below 32,768 (high bit reserved). | |
| record × regionCount | variationRegions | Each region is, per axis in fvar order, F2DOT14 startCoord, peakCoord, endCoord. | |
| ItemVariationData | uint16 | itemCount | Number of rows (delta sets). |
uint16 | wordDeltaCount | Low 15 bits: how many leading columns use the longer type. Bit 15 LONG_WORDS: the types are int32/int16 instead of int16/int8 (only for COLR). | |
uint16 | regionIndexCount | Number of columns. | |
uint16 × count | regionIndexes | Which region each column is. | |
| rows × itemCount | deltaSets | Each row: the long deltas, then the short ones; regionIndexCount + wordCount bytes, doubled with LONG_WORDS. |
An item is found by an outer index (which ItemVariationData) and an inner index (which
row). The pair 0xFFFF/0xFFFF means no variation. The item’s
value is its default plus each delta in the row times its column region’s scalar.
Deltas are stored plainly, not packed.
DeltaSetIndexMap
| Type | Name | Description |
|---|---|---|
uint8 | format | 0 or 1. |
uint8 | entryFormat | Low 4 bits (0x0F): inner index bit count − 1. Bits 4–5 (0x30): entry size in bytes − 1. Bits 6–7 reserved. |
uint16 (format 0) or uint32 (format 1) | mapCount | Number of entries. |
uint8 × size × count | mapData | Big-endian entries. |
For an entry e with b = (entryFormat & 0x0F) + 1: outer index = e >> b, inner index = e & (2b − 1). An index past the end uses the last entry. Format 0 is the old single 16-bit field, whose high byte was always 0.
HVAR and VVAR
Glyph metric variations, so layout can get advances without processing outlines.
They are required with CFF2 outlines and recommended with
gvar.
| HVAR field | VVAR field | Type | Description |
|---|---|---|---|
majorVersion, minorVersion | same | uint16 × 2 | 1.0. |
itemVariationStoreOffset | same | Offset32 | The deltas. |
advanceWidthMappingOffset | advanceHeightMappingOffset | Offset32 | DeltaSetIndexMap for advances, or NULL: then outer index 0 and inner index = glyph ID. |
lsbMappingOffset | tsbMappingOffset | Offset32 | Map for left (top) side bearings, or NULL for none. |
rsbMappingOffset | bsbMappingOffset | Offset32 | Map for right (bottom) side bearings, or NULL. |
| – | vOrgMappingOffset | Offset32 | Map for VORG vertical origins, or NULL. |
All offsets count from the start of the table.
MVAR
| Offset | Type | Name | Description |
|---|---|---|---|
0 | uint16 × 2 | version | 1.0. |
4 | uint16 | reserved | 0. |
6 | uint16 | valueRecordSize | Size of a value record (8); readers step by it. |
8 | uint16 | valueRecordCount | May be 0. |
10 | Offset16 | itemVariationStoreOffset | 0 when there are no records. |
12 | record × count | valueRecords | Each Tag valueTag, uint16 deltaSetOuterIndex, uint16 deltaSetInnerIndex; sorted by tag. |
A tag that is absent means that value does not vary. Defined tags are lower case; private tags are upper case only and ignored by software that does not know them.
| Tag | Varies | Tag | Varies |
|---|---|---|---|
hasc | OS/2.sTypoAscender | sbxs | OS/2.ySubscriptXSize |
hdsc | OS/2.sTypoDescender | sbys | OS/2.ySubscriptYSize |
hlgp | OS/2.sTypoLineGap | sbxo | OS/2.ySubscriptXOffset |
hcla | OS/2.usWinAscent | sbyo | OS/2.ySubscriptYOffset |
hcld | OS/2.usWinDescent | spxs | OS/2.ySuperscriptXSize |
vasc | vhea.ascent | spys | OS/2.ySuperscriptYSize |
vdsc | vhea.descent | spxo | OS/2.ySuperscriptXOffset |
vlgp | vhea.lineGap | spyo | OS/2.ySuperscriptYOffset |
hcrs | hhea.caretSlopeRise | strs | OS/2.yStrikeoutSize |
hcrn | hhea.caretSlopeRun | stro | OS/2.yStrikeoutPosition |
hcof | hhea.caretOffset | unds | post.underlineThickness |
vcrs | vhea.caretSlopeRise | undo | post.underlinePosition |
vcrn | vhea.caretSlopeRun | gsp0–gsp9 | rangeMaxPPEM of gasp ranges 0 to 9 |
vcof | vhea.caretOffset | xhgt | OS/2.sxHeight |
cpht | OS/2.sCapHeight |
The last gasp range always ends at 0xFFFF and is never
varied, so there are at most one fewer gsp tags than ranges.
usWeightClass, usWidthClass and italicAngle have
no tags: they follow the wght, wdth and slnt
axes.
Example
SF Mono (SFNSMono.ttf, macOS), whose axes are shown on the
fvar page. Its 66-byte
HVAR:
0001CF74 00 01 00 00 00 00 00 14 00 00 00 00 00 00 00 00
0001CF84 00 00 00 00 00 01 00 00 00 0C 00 01 00 00 00 28
0001CF94 00 02 00 02 00 00 40 00 40 00 00 00 00 00 00 00
0001CFA4 00 00 00 00 00 00 00 00 40 00 40 00 06 60 00 00
0001CFB4 00 00
| Bytes | Meaning |
|---|---|
00 01 00 00 | Version 1.0. |
00 00 00 14 | Item variation store at 20. |
| 12 zero bytes | No advance, left or right maps: glyph ID is the inner index. |
00 01 00 00 00 0C 00 01 00 00 00 28 | Store format 1; region list at +12; one data subtable at +40. |
00 02 00 02 | 2 axes, 2 regions. |
00 00 40 00 40 00 · 00 00 00 00 00 00 | Region 0: axis 0 from 0, peak 1, end 1; axis 1 not involved. |
00 00 00 00 00 00 · 00 00 40 00 40 00 | Region 1: axis 1 from 0 to 1, peak 1. |
06 60 00 00 00 00 | Data: 1,632 rows (one per glyph), no word columns, no regions. |
With no regions selected, every row is empty: as befits a monospaced font, no advance
width varies. Its gvar starts 00 01 00 00 00 02 00 02 00 00 0C D6
06 60 00 00 00 00 0C DE: 2 axes, 2 shared tuples at 3,286, 1,632 glyphs, 16-bit
halved offsets, data at 3,294. The offsets begin 00 00 00 00 00 00 00 1F,
so glyphs 0 and 1 have no variation data and glyph 2 has 62 bytes. Its
MVAR has seven 8-byte records, beginning sbxs,
sbyo, sbys, spxs: the subscript and superscript
sizes and offsets change with weight.