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

Conversation

josh-hadley
Copy link
Collaborator

@josh-hadley josh-hadley commented Apr 22, 2021

Description

Includes the following changes to the OpenType Feature File Specification aka "FEA Syntax":

  • proposed syntax for variable scalar values from @simoncozens
  • proposed syntax for featureVariations from @punchcutter with additional modifications based on offline discussions

This is the culmination of discussions and proposals in #153.

Checklist:

Includes the following:
- proposed syntax for variable scalar values from @simoncozens
- proposed syntax for featureVariation from @punchcutter with additional modifications based on offline discussions
@simoncozens
Copy link
Collaborator

Thanks for this. My part looks good, with the note I've added.

##### 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:
```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.

<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!

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.

Copy link

@ctrlcctrlv ctrlcctrlv left a comment

Choose a reason for hiding this comment

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

Because conditionset's work on ranges, it's strange that §2.e.ii. metrics don't seem to? §2.e.ii. is unclear.

@simoncozens
Copy link
Collaborator

Because conditionset's work on ranges, it's strange that §2.e.ii. metrics don't seem to? §2.e.ii. is unclear.

No, not at all.

Variable scalars are defining an interpolation model - describing how a value changes across the design space - by specifying masters values at particular locations in the design space. In this sense they are just like masters of a glyph outline: you wouldn't want a bold master to operate between wght=500 and wght=700 - it's necessarily a description of the outline at a particular point. So too is your "bold master" kern value: it operates at a point location, and its value is interpolated at non-master point locations.

Conditionsets are completely different, as they specify when something applies and when it doesn't.

@simoncozens
Copy link
Collaborator

Just for interest, I wrote a utility called ds2varlayout which creates a single variable layout file (according to the syntax of this PR) from a designspace file and set of UFOs. It requires this branch of fonttools.

Here is the result of running it on MutatorSans:

conditionset ConditionSet1 {
   wdth 0.0 328.0;
} ConditionSet1;

variation rvrn ConditionSet1 {
    sub I by I.narrow;
} rvrn;

conditionset ConditionSet2 {
   wdth 0.0 1000.0;
   wght 0.0 500.0;
} ConditionSet2;

variation rvrn ConditionSet2 {
    sub S by S.closed;
} rvrn;

@kern1.MMK_L_A = [A];
@kern2.MMK_R_A = [A];

lookup kern_ltr {
    lookupflag IgnoreMarks;
    pos A J (wdth=0,wght=0:0 wdth=0,wght=1000:-20 wdth=0,wght=700:0 wdth=1000,wght=700:0 wdth=569,wght=700:0);
    pos A O (wdth=0,wght=0:0 wdth=0,wght=1000:-30 wdth=0,wght=700:0 wdth=1000,wght=700:0 wdth=569,wght=700:0);
    # ...
    enum pos T @kern2.MMK_R_A (wdth=0,wght=0:-75 wdth=1000,wght=0:-215 wdth=1000,wght=1000:-150 wdth=0,wght=700:-75 wdth=1000,wght=700:-75 wdth=569,wght=700:-75);
    # ...
} kern_ltr;

feature kern {
    lookup kern_ltr;
} kern;

Hopefully it is easy to see how this enables variable-first building without having to merge layout tables between instance TTFs.

@khaledhosny
Copy link
Collaborator

Can we have a syntax for “inline” condition sets as well? something like:

variation rvrn (wdth 0 328) {
    sub I by I.narrow;
} rvrn;

variation rvrn (wdth 0 1000, wght 0 500) {
    sub S by S.closed;
} rvrn;

Or would that be too much complication?

@behdad
Copy link
Contributor

behdad commented May 21, 2021

I feel like the variation keyword is confusing. Why not just stick with feature and add conditionset?


<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.

@simoncozens
Copy link
Collaborator

I believe I now have a complete implementation of this syntax in fonttools/fonttools#2228

@ctrlcctrlv
Copy link

ctrlcctrlv commented May 25, 2021

Just for interest, I wrote a utility called ds2varlayout which creates a single variable layout file (according to the syntax of this PR) from a designspace file and set of UFOs. It requires this branch of fonttools.

Here is the result of running it on MutatorSans:

conditionset ConditionSet1 {
   wdth 0.0 328.0;
} ConditionSet1;

variation rvrn ConditionSet1 {
    sub I by I.narrow;
} rvrn;

conditionset ConditionSet2 {
   wdth 0.0 1000.0;
   wght 0.0 500.0;
} ConditionSet2;

variation rvrn ConditionSet2 {
    sub S by S.closed;
} rvrn;

@kern1.MMK_L_A = [A];
@kern2.MMK_R_A = [A];

lookup kern_ltr {
    lookupflag IgnoreMarks;
    pos A J (wdth=0,wght=0:0 wdth=0,wght=1000:-20 wdth=0,wght=700:0 wdth=1000,wght=700:0 wdth=569,wght=700:0);
    pos A O (wdth=0,wght=0:0 wdth=0,wght=1000:-30 wdth=0,wght=700:0 wdth=1000,wght=700:0 wdth=569,wght=700:0);
    # ...
    enum pos T @kern2.MMK_R_A (wdth=0,wght=0:-75 wdth=1000,wght=0:-215 wdth=1000,wght=1000:-150 wdth=0,wght=700:-75 wdth=1000,wght=700:-75 wdth=569,wght=700:-75);
    # ...
} kern_ltr;

feature kern {
    lookup kern_ltr;
} kern;

Hopefully it is easy to see how this enables variable-first building without having to merge layout tables between instance TTFs.

I notice decimal (floating point) numbers in the quoted file. As far as I'm aware there are no decimal numbers in FEA grammar. Was this intentional?

@simoncozens
Copy link
Collaborator

No. It's fixed now.

@anthrotype
Copy link
Member

but we do want to be able to use floats for locations (fvar has Fixed 16.16 for user-space min/default/max, and FeatureVariations Condition has F2Dot14 for normalized internal min/max coords)

@simoncozens
Copy link
Collaborator

The positions in the feature file are un-normalized, userspace coordinates. They should be normalised by the feature compiler when generating GDEF variable scalars and FeatureVariations conditions.

@anthrotype
Copy link
Member

The positions in the feature file are un-normalized, userspace coordinates.

I know. But even user-space coordinates in VFs can be floats (fvar Fixed) so feature file should allow that too.

@twardoch
Copy link

I intuitively like the variation keyword. And I think we have agreement and movement on this issue, which I'm very glad about, given that it's been dormant for five years since I raised it :)

@behdad
Copy link
Contributor

behdad commented May 25, 2021

I know I wasn't at the meeting that this was discussed. But I've been thinking about the proposed syntax and I highly encourage you to reconsider a couple of things.

Simon wrote:

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

In fact, I think this is extremely cumbersome to use. Since the "varied feature" needs to list everything that applies at that point.

Instead, I'm of the belief that the FEA syntax should allow conditioning on the lookup / rule level within each feature, and let the compiler figure out how to translate that to OT1.8.

So I suggest an alternative that is more like code:

feature test {
   sub a by b;
   sub d by e if heavy;
} test;

@behdad
Copy link
Contributor

behdad commented May 25, 2021

In fact, I think this is extremely cumbersome to use. Since the "varied feature" needs to list everything that applies at that point.

In other words, the current design would require would to call fontTools.varLib.featureVars-like code to resolve rules that apply at every region to generate the .fea file. Whereas in my proposal, the designer intentions are encoded directly and resolved when compiling the font.

cc @justvanrossum

@Lorp
Copy link

Lorp commented May 26, 2021

Can we please clarify why locations must be specified in user-space? If “we all agreed that FEA should be regarded as assembly-equivalent”↗︎, then isn’t it a little bit odd? I’m not against allowing user-space, but this does represent a major proliferation of it in font sources, so seems to me to require justification to overcome possible fragility. I’m thinking in particular of the common use case of changing source locations late in the design process.

How about allowing syntax using default, min and max keywords, representing normalized 0, -1 and 1, as an alternative to user-space? Consider the following example, which expresses the same thing in user-space, then in normalized space. (Axes: wght 100/400/800, wdth 75/100/100. Line breaks added for clarity.)

# user-space, needs every location updated if source locations change
pos A J (
    wght=400,wdth=100:0
    wght=400,wdth=75:-20
    wght=100,wdth=100:-20
    wght=100,wdth=75:-20
    wght=800,wdth=100:-10
    wght=800,wdth=75:-10
);

# normalized, allows source locations to be changed without needing updates here
pos A J (
    wght=default,wdth=default:0
    wght=default,wdth=min:-20
    wght=min,wdth=default:-20
    wght=min,wdth=min:-20
    wght=max,wdth=default:-10
    wght=max,wdth=min:-10
);

Syntax would also be needed for intermediate locations, for which I suggest 0.5min, 0.812max etc. Again, such specifications are robust in design terms when source locations change.

@twardoch
Copy link

twardoch commented May 26, 2021

I think most people will find it challenging that locations in FEA are not in designer space. But user space is still approachable to them. Normalized space is not, I think. It's two times removed from how people usually work.

Normalized space locations change dramatically when you relocate the neutral master. I think user space is the most sensible solution, the best compromise.

If normalized space, what units? The theoretical –1 to 1 with an unpredictable decimal precision, or the real –16k to 16k, or something else?

@Lorp
Copy link

Lorp commented May 26, 2021

To clarify, I’m proposing not a replacement to user-space syntax but an alternative syntax that can be used if it’s better for one’s workflow. I’ve repeated the alternative syntax below with implicit defaults to show potential brevity.

pos A J (
    0
    wdth=min:-20
    wght=min:-20
    wght=min,wdth=min:-20
    wght=max:-10
    wght=max,wdth=min:-10
);

Is there a spec of how arbitrary user-space locations get translated into variation regions?

@behdad
Copy link
Contributor

behdad commented Oct 29, 2021

Problem with "designspace coordinates" is that, such a concept is undefined by OpenType and FEA file, and only defined by the Designspace format and varLib.

@justvanrossum
Copy link

So am I correct to summarize the .fea design choices available here as:

  • userspace coordinates
  • normalized coordinates pre avar
  • normalized coordinates post avar

?

@simoncozens
Copy link
Collaborator

I think putting normalised (-1.0<=>1.0) coordinates into the FEA file would be really unpleasant for the user. I know I’ve previously said that humans shouldn’t write FEA, but they probably should be able to understand it! But of course they will end up as normalised values in the GDEF variation store.

From my perspective, the choice is coming down to being between userspace values as taken from the fvar table (wght=400 in the Nunito example), which is how we’ve interpreted things so far; or designspace values (wght=42) plus some additional syntax allowing the user to provide a mapping which tells the compiler how to turn that into a normalised value.

@behdad
Copy link
Contributor

behdad commented Oct 30, 2021

So am I correct to summarize the .fea design choices available here as:

  • userspace coordinates
  • normalized coordinates pre avar
  • normalized coordinates post avar

?

What I suggest is that, but in Designspace file and Fea file, we allow specifying the coordinate space. And allow the following coordinate spaces:

  • userspace
  • normalized userspace
  • designspace
  • normalized designspace
  • integer normalized designspace in F2DOT14

If designspace coordinate system is used in .fea file, then it either references an externally defined coordinate space, or a mapping ala one from Designspace should be specified in .fea file in a new syntax to be defined.

So yes, we need avar and fvar syntax as well.

@josh-hadley
Copy link
Collaborator Author

Folks -- thanks for the renewed discussion on this. It seems like there's still sufficient disagreement on some finer (and major) points to warrant a[nother] meeting or maybe workshop of sorts of stakeholders to try to get agreement on something that we can solidify in FEA syntax. If people are open to that, I can work on scheduling something.

@josh-hadley
Copy link
Collaborator Author

Hi folks, I just wanted to let people know that we are still actively working on updates around FEA variable syntax.

@madig
Copy link

madig commented Mar 14, 2023

I, uh, how do user-space locations in a feature file interact with mapping-laced user locations in a Designspace? I found a case where a mapping in a Designspace file like so:

  <axes>
    <axis tag="wght" name="Weight" minimum="100" maximum="900" default="100">
      <map input="100" output="22"/>
      <map input="600" output="115"/>
      <map input="900" output="185"/>
    </axis>
  </axes>
  <rules processing="last">
    <rule name="conditional_sub">
      <conditionset>
        <condition name="Weight" minimum="116" maximum="185"/>
      </conditionset>
      <sub name="e" with="a"/>
    </rule>
  </rules>

means that "e" is substituted by "a" at wght=606 and upwards (116 in design coordinates is a tad over 600 in user coordinates), whereas with a feature file snippet like this:

lookup conditional_sub {
    sub e by a;
} conditional_sub;

conditionset test {
    wght 606 900;
} test;

variation rlig test {
    lookup conditional_sub;
} rlig;

it is substituted at wght=644 and upwards?

@anthrotype
Copy link
Member

hey @josh-hadley, following the activity on the fonttools end, I wonder if you're still working on this FEA spec update?

@skef
Copy link
Collaborator

skef commented Mar 14, 2023

@anthrotype Yes, the Adobe font folks are actively synthesizing the feedback on this PR and other ideas into a new proposal and draft of relevant portions of the AFDKO. Sometimes that work has to take a back seat to other things that come up but we're working on it.

@davelab6
Copy link

@josh-hadley would an online workshop day or two be helpful? I'm interested to organize one for following up on the atypi panel.

Will anyone here attend typographics/typelab in NYC in June or typecon in Portland Oregon in August?

@anthrotype
Copy link
Member

anthrotype commented Dec 11, 2023

@schriftgestalt might be interested in implementing this in Glyphs.app, now that fontTools feaLib and fea-rs have already added support for this proposed variable FEA syntax. Can we do something to unblock this?
I see there are merge conflicts, maybe @simoncozens can try rebasing his PR.

@skef
Copy link
Collaborator

skef commented Dec 11, 2023

@anthrotype Although we understand this is taking a long time, due to a lot of reasons including resources needed to keep up with the ongoing OpenType spec work,

  1. Adobe is still developing the VF-first feature file specification and AFDKO implemenation
  2. This PR does not represent the current thinking, and there are some aspects of the proposal (e.g. the low-level syntax for the substitution portion) that we would not recommend).

@rsheeter
Copy link

@skef when do you expect to be able to share more? Even a preliminary draft would be helpful, otherwise the implementations will (continue to) drift.

@skef
Copy link
Collaborator

skef commented Dec 11, 2023

With luck we'll have a beta of the positioning/variable value portion of the spec and implementation available in the first quarter of next year for review and feedback, with the substitution portion following along after that.

@schriftgestalt
Copy link
Contributor

Just to mention that GlyphsApp supports it’s own version of this. By the time we implemented this, there was no discussion or active proposal around.
https://handbook.glyphsapp.com/layout/variable-gpos/
https://handbook.glyphsapp.com/layout/conditional-feature-code/

(There are some more simple improvements to the fea syntax that we implemented and like to propose when it seems useful).

@skef
Copy link
Collaborator

skef commented Oct 24, 2024

I forgot to add a comment here until now: This PR has been partly superseded by a newer proposal now under review (until January 15th 2025) at https://github.com/adobe-type-tools/feature_file_change_review . The implementation replaces the variable value portion of this work. There will be a second phase, probably in the first half of 2025, to address the remaining Feature Variations cases. We expect that proposal to be somewhat different from the draft here as well.

I'll close this PR once we have a draft of the phase 2 proposal.

@ErwinDenissen
Copy link

I hope all font software vendors adopt this new feature syntax as soon as possible, as it will significantly enhance interchangeability between source font file formats.

P.S. In the link you provided, you refer to January 15th 2025 and the first half of 2025, which I assume is what you meant here as well.

@skef
Copy link
Collaborator

skef commented Oct 24, 2024

Ah, yes, thank you. Edited.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.