-
Notifications
You must be signed in to change notification settings - Fork 9
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
Should variable composites be in the glyf table, and why? #103
Comments
Note: Although I can't make any promises, I've thought through some of what one |
Thanks skef. Indeed a separate table was originally how VarComposites were proposed by @justvanrossum et al. While this might architecturally look cleaner, I don't think in reality it is. I tried implementing this yesterday and found it extremely hard to decouple this table from the underlying glyph outline tables. So, from an implementation point of view, it's not like this can be an extra layer on top like you suggest. I personally am of the opinion that this would better fit within glyf / CFF2 tables, as currently proposed as well as a new CharString operator. Since currently CFF does not have components, just subroutines, it's conceivable that the new operator would NOT invoke another glyph either, just change the variation coefficients for the subsequent Note that when variable fonts were being implemented, it would have been possible to reuse the If such a proposed My .02 CAD. :) |
Can you say more about this? Given that COLR is already supported on most platforms it's not clear to me why "vcmp" (or whatever) would be so different. And also: there was a request at the TypeCon meeting for composite glyphs to have their own glyph data, and also to allow hinting instructions "overrides" in the composites. Would the need to support either or both of those change your answer?
I get the parallel, but the two situations seem distinct enough to me that things would weigh either way.
Aren't TupleVariationStores per-glyph? Wouldn't that just be a matter of an offset within the "vcmp" per-glyph subtable? |
GSUB / COLR are easy to implement as extra layers. Basically:
Now, if we try to add a "varcomposites" layer before "outline", it's output will be glyph index and a normalized designspace location. So the outline extraction cannot use the existing font. It has to instantiate a new font. Moreover, the "outline" function should be replaced with "low-level outline" that extracts the outline from glyf/CFF2 only if we were to take your idea of mixing in the glyph outline from the same glyph index. We also have a hurdle in HarfBuzz that we cannot modify the font in-place for new variations (breaks threadsafety of the font), nor our API allows for creating a new font at the new variations. So we have to add new internal API that accesses glyf/CFF2 directly with the requested variations without instantiating a new font (also important for performance). If we were to implement this, I expect that we modify the GLYF/CFF2 tables to directly access and handle the varcomposites table, not the other way around. In short: it doesn't have the nice layering properties of COLR. It's certainly possible. Just doesn't look as clean as the design originally suggests.
I understand the mixing of outline & composites comes naturally in your design. I considered that in the
They mostly are. There's the shared tuples part that is shared at the table level. If we go down this route we may want to reconsider some gvar decisions as well, like a more compact axis-region storage. If we do that we may want to do the same in a new |
OK, I understand the implementation concerns. I would characterize them as "This would violate a number of layer separations in a HarfBuzz/FreeType stack in a way that COLR does not, which might suggest that it would do something similar in other implementation stacks." I would say that would be something to weigh against other factors, with input from those in charge of those other stacks, but isn't definitive on its own. |
Let's work on a concrete table proposal. I'm willing to implement this and try. |
Let's design this.
The per-glyph data will be a concatenation of records similar to the existing proposal: https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1-varComposites.md Possibly adding a Also, unlike your proposal, I think regular Composite glyphs in |
I think the overhead of this approach, compared to the current proposal, is roughly 6 bytes per glyph. In the CJK font with 45,000 Kanji that I built, that translates to about 5% larger font. UPDATE: possibly only 4 bytes for larger fonts. This is how I calculated:
|
We can slightly optimize the variation storage by using a cvar-style, instead of gvar-style, TupleVariationStore. That is, store scalars instead of x,y variation values. I'm leaning towards keeping it simple and just use gvar though. Maybe can be prototyped and measured. |
I really dislike all the duplication here though, of loca/gvar basically. A middle approach would be to use the |
Hmm, I really like that we can mix outlines and components. I would prefer a solution that doesn’t distinguish between glyf and cff. |
I was thinking of that as a simplification but I agree that just leaving things as they are might be preferable.
Does no
What
If lowering the file size is the highest priority with this system, as it seems to be, maybe we should put more thought into this. We wouldn't need a sixteen bit tupleVariationCount, would we? Do we need it at all (can the count just be deduced)? Etc. |
(I think that part of the last bit of my last comment reflects my lack of detailed understanding of gvar. I'm trying to remedy that now so (with luck) I can be more useful to this thread ... ) |
I'm leaning towards reusing gvar unmodified, and leave optimizations to a newly designed version of gvar. The gvar with only scalars instead of x,y has some precedent in the cvar table. |
True. So the damage is 2 bytes only. That's nice. |
I think I'm a bit better on gvar now ... It appears that for the most part there are two variable data "models" in current use in OpenType:
Either of these models are broadly compatible with our use case, but neither seems like a particularly good match for our expected data. Making a big list of major/minor pair offsets in VARC glyph entries would throw away any locality, so that seems pretty definitively poor. The TVS is closer but how relevant it is depends on the data patterns we're likely to see in practice. Will all data, or all data for a particular glyph, tend to have the same masters? Will every value tend to either be variable or not variable? When things are mixed up you'll pay a higher price encoding the point values. @behdad et al have the advantage of having developed font data to look at, but even that might be specific to particular cases or methods of encoding and not entirely predictive. Anyway, it seems to me that:
After thinking about this for a while, I'm trying to sketch out how an IVS-based VARC table might work. Obviously there would be an offset to the IVS itself in the table header. The only engineering question is how the mapping from the VCR values in the array to the major/minor numbers is encoded. The gvar point number mapping already provides some ideas in that area. I'm just going to try to come up with something pretty simple but still somewhat optimized (at least in the sense that it will work pretty well when many subsequent values use the same set of locations, and therefore the same IVD). Then maybe people with access to more data might consider how it might perform in practice. I'm not sure but if the encoded mappings (value index to tuples in TVS, value index to major/minor number in IVS) wind up similarly sized, you might do a bit better with the IVS when it comes to header data (you can "share" that stuff with TVS but it takes a bit of header info to do it). Anyway, I'll post here if and when I come up with something. |
OK, here is what I'm thinking for an Item Variation Store-based design:
Bit maskI think it's very likely that the most common difference in variation model among axis values and transformation parameters is that some will be variable and some won't. So let's address that with a bitmap added after the Let Given this field, we can assign an entry index, in increasing MappingThe remaining problem is to map each entry index to an IVS major/minor number pair. How to do this efficiently depends on how we expect those entries to vary in "model" (the set of IVS regions used, corresponding to an Item Variation Data subtable) and to the extent of value duplication within a model. This is all on the assumption that the most typical case will be that subsequent values are unique and use the same model (that model most often being one region for each master in the glyph). These can be stored succeeding item entries in one IVD subtable. Then sometimes one value, or a string of values (perhaps in a particular variable component entry) will use a different model one needs to switch to temporarily. And at other times a given value (more often within the same model) will already have an IVD item, but not in the current ordering, so one needs to switch to that and get back to the unique values. These are just assumptions, of course, but they're not drastically different from the set of assumptions implicit in the Tuple Value Store architecture. (The TVS is somewhat more flexible when it comes to regions.) So let's consider a simple byte-length operator-based mapping for this purpose. Operators
Argument sizes
Temporary scopes
Mapping constraints
Initial valuesBoth the initial major and the initial minor value for a glyph start at 0 PseudocodeThis is just for illustration purposes and is probably full of off-by-one errors and such:
ExamplesMajor number 0 for all entries, minor number starting at 516 (out of less than
Do the same up to entry 101, which uses major number 4 (out of less than 255) and minor number 28, then continue:
|
That was just a sketch, of course. There are probably better mappings and may be better overall mapping strategies. Anyway, as we consider other TVS-based or TVS-like implementations we can consider the relative advantages of those vs an IVS-based system, with the main criterion presumably being how much space each uses. |
What we did in the COLRv1 is to store one uint32_t per item (can be glyph here, or record) and use consecutive numbers. This, however, would require a DeltaSetMapIndex to map the values most of the time, which itself adds bytes as well.
To me it's fairly clear that TVS is more optimized and better suited to this problem. Any reason you are pursuing the IVS approach so much? I think your sketch complicated things unnecessarily. |
It's not yet obvious to me that a TVS is more optimized in terms of disk space usage, depending on what patterns of parameter we're likely to see in practice. And based on the earlier discussion we're in this engineering realm where (for example) an average cost of six bytes per glyph could be reason to choose one solution over another, so presumably we want to be careful in our choices. If I understand the current system correctly, each specified parameter is given an index, which are treated as point numbers in a TVS context. Suppose you have a font where this is the typical pattern (which doesn't seem very unusual to me):
If I'm understanding the current TVS implementation correctly (which I still may not be), one would need to do one of two things to handle the values that aren't variable:
If the former, and the typical number of tuples/master locations is 8 + default, you would add 16 zero bytes per non-variable value you might otherwise avoid. If the latter --- well, it gets complicated. Packed point numbers appear to be run-length encoded, so one can assume you'll need at least one byte to stop and resume the count (right)? And that will be per-tuple-variation? The motivating idea of the TVS is that the per-glyph data will be quite uniform, which is a good assumption for outline data. For example you wouldn't expect to see points with all zero deltas frequently mixed in with other points, and run lengths will often correspond to every point in the glyph. This will also be true for composite glyphs in the current system, because transformation data doesn't currently vary, only the phantom point data varies (or can vary). Therefore, what system is optimal in terms of disk space usage would depend on what data patterns will be typical, which in turn depends on how we expect the variable composite system to be used. It seems to me that there will be cases where a TVS is smaller and cases where an IVS is smaller. Perhaps I'm just mistaken about all of this? I am pretty new to all the gvar stuff. |
If TVS is being revisited then it would be good to improve its handling of shared tuples for intermediate regions. Currently only the tuple’s peak may be shared. This means a font with an intermediate master wastes 4 * axisCount bytes per glyph by recording the same start and end values repeatedly. A simple refinement, where the Reserved bit 0x1000 in tupleIndex is set to 1 if start and end are also shared, would fix this. Start tuple would be at masked tupleIndex+1 in shared tuples. End tuple would be at masked tupleIndex+2. |
In case we need some more flags, which wouldn't surprise me, something like the following could make sense:
|
I think this is complicating it too much. I agree flat 2 is unnecessary and should go. We'll just reserve the top bit for "more flags" and spec that if it's set and the implementation doesn't understand it, then processing should be stopped. |
Further thoughts / investigation:
I'll prepare something based on it. |
I don't know how complicated we're interested in getting with an IVS mapping. Clearly the main advantage to be exploited over major/minor pairs for everything are sequential IVS entries (with the same major number). There will be times when you already have an individual entry "somewhere else" and you can do better (in terms of file size) by making an exception for it. When I was thinking about this before I sketched out a micro-language that I thought would do pretty well in terms of minimizing storage. I'll reproduce it here in case it's useful. (Note: I've gone back over the top portion of this but haven't reviewed the examples or pseudocode in detail before posting this.)
|
There are enough differences between a TVS and and IVS that comparing can be difficult, but one thing about the IVS is that given that you determine the mapping, you aren't required to have all the data for a given glyph be "adjacent". So the idea I was trying to work out with the write-up above is how to efficiently grab an individual delta-set, or short runs of delta-sets, if they're already defined for another glyph or glyphs, or are duplicated within a glyph. Given the mapping above you can do so at a cost of 3-7 bytes. So if 10% of delta-sets occur in more than one glyph that savings could be significant. |
Thanks Skef. I'm not comfortable with that level of bit fiddling. Also, the number of datasets or the number of rows within a dataset of an IVS is its internal affair and should not affect parsing. Give me a few days to think it more and see whether a new datastructure is justified. |
Here's a new data-structure that is optimized for this use-case. It's similar to ItemVariationStore, but stores deltas for multiple values at the same time (with same major/minor). My proposal is that we add one major/minor for tranform fields of the VarComposite record, and another one for the axes variations. Both will be only available if a flag is set. DesignThe design has a few distinct properties:
MultiItemVariationStore
|
Hangul: The woff2-encoded VARC font is 20% of the woff2-encoded OT font. |
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
I've now written this out at: |
I saved some more by encoding flags as a VarInt32 and reordering to bring the most common flag bits lower. New numbers are: Hangul: Hanzi:
|
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
Might revert as the savings are small. harfbuzz/boring-expansion-spec#103 (comment)
No spec yet. For results see: harfbuzz/boring-expansion-spec#103 (comment)
I think I understand how we got to the current variable composite
proposal. Roughly:
composites mechanism.
in that the offsets can remain in loca and you share the Tuple
Variation Store offsets with gvar.
However:
directly derived from the glyf mechanism (see Variable Compositing is analogous to shaping. So what about substitution? #104)
outlines out of a CFF2 table.
that being COLR.
Right now, a system that understands COLR starts by looking in that table for
an entry. If it finds one, it pulls path data from either glyf or CFF(2). If it
doesn't, it falls back to glyf or CFF(2). All of this happens
"below"/subsequent to shaping:
(shaping) -> COLR -> (glyf | CFF(2))
It seems like what "variable compositing" amounts to is an additional,
simplified shaping step. Call it "intra-glyph shaping", which occurs here:
(inter-glyph shaping) -> COLR -> (intra-glyph shaping) -> (glyf | CFF2)
The only reason the system doesn't already look like this is that the
compositing data is stored in the glyf table.
Set aside the question of other potential changes and just consider the current
proposal: If one wanted to have this mechanism for CFF2 also, would it be
substantially different? If it had to live inside the CFF2 table it would be
formatted differently (with blends instead of a separate tuple variation store,
perhaps using floats instead of fixed-point values of different scales, etc.)
But would the meaning of the parameters be any different? Would other
parameters be needed, or redundant, in the CFF2 case? I don't see how, or why.
So suppose the system worked this way instead:
top-level mechanism for mapping data to GIDs analogous to that of COLR.
The per-glyph tuple variation stores could be at an offset within the
data.
now, except for an additional
hint flags
field in the component record(and minus the stuff needed to play nice in the glyf table, like
numberOfContours
).glyf
composite mechanism when usingthis separate table.
and that GID also has a composite entry, the path data is added with no
transformation to the composite data. (This was asked for toward the end
of the TypeCon meeting.)
(TrueType instructions or CFF stems) and that GID also has a composite entry,
the relationship of the additional hinting data to the component hinting data
is determined by the hint flags.
The main thing to work out with this system would be the details of the hint
flags, but those problems are analogous for the two path data sources. Maybe
you need different flags for glyf and for CFF2 — which could overlap, because
one assumes mixing sources is off the table — but in each case the only thing
to be worked out is how to reconcile the hinting data. (We know this because we
already have COLR, so we already have implementations that grab data from the
bottom-level tables, alter the points according to affine transformations, and
render the results.)
This change would have these cons:
duplication across the tuple variation stores (header, regions).
And these pros:
the system would work just as well with CFF2 and glyf. This reduces pressure
on glyf format changes. CFF2 already goes above 64k glyphs, already supports
cubics, and can already losslessly represent quadratics as cubics (at the cost
of using floating point values in the conversion, when that precision is
needed).
doesn't need to be so closely tied to the older glyf composite mechanism.
The text was updated successfully, but these errors were encountered: