Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spec] Variable FEA Syntax #1350

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 68 additions & 4 deletions docs/OpenTypeFeatureFileSpecification.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ layout: default
OpenType™ Feature File Specification
---

Copyright 2015-2020 Adobe. All Rights Reserved. This software is licensed as
Copyright 2015-2021 Adobe. All Rights Reserved. This software is licensed as
OpenSource, under the Apache License, Version 2.0. This license is available at:
http://opensource.org/licenses/Apache-2.0.

Document version 1.25.1
Last updated 5 July 2020
Document version 1.26.0
Last updated XX XXXXXX 2021 **TODO: adjust date**

**Caution: Portions of the syntax unimplemented by Adobe are subject to change.**

Expand Down Expand Up @@ -48,6 +48,7 @@ Last updated 5 July 2020
- [c. parameters](#4.c)
- [d. lookupflag](#4.d)
- [e. lookup](#4.e)
- [i. Specifying FeatureVariations (variable fonts)](#4.e.i)
- [f. markClass](#4.f)
- [g. subtable](#4.g)
- [h. Examples](#4.h)
Expand Down Expand Up @@ -261,6 +262,9 @@ The following are keywords only in their corresponding table/feature blocks:
| [`location`](#9.i) | STAT table | ✅ |
| [`ElidableAxisValueName`](#9.i) | STAT table | ✅ |
| [`OlderSiblingFontAttribute`](#9.i) | STAT table | ✅ |
| [`conditionset`](#X.x) TODO: link up | Variable sub and pos lookups | ❌ |
| [`variation`](#4.e.i) | Variable sub and pos lookups | ❌ |


The following are keywords only where a tag is expected:

Expand All @@ -284,7 +288,7 @@ dflt # can be used only with the language keyword and as the language value wit
{ } braces Enclose a feature, lookup, table, or anonymous block
[ ] square brackets Enclose components of a glyph class
< > angle brackets Enclose a device, value record, contour point, anchor, or caret
( ) parentheses Enclose the file name to be included
( ) parentheses Enclose the file name to be included. Within a `<metric>`, enclose variable scalar values.


<a name="2.e"></a>
Expand All @@ -310,6 +314,28 @@ the values of various table fields [§[9](#9)].

_[ Note: Multiple master support has been withdrawn as of OpenType specification 1.3. ]_

_[ Note: the following is unimplemented and is subject to change. ]_

For **variable fonts only**, a `<metric>` value can make use of the following syntax to specify how values should vary based on axis locations:
```
(<location_spec1>:<value1> [...] <location_specN>:<valueN>)
```

Notes:
- A `<location_spec>` consists of one or more `<axis_tag>=<axis_location>` pairs, separated by commas
- `<location_spec>:<value>` pairs are separated by spaces within the variable `<metric>`
- multiple `<location_spec>:<value>` pairs may be specified within a variable `<metric>`

Example showing a simple single-axis variable scalar value. The value is -100 when Weight is 200, and -150 when Weight is 900:
```
<(wght=200:-100 wght=900:-150)>
```

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear what happens when Weight is neither 200 or 900.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simoncozens could you comment/clarify here?

Copy link
Collaborator

@simoncozens simoncozens May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I've been actually using this, I can add more here:

  • We should probably clarify that location coordinates are specified in userspace coordinates.
  • We should probably clarify that a value corresponding to the default location (the default axis value on each axis) must be included in the variable scalar.
  • We may want to provide shortcut syntax for this, where the default location is elided, I don't know; e.g. <(-100 wght=900:-150)>

In answer to Fred's question, we could add something like "The value of the variable scalar at locations other than the provided master locations will be interpolated according to OpenType Font Variation interpolation rules" if necessary, but to me it feels tautological.


A more complex example combining single and multiple `<axis_location>`s extends the above example to specify that the value should be -120 when Weight is 900 and Width is 150:
```
<(wght=200:-100 wght=900:-150 wght=900,wdth=150:-120)>
```

<a name="2.e.iii"></a>
#### 2.e.iii. Device table _[ Currently not implemented. ]_

Expand Down Expand Up @@ -1267,6 +1293,44 @@ rules, the implementation sorts the rules to avoid conflict; for example, the
ligature substitution rule for f_f_i will be written before the ligature
substitution rule for f_i, no matter what their order is in the feature file.

<a name="4.e.i"></a>
#### 4.e.i Feature Variations (variable font feature replacement)

For variable fonts *only*, it is possible to specify [FeatureVariations](https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table), which allow the use of different lookups for different parts of the variation space.

<a name="4.e.i.1"></a>
##### 4.e.i.1 `conditionset`

A named `conditionset` defines [conditions](https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#conditionset-table) that can trigger a variable font lookup to swap in different lookups when the conditions are matched. A `conditionset` is simply a list of axis tags and min/max values. A conditionset is matched when all conditions are true. Syntax:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A conditionset is matched when all conditions are true.

So the conditionset statement can only do boolean AND but not OR. I wonder if this (or another keyword) should be allowed to define not only "boxes" but also "regions", to borrow @justvanrossum's terminology from
https://github.com/fonttools/fonttools/blob/9959916c64c5ab24de416aabf05c2bdace610014/Lib/fontTools/varLib/featureVars.py#L22-L29

Copy link

@justvanrossum justvanrossum Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't followed this, but if .fea strictly follows the OTF FeatureVariations structure, it will end up fairly limited in its practical use.

One of the reasons fontTools.varLib.featureVars became so complex* is that it allows both AND and OR.

*) so complex that Behdad rewrote it almost from scratch, to make it behave and perform decently, after my initial sloppy implementation

Copy link

@twardoch twardoch Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the situations when "OR" cannot be replaced by separate conditionsets, each having just AND?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. (a OR b) AND (c AND d) can be rewritten as

  • a AND c AND d
  • b AND c AND d

I may be missing something but if you want 'OR' matching, you just define multiple conditionsets, each describing all 'AND' conditions, no?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In FontLab 7, when you assign a special tag to the target glyph, it replaces the source glyph under specified conditions:

[srcglyph]~cond1[&cond2][&cond3...]

if srcglyph is absent, the current glyph’s name without a suffix is used.

Each cond is a 2-letter (internal FL) axis tag with bounds, like wt>500 or 340>wd or 100<op<600 etc. — and they're concatenated with &.

If I need 'OR', I add a second special tag.

I know there is some redundancy in this, but I’m genuinely curious if there are conditionsets that involve both OR and AND which cannot be expressed as a list of 'AND' conditionsets.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Just wasn't at the meeting where we all agreed that FEA should be regarded as assembly-equivalent, to be emitted by programs, and he's still trying to make it easy for humans to write. ;-)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Just wasn't at the meeting where we all agreed that FEA should be regarded as assembly-equivalent

TIL. Thanks, and agreed that's a good thing. Back to lurk mode!

```fea
conditionset <name of set> {
<axis tag> <minValue> <maxValue>;
# additional tag min max entries
} <name of set>;
```
For example, to define a "heavy" condition, for the 'wght' axis between 700-900:
```fea
conditionset heavy {
wght 700 900;
} heavy;
```

<a name="4.e.i.2"></a>
##### 4.e.i.2 `variation` (FeatureVariation)
A `variation` is akin to a regular `feature` definition, with an added `conditionset` specifier to indicate that the feature should be active when the conditions of the conditionset are met. The syntax is:
Copy link
Collaborator

@simoncozens simoncozens May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OT gives us feature table substitutions - in the sense that one feature table is swapped out and another is swapped in. So if you have:

feature test {
   sub a by b;
} test;

variation test heavy {
   sub d by e;
} test;

then if the heavy condition is met, the variation gets swapped in, the original feature gets swapped out, and only the d->e substitution will happen, as I understand it.

If this is the intent, then this should be clearer in the text here. I suggest replacing "to indicate that the feature should be active" (which is kind of wooly anyway - "active"?) with "to indicate that the feature should be replaced by the rules contained in the variation statement".

Copy link
Member

@anthrotype anthrotype Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this important point when I first reviewed this. The way we implemented in feaLib, by using featureVars.addFeartureVariationsRaw, means that the new Feature table that replaces the default one when the conditions are met simply extends the existing feature's lookup list indices, but does not replaces it:

https://github.com/fonttools/fonttools/blob/7412268cbe2a36f588e2d06b45faf35a3b803633/Lib/fontTools/varLib/featureVars.py#L429-L436

Maybe we should try to support both use-cases somehow, making this explicit at the syntax level.

```fea
variation <feature tag> <conditionset name or NULL> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the meaning of NULL here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it means this (quoting from ot spec):

If a given condition set contains no conditions, then it matches all contexts, and the associated feature table substitution is always applied

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, NULL here is for "a condition set with no conditions". Although I suppose we could push that back and require a conditionset here, even if it is:

conditionset empty {
} empty;

Do you have a preference?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between the two following?

variation <feature tag> NULL {
# feature specifications
} <feature tag>;

and

feature <feature tag> {
# feature specifications
} <feature tag>;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moyogo variations are very similar to feature definitions, but are stored differently in OT tables, and also have the possibly-NULL conditionset as part of their definition.

# feature specifications
} <feature tag>;
```
For example, let's say we've defined a `lookup` called "heavy_symbols" that we want to swap in when the "wght" axis is between 700-900 (our "heavy" `conditionset` from above):
```
variation rvrn heavy {
lookup symbols_heavy;
} rvrn;
```


<a name="4.f"></a>
### 4.f. markClass

Expand Down