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

fix compilation of AAT kern tables #1094

Merged
merged 9 commits into from
Nov 4, 2017

Conversation

anthrotype
Copy link
Member

  • When compiling kern subtables for version=1.0 kern tables (AAT) the subtable header was written incorrectly: there is no version there, the length is a uint32 and there's an additional uint16 for tupleIndex (used for GX)
  • Use the coverage low byte to select subtable "format", instead of the version field, only present in OT kern subtable header.
  • KernTable_format_0 now takes an apple=False argument, used to choose the different headers and whether to read/write tupleIndex.
  • A new tupleIndex attribute is written out to TTX for Apple-style kern subtables. Old TTX files which lack that attribute will still be read (with a warning) and will default to 0 when recompiled or dumped with current fonttools.

Fixes #1089

- When compiling kern subtables for version=1.0 kern tables (AAT)
  the subtable header was written incorrectly: there is no version,
  the length is a uint32 and there's an additional uint16 for
  tupleIndex
- Use the 'coverage' low byte to select subtable "format", instead
  of the 'version' field, only present in OT kern subtable header.
  The getkern method was failing with AttributeError on 'unknown'
  subtable formats, as their class only has 'format' instead of
  'version' attribute. The 'version' attribute is renamed to
  'format' also to avoid confusion, but the old one is kept for
  backward compatiblity. In the only implemeted subtable class,
  'format' becomes a class attribute rather than instance's
  (it must always be 0).
- KernTable_format_0 now takes an 'apple=False' argument, used to
  know the different headers and whether to read/write tupleIndex.
- minor pep8 whitespace and indentation fixes
- A new 'tupleIndex' attribute is written out to TTX for apple
  kern subtables. Old ttx files which lack that attribute will
  still be read (with a warning) and will default to tupleIndex=0
  when recompiled or dumped with current fonttools.

Fixes fonttools#1089
@@ -33,21 +33,23 @@ def decompile(self, data, ttFont):
else:
self.version = version
data = data[4:]
tablesIndex = []
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this tableIndex was unused so I removed it

@@ -59,7 +61,7 @@ def compile(self, ttFont):
nTables = 0
if self.version == 1.0:
# AAT Apple's "new" format.
data = struct.pack(">ll", fl2fi(self.version, 16), nTables)
data = struct.pack(">LL", fl2fi(self.version, 16), nTables)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Apple spec has unsigned long integers (uint32) for version and nTables, not signed

data = data[6:]
else:
version, length, coverage = struct.unpack(">LHH", data[:8])
length, coverage, tupleIndex = struct.unpack(">LHH", data[:8])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were also decompiling coverage incorrectly here for apple kern subtables: the tupleIndex was being read in as "coverage" attribute, whereas the actual coverage (in second position, where length used to be) was being thrown away.

kernTable[(
ttFont.getGlyphName(left),
ttFont.getGlyphName(right))] = value
if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps part of the responsibility for not noticing this bug before is that we are silently letting pass up to 4 extra bytes in the kern subtable. Why don't we always warn when the data length doesn't match the stated number of pairs or is not multiple of 6?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, Calibri has 4 bytes in excess.. 5cd0a55
ok then 🤷‍♂️

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I still see Calibri's kern table shrink a lot after roundtripping with ttx. I mentioned it in 5cd0a55 but I don't think anyone followed up on that. Check ttx -l before and after roundtripping. But then again, I'm not entirely sure where that Calibri came from.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can send you the version I have if you're curious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly. I'd be more interested in having the AAT kern compilation fixed. Maybe you could also do a quick review since you wrote the original code? Thanks anyway :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, you can send me that file, thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see Win10 calibri.ttf has 160kb kern table. I suppose MS started ignoring the binary search header completely and use the subtable length instead. We should do the same. I'll fix HarfBuzz as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. Ignoring subtable length, respecting bsearch header! This is now implemented in HarfBuzz:
harfbuzz/harfbuzz@82a38d1


for left, right, value in kernTable:
data = data + struct.pack(">HHh", left, right, value)
return struct.pack(">HHH", self.version, len(data) + 6, self.coverage) + data
# ensure mask bits 8-15 (subtable format) are set to 0
self.coverage &= ~0xFF
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I follow the pattern found elsewhere in ttLib whereby any necessary fix-ups or modifications occur at compile time.

# previous fontTools versions didn't export tupleIndex
log.warning(
"Apple kern subtable is missing 'tupleIndex' attribute")
self.tupleIndex = None
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I decided to set tupleIndex to None when it's missing from a kern TTX file; if I had set it to 0, it wouldn't be possible to distinguish from a kern TTX file with a missing tupleIndex attribute or one with tupleIndex=0.
The value still defaults to 0 when the imported font is compiled or dumped again to XML.

@behdad
Copy link
Member

behdad commented Nov 3, 2017

Kinda relevant #314

@anthrotype
Copy link
Member Author

#314 (comment)

we can detect [kern subtable exceeding maximum length] and possibly split into multiple subtables.

I could do that later in another PR, as this is about fixing AAT kern decompilation/compilation.

as we don't run test on big endian systems so that check is always true anyway...
had removed that fixture and squashed.. should have re-run the test
before pushing :-/
# Since this "version" is always 0 (and is not present in the
# later AAT extensions), we simply ignore it here
_, length, coverage = struct.unpack(">HHH", data[:6])
subtableFormat = coverage & 0xff
Copy link
Member Author

@anthrotype anthrotype Nov 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh.... actually also the order of format and coverage bytes is different between OT and AAT kern! In OT the first byte is format and the following one is coverage, whereas in AAT it's the reverse. I got this completely wrong, sorry.. 😬
Thank @justvanrossum that had me check that CALIBRI.TTF kern. I was surprised when I saw a microsoft font with a kern subtable format 1 (Apple-only). It turns out I was reading wrong, the 1 was the coverage bit (horizontal data), not the format.
Why did they make it so complicated 😖

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's as if someone took the existing format and said let's change everything about it! Allocate higher byte for coverage, allocate bits MSB-to-LSB, and allocate a bit for horizontal, instead of vertical :)).

In OT kern subtable header, the format is the high byte of 'coverage'
bit mask (bits 8-15), and the low byte (bits 0-7) is the actual coverage
bits.

In AAT kern, it's the opposite: the coverage flags are the high byte,
whereas the subtable format is the low byte.

Also adjusted the test data, and set coverage to 1 for OT kern subtable
(which means the usual horizontal kerning).
@anthrotype
Copy link
Member Author

About that CALIBRI.TTF font mentioned by @justvanrossum, it also seems to use the trick of having a huge single kern subtable with the USHORT length truncated (like @twardoch explained in #314 (comment)).
I wonder if that kerning works at all.
But again, this PR is about fixing compilation of valid AAT kern tables, rather than rescuing broken OT kern tables.

@behdad
Copy link
Member

behdad commented Nov 4, 2017

we can detect [kern subtable exceeding maximum length] and possibly split into multiple subtables.

I could do that later in another PR, as this is about fixing AAT kern decompilation/compilation.

Note that splitting doesn't help as Windows will ignore them all. Looks like we should ignore num-items and use table length instead. But then that's not what this PR is about. Just wanted to make sure you don't go down that route.

@anthrotype
Copy link
Member Author

The description of the kern subtable format 0 on Apple website says that:
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html

The table needs to end with an entry for a left-hand glyph of 0xFFFF, a right-hand glyph of 0xFFFF, and a kerning value of 0.

However, Zapfino.ttf (12.0d1e6; 2016-07-18), which is the only font I was able to find on my macOS Sierra which has a kern version=1.0 and a format 0 subtable does not end with FFFF FFFF 0000, but with a normal kern pair 04BA 04BA 0019 (which is <pair l="fouroldstyle" r="fouroldstyle" v="25"/>).

Shall we enforce that requirement somehow? Not raise of course, but maybe log a warning?

Copy link
Member

@behdad behdad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we enforce that requirement somehow? Not raise of course, but maybe log a warning?

No idea how to roundtrip properly or if it matters. If we see such a pair we shouldn't try looking it up as a glyph-id, but not sure what to do otherwise.

@@ -17,7 +19,7 @@ class table__k_e_r_n(DefaultTable.DefaultTable):

def getkern(self, format):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this function is so wrong! Shall we remove it or what?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt anyone ever used it because it was throwing AttributeError because of missing "version" attribute on anything other than format-0 subtables (the only one implemented). Sure, I can remove it. We can add a better one when we implement other subtable formats.

# 'version' is kept for backward compatibility
version = format = 0

def __init__(self, apple=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The apple is really an input to decompile() and compile(), not a property of the table itself. What do you think about removing it and moving it there? (like isCFF2 if you remember that discussion.)

Also, the way I implemented this in HarfBuzz, I separated the Apple/MS subtable wrapper abstraction from the subtable types themselves. I think we can do something similar here to simplify the code if you feel like.

Or you merge this, and I do that when implementing format-2.

Here's the HB code BTW:
https://github.com/behdad/harfbuzz/blob/master/src/hb-ot-kern-table.hh

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see Win10 calibri.ttf has 160kb kern table. I suppose MS started ignoring the binary search header completely and use the subtable length instead. We should do the same. I'll fix HarfBuzz as well.

I keep confusing myself. Even subtable length is being ignored. The whole table length is used as subtable length. Argh!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about putting "apple" as an argument for decompile/compile, but then I needed that attribute inside fromXML and toXML as well to decide whether to read/write the tupleIndex...
I think I shall leave it like that until we add more formats.

if version != 0:
from fontTools.ttLib import TTLibError
raise TTLibError(
"unsupported kern subtable version: %d" % version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone is going to get really confused by this error some time. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt anyone will ever see that error. All the version=0 kern tables out there almost certainly have a subtable version=0, whatever that means. We'll see..

raise TTLibError(
"unsupported kern subtable version: %d" % version)
tupleIndex = None
# Should we also assert length == len(data)?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should always pass, right? If that's the case, sure. If it would fail on bogus fonts, no.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should, because the wrapper class used the same length to trim the data and pass it down. The assert would simply check that this assumption holds.

kernTable[(
ttFont.getGlyphName(left),
ttFont.getGlyphName(right))] = value
if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see Win10 calibri.ttf has 160kb kern table. I suppose MS started ignoring the binary search header completely and use the subtable length instead. We should do the same. I'll fix HarfBuzz as well.

@behdad
Copy link
Member

behdad commented Nov 4, 2017

Let's merge and move on. We'll fix more later. I've asked for clarification re calibri.ttf.

@anthrotype anthrotype merged commit 34f06f4 into fonttools:master Nov 4, 2017
@anthrotype anthrotype deleted the apple-kern branch November 4, 2017 16:41
behdad added a commit to harfbuzz/harfbuzz that referenced this pull request Nov 6, 2017
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

Successfully merging this pull request may close these issues.

None yet

3 participants