Merge pull request #3395 from fonttools/varc-table

[VARC] Variable Composites table
This commit is contained in:
Behdad Esfahbod 2024-05-24 08:09:51 -07:00 committed by GitHub
commit db60a248dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 2810 additions and 2734 deletions

View File

@ -656,11 +656,7 @@ class FontBuilder(object):
if validateGlyphFormat and self.font["head"].glyphDataFormat == 0: if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
for name, g in glyphs.items(): for name, g in glyphs.items():
if g.isVarComposite(): if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
raise ValueError(
f"Glyph {name!r} is a variable composite, but glyphDataFormat=0"
)
elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
raise ValueError( raise ValueError(
f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; " f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
"either convert to quadratics with cu2qu or set glyphDataFormat=1." "either convert to quadratics with cu2qu or set glyphDataFormat=1."

View File

@ -225,7 +225,7 @@ def merge(self, m, tables):
g.removeHinting() g.removeHinting()
# Expand composite glyphs to load their # Expand composite glyphs to load their
# composite glyph names. # composite glyph names.
if g.isComposite() or g.isVarComposite(): if g.isComposite():
g.expand(table) g.expand(table)
return DefaultTable.merge(self, m, tables) return DefaultTable.merge(self, m, tables)

View File

@ -0,0 +1,12 @@
from itertools import *
# Python 3.12:
if "batched" not in globals():
# https://docs.python.org/3/library/itertools.html#itertools.batched
def batched(iterable, n):
# batched('ABCDEFG', 3) --> ABC DEF G
if n < 1:
raise ValueError("n must be at least one")
it = iter(iterable)
while batch := tuple(islice(it, n)):
yield batch

View File

@ -0,0 +1,42 @@
from collections import UserDict, UserList
__all__ = ["LazyDict", "LazyList"]
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(k)
self.data[k] = v
return v
class LazyList(UserList):
def __getitem__(self, k):
if isinstance(k, slice):
indices = range(*k.indices(len(self)))
return [self[i] for i in indices]
v = self.data[k]
if callable(v):
v = v(k)
self.data[k] = v
return v
def __add__(self, other):
if isinstance(other, LazyList):
other = list(other)
elif isinstance(other, list):
pass
else:
return NotImplemented
return list(self) + other
def __radd__(self, other):
if not isinstance(other, list):
return NotImplemented
return other + list(self)

View File

@ -422,6 +422,19 @@ class DecomposedTransform:
tCenterX: float = 0 tCenterX: float = 0
tCenterY: float = 0 tCenterY: float = 0
def __bool__(self):
return (
self.translateX != 0
or self.translateY != 0
or self.rotation != 0
or self.scaleX != 1
or self.scaleY != 1
or self.skewX != 0
or self.skewY != 0
or self.tCenterX != 0
or self.tCenterY != 0
)
@classmethod @classmethod
def fromTransform(self, transform): def fromTransform(self, transform):
# Adapted from an answer on # Adapted from an answer on

View File

@ -2,7 +2,7 @@ from typing import Callable
from fontTools.pens.basePen import BasePen from fontTools.pens.basePen import BasePen
def pointToString(pt, ntos=str): def pointToString(pt, ntos):
return " ".join(ntos(i) for i in pt) return " ".join(ntos(i) for i in pt)
@ -37,7 +37,13 @@ class SVGPathPen(BasePen):
print(tpen.getCommands()) print(tpen.getCommands())
""" """
def __init__(self, glyphSet, ntos: Callable[[float], str] = str): def __init__(
self,
glyphSet,
ntos: Callable[[float], str] = (
lambda x: ("%.2f" % x) if x != int(x) else str(int(x))
),
):
BasePen.__init__(self, glyphSet) BasePen.__init__(self, glyphSet)
self._commands = [] self._commands = []
self._lastCommand = None self._lastCommand = None

View File

@ -14,7 +14,7 @@ from fontTools.misc.cliTools import makeOutputFileName
from fontTools.subset.util import _add_method, _uniq_sort from fontTools.subset.util import _add_method, _uniq_sort
from fontTools.subset.cff import * from fontTools.subset.cff import *
from fontTools.subset.svg import * from fontTools.subset.svg import *
from fontTools.varLib import varStore # for subset_varidxes from fontTools.varLib import varStore, multiVarStore # For monkey-patching
from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor
import sys import sys
import struct import struct
@ -2630,6 +2630,88 @@ def closure_glyphs(self, s):
s.glyphs.update(variants) s.glyphs.update(variants)
@_add_method(ttLib.getTableClass("VARC"))
def subset_glyphs(self, s):
indices = self.table.Coverage.subset(s.glyphs)
self.table.VarCompositeGlyphs.VarCompositeGlyph = _list_subset(
self.table.VarCompositeGlyphs.VarCompositeGlyph, indices
)
return bool(self.table.VarCompositeGlyphs.VarCompositeGlyph)
@_add_method(ttLib.getTableClass("VARC"))
def closure_glyphs(self, s):
if self.table.VarCompositeGlyphs is None:
return
glyphMap = {glyphName: i for i, glyphName in enumerate(self.table.Coverage.glyphs)}
glyphRecords = self.table.VarCompositeGlyphs.VarCompositeGlyph
glyphs = s.glyphs
covered = set()
new = set(glyphs)
while new:
oldNew = new
new = set()
for glyphName in oldNew:
if glyphName in covered:
continue
idx = glyphMap.get(glyphName)
if idx is None:
continue
glyph = glyphRecords[idx]
for comp in glyph.components:
name = comp.glyphName
glyphs.add(name)
if name not in covered:
new.add(name)
@_add_method(ttLib.getTableClass("VARC"))
def prune_post_subset(self, font, options):
table = self.table
store = table.MultiVarStore
if store is not None:
usedVarIdxes = set()
table.collect_varidxes(usedVarIdxes)
varidx_map = store.subset_varidxes(usedVarIdxes)
table.remap_varidxes(varidx_map)
axisIndicesList = table.AxisIndicesList.Item
if axisIndicesList is not None:
usedIndices = set()
for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
for comp in glyph.components:
if comp.axisIndicesIndex is not None:
usedIndices.add(comp.axisIndicesIndex)
usedIndices = sorted(usedIndices)
table.AxisIndicesList.Item = _list_subset(axisIndicesList, usedIndices)
mapping = {old: new for new, old in enumerate(usedIndices)}
for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
for comp in glyph.components:
if comp.axisIndicesIndex is not None:
comp.axisIndicesIndex = mapping[comp.axisIndicesIndex]
conditionList = table.ConditionList
if conditionList is not None:
conditionTables = conditionList.ConditionTable
usedIndices = set()
for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
for comp in glyph.components:
if comp.conditionIndex is not None:
usedIndices.add(comp.conditionIndex)
usedIndices = sorted(usedIndices)
conditionList.ConditionTable = _list_subset(conditionTables, usedIndices)
mapping = {old: new for new, old in enumerate(usedIndices)}
for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
for comp in glyph.components:
if comp.conditionIndex is not None:
comp.conditionIndex = mapping[comp.conditionIndex]
return True
@_add_method(ttLib.getTableClass("MATH")) @_add_method(ttLib.getTableClass("MATH"))
def closure_glyphs(self, s): def closure_glyphs(self, s):
if self.table.MathVariants: if self.table.MathVariants:
@ -3298,20 +3380,6 @@ class Subsetter(object):
self.glyphs.add(font.getGlyphName(i)) self.glyphs.add(font.getGlyphName(i))
log.info("Added first four glyphs to subset") log.info("Added first four glyphs to subset")
if self.options.layout_closure and "GSUB" in font:
with timer("close glyph list over 'GSUB'"):
log.info(
"Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
font["GSUB"].closure_glyphs(self)
self.glyphs.intersection_update(realGlyphs)
log.info(
"Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
self.glyphs_gsubed = frozenset(self.glyphs)
if "MATH" in font: if "MATH" in font:
with timer("close glyph list over 'MATH'"): with timer("close glyph list over 'MATH'"):
log.info( log.info(
@ -3326,6 +3394,20 @@ class Subsetter(object):
log.glyphs(self.glyphs, font=font) log.glyphs(self.glyphs, font=font)
self.glyphs_mathed = frozenset(self.glyphs) self.glyphs_mathed = frozenset(self.glyphs)
if self.options.layout_closure and "GSUB" in font:
with timer("close glyph list over 'GSUB'"):
log.info(
"Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
font["GSUB"].closure_glyphs(self)
self.glyphs.intersection_update(realGlyphs)
log.info(
"Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
self.glyphs_gsubed = frozenset(self.glyphs)
for table in ("COLR", "bsln"): for table in ("COLR", "bsln"):
if table in font: if table in font:
with timer("close glyph list over '%s'" % table): with timer("close glyph list over '%s'" % table):
@ -3345,6 +3427,20 @@ class Subsetter(object):
log.glyphs(self.glyphs, font=font) log.glyphs(self.glyphs, font=font)
setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs)) setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs))
if "VARC" in font:
with timer("close glyph list over 'VARC'"):
log.info(
"Closing glyph list over 'VARC': %d glyphs before", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
font["VARC"].closure_glyphs(self)
self.glyphs.intersection_update(realGlyphs)
log.info(
"Closed glyph list over 'VARC': %d glyphs after", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
self.glyphs_glyfed = frozenset(self.glyphs)
if "glyf" in font: if "glyf" in font:
with timer("close glyph list over 'glyf'"): with timer("close glyph list over 'glyf'"):
log.info( log.info(

View File

@ -10,8 +10,10 @@ import fontTools.ttLib.tables.otTables as otTables
from fontTools.cffLib import VarStoreData from fontTools.cffLib import VarStoreData
import fontTools.cffLib.specializer as cffSpecializer import fontTools.cffLib.specializer as cffSpecializer
from fontTools.varLib import builder # for VarData.calculateNumShorts from fontTools.varLib import builder # for VarData.calculateNumShorts
from fontTools.varLib.multiVarStore import OnlineMultiVarStoreBuilder
from fontTools.misc.vector import Vector
from fontTools.misc.fixedTools import otRound from fontTools.misc.fixedTools import otRound
from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags from fontTools.misc.iterTools import batched
__all__ = ["scale_upem", "ScalerVisitor"] __all__ = ["scale_upem", "ScalerVisitor"]
@ -123,13 +125,6 @@ def visit(visitor, obj, attr, glyphs):
component.y = visitor.scale(component.y) component.y = visitor.scale(component.y)
continue continue
if g.isVarComposite():
for component in g.components:
for attr in ("translateX", "translateY", "tCenterX", "tCenterY"):
v = getattr(component.transform, attr)
setattr(component.transform, attr, visitor.scale(v))
continue
if hasattr(g, "coordinates"): if hasattr(g, "coordinates"):
coordinates = g.coordinates coordinates = g.coordinates
for i, (x, y) in enumerate(coordinates): for i, (x, y) in enumerate(coordinates):
@ -138,59 +133,107 @@ def visit(visitor, obj, attr, glyphs):
@ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations") @ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations")
def visit(visitor, obj, attr, variations): def visit(visitor, obj, attr, variations):
# VarComposites are a pain to handle :-(
glyfTable = visitor.font["glyf"] glyfTable = visitor.font["glyf"]
for glyphName, varlist in variations.items(): for glyphName, varlist in variations.items():
glyph = glyfTable[glyphName] glyph = glyfTable[glyphName]
isVarComposite = glyph.isVarComposite()
for var in varlist: for var in varlist:
coordinates = var.coordinates coordinates = var.coordinates
for i, xy in enumerate(coordinates):
if not isVarComposite: if xy is None:
for i, xy in enumerate(coordinates): continue
if xy is None:
continue
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
continue
# VarComposite glyph
i = 0
for component in glyph.components:
if component.flags & VarComponentFlags.AXES_HAVE_VARIATION:
i += len(component.location)
if component.flags & (
VarComponentFlags.HAVE_TRANSLATE_X
| VarComponentFlags.HAVE_TRANSLATE_Y
):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
i += 1
if component.flags & VarComponentFlags.HAVE_ROTATION:
i += 1
if component.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
i += 1
if component.flags & (
VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y
):
i += 1
if component.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
i += 1
# Phantom points
assert i + 4 == len(coordinates)
for i in range(i, len(coordinates)):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
@ScalerVisitor.register_attr(ttLib.getTableClass("VARC"), "table")
def visit(visitor, obj, attr, varc):
# VarComposite variations are a pain
fvar = visitor.font["fvar"]
fvarAxes = [a.axisTag for a in fvar.axes]
store = varc.MultiVarStore
storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes)
for g in varc.VarCompositeGlyphs.VarCompositeGlyph:
for component in g.components:
t = component.transform
t.translateX = visitor.scale(t.translateX)
t.translateY = visitor.scale(t.translateY)
t.tCenterX = visitor.scale(t.tCenterX)
t.tCenterY = visitor.scale(t.tCenterY)
if component.axisValuesVarIndex != otTables.NO_VARIATION_INDEX:
varIdx = component.axisValuesVarIndex
# TODO Move this code duplicated below to MultiVarStore.__getitem__,
# or a getDeltasAndSupports().
if varIdx != otTables.NO_VARIATION_INDEX:
major = varIdx >> 16
minor = varIdx & 0xFFFF
varData = store.MultiVarData[major]
vec = varData.Item[minor]
storeBuilder.setSupports(store.get_supports(major, fvar.axes))
if vec:
m = len(vec) // varData.VarRegionCount
vec = list(batched(vec, m))
vec = [Vector(v) for v in vec]
component.axisValuesVarIndex = storeBuilder.storeDeltas(vec)
else:
component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX
if component.transformVarIndex != otTables.NO_VARIATION_INDEX:
varIdx = component.transformVarIndex
if varIdx != otTables.NO_VARIATION_INDEX:
major = varIdx >> 16
minor = varIdx & 0xFFFF
vec = varData.Item[varIdx & 0xFFFF]
major = varIdx >> 16
minor = varIdx & 0xFFFF
varData = store.MultiVarData[major]
vec = varData.Item[minor]
storeBuilder.setSupports(store.get_supports(major, fvar.axes))
if vec:
m = len(vec) // varData.VarRegionCount
flags = component.flags
vec = list(batched(vec, m))
newVec = []
for v in vec:
v = list(v)
i = 0
## Scale translate & tCenter
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X:
v[i] = visitor.scale(v[i])
i += 1
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y:
v[i] = visitor.scale(v[i])
i += 1
if flags & otTables.VarComponentFlags.HAVE_ROTATION:
i += 1
if flags & otTables.VarComponentFlags.HAVE_SCALE_X:
i += 1
if flags & otTables.VarComponentFlags.HAVE_SCALE_Y:
i += 1
if flags & otTables.VarComponentFlags.HAVE_SKEW_X:
i += 1
if flags & otTables.VarComponentFlags.HAVE_SKEW_Y:
i += 1
if flags & otTables.VarComponentFlags.HAVE_TCENTER_X:
v[i] = visitor.scale(v[i])
i += 1
if flags & otTables.VarComponentFlags.HAVE_TCENTER_Y:
v[i] = visitor.scale(v[i])
i += 1
newVec.append(Vector(v))
vec = newVec
component.transformVarIndex = storeBuilder.storeDeltas(vec)
else:
component.transformVarIndex = otTables.NO_VARIATION_INDEX
varc.MultiVarStore = storeBuilder.finish()
@ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables") @ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables")
def visit(visitor, obj, attr, kernTables): def visit(visitor, obj, attr, kernTables):
for table in kernTables: for table in kernTables:

View File

@ -22,6 +22,8 @@ PRIVATE_POINT_NUMBERS = 0x2000
DELTAS_ARE_ZERO = 0x80 DELTAS_ARE_ZERO = 0x80
DELTAS_ARE_WORDS = 0x40 DELTAS_ARE_WORDS = 0x40
DELTAS_ARE_LONGS = 0xC0
DELTAS_SIZE_MASK = 0xC0
DELTA_RUN_COUNT_MASK = 0x3F DELTA_RUN_COUNT_MASK = 0x3F
POINTS_ARE_WORDS = 0x80 POINTS_ARE_WORDS = 0x80
@ -366,8 +368,10 @@ class TupleVariation(object):
pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
elif -128 <= value <= 127: elif -128 <= value <= 127:
pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr)
else: elif -32768 <= value <= 32767:
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
else:
pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr)
return bytearr return bytearr
@staticmethod @staticmethod
@ -420,6 +424,7 @@ class TupleVariation(object):
numDeltas = len(deltas) numDeltas = len(deltas)
while pos < numDeltas: while pos < numDeltas:
value = deltas[pos] value = deltas[pos]
# Within a word-encoded run of deltas, it is easiest # Within a word-encoded run of deltas, it is easiest
# to start a new run (with a different encoding) # to start a new run (with a different encoding)
# whenever we encounter a zero value. For example, # whenever we encounter a zero value. For example,
@ -442,6 +447,10 @@ class TupleVariation(object):
and (-128 <= deltas[pos + 1] <= 127) and (-128 <= deltas[pos + 1] <= 127)
): ):
break break
if not (-32768 <= value <= 32767):
break
pos += 1 pos += 1
runLength = pos - offset runLength = pos - offset
while runLength >= 64: while runLength >= 64:
@ -461,18 +470,47 @@ class TupleVariation(object):
return pos return pos
@staticmethod @staticmethod
def decompileDeltas_(numDeltas, data, offset): def encodeDeltaRunAsLongs_(deltas, offset, bytearr):
pos = offset
numDeltas = len(deltas)
while pos < numDeltas:
value = deltas[pos]
if -32768 <= value <= 32767:
break
pos += 1
runLength = pos - offset
while runLength >= 64:
bytearr.append(DELTAS_ARE_LONGS | 63)
a = array.array("i", deltas[offset : offset + 64])
if sys.byteorder != "big":
a.byteswap()
bytearr.extend(a)
offset += 64
runLength -= 64
if runLength:
bytearr.append(DELTAS_ARE_LONGS | (runLength - 1))
a = array.array("i", deltas[offset:pos])
if sys.byteorder != "big":
a.byteswap()
bytearr.extend(a)
return pos
@staticmethod
def decompileDeltas_(numDeltas, data, offset=0):
"""(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)""" """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
result = [] result = []
pos = offset pos = offset
while len(result) < numDeltas: while len(result) < numDeltas if numDeltas is not None else pos < len(data):
runHeader = data[pos] runHeader = data[pos]
pos += 1 pos += 1
numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1 numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1
if (runHeader & DELTAS_ARE_ZERO) != 0: if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_ZERO:
result.extend([0] * numDeltasInRun) result.extend([0] * numDeltasInRun)
else: else:
if (runHeader & DELTAS_ARE_WORDS) != 0: if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_LONGS:
deltas = array.array("i")
deltasSize = numDeltasInRun * 4
elif (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS:
deltas = array.array("h") deltas = array.array("h")
deltasSize = numDeltasInRun * 2 deltasSize = numDeltasInRun * 2
else: else:
@ -481,10 +519,10 @@ class TupleVariation(object):
deltas.frombytes(data[pos : pos + deltasSize]) deltas.frombytes(data[pos : pos + deltasSize])
if sys.byteorder != "big": if sys.byteorder != "big":
deltas.byteswap() deltas.byteswap()
assert len(deltas) == numDeltasInRun assert len(deltas) == numDeltasInRun, (len(deltas), numDeltasInRun)
pos += deltasSize pos += deltasSize
result.extend(deltas) result.extend(deltas)
assert len(result) == numDeltas assert numDeltas is None or len(result) == numDeltas
return (result, pos) return (result, pos)
@staticmethod @staticmethod

View File

@ -0,0 +1,5 @@
from .otBase import BaseTTXConverter
class table_V_A_R_C_(BaseTTXConverter):
pass

View File

@ -424,29 +424,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
for c in glyph.components for c in glyph.components
], ],
) )
elif glyph.isVarComposite():
coords = []
controls = []
for component in glyph.components:
(
componentCoords,
componentControls,
) = component.getCoordinatesAndControls()
coords.extend(componentCoords)
controls.extend(componentControls)
coords = GlyphCoordinates(coords)
controls = _GlyphControls(
numberOfContours=glyph.numberOfContours,
endPts=list(range(len(coords))),
flags=None,
components=[
(c.glyphName, getattr(c, "flags", None)) for c in glyph.components
],
)
else: else:
coords, endPts, flags = glyph.getCoordinates(self) coords, endPts, flags = glyph.getCoordinates(self)
coords = coords.copy() coords = coords.copy()
@ -492,10 +469,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
for p, comp in zip(coord, glyph.components): for p, comp in zip(coord, glyph.components):
if hasattr(comp, "x"): if hasattr(comp, "x"):
comp.x, comp.y = p comp.x, comp.y = p
elif glyph.isVarComposite():
for comp in glyph.components:
coord = comp.setCoordinates(coord)
assert not coord
elif glyph.numberOfContours == 0: elif glyph.numberOfContours == 0:
assert len(coord) == 0 assert len(coord) == 0
else: else:
@ -737,8 +710,6 @@ class Glyph(object):
return return
if self.isComposite(): if self.isComposite():
self.decompileComponents(data, glyfTable) self.decompileComponents(data, glyfTable)
elif self.isVarComposite():
self.decompileVarComponents(data, glyfTable)
else: else:
self.decompileCoordinates(data) self.decompileCoordinates(data)
@ -758,8 +729,6 @@ class Glyph(object):
data = sstruct.pack(glyphHeaderFormat, self) data = sstruct.pack(glyphHeaderFormat, self)
if self.isComposite(): if self.isComposite():
data = data + self.compileComponents(glyfTable) data = data + self.compileComponents(glyfTable)
elif self.isVarComposite():
data = data + self.compileVarComponents(glyfTable)
else: else:
data = data + self.compileCoordinates() data = data + self.compileCoordinates()
return data return data
@ -769,10 +738,6 @@ class Glyph(object):
for compo in self.components: for compo in self.components:
compo.toXML(writer, ttFont) compo.toXML(writer, ttFont)
haveInstructions = hasattr(self, "program") haveInstructions = hasattr(self, "program")
elif self.isVarComposite():
for compo in self.components:
compo.toXML(writer, ttFont)
haveInstructions = False
else: else:
last = 0 last = 0
for i in range(self.numberOfContours): for i in range(self.numberOfContours):
@ -842,15 +807,6 @@ class Glyph(object):
component = GlyphComponent() component = GlyphComponent()
self.components.append(component) self.components.append(component)
component.fromXML(name, attrs, content, ttFont) component.fromXML(name, attrs, content, ttFont)
elif name == "varComponent":
if self.numberOfContours > 0:
raise ttLib.TTLibError("can't mix composites and contours in glyph")
self.numberOfContours = -2
if not hasattr(self, "components"):
self.components = []
component = GlyphVarComponent()
self.components.append(component)
component.fromXML(name, attrs, content, ttFont)
elif name == "instructions": elif name == "instructions":
self.program = ttProgram.Program() self.program = ttProgram.Program()
for element in content: for element in content:
@ -860,7 +816,7 @@ class Glyph(object):
self.program.fromXML(name, attrs, content, ttFont) self.program.fromXML(name, attrs, content, ttFont)
def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
assert self.isComposite() or self.isVarComposite() assert self.isComposite()
nContours = 0 nContours = 0
nPoints = 0 nPoints = 0
initialMaxComponentDepth = maxComponentDepth initialMaxComponentDepth = maxComponentDepth
@ -904,13 +860,6 @@ class Glyph(object):
len(data), len(data),
) )
def decompileVarComponents(self, data, glyfTable):
self.components = []
while len(data) >= GlyphVarComponent.MIN_SIZE:
component = GlyphVarComponent()
data = component.decompile(data, glyfTable)
self.components.append(component)
def decompileCoordinates(self, data): def decompileCoordinates(self, data):
endPtsOfContours = array.array("H") endPtsOfContours = array.array("H")
endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
@ -1027,9 +976,6 @@ class Glyph(object):
data = data + struct.pack(">h", len(instructions)) + instructions data = data + struct.pack(">h", len(instructions)) + instructions
return data return data
def compileVarComponents(self, glyfTable):
return b"".join(c.compile(glyfTable) for c in self.components)
def compileCoordinates(self): def compileCoordinates(self):
assert len(self.coordinates) == len(self.flags) assert len(self.coordinates) == len(self.flags)
data = [] data = []
@ -1231,13 +1177,6 @@ class Glyph(object):
else: else:
return self.numberOfContours == -1 return self.numberOfContours == -1
def isVarComposite(self):
"""Test whether a glyph has variable components"""
if hasattr(self, "data"):
return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False
else:
return self.numberOfContours == -2
def getCoordinates(self, glyfTable): def getCoordinates(self, glyfTable):
"""Return the coordinates, end points and flags """Return the coordinates, end points and flags
@ -1308,8 +1247,6 @@ class Glyph(object):
allCoords.extend(coordinates) allCoords.extend(coordinates)
allFlags.extend(flags) allFlags.extend(flags)
return allCoords, allEndPts, allFlags return allCoords, allEndPts, allFlags
elif self.isVarComposite():
raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs")
else: else:
return GlyphCoordinates(), [], bytearray() return GlyphCoordinates(), [], bytearray()
@ -1319,12 +1256,8 @@ class Glyph(object):
This method can be used on simple glyphs (in which case it returns an This method can be used on simple glyphs (in which case it returns an
empty list) or composite glyphs. empty list) or composite glyphs.
""" """
if hasattr(self, "data") and self.isVarComposite():
# TODO(VarComposite) Add implementation without expanding glyph
self.expand(glyfTable)
if not hasattr(self, "data"): if not hasattr(self, "data"):
if self.isComposite() or self.isVarComposite(): if self.isComposite():
return [c.glyphName for c in self.components] return [c.glyphName for c in self.components]
else: else:
return [] return []
@ -1367,8 +1300,6 @@ class Glyph(object):
if self.isComposite(): if self.isComposite():
if hasattr(self, "program"): if hasattr(self, "program"):
del self.program del self.program
elif self.isVarComposite():
pass # Doesn't have hinting
else: else:
self.program = ttProgram.Program() self.program = ttProgram.Program()
self.program.fromBytecode([]) self.program.fromBytecode([])
@ -1450,13 +1381,6 @@ class Glyph(object):
i += 2 + instructionLen i += 2 + instructionLen
# Remove padding # Remove padding
data = data[:i] data = data[:i]
elif self.isVarComposite():
i = 0
MIN_SIZE = GlyphVarComponent.MIN_SIZE
while len(data[i : i + MIN_SIZE]) >= MIN_SIZE:
size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE])
i += size
data = data[:i]
self.data = data self.data = data
@ -1942,391 +1866,6 @@ class GlyphComponent(object):
return result if result is NotImplemented else not result return result if result is NotImplemented else not result
#
# Variable Composite glyphs
# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
#
class VarComponentFlags(IntFlag):
USE_MY_METRICS = 0x0001
AXIS_INDICES_ARE_SHORT = 0x0002
UNIFORM_SCALE = 0x0004
HAVE_TRANSLATE_X = 0x0008
HAVE_TRANSLATE_Y = 0x0010
HAVE_ROTATION = 0x0020
HAVE_SCALE_X = 0x0040
HAVE_SCALE_Y = 0x0080
HAVE_SKEW_X = 0x0100
HAVE_SKEW_Y = 0x0200
HAVE_TCENTER_X = 0x0400
HAVE_TCENTER_Y = 0x0800
GID_IS_24BIT = 0x1000
AXES_HAVE_VARIATION = 0x2000
RESET_UNSPECIFIED_AXES = 0x4000
VarComponentTransformMappingValues = namedtuple(
"VarComponentTransformMappingValues",
["flag", "fractionalBits", "scale", "defaultValue"],
)
VAR_COMPONENT_TRANSFORM_MAPPING = {
"translateX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
),
"translateY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
),
"rotation": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_ROTATION, 12, 180, 0
),
"scaleX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SCALE_X, 10, 1, 1
),
"scaleY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1
),
"skewX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SKEW_X, 12, -180, 0
),
"skewY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0
),
"tCenterX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0
),
"tCenterY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0
),
}
class GlyphVarComponent(object):
MIN_SIZE = 5
def __init__(self):
self.location = {}
self.transform = DecomposedTransform()
@staticmethod
def getSize(data):
size = 5
flags = struct.unpack(">H", data[:2])[0]
numAxes = int(data[2])
if flags & VarComponentFlags.GID_IS_24BIT:
size += 1
size += numAxes
if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
size += 2 * numAxes
else:
axisIndices = array.array("B", data[:numAxes])
size += numAxes
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
if flags & mapping_values.flag:
size += 2
return size
def decompile(self, data, glyfTable):
flags = struct.unpack(">H", data[:2])[0]
self.flags = int(flags)
data = data[2:]
numAxes = int(data[0])
data = data[1:]
if flags & VarComponentFlags.GID_IS_24BIT:
glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0])
data = data[3:]
flags ^= VarComponentFlags.GID_IS_24BIT
else:
glyphID = int(struct.unpack(">H", data[:2])[0])
data = data[2:]
self.glyphName = glyfTable.getGlyphName(int(glyphID))
if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
axisIndices = array.array("H", data[: 2 * numAxes])
if sys.byteorder != "big":
axisIndices.byteswap()
data = data[2 * numAxes :]
flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT
else:
axisIndices = array.array("B", data[:numAxes])
data = data[numAxes:]
assert len(axisIndices) == numAxes
axisIndices = list(axisIndices)
axisValues = array.array("h", data[: 2 * numAxes])
if sys.byteorder != "big":
axisValues.byteswap()
data = data[2 * numAxes :]
assert len(axisValues) == numAxes
axisValues = [fi2fl(v, 14) for v in axisValues]
self.location = {
glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues)
}
def read_transform_component(data, values):
if flags & values.flag:
return (
data[2:],
fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits)
* values.scale,
)
else:
return data, values.defaultValue
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
data, value = read_transform_component(data, mapping_values)
setattr(self.transform, attr_name, value)
if flags & VarComponentFlags.UNIFORM_SCALE:
if flags & VarComponentFlags.HAVE_SCALE_X and not (
flags & VarComponentFlags.HAVE_SCALE_Y
):
self.transform.scaleY = self.transform.scaleX
flags |= VarComponentFlags.HAVE_SCALE_Y
flags ^= VarComponentFlags.UNIFORM_SCALE
return data
def compile(self, glyfTable):
data = b""
if not hasattr(self, "flags"):
flags = 0
# Calculate optimal transform component flags
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
value = getattr(self.transform, attr_name)
if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi(
mapping.defaultValue / mapping.scale, mapping.fractionalBits
):
flags |= mapping.flag
else:
flags = self.flags
if (
flags & VarComponentFlags.HAVE_SCALE_X
and flags & VarComponentFlags.HAVE_SCALE_Y
and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10)
):
flags |= VarComponentFlags.UNIFORM_SCALE
flags ^= VarComponentFlags.HAVE_SCALE_Y
numAxes = len(self.location)
data = data + struct.pack(">B", numAxes)
glyphID = glyfTable.getGlyphID(self.glyphName)
if glyphID > 65535:
flags |= VarComponentFlags.GID_IS_24BIT
data = data + struct.pack(">L", glyphID)[1:]
else:
data = data + struct.pack(">H", glyphID)
axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()]
if all(a <= 255 for a in axisIndices):
axisIndices = array.array("B", axisIndices)
else:
axisIndices = array.array("H", axisIndices)
if sys.byteorder != "big":
axisIndices.byteswap()
flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT
data = data + bytes(axisIndices)
axisValues = self.location.values()
axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues))
if sys.byteorder != "big":
axisValues.byteswap()
data = data + bytes(axisValues)
def write_transform_component(data, value, values):
if flags & values.flag:
return data + struct.pack(
">h", fl2fi(value / values.scale, values.fractionalBits)
)
else:
return data
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
value = getattr(self.transform, attr_name)
data = write_transform_component(data, value, mapping_values)
return struct.pack(">H", flags) + data
def toXML(self, writer, ttFont):
attrs = [("glyphName", self.glyphName)]
if hasattr(self, "flags"):
attrs = attrs + [("flags", hex(self.flags))]
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
v = getattr(self.transform, attr_name)
if v != mapping.defaultValue:
attrs.append((attr_name, fl2str(v, mapping.fractionalBits)))
writer.begintag("varComponent", attrs)
writer.newline()
writer.begintag("location")
writer.newline()
for tag, v in self.location.items():
writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))])
writer.newline()
writer.endtag("location")
writer.newline()
writer.endtag("varComponent")
writer.newline()
def fromXML(self, name, attrs, content, ttFont):
self.glyphName = attrs["glyphName"]
if "flags" in attrs:
self.flags = safeEval(attrs["flags"])
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
if attr_name not in attrs:
continue
v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits)
setattr(self.transform, attr_name, v)
for c in content:
if not isinstance(c, tuple):
continue
name, attrs, content = c
if name != "location":
continue
for c in content:
if not isinstance(c, tuple):
continue
name, attrs, content = c
assert name == "axis"
assert not content
self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14)
def getPointCount(self):
assert hasattr(self, "flags"), "VarComponent with variations must have flags"
count = 0
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
count += len(self.location)
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
count += 1
if self.flags & VarComponentFlags.HAVE_ROTATION:
count += 1
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
count += 1
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
count += 1
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
count += 1
return count
def getCoordinatesAndControls(self):
coords = []
controls = []
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
for tag, v in self.location.items():
controls.append(tag)
coords.append((fl2fi(v, 14), 0))
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
controls.append("translate")
coords.append((self.transform.translateX, self.transform.translateY))
if self.flags & VarComponentFlags.HAVE_ROTATION:
controls.append("rotation")
coords.append((fl2fi(self.transform.rotation / 180, 12), 0))
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
controls.append("scale")
coords.append(
(fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10))
)
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
controls.append("skew")
coords.append(
(
fl2fi(self.transform.skewX / -180, 12),
fl2fi(self.transform.skewY / 180, 12),
)
)
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
controls.append("tCenter")
coords.append((self.transform.tCenterX, self.transform.tCenterY))
return coords, controls
def setCoordinates(self, coords):
i = 0
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
newLocation = {}
for tag in self.location:
newLocation[tag] = fi2fl(coords[i][0], 14)
i += 1
self.location = newLocation
self.transform = DecomposedTransform()
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
self.transform.translateX, self.transform.translateY = coords[i]
i += 1
if self.flags & VarComponentFlags.HAVE_ROTATION:
self.transform.rotation = fi2fl(coords[i][0], 12) * 180
i += 1
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
self.transform.scaleX, self.transform.scaleY = fi2fl(
coords[i][0], 10
), fi2fl(coords[i][1], 10)
i += 1
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
self.transform.skewX, self.transform.skewY = (
fi2fl(coords[i][0], 12) * -180,
fi2fl(coords[i][1], 12) * 180,
)
i += 1
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
self.transform.tCenterX, self.transform.tCenterY = coords[i]
i += 1
return coords[i:]
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
return self.__dict__ == other.__dict__
def __ne__(self, other):
result = self.__eq__(other)
return result if result is NotImplemented else not result
class GlyphCoordinates(object): class GlyphCoordinates(object):
"""A list of glyph coordinates. """A list of glyph coordinates.

View File

@ -1,7 +1,8 @@
from collections import UserDict, deque from collections import deque
from functools import partial from functools import partial
from fontTools.misc import sstruct from fontTools.misc import sstruct
from fontTools.misc.textTools import safeEval from fontTools.misc.textTools import safeEval
from fontTools.misc.lazyTools import LazyDict
from . import DefaultTable from . import DefaultTable
import array import array
import itertools import itertools
@ -39,19 +40,6 @@ GVAR_HEADER_FORMAT = """
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) 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): class table__g_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar", "glyf"] dependencies = ["fvar", "glyf"]
@ -116,11 +104,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
assert len(glyphs) == self.glyphCount assert len(glyphs) == self.glyphCount
assert len(axisTags) == self.axisCount assert len(axisTags) == self.axisCount
offsets = self.decompileOffsets_(
data[GVAR_HEADER_SIZE:],
tableFormat=(self.flags & 1),
glyphCount=self.glyphCount,
)
sharedCoords = tv.decompileSharedTuples( sharedCoords = tv.decompileSharedTuples(
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
) )
@ -128,20 +111,35 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
offsetToData = self.offsetToGlyphVariationData offsetToData = self.offsetToGlyphVariationData
glyf = ttFont["glyf"] glyf = ttFont["glyf"]
def decompileVarGlyph(glyphName, gid): def get_read_item():
gvarData = data[ reverseGlyphMap = ttFont.getReverseGlyphMap()
offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] tableFormat = self.flags & 1
]
if not gvarData:
return []
glyph = glyf[glyphName]
numPointsInGlyph = self.getNumPoints_(glyph)
return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
for gid in range(self.glyphCount): def read_item(glyphName):
glyphName = glyphs[gid] gid = reverseGlyphMap[glyphName]
variations[glyphName] = partial(decompileVarGlyph, glyphName, gid) offsetSize = 2 if tableFormat == 0 else 4
self.variations = _LazyDict(variations) startOffset = GVAR_HEADER_SIZE + offsetSize * gid
endOffset = startOffset + offsetSize * 2
offsets = table__g_v_a_r.decompileOffsets_(
data[startOffset:endOffset],
tableFormat=tableFormat,
glyphCount=1,
)
gvarData = data[offsetToData + offsets[0] : offsetToData + offsets[1]]
if not gvarData:
return []
glyph = glyf[glyphName]
numPointsInGlyph = self.getNumPoints_(glyph)
return decompileGlyph_(
numPointsInGlyph, sharedCoords, axisTags, gvarData
)
return read_item
read_item = get_read_item()
l = LazyDict({glyphs[gid]: read_item for gid in range(self.glyphCount)})
self.variations = l
if ttFont.lazy is False: # Be lazy for None and True if ttFont.lazy is False: # Be lazy for None and True
self.ensureDecompiled() self.ensureDecompiled()
@ -245,11 +243,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
if glyph.isComposite(): if glyph.isComposite():
return len(glyph.components) + NUM_PHANTOM_POINTS return len(glyph.components) + NUM_PHANTOM_POINTS
elif glyph.isVarComposite():
count = 0
for component in glyph.components:
count += component.getPointCount()
return count + NUM_PHANTOM_POINTS
else: else:
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute. # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS

View File

@ -21,10 +21,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable):
if sys.byteorder != "big": if sys.byteorder != "big":
locations.byteswap() locations.byteswap()
if not longFormat: if not longFormat:
l = array.array("I") locations = array.array("I", (2 * l for l in locations))
for i in range(len(locations)):
l.append(locations[i] * 2)
locations = l
if len(locations) < (ttFont["maxp"].numGlyphs + 1): if len(locations) < (ttFont["maxp"].numGlyphs + 1):
log.warning( log.warning(
"corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d", "corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d",

View File

@ -1146,7 +1146,10 @@ class BaseTable(object):
except KeyError: except KeyError:
raise # XXX on KeyError, raise nice error raise # XXX on KeyError, raise nice error
value = conv.xmlRead(attrs, content, font) value = conv.xmlRead(attrs, content, font)
if conv.repeat: # Some manually-written tables have a conv.repeat of ""
# to represent lists. Hence comparing to None here to
# allow those lists to be read correctly from XML.
if conv.repeat is not None:
seq = getattr(self, conv.name, None) seq = getattr(self, conv.name, None)
if seq is None: if seq is None:
seq = [] seq = []

View File

@ -6,8 +6,10 @@ from fontTools.misc.fixedTools import (
ensureVersionIsLong as fi2ve, ensureVersionIsLong as fi2ve,
versionToFixed as ve2fi, versionToFixed as ve2fi,
) )
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
from fontTools.misc.lazyTools import LazyList
from fontTools.ttLib import getSearchRange from fontTools.ttLib import getSearchRange
from .otBase import ( from .otBase import (
CountReference, CountReference,
@ -18,6 +20,7 @@ from .otBase import (
) )
from .otTables import ( from .otTables import (
lookupTypes, lookupTypes,
VarCompositeGlyph,
AATStateTable, AATStateTable,
AATState, AATState,
AATAction, AATAction,
@ -29,8 +32,9 @@ from .otTables import (
CompositeMode as _CompositeMode, CompositeMode as _CompositeMode,
NO_VARIATION_INDEX, NO_VARIATION_INDEX,
) )
from itertools import zip_longest from itertools import zip_longest, accumulate
from functools import partial from functools import partial
from types import SimpleNamespace
import re import re
import struct import struct
from typing import Optional from typing import Optional
@ -78,7 +82,7 @@ def buildConverters(tableSpec, tableNamespace):
conv = converterClass(name, repeat, aux, description=descr) conv = converterClass(name, repeat, aux, description=descr)
if conv.tableClass: if conv.tableClass:
# A "template" such as OffsetTo(AType) knowss the table class already # A "template" such as OffsetTo(AType) knows the table class already
tableClass = conv.tableClass tableClass = conv.tableClass
elif tp in ("MortChain", "MortSubtable", "MorxChain"): elif tp in ("MortChain", "MortSubtable", "MorxChain"):
tableClass = tableNamespace.get(tp) tableClass = tableNamespace.get(tp)
@ -105,46 +109,6 @@ def buildConverters(tableSpec, tableNamespace):
return converters, convertersByName return converters, convertersByName
class _MissingItem(tuple):
__slots__ = ()
try:
from collections import UserList
except ImportError:
from UserList import UserList
class _LazyList(UserList):
def __getslice__(self, i, j):
return self.__getitem__(slice(i, j))
def __getitem__(self, k):
if isinstance(k, slice):
indices = range(*k.indices(len(self)))
return [self[i] for i in indices]
item = self.data[k]
if isinstance(item, _MissingItem):
self.reader.seek(self.pos + item[0] * self.recordSize)
item = self.conv.read(self.reader, self.font, {})
self.data[k] = item
return item
def __add__(self, other):
if isinstance(other, _LazyList):
other = list(other)
elif isinstance(other, list):
pass
else:
return NotImplemented
return list(self) + other
def __radd__(self, other):
if not isinstance(other, list):
return NotImplemented
return other + list(self)
class BaseConverter(object): class BaseConverter(object):
"""Base class for converter objects. Apart from the constructor, this """Base class for converter objects. Apart from the constructor, this
is an abstract class.""" is an abstract class."""
@ -176,6 +140,7 @@ class BaseConverter(object):
"AxisCount", "AxisCount",
"BaseGlyphRecordCount", "BaseGlyphRecordCount",
"LayerRecordCount", "LayerRecordCount",
"AxisIndicesList",
] ]
self.description = description self.description = description
@ -192,14 +157,21 @@ class BaseConverter(object):
l.append(self.read(reader, font, tableDict)) l.append(self.read(reader, font, tableDict))
return l return l
else: else:
l = _LazyList()
l.reader = reader.copy() def get_read_item():
l.pos = l.reader.pos reader_copy = reader.copy()
l.font = font pos = reader.pos
l.conv = self
l.recordSize = recordSize def read_item(i):
l.extend(_MissingItem([i]) for i in range(count)) reader_copy.seek(pos + i * recordSize)
return self.read(reader_copy, font, {})
return read_item
read_item = get_read_item()
l = LazyList(read_item for i in range(count))
reader.advance(count * recordSize) reader.advance(count * recordSize)
return l return l
def getRecordSize(self, reader): def getRecordSize(self, reader):
@ -1833,6 +1805,169 @@ class VarDataValue(BaseConverter):
return safeEval(attrs["value"]) return safeEval(attrs["value"])
class TupleValues:
def read(self, data, font):
return TupleVariation.decompileDeltas_(None, data)[0]
def write(self, writer, font, tableDict, values, repeatIndex=None):
return bytes(TupleVariation.compileDeltaValues_(values))
def xmlRead(self, attrs, content, font):
return safeEval(attrs["value"])
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
xmlWriter.newline()
class CFF2Index(BaseConverter):
def __init__(
self,
name,
repeat,
aux,
tableClass=None,
*,
itemClass=None,
itemConverterClass=None,
description="",
):
BaseConverter.__init__(
self, name, repeat, aux, tableClass, description=description
)
self._itemClass = itemClass
self._converter = (
itemConverterClass() if itemConverterClass is not None else None
)
def read(self, reader, font, tableDict):
count = reader.readULong()
if count == 0:
return []
offSize = reader.readUInt8()
def getReadArray(reader, offSize):
return {
1: reader.readUInt8Array,
2: reader.readUShortArray,
3: reader.readUInt24Array,
4: reader.readULongArray,
}[offSize]
readArray = getReadArray(reader, offSize)
lazy = font.lazy is not False and count > 8
if not lazy:
offsets = readArray(count + 1)
items = []
lastOffset = offsets.pop(0)
reader.readData(lastOffset - 1) # In case first offset is not 1
for offset in offsets:
assert lastOffset <= offset
item = reader.readData(offset - lastOffset)
if self._itemClass is not None:
obj = self._itemClass()
obj.decompile(item, font, reader.localState)
item = obj
elif self._converter is not None:
item = self._converter.read(item, font)
items.append(item)
lastOffset = offset
return items
else:
def get_read_item():
reader_copy = reader.copy()
offset_pos = reader.pos
data_pos = offset_pos + (count + 1) * offSize - 1
readArray = getReadArray(reader_copy, offSize)
def read_item(i):
reader_copy.seek(offset_pos + i * offSize)
offsets = readArray(2)
reader_copy.seek(data_pos + offsets[0])
item = reader_copy.readData(offsets[1] - offsets[0])
if self._itemClass is not None:
obj = self._itemClass()
obj.decompile(item, font, reader_copy.localState)
item = obj
elif self._converter is not None:
item = self._converter.read(item, font)
return item
return read_item
read_item = get_read_item()
l = LazyList([read_item] * count)
# TODO: Advance reader
return l
def write(self, writer, font, tableDict, values, repeatIndex=None):
items = values
writer.writeULong(len(items))
if not len(items):
return
if self._itemClass is not None:
items = [item.compile(font) for item in items]
elif self._converter is not None:
items = [
self._converter.write(writer, font, tableDict, item, i)
for i, item in enumerate(items)
]
offsets = [len(item) for item in items]
offsets = list(accumulate(offsets, initial=1))
lastOffset = offsets[-1]
offSize = (
1
if lastOffset < 0x100
else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4
)
writer.writeUInt8(offSize)
writeArray = {
1: writer.writeUInt8Array,
2: writer.writeUShortArray,
3: writer.writeUInt24Array,
4: writer.writeULongArray,
}[offSize]
writeArray(offsets)
for item in items:
writer.writeData(item)
def xmlRead(self, attrs, content, font):
if self._itemClass is not None:
obj = self._itemClass()
obj.fromXML(None, attrs, content, font)
return obj
elif self._converter is not None:
return self._converter.xmlRead(attrs, content, font)
else:
raise NotImplementedError()
def xmlWrite(self, xmlWriter, font, value, name, attrs):
if self._itemClass is not None:
for i, item in enumerate(value):
item.toXML(xmlWriter, font, [("index", i)], name)
elif self._converter is not None:
for i, item in enumerate(value):
self._converter.xmlWrite(
xmlWriter, font, item, name, attrs + [("index", i)]
)
else:
raise NotImplementedError()
class LookupFlag(UShort): class LookupFlag(UShort):
def xmlWrite(self, xmlWriter, font, value, name, attrs): def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)]) xmlWriter.simpletag(name, attrs + [("value", value)])
@ -1910,6 +2045,8 @@ converterMapping = {
"ExtendMode": ExtendMode, "ExtendMode": ExtendMode,
"CompositeMode": CompositeMode, "CompositeMode": CompositeMode,
"STATFlags": STATFlags, "STATFlags": STATFlags,
"TupleList": partial(CFF2Index, itemConverterClass=TupleValues),
"VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph),
# AAT # AAT
"CIDGlyphMap": CIDGlyphMap, "CIDGlyphMap": CIDGlyphMap,
"GlyphCIDMap": GlyphCIDMap, "GlyphCIDMap": GlyphCIDMap,

View File

@ -3168,6 +3168,25 @@ otData = [
), ),
], ],
), ),
(
"ConditionList",
[
(
"uint32",
"ConditionCount",
None,
None,
"Number of condition tables in the ConditionTable array",
),
(
"LOffset",
"ConditionTable",
"ConditionCount",
0,
"Array of offset to condition tables, from the beginning of the ConditionList table.",
),
],
),
( (
"ConditionSet", "ConditionSet",
[ [
@ -3183,7 +3202,7 @@ otData = [
"ConditionTable", "ConditionTable",
"ConditionCount", "ConditionCount",
0, 0,
"Array of condition tables.", "Array of offset to condition tables, from the beginning of the ConditionSet table.",
), ),
], ],
), ),
@ -3214,6 +3233,79 @@ otData = [
), ),
], ],
), ),
(
"ConditionTableFormat2",
[
("uint16", "Format", None, None, "Format, = 2"),
(
"int16",
"DefaultValue",
None,
None,
"Value at default instance.",
),
(
"uint32",
"VarIdx",
None,
None,
"Variation index to vary the value based on current designspace location.",
),
],
),
(
"ConditionTableFormat3",
[
("uint16", "Format", None, None, "Format, = 3"),
(
"uint8",
"ConditionCount",
None,
None,
"Index for the variation axis within the fvar table, base 0.",
),
(
"Offset24",
"ConditionTable",
"ConditionCount",
0,
"Array of condition tables for this conjunction (AND) expression.",
),
],
),
(
"ConditionTableFormat4",
[
("uint16", "Format", None, None, "Format, = 4"),
(
"uint8",
"ConditionCount",
None,
None,
"Index for the variation axis within the fvar table, base 0.",
),
(
"Offset24",
"ConditionTable",
"ConditionCount",
0,
"Array of condition tables for this disjunction (OR) expression.",
),
],
),
(
"ConditionTableFormat5",
[
("uint16", "Format", None, None, "Format, = 5"),
(
"Offset24",
"ConditionTable",
None,
None,
"Condition to negate.",
),
],
),
( (
"FeatureTableSubstitution", "FeatureTableSubstitution",
[ [
@ -3322,6 +3414,78 @@ otData = [
("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"), ("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"),
], ],
), ),
# MultiVariationStore
(
"SparseVarRegionAxis",
[
("uint16", "AxisIndex", None, None, ""),
("F2Dot14", "StartCoord", None, None, ""),
("F2Dot14", "PeakCoord", None, None, ""),
("F2Dot14", "EndCoord", None, None, ""),
],
),
(
"SparseVarRegion",
[
("uint16", "SparseRegionCount", None, None, ""),
("struct", "SparseVarRegionAxis", "SparseRegionCount", 0, ""),
],
),
(
"SparseVarRegionList",
[
("uint16", "RegionCount", None, None, ""),
("LOffsetTo(SparseVarRegion)", "Region", "RegionCount", 0, ""),
],
),
(
"MultiVarData",
[
("uint8", "Format", None, None, "Set to 1."),
("uint16", "VarRegionCount", None, None, ""),
("uint16", "VarRegionIndex", "VarRegionCount", 0, ""),
("TupleList", "Item", "", 0, ""),
],
),
(
"MultiVarStore",
[
("uint16", "Format", None, None, "Set to 1."),
("LOffset", "SparseVarRegionList", None, None, ""),
("uint16", "MultiVarDataCount", None, None, ""),
("LOffset", "MultiVarData", "MultiVarDataCount", 0, ""),
],
),
# VariableComposites
(
"VARC",
[
(
"Version",
"Version",
None,
None,
"Version of the HVAR table-initially = 0x00010000",
),
("LOffset", "Coverage", None, None, ""),
("LOffset", "MultiVarStore", None, None, "(may be NULL)"),
("LOffset", "ConditionList", None, None, "(may be NULL)"),
("LOffset", "AxisIndicesList", None, None, "(may be NULL)"),
("LOffset", "VarCompositeGlyphs", None, None, ""),
],
),
(
"AxisIndicesList",
[
("TupleList", "Item", "", 0, ""),
],
),
(
"VarCompositeGlyphs",
[
("VarCompositeGlyphList", "VarCompositeGlyph", "", None, ""),
],
),
# Glyph advance variations # Glyph advance variations
( (
"HVAR", "HVAR",

View File

@ -11,11 +11,13 @@ from functools import reduce
from math import radians from math import radians
import itertools import itertools
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables.otTraverse import dfs_base_table from fontTools.ttLib.tables.otTraverse import dfs_base_table
from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.arrayTools import quantizeRect
from fontTools.misc.roundTools import otRound from fontTools.misc.roundTools import otRound
from fontTools.misc.transform import Transform, Identity from fontTools.misc.transform import Transform, Identity, DecomposedTransform
from fontTools.misc.textTools import bytesjoin, pad, safeEval from fontTools.misc.textTools import bytesjoin, pad, safeEval
from fontTools.misc.vector import Vector
from fontTools.pens.boundsPen import ControlBoundsPen from fontTools.pens.boundsPen import ControlBoundsPen
from fontTools.pens.transformPen import TransformPen from fontTools.pens.transformPen import TransformPen
from .otBase import ( from .otBase import (
@ -25,9 +27,18 @@ from .otBase import (
CountReference, CountReference,
getFormatSwitchingBaseTableClass, getFormatSwitchingBaseTableClass,
) )
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
floatToFixed as fl2fi,
floatToFixedToStr as fl2str,
strToFixedToFloat as str2fl,
)
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
import logging import logging
import struct import struct
import array
import sys
from enum import IntFlag
from typing import TYPE_CHECKING, Iterator, List, Optional, Set from typing import TYPE_CHECKING, Iterator, List, Optional, Set
if TYPE_CHECKING: if TYPE_CHECKING:
@ -37,6 +48,389 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VarComponentFlags(IntFlag):
RESET_UNSPECIFIED_AXES = 1 << 0
HAVE_AXES = 1 << 1
AXIS_VALUES_HAVE_VARIATION = 1 << 2
TRANSFORM_HAS_VARIATION = 1 << 3
HAVE_TRANSLATE_X = 1 << 4
HAVE_TRANSLATE_Y = 1 << 5
HAVE_ROTATION = 1 << 6
HAVE_CONDITION = 1 << 7
HAVE_SCALE_X = 1 << 8
HAVE_SCALE_Y = 1 << 9
HAVE_TCENTER_X = 1 << 10
HAVE_TCENTER_Y = 1 << 11
GID_IS_24BIT = 1 << 12
HAVE_SKEW_X = 1 << 13
HAVE_SKEW_Y = 1 << 14
RESERVED_MASK = (1 << 32) - (1 << 15)
VarTransformMappingValues = namedtuple(
"VarTransformMappingValues",
["flag", "fractionalBits", "scale", "defaultValue"],
)
VAR_TRANSFORM_MAPPING = {
"translateX": VarTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
),
"translateY": VarTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
),
"rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0),
"scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1),
"scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1),
"skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0),
"skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0),
"tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0),
"tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0),
}
# Probably should be somewhere in fontTools.misc
_packer = {
1: lambda v: struct.pack(">B", v),
2: lambda v: struct.pack(">H", v),
3: lambda v: struct.pack(">L", v)[1:],
4: lambda v: struct.pack(">L", v),
}
_unpacker = {
1: lambda v: struct.unpack(">B", v)[0],
2: lambda v: struct.unpack(">H", v)[0],
3: lambda v: struct.unpack(">L", b"\0" + v)[0],
4: lambda v: struct.unpack(">L", v)[0],
}
def _read_uint32var(data, i):
"""Read a variable-length number from data starting at index i.
Return the number and the next index.
"""
b0 = data[i]
if b0 < 0x80:
return b0, i + 1
elif b0 < 0xC0:
return (b0 - 0x80) << 8 | data[i + 1], i + 2
elif b0 < 0xE0:
return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3
elif b0 < 0xF0:
return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[
i + 3
], i + 4
else:
return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[
i + 3
] << 8 | data[i + 4], i + 5
def _write_uint32var(v):
"""Write a variable-length number.
Return the data.
"""
if v < 0x80:
return struct.pack(">B", v)
elif v < 0x4000:
return struct.pack(">H", (v | 0x8000))
elif v < 0x200000:
return struct.pack(">L", (v | 0xC00000))[1:]
elif v < 0x10000000:
return struct.pack(">L", (v | 0xE0000000))
else:
return struct.pack(">B", 0xF0) + struct.pack(">L", v)
class VarComponent:
def __init__(self):
self.populateDefaults()
def populateDefaults(self, propagator=None):
self.flags = 0
self.glyphName = None
self.conditionIndex = None
self.axisIndicesIndex = None
self.axisValues = ()
self.axisValuesVarIndex = NO_VARIATION_INDEX
self.transformVarIndex = NO_VARIATION_INDEX
self.transform = DecomposedTransform()
def decompile(self, data, font, localState):
i = 0
self.flags, i = _read_uint32var(data, i)
flags = self.flags
gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2
glyphID = _unpacker[gidSize](data[i : i + gidSize])
i += gidSize
self.glyphName = font.glyphOrder[glyphID]
if flags & VarComponentFlags.HAVE_CONDITION:
self.conditionIndex, i = _read_uint32var(data, i)
if flags & VarComponentFlags.HAVE_AXES:
self.axisIndicesIndex, i = _read_uint32var(data, i)
else:
self.axisIndicesIndex = None
if self.axisIndicesIndex is None:
numAxes = 0
else:
axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex]
numAxes = len(axisIndices)
if flags & VarComponentFlags.HAVE_AXES:
axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i)
self.axisValues = tuple(fi2fl(v, 14) for v in axisValues)
else:
self.axisValues = ()
assert len(self.axisValues) == numAxes
if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION:
self.axisValuesVarIndex, i = _read_uint32var(data, i)
else:
self.axisValuesVarIndex = NO_VARIATION_INDEX
if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION:
self.transformVarIndex, i = _read_uint32var(data, i)
else:
self.transformVarIndex = NO_VARIATION_INDEX
self.transform = DecomposedTransform()
def read_transform_component(values):
nonlocal i
if flags & values.flag:
v = (
fi2fl(
struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits
)
* values.scale
)
i += 2
return v
else:
return values.defaultValue
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
value = read_transform_component(mapping_values)
setattr(self.transform, attr_name, value)
if not (flags & VarComponentFlags.HAVE_SCALE_Y):
self.transform.scaleY = self.transform.scaleX
n = flags & VarComponentFlags.RESERVED_MASK
while n:
_, i = _read_uint32var(data, i)
n &= n - 1
return data[i:]
def compile(self, font):
data = []
flags = self.flags
glyphID = font.getGlyphID(self.glyphName)
if glyphID > 65535:
flags |= VarComponentFlags.GID_IS_24BIT
data.append(_packer[3](glyphID))
else:
flags &= ~VarComponentFlags.GID_IS_24BIT
data.append(_packer[2](glyphID))
if self.conditionIndex is not None:
flags |= VarComponentFlags.HAVE_CONDITION
data.append(_write_uint32var(self.conditionIndex))
numAxes = len(self.axisValues)
if numAxes:
flags |= VarComponentFlags.HAVE_AXES
data.append(_write_uint32var(self.axisIndicesIndex))
data.append(
TupleVariation.compileDeltaValues_(
[fl2fi(v, 14) for v in self.axisValues]
)
)
else:
flags &= ~VarComponentFlags.HAVE_AXES
if self.axisValuesVarIndex != NO_VARIATION_INDEX:
flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
data.append(_write_uint32var(self.axisValuesVarIndex))
else:
flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
if self.transformVarIndex != NO_VARIATION_INDEX:
flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION
data.append(_write_uint32var(self.transformVarIndex))
else:
flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION
def write_transform_component(value, values):
if flags & values.flag:
return struct.pack(
">h", fl2fi(value / values.scale, values.fractionalBits)
)
else:
return b""
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
value = getattr(self.transform, attr_name)
data.append(write_transform_component(value, mapping_values))
return _write_uint32var(flags) + bytesjoin(data)
def toXML(self, writer, ttFont, attrs):
writer.begintag("VarComponent", attrs)
writer.newline()
def write(name, value, attrs=()):
if value is not None:
writer.simpletag(name, (("value", value),) + attrs)
writer.newline()
write("glyphName", self.glyphName)
if self.conditionIndex is not None:
write("conditionIndex", self.conditionIndex)
if self.axisIndicesIndex is not None:
write("axisIndicesIndex", self.axisIndicesIndex)
if (
self.axisIndicesIndex is not None
or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
):
if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES:
attrs = (("resetUnspecifiedAxes", 1),)
else:
attrs = ()
write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs)
if self.axisValuesVarIndex != NO_VARIATION_INDEX:
write("axisValuesVarIndex", self.axisValuesVarIndex)
if self.transformVarIndex != NO_VARIATION_INDEX:
write("transformVarIndex", self.transformVarIndex)
# Only write transform components that are specified in the
# flags, even if they are the default value.
for attr_name, mapping in VAR_TRANSFORM_MAPPING.items():
if not (self.flags & mapping.flag):
continue
v = getattr(self.transform, attr_name)
write(attr_name, fl2str(v, mapping.fractionalBits))
writer.endtag("VarComponent")
writer.newline()
def fromXML(self, name, attrs, content, ttFont):
content = [c for c in content if isinstance(c, tuple)]
self.populateDefaults()
for name, attrs, content in content:
assert not content
v = attrs["value"]
if name == "glyphName":
self.glyphName = v
elif name == "conditionIndex":
self.conditionIndex = safeEval(v)
elif name == "axisIndicesIndex":
self.axisIndicesIndex = safeEval(v)
elif name == "axisValues":
self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v))
if safeEval(attrs.get("resetUnspecifiedAxes", "0")):
self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES
elif name == "axisValuesVarIndex":
self.axisValuesVarIndex = safeEval(v)
elif name == "transformVarIndex":
self.transformVarIndex = safeEval(v)
elif name in VAR_TRANSFORM_MAPPING:
setattr(
self.transform,
name,
safeEval(v),
)
self.flags |= VAR_TRANSFORM_MAPPING[name].flag
else:
assert False, name
def applyTransformDeltas(self, deltas):
i = 0
def read_transform_component_delta(values):
nonlocal i
if self.flags & values.flag:
v = fi2fl(deltas[i], values.fractionalBits) * values.scale
i += 1
return v
else:
return 0
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
value = read_transform_component_delta(mapping_values)
setattr(
self.transform, attr_name, getattr(self.transform, attr_name) + value
)
if not (self.flags & VarComponentFlags.HAVE_SCALE_Y):
self.transform.scaleY = self.transform.scaleX
assert i == len(deltas), (i, len(deltas))
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
return self.__dict__ == other.__dict__
def __ne__(self, other):
result = self.__eq__(other)
return result if result is NotImplemented else not result
class VarCompositeGlyph:
def __init__(self, components=None):
self.components = components if components is not None else []
def decompile(self, data, font, localState):
self.components = []
while data:
component = VarComponent()
data = component.decompile(data, font, localState)
self.components.append(component)
def compile(self, font):
data = []
for component in self.components:
data.append(component.compile(font))
return bytesjoin(data)
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag("VarCompositeGlyph", attrs)
xmlWriter.newline()
for i, component in enumerate(self.components):
component.toXML(xmlWriter, font, [("index", i)])
xmlWriter.endtag("VarCompositeGlyph")
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
content = [c for c in content if isinstance(c, tuple)]
for name, attrs, content in content:
assert name == "VarComponent"
component = VarComponent()
component.fromXML(name, attrs, content, font)
self.components.append(component)
class AATStateTable(object): class AATStateTable(object):
def __init__(self): def __init__(self):
self.GlyphClasses = {} # GlyphID --> GlyphClass self.GlyphClasses = {} # GlyphID --> GlyphClass

View File

@ -4,7 +4,12 @@ from fontTools.misc.configTools import AbstractConfig
from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.textTools import Tag, byteord, tostr
from fontTools.misc.loggingTools import deprecateArgument from fontTools.misc.loggingTools import deprecateArgument
from fontTools.ttLib import TTLibError from fontTools.ttLib import TTLibError
from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf from fontTools.ttLib.ttGlyphSet import (
_TTGlyph,
_TTGlyphSetCFF,
_TTGlyphSetGlyf,
_TTGlyphSetVARC,
)
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
from io import BytesIO, StringIO, UnsupportedOperation from io import BytesIO, StringIO, UnsupportedOperation
import os import os
@ -764,12 +769,16 @@ class TTFont(object):
location = None location = None
if location and not normalized: if location and not normalized:
location = self.normalizeLocation(location) location = self.normalizeLocation(location)
glyphSet = None
if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
return _TTGlyphSetCFF(self, location) glyphSet = _TTGlyphSetCFF(self, location)
elif "glyf" in self: elif "glyf" in self:
return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
else: else:
raise TTLibError("Font contains no outlines") raise TTLibError("Font contains no outlines")
if "VARC" in self:
glyphSet = _TTGlyphSetVARC(self, location, glyphSet)
return glyphSet
def normalizeLocation(self, location): def normalizeLocation(self, location):
"""Normalize a ``location`` from the font's defined axes space (also """Normalize a ``location`` from the font's defined axes space (also

View File

@ -3,11 +3,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Mapping from collections.abc import Mapping
from contextlib import contextmanager from contextlib import contextmanager
from copy import copy from copy import copy, deepcopy
from types import SimpleNamespace from types import SimpleNamespace
from fontTools.misc.fixedTools import otRound from fontTools.misc.vector import Vector
from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl
from fontTools.misc.loggingTools import deprecateFunction from fontTools.misc.loggingTools import deprecateFunction
from fontTools.misc.transform import Transform from fontTools.misc.transform import Transform, DecomposedTransform
from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.pens.recordingPen import ( from fontTools.pens.recordingPen import (
DecomposingRecordingPen, DecomposingRecordingPen,
@ -103,6 +104,16 @@ class _TTGlyphSetGlyf(_TTGlyphSet):
return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
class _TTGlyphSetGlyf(_TTGlyphSet):
def __init__(self, font, location, recalcBounds=True):
self.glyfTable = font["glyf"]
super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds)
self.gvarTable = font.get("gvar")
def __getitem__(self, glyphName):
return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
class _TTGlyphSetCFF(_TTGlyphSet): class _TTGlyphSetCFF(_TTGlyphSet):
def __init__(self, font, location): def __init__(self, font, location):
tableTag = "CFF2" if "CFF2" in font else "CFF " tableTag = "CFF2" if "CFF2" in font else "CFF "
@ -123,6 +134,19 @@ class _TTGlyphSetCFF(_TTGlyphSet):
return _TTGlyphCFF(self, glyphName) return _TTGlyphCFF(self, glyphName)
class _TTGlyphSetVARC(_TTGlyphSet):
def __init__(self, font, location, glyphSet):
self.glyphSet = glyphSet
super().__init__(font, location, glyphSet)
self.varcTable = font["VARC"].table
def __getitem__(self, glyphName):
varc = self.varcTable
if glyphName not in varc.Coverage.glyphs:
return self.glyphSet[glyphName]
return _TTGlyphVARC(self, glyphName)
class _TTGlyph(ABC): class _TTGlyph(ABC):
"""Glyph object that supports the Pen protocol, meaning that it has """Glyph object that supports the Pen protocol, meaning that it has
.draw() and .drawPoints() methods that take a pen object as their only .draw() and .drawPoints() methods that take a pen object as their only
@ -178,10 +202,6 @@ class _TTGlyphGlyf(_TTGlyph):
if depth: if depth:
offset = 0 # Offset should only apply at top-level offset = 0 # Offset should only apply at top-level
if glyph.isVarComposite():
self._drawVarComposite(glyph, pen, False)
return
glyph.draw(pen, self.glyphSet.glyfTable, offset) glyph.draw(pen, self.glyphSet.glyfTable, offset)
def drawPoints(self, pen): def drawPoints(self, pen):
@ -194,35 +214,8 @@ class _TTGlyphGlyf(_TTGlyph):
if depth: if depth:
offset = 0 # Offset should only apply at top-level offset = 0 # Offset should only apply at top-level
if glyph.isVarComposite():
self._drawVarComposite(glyph, pen, True)
return
glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
def _drawVarComposite(self, glyph, pen, isPointPen):
from fontTools.ttLib.tables._g_l_y_f import (
VarComponentFlags,
VAR_COMPONENT_TRANSFORM_MAPPING,
)
for comp in glyph.components:
with self.glyphSet.pushLocation(
comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
):
try:
pen.addVarComponent(
comp.glyphName, comp.transform, self.glyphSet.rawLocation
)
except AttributeError:
t = comp.transform.toTransform()
if isPointPen:
tPen = TransformPointPen(pen, t)
self.glyphSet[comp.glyphName].drawPoints(tPen)
else:
tPen = TransformPen(pen, t)
self.glyphSet[comp.glyphName].draw(tPen)
def _getGlyphAndOffset(self): def _getGlyphAndOffset(self):
if self.glyphSet.location and self.glyphSet.gvarTable is not None: if self.glyphSet.location and self.glyphSet.gvarTable is not None:
glyph = self._getGlyphInstance() glyph = self._getGlyphInstance()
@ -283,6 +276,128 @@ class _TTGlyphCFF(_TTGlyph):
self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender)
def _evaluateCondition(condition, fvarAxes, location, instancer):
if condition.Format == 1:
# ConditionAxisRange
axisIndex = condition.AxisIndex
axisTag = fvarAxes[axisIndex].axisTag
axisValue = location.get(axisTag, 0)
minValue = condition.FilterRangeMinValue
maxValue = condition.FilterRangeMaxValue
return minValue <= axisValue <= maxValue
elif condition.Format == 2:
# ConditionValue
value = condition.DefaultValue
value += instancer[condition.VarIdx][0]
return value > 0
elif condition.Format == 3:
# ConditionAnd
for subcondition in condition.ConditionTable:
if not _evaluateCondition(subcondition, fvarAxes, location, instancer):
return False
return True
elif condition.Format == 4:
# ConditionOr
for subcondition in condition.ConditionTable:
if _evaluateCondition(subcondition, fvarAxes, location, instancer):
return True
return False
elif condition.Format == 5:
# ConditionNegate
return not _evaluateCondition(
condition.conditionTable, fvarAxes, location, instancer
)
else:
return False # Unkonwn condition format
class _TTGlyphVARC(_TTGlyph):
def _draw(self, pen, isPointPen):
"""Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
how that works.
"""
from fontTools.ttLib.tables.otTables import (
VarComponentFlags,
NO_VARIATION_INDEX,
)
glyphSet = self.glyphSet
varc = glyphSet.varcTable
idx = varc.Coverage.glyphs.index(self.name)
glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx]
from fontTools.varLib.multiVarStore import MultiVarStoreInstancer
from fontTools.varLib.varStore import VarStoreInstancer
fvarAxes = glyphSet.font["fvar"].axes
instancer = MultiVarStoreInstancer(
varc.MultiVarStore, fvarAxes, self.glyphSet.location
)
for comp in glyph.components:
if comp.flags & VarComponentFlags.HAVE_CONDITION:
condition = varc.ConditionList.ConditionTable[comp.conditionIndex]
if not _evaluateCondition(
condition, fvarAxes, self.glyphSet.location, instancer
):
continue
location = {}
if comp.axisIndicesIndex is not None:
axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex]
axisValues = Vector(comp.axisValues)
if comp.axisValuesVarIndex != NO_VARIATION_INDEX:
axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14)
assert len(axisIndices) == len(axisValues), (
len(axisIndices),
len(axisValues),
)
location = {
fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues)
}
if comp.transformVarIndex != NO_VARIATION_INDEX:
deltas = instancer[comp.transformVarIndex]
comp = deepcopy(comp)
comp.applyTransformDeltas(deltas)
transform = comp.transform
reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
with self.glyphSet.glyphSet.pushLocation(location, reset):
with self.glyphSet.pushLocation(location, reset):
shouldDecompose = self.name == comp.glyphName
if not shouldDecompose:
try:
pen.addVarComponent(
comp.glyphName, transform, self.glyphSet.rawLocation
)
except AttributeError:
shouldDecompose = True
if shouldDecompose:
t = transform.toTransform()
compGlyphSet = (
self.glyphSet
if comp.glyphName != self.name
else glyphSet.glyphSet
)
g = compGlyphSet[comp.glyphName]
if isPointPen:
tPen = TransformPointPen(pen, t)
g.drawPoints(tPen)
else:
tPen = TransformPen(pen, t)
g.draw(tPen)
def draw(self, pen):
self._draw(pen, False)
def drawPoints(self, pen):
self._draw(pen, True)
def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
# Handle phantom points for (left, right, top, bottom) positions. # Handle phantom points for (left, right, top, bottom) positions.
assert len(coord) >= 4 assert len(coord) >= 4
@ -300,11 +415,6 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
for p, comp in zip(coord, glyph.components): for p, comp in zip(coord, glyph.components):
if hasattr(comp, "x"): if hasattr(comp, "x"):
comp.x, comp.y = p comp.x, comp.y = p
elif glyph.isVarComposite():
glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy
for comp in glyph.components:
coord = comp.setCoordinates(coord)
assert not coord
elif glyph.numberOfContours == 0: elif glyph.numberOfContours == 0:
assert len(coord) == 0 assert len(coord) == 0
else: else:

View File

@ -1017,8 +1017,6 @@ class WOFF2GlyfTable(getTableClass("glyf")):
return return
elif glyph.isComposite(): elif glyph.isComposite():
self._encodeComponents(glyph) self._encodeComponents(glyph)
elif glyph.isVarComposite():
raise NotImplementedError
else: else:
self._encodeCoordinates(glyph) self._encodeCoordinates(glyph)
self._encodeOverlapSimpleFlag(glyph, glyphID) self._encodeOverlapSimpleFlag(glyph, glyphID)

View File

@ -10,6 +10,13 @@ def buildVarRegionAxis(axisSupport):
return self return self
def buildSparseVarRegionAxis(axisIndex, axisSupport):
self = ot.SparseVarRegionAxis()
self.AxisIndex = axisIndex
self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport]
return self
def buildVarRegion(support, axisTags): def buildVarRegion(support, axisTags):
assert all(tag in axisTags for tag in support.keys()), ( assert all(tag in axisTags for tag in support.keys()), (
"Unknown axis tag found.", "Unknown axis tag found.",
@ -23,6 +30,24 @@ def buildVarRegion(support, axisTags):
return self return self
def buildSparseVarRegion(support, axisTags):
assert all(tag in axisTags for tag in support.keys()), (
"Unknown axis tag found.",
support,
axisTags,
)
self = ot.SparseVarRegion()
self.SparseVarRegionAxis = []
for i, tag in enumerate(axisTags):
if tag not in support:
continue
self.SparseVarRegionAxis.append(
buildSparseVarRegionAxis(i, support.get(tag, (0, 0, 0)))
)
self.SparseRegionCount = len(self.SparseVarRegionAxis)
return self
def buildVarRegionList(supports, axisTags): def buildVarRegionList(supports, axisTags):
self = ot.VarRegionList() self = ot.VarRegionList()
self.RegionAxisCount = len(axisTags) self.RegionAxisCount = len(axisTags)
@ -33,6 +58,16 @@ def buildVarRegionList(supports, axisTags):
return self return self
def buildSparseVarRegionList(supports, axisTags):
self = ot.SparseVarRegionList()
self.RegionAxisCount = len(axisTags)
self.Region = []
for support in supports:
self.Region.append(buildSparseVarRegion(support, axisTags))
self.RegionCount = len(self.Region)
return self
def _reorderItem(lst, mapping): def _reorderItem(lst, mapping):
return [lst[i] for i in mapping] return [lst[i] for i in mapping]
@ -130,6 +165,29 @@ def buildVarStore(varRegionList, varDataList):
return self return self
def buildMultiVarData(varRegionIndices, items):
self = ot.MultiVarData()
self.Format = 1
self.VarRegionIndex = list(varRegionIndices)
regionCount = self.VarRegionCount = len(self.VarRegionIndex)
records = self.Item = []
if items:
for item in items:
assert len(item) == regionCount
records.append(list(item))
self.ItemCount = len(self.Item)
return self
def buildMultiVarStore(varRegionList, multiVarDataList):
self = ot.MultiVarStore()
self.Format = 1
self.SparseVarRegionList = varRegionList
self.MultiVarData = list(multiVarDataList)
self.MultiVarDataCount = len(self.MultiVarData)
return self
# Variation helpers # Variation helpers

View File

@ -111,6 +111,7 @@ from fontTools.varLib.instancer import names
from .featureVars import instantiateFeatureVariations from .featureVars import instantiateFeatureVariations
from fontTools.misc.cliTools import makeOutputFileName from fontTools.misc.cliTools import makeOutputFileName
from fontTools.varLib.instancer import solver from fontTools.varLib.instancer import solver
from fontTools.ttLib.tables.otTables import VarComponentFlags
import collections import collections
import dataclasses import dataclasses
from contextlib import contextmanager from contextlib import contextmanager
@ -465,6 +466,42 @@ class OverlapMode(IntEnum):
REMOVE_AND_IGNORE_ERRORS = 3 REMOVE_AND_IGNORE_ERRORS = 3
def instantiateVARC(varfont, axisLimits):
log.info("Instantiating VARC tables")
# TODO(behdad) My confidence in this function is rather low;
# It needs more testing. Specially with partial-instancing,
# I don't think it currently works.
varc = varfont["VARC"].table
fvarAxes = varfont["fvar"].axes if "fvar" in varfont else []
location = axisLimits.pinnedLocation()
axisMap = [i for i, axis in enumerate(fvarAxes) if axis.axisTag not in location]
reverseAxisMap = {i: j for j, i in enumerate(axisMap)}
if varc.AxisIndicesList:
axisIndicesList = varc.AxisIndicesList.Item
for i, axisIndices in enumerate(axisIndicesList):
if any(fvarAxes[j].axisTag in axisLimits for j in axisIndices):
raise NotImplementedError(
"Instancing across VarComponent axes is not supported."
)
axisIndicesList[i] = [reverseAxisMap[j] for j in axisIndices]
store = varc.MultiVarStore
if store:
for region in store.SparseVarRegionList.Region:
newRegionAxis = []
for regionRecord in region.SparseVarRegionAxis:
tag = fvarAxes[regionRecord.AxisIndex].axisTag
if tag in axisLimits:
raise NotImplementedError(
"Instancing across VarComponent axes is not supported."
)
regionRecord.AxisIndex = reverseAxisMap[regionRecord.AxisIndex]
def instantiateTupleVariationStore( def instantiateTupleVariationStore(
variations, axisLimits, origCoords=None, endPts=None variations, axisLimits, origCoords=None, endPts=None
): ):
@ -843,23 +880,6 @@ def _instantiateGvarGlyph(
if defaultDeltas: if defaultDeltas:
coordinates += _g_l_y_f.GlyphCoordinates(defaultDeltas) coordinates += _g_l_y_f.GlyphCoordinates(defaultDeltas)
glyph = glyf[glyphname]
if glyph.isVarComposite():
for component in glyph.components:
newLocation = {}
for tag, loc in component.location.items():
if tag not in axisLimits:
newLocation[tag] = loc
continue
if component.flags & _g_l_y_f.VarComponentFlags.AXES_HAVE_VARIATION:
raise NotImplementedError(
"Instancing accross VarComposite axes with variation is not supported."
)
limits = axisLimits[tag]
loc = limits.renormalizeValue(loc, extrapolate=False)
newLocation[tag] = loc
component.location = newLocation
# _setCoordinates also sets the hmtx/vmtx advance widths and sidebearings from # _setCoordinates also sets the hmtx/vmtx advance widths and sidebearings from
# the four phantom points and glyph bounding boxes. # the four phantom points and glyph bounding boxes.
# We call it unconditionally even if a glyph has no variations or no deltas are # We call it unconditionally even if a glyph has no variations or no deltas are
@ -910,7 +930,7 @@ def instantiateGvar(varfont, axisLimits, optimize=True):
key=lambda name: ( key=lambda name: (
( (
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
if glyf[name].isComposite() or glyf[name].isVarComposite() if glyf[name].isComposite()
else 0 else 0
), ),
name, name,
@ -1598,6 +1618,9 @@ def instantiateVariableFont(
log.info("Updating name table") log.info("Updating name table")
names.updateNameTable(varfont, axisLimits) names.updateNameTable(varfont, axisLimits)
if "VARC" in varfont:
instantiateVARC(varfont, normalizedLimits)
if "CFF2" in varfont: if "CFF2" in varfont:
instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2) instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2)

View File

@ -75,7 +75,7 @@ def normalizeValue(v, triple, extrapolate=False):
return (v - default) / (upper - default) return (v - default) / (upper - default)
def normalizeLocation(location, axes, extrapolate=False): def normalizeLocation(location, axes, extrapolate=False, *, validate=False):
"""Normalizes location based on axis min/default/max values from axes. """Normalizes location based on axis min/default/max values from axes.
>>> axes = {"wght": (100, 400, 900)} >>> axes = {"wght": (100, 400, 900)}
@ -114,6 +114,10 @@ def normalizeLocation(location, axes, extrapolate=False):
>>> normalizeLocation({"wght": 1001}, axes) >>> normalizeLocation({"wght": 1001}, axes)
{'wght': 0.0} {'wght': 0.0}
""" """
if validate:
assert set(location.keys()) <= set(axes.keys()), set(location.keys()) - set(
axes.keys()
)
out = {} out = {}
for tag, triple in axes.items(): for tag, triple in axes.items():
v = location.get(tag, triple[1]) v = location.get(tag, triple[1])
@ -453,7 +457,10 @@ class VariationModel(object):
self.deltaWeights.append(deltaWeight) self.deltaWeights.append(deltaWeight)
def getDeltas(self, masterValues, *, round=noRound): def getDeltas(self, masterValues, *, round=noRound):
assert len(masterValues) == len(self.deltaWeights) assert len(masterValues) == len(self.deltaWeights), (
len(masterValues),
len(self.deltaWeights),
)
mapping = self.reverseMapping mapping = self.reverseMapping
out = [] out = []
for i, weights in enumerate(self.deltaWeights): for i, weights in enumerate(self.deltaWeights):

View File

@ -0,0 +1,253 @@
from fontTools.misc.roundTools import noRound, otRound
from fontTools.misc.intTools import bit_count
from fontTools.misc.vector import Vector
from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib.models import supportScalar
import fontTools.varLib.varStore # For monkey-patching
from fontTools.varLib.builder import (
buildVarRegionList,
buildSparseVarRegionList,
buildSparseVarRegion,
buildMultiVarStore,
buildMultiVarData,
)
from fontTools.misc.iterTools import batched
from functools import partial
from collections import defaultdict
from heapq import heappush, heappop
NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX
ot.MultiVarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX
def _getLocationKey(loc):
return tuple(sorted(loc.items(), key=lambda kv: kv[0]))
class OnlineMultiVarStoreBuilder(object):
def __init__(self, axisTags):
self._axisTags = axisTags
self._regionMap = {}
self._regionList = buildSparseVarRegionList([], axisTags)
self._store = buildMultiVarStore(self._regionList, [])
self._data = None
self._model = None
self._supports = None
self._varDataIndices = {}
self._varDataCaches = {}
self._cache = None
def setModel(self, model):
self.setSupports(model.supports)
self._model = model
def setSupports(self, supports):
self._model = None
self._supports = list(supports)
if not self._supports[0]:
del self._supports[0] # Drop base master support
self._cache = None
self._data = None
def finish(self, optimize=True):
self._regionList.RegionCount = len(self._regionList.Region)
self._store.MultiVarDataCount = len(self._store.MultiVarData)
return self._store
def _add_MultiVarData(self):
regionMap = self._regionMap
regionList = self._regionList
regions = self._supports
regionIndices = []
for region in regions:
key = _getLocationKey(region)
idx = regionMap.get(key)
if idx is None:
varRegion = buildSparseVarRegion(region, self._axisTags)
idx = regionMap[key] = len(regionList.Region)
regionList.Region.append(varRegion)
regionIndices.append(idx)
# Check if we have one already...
key = tuple(regionIndices)
varDataIdx = self._varDataIndices.get(key)
if varDataIdx is not None:
self._outer = varDataIdx
self._data = self._store.MultiVarData[varDataIdx]
self._cache = self._varDataCaches[key]
if len(self._data.Item) == 0xFFFF:
# This is full. Need new one.
varDataIdx = None
if varDataIdx is None:
self._data = buildMultiVarData(regionIndices, [])
self._outer = len(self._store.MultiVarData)
self._store.MultiVarData.append(self._data)
self._varDataIndices[key] = self._outer
if key not in self._varDataCaches:
self._varDataCaches[key] = {}
self._cache = self._varDataCaches[key]
def storeMasters(self, master_values, *, round=round):
deltas = self._model.getDeltas(master_values, round=round)
base = deltas.pop(0)
return base, self.storeDeltas(deltas, round=noRound)
def storeDeltas(self, deltas, *, round=round):
deltas = tuple(round(d) for d in deltas)
if not any(deltas):
return NO_VARIATION_INDEX
deltas_tuple = tuple(tuple(d) for d in deltas)
if not self._data:
self._add_MultiVarData()
varIdx = self._cache.get(deltas_tuple)
if varIdx is not None:
return varIdx
inner = len(self._data.Item)
if inner == 0xFFFF:
# Full array. Start new one.
self._add_MultiVarData()
return self.storeDeltas(deltas, round=noRound)
self._data.addItem(deltas, round=noRound)
varIdx = (self._outer << 16) + inner
self._cache[deltas_tuple] = varIdx
return varIdx
def MultiVarData_addItem(self, deltas, *, round=round):
deltas = tuple(round(d) for d in deltas)
assert len(deltas) == self.VarRegionCount
values = []
for d in deltas:
values.extend(d)
self.Item.append(values)
self.ItemCount = len(self.Item)
ot.MultiVarData.addItem = MultiVarData_addItem
def SparseVarRegion_get_support(self, fvar_axes):
return {
fvar_axes[reg.AxisIndex].axisTag: (reg.StartCoord, reg.PeakCoord, reg.EndCoord)
for reg in self.SparseVarRegionAxis
}
ot.SparseVarRegion.get_support = SparseVarRegion_get_support
def MultiVarStore___bool__(self):
return bool(self.MultiVarData)
ot.MultiVarStore.__bool__ = MultiVarStore___bool__
class MultiVarStoreInstancer(object):
def __init__(self, multivarstore, fvar_axes, location={}):
self.fvar_axes = fvar_axes
assert multivarstore is None or multivarstore.Format == 1
self._varData = multivarstore.MultiVarData if multivarstore else []
self._regions = (
multivarstore.SparseVarRegionList.Region if multivarstore else []
)
self.setLocation(location)
def setLocation(self, location):
self.location = dict(location)
self._clearCaches()
def _clearCaches(self):
self._scalars = {}
def _getScalar(self, regionIdx):
scalar = self._scalars.get(regionIdx)
if scalar is None:
support = self._regions[regionIdx].get_support(self.fvar_axes)
scalar = supportScalar(self.location, support)
self._scalars[regionIdx] = scalar
return scalar
@staticmethod
def interpolateFromDeltasAndScalars(deltas, scalars):
if not deltas:
return Vector([])
assert len(deltas) % len(scalars) == 0, (len(deltas), len(scalars))
m = len(deltas) // len(scalars)
delta = Vector([0] * m)
for d, s in zip(batched(deltas, m), scalars):
if not s:
continue
delta += Vector(d) * s
return delta
def __getitem__(self, varidx):
major, minor = varidx >> 16, varidx & 0xFFFF
if varidx == NO_VARIATION_INDEX:
return Vector([])
varData = self._varData
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
deltas = varData[major].Item[minor]
return self.interpolateFromDeltasAndScalars(deltas, scalars)
def interpolateFromDeltas(self, varDataIndex, deltas):
varData = self._varData
scalars = [self._getScalar(ri) for ri in varData[varDataIndex].VarRegionIndex]
return self.interpolateFromDeltasAndScalars(deltas, scalars)
def MultiVarStore_subset_varidxes(self, varIdxes):
return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData")
def MultiVarStore_prune_regions(self):
return ot.VarStore.prune_regions(
self, VarData="MultiVarData", VarRegionList="SparseVarRegionList"
)
ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions
ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes
def MultiVarStore_get_supports(self, major, fvarAxes):
supports = []
varData = self.MultiVarData[major]
for regionIdx in varData.VarRegionIndex:
region = self.SparseVarRegionList.Region[regionIdx]
support = region.get_support(fvarAxes)
supports.append(support)
return supports
ot.MultiVarStore.get_supports = MultiVarStore_get_supports
def VARC_collect_varidxes(self, varidxes):
for glyph in self.VarCompositeGlyphs.VarCompositeGlyph:
for component in glyph.components:
varidxes.add(component.axisValuesVarIndex)
varidxes.add(component.transformVarIndex)
def VARC_remap_varidxes(self, varidxes_map):
for glyph in self.VarCompositeGlyphs.VarCompositeGlyph:
for component in glyph.components:
component.axisValuesVarIndex = varidxes_map[component.axisValuesVarIndex]
component.transformVarIndex = varidxes_map[component.transformVarIndex]
ot.VARC.collect_varidxes = VARC_collect_varidxes
ot.VARC.remap_varidxes = VARC_remap_varidxes

View File

@ -201,7 +201,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
key=lambda name: ( key=lambda name: (
( (
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
if glyf[name].isComposite() or glyf[name].isVarComposite() if glyf[name].isComposite()
else 0 else 0
), ),
name, name,

View File

@ -32,7 +32,7 @@ class OnlineVarStoreBuilder(object):
self._supports = None self._supports = None
self._varDataIndices = {} self._varDataIndices = {}
self._varDataCaches = {} self._varDataCaches = {}
self._cache = {} self._cache = None
def setModel(self, model): def setModel(self, model):
self.setSupports(model.supports) self.setSupports(model.supports)
@ -43,7 +43,7 @@ class OnlineVarStoreBuilder(object):
self._supports = list(supports) self._supports = list(supports)
if not self._supports[0]: if not self._supports[0]:
del self._supports[0] # Drop base master support del self._supports[0] # Drop base master support
self._cache = {} self._cache = None
self._data = None self._data = None
def finish(self, optimize=True): def finish(self, optimize=True):
@ -54,7 +54,7 @@ class OnlineVarStoreBuilder(object):
data.calculateNumShorts(optimize=optimize) data.calculateNumShorts(optimize=optimize)
return self._store return self._store
def _add_VarData(self): def _add_VarData(self, num_items=1):
regionMap = self._regionMap regionMap = self._regionMap
regionList = self._regionList regionList = self._regionList
@ -76,7 +76,7 @@ class OnlineVarStoreBuilder(object):
self._outer = varDataIdx self._outer = varDataIdx
self._data = self._store.VarData[varDataIdx] self._data = self._store.VarData[varDataIdx]
self._cache = self._varDataCaches[key] self._cache = self._varDataCaches[key]
if len(self._data.Item) == 0xFFFF: if len(self._data.Item) + num_items > 0xFFFF:
# This is full. Need new one. # This is full. Need new one.
varDataIdx = None varDataIdx = None
@ -94,6 +94,14 @@ class OnlineVarStoreBuilder(object):
base = deltas.pop(0) base = deltas.pop(0)
return base, self.storeDeltas(deltas, round=noRound) return base, self.storeDeltas(deltas, round=noRound)
def storeMastersMany(self, master_values_list, *, round=round):
deltas_list = [
self._model.getDeltas(master_values, round=round)
for master_values in master_values_list
]
base_list = [deltas.pop(0) for deltas in deltas_list]
return base_list, self.storeDeltasMany(deltas_list, round=noRound)
def storeDeltas(self, deltas, *, round=round): def storeDeltas(self, deltas, *, round=round):
deltas = [round(d) for d in deltas] deltas = [round(d) for d in deltas]
if len(deltas) == len(self._supports) + 1: if len(deltas) == len(self._supports) + 1:
@ -102,23 +110,51 @@ class OnlineVarStoreBuilder(object):
assert len(deltas) == len(self._supports) assert len(deltas) == len(self._supports)
deltas = tuple(deltas) deltas = tuple(deltas)
if not self._data:
self._add_VarData()
varIdx = self._cache.get(deltas) varIdx = self._cache.get(deltas)
if varIdx is not None: if varIdx is not None:
return varIdx return varIdx
if not self._data:
self._add_VarData()
inner = len(self._data.Item) inner = len(self._data.Item)
if inner == 0xFFFF: if inner == 0xFFFF:
# Full array. Start new one. # Full array. Start new one.
self._add_VarData() self._add_VarData()
return self.storeDeltas(deltas) return self.storeDeltas(deltas, round=noRound)
self._data.addItem(deltas, round=noRound) self._data.addItem(deltas, round=noRound)
varIdx = (self._outer << 16) + inner varIdx = (self._outer << 16) + inner
self._cache[deltas] = varIdx self._cache[deltas] = varIdx
return varIdx return varIdx
def storeDeltasMany(self, deltas_list, *, round=round):
deltas_list = [[round(d) for d in deltas] for deltas in deltas_list]
deltas_list = tuple(tuple(deltas) for deltas in deltas_list)
if not self._data:
self._add_VarData(len(deltas_list))
varIdx = self._cache.get(deltas_list)
if varIdx is not None:
return varIdx
inner = len(self._data.Item)
if inner + len(deltas_list) > 0xFFFF:
# Full array. Start new one.
self._add_VarData(len(deltas_list))
return self.storeDeltasMany(deltas_list, round=noRound)
for i, deltas in enumerate(deltas_list):
self._data.addItem(deltas, round=noRound)
varIdx = (self._outer << 16) + inner + i
self._cache[deltas] = varIdx
varIdx = (self._outer << 16) + inner
self._cache[deltas_list] = varIdx
return varIdx
def VarData_addItem(self, deltas, *, round=round): def VarData_addItem(self, deltas, *, round=round):
deltas = [round(d) for d in deltas] deltas = [round(d) for d in deltas]
@ -210,26 +246,29 @@ class VarStoreInstancer(object):
def VarStore_subset_varidxes( def VarStore_subset_varidxes(
self, varIdxes, optimize=True, retainFirstMap=False, advIdxes=set() self,
varIdxes,
optimize=True,
retainFirstMap=False,
advIdxes=set(),
*,
VarData="VarData",
): ):
# Sort out used varIdxes by major/minor. # Sort out used varIdxes by major/minor.
used = {} used = defaultdict(set)
for varIdx in varIdxes: for varIdx in varIdxes:
if varIdx == NO_VARIATION_INDEX: if varIdx == NO_VARIATION_INDEX:
continue continue
major = varIdx >> 16 major = varIdx >> 16
minor = varIdx & 0xFFFF minor = varIdx & 0xFFFF
d = used.get(major) used[major].add(minor)
if d is None:
d = used[major] = set()
d.add(minor)
del varIdxes del varIdxes
# #
# Subset VarData # Subset VarData
# #
varData = self.VarData varData = getattr(self, VarData)
newVarData = [] newVarData = []
varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX} varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX}
for major, data in enumerate(varData): for major, data in enumerate(varData):
@ -260,10 +299,11 @@ def VarStore_subset_varidxes(
data.Item = newItems data.Item = newItems
data.ItemCount = len(data.Item) data.ItemCount = len(data.Item)
data.calculateNumShorts(optimize=optimize) if VarData == "VarData":
data.calculateNumShorts(optimize=optimize)
self.VarData = newVarData setattr(self, VarData, newVarData)
self.VarDataCount = len(self.VarData) setattr(self, VarData + "Count", len(newVarData))
self.prune_regions() self.prune_regions()
@ -273,7 +313,7 @@ def VarStore_subset_varidxes(
ot.VarStore.subset_varidxes = VarStore_subset_varidxes ot.VarStore.subset_varidxes = VarStore_subset_varidxes
def VarStore_prune_regions(self): def VarStore_prune_regions(self, *, VarData="VarData", VarRegionList="VarRegionList"):
"""Remove unused VarRegions.""" """Remove unused VarRegions."""
# #
# Subset VarRegionList # Subset VarRegionList
@ -281,10 +321,10 @@ def VarStore_prune_regions(self):
# Collect. # Collect.
usedRegions = set() usedRegions = set()
for data in self.VarData: for data in getattr(self, VarData):
usedRegions.update(data.VarRegionIndex) usedRegions.update(data.VarRegionIndex)
# Subset. # Subset.
regionList = self.VarRegionList regionList = getattr(self, VarRegionList)
regions = regionList.Region regions = regionList.Region
newRegions = [] newRegions = []
regionMap = {} regionMap = {}
@ -294,7 +334,7 @@ def VarStore_prune_regions(self):
regionList.Region = newRegions regionList.Region = newRegions
regionList.RegionCount = len(regionList.Region) regionList.RegionCount = len(regionList.Region)
# Map. # Map.
for data in self.VarData: for data in getattr(self, VarData):
data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex]

View File

@ -428,14 +428,14 @@ class SubsetTest:
def test_varComposite(self): def test_varComposite(self):
fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf") fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf")
origfont = TTFont(fontpath) origfont = TTFont(fontpath)
assert len(origfont.getGlyphOrder()) == 6 assert len(origfont.getGlyphOrder()) == 11
subsetpath = self.temp_path(".ttf") subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath]) subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath) subsetfont = TTFont(subsetpath)
assert len(subsetfont.getGlyphOrder()) == 4 assert len(subsetfont.getGlyphOrder()) == 6
subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath]) subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath) subsetfont = TTFont(subsetpath)
assert len(subsetfont.getGlyphOrder()) == 5 assert len(subsetfont.getGlyphOrder()) == 8
def test_timing_publishes_parts(self): def test_timing_publishes_parts(self):
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,6 @@
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
from fontTools.ttLib.scaleUpem import scale_upem from fontTools.ttLib.scaleUpem import scale_upem
from io import BytesIO
import difflib import difflib
import os import os
import shutil import shutil
@ -70,6 +71,12 @@ class ScaleUpemTest(unittest.TestCase):
scale_upem(font, 500) scale_upem(font, 500)
# Save / load to ensure calculated values are correct
# XXX This wans't needed before. So needs investigation.
iobytes = BytesIO()
font.save(iobytes)
# Just saving is enough to fix the numbers. Sigh...
expected_ttx_path = self.get_path("varc-ac00-ac01-500upem.ttx") expected_ttx_path = self.get_path("varc-ac00-ac01-500upem.ttx")
self.expect_ttx(font, expected_ttx_path, tables) self.expect_ttx(font, expected_ttx_path, tables)

View File

@ -0,0 +1,87 @@
from fontTools.ttLib import TTFont
from io import StringIO, BytesIO
import pytest
import os
import unittest
CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
DATA_DIR = os.path.join(CURR_DIR, "data")
class VarCompositeTest(unittest.TestCase):
def test_basic(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
varc = font["VARC"]
assert varc.table.Coverage.glyphs == [
"uniAC00",
"uniAC01",
"glyph00003",
"glyph00005",
"glyph00007",
"glyph00008",
"glyph00009",
]
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
varc = font["VARC"]
assert varc.table.Coverage.glyphs == [
"uni6868",
"glyph00002",
"glyph00005",
"glyph00007",
]
def test_roundtrip(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea", "name", "fvar"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
if __name__ == "__main__":
import sys
sys.exit(unittest.main())

View File

@ -719,65 +719,6 @@ class GlyphComponentTest:
assert (comp.firstPt, comp.secondPt) == (1, 2) assert (comp.firstPt, comp.secondPt) == (1, 2)
assert not hasattr(comp, "transform") assert not hasattr(comp, "transform")
def test_trim_varComposite_glyph(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
glyf = font["glyf"]
glyf.glyphs["uniAC00"].trim()
glyf.glyphs["uniAC01"].trim()
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
glyf = font["glyf"]
glyf.glyphs["uni6868"].trim()
def test_varComposite_basic(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea", "name", "fvar"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
class GlyphCubicTest: class GlyphCubicTest:
def test_roundtrip(self): def test_roundtrip(self):

View File

@ -427,9 +427,12 @@ class AATLookupTest(unittest.TestCase):
) )
from fontTools.misc.lazyTools import LazyList
class LazyListTest(unittest.TestCase): class LazyListTest(unittest.TestCase):
def test_slice(self): def test_slice(self):
ll = otConverters._LazyList([10, 11, 12, 13]) ll = LazyList([10, 11, 12, 13])
sl = ll[:] sl = ll[:]
self.assertIsNot(sl, ll) self.assertIsNot(sl, ll)
@ -438,26 +441,9 @@ class LazyListTest(unittest.TestCase):
self.assertEqual([11, 12], ll[1:3]) self.assertEqual([11, 12], ll[1:3])
def test_getitem(self):
count = 2
reader = OTTableReader(b"\x00\xFE\xFF\x00\x00\x00", offset=1)
converter = otConverters.UInt8("UInt8", 0, None, None)
recordSize = converter.staticSize
l = otConverters._LazyList()
l.reader = reader
l.pos = l.reader.pos
l.font = None
l.conv = converter
l.recordSize = recordSize
l.extend(otConverters._MissingItem([i]) for i in range(count))
reader.advance(count * recordSize)
self.assertEqual(l[0], 254)
self.assertEqual(l[1], 255)
def test_add_both_LazyList(self): def test_add_both_LazyList(self):
ll1 = otConverters._LazyList([1]) ll1 = LazyList([1])
ll2 = otConverters._LazyList([2]) ll2 = LazyList([2])
l3 = ll1 + ll2 l3 = ll1 + ll2
@ -465,7 +451,7 @@ class LazyListTest(unittest.TestCase):
self.assertEqual([1, 2], l3) self.assertEqual([1, 2], l3)
def test_add_LazyList_and_list(self): def test_add_LazyList_and_list(self):
ll1 = otConverters._LazyList([1]) ll1 = LazyList([1])
l2 = [2] l2 = [2]
l3 = ll1 + l2 l3 = ll1 + l2
@ -475,13 +461,13 @@ class LazyListTest(unittest.TestCase):
def test_add_not_implemented(self): def test_add_not_implemented(self):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
otConverters._LazyList() + 0 LazyList() + 0
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
otConverters._LazyList() + tuple() LazyList() + tuple()
def test_radd_list_and_LazyList(self): def test_radd_list_and_LazyList(self):
l1 = [1] l1 = [1]
ll2 = otConverters._LazyList([2]) ll2 = LazyList([2])
l3 = l1 + ll2 l3 = l1 + ll2
@ -490,9 +476,9 @@ class LazyListTest(unittest.TestCase):
def test_radd_not_implemented(self): def test_radd_not_implemented(self):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
0 + otConverters._LazyList() 0 + LazyList()
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
tuple() + otConverters._LazyList() tuple() + LazyList()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -227,33 +227,57 @@ class TTGlyphSetTest(object):
"addVarComponent", "addVarComponent",
( (
"glyph00003", "glyph00003",
DecomposedTransform(460.0, 676.0, 0, 1, 1, 0, 0, 0, 0), DecomposedTransform(
{ translateX=0,
"0000": 0.84661865234375, translateY=0,
"0001": 0.98944091796875, rotation=0,
"0002": 0.47283935546875, scaleX=1,
"0003": 0.446533203125, scaleY=1,
}, skewX=0,
skewY=0,
tCenterX=0,
tCenterY=0,
),
{},
), ),
), ),
( (
"addVarComponent", "addVarComponent",
( (
"glyph00004", "glyph00005",
DecomposedTransform(932.0, 382.0, 0, 1, 1, 0, 0, 0, 0), DecomposedTransform(
{ translateX=0,
"0000": 0.93359375, translateY=0,
"0001": 0.916015625, rotation=0,
"0002": 0.523193359375, scaleX=1,
"0003": 0.32806396484375, scaleY=1,
"0004": 0.85089111328125, skewX=0,
}, skewY=0,
tCenterX=0,
tCenterY=0,
),
{},
), ),
), ),
] ]
assert actual == expected, (actual, expected) assert actual == expected, (actual, expected)
def test_glyphset_varComposite_conditional(self):
font = TTFont(self.getpath("varc-ac01-conditional.ttf"))
glyphset = font.getGlyphSet()
pen = RecordingPen()
glyph = glyphset["uniAC01"]
glyph.draw(pen)
assert len(pen.value) == 2
glyphset = font.getGlyphSet(location={"wght": 800})
pen = RecordingPen()
glyph = glyphset["uniAC01"]
glyph.draw(pen)
assert len(pen.value) == 3
def test_glyphset_varComposite1(self): def test_glyphset_varComposite1(self):
font = TTFont(self.getpath("varc-ac00-ac01.ttf")) font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
glyphset = font.getGlyphSet(location={"wght": 600}) glyphset = font.getGlyphSet(location={"wght": 600})
@ -265,77 +289,24 @@ class TTGlyphSetTest(object):
actual = pen.value actual = pen.value
expected = [ expected = [
("moveTo", ((432, 678),)), ("moveTo", ((82, 108),)),
("lineTo", ((432, 620),)), ("qCurveTo", ((188, 138), (350, 240), (461, 384), (518, 567), (518, 678))),
( ("lineTo", ((518, 732),)),
"qCurveTo", ("lineTo", ((74, 732),)),
( ("lineTo", ((74, 630),)),
(419, 620), ("lineTo", ((456, 630),)),
(374, 621), ("lineTo", ((403, 660),)),
(324, 619), ("qCurveTo", ((403, 575), (358, 431), (267, 314), (128, 225), (34, 194))),
(275, 618),
(237, 617),
(228, 616),
),
),
("qCurveTo", ((218, 616), (188, 612), (160, 605), (149, 601))),
("qCurveTo", ((127, 611), (83, 639), (67, 654))),
("qCurveTo", ((64, 657), (63, 662), (64, 666))),
("lineTo", ((72, 678),)),
("qCurveTo", ((93, 674), (144, 672), (164, 672))),
(
"qCurveTo",
(
(173, 672),
(213, 672),
(266, 673),
(323, 674),
(377, 675),
(421, 678),
(432, 678),
),
),
("closePath", ()), ("closePath", ()),
("moveTo", ((525, 619),)), ("moveTo", ((702, 385),)),
("lineTo", ((412, 620),)), ("lineTo", ((897, 385),)),
("lineTo", ((429, 678),)), ("lineTo", ((897, 485),)),
("lineTo", ((466, 697),)), ("lineTo", ((702, 485),)),
("qCurveTo", ((470, 698), (482, 698), (486, 697))),
("qCurveTo", ((494, 693), (515, 682), (536, 670), (541, 667))),
("qCurveTo", ((545, 663), (545, 656), (543, 652))),
("lineTo", ((525, 619),)),
("closePath", ()), ("closePath", ()),
("moveTo", ((63, 118),)), ("moveTo", ((641, -92),)),
("lineTo", ((47, 135),)), ("lineTo", ((752, -92),)),
("qCurveTo", ((42, 141), (48, 146))), ("lineTo", ((752, 813),)),
("qCurveTo", ((135, 213), (278, 373), (383, 541), (412, 620))), ("lineTo", ((641, 813),)),
("lineTo", ((471, 642),)),
("lineTo", ((525, 619),)),
("qCurveTo", ((496, 529), (365, 342), (183, 179), (75, 121))),
("qCurveTo", ((72, 119), (65, 118), (63, 118))),
("closePath", ()),
("moveTo", ((925, 372),)),
("lineTo", ((739, 368),)),
("lineTo", ((739, 427),)),
("lineTo", ((822, 430),)),
("lineTo", ((854, 451),)),
("qCurveTo", ((878, 453), (930, 449), (944, 445))),
("qCurveTo", ((961, 441), (962, 426))),
("qCurveTo", ((964, 411), (956, 386), (951, 381))),
("qCurveTo", ((947, 376), (931, 372), (925, 372))),
("closePath", ()),
("moveTo", ((729, -113),)),
("lineTo", ((674, -113),)),
("qCurveTo", ((671, -98), (669, -42), (666, 22), (665, 83), (665, 102))),
("lineTo", ((665, 763),)),
("qCurveTo", ((654, 780), (608, 810), (582, 820))),
("lineTo", ((593, 850),)),
("qCurveTo", ((594, 852), (599, 856), (607, 856))),
("qCurveTo", ((628, 855), (684, 846), (736, 834), (752, 827))),
("qCurveTo", ((766, 818), (766, 802))),
("lineTo", ((762, 745),)),
("lineTo", ((762, 134),)),
("qCurveTo", ((762, 107), (757, 43), (749, -25), (737, -87), (729, -113))),
("closePath", ()), ("closePath", ()),
] ]
@ -530,7 +501,7 @@ class TTGlyphSetTest(object):
"qCurveTo", "qCurveTo",
( (
(919, 41), (919, 41),
(854, 67), (854, 68),
(790, 98), (790, 98),
(729, 134), (729, 134),
(671, 173), (671, 173),
@ -542,7 +513,7 @@ class TTGlyphSetTest(object):
("lineTo", ((522, 286),)), ("lineTo", ((522, 286),)),
("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))), ("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))),
("lineTo", ((515, 209),)), ("lineTo", ((515, 209),)),
("qCurveTo", ((569, 146), (695, 44), (835, -32), (913, -57))), ("qCurveTo", ((569, 146), (695, 45), (835, -32), (913, -57))),
("closePath", ()), ("closePath", ()),
("moveTo", ((474, 274),)), ("moveTo", ((474, 274),)),
("lineTo", ((452, 284),)), ("lineTo", ((452, 284),)),

View File

@ -1699,23 +1699,33 @@ class InstantiateVariableFontTest(object):
def test_varComposite(self): def test_varComposite(self):
input_path = os.path.join( input_path = os.path.join(
TESTDATA, "..", "..", "..", "ttLib", "data", "varc-ac00-ac01.ttf" TESTDATA, "..", "..", "..", "ttLib", "data", "varc-6868.ttf"
) )
varfont = ttLib.TTFont(input_path) varfont = ttLib.TTFont(input_path)
location = {"wght": 600} location = {"wght": 600}
instance = instancer.instantiateVariableFont( # We currently do not allow this either; although in theory
varfont, # it should be possible.
location, with pytest.raises(
) NotImplementedError,
match="is not supported.",
):
instance = instancer.instantiateVariableFont(
varfont,
location,
)
location = {"0000": 0.5} location = {"0000": 0.5}
instance = instancer.instantiateVariableFont( with pytest.raises(
varfont, NotImplementedError,
location, match="is not supported.",
) ):
instance = instancer.instantiateVariableFont(
varfont,
location,
)
def _conditionSetAsDict(conditionSet, axisOrder): def _conditionSetAsDict(conditionSet, axisOrder):