Merge pull request #3395 from fonttools/varc-table
[VARC] Variable Composites table
This commit is contained in:
commit
db60a248dc
@ -656,11 +656,7 @@ class FontBuilder(object):
|
||||
|
||||
if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
|
||||
for name, g in glyphs.items():
|
||||
if g.isVarComposite():
|
||||
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):
|
||||
if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
|
||||
raise ValueError(
|
||||
f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
|
||||
"either convert to quadratics with cu2qu or set glyphDataFormat=1."
|
||||
|
@ -225,7 +225,7 @@ def merge(self, m, tables):
|
||||
g.removeHinting()
|
||||
# Expand composite glyphs to load their
|
||||
# composite glyph names.
|
||||
if g.isComposite() or g.isVarComposite():
|
||||
if g.isComposite():
|
||||
g.expand(table)
|
||||
return DefaultTable.merge(self, m, tables)
|
||||
|
||||
|
12
Lib/fontTools/misc/iterTools.py
Normal file
12
Lib/fontTools/misc/iterTools.py
Normal 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
|
42
Lib/fontTools/misc/lazyTools.py
Normal file
42
Lib/fontTools/misc/lazyTools.py
Normal 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)
|
@ -422,6 +422,19 @@ class DecomposedTransform:
|
||||
tCenterX: 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
|
||||
def fromTransform(self, transform):
|
||||
# Adapted from an answer on
|
||||
|
@ -2,7 +2,7 @@ from typing import Callable
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
def pointToString(pt, ntos=str):
|
||||
def pointToString(pt, ntos):
|
||||
return " ".join(ntos(i) for i in pt)
|
||||
|
||||
|
||||
@ -37,7 +37,13 @@ class SVGPathPen(BasePen):
|
||||
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)
|
||||
self._commands = []
|
||||
self._lastCommand = None
|
||||
|
@ -14,7 +14,7 @@ from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.subset.util import _add_method, _uniq_sort
|
||||
from fontTools.subset.cff 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
|
||||
import sys
|
||||
import struct
|
||||
@ -2630,6 +2630,88 @@ def closure_glyphs(self, s):
|
||||
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"))
|
||||
def closure_glyphs(self, s):
|
||||
if self.table.MathVariants:
|
||||
@ -3298,20 +3380,6 @@ class Subsetter(object):
|
||||
self.glyphs.add(font.getGlyphName(i))
|
||||
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:
|
||||
with timer("close glyph list over 'MATH'"):
|
||||
log.info(
|
||||
@ -3326,6 +3394,20 @@ class Subsetter(object):
|
||||
log.glyphs(self.glyphs, font=font)
|
||||
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"):
|
||||
if table in font:
|
||||
with timer("close glyph list over '%s'" % table):
|
||||
@ -3345,6 +3427,20 @@ class Subsetter(object):
|
||||
log.glyphs(self.glyphs, font=font)
|
||||
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:
|
||||
with timer("close glyph list over 'glyf'"):
|
||||
log.info(
|
||||
|
@ -10,8 +10,10 @@ import fontTools.ttLib.tables.otTables as otTables
|
||||
from fontTools.cffLib import VarStoreData
|
||||
import fontTools.cffLib.specializer as cffSpecializer
|
||||
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.ttLib.tables._g_l_y_f import VarComponentFlags
|
||||
from fontTools.misc.iterTools import batched
|
||||
|
||||
|
||||
__all__ = ["scale_upem", "ScalerVisitor"]
|
||||
@ -123,13 +125,6 @@ def visit(visitor, obj, attr, glyphs):
|
||||
component.y = visitor.scale(component.y)
|
||||
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"):
|
||||
coordinates = g.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")
|
||||
def visit(visitor, obj, attr, variations):
|
||||
# VarComposites are a pain to handle :-(
|
||||
glyfTable = visitor.font["glyf"]
|
||||
|
||||
for glyphName, varlist in variations.items():
|
||||
glyph = glyfTable[glyphName]
|
||||
isVarComposite = glyph.isVarComposite()
|
||||
for var in varlist:
|
||||
coordinates = var.coordinates
|
||||
|
||||
if not isVarComposite:
|
||||
for i, xy in enumerate(coordinates):
|
||||
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]
|
||||
for i, xy in enumerate(coordinates):
|
||||
if xy is None:
|
||||
continue
|
||||
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")
|
||||
def visit(visitor, obj, attr, kernTables):
|
||||
for table in kernTables:
|
||||
|
@ -22,6 +22,8 @@ PRIVATE_POINT_NUMBERS = 0x2000
|
||||
|
||||
DELTAS_ARE_ZERO = 0x80
|
||||
DELTAS_ARE_WORDS = 0x40
|
||||
DELTAS_ARE_LONGS = 0xC0
|
||||
DELTAS_SIZE_MASK = 0xC0
|
||||
DELTA_RUN_COUNT_MASK = 0x3F
|
||||
|
||||
POINTS_ARE_WORDS = 0x80
|
||||
@ -366,8 +368,10 @@ class TupleVariation(object):
|
||||
pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
|
||||
elif -128 <= value <= 127:
|
||||
pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr)
|
||||
else:
|
||||
elif -32768 <= value <= 32767:
|
||||
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
|
||||
else:
|
||||
pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr)
|
||||
return bytearr
|
||||
|
||||
@staticmethod
|
||||
@ -420,6 +424,7 @@ class TupleVariation(object):
|
||||
numDeltas = len(deltas)
|
||||
while pos < numDeltas:
|
||||
value = deltas[pos]
|
||||
|
||||
# Within a word-encoded run of deltas, it is easiest
|
||||
# to start a new run (with a different encoding)
|
||||
# whenever we encounter a zero value. For example,
|
||||
@ -442,6 +447,10 @@ class TupleVariation(object):
|
||||
and (-128 <= deltas[pos + 1] <= 127)
|
||||
):
|
||||
break
|
||||
|
||||
if not (-32768 <= value <= 32767):
|
||||
break
|
||||
|
||||
pos += 1
|
||||
runLength = pos - offset
|
||||
while runLength >= 64:
|
||||
@ -461,18 +470,47 @@ class TupleVariation(object):
|
||||
return pos
|
||||
|
||||
@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)"""
|
||||
result = []
|
||||
pos = offset
|
||||
while len(result) < numDeltas:
|
||||
while len(result) < numDeltas if numDeltas is not None else pos < len(data):
|
||||
runHeader = data[pos]
|
||||
pos += 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)
|
||||
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")
|
||||
deltasSize = numDeltasInRun * 2
|
||||
else:
|
||||
@ -481,10 +519,10 @@ class TupleVariation(object):
|
||||
deltas.frombytes(data[pos : pos + deltasSize])
|
||||
if sys.byteorder != "big":
|
||||
deltas.byteswap()
|
||||
assert len(deltas) == numDeltasInRun
|
||||
assert len(deltas) == numDeltasInRun, (len(deltas), numDeltasInRun)
|
||||
pos += deltasSize
|
||||
result.extend(deltas)
|
||||
assert len(result) == numDeltas
|
||||
assert numDeltas is None or len(result) == numDeltas
|
||||
return (result, pos)
|
||||
|
||||
@staticmethod
|
||||
|
5
Lib/fontTools/ttLib/tables/V_A_R_C_.py
Normal file
5
Lib/fontTools/ttLib/tables/V_A_R_C_.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .otBase import BaseTTXConverter
|
||||
|
||||
|
||||
class table_V_A_R_C_(BaseTTXConverter):
|
||||
pass
|
@ -424,29 +424,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
|
||||
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:
|
||||
coords, endPts, flags = glyph.getCoordinates(self)
|
||||
coords = coords.copy()
|
||||
@ -492,10 +469,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
|
||||
for p, comp in zip(coord, glyph.components):
|
||||
if hasattr(comp, "x"):
|
||||
comp.x, comp.y = p
|
||||
elif glyph.isVarComposite():
|
||||
for comp in glyph.components:
|
||||
coord = comp.setCoordinates(coord)
|
||||
assert not coord
|
||||
elif glyph.numberOfContours == 0:
|
||||
assert len(coord) == 0
|
||||
else:
|
||||
@ -737,8 +710,6 @@ class Glyph(object):
|
||||
return
|
||||
if self.isComposite():
|
||||
self.decompileComponents(data, glyfTable)
|
||||
elif self.isVarComposite():
|
||||
self.decompileVarComponents(data, glyfTable)
|
||||
else:
|
||||
self.decompileCoordinates(data)
|
||||
|
||||
@ -758,8 +729,6 @@ class Glyph(object):
|
||||
data = sstruct.pack(glyphHeaderFormat, self)
|
||||
if self.isComposite():
|
||||
data = data + self.compileComponents(glyfTable)
|
||||
elif self.isVarComposite():
|
||||
data = data + self.compileVarComponents(glyfTable)
|
||||
else:
|
||||
data = data + self.compileCoordinates()
|
||||
return data
|
||||
@ -769,10 +738,6 @@ class Glyph(object):
|
||||
for compo in self.components:
|
||||
compo.toXML(writer, ttFont)
|
||||
haveInstructions = hasattr(self, "program")
|
||||
elif self.isVarComposite():
|
||||
for compo in self.components:
|
||||
compo.toXML(writer, ttFont)
|
||||
haveInstructions = False
|
||||
else:
|
||||
last = 0
|
||||
for i in range(self.numberOfContours):
|
||||
@ -842,15 +807,6 @@ class Glyph(object):
|
||||
component = GlyphComponent()
|
||||
self.components.append(component)
|
||||
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":
|
||||
self.program = ttProgram.Program()
|
||||
for element in content:
|
||||
@ -860,7 +816,7 @@ class Glyph(object):
|
||||
self.program.fromXML(name, attrs, content, ttFont)
|
||||
|
||||
def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
|
||||
assert self.isComposite() or self.isVarComposite()
|
||||
assert self.isComposite()
|
||||
nContours = 0
|
||||
nPoints = 0
|
||||
initialMaxComponentDepth = maxComponentDepth
|
||||
@ -904,13 +860,6 @@ class Glyph(object):
|
||||
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):
|
||||
endPtsOfContours = array.array("H")
|
||||
endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
|
||||
@ -1027,9 +976,6 @@ class Glyph(object):
|
||||
data = data + struct.pack(">h", len(instructions)) + instructions
|
||||
return data
|
||||
|
||||
def compileVarComponents(self, glyfTable):
|
||||
return b"".join(c.compile(glyfTable) for c in self.components)
|
||||
|
||||
def compileCoordinates(self):
|
||||
assert len(self.coordinates) == len(self.flags)
|
||||
data = []
|
||||
@ -1231,13 +1177,6 @@ class Glyph(object):
|
||||
else:
|
||||
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):
|
||||
"""Return the coordinates, end points and flags
|
||||
|
||||
@ -1308,8 +1247,6 @@ class Glyph(object):
|
||||
allCoords.extend(coordinates)
|
||||
allFlags.extend(flags)
|
||||
return allCoords, allEndPts, allFlags
|
||||
elif self.isVarComposite():
|
||||
raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs")
|
||||
else:
|
||||
return GlyphCoordinates(), [], bytearray()
|
||||
|
||||
@ -1319,12 +1256,8 @@ class Glyph(object):
|
||||
This method can be used on simple glyphs (in which case it returns an
|
||||
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 self.isComposite() or self.isVarComposite():
|
||||
if self.isComposite():
|
||||
return [c.glyphName for c in self.components]
|
||||
else:
|
||||
return []
|
||||
@ -1367,8 +1300,6 @@ class Glyph(object):
|
||||
if self.isComposite():
|
||||
if hasattr(self, "program"):
|
||||
del self.program
|
||||
elif self.isVarComposite():
|
||||
pass # Doesn't have hinting
|
||||
else:
|
||||
self.program = ttProgram.Program()
|
||||
self.program.fromBytecode([])
|
||||
@ -1450,13 +1381,6 @@ class Glyph(object):
|
||||
i += 2 + instructionLen
|
||||
# Remove padding
|
||||
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
|
||||
|
||||
@ -1942,391 +1866,6 @@ class GlyphComponent(object):
|
||||
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):
|
||||
"""A list of glyph coordinates.
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
from collections import UserDict, deque
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from fontTools.misc import sstruct
|
||||
from fontTools.misc.textTools import safeEval
|
||||
from fontTools.misc.lazyTools import LazyDict
|
||||
from . import DefaultTable
|
||||
import array
|
||||
import itertools
|
||||
@ -39,19 +40,6 @@ 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):
|
||||
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)
|
||||
assert len(glyphs) == self.glyphCount
|
||||
assert len(axisTags) == self.axisCount
|
||||
offsets = self.decompileOffsets_(
|
||||
data[GVAR_HEADER_SIZE:],
|
||||
tableFormat=(self.flags & 1),
|
||||
glyphCount=self.glyphCount,
|
||||
)
|
||||
sharedCoords = tv.decompileSharedTuples(
|
||||
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
|
||||
)
|
||||
@ -128,20 +111,35 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
|
||||
offsetToData = self.offsetToGlyphVariationData
|
||||
glyf = ttFont["glyf"]
|
||||
|
||||
def decompileVarGlyph(glyphName, gid):
|
||||
gvarData = data[
|
||||
offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
|
||||
]
|
||||
if not gvarData:
|
||||
return []
|
||||
glyph = glyf[glyphName]
|
||||
numPointsInGlyph = self.getNumPoints_(glyph)
|
||||
return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
|
||||
def get_read_item():
|
||||
reverseGlyphMap = ttFont.getReverseGlyphMap()
|
||||
tableFormat = self.flags & 1
|
||||
|
||||
for gid in range(self.glyphCount):
|
||||
glyphName = glyphs[gid]
|
||||
variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
|
||||
self.variations = _LazyDict(variations)
|
||||
def read_item(glyphName):
|
||||
gid = reverseGlyphMap[glyphName]
|
||||
offsetSize = 2 if tableFormat == 0 else 4
|
||||
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
|
||||
self.ensureDecompiled()
|
||||
@ -245,11 +243,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
|
||||
|
||||
if glyph.isComposite():
|
||||
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:
|
||||
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
|
||||
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
|
||||
|
@ -21,10 +21,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable):
|
||||
if sys.byteorder != "big":
|
||||
locations.byteswap()
|
||||
if not longFormat:
|
||||
l = array.array("I")
|
||||
for i in range(len(locations)):
|
||||
l.append(locations[i] * 2)
|
||||
locations = l
|
||||
locations = array.array("I", (2 * l for l in locations))
|
||||
if len(locations) < (ttFont["maxp"].numGlyphs + 1):
|
||||
log.warning(
|
||||
"corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d",
|
||||
|
@ -1146,7 +1146,10 @@ class BaseTable(object):
|
||||
except KeyError:
|
||||
raise # XXX on KeyError, raise nice error
|
||||
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)
|
||||
if seq is None:
|
||||
seq = []
|
||||
|
@ -6,8 +6,10 @@ from fontTools.misc.fixedTools import (
|
||||
ensureVersionIsLong as fi2ve,
|
||||
versionToFixed as ve2fi,
|
||||
)
|
||||
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
||||
from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
|
||||
from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
|
||||
from fontTools.misc.lazyTools import LazyList
|
||||
from fontTools.ttLib import getSearchRange
|
||||
from .otBase import (
|
||||
CountReference,
|
||||
@ -18,6 +20,7 @@ from .otBase import (
|
||||
)
|
||||
from .otTables import (
|
||||
lookupTypes,
|
||||
VarCompositeGlyph,
|
||||
AATStateTable,
|
||||
AATState,
|
||||
AATAction,
|
||||
@ -29,8 +32,9 @@ from .otTables import (
|
||||
CompositeMode as _CompositeMode,
|
||||
NO_VARIATION_INDEX,
|
||||
)
|
||||
from itertools import zip_longest
|
||||
from itertools import zip_longest, accumulate
|
||||
from functools import partial
|
||||
from types import SimpleNamespace
|
||||
import re
|
||||
import struct
|
||||
from typing import Optional
|
||||
@ -78,7 +82,7 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
conv = converterClass(name, repeat, aux, description=descr)
|
||||
|
||||
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
|
||||
elif tp in ("MortChain", "MortSubtable", "MorxChain"):
|
||||
tableClass = tableNamespace.get(tp)
|
||||
@ -105,46 +109,6 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
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):
|
||||
"""Base class for converter objects. Apart from the constructor, this
|
||||
is an abstract class."""
|
||||
@ -176,6 +140,7 @@ class BaseConverter(object):
|
||||
"AxisCount",
|
||||
"BaseGlyphRecordCount",
|
||||
"LayerRecordCount",
|
||||
"AxisIndicesList",
|
||||
]
|
||||
self.description = description
|
||||
|
||||
@ -192,14 +157,21 @@ class BaseConverter(object):
|
||||
l.append(self.read(reader, font, tableDict))
|
||||
return l
|
||||
else:
|
||||
l = _LazyList()
|
||||
l.reader = reader.copy()
|
||||
l.pos = l.reader.pos
|
||||
l.font = font
|
||||
l.conv = self
|
||||
l.recordSize = recordSize
|
||||
l.extend(_MissingItem([i]) for i in range(count))
|
||||
|
||||
def get_read_item():
|
||||
reader_copy = reader.copy()
|
||||
pos = reader.pos
|
||||
|
||||
def read_item(i):
|
||||
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)
|
||||
|
||||
return l
|
||||
|
||||
def getRecordSize(self, reader):
|
||||
@ -1833,6 +1805,169 @@ class VarDataValue(BaseConverter):
|
||||
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):
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
@ -1910,6 +2045,8 @@ converterMapping = {
|
||||
"ExtendMode": ExtendMode,
|
||||
"CompositeMode": CompositeMode,
|
||||
"STATFlags": STATFlags,
|
||||
"TupleList": partial(CFF2Index, itemConverterClass=TupleValues),
|
||||
"VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph),
|
||||
# AAT
|
||||
"CIDGlyphMap": CIDGlyphMap,
|
||||
"GlyphCIDMap": GlyphCIDMap,
|
||||
|
@ -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",
|
||||
[
|
||||
@ -3183,7 +3202,7 @@ otData = [
|
||||
"ConditionTable",
|
||||
"ConditionCount",
|
||||
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",
|
||||
[
|
||||
@ -3322,6 +3414,78 @@ otData = [
|
||||
("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
|
||||
(
|
||||
"HVAR",
|
||||
|
@ -11,11 +11,13 @@ from functools import reduce
|
||||
from math import radians
|
||||
import itertools
|
||||
from collections import defaultdict, namedtuple
|
||||
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
||||
from fontTools.ttLib.tables.otTraverse import dfs_base_table
|
||||
from fontTools.misc.arrayTools import quantizeRect
|
||||
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.vector import Vector
|
||||
from fontTools.pens.boundsPen import ControlBoundsPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from .otBase import (
|
||||
@ -25,9 +27,18 @@ from .otBase import (
|
||||
CountReference,
|
||||
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
|
||||
import logging
|
||||
import struct
|
||||
import array
|
||||
import sys
|
||||
from enum import IntFlag
|
||||
from typing import TYPE_CHECKING, Iterator, List, Optional, Set
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -37,6 +48,389 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
def __init__(self):
|
||||
self.GlyphClasses = {} # GlyphID --> GlyphClass
|
||||
|
@ -4,7 +4,12 @@ from fontTools.misc.configTools import AbstractConfig
|
||||
from fontTools.misc.textTools import Tag, byteord, tostr
|
||||
from fontTools.misc.loggingTools import deprecateArgument
|
||||
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 io import BytesIO, StringIO, UnsupportedOperation
|
||||
import os
|
||||
@ -764,12 +769,16 @@ class TTFont(object):
|
||||
location = None
|
||||
if location and not normalized:
|
||||
location = self.normalizeLocation(location)
|
||||
glyphSet = None
|
||||
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:
|
||||
return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
|
||||
glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
|
||||
else:
|
||||
raise TTLibError("Font contains no outlines")
|
||||
if "VARC" in self:
|
||||
glyphSet = _TTGlyphSetVARC(self, location, glyphSet)
|
||||
return glyphSet
|
||||
|
||||
def normalizeLocation(self, location):
|
||||
"""Normalize a ``location`` from the font's defined axes space (also
|
||||
|
@ -3,11 +3,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from copy import copy, deepcopy
|
||||
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.transform import Transform
|
||||
from fontTools.misc.transform import Transform, DecomposedTransform
|
||||
from fontTools.pens.transformPen import TransformPen, TransformPointPen
|
||||
from fontTools.pens.recordingPen import (
|
||||
DecomposingRecordingPen,
|
||||
@ -103,6 +104,16 @@ class _TTGlyphSetGlyf(_TTGlyphSet):
|
||||
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):
|
||||
def __init__(self, font, location):
|
||||
tableTag = "CFF2" if "CFF2" in font else "CFF "
|
||||
@ -123,6 +134,19 @@ class _TTGlyphSetCFF(_TTGlyphSet):
|
||||
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):
|
||||
"""Glyph object that supports the Pen protocol, meaning that it has
|
||||
.draw() and .drawPoints() methods that take a pen object as their only
|
||||
@ -178,10 +202,6 @@ class _TTGlyphGlyf(_TTGlyph):
|
||||
if depth:
|
||||
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)
|
||||
|
||||
def drawPoints(self, pen):
|
||||
@ -194,35 +214,8 @@ class _TTGlyphGlyf(_TTGlyph):
|
||||
if depth:
|
||||
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)
|
||||
|
||||
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):
|
||||
if self.glyphSet.location and self.glyphSet.gvarTable is not None:
|
||||
glyph = self._getGlyphInstance()
|
||||
@ -283,6 +276,128 @@ class _TTGlyphCFF(_TTGlyph):
|
||||
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):
|
||||
# Handle phantom points for (left, right, top, bottom) positions.
|
||||
assert len(coord) >= 4
|
||||
@ -300,11 +415,6 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
|
||||
for p, comp in zip(coord, glyph.components):
|
||||
if hasattr(comp, "x"):
|
||||
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:
|
||||
assert len(coord) == 0
|
||||
else:
|
||||
|
@ -1017,8 +1017,6 @@ class WOFF2GlyfTable(getTableClass("glyf")):
|
||||
return
|
||||
elif glyph.isComposite():
|
||||
self._encodeComponents(glyph)
|
||||
elif glyph.isVarComposite():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
self._encodeCoordinates(glyph)
|
||||
self._encodeOverlapSimpleFlag(glyph, glyphID)
|
||||
|
@ -10,6 +10,13 @@ def buildVarRegionAxis(axisSupport):
|
||||
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):
|
||||
assert all(tag in axisTags for tag in support.keys()), (
|
||||
"Unknown axis tag found.",
|
||||
@ -23,6 +30,24 @@ def buildVarRegion(support, axisTags):
|
||||
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):
|
||||
self = ot.VarRegionList()
|
||||
self.RegionAxisCount = len(axisTags)
|
||||
@ -33,6 +58,16 @@ def buildVarRegionList(supports, axisTags):
|
||||
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):
|
||||
return [lst[i] for i in mapping]
|
||||
|
||||
@ -130,6 +165,29 @@ def buildVarStore(varRegionList, varDataList):
|
||||
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
|
||||
|
||||
|
||||
|
@ -111,6 +111,7 @@ from fontTools.varLib.instancer import names
|
||||
from .featureVars import instantiateFeatureVariations
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.varLib.instancer import solver
|
||||
from fontTools.ttLib.tables.otTables import VarComponentFlags
|
||||
import collections
|
||||
import dataclasses
|
||||
from contextlib import contextmanager
|
||||
@ -465,6 +466,42 @@ class OverlapMode(IntEnum):
|
||||
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(
|
||||
variations, axisLimits, origCoords=None, endPts=None
|
||||
):
|
||||
@ -843,23 +880,6 @@ def _instantiateGvarGlyph(
|
||||
if 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
|
||||
# the four phantom points and glyph bounding boxes.
|
||||
# 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: (
|
||||
(
|
||||
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
||||
if glyf[name].isComposite() or glyf[name].isVarComposite()
|
||||
if glyf[name].isComposite()
|
||||
else 0
|
||||
),
|
||||
name,
|
||||
@ -1598,6 +1618,9 @@ def instantiateVariableFont(
|
||||
log.info("Updating name table")
|
||||
names.updateNameTable(varfont, axisLimits)
|
||||
|
||||
if "VARC" in varfont:
|
||||
instantiateVARC(varfont, normalizedLimits)
|
||||
|
||||
if "CFF2" in varfont:
|
||||
instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2)
|
||||
|
||||
|
@ -75,7 +75,7 @@ def normalizeValue(v, triple, extrapolate=False):
|
||||
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.
|
||||
|
||||
>>> axes = {"wght": (100, 400, 900)}
|
||||
@ -114,6 +114,10 @@ def normalizeLocation(location, axes, extrapolate=False):
|
||||
>>> normalizeLocation({"wght": 1001}, axes)
|
||||
{'wght': 0.0}
|
||||
"""
|
||||
if validate:
|
||||
assert set(location.keys()) <= set(axes.keys()), set(location.keys()) - set(
|
||||
axes.keys()
|
||||
)
|
||||
out = {}
|
||||
for tag, triple in axes.items():
|
||||
v = location.get(tag, triple[1])
|
||||
@ -453,7 +457,10 @@ class VariationModel(object):
|
||||
self.deltaWeights.append(deltaWeight)
|
||||
|
||||
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
|
||||
out = []
|
||||
for i, weights in enumerate(self.deltaWeights):
|
||||
|
253
Lib/fontTools/varLib/multiVarStore.py
Normal file
253
Lib/fontTools/varLib/multiVarStore.py
Normal 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
|
@ -201,7 +201,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
|
||||
key=lambda name: (
|
||||
(
|
||||
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
||||
if glyf[name].isComposite() or glyf[name].isVarComposite()
|
||||
if glyf[name].isComposite()
|
||||
else 0
|
||||
),
|
||||
name,
|
||||
|
@ -32,7 +32,7 @@ class OnlineVarStoreBuilder(object):
|
||||
self._supports = None
|
||||
self._varDataIndices = {}
|
||||
self._varDataCaches = {}
|
||||
self._cache = {}
|
||||
self._cache = None
|
||||
|
||||
def setModel(self, model):
|
||||
self.setSupports(model.supports)
|
||||
@ -43,7 +43,7 @@ class OnlineVarStoreBuilder(object):
|
||||
self._supports = list(supports)
|
||||
if not self._supports[0]:
|
||||
del self._supports[0] # Drop base master support
|
||||
self._cache = {}
|
||||
self._cache = None
|
||||
self._data = None
|
||||
|
||||
def finish(self, optimize=True):
|
||||
@ -54,7 +54,7 @@ class OnlineVarStoreBuilder(object):
|
||||
data.calculateNumShorts(optimize=optimize)
|
||||
return self._store
|
||||
|
||||
def _add_VarData(self):
|
||||
def _add_VarData(self, num_items=1):
|
||||
regionMap = self._regionMap
|
||||
regionList = self._regionList
|
||||
|
||||
@ -76,7 +76,7 @@ class OnlineVarStoreBuilder(object):
|
||||
self._outer = varDataIdx
|
||||
self._data = self._store.VarData[varDataIdx]
|
||||
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.
|
||||
varDataIdx = None
|
||||
|
||||
@ -94,6 +94,14 @@ class OnlineVarStoreBuilder(object):
|
||||
base = deltas.pop(0)
|
||||
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):
|
||||
deltas = [round(d) for d in deltas]
|
||||
if len(deltas) == len(self._supports) + 1:
|
||||
@ -102,23 +110,51 @@ class OnlineVarStoreBuilder(object):
|
||||
assert len(deltas) == len(self._supports)
|
||||
deltas = tuple(deltas)
|
||||
|
||||
if not self._data:
|
||||
self._add_VarData()
|
||||
|
||||
varIdx = self._cache.get(deltas)
|
||||
if varIdx is not None:
|
||||
return varIdx
|
||||
|
||||
if not self._data:
|
||||
self._add_VarData()
|
||||
inner = len(self._data.Item)
|
||||
if inner == 0xFFFF:
|
||||
# Full array. Start new one.
|
||||
self._add_VarData()
|
||||
return self.storeDeltas(deltas)
|
||||
return self.storeDeltas(deltas, round=noRound)
|
||||
self._data.addItem(deltas, round=noRound)
|
||||
|
||||
varIdx = (self._outer << 16) + inner
|
||||
self._cache[deltas] = 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):
|
||||
deltas = [round(d) for d in deltas]
|
||||
@ -210,26 +246,29 @@ class VarStoreInstancer(object):
|
||||
|
||||
|
||||
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.
|
||||
used = {}
|
||||
used = defaultdict(set)
|
||||
for varIdx in varIdxes:
|
||||
if varIdx == NO_VARIATION_INDEX:
|
||||
continue
|
||||
major = varIdx >> 16
|
||||
minor = varIdx & 0xFFFF
|
||||
d = used.get(major)
|
||||
if d is None:
|
||||
d = used[major] = set()
|
||||
d.add(minor)
|
||||
used[major].add(minor)
|
||||
del varIdxes
|
||||
|
||||
#
|
||||
# Subset VarData
|
||||
#
|
||||
|
||||
varData = self.VarData
|
||||
varData = getattr(self, VarData)
|
||||
newVarData = []
|
||||
varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX}
|
||||
for major, data in enumerate(varData):
|
||||
@ -260,10 +299,11 @@ def VarStore_subset_varidxes(
|
||||
data.Item = newItems
|
||||
data.ItemCount = len(data.Item)
|
||||
|
||||
data.calculateNumShorts(optimize=optimize)
|
||||
if VarData == "VarData":
|
||||
data.calculateNumShorts(optimize=optimize)
|
||||
|
||||
self.VarData = newVarData
|
||||
self.VarDataCount = len(self.VarData)
|
||||
setattr(self, VarData, newVarData)
|
||||
setattr(self, VarData + "Count", len(newVarData))
|
||||
|
||||
self.prune_regions()
|
||||
|
||||
@ -273,7 +313,7 @@ def 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."""
|
||||
#
|
||||
# Subset VarRegionList
|
||||
@ -281,10 +321,10 @@ def VarStore_prune_regions(self):
|
||||
|
||||
# Collect.
|
||||
usedRegions = set()
|
||||
for data in self.VarData:
|
||||
for data in getattr(self, VarData):
|
||||
usedRegions.update(data.VarRegionIndex)
|
||||
# Subset.
|
||||
regionList = self.VarRegionList
|
||||
regionList = getattr(self, VarRegionList)
|
||||
regions = regionList.Region
|
||||
newRegions = []
|
||||
regionMap = {}
|
||||
@ -294,7 +334,7 @@ def VarStore_prune_regions(self):
|
||||
regionList.Region = newRegions
|
||||
regionList.RegionCount = len(regionList.Region)
|
||||
# Map.
|
||||
for data in self.VarData:
|
||||
for data in getattr(self, VarData):
|
||||
data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex]
|
||||
|
||||
|
||||
|
@ -428,14 +428,14 @@ class SubsetTest:
|
||||
def test_varComposite(self):
|
||||
fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf")
|
||||
origfont = TTFont(fontpath)
|
||||
assert len(origfont.getGlyphOrder()) == 6
|
||||
assert len(origfont.getGlyphOrder()) == 11
|
||||
subsetpath = self.temp_path(".ttf")
|
||||
subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath])
|
||||
subsetfont = TTFont(subsetpath)
|
||||
assert len(subsetfont.getGlyphOrder()) == 4
|
||||
assert len(subsetfont.getGlyphOrder()) == 6
|
||||
subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath])
|
||||
subsetfont = TTFont(subsetpath)
|
||||
assert len(subsetfont.getGlyphOrder()) == 5
|
||||
assert len(subsetfont.getGlyphOrder()) == 8
|
||||
|
||||
def test_timing_publishes_parts(self):
|
||||
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.
BIN
Tests/ttLib/data/varc-ac01-conditional.ttf
Normal file
BIN
Tests/ttLib/data/varc-ac01-conditional.ttf
Normal file
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.scaleUpem import scale_upem
|
||||
from io import BytesIO
|
||||
import difflib
|
||||
import os
|
||||
import shutil
|
||||
@ -70,6 +71,12 @@ class ScaleUpemTest(unittest.TestCase):
|
||||
|
||||
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")
|
||||
self.expect_ttx(font, expected_ttx_path, tables)
|
||||
|
||||
|
87
Tests/ttLib/tables/V_A_R_C_test.py
Normal file
87
Tests/ttLib/tables/V_A_R_C_test.py
Normal 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())
|
@ -719,65 +719,6 @@ class GlyphComponentTest:
|
||||
assert (comp.firstPt, comp.secondPt) == (1, 2)
|
||||
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:
|
||||
def test_roundtrip(self):
|
||||
|
@ -427,9 +427,12 @@ class AATLookupTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
from fontTools.misc.lazyTools import LazyList
|
||||
|
||||
|
||||
class LazyListTest(unittest.TestCase):
|
||||
def test_slice(self):
|
||||
ll = otConverters._LazyList([10, 11, 12, 13])
|
||||
ll = LazyList([10, 11, 12, 13])
|
||||
sl = ll[:]
|
||||
|
||||
self.assertIsNot(sl, ll)
|
||||
@ -438,26 +441,9 @@ class LazyListTest(unittest.TestCase):
|
||||
|
||||
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):
|
||||
ll1 = otConverters._LazyList([1])
|
||||
ll2 = otConverters._LazyList([2])
|
||||
ll1 = LazyList([1])
|
||||
ll2 = LazyList([2])
|
||||
|
||||
l3 = ll1 + ll2
|
||||
|
||||
@ -465,7 +451,7 @@ class LazyListTest(unittest.TestCase):
|
||||
self.assertEqual([1, 2], l3)
|
||||
|
||||
def test_add_LazyList_and_list(self):
|
||||
ll1 = otConverters._LazyList([1])
|
||||
ll1 = LazyList([1])
|
||||
l2 = [2]
|
||||
|
||||
l3 = ll1 + l2
|
||||
@ -475,13 +461,13 @@ class LazyListTest(unittest.TestCase):
|
||||
|
||||
def test_add_not_implemented(self):
|
||||
with self.assertRaises(TypeError):
|
||||
otConverters._LazyList() + 0
|
||||
LazyList() + 0
|
||||
with self.assertRaises(TypeError):
|
||||
otConverters._LazyList() + tuple()
|
||||
LazyList() + tuple()
|
||||
|
||||
def test_radd_list_and_LazyList(self):
|
||||
l1 = [1]
|
||||
ll2 = otConverters._LazyList([2])
|
||||
ll2 = LazyList([2])
|
||||
|
||||
l3 = l1 + ll2
|
||||
|
||||
@ -490,9 +476,9 @@ class LazyListTest(unittest.TestCase):
|
||||
|
||||
def test_radd_not_implemented(self):
|
||||
with self.assertRaises(TypeError):
|
||||
0 + otConverters._LazyList()
|
||||
0 + LazyList()
|
||||
with self.assertRaises(TypeError):
|
||||
tuple() + otConverters._LazyList()
|
||||
tuple() + LazyList()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -227,33 +227,57 @@ class TTGlyphSetTest(object):
|
||||
"addVarComponent",
|
||||
(
|
||||
"glyph00003",
|
||||
DecomposedTransform(460.0, 676.0, 0, 1, 1, 0, 0, 0, 0),
|
||||
{
|
||||
"0000": 0.84661865234375,
|
||||
"0001": 0.98944091796875,
|
||||
"0002": 0.47283935546875,
|
||||
"0003": 0.446533203125,
|
||||
},
|
||||
DecomposedTransform(
|
||||
translateX=0,
|
||||
translateY=0,
|
||||
rotation=0,
|
||||
scaleX=1,
|
||||
scaleY=1,
|
||||
skewX=0,
|
||||
skewY=0,
|
||||
tCenterX=0,
|
||||
tCenterY=0,
|
||||
),
|
||||
{},
|
||||
),
|
||||
),
|
||||
(
|
||||
"addVarComponent",
|
||||
(
|
||||
"glyph00004",
|
||||
DecomposedTransform(932.0, 382.0, 0, 1, 1, 0, 0, 0, 0),
|
||||
{
|
||||
"0000": 0.93359375,
|
||||
"0001": 0.916015625,
|
||||
"0002": 0.523193359375,
|
||||
"0003": 0.32806396484375,
|
||||
"0004": 0.85089111328125,
|
||||
},
|
||||
"glyph00005",
|
||||
DecomposedTransform(
|
||||
translateX=0,
|
||||
translateY=0,
|
||||
rotation=0,
|
||||
scaleX=1,
|
||||
scaleY=1,
|
||||
skewX=0,
|
||||
skewY=0,
|
||||
tCenterX=0,
|
||||
tCenterY=0,
|
||||
),
|
||||
{},
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
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):
|
||||
font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
|
||||
glyphset = font.getGlyphSet(location={"wght": 600})
|
||||
@ -265,77 +289,24 @@ class TTGlyphSetTest(object):
|
||||
actual = pen.value
|
||||
|
||||
expected = [
|
||||
("moveTo", ((432, 678),)),
|
||||
("lineTo", ((432, 620),)),
|
||||
(
|
||||
"qCurveTo",
|
||||
(
|
||||
(419, 620),
|
||||
(374, 621),
|
||||
(324, 619),
|
||||
(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),
|
||||
),
|
||||
),
|
||||
("moveTo", ((82, 108),)),
|
||||
("qCurveTo", ((188, 138), (350, 240), (461, 384), (518, 567), (518, 678))),
|
||||
("lineTo", ((518, 732),)),
|
||||
("lineTo", ((74, 732),)),
|
||||
("lineTo", ((74, 630),)),
|
||||
("lineTo", ((456, 630),)),
|
||||
("lineTo", ((403, 660),)),
|
||||
("qCurveTo", ((403, 575), (358, 431), (267, 314), (128, 225), (34, 194))),
|
||||
("closePath", ()),
|
||||
("moveTo", ((525, 619),)),
|
||||
("lineTo", ((412, 620),)),
|
||||
("lineTo", ((429, 678),)),
|
||||
("lineTo", ((466, 697),)),
|
||||
("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),)),
|
||||
("moveTo", ((702, 385),)),
|
||||
("lineTo", ((897, 385),)),
|
||||
("lineTo", ((897, 485),)),
|
||||
("lineTo", ((702, 485),)),
|
||||
("closePath", ()),
|
||||
("moveTo", ((63, 118),)),
|
||||
("lineTo", ((47, 135),)),
|
||||
("qCurveTo", ((42, 141), (48, 146))),
|
||||
("qCurveTo", ((135, 213), (278, 373), (383, 541), (412, 620))),
|
||||
("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))),
|
||||
("moveTo", ((641, -92),)),
|
||||
("lineTo", ((752, -92),)),
|
||||
("lineTo", ((752, 813),)),
|
||||
("lineTo", ((641, 813),)),
|
||||
("closePath", ()),
|
||||
]
|
||||
|
||||
@ -530,7 +501,7 @@ class TTGlyphSetTest(object):
|
||||
"qCurveTo",
|
||||
(
|
||||
(919, 41),
|
||||
(854, 67),
|
||||
(854, 68),
|
||||
(790, 98),
|
||||
(729, 134),
|
||||
(671, 173),
|
||||
@ -542,7 +513,7 @@ class TTGlyphSetTest(object):
|
||||
("lineTo", ((522, 286),)),
|
||||
("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))),
|
||||
("lineTo", ((515, 209),)),
|
||||
("qCurveTo", ((569, 146), (695, 44), (835, -32), (913, -57))),
|
||||
("qCurveTo", ((569, 146), (695, 45), (835, -32), (913, -57))),
|
||||
("closePath", ()),
|
||||
("moveTo", ((474, 274),)),
|
||||
("lineTo", ((452, 284),)),
|
||||
|
@ -1699,23 +1699,33 @@ class InstantiateVariableFontTest(object):
|
||||
|
||||
def test_varComposite(self):
|
||||
input_path = os.path.join(
|
||||
TESTDATA, "..", "..", "..", "ttLib", "data", "varc-ac00-ac01.ttf"
|
||||
TESTDATA, "..", "..", "..", "ttLib", "data", "varc-6868.ttf"
|
||||
)
|
||||
varfont = ttLib.TTFont(input_path)
|
||||
|
||||
location = {"wght": 600}
|
||||
|
||||
instance = instancer.instantiateVariableFont(
|
||||
varfont,
|
||||
location,
|
||||
)
|
||||
# We currently do not allow this either; although in theory
|
||||
# it should be possible.
|
||||
with pytest.raises(
|
||||
NotImplementedError,
|
||||
match="is not supported.",
|
||||
):
|
||||
instance = instancer.instantiateVariableFont(
|
||||
varfont,
|
||||
location,
|
||||
)
|
||||
|
||||
location = {"0000": 0.5}
|
||||
|
||||
instance = instancer.instantiateVariableFont(
|
||||
varfont,
|
||||
location,
|
||||
)
|
||||
with pytest.raises(
|
||||
NotImplementedError,
|
||||
match="is not supported.",
|
||||
):
|
||||
instance = instancer.instantiateVariableFont(
|
||||
varfont,
|
||||
location,
|
||||
)
|
||||
|
||||
|
||||
def _conditionSetAsDict(conditionSet, axisOrder):
|
||||
|
Loading…
x
Reference in New Issue
Block a user