A proposal for an add-on to OpenType 1.8 by Black[Foundry]
Superceded by github.com/harfbuzz/boring-expansion-spec in 2023
TrueType has had the capability to form composite glyphs since its inception. A composite glyph references other glyphs as “components”. This is a simple way to save data storage, for glyphs that can be composed of other glyphs, such as diacritics. Components can be arbitrarily positioned in the composite glyph, and can be scaled and rotated and skewed if needed.
“Variable Components”, as described in this document, add further parameters to customize the appearance of components in the composite glyph.
With OpenType 1.8, “variations” were added to the format, allowing for live interpolation between (say) regular and bold. These variations are global to the font: they are controlled by the user for the font as a whole. Such a font is no longer static, and the end user can navigate a designspace along dimensions (axes) defined by the font. The chosen variation is defined by the designspace location, as a set of coordinates in the designspace.
“Variable Components” add the possibility to place a variable glyph in a composite glyph, specifying the interpolation settings (the designspace location) for that single occurrence. A single glyph may define its own design space, for composites to use as they see fit.
Some design tools already implement a methodology like Variable Components (for example “Smart Components” in Glyphs.app). Users find it often a more efficient way to design certain glyphs. Upon export as TTF, such components have to be converted to traditional outlines.
However, a lot of space could be saved if the Variable Component information would be stored in the font, instead of traditional outlines: a component reference will typically take up less space than a discrete outline.
This is especially true for CJK fonts, which tend to contain very large numbers of glyphs, many of which can be composed of variations of more basic glyphs.
More generally, fonts often contain glyphs that can be seen as variations of regular glyphs, for example superior and inferior numerals and small caps. It can be beneficial to represent these variations as local instances using Variable Components.
The proposal presented here was informed by the following priorities:
glyf
table composites/componentsgvar
for variationsThe first thing we need to pin down is how to do “local designspaces”. How does a glyph define its own designspace, to be used by composites?
Here are some of the insights that led to the current design, in order:
glyf
-based glyph, using gvar
for
variations, but it needs to be able to use axes that are not user-controllable.fvar
axes, but we need to be able to flag an axis as “This axis
is for variable component use only, do not expose it to the user, ever, at all”.
This is perhaps more strict than the definition of the existing “hidden” axis
flag, and we need to establish whether a new axis flag may be needed.fvar
table) does not have an unreasonable limit per se (65536), but it is not
without cost: in some places – for example in gvar
variation tuples or
VarStore regions – there is a value specified for every single axis in the
font, even if that axis does not participate in a certain variation. This is
especially relevant for gvar
, as there can be many tuple variations (many
glyphs × many variations per glyph), so adding even a single axis to a font can
have a significant impact on the file size. So: let’s not use more axes than
strictly necessary.avar
-like functionality to be necessary here.fvar
table.)gvar
table.To make Variable Components work, the only thing that is missing from OpenType
1.8 is the capability to store some additional information for each component of
composite glyphs. The core of this proposal is to add a single new table, called
VarC
, that will provide a space for all new information.
A Variable Component reference needs the following information:
We use the composites/components mechanism from the glyf
table, so some of
these values are already taken care of: the base glyph ID and the offset.
Components in the glyf
table can optionally specify a scale value, or x/y
scale values, or a 2×2 transformation matrix, but we chose not to use these for
several reasons:
gvar
only supports interpolation of the component offset values, not of the
scale values or the matrix.Summarizing:
glyf
+ gvar
. Additional transformation values (scale, rotation, etc.) and
its variations will be stored in VarC
VarC
Base glyphs are totally ordinary glyf
+ gvar
glyphs, but can also be
composites themselves, using Variable Components, so we fully embrace the
recursive nature of TrueType components.
High level structure of the VarC
table:
name | description |
---|---|
Version | version field, initially 0x00010000 |
numGlyphs | the number of glyphs |
GlyphData[numGlyphs] | array of glyph data |
VarStore | variation data |
GlyphData
is an array of offsets to Glyph
subtables, indexed by GlyphID
.
numGlyphs
must be less than or equal to the numGlyphs
field in the maxp
table.
A Glyph
subtable is an array of variable length Component
data.
The VarC
table depends on the glyf
table: to parse a VarC
Glyph
, one
needs to know the number of components from the glyf
table. That number is
not duplicated in VarC
.
The Component
data structure stores the additional transformation fields,
the designspace location for the components, and indices into the VarStore
for each value that needs variations.
The transformation data consists of individual optional fields, which can be used to construct a transformation matrix.
Transformation fields:
name | default value |
---|---|
Rotation | 0 |
ScaleX | 1 |
ScaleY | 1 |
SkewX | 0 |
SkewY | 0 |
TCenterX | 0 |
TCenterY | 0 |
The TCenterX
and TCenterY
values represent the “center of transformation”.
This is separate from the component offset as stored in the glyf
table.
Details of how to build a transformation matrix, as pseudo-Python/fontTools
code, where (X, Y)
is the component offset from the glyf
table:
# Using fontTools.misc.transform.Transform
t = Transform() # Identity
t = t.translate(X + TCenterX, Y + TCenterY)
t = t.rotate(Rotation)
t = t.scale(ScaleX, ScaleY)
t = t.skew(SkewX, SkewY)
t = t.translate(-TCenterX, -TCenterX)
The transformation fields are stored as individual fields, and are interpolated as individual fields. If the client needs a transformation matrix, then this matrix needs to be constructed after interpolation.
Rationale for using a transformation center, using Rotation as an example:
The designspace location for components is stored as an array of axis indices and a matching array of axis values.
The VarStore subtable is used to store variation deltas. It uses 16 bit integer values, but we use these for various flavors of 16 bit fixed values, too.
When preparing a glyph outline for the rasterizer, the following logic needs to be applied:
Inputs:
Output:
Steps:
VarC
table:
Or in pseudo code:
def getGlyphOutline(gid, location):
if gid is a composite:
for each component:
if gid in VarC:
compoTransform = instantiateTransform(location)
compoLocation = instantiateLocalLocation(location)
else:
offset = instantiateOffset(location)
compoTransform = getComponentTransform(component, offset)
compoLocation = location # global
outline = getGlyphOutline(compoGID, compoLocation)
outline = transformOutline(outline, compoTransform)
else:
outline = instantiateGlyfGvarGlyph(gid, location)
return outline
To clarify: Variable Components completely determine the designspace location for the base glyph. Any axis not specified by a Variable Component has to be interpreted as set to its default, regardless of the global designspace location. In other words, Variable Components do not implicitly pass the global designspace location down to the base glyphs. (It can’t pass down local designspace coordinates, as local designspace may reuse axis IDs for different purposes. Axis X may do something completely different for glyph A than for glyph B.
This may be opened for discussion: it can be useful to pass down the global
designspace coordinates down to base glyphs (unless overridden), but then we
need to distinguish between global fvar
axes and local (anonymous) fvar
axes, due to the reusable nature of local axes in this design. To allow this, we
need a new fvar
axis flag in addition to the “hidden” flag. Please discuss
here: https://github.com/BlackFoundryCom/variable-components-spec/issues/1
Local designspace coordinates need to be clamped, but it’s not clear yet how: https://github.com/BlackFoundryCom/variable-components-spec/issues/3
type | name | value |
---|---|---|
Version | Version | 0x00010000 |
uint16 | numGlyphs | |
LOffset | GlyphData[numGlyphs] | |
LOffset | VarStore |
GlyphData: this is an array of offsets to glyph data subtables. It is indexed by
glyph ID. If an offset is zero, then there is no Glyph
data for this glyph.
The numGlyphs field less than or equal to the total number of glyphs in the
font.
VarStore: existing data structure to store all variation data, as used by GDEF, HVAR, VVAR, MVAR, etc.
Glyph: the data for a single glyph contains the component data for all
components. The number of components is derived from the glyf
table.
Component:
type | name | notes |
---|---|---|
uint16 | flags | see below |
uint8 or uint16 | numAxes | This is a uint16 if bit 3 of flags is set, else a uint8 |
uint8 or uint16 | axisIndices[numAxes] | This is a uint16 if bit 3 of flags is set, else a uint8The most significant bit of each axisIndex tells whether this axis has a VarIdx in the VarIdxs array below. Bits 0..6 (uint8) or 0..14 (uint16) form the axis index. |
Coord16 | axisValues[numAxes] | The axis value for each axis |
Angle16 | Rotation | Optional, only present if it 5 of flags is set |
Scale16 | ScaleX | Optional, only present if it 6 of flags is set |
Scale16 | ScaleY | Optional, only present if it 7 of flags is set |
Angle16 | SkewX | Optional, only present if it 8 of flags is set |
Angle16 | SkewY | Optional, only present if it 9 of flags is set |
Int16 | TCenterX | Optional, only present if it 10 of flags is set |
int16 | TCenterY | Optional, only present if it 11 of flags is set |
uint8 | entryFormat | See below |
VarIdx | VarIdxs[varIdxCount] | see below |
VarIdx
value represents an index into the VarStore
, which contains all
variation data.varIdxCount
is determined by the sum of:
VarIdx
flags
is setVarIdx
entries are 1, 2, 3 or 4 bytes long. This is determined by the
entryFormat
field, see below.Component flags:
bit number | meaning |
---|---|
0..2 | Number of integer bits for ScaleX and ScaleY, mask: 0x07 |
3 | axis indices are shorts (clear = bytes, set = shorts) |
4 | Transformation fields have VarIdx |
5 | have Rotation |
6 | have ScaleX |
7 | have ScaleY |
8 | have SkewX |
9 | have SkewY |
10 | have TCenterX |
11 | have TCenterY |
12 | If ScaleY is missing: take value from ScaleX (to be discussed here: https://github.com/BlackFoundryCom/variable-components-spec/issues/2) |
13 | (reserved, set to 0) |
14 | (reserved, set to 0) |
15 | (reserved, set to 0) |
We chose to store all relevant fields as 16-bit values for maximum compactness, and compatibility with the VarStore format. The downside of this is that we need to choose the range of the fields carefully, as the range of delta values may exceed the range of master values by a factor related to the number of axes involved.
In one case (component scale factors) we chose to use a three-bit field to specify the number of integer bits to be used for scale factors. This gives us a flexible range that can be made to fit the required range.
In other cases (Angle16
and Coord16
) we simply chose a larger range than
required for the master values, so there is some wiggle room for delta values
that are outside of the master value range.
Delta values are stored in the VarStore
subtable, and they use the same
formats as their corresponding master values.
We observe that https://github.com/googlefonts/colr-gradients-spec/ adds 32-bit
value support to the VarStore format. VarC
table could benefit from that as
well, at the expense of compactness.
Angle16
: this is an int16 value used to represent an angle. To scale an
angle in degrees to this format, multiply the angle by 0x8000 / (4 * 360)
.
This gives us an effective range of -1440 degrees to +1440 degrees. Master
values are expected to be between -360 and +360 degrees. The extra headroom
is to allow for delta values that are outside of the master range.
Scale16
: this is an int16 used as a 16 bit Fixed number, where the number
of integer bits is specified by bits 0..2 of the flags
field. This allows us
to use 16 bits precision for a flexible range of scale values, depending on what
the component needs. It avoids having a small maximum (as with Fixed2Dot14,
which goes from -2 to +2) while sticking to 16 bits precision. The number of
precision bits is 16 - number-of-integer-bits.
Coord16
: this is in int16 value used to represent a coordinate in a
designspace location. This is defined as a Fixed4Dot12. Master values are
expected to be between -1.0 and +1.0, but delta values may be outside that
range.
VarIdx
array: this is a compactly stored array with VarIdx
values, which
reference items in the VarStore. A VarIdx value is normally 32 bit, using 16
bits for the outer
index and 16 bits for the inner
index. The array items
are 1, 2, 3 or 4 bytes long, and are formatted as specified by the entryFormat
field. This is identical to the entryFormat
field of the
DeltaSetIndexMap
subtable
from the HVAR
table.
The axes used to implement local designspaces for components should never be
exposed to users, and should be marked as such with a new fvar
axis flag:
Mask | Name | Description |
---|---|---|
0x0002 | INTERNAL_AXIS | The axis is only used internally, and should not be exposed in user interfaces. Used to implement local designspaces for Variable Components. |
This is a backwards-compatible change, and therefore the fvar
table version
does not need to be updated.
Because the (local) axes for VariableComponents are controlled by global (user) axes, this proposal contains the possibility to do non-linear interpolation, without the need for duplicate fvar axis tags (*). However, it is currently limited to variable components. It is possible to change the proposal in a small way that could apply the same control over designspace location and transformation on the outlines of the glyph, if it is not a composite.
*) By giving multiple fvar axes the same axis tags, many implementations allow multiple axes to be controlled from a single value.
There is a proposal which adds a much enhanced version of the COLR table: https://github.com/googlefonts/colr-gradients-spec/
There happens to be some overlap between the Variable Components proposal and the COLRv1 proposal.
A COLR glyph can be seen as a composite glyph, but with paint properties associated with each component. COLRv1 enhances this idea by adding transformations to the “components”, in a way that is conceptually very similar to the Variable Components proposal, but is different in every detail.
From a bird’s eye perspective, both proposals implement “doing components better”, but for different use cases.
We currently see two ways of addressing this:
COLRv1 effectively adds a new glyph type, one that overrides glyf
and
CFF
/CFF2
(but uses those as outline sources), whereas the Variable
Components proposal builds on the composite/component structure of the glyf
table (and is not compatible with CFF
/CFF2
).
More research and discussion is needed:
There is some prototype-level code that implements readers and writers for the
VarC
table as part of this repository:
https://github.com/BlackFoundryCom/rcjk-tools
It relates to Black[Foundry]’s Robo-CJK RoboFont extension, which heavily uses variable components: https://github.com/BlackFoundryCom/robo-cjk
In March 2020 we published the RoboCJK Deep Components Proposal.
In September 2018, Microsoft published an XVAR Proposal.