[AAT] Implement anchor point table

The AAT `ankr` anchor point table is an auxiliary table for `kerx`,
used to store anchor overrides in case the glyph itself does not
supply the needed anchors as control points. Among the fonts that
come pre-installed with MacOS 10.12.6, `ankr` is used by a handful
of non-Latin fonts such as “Myanmar MN”, “Devanagari Sangam MN”,
and “Arial HB”.

https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html
This commit is contained in:
Sascha Brawer 2017-08-31 11:10:06 +02:00
parent 90f257cc60
commit 92124ee5a6
6 changed files with 257 additions and 5 deletions

View File

@ -50,6 +50,7 @@ def _moduleFinderHint():
from . import V_D_M_X_
from . import V_O_R_G_
from . import V_V_A_R_
from . import _a_n_k_r
from . import _a_v_a_r
from . import _b_s_l_n
from . import _c_m_a_p

View File

@ -0,0 +1,13 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from .otBase import BaseTTXConverter
# The anchor point table provides a way to define anchor points.
# These are points within the coordinate space of a given glyph,
# independent of the control points used to render the glyph.
# Anchor points are used in conjunction with the 'kerx' table.
#
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html
class table__a_n_k_r(BaseTTXConverter):
pass

View File

@ -5,7 +5,7 @@ from fontTools.misc.fixedTools import (
versionToFixed as ve2fi)
from fontTools.misc.textTools import safeEval
from fontTools.ttLib import getSearchRange
from .otBase import ValueRecordFactory, CountReference
from .otBase import ValueRecordFactory, CountReference, OTTableWriter
from functools import partial
import struct
import logging
@ -806,6 +806,74 @@ class AATLookup(BaseConverter):
xmlWriter.newline()
# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup
# followed by an offset to a glyph data table. Other than usual, the
# offsets in the AATLookup are not relative to the beginning of
# the beginning of the 'ankr' table, but relative to the glyph data table.
# So, to find the anchor data for a glyph, one needs to add the offset
# to the data table to the offset found in the AATLookup, and then use
# the sum of these two offsets to find the actual data.
class AATLookupWithDataOffset(BaseConverter):
def read(self, reader, font, tableDict):
lookupOffset = reader.readULong()
dataOffset = reader.readULong()
lookupReader = reader.getSubReader(lookupOffset)
lookup = AATLookup('DataOffsets', None, None, UShort)
offsets = lookup.read(lookupReader, font, tableDict)
result = {}
for glyph, offset in offsets.items():
dataReader = reader.getSubReader(offset + dataOffset)
item = self.tableClass()
item.decompile(dataReader, font)
result[glyph] = item
return result
def write(self, writer, font, tableDict, value, repeatIndex=None):
# We do not work with OTTableWriter sub-writers because
# the offsets in our AATLookup are relative to our data
# table, for which we need to provide an offset value itself.
# It might have been possible to somehow make a kludge for
# performing this indirect offset computation directly inside
# OTTableWriter. But this would have made the internal logic
# of OTTableWriter even more complex than it already is,
# so we decided to roll our own offset computation for the
# contents of the AATLookup and associated data table.
offsetByGlyph, offsetByData, dataLen = {}, {}, 0
compiledData = []
for glyph in sorted(value, key=font.getGlyphID):
subWriter = OTTableWriter()
value[glyph].compile(subWriter, font)
data = subWriter.getAllData()
offset = offsetByData.get(data, None)
if offset == None:
offset = dataLen
dataLen = dataLen + len(data)
offsetByData[data] = offset
compiledData.append(data)
offsetByGlyph[glyph] = offset
# For calculating the offsets to our AATLookup and data table,
# we can use the regular OTTableWriter infrastructure.
lookupWriter = writer.getSubWriter()
lookupWriter.longOffset = True
lookup = AATLookup('DataOffsets', None, None, UShort)
lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)
dataWriter = writer.getSubWriter()
dataWriter.longOffset = True
writer.writeSubTable(lookupWriter)
writer.writeSubTable(dataWriter)
for d in compiledData:
dataWriter.writeData(d)
def xmlRead(self, attrs, content, font):
lookup = AATLookup('DataOffsets', None, None, self.tableClass)
return lookup.xmlRead(attrs, content, font)
def xmlWrite(self, xmlWriter, font, value, name, attrs):
lookup = AATLookup('DataOffsets', None, None, self.tableClass)
lookup.xmlWrite(xmlWriter, font, value, name, attrs)
class DeltaValue(BaseConverter):
def read(self, reader, font, tableDict):
@ -970,13 +1038,16 @@ converterMapping = {
"DeltaValue": DeltaValue,
"VarIdxMapValue": VarIdxMapValue,
"VarDataValue": VarDataValue,
# AAT
"MortChain": StructWithLength,
"MortSubtable": StructWithLength,
"MorxChain": StructWithLength,
"MorxSubtable": StructWithLength,
# "Template" types
"AATLookup": lambda C: partial(AATLookup, tableClass=C),
"AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C),
"OffsetTo": lambda C: partial(Table, tableClass=C),
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
}

View File

@ -1186,6 +1186,31 @@ otData = [
('uint16', 'value', None, None, 'A 16-bit offset from the start of the table to the data.'),
]),
#
# ankr
#
('ankr', [
('struct', 'AnchorPoints', None, None, 'Anchor points table.'),
]),
('AnchorPointsFormat0', [
('uint16', 'Format', None, None, 'Format of the anchor points table, = 0.'),
('uint16', 'Flags', None, None, 'Flags. Currenty unused, set to zero.'),
('AATLookupWithDataOffset(AnchorGlyphData)', 'Anchors', None, None, 'Table of with anchor overrides for each glyph.'),
]),
('AnchorGlyphData', [
('uint32', 'AnchorPointCount', None, None, 'Number of anchor points for this glyph.'),
('struct', 'AnchorPoint', 'AnchorPointCount', 0, 'Individual anchor points.'),
]),
('AnchorPoint', [
('int16', 'XCoordinate', None, None, 'X coordinate of this anchor point.'),
('int16', 'YCoordinate', None, None, 'Y coordinate of this anchor point.'),
]),
#
# bsln
#

View File

@ -100,10 +100,10 @@ The following tables are currently supported:
BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
GDEF, GMAP, GPKG, GPOS, GSUB, HVAR, JSTF, LTSH, MATH, META, MVAR,
OS/2, SING, STAT, SVG, TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSID,
TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, VORG, VVAR, avar, bsln, cmap,
cvar, cvt, feat, fpgm, fvar, gasp, glyf, gvar, hdmx, head, hhea,
hmtx, kern, lcar, loca, ltag, maxp, meta, mort, morx, name, opbd,
post, prep, prop, sbix, trak, vhea and vmtx
TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, VORG, VVAR, ankr, avar, bsln,
cmap, cvar, cvt, feat, fpgm, fvar, gasp, glyf, gvar, hdmx, head,
hhea, hmtx, kern, lcar, loca, ltag, maxp, meta, mort, morx, name,
opbd, post, prep, prop, sbix, trak, vhea and vmtx
.. end table list
Other tables are dumped as hexadecimal data.

View File

@ -0,0 +1,142 @@
# coding: utf-8
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.testTools import FakeFont, getXML, parseXML
from fontTools.misc.textTools import deHexStr, hexStr
from fontTools.ttLib import newTable
import unittest
# This is the anchor points table of the first font file in
# “/Library/Fonts/Devanagari Sangam MN.ttc” on macOS 10.12.6.
# For testing, weve changed the GlyphIDs to smaller values.
# Also, in the AATLookup, weve changed GlyphDataOffset value
# for the end-of-table marker from 0xFFFF to 0 since that is
# what our encoder emits. (The value for end-of-table markers
# does not actually matter).
ANKR_FORMAT_0_DATA = deHexStr(
'0000 0000 ' # 0: Format=0, Flags=0
'0000 000C ' # 4: LookupTableOffset=12
'0000 0024 ' # 8: GlyphDataTableOffset=36
'0006 0004 0002 ' # 12: LookupFormat=6, UnitSize=4, NUnits=2
'0008 0001 0000 ' # 18: SearchRange=8, EntrySelector=1, RangeShift=0
'0001 0000 ' # 24: Glyph=A, GlyphDataOffset=0 (+GlyphDataOffset=36)
'0003 0008 ' # 28: Glyph=C, GlyphDataOffset=8 (+GlyphDataOffset=44)
'FFFF 0000 ' # 32: Glyph=<end>, GlyphDataOffset=<n/a>
'0000 0001 ' # 36: GlyphData[A].NumPoints=1
'0235 045E ' # 40: GlyphData[A].Points[0].X=565, .Y=1118
'0000 0001 ' # 44: GlyphData[C].NumPoints=1
'FED2 045E ' # 48: GlyphData[C].Points[0].X=-302, .Y=1118
) # 52: <end>
assert len(ANKR_FORMAT_0_DATA) == 52
ANKR_FORMAT_0_XML = [
'<AnchorPoints Format="0">',
' <Flags value="0"/>',
' <Anchors>',
' <Lookup glyph="A">',
' <!-- AnchorPointCount=1 -->',
' <AnchorPoint index="0">',
' <XCoordinate value="565"/>',
' <YCoordinate value="1118"/>',
' </AnchorPoint>',
' </Lookup>',
' <Lookup glyph="C">',
' <!-- AnchorPointCount=1 -->',
' <AnchorPoint index="0">',
' <XCoordinate value="-302"/>',
' <YCoordinate value="1118"/>',
' </AnchorPoint>',
' </Lookup>',
' </Anchors>',
'</AnchorPoints>',
]
# Constructed test case where glyphs A and D share the same anchor data.
ANKR_FORMAT_0_SHARING_DATA = deHexStr(
'0000 0000 ' # 0: Format=0, Flags=0
'0000 000C ' # 4: LookupTableOffset=12
'0000 0028 ' # 8: GlyphDataTableOffset=40
'0006 0004 0003 ' # 12: LookupFormat=6, UnitSize=4, NUnits=3
'0008 0001 0004 ' # 18: SearchRange=8, EntrySelector=1, RangeShift=4
'0001 0000 ' # 24: Glyph=A, GlyphDataOffset=0 (+GlyphDataOffset=36)
'0003 0008 ' # 28: Glyph=C, GlyphDataOffset=8 (+GlyphDataOffset=44)
'0004 0000 ' # 32: Glyph=D, GlyphDataOffset=0 (+GlyphDataOffset=36)
'FFFF 0000 ' # 36: Glyph=<end>, GlyphDataOffset=<n/a>
'0000 0001 ' # 40: GlyphData[A].NumPoints=1
'0235 045E ' # 44: GlyphData[A].Points[0].X=565, .Y=1118
'0000 0002 ' # 48: GlyphData[C].NumPoints=2
'000B 000C ' # 52: GlyphData[C].Points[0].X=11, .Y=12
'001B 001C ' # 56: GlyphData[C].Points[1].X=27, .Y=28
) # 60: <end>
assert len(ANKR_FORMAT_0_SHARING_DATA) == 60
ANKR_FORMAT_0_SHARING_XML = [
'<AnchorPoints Format="0">',
' <Flags value="0"/>',
' <Anchors>',
' <Lookup glyph="A">',
' <!-- AnchorPointCount=1 -->',
' <AnchorPoint index="0">',
' <XCoordinate value="565"/>',
' <YCoordinate value="1118"/>',
' </AnchorPoint>',
' </Lookup>',
' <Lookup glyph="C">',
' <!-- AnchorPointCount=2 -->',
' <AnchorPoint index="0">',
' <XCoordinate value="11"/>',
' <YCoordinate value="12"/>',
' </AnchorPoint>',
' <AnchorPoint index="1">',
' <XCoordinate value="27"/>',
' <YCoordinate value="28"/>',
' </AnchorPoint>',
' </Lookup>',
' <Lookup glyph="D">',
' <!-- AnchorPointCount=1 -->',
' <AnchorPoint index="0">',
' <XCoordinate value="565"/>',
' <YCoordinate value="1118"/>',
' </AnchorPoint>',
' </Lookup>',
' </Anchors>',
'</AnchorPoints>',
]
class ANKRTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.maxDiff = None
cls.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D'])
def decompileToXML(self, data, xml):
table = newTable('ankr')
table.decompile(data, self.font)
self.assertEqual(getXML(table.toXML), xml)
def compileFromXML(self, xml, data):
table = newTable('ankr')
for name, attrs, content in parseXML(xml):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)), hexStr(data))
def roundtrip(self, data, xml):
self.decompileToXML(data, xml)
self.compileFromXML(xml, data)
def testFormat0(self):
self.roundtrip(ANKR_FORMAT_0_DATA, ANKR_FORMAT_0_XML)
def testFormat0_sharing(self):
self.roundtrip(ANKR_FORMAT_0_SHARING_DATA, ANKR_FORMAT_0_SHARING_XML)
if __name__ == '__main__':
import sys
sys.exit(unittest.main())