Merge pull request #3395 from fonttools/varc-table

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

View File

@ -656,11 +656,7 @@ class FontBuilder(object):
if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
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."

View File

@ -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)

View File

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

View File

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

View File

@ -422,6 +422,19 @@ class DecomposedTransform:
tCenterX: float = 0
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

View File

@ -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

View File

@ -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(

View File

@ -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,57 +133,105 @@ 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
@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
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])
## Scale translate & tCenter
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X:
v[i] = visitor.scale(v[i])
i += 1
if component.flags & VarComponentFlags.HAVE_ROTATION:
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y:
v[i] = visitor.scale(v[i])
i += 1
if component.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
if flags & otTables.VarComponentFlags.HAVE_ROTATION:
i += 1
if component.flags & (
VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y
):
if flags & otTables.VarComponentFlags.HAVE_SCALE_X:
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])
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
# Phantom points
assert i + 4 == len(coordinates)
for i in range(i, len(coordinates)):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
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")

View File

@ -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

View File

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

View File

@ -424,29 +424,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
for c in glyph.components
],
)
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.

View File

@ -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]
]
def get_read_item():
reverseGlyphMap = ttFont.getReverseGlyphMap()
tableFormat = self.flags & 1
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 decompileGlyph_(
numPointsInGlyph, sharedCoords, axisTags, gvarData
)
for gid in range(self.glyphCount):
glyphName = glyphs[gid]
variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
self.variations = _LazyDict(variations)
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

View File

@ -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",

View File

@ -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 = []

View File

@ -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,

View File

@ -3168,6 +3168,25 @@ otData = [
),
],
),
(
"ConditionList",
[
(
"uint32",
"ConditionCount",
None,
None,
"Number of condition tables in the ConditionTable array",
),
(
"LOffset",
"ConditionTable",
"ConditionCount",
0,
"Array of offset to condition tables, from the beginning of the ConditionList table.",
),
],
),
(
"ConditionSet",
[
@ -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",

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

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

View File

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

View File

@ -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)
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]

View File

@ -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.

Binary file not shown.

View File

@ -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)

View File

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

View File

@ -719,65 +719,6 @@ class GlyphComponentTest:
assert (comp.firstPt, comp.secondPt) == (1, 2)
assert 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):

View File

@ -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__":

View File

@ -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),)),

View File

@ -1699,12 +1699,18 @@ 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}
# 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,
@ -1712,6 +1718,10 @@ class InstantiateVariableFontTest(object):
location = {"0000": 0.5}
with pytest.raises(
NotImplementedError,
match="is not supported.",
):
instance = instancer.instantiateVariableFont(
varfont,
location,