[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:
parent
90f257cc60
commit
92124ee5a6
@ -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
|
||||
|
13
Lib/fontTools/ttLib/tables/_a_n_k_r.py
Normal file
13
Lib/fontTools/ttLib/tables/_a_n_k_r.py
Normal 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
|
@ -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),
|
||||
}
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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.
|
||||
|
142
Tests/ttLib/tables/_a_n_k_r_test.py
Normal file
142
Tests/ttLib/tables/_a_n_k_r_test.py
Normal 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, we’ve changed the GlyphIDs to smaller values.
|
||||
# Also, in the AATLookup, we’ve 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())
|
Loading…
x
Reference in New Issue
Block a user