-
Notifications
You must be signed in to change notification settings - Fork 167
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
base: develop
Are you sure you want to change the base?
Conversation
Includes the following: - proposed syntax for variable scalar values from @simoncozens - proposed syntax for featureVariation from @punchcutter with additional modifications based on offline discussions
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> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@moyogo variation
s 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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. ;-)
There was a problem hiding this comment.
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)> | ||
``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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. |
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. |
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? |
I feel like the |
|
||
<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: |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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:
Maybe we should try to support both use-cases somehow, making this explicit at the syntax level.
I believe I now have a complete implementation of this syntax in fonttools/fonttools#2228 |
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? |
No. It's fixed now. |
but we do want to be able to use floats for locations (fvar has |
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. |
I know. But even user-space coordinates in VFs can be floats (fvar Fixed) so feature file should allow that too. |
I intuitively like the |
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:
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:
|
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. |
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
Syntax would also be needed for intermediate locations, for which I suggest |
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? |
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.
Is there a spec of how arbitrary user-space locations get translated into variation regions? |
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. |
So am I correct to summarize the .fea design choices available here as:
? |
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. |
What I suggest is that, but in Designspace file and Fea file, we allow specifying the coordinate space. And allow the following coordinate spaces:
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. |
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. |
Hi folks, I just wanted to let people know that we are still actively working on updates around FEA variable syntax. |
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:
it is substituted at wght=644 and upwards? |
hey @josh-hadley, following the activity on the fonttools end, I wonder if you're still working on this FEA spec update? |
@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. |
@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? |
@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? |
@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,
|
@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. |
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. |
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. (There are some more simple improvements to the fea syntax that we implemented and like to propose when it seems useful). |
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. |
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. |
Ah, yes, thank you. Edited. |
Description
Includes the following changes to the OpenType Feature File Specification aka "FEA Syntax":
This is the culmination of discussions and proposals in #153.
Checklist: