Pomax / lib-font

This library adds a new Font() object to the JavaScript toolbox, similar to new Image() for images
MIT License
730 stars 72 forks source link

GSUB lookup type 6 sometimes fails on inputGlyphCount #130

Closed RoelN closed 2 years ago

RoelN commented 2 years ago

Testfont:

https://github.com/google/fonts/blob/main/ofl/abeezee/ABeeZee-Regular.ttf

Code:

import { Font } from "lib-font";

// Create a font object
const myFont = new Font(`Lib Font Test Font`);

// Assign event handling (.addEventListener version supported too, of course)
myFont.onerror = (evt) => console.error(evt);
myFont.onload = (evt) => doSomeFontThings(evt);

// Kick off the font load by setting a source file, exactly as you would
myFont.src = `./fonts/ABeeZee-Regular.ttf`;

// When the font's up and loaded in, let's do some testing!
function doSomeFontThings(evt) {
  const font = evt.detail.font;
  const GSUB = font.opentype.tables.GSUB;

  const scripts = GSUB.getSupportedScripts();
  let allGlyphs = {};

  scripts.forEach((script) => {
    let langsys = GSUB.getSupportedLangSys(script);

    allGlyphs[script] = {};

    langsys.forEach((lang) => {
      let langSysTable = GSUB.getLangSysTable(script, lang);
      let features = GSUB.getFeatures(langSysTable);

      allGlyphs[script][lang] = {};

      features.forEach((feature) => {
        const lookupIDs = feature.lookupListIndices;
        allGlyphs[script][lang][feature.featureTag] = {};
        allGlyphs[script][lang][feature.featureTag]["lookups"] = [];

        lookupIDs.forEach((id) => {
          const lookup = GSUB.getLookup(id);
          lookup.subtableOffsets.forEach((_, i) => {
            if (lookup.lookupType === 6) {
              console.log(lookup.getSubTable(i).inputGlyphCount); // ❌
            }
          });
        });
      });
    });
  });
}

Output:

1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
undefined

Problem:

Instead of a positive number, undefined is returned.

RoelN commented 2 years ago

Related error (not finding input coverage for lookup type 6) can also be observed in:

https://github.com/google/fonts/blob/main/ofl/abyssinicasil/AbyssinicaSIL-Regular.ttf

Which will return undefined for all counts when loaded in the example code above.

Pomax commented 2 years ago

nice finds! I'll have to dig into this over the weekend.

RoelN commented 2 years ago

Another font with an undefined inputGlyphCount: https://github.com/google/fonts/blob/main/ofl/akayakanadaka/AkayaKanadaka-Regular.ttf

RoelN commented 2 years ago

Another one likely getting bitten by the same bug: https://github.com/google/fonts/blob/main/ofl/alice/Alice-Regular.ttf

Pomax commented 2 years ago

I rewrote your original example code a little to output a treeish structure to see what we're dealing with, which for the original font shows:

script id 0
|--lang id 0
| |--feature id 0
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 2)
| |--feature id 1
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 2)
| |--feature id 2
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 2 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 3 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | |--lookup id 1
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| |--feature id 3
| | |--lookup id 0
| | | |--subtable id 0 (type 4, substFormat 1)
| |--feature id 4
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| |--feature id 5
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 1)
script id 1
|--lang id 0
| |--feature id 0
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 2)
| |--feature id 1
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 2)
| |--feature id 2
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 2 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 3 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | |--lookup id 1
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| |--feature id 3
| | |--lookup id 0
| | | |--subtable id 0 (type 4, substFormat 1)
| |--feature id 4
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| | | |--subtable id 1 (type 6, substFormat 3)
| | | | |--subtable glyph count: 1
| |--feature id 5
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 1)
|--lang id 1
| |--feature id 0
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 1)
| | | | |--subtable glyph count: undefined

So it looks like this only affects GSUB lookup type 6, subtable format 1, which is the Chained Sequence Context. Looking at its table structure, this format does not expose the inputGlyphCount at the subtable level, but encodes a list of chainSubRuleSets, which in turn encode a set of chainSubRule objects, and each of those have an inputGlyphCount record.

Pomax commented 2 years ago

Updating the code to this:

            // ...

            if (lookup.lookupType === 6) {
              const inputGlyphCount = subtable.inputGlyphCount;

              console.log(
                `| | | | |--subtable glyph count: ${inputGlyphCount}`
              );

              const chainSubRuleSetCount = subtable.chainSubRuleSetCount;
              console.log(
                `| | | | |--chainSubRuleSetCount: ${chainSubRuleSetCount}`
              );

              for (let css = 0; css < chainSubRuleSetCount; css++) {
                const chainSubRuleSet = subtable.getChainSubRuleSet(css);
                const chainSubRuleCount = chainSubRuleSet.chainSubRuleCount;
                console.log(
                  `| | | | | |--chainSubRuleCount for set ${css}: ${chainSubRuleCount}`
                );

                for (let csr = 0; csr < chainSubRuleCount; csr++) {
                  const chainSubRule = chainSubRuleSet.getSubRule(csr);
                  const inputGlyphCount = chainSubRule.inputGlyphCount;
                  console.log(
                    `| | | | | | |--inputGlyphCount for rule ${csr}: ${inputGlyphCount}`
                  );
                }
              }
            }

            // ...

Shows:

script id 1
|--lang id 0
| |--feature id 0
| | |--lookup id 0
| | | |--subtable id 0 (type 1, substFormat 2)
...
|--lang id 1
| |--feature id 0
| | |--lookup id 0
| | | |--subtable id 0 (type 6, substFormat 1)
| | | | |--subtable glyph count: undefined
| | | | |--chainSubRuleSetCount: 2
| | | | | |--chainSubRuleCount for set 0: 1
| | | | | | |--inputGlyphCount for rule 0: 2
| | | | | |--chainSubRuleCount for set 1: 1
| | | | | | |--inputGlyphCount for rule 0: 2
Pomax commented 2 years ago

@RoelN this feels like it's just part of the GSUB insanity rather than a bug: the values are there, but lookup 6.1 just encodes them in a much deeper spot than 6.3

(the master lookuptype list, linking through to each separate subtable format, can be found here)

RoelN commented 2 years ago

@Pomax Thanks for looking into it. What does this mean for when you want to get the 6.1 lookups? I'd like to show these features plus their input in Wakamai Fondue! :-)

Pomax commented 2 years ago

You can check for the substFormat value to determine which flavour of lookup type 6 you're handling, and if it's 1, iterate over the ChainSubRuleSets using the code above:

let inputGlyphCount = 0;

for (let css = 0; css < chainSubRuleSetCount; css++) {
  const chainSubRuleSet = subtable.getChainSubRuleSet(css);
  const chainSubRuleCount = chainSubRuleSet.chainSubRuleCount;

  for (let csr = 0; csr < chainSubRuleCount; csr++) {
    const chainSubRule = chainSubRuleSet.getSubRule(csr);
    // and then, for example:
    inputGlyphCount += chainSubRule.inputGlyphCount;
  }
}

// inputGlyphCount is now the total tally of all input counts spanned by this 6.1 lookup
RoelN commented 2 years ago

Thanks very much! I'll implement this in the Fondue a.s.a.p. as 6.1 is now broken 😅

Pomax commented 2 years ago

(closing as not actually a bug but just "how opentype GSUB works, in all its insane glory =D")

RoelN commented 2 years ago

Of course, thanks!

how opentype GSUB works, in all its insane glory =D

This might be my commit message when I fix it!