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

[COLRv2] Proposal: Reusing layers with the "self GID" #370

Open
simoncozens opened this issue Jul 19, 2023 · 19 comments
Open

[COLRv2] Proposal: Reusing layers with the "self GID" #370

simoncozens opened this issue Jul 19, 2023 · 19 comments
Milestone

Comments

@simoncozens
Copy link

To paint glyph 0 with the first palette color, you currently do this:

"baseGlyphPaintRecords": [
    {
        "gid": 0,
        "paint": {
            "version": 10,
            "glyphID": 0,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },

But doing basically the same thing with all glyphs in a font is quite a common use case. In COLRv1, to further paint glyphs 1, 2, 3...n with the first palette color, you need to do:

"baseGlyphPaintRecords": [
    {
        "gid": 0,
        "paint": {
            "version": 10,
            "glyphID": 0,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },
    {
        "gid": 1,
        "paint": {
            "version": 10,
            "glyphID": 1,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },
    // ...
    {
        "gid": n,
        "paint": {
            "version": 10,
            "glyphID": n,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },

(Modify for more complicated paint trees as appropriate.)

Effectively the same paint, differing only in the GID of the glyph that gets painted, and more to the point, this GID is known in parent the BaseGlyphPaintRecord.

fontpainter is a COLR editor which, internally, uses an abstraction called SELF_GID. The paints above would be represented internally as:

"baseGlyphPaintRecords": [
    {
        "gid": 0,
        "paint": {
            "version": 10,
            "glyphID": SELF_GID,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },
    {
        "gid": 1,
        "paint": {
            "version": 10,
            "glyphID": SELF_GID,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },
   //  ...
    {
        "gid": n,
        "paint": {
            "version": 10,
            "glyphID": SELF_GID,
            "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
        }
    },

which of course allows you to reuse layers:

var paintPallete0 = {
    "version": 10,
    "glyphID": SELF_GID,
    "paint": {"version": 2, "paletteIndex": 0, "alpha": 1 }
};

{
        "baseGlyphPaintRecords": [
            {"gid": 0, "paint": paintPallete0 },
            {"gid": 1, "paint": paintPallete0 },
            ...
            {"gid": n, "paint": paintPallete0 },
}

When the font is written to binary, any SELF_GID in a paint tree is replaced by the gid of the current BaseGlyphPaintRecord. On the OpenType side, this currently requires instantiating n individual paint trees to do effectively the same thing.

If the font format itself supported a magic number as a parameter to glyphID with the semantics of "gid of the current BaseGlyphPaintRecord", this could dramatically reduce the size of COLR tables in fonts where each glyph is painted in a substantially similar way.

@davelab6
Copy link
Member

Do you think you could support this within fontpainter's own implementation, such that you can demo a pair of TTFs that show the file size goes down, and the "after" TTF only works in fontpainter?

@simoncozens
Copy link
Author

To get this effect (each glyph painting a translated copy of its own source glyph three times)

Screenshot 2023-07-21 at 11 40 22

with a font of 488 glyphs currently takes a COLR table of 28361 bytes.

With a naive implementation of this idea (use self-GID in the paint table, the paint layer subtables become shared, but each glyph still has three distinct entries in the layerlist table) the byte size of the COLR table drops to 11803 bytes, a 39% reduction.

But more compression is available. At this point the layerlist entries are identical between the glyphs; if each glyph shared the same firstlayerindex, the layer count drops from 1464 to 3 entries, and the byte size of the COLR table drops down to 3037, a 90% reduction.

@Lorp
Copy link

Lorp commented Jul 21, 2023

It‘s a great reduction. However using an identical glyph shape repeatedly for each glyph is not a common case. I’d like to see an estimate based on 3 different-shaped layers, which still feels like "doing basically the same thing with all glyphs".

@behdad
Copy link
Collaborator

behdad commented Jul 21, 2023

I'm not convinced this is worth pursuing. The sharing happens only if the same paint is also used...

@behdad
Copy link
Collaborator

behdad commented Jul 21, 2023

I'm not convinced this is worth pursuing. The sharing happens only if the same paint is also used...

Okay maybe it is useful, for font-wide effects...

@simoncozens
Copy link
Author

Maybe a demonstration with three layers was confusing the issue.

Consider the very simple use case of a font which just applies a gradient to each glyph. I can imagine that becoming a very common practice. Currently that requires one paint per glyph; with this it would require one paint.

@behdad
Copy link
Collaborator

behdad commented Jul 21, 2023

Using GlyphID16 65535 for this works, since that glyph is inaccessible in current font technology. (max glyphid is 65534, because maxp.numGlyphs can at most be 65535).

@anthrotype
Copy link
Member

inaccessible in current font technology

since a change in the semantics of GlyphID would require a COLRv2 anyway, we might as well bundle that up with 32-bit GlyphIDs which are "in the air"...

@behdad
Copy link
Collaborator

behdad commented Jul 21, 2023

inaccessible in current font technology

since a change in the semantics of GlyphID would require a COLRv2 anyway, we might as well bundle that up with 32-bit GlyphIDs which are "in the air"...

Yes, GlyphID24 paints are needed when we get beyond64k... Maybe you're right.

@behdad
Copy link
Collaborator

behdad commented Jul 21, 2023

Alternatively it can be a separate PaintSelf with no data.

@behdad behdad changed the title Proposal: Reusing layers with the "self GID" [COLRv2] Proposal: Reusing layers with the "self GID" Jul 21, 2023
behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
@behdad
Copy link
Collaborator

behdad commented Aug 6, 2023

I implemented this in FontTools. See fonttools/fonttools#3244 (comment)

behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
behdad added a commit to fonttools/fonttools that referenced this issue Aug 6, 2023
@behdad
Copy link
Collaborator

behdad commented Aug 6, 2023

To get this effect (each glyph painting a translated copy of its own source glyph three times)

Where can I get this?

@behdad behdad added this to the COLRv2 milestone Aug 6, 2023
@simoncozens
Copy link
Author

Sixtyfour-VF-COLR.ttf.gz

@anthrotype
Copy link
Member

anthrotype commented Aug 7, 2023

in the fonttools PR you added something called PaintGlyphDelta. I see the rationale for PaintGlyphSelf (you can omit the glyph id) but why do we need the second one? It still contains a uint16 DeltaGlyphID (to be added on to obtain the desired glyph) so why not just use PaintGlyph?

@anthrotype
Copy link
Member

is it somehow to increase the chances of reusing a paint subtree? not sure how exactly but.

@simoncozens
Copy link
Author

Generally a colour font is made up of "layers" in the font editor which get composed to separate glyphs in the binary. Suppose you have the construction A, A.highlight, A.shadow, B, B.highlight, B.shadow... If this is repeated all the way through your font, you would want a template which says "Paint me with the default colour, paint my GID + 1 with a gradient, paint my GID+2 with 50% transparent black".

@Lorp
Copy link

Lorp commented Aug 7, 2023

There is a common misconception that these source "layers" remain intact in the binary font, as if color IDs in a palette are layer IDs. If this were true, "layers" could reliably be separated later, which is not the case. Templates would allow layers actually to survive, grouped by the glyphs that share a layer structure.

@behdad
Copy link
Collaborator

behdad commented Aug 7, 2023

in the fonttools PR you added something called PaintGlyphDelta. I see the rationale for PaintGlyphSelf (you can omit the glyph id) but why do we need the second one? It still contains a uint16 DeltaGlyphID (to be added on to obtain the desired glyph) so why not just use PaintGlyph?

The font I was experimenting with is a shadow font and has this glyph order:

  <GlyphOrder>
    <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
    <GlyphID id="0" name=".notdef"/>
    <GlyphID id="1" name=".notdef.shadow"/>
    <GlyphID id="2" name=".null"/>
    <GlyphID id="3" name="A"/>
    <GlyphID id="4" name="A.shadow"/>
    <GlyphID id="5" name="AE"/>
    <GlyphID id="6" name="AE.shadow"/>
    <GlyphID id="7" name="Aacute"/>
    <GlyphID id="8" name="Aacute.shadow"/>
    <GlyphID id="9" name="Abreve"/>
    <GlyphID id="10" name="Abreve.shadow"/>
    <GlyphID id="11" name="Acaron"/>
    <GlyphID id="12" name="Acaron.shadow"/>
    <GlyphID id="13" name="Acircumflex"/>
    <GlyphID id="14" name="Acircumflex.shadow"/>
    <GlyphID id="15" name="Adieresis"/>
    <GlyphID id="16" name="Adieresis.shadow"/>
    <GlyphID id="17" name="Agrave"/>
    ....

Ie. every glyph has two layers, and the glyph ids are predictable. It did indeed cause a lot of reusing. Just check number of layers:

841 glyph paints
1682 layer paints
Original COLRv1 table is 27016 bytes
Detecting self/delta glyph paints.
Templatizing paints.
2 unique templates for 841 glyphs
Skipped 2 templates as they didn't save space
Building COLRv2 font
841 glyph paints
4 layer paints
Constructed COLRv2 table is 5192 bytes
COLRv2 table is 80.8% smaller.
Saving Nupuram-Color.colrv1.COLRv2.ttf

@behdad
Copy link
Collaborator

behdad commented Aug 7, 2023

With a naive implementation of this idea (use self-GID in the paint table, the paint layer subtables become shared, but each glyph still has three distinct entries in the layerlist table) the byte size of the COLR table drops to 11803 bytes, a 39% reduction.

But more compression is available. At this point the layerlist entries are identical between the glyphs; if each glyph shared the same firstlayerindex, the layer count drops from 1464 to 3 entries, and the byte size of the COLR table drops down to 3037, a 90% reduction.

I tried my tool but it failed to do what you expected, because all layers have different VarIndexBase. But the varstore entries look identical:

       <Item index="2418" value="[0]"/>
        <Item index="2419" value="[80]"/>
        <Item index="2420" value="[0]"/>
        <Item index="2421" value="[0]"/>
        <Item index="2422" value="[40]"/>
        <Item index="2423" value="[0]"/>
        <Item index="2424" value="[80]"/>
        <Item index="2425" value="[0]"/>
        <Item index="2426" value="[0]"/>
        <Item index="2427" value="[40]"/>
        <Item index="2428" value="[0]"/>
        <Item index="2429" value="[80]"/>
        <Item index="2430" value="[0]"/>
        <Item index="2431" value="[0]"/>

How did you build this font? The VarStore looks unoptimized and not even built with OnlineVarStoreBuilder.

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

5 participants