In favor of separate VARC table. ttGlyphSet does NOT yet know how to draw VARC table though. The 9 failing tests are all VarComposite-related and need to be updated with VARC equivalents eventually when we add VARC support to subsetter and instancer.
280 lines
9.8 KiB
Python
280 lines
9.8 KiB
Python
from collections import UserDict, deque
|
|
from functools import partial
|
|
from fontTools.misc import sstruct
|
|
from fontTools.misc.textTools import safeEval
|
|
from . import DefaultTable
|
|
import array
|
|
import itertools
|
|
import logging
|
|
import struct
|
|
import sys
|
|
import fontTools.ttLib.tables.TupleVariation as tv
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
TupleVariation = tv.TupleVariation
|
|
|
|
|
|
# https://www.microsoft.com/typography/otspec/gvar.htm
|
|
# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
|
|
#
|
|
# Apple's documentation of 'gvar':
|
|
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
|
|
#
|
|
# FreeType2 source code for parsing 'gvar':
|
|
# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
|
|
|
|
GVAR_HEADER_FORMAT = """
|
|
> # big endian
|
|
version: H
|
|
reserved: H
|
|
axisCount: H
|
|
sharedTupleCount: H
|
|
offsetToSharedTuples: I
|
|
glyphCount: H
|
|
flags: H
|
|
offsetToGlyphVariationData: I
|
|
"""
|
|
|
|
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
|
|
|
|
|
|
class _LazyDict(UserDict):
|
|
def __init__(self, data):
|
|
super().__init__()
|
|
self.data = data
|
|
|
|
def __getitem__(self, k):
|
|
v = self.data[k]
|
|
if callable(v):
|
|
v = v()
|
|
self.data[k] = v
|
|
return v
|
|
|
|
|
|
class table__g_v_a_r(DefaultTable.DefaultTable):
|
|
dependencies = ["fvar", "glyf"]
|
|
|
|
def __init__(self, tag=None):
|
|
DefaultTable.DefaultTable.__init__(self, tag)
|
|
self.version, self.reserved = 1, 0
|
|
self.variations = {}
|
|
|
|
def compile(self, ttFont):
|
|
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
|
|
sharedTuples = tv.compileSharedTuples(
|
|
axisTags, itertools.chain(*self.variations.values())
|
|
)
|
|
sharedTupleIndices = {coord: i for i, coord in enumerate(sharedTuples)}
|
|
sharedTupleSize = sum([len(c) for c in sharedTuples])
|
|
compiledGlyphs = self.compileGlyphs_(ttFont, axisTags, sharedTupleIndices)
|
|
offset = 0
|
|
offsets = []
|
|
for glyph in compiledGlyphs:
|
|
offsets.append(offset)
|
|
offset += len(glyph)
|
|
offsets.append(offset)
|
|
compiledOffsets, tableFormat = self.compileOffsets_(offsets)
|
|
|
|
header = {}
|
|
header["version"] = self.version
|
|
header["reserved"] = self.reserved
|
|
header["axisCount"] = len(axisTags)
|
|
header["sharedTupleCount"] = len(sharedTuples)
|
|
header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets)
|
|
header["glyphCount"] = len(compiledGlyphs)
|
|
header["flags"] = tableFormat
|
|
header["offsetToGlyphVariationData"] = (
|
|
header["offsetToSharedTuples"] + sharedTupleSize
|
|
)
|
|
compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
|
|
|
|
result = [compiledHeader, compiledOffsets]
|
|
result.extend(sharedTuples)
|
|
result.extend(compiledGlyphs)
|
|
return b"".join(result)
|
|
|
|
def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
|
|
result = []
|
|
glyf = ttFont["glyf"]
|
|
for glyphName in ttFont.getGlyphOrder():
|
|
variations = self.variations.get(glyphName, [])
|
|
if not variations:
|
|
result.append(b"")
|
|
continue
|
|
pointCountUnused = 0 # pointCount is actually unused by compileGlyph
|
|
result.append(
|
|
compileGlyph_(
|
|
variations, pointCountUnused, axisTags, sharedCoordIndices
|
|
)
|
|
)
|
|
return result
|
|
|
|
def decompile(self, data, ttFont):
|
|
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
|
|
glyphs = ttFont.getGlyphOrder()
|
|
sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
|
|
assert len(glyphs) == self.glyphCount
|
|
assert len(axisTags) == self.axisCount
|
|
offsets = self.decompileOffsets_(
|
|
data[GVAR_HEADER_SIZE:],
|
|
tableFormat=(self.flags & 1),
|
|
glyphCount=self.glyphCount,
|
|
)
|
|
sharedCoords = tv.decompileSharedTuples(
|
|
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
|
|
)
|
|
variations = {}
|
|
offsetToData = self.offsetToGlyphVariationData
|
|
glyf = ttFont["glyf"]
|
|
|
|
def decompileVarGlyph(glyphName, gid):
|
|
gvarData = data[
|
|
offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
|
|
]
|
|
if not gvarData:
|
|
return []
|
|
glyph = glyf[glyphName]
|
|
numPointsInGlyph = self.getNumPoints_(glyph)
|
|
return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
|
|
|
|
for gid in range(self.glyphCount):
|
|
glyphName = glyphs[gid]
|
|
variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
|
|
self.variations = _LazyDict(variations)
|
|
|
|
if ttFont.lazy is False: # Be lazy for None and True
|
|
self.ensureDecompiled()
|
|
|
|
def ensureDecompiled(self, recurse=False):
|
|
# The recurse argument is unused, but part of the signature of
|
|
# ensureDecompiled across the library.
|
|
# Use a zero-length deque to consume the lazy dict
|
|
deque(self.variations.values(), maxlen=0)
|
|
|
|
@staticmethod
|
|
def decompileOffsets_(data, tableFormat, glyphCount):
|
|
if tableFormat == 0:
|
|
# Short format: array of UInt16
|
|
offsets = array.array("H")
|
|
offsetsSize = (glyphCount + 1) * 2
|
|
else:
|
|
# Long format: array of UInt32
|
|
offsets = array.array("I")
|
|
offsetsSize = (glyphCount + 1) * 4
|
|
offsets.frombytes(data[0:offsetsSize])
|
|
if sys.byteorder != "big":
|
|
offsets.byteswap()
|
|
|
|
# In the short format, offsets need to be multiplied by 2.
|
|
# This is not documented in Apple's TrueType specification,
|
|
# but can be inferred from the FreeType implementation, and
|
|
# we could verify it with two sample GX fonts.
|
|
if tableFormat == 0:
|
|
offsets = [off * 2 for off in offsets]
|
|
|
|
return offsets
|
|
|
|
@staticmethod
|
|
def compileOffsets_(offsets):
|
|
"""Packs a list of offsets into a 'gvar' offset table.
|
|
|
|
Returns a pair (bytestring, tableFormat). Bytestring is the
|
|
packed offset table. Format indicates whether the table
|
|
uses short (tableFormat=0) or long (tableFormat=1) integers.
|
|
The returned tableFormat should get packed into the flags field
|
|
of the 'gvar' header.
|
|
"""
|
|
assert len(offsets) >= 2
|
|
for i in range(1, len(offsets)):
|
|
assert offsets[i - 1] <= offsets[i]
|
|
if max(offsets) <= 0xFFFF * 2:
|
|
packed = array.array("H", [n >> 1 for n in offsets])
|
|
tableFormat = 0
|
|
else:
|
|
packed = array.array("I", offsets)
|
|
tableFormat = 1
|
|
if sys.byteorder != "big":
|
|
packed.byteswap()
|
|
return (packed.tobytes(), tableFormat)
|
|
|
|
def toXML(self, writer, ttFont):
|
|
writer.simpletag("version", value=self.version)
|
|
writer.newline()
|
|
writer.simpletag("reserved", value=self.reserved)
|
|
writer.newline()
|
|
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
|
|
for glyphName in ttFont.getGlyphNames():
|
|
variations = self.variations.get(glyphName)
|
|
if not variations:
|
|
continue
|
|
writer.begintag("glyphVariations", glyph=glyphName)
|
|
writer.newline()
|
|
for gvar in variations:
|
|
gvar.toXML(writer, axisTags)
|
|
writer.endtag("glyphVariations")
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
if name == "version":
|
|
self.version = safeEval(attrs["value"])
|
|
elif name == "reserved":
|
|
self.reserved = safeEval(attrs["value"])
|
|
elif name == "glyphVariations":
|
|
if not hasattr(self, "variations"):
|
|
self.variations = {}
|
|
glyphName = attrs["glyph"]
|
|
glyph = ttFont["glyf"][glyphName]
|
|
numPointsInGlyph = self.getNumPoints_(glyph)
|
|
glyphVariations = []
|
|
for element in content:
|
|
if isinstance(element, tuple):
|
|
name, attrs, content = element
|
|
if name == "tuple":
|
|
gvar = TupleVariation({}, [None] * numPointsInGlyph)
|
|
glyphVariations.append(gvar)
|
|
for tupleElement in content:
|
|
if isinstance(tupleElement, tuple):
|
|
tupleName, tupleAttrs, tupleContent = tupleElement
|
|
gvar.fromXML(tupleName, tupleAttrs, tupleContent)
|
|
self.variations[glyphName] = glyphVariations
|
|
|
|
@staticmethod
|
|
def getNumPoints_(glyph):
|
|
NUM_PHANTOM_POINTS = 4
|
|
|
|
if glyph.isComposite():
|
|
return len(glyph.components) + NUM_PHANTOM_POINTS
|
|
else:
|
|
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
|
|
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
|
|
|
|
|
|
def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
|
|
tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
|
|
variations, pointCount, axisTags, sharedCoordIndices
|
|
)
|
|
if tupleVariationCount == 0:
|
|
return b""
|
|
result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data]
|
|
if (len(tuples) + len(data)) % 2 != 0:
|
|
result.append(b"\0") # padding
|
|
return b"".join(result)
|
|
|
|
|
|
def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
|
|
if len(data) < 4:
|
|
return []
|
|
tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4])
|
|
dataPos = offsetToData
|
|
return tv.decompileTupleVariationStore(
|
|
"gvar",
|
|
axisTags,
|
|
tupleVariationCount,
|
|
pointCount,
|
|
sharedTuples,
|
|
data,
|
|
4,
|
|
offsetToData,
|
|
)
|