Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

279 lines
9.8 KiB
Python
Raw Normal View History

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
"""
2015-04-23 10:32:05 +02:00
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
2022-12-13 11:26:36 +00:00
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
2015-04-24 12:14:08 +02:00
class table__g_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar", "glyf"]
2022-12-13 11:26:36 +00:00
2017-01-10 17:55:30 +01:00
def __init__(self, tag=None):
DefaultTable.DefaultTable.__init__(self, tag)
self.version, self.reserved = 1, 0
self.variations = {}
2022-12-13 11:26:36 +00:00
def compile(self, ttFont):
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
sharedTuples = tv.compileSharedTuples(
axisTags, itertools.chain(*self.variations.values())
2022-12-13 11:26:36 +00:00
)
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)
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
)
compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
2022-12-13 11:26:36 +00:00
result = [compiledHeader, compiledOffsets]
result.extend(sharedTuples)
result.extend(compiledGlyphs)
2021-04-08 12:18:58 -06:00
return b"".join(result)
2022-12-13 11:26:36 +00:00
def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
2015-05-04 19:18:05 +02:00
result = []
glyf = ttFont["glyf"]
2015-05-04 19:18:05 +02:00
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
2017-01-10 17:55:30 +01:00
)
2022-12-13 11:26:36 +00:00
)
2015-05-04 19:18:05 +02:00
return result
2022-12-13 11:26:36 +00:00
def decompile(self, data, ttFont):
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
glyphs = ttFont.getGlyphOrder()
2015-04-23 10:32:05 +02:00
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"]
2022-12-13 11:26:36 +00:00
def decompileVarGlyph(glyphName, gid):
gvarData = data[
offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
2022-12-13 11:26:36 +00:00
]
if not gvarData:
return []
glyph = glyf[glyphName]
numPointsInGlyph = self.getNumPoints_(glyph)
return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
2022-12-13 11:26:36 +00:00
for gid in range(self.glyphCount):
glyphName = glyphs[gid]
variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
self.variations = _LazyDict(variations)
2022-12-13 11:26:36 +00:00
if ttFont.lazy is False: # Be lazy for None and True
self.ensureDecompiled()
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
2015-04-23 10:32:05 +02:00
@staticmethod
def decompileOffsets_(data, tableFormat, glyphCount):
if tableFormat == 0:
# Short format: array of UInt16
offsets = array.array("H")
2015-04-23 10:32:05 +02:00
offsetsSize = (glyphCount + 1) * 2
else:
# Long format: array of UInt32
offsets = array.array("I")
2015-04-23 10:32:05 +02:00
offsetsSize = (glyphCount + 1) * 4
offsets.frombytes(data[0:offsetsSize])
if sys.byteorder != "big":
offsets.byteswap()
2022-12-13 11:26:36 +00:00
# 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]
2022-12-13 11:26:36 +00:00
return offsets
2022-12-13 11:26:36 +00:00
2015-04-23 10:32:05 +02:00
@staticmethod
def compileOffsets_(offsets):
"""Packs a list of offsets into a 'gvar' offset table.
2022-12-13 11:26:36 +00:00
Returns a pair (bytestring, tableFormat). Bytestring is the
2015-04-23 10:32:05 +02:00
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
2015-04-24 08:28:52 +02:00
of the 'gvar' header.
2015-04-23 10:32:05 +02:00
"""
assert len(offsets) >= 2
for i in range(1, len(offsets)):
2015-04-23 10:32:05 +02:00
assert offsets[i - 1] <= offsets[i]
if max(offsets) <= 0xFFFF * 2:
2015-05-05 16:50:25 +02:00
packed = array.array("H", [n >> 1 for n in offsets])
tableFormat = 0
2015-04-23 10:32:05 +02:00
else:
2015-05-05 16:50:25 +02:00
packed = array.array("I", offsets)
tableFormat = 1
if sys.byteorder != "big":
packed.byteswap()
return (packed.tobytes(), tableFormat)
2022-12-13 11:26:36 +00:00
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]
2020-04-29 16:31:54 +02:00
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()
2022-12-13 11:26:36 +00:00
2015-04-27 17:10:59 +02:00
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)
2015-04-27 17:10:59 +02:00
glyphVariations = []
for element in content:
if isinstance(element, tuple):
name, attrs, content = element
if name == "tuple":
gvar = TupleVariation({}, [None] * numPointsInGlyph)
2015-04-27 17:10:59 +02:00
glyphVariations.append(gvar)
for tupleElement in content:
if isinstance(tupleElement, tuple):
tupleName, tupleAttrs, tupleContent = tupleElement
gvar.fromXML(tupleName, tupleAttrs, tupleContent)
self.variations[glyphName] = glyphVariations
2022-12-13 11:26:36 +00:00
2015-04-27 17:10:59 +02:00
@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
2017-01-10 17:55:30 +01:00
def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
variations, pointCount, axisTags, sharedCoordIndices
)
if tupleVariationCount == 0:
return b""
2021-04-08 12:31:29 -06:00
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)
2017-01-10 17:55:30 +01:00
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,
)