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

[varLib] Support for sparse kerning / anchors #3168

Open
behdad opened this issue Jun 18, 2023 · 45 comments
Open

[varLib] Support for sparse kerning / anchors #3168

behdad opened this issue Jun 18, 2023 · 45 comments
Assignees

Comments

@behdad
Copy link
Member

behdad commented Jun 18, 2023

I want to make sparse kerning / anchor masters possible.

@behdad
Copy link
Member Author

behdad commented Jun 18, 2023

Here's my current idea for sparse kerning. We'll add an extension keyword to the .fea format override. Then the following block:

override feature kern {

} kern;

will signify an sparse kerning block. The implementation simply create a class-kerning table that has 0x7FFF instead of 0 for missing entries. In varLib.merger then we just replace the 0x7FFF values with None, which will automatically remove them from the interpolation model.

@behdad behdad self-assigned this Jun 18, 2023
@behdad
Copy link
Member Author

behdad commented Jun 18, 2023

Same idea override can be extended to other lookup types.

@behdad
Copy link
Member Author

behdad commented Jun 19, 2023

@anthrotype wdyt?

@rsheeter
Copy link
Collaborator

rsheeter commented Jun 23, 2023

@cmyr might also be interested.

Current behavior, iiuc googlefonts/fontmake#998 correctly, is not entirely intuitive: if a static being merged:

The ability to specify some and let the rest interpolate is notably missing, I quite like the idea. I'm curious if type designers would find this useful, @davelab6 wdyt?

@davelab6
Copy link
Contributor

davelab6 commented Jun 26, 2023

VERY USEFUL! :)

Rod recapped the current situation: Masters can have full set of kerning, which are interpolated across masters, or zero kerning, also meaning kerning is interpolated for their design space locations; but masters currently can not have partial set of kerning and interpolate the rest. This would address that shortcoming.

@behdad
Copy link
Member Author

behdad commented Jun 26, 2023

VERY USEFUL! :)

Cool. I'll prioritize.

I need input on the .fea file extension (override keyword). cc @simoncozens @khaledhosny

@rsheeter
Copy link
Collaborator

@skef penny for Adobe's thoughts?

@skef
Copy link
Contributor

skef commented Jun 26, 2023

There are a lot of potential questions in this space.

I'll start by noting a deep rabbit hole we went down considering whether to support a distributed data model in a VF-first workflow. That is, a model where a single hierarchy of fea files could still support supplying kerning data for individual masters separately. At this point we're leaning against that model because although it could work fine in common cases, in the general case it will sometimes be hard to match up lookups between the per-master lists, and accordingly hard to describe the heuristics for doing so and to implement those heuristics in the varying implementations out in the field, which don't currently do anything like that. Given that except for things like chaining rules most of these files are derived from other sources, and can now be given in a centralized format with the appropriate "sparseness caveats" it seems like more trouble than its worth.

Given that, and coming into this issue with little background, I would need to know more about the scope and use of this idea. I suppose this is for an interim workflow where 0x7FFF is being used as a signal to the merger that the value is supposed to be interpolated. That does seem like a nice thing to have in the mean time; Adobe has already run into situations with its fonts where we've discussed this sort of thing.

Just looking at the description, though, I'm wondering about the matching problem. I take it that uses with an actually empty block would be rare. Usually the problem is that some glyphs are missing from your sparse master and you want to assign kerning values to the rest, with some implication that anything missing gets omitted from the interpolation model at that location. So the problem is how to flag a negative, right? Or maybe I just don't have a grasp on what's being proposed.

@behdad
Copy link
Member Author

behdad commented Jun 26, 2023

So the problem is how to flag a negative, right?

Correct.

@behdad
Copy link
Member Author

behdad commented Jun 26, 2023

I'll start by noting a deep rabbit hole we went down considering whether to support a distributed data model in a VF-first workflow. That is, a model where a single hierarchy of fea files could still support supplying kerning data for individual masters separately. At this point we're leaning against that model because although it could work fine in common cases, in the general case it will sometimes be hard to match up lookups between the per-master lists, and accordingly hard to describe the heuristics for doing so and to implement those heuristics in the varying implementations out in the field, which don't currently do anything like that.

I agree this issue is more of an implementation detail of fontmake, than a general extension I'm proposing to .fea.

@skef
Copy link
Contributor

skef commented Jun 26, 2023

Well, then it seems like the question is how it works when the relevant feature is "partially" rather than "completely" sparse, or I guess alternatively whether this mechanism would be limited to the latter case.

@skef
Copy link
Contributor

skef commented Jun 26, 2023

BTW I hope for Google's sake that they're good for this. I think we're up to 2 pennies now.

@behdad
Copy link
Member Author

behdad commented Jun 26, 2023

Well, then it seems like the question is how it works when the relevant feature is "partially" rather than "completely" sparse, or I guess alternatively whether this mechanism would be limited to the latter case.

Not sure I understand. User can specify 0 kerns where that's intended. Is that what you mean?

@skef
Copy link
Contributor

skef commented Jun 26, 2023

Maybe we should talk in specifics. Suppose we have something like

feature kern {
  pos [a b] [c d] 40;
} kern;

in the default master and

feature kern {
  pos [a b] [c] 30;
} kern;

in a sparse master, because "d" is missing from it. That's probably just wrong, at least without "enum", because we're pretending there's a single value here and there isn't -- the one for [a b] [d] is interpolated differently.

So what about

feature kern {
   pos a c 40;
   pos b c 40;
   pos a d 40;
   pos b d 40;
} kern;

and the analogous in the sparse master without the "d" lines. That should work conceptually, but what notation in the fea files really helps with this? "d" isn't in the sparse master so you can't "refer" to it in the intermediate file. If you "know" it's missing when merging you can do something clever, but that cleverness doesn't depend on fea grammar.

So, all that said, what cases are handled by this extension? In your example the kern feature is either empty or there's just an ellipsis to be understood in the contents. If the former, how often is a whole feature sparse? If the latter, what are some example cases that the keyword is supposed to cover?

@schriftgestalt
Copy link
Contributor

I agree this issue is more of an implementation detail of fontmake, than a general extension I'm proposing to .fea.

That was my first thought when starting to read that thread.

The problem needs to be addressed higher up in the data model, before the kern feature is generates, so in the kerning data structure.

And instead of writing one kern feature per master, and try to mash that together on the lookup level, it might be better to write one variable kern feature. Then it is trivial to define what master gets a value and what not. We came up with a proposal of a possible syntax: http://handbook.glyphsapp.com/en/layout/variable-gpos/

@skef
Copy link
Contributor

skef commented Jun 26, 2023

"one variable kern feature" and "one kern feature per master" are what Adobe has internally been calling the centralized and distributed models, respectively. Until recently we were going down the path of supporting either model, with the distributed model providing some backward compatibility with current practice.

The problem we've come to see with the distributed model is that once you allow things to vary even a little, which is the motivation for going beyond current practice, it's hard to match rule for rule in the general case. Existing tools build a set of lookups as they go. If you want to match in the general case, that's probably premature.

Say that a feature specification in the default has two blocks of pair positions separated by a single chaining rule. Now a sparse master is missing that chaining rule because none of its glyphs participate in it. The ordinary thing to do when reading those sparse master rules would be to merge all the pair positions together into a single lookup. But now that lookup won't match either of the two pair lookups you built for the default, so you'll have to dig around and try to rematch things.

Alternatively, you could, and would probably want to, keep all the data semi-symbolic before merging everything together at a later stage. That can probably be made to work but it's a dramatically different system than what we have now, and would probably drastically add to the upgrade costs for existing systems. (It would certainly do so for AFDKO.) All of this to process files that, in the normal case, are derived using higher-level tools anyway.

You could solve many of these problems by disallowing "missing" rules entirely, so that all features in all masters need to match up lexicographically. After all, assuming we're building features late in the process there's no longer any genuine sparseness of glyphs, only different patterns of interpolation you want to wind up with. So instead omitting a statement from a master it could just have "NULL" as the value, as a signal to leave that location out of the interpolation. Maybe this would be worth supporting, we haven't absolutely decided against it.

Note that the discussion in adobe-type-tools/afdko#1350 assumes a centralized model, so we assumed that this was the overall direction the community was expecting.

@simoncozens
Copy link
Collaborator

This is what I have been saying for the past few years to anyone who would listen: these kinds of problems are inherent in a "distributed" build workflow. Using a variable-first kerning syntax all the issues of both sparse masters and aligning rules all go away.

Progress isn't improving the merge, it's eliminating it.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

So, all that said, what cases are handled by this extension? In your example the kern feature is either empty or there's just an ellipsis to be understood in the contents. If the former, how often is a whole feature sparse? If the latter, what are some example cases that the keyword is supposed to cover?

My thinking was this. Eg. default master has:

feature kern {
  pos [a b] [c d] 40;
} kern;

and Black master has:

feature kern {
  pos [a b] [c d] 100;
} kern;

Currently if you add this to the Bold master:

feature kern {
  pos a d 60;
} kern;

then it's assumed that the rest of the pairs (a c, b c, b d) have a kern of zero. My suggestion with the override keyword is that it will only override the a d kern and interpolate the rest.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

This is what I have been saying for the past few years to anyone who would listen: these kinds of problems are inherent in a "distributed" build workflow. Using a variable-first kerning syntax all the issues of both sparse masters and aligning rules all go away.

Progress isn't improving the merge, it's eliminating it.

I agree. Hence why this is a fontmake hack not a larger .fea suggestion IMO.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

That said, we do need to think in UFO terms about how to highlight a master as having sparse kerning.

With glyph outlines it's easy: a missing glyph is assumed to be sparse. With kerning we assume zero.

@rsheeter
Copy link
Collaborator

Progress isn't improving the merge, it's eliminating it.

Agreed; fontc aspires to heed this advice. fea-rs will implement adobe-type-tools/afdko#1350 and fontc will convert source kerns to a fea file in this form.

we do need to think in UFO terms about how to highlight a master as having sparse kerning

Naively I might then think to add markers in the .designspace, perhaps to designate a source as having sparse kerning. No marker means current behavior.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

Naively I might then think to add markers in the .designspace, perhaps to designate a source as having sparse kerning. No marker means current behavior.

Right. @LettError @justvanrossum

@LettError
Copy link
Collaborator

Is there a definition of what sparse kerning is?

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

Is there a definition of what sparse kerning is?

This is what I mean:
#3168 (comment)

@cmyr
Copy link
Contributor

cmyr commented Jun 27, 2023

Just one small note: if it turns out that this work is a useful compromise in the near-term, I would consider avoiding introducing a new token into the grammar, in favour of some sort of magic-comment, to reflect the fact that this is a tool-specific hack and not a proposal for a general change.

@LettError
Copy link
Collaborator

Hm interesting! There are some questions to think about:

  • is this acceptable on masters on extreme locations - if so what is the expected value of the extrapolating pair?
  • what happens if the override pair creates an exception?
  • does a sparse master need the groups as well?

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

Hm interesting! There are some questions to think about:

  • is this acceptable on masters on extreme locations - if so what is the expected value of the extrapolating pair?

Yes. I can imagine sparse kerning needed at eg. Black Expanded. Or opsz=144.

  • what happens if the override pair creates an exception?

The varLib.merger should handle that. It already kinda works that way. It pulls the de facto kerning from the group-kerning to fill in exceptions for the masters that miss it.

  • does a sparse master need the groups as well?

Not necessarily. Again, the varLib.merger would fill in anything needed.

@skef
Copy link
Contributor

skef commented Jun 27, 2023

It seems like in this situation the tool already knows:

  1. That it is processing a variable font
  2. That the default location, and therefore the kerning data for it, can't be sparse in the relevant sense.

So, if we're talking about workarounds for this case involving new syntax or processing, why couldn't the system just assume:

  1. Values are interpolated at non-default locations,
  2. Unless the user adds an explicit value, which may be zero

?

Right now zeros are ambiguous, but you've already identified how one might get around that in the toolchain: just use a signal value like 0x7FFF until you get to the merging stage.

Maybe this leaves some hole that's not occurring to me?

@skef
Copy link
Contributor

skef commented Jun 27, 2023

BTW I still think the situation described in #3168 (comment) is mal-formed without an "enum". That statement is supposed to become one lookup with a single value. A tool that lets sparseness cut across the statement will need to turn it into at least two lookups with different values.

I think a good guideline here is that designers should organize their glyph classes in a way that would be compatible with a centralized model, even if they're currently using a distributed one. So, if you have sparse masters, split the glyphs missing from some masters into their own classes and give separate rules for them. Then the difference between master-specific fea files is the absence or presence of statements, rather than statements with somewhat different contents. In cases where the statement is documented as expanding to multiple rules (like with "enum") the tool level can be more flexible. This convention is a bit wordier in the fea files but it makes the processing a lot less mysterious.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

BTW I still think the situation described in #3168 (comment) is mal-formed without an "enum". That statement is supposed to become one lookup with a single value. A tool that lets sparseness cut across the statement will need to turn it into at least two lookups with different values.

I think a good guideline here is that designers should organize their glyph classes in a way that would be compatible with a centralized model, even if they're currently using a distributed one. So, if you have sparse masters, split the glyphs missing from some masters into their own classes and give separate rules for them. Then the difference between master-specific fea files is the absence or presence of statements, rather than statements with somewhat different contents. In cases where the statement is documented as expanding to multiple rules (like with "enum") the tool level can be more flexible. This convention is a bit wordier in the fea files but it makes the processing a lot less mysterious.

My personal opinion is that the "enum" keyword is implementation detail that has leaked into the .fea format. The compiler should just take care of it...

At any rate, our merge tool is smart enough already to take care of differing classes across masters and merging them properly, as well as handling different masters having exception lookup only, classs lookup only, or both.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

It seems like in this situation the tool already knows:

  1. That it is processing a variable font
  2. That the default location, and therefore the kerning data for it, can't be sparse in the relevant sense.

So, if we're talking about workarounds for this case involving new syntax or processing, why couldn't the system just assume:

  1. Values are interpolated at non-default locations,
  2. Unless the user adds an explicit value, which may be zero

?

Right now zeros are ambiguous, but you've already identified how one might get around that in the toolchain: just use a signal value like 0x7FFF until you get to the merging stage.

Maybe this leaves some hole that's not occurring to me?

If I was to do this again I probably would have done it that way. But changing it now would be a non-backward-compatible change. Hence why I'm asking for a marker in UFO / transient-fea.

The reason it was done the way it's done though, is that we wanted the varfont to match the static masters exactly at those locations.

@LettError
Copy link
Collaborator

@behdad are you solving a specific issue or making a general solution?

@skef
Copy link
Contributor

skef commented Jun 27, 2023

With this system, what if a designer needs a mix of actual zeros and interpolated values at a sparse location? Is the idea that this new keyword would turn on the behavior I described?

If that is the case, rather than a new keyword couldn't this just be a flag at the tool level, indicating that you want the new overall behavior rather than the backward-compatible behavior?

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

With this system, what if a designer needs a mix of actual zeros and interpolated values at a sparse location? Is the idea that this new keyword would turn on the behavior I described?

Then they would insert an actual kerning of zero. The hacked-up keyword basically only tells the compiler to fill in the unspecified class-kerns with 0x7FFF, and the merger takes it from there.

If that is the case, rather than a new keyword couldn't this just be a flag at the tool level, indicating that you want the new overall behavior rather than the backward-compatible behavior?

Possibly. Thanks for the idea. Let me think about that. That's probably much cleaner.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

@behdad are you solving a specific issue or making a general solution?

I'm trying to make a general solution. This has been an actual problem for designers for a long time. For example, @arrowtype demonstrated this issue in his Typographics talk earlier this month, where he wants the kern for Te to drop to zero somewhere along the weight axis that is not on top of a master. Currently to do that he either needs to interpolate and insert the entire kern data as two new masters... or the hack he did which is to substitute for a new T glyph!

My proposed solution would involve inserting two sparse kern masters only with the Te kern. One of them positive, the one immediately after it on the axis with zero.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

I can already fix varLib to do the right thing for anchors, to only consider for interpolation those masters that a glyph has anchors for. It's only kerning that needs a flag because of the mentioned backward-compatibility issue.

@Lorp
Copy link

Lorp commented Jun 27, 2023

One of them positive, the one immediately after it on the axis with zero.

BTW is there already syntax or method to express "immediately after N" or "immediately before N" when specifying location? Meaning + or - 1/16384 in 2.14 units. It’s a useful thing, as shown by the Q stroke in Apple Skia having a tiny zone that unintentionally interpolates.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

One of them positive, the one immediately after it on the axis with zero.

BTW is there already syntax or method to express "immediately after N" or "immediately before N" when specifying location? Meaning + or - 1/16384 in 2.14 units. It’s a useful thing, as shown by the Q stroke in Apple Skia having a tiny zone that unintentionally interpolates.

I agree! I was going to suggest this argument as a reason to allow integer normalized (not float) values. But +/-epsilon is probably better syntax!

@skef
Copy link
Contributor

skef commented Jun 27, 2023

BTW is there already syntax or method to express "immediately after N" or "immediately before N" when specifying location? Meaning + or - 1/16384 in 2.14 units. It’s a useful thing, as shown by the Q stroke in Apple Skia having a tiny zone that unintentionally interpolates.

We (Adobe) has this on our informal list but we haven't arrived at a notation yet.

@LettError
Copy link
Collaborator

I know it's useful. Mutatormath does it.

@behdad
Copy link
Member Author

behdad commented Jun 27, 2023

So to recap the discussion so far, it looks like I can do this by just defining a Lib key in the .designspace file; or individual Lib keys in the sparse-kern UFOs.

@madig
Copy link
Contributor

madig commented Jul 3, 2023

@behdad how would this interact with our compile-variable-fea effort at googlefonts/ufo2ft#635 (comment)? As Simon pointed out, sparsity is easy to do with variable fea syntax. To match current semantics, we actually have to first make a union of all kerning pairs used across all UFOs and then fill in the missing values with ufoLib.kerning.lookupKerningValue instead of a plain zero to properly handle exceptions and their absence in some sources. It would be easy to make a conditional that would instead skip missing pairs and fill in zero only for the default location. Not sure if we have to handle exceptions specially then.

Oh yeah, not sure if this has been discussed, but in a Light, Regular and Bold setup, if Lt and Bd have a certain kern pair value and Rg does not, does Rg get 1) a zero or 2) an interpolated value? What if there are multiple axes?

@behdad
Copy link
Member Author

behdad commented Jul 18, 2023

@behdad how would this interact with our compile-variable-fea effort at googlefonts/ufo2ft#635 (comment)? As Simon pointed out, sparsity is easy to do with variable fea syntax. To match current semantics, we actually have to first make a union of all kerning pairs used across all UFOs and then fill in the missing values with ufoLib.kerning.lookupKerningValue instead of a plain zero to properly handle exceptions and their absence in some sources. It would be easy to make a conditional that would instead skip missing pairs and fill in zero only for the default location. Not sure if we have to handle exceptions specially then.

Why do you use lookupKerningValue? That sounds wrong to me. Just skip that master.

Oh yeah, not sure if this has been discussed, but in a Light, Regular and Bold setup, if Lt and Bd have a certain kern pair value and Rg does not, does Rg get 1) a zero or 2) an interpolated value? What if there are multiple axes?

If Rg is default we require it to define the kern. Otherwise, currently it gets zero. With my proposed hack / lib key, it will be out of interpolation.

behdad added a commit that referenced this issue Jul 18, 2023
behdad added a commit that referenced this issue Jul 18, 2023
behdad added a commit that referenced this issue Jul 18, 2023
@madig
Copy link
Contributor

madig commented Jul 18, 2023

Why do you use lookupKerningValue? That sounds wrong to me. Just skip that master.

Not when trying to match current behaviour. The compile-variable-fea PR would need to know when to just leave stuff out :)

@behdad
Copy link
Member Author

behdad commented Jul 18, 2023

Why do you use lookupKerningValue? That sounds wrong to me. Just skip that master.

Not when trying to match current behaviour. The compile-variable-fea PR would need to know when to just leave stuff out :)

Ah right. That's indeed what we're also discussing here.

behdad added a commit that referenced this issue Jul 18, 2023
behdad added a commit that referenced this issue Jul 18, 2023
behdad added a commit that referenced this issue Jul 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants