-
Notifications
You must be signed in to change notification settings - Fork 449
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
Conversation
- 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 = [] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🤷♂️
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Kinda relevant #314 |
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...
# 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 |
There was a problem hiding this comment.
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 😖
There was a problem hiding this comment.
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).
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)). |
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. |
The description of the kern subtable format 0 on Apple website says that:
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 Shall we enforce that requirement somehow? Not raise of course, but maybe log a warning? |
There was a problem hiding this 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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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)? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Let's merge and move on. We'll fix more later. I've asked for clarification re calibri.ttf. |
Apparently calibri.ttf does this: fonttools/fonttools#1094 (comment)
version
there, the length is a uint32 and there's an additional uint16 fortupleIndex
(used for GX)coverage
low byte to select subtable "format", instead of theversion
field, only present in OT kern subtable header.KernTable_format_0
now takes anapple=False
argument, used to choose the different headers and whether to read/writetupleIndex
.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