afdko icon indicating copy to clipboard operation
afdko copied to clipboard

[spec] Variable FEA Syntax

Open josh-hadley opened this issue 4 years ago • 51 comments

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:

josh-hadley avatar Apr 22 '21 23:04 josh-hadley

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

simoncozens avatar Apr 23 '21 07:04 simoncozens

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 avatar May 20 '21 13:05 simoncozens

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.

simoncozens avatar May 20 '21 15:05 simoncozens

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?

khaledhosny avatar May 20 '21 23:05 khaledhosny

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

behdad avatar May 21 '21 15:05 behdad

I believe I now have a complete implementation of this syntax in https://github.com/fonttools/fonttools/pull/2228

simoncozens avatar May 24 '21 14:05 simoncozens

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?

ctrlcctrlv avatar May 25 '21 14:05 ctrlcctrlv

No. It's fixed now.

simoncozens avatar May 25 '21 15:05 simoncozens

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)

anthrotype avatar May 25 '21 15:05 anthrotype

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.

simoncozens avatar May 25 '21 15:05 simoncozens

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.

anthrotype avatar May 25 '21 15:05 anthrotype

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 :)

twardoch avatar May 25 '21 19:05 twardoch

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 avatar May 25 '21 20:05 behdad

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

behdad avatar May 25 '21 20:05 behdad

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.

Lorp avatar May 26 '21 01:05 Lorp

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?

twardoch avatar May 26 '21 01:05 twardoch

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?

Lorp avatar May 26 '21 12:05 Lorp

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

I don't mean to be facetious, but this is almost certainly the wrong question. Variation regions for the calculation of variable scalar supports is purely a matter of internal representation within the GDEF/gvar tables. Nothing good will come of exposing this to the user.

simoncozens avatar May 26 '21 12:05 simoncozens

Let’s say we have the same wght 100/400/800, wdth 75/100/100 font described above. Currently I don’t know whether the following feature files are valid, nor how they are supposed to compile if they are. Apologies if I’m being dumb.

# default specified, one location specified that is intermediate on both axes
pos A J (
    wght=400,wdth=100:0
    wght=600,wdth=85:0
);
# default unspecified, one location specified that is intermediate on both axes
pos A J (
    wght=600,wdth=85:0
);
# default unspecified, max on one axis specified
pos A J (
    wght=800:0
);

Lorp avatar May 26 '21 14:05 Lorp

Let’s say we have the same wght 100/400/800, wdth 75/100/100 font described above. Currently I don’t know whether the following feature files are valid, nor how they are supposed to compile if they are. Apologies if I’m being dumb.

# default specified, one location specified that is intermediate on both axes
pos A J (
    wght=400,wdth=100:0
    wght=600,wdth=85:0
);
# default unspecified, one location specified that is intermediate on both axes
pos A J (
   wght=600,wdth=85:0
);
# default unspecified, max on one axis specified
pos A J (
   wght=800:0
);

The first one is valid. The other two are not. You need to specify the value for the default position. (Again, this is exactly like outline interpolation.) You can also try compiling these using the fontTools branch mentioned above.

simoncozens avatar May 26 '21 16:05 simoncozens

FYI: @ErwinDenissen implemented @simoncozens proposal in Font Creator 14, see the announcement on TypeDrawers.

moyogo avatar Jun 28 '21 12:06 moyogo

That means there are now two independent implementations of this! Nice!

@josh-hadley, what’s needed to drive this forward? (I have a horrible feeling you’re going to say something about makeotf...)

simoncozens avatar Jul 12 '21 15:07 simoncozens

Just fixing the Designspace format would eliminate a lot of the need for this complicated syntax. https://github.com/unified-font-object/ufo-spec/issues/194

I understand it still has other uses, but also this should be kept in mind, that those uses don't include current generation UFO, which are single master.

ctrlcctrlv avatar Oct 25 '21 14:10 ctrlcctrlv

Just fixing the Designspace format would eliminate a lot of the need for this complicated syntax.

No, it would not, because

I understand it still has other uses... those uses don't include current generation UFO

Right, but AFDKO is completely ignorant about UFO. UFO is really really not the only way fonts are made, and supporting things that aren't UFO is quite important to the tools. :-)

If you're going to do, say, contextual variable kerning in a font editor, you need some syntax for expressing that. Designspace won't help you there.

simoncozens avatar Oct 28 '21 11:10 simoncozens

Two more points:

  • This is now implemented in the main branch of fonttools!
  • We should probably have clarified whether the location values are designspace or userspace coordinates. :-/ I don't actually know what the answer is to that.

simoncozens avatar Oct 29 '21 11:10 simoncozens

  • We should probably have clarified whether the location values are designspace or userspace coordinates. :-/ I don't actually know what the answer is to that.

userspace I would say. wght=800 etc are userspace values.

behdad avatar Oct 29 '21 11:10 behdad

userspace I would say. wght=800 etc are userspace values.

as in user-facing values.

behdad avatar Oct 29 '21 11:10 behdad

They should be user-facing (not normalised -1.0 <-> 1.0) values. The question is whether or not they should be aware of avar and perform avar mapping. (i.e. wdth=800 rather than wdth=768 or some stroke thickness, or whatever)

I think they probably should. I'm working on a fix to fonttools now. (It turns out I need this for a project...)

simoncozens avatar Oct 29 '21 13:10 simoncozens

The question is whether or not they should be aware of avar and perform avar mapping

They shouldn't. user-space is conceptually before avar mapping, it's the scale that is used is fvar axes and instances. The avar is used to map from the default normalized values (obtained by mapping user-space's min to -1.0, default to 0.0, max to +1.0, and lerp'ing everyting in between) to custom normalized values (e.g. -0.7:-0.6, 0.2:0.4, etc.).

anthrotype avatar Oct 29 '21 13:10 anthrotype

I'm not convinced. And I think this is actually awkward.

The FEA files are part of the design sources, and so should "speak the designer's language". But they're also used to add stuff to a binary TTF, so should speak the binary's language too.

Let me ask the question in another way. Imagine a font like Nunito where the design sources specify a regular master at wght=42 and bold master at wght=208, but the binary is set up so that the axis in the fvar table runs from 400-1000.

When I as designer want to add a contextual kern in my design sources, I'm going to want to specify it in terms of my design masters. I've put masters at wght=42 and wght=208, so I want to say what the values are at those coordinates: pos a b' (wght=42:-15 wght=208:-30) c;. I'd probably rather do that than to specify it in terms of the exported, end-user coordinates (pos a b' (wght=400:-15 wght=1000:-30) c;).

But as the compiler implementor, I'm not sure that I'm able to get at the design master coordinates at the point of compilation without access to the sources.

So we either need to tell the feature file where the design masters are in user space, or we force people writing FEA files to do the user mapping in their heads.

simoncozens avatar Oct 29 '21 13:10 simoncozens