Variable Compositing is analogous to shaping. So what about substitution?
Closed this issue · 22 comments
I note in #103 that "variable compositing" seems to amount to an
additional, simplified shaping step. However, as specified the system only
includes an analog of positioning, and lacks an analog of substitution.
Consider this case:
Suppose that you are working in a model that has three conceptual layers: atoms,
molecules, and glyphs. Perhaps these are exposed by a font editor.
For a given molecule, the designer decides she wants the outline of one atom to
change within one sub-region of design space, and a different atom to change
within a slightly different sub-region of design space. The molecule is used in
25 different glyphs. With the existing proposal it seems like there are two
options:
- Force the designer to play tricks with the masters so that all versions of
the atom interpolate, and then position the masters in design space right next
to each other for quick interpolations. This increases the burden on the
designer. - Allow the designer to specify different, non-interpolable versions of an
atom in different subspaces of design space, and sort things out in the
compiled font.
In our example, it seems like the only option for 2 with the current proposal
would be to use GSUB's rvrn
or something similar. Given that the molecule has
four versions (for each permutation of default and altered atom), you would
need 100 GIDs to handle the 25 glyphs. You would also need to either duplicate
the composite data for the other, always-present atoms across the four
molecules, or add another "base molecule" layer into the hierarchy to collect
that data together to avoid duplication.
Now, of course, in some cases you'll need to do something like this anyway:
mainly when swapping an atom affects the metrics of the ultimate glyph. But
such cases seem like the exception rather than the rule.
So:
- Should there be some more targeted way of supporting this sort of case
in a variable composite model? - Does this suggest that the model should draw a little bit more from
GSUB/GPOS and perhaps be less closely tied to the older glyf model? (For
example, you might need distinct positioning data for the different atoms
that can be substituted into a molecule, perhaps loosely analogous to
distinct contextual positioning GPOS rules that could apply after a
substitution.)
Some very initial thoughts:
Conceptually you don't need much infrastructure to support this. Right now a variable component record "directly" includes some path data:
VC
If you instead had something like:
if (condition set)
VC1
else if (condition set)
VC2
else
VC3
You'd handle the cases that seem most needed (assuming a VC can be null/"blank").
There's a bunch of annoying things to work out with layouts and record lengths and offsets for the condition sets and such, but that's all pretty normal for OpenType tables.
My gut feeling is that this complicates things unnecessarily. One can already use GSUB to compose glyphs conditionally.
Where in the space would you put this:
- Designers aren't likely to need non-interpolable atoms at different points in the designspace of a molecule, so it's sufficiently unusual to add support for.
- Designers might use this routinely but it's acceptable to burn extra GIDS (and with the metrics) to enable the substitutions.
I would disagree with 1. I guess 2 is a matter of opinion but seems to go against the spirit of the rest of the concept.
The following is the text of a message about this sent to the MPEG-OTSPEC mailing list:
I still think a substitution mechanism could be a good addition but I'm not able to do a proper proposal. Instead I'm going to outline the reasoning and a design. The design turns out to be pretty simple and I don't think it would be a burden to specify or support.
The reasoning
Sometimes the basic design for a glyph or a component is appropriate at one part of design space but not at another. This is clear at the glyph level, with the dollar sign as an archetypal example. I don't have first hand knowledge of this but asking around some designers have indicated that glyphs in pre-digital font faces, including CJK glyphs, used to differ somewhat more between weights, implying that multi-master based design has washed out some of those idiosyncrasies. Even so, I have confirmed that Adobe fonts using multi-master design sometimes need little differences. Most of these can be handled by playing tricks with masters, because they have more to do with proportion than changes in the visible elements, but playing tricks with masters in unusual locations has its own pitfalls.
Now, suppose it were desirable to have a component of a CJK glyph have a distinct design at different points of design space that is either non-interpolable or would be difficult to design as interpolable (perhaps it would require "trick geometry"). If there were one such component with two such designs, every composite that includes the component would need two GIDs: one for each design. A composite including two such components, assuming they don't "flip" at the same location(s), would need four GIDs for the four combinations. And so on. Each of these glyphs would duplicate a lot of data, including the variable composite records themselves, hmtx, any kerning, and so on.
If instead one could vary component inclusion based on condition sets, as long as the component differences don't change the composites out "outer metrics" (the normal case, I think), one GID would suffice. And having such an option in the spec might feed backward into tools, making it easier to vary component designs in this way.
The design
This is how I would modify the current VARC proposal to support component substitution:
- The VARC table header gets a new Offset32To<CFF2IndexOf<ConditionSet>>. When non-zero this points to an index structure of ConditionSet Tables, which in turn have offsets to condition tables. For any condition value tables, the major and minor numbers pick out an entry in the MultiItemVariationStore of the same table.
- A new IS_CONDITIONAL variable component flag indicates that the component entry has a new optional uint16 field ConditionSetIndex, which is the index of a condition set in the added top-level ConditionSetIndex.
- Variable component records are then processed this way: When the first entry has IS_CONDITIONAL set, or an entry has that flag when the previous entry did not, the following entries until and including the next entry without the flag are grouped together. Each condition set is checked in order until one is met, and that entry is composited as specified. If none of the condition sets are met the last entry (without the flag) is composited as specified.
- In order to make this setup complete there should be a compact means of specifying an "empty" entry to indicate that nothing should be composited when that condition set is met. One option is another flag, another option is a special GlyphID24 with that implication.
Note that:
- The cost of adding this mechanism and not using it is an extra 4 bytes per VARC table.
- The cost of using it a little is small. Two condition sets with three conditions each is 70 to 100 bytes, then add the cost of duplicating the variable component records.
- The grouping is not difficult to implement or hard to explain in the specification.
I generally like the idea, although I'm not sure how many use cases there are.
It does make me think of a similar use case, which I don't think your proposal would cover: sometimes localized CJK glyphs only need a different component: switching component glyph based on locale. This goes more in the direction of "shaping at the component level". Not saying this should be considered right now, but it's at least food for thought.
I kinda like the idea.
I didn’t quite get point 3: did you mean "Each condition set is checked in order until one is met, and that group of entries is composited as specified". (It would help to avoid the word "entry" when talking about both components and conditions.)
Assuming my reading is correct, I don’t like:
- the decoder must decode components it will not use (since it does not know the length to jump over)
- the introduction of "grouping"
- the imbalance between IF (unlimited components) and ELSE (one component)
- the "empty" handling in point 4
I’d therefore suggest a couple of refinements:
- Limit the concept to 0 or 1 components for IF, 0 or 1 components for ELSE.
- In the appropriate place in the structure when IS_CONDITIONAL is set, store the <ConditionSet> offset, then store two uint32var numbers m and n, representing the byte lengths of the components to be used if true and false respectively. Use length=0 to represent no component.
- Then store 0, 1 or 2 components, as appropriate.
- On TRUE, the decoder decodes the next component, then jumps n bytes. On FALSE, the decoder jumps m bytes then decodes the next component.
It does make me think of a similar use case, which I don't think your proposal would cover: sometimes localized CJK glyphs only need a different component: switching component glyph based on locale. This goes more in the direction of "shaping at the component level". Not saying this should be considered right now, but it's at least food for thought.
Since VARC already introduces the idea of axes that are not represented in fvar, I wonder if locale could, via a lookup*, be represented by F2DOT14 axis location. VARC would then use normal <ConditionSet> conditions to select components depending on locale.
* not a OT layout-style lookup!
I have a simpler proposal: introduce a new flag CONDITIONAL
. And if CONDITIONAL
is set, have a conditionVarIndex
, which refers to a tuple of one value in the MultiVarStore
. If the fetched value is >= 0 then the component is added to the glyph. Otherwise it's ignored.
I think that addresses the requirements?
And if
CONDITIONAL
is set, have aconditionVarIndex
, which refers to a tuple of one value in theMultiVarStore
It seems like this idea also fulfils @skef’s proposal OTT Variation Conditions: Condition Value via VARC.
I think in this case I don't favor the alternative proposal for expressing the conditions. As I see it the pros and cons are:
Pros:
- Slightly more compact in size
- Probably a bit easier to specify, maybe to implement
Cons:
- This would make the condition mechanism in VARC different from the condition mechanism used elsewhere, so if the two need to be "synchronized" in various cases (like to drive kerning decisions related to substitution decisions) it could get tricky.
- Something like a condition value was needed to plug a hole in what condition sets are able to express. However, I don't think they're great for the general case because they're rather opaque. If you have more than two axes, even telling what's going on with a condition value can be tricky, whereas normal axis conditions are pretty much self-explanatory. If I'm
ttx
-ing something to identify some problem I'd much rather have range conditions for cases that can be expressed with range conditions. - The condition set specification is already designed to be extensible, and we're extending it in this working draft. I think it's preferable to have a VARC condition mechanism extensible in the same way, and the easiest way to do that is to use the same sub-specification.
For me these cons outweigh the pros.
I generally agree with your assessment. Your proposal to the ConditionTable requires a VarStore. Should we use the MultiVarStore, or add a VarStore to the table?
Tangent:
Re https://github.com/adobe-type-tools/opentype-spec-drafts/blob/main/condvalue_spec.pdf should we allow warping the min/max as well as the value?
I may be mistaken but from my reading of the VARC spec a MultiVarStore should work fine. We'd have to add a bit of explanation to the spec to clarify the VARC case, but I think all we need to say is one of the two following:
- The entry in the MultiVarStore picked out by the major/minor index pair must always have (effective) length 1, or
- If the entry in the MultiVarStore picked out by the major/minor index pair has a length greater than 1, the first (well ... 0th) value should be used.
Does that sound right? I certainly think it would be a bummer if we couldn't just use the MultiVarStore one way or another.
- The entry in the MultiVarStore picked out by the major/minor index pair must always have (effective) length 1, or
This sounds good to me.
(We could also say that condition value deltas are relative to the GDEF item store even when they're in VARC, but that breaks encapsulation in an ugly way that seems best to avoid.)
Re https://github.com/adobe-type-tools/opentype-spec-drafts/blob/main/condvalue_spec.pdf should we allow warping the min/max as well as the value?
I don't think I understand the question. With a condition value the "value" part ranges over the whole space, so there isn't a min/max for that type. Are you asking about further options relative to format 1 (or a new format with similar properties), maybe allowing the places it applies on a given axis to vary, or something else?
switching component glyph based on locale
or based on opsz; its common to do "stroke reduction" in CJK. https://commons.wikimedia.org/wiki/File:CJK_SO-stroke_reduction.svg etc
- Limit the concept to 0 or 1 components for IF, 0 or 1 components for ELSE.
- In the appropriate place in the structure when IS_CONDITIONAL is set, store the offset, then store two uint32var numbers m and n, representing the byte lengths of the components to be used if true and false respectively. Use length=0 to represent no component.
- Then store 0, 1 or 2 components, as appropriate.
- On TRUE, the decoder decodes the next component, then jumps n bytes. On FALSE, the decoder jumps m bytes then decodes the next component.
I think there are a few reasons why this is less desirable than the original proposal.
First off, there's no imbalance between "if" and "else", as is usual the semantic is:
if A
use component W
else if B
use component X
else if C
use component Y
else
use component Z
The only asymmetry in the structure is that the last else doesn't have a guard, but that's because it doesn't need one.
Now, one could object to requiring the trailing else, which is how I wrote up the design, but that's what the "empty" component is for. And if that's handled with a flag the semantic effectively becomes something else.
Second, condition sets aren't full boolean expressions so a single condition set might not be able to fully express "everywhere" a given component needs to go. So suppose you wind up having a choice between two sets with a non-zero intersection in design space or four that don't intersect. Using your system you would have to use the four, precisely because you've avoided grouping. With the system as specified you can use the two, because the if/else structure means that only one component from the group will ever be included.
As far as offsets go I don't see the extra decoding as a problem given the modest typical sizes of these structures, and spending the extra time decoding rather than adding bytes for offsets is in the same spirit as using var
types rather than fixed sizes (the latter being, after all, what adds the decoding requirement in the first place). But I can see going either way.
Right, rereading your point 3 along with the IF…ELSEIF…ELSE pseudocode, it makes sense.
It’s tempting to allow multiple components in each conditional block. Otherwise it gets painful to include a set of components. Nested conditions would also be cool. Both of these additions would, I think, require offset fields.
Re https://github.com/adobe-type-tools/opentype-spec-drafts/blob/main/condvalue_spec.pdf should we allow warping the min/max as well as the value?
I don't think I understand the question. With a condition value the "value" part ranges over the whole space, so there isn't a min/max for that type. Are you asking about further options relative to format 1 (or a new format with similar properties), maybe allowing the places it applies on a given axis to vary, or something else?
My bad. I misremembered how it works.
As far as offsets go I don't see the extra decoding as a problem given the modest typical sizes of these structures, and spending the extra time decoding rather than adding bytes for offsets is in the same spirit as using
var
types rather than fixed sizes (the latter being, after all, what adds the decoding requirement in the first place).
I also don't think decoding unused components is a big deal that should affect the design.
This is done now.