Merge pull request #2784 from fonttools/varglyphset-cff

[ttVarGlyphSet] Support CFF
This commit is contained in:
Just van Rossum 2022-08-29 19:26:14 +02:00 committed by GitHub
commit 633bc2732f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 108 deletions

View File

@ -1125,8 +1125,9 @@ class CharStrings(object):
"""
def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray,
isCFF2=None):
isCFF2=None, varStore=None):
self.globalSubrs = globalSubrs
self.varStore = varStore
if file is not None:
self.charStringsIndex = SubrsIndex(
file, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2)
@ -1516,6 +1517,7 @@ class CharStringsConverter(TableConverter):
file = parent.file
isCFF2 = parent._isCFF2
charset = parent.charset
varStore = getattr(parent, "VarStore", None)
globalSubrs = parent.GlobalSubrs
if hasattr(parent, "FDArray"):
fdArray = parent.FDArray
@ -1529,7 +1531,7 @@ class CharStringsConverter(TableConverter):
private = parent.Private
file.seek(value) # Offset(0)
charStrings = CharStrings(
file, charset, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2)
file, charset, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2, varStore=varStore)
return charStrings
def write(self, parent, value):
@ -1551,7 +1553,7 @@ class CharStringsConverter(TableConverter):
# there is no fdArray.
private, fdSelect, fdArray = parent.Private, None, None
charStrings = CharStrings(
None, None, parent.GlobalSubrs, private, fdSelect, fdArray)
None, None, parent.GlobalSubrs, private, fdSelect, fdArray, varStore=getattr(parent, "VarStore", None))
charStrings.fromXML(name, attrs, content)
return charStrings

View File

@ -263,12 +263,13 @@ class CharStringCompileError(Exception): pass
class SimpleT2Decompiler(object):
def __init__(self, localSubrs, globalSubrs, private=None):
def __init__(self, localSubrs, globalSubrs, private=None, blender=None):
self.localSubrs = localSubrs
self.localBias = calcSubrBias(localSubrs)
self.globalSubrs = globalSubrs
self.globalBias = calcSubrBias(globalSubrs)
self.private = private
self.blender = blender
self.reset()
def reset(self):
@ -277,6 +278,7 @@ class SimpleT2Decompiler(object):
self.hintCount = 0
self.hintMaskBytes = 0
self.numRegions = 0
self.vsIndex = 0
def execute(self, charString):
self.callingStack.append(charString)
@ -410,17 +412,28 @@ class SimpleT2Decompiler(object):
def op_roll(self, index):
raise NotImplementedError
# TODO(behdad): move to T2OutlineExtractor and add a 'setVariation'
# method that takes VarStoreData and a location
def op_blend(self, index):
if self.numRegions == 0:
self.numRegions = self.private.getNumRegions()
numBlends = self.pop()
numOps = numBlends * (self.numRegions + 1)
del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack.
if self.blender is None:
del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack.
else:
argi = len(self.operandStack) - numOps
end_args = tuplei = argi + numBlends
while argi < end_args:
next_ti = tuplei + self.numRegions
deltas = self.operandStack[tuplei:next_ti]
delta = self.blender(self.vsIndex, deltas)
self.operandStack[argi] += delta
tuplei = next_ti
argi += 1
self.operandStack[end_args:] = []
def op_vsindex(self, index):
vi = self.pop()
self.vsIndex = vi
self.numRegions = self.private.getNumRegions(vi)
@ -456,8 +469,8 @@ t1Operators = [
class T2WidthExtractor(SimpleT2Decompiler):
def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None, blender=None):
SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private, blender)
self.nominalWidthX = nominalWidthX
self.defaultWidthX = defaultWidthX
@ -498,9 +511,9 @@ class T2WidthExtractor(SimpleT2Decompiler):
class T2OutlineExtractor(T2WidthExtractor):
def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None, blender=None):
T2WidthExtractor.__init__(
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private)
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private, blender)
self.pen = pen
self.subrLevel = 0
@ -986,11 +999,11 @@ class T2CharString(object):
decompiler = self.decompilerClass(subrs, self.globalSubrs, self.private)
decompiler.execute(self)
def draw(self, pen):
def draw(self, pen, blender=None):
subrs = getattr(self.private, "Subrs", [])
extractor = self.outlineExtractor(pen, subrs, self.globalSubrs,
self.private.nominalWidthX, self.private.defaultWidthX,
self.private)
self.private, blender)
extractor.execute(self)
self.width = extractor.width

View File

@ -8,6 +8,7 @@ from fontTools.ttLib.ttGlyphSet import (
_TTGlyphSet, _TTGlyph,
_TTGlyphCFF, _TTGlyphGlyf,
_TTVarGlyphSet,
_TTVarGlyphCFF, _TTVarGlyphGlyf,
)
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
from io import BytesIO, StringIO
@ -703,14 +704,17 @@ class TTFont(object):
if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or
("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))):
table_tag = "CFF2" if "CFF2" in self else "CFF "
if location:
raise NotImplementedError # TODO
glyphs = _TTGlyphSet(self,
list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF)
glyphs = list(self[table_tag].cff.values())[0].CharStrings
if location and 'fvar' in self:
glyphs = _TTVarGlyphSet(self, glyphs, _TTVarGlyphCFF,
location, normalized)
else:
glyphs = _TTGlyphSet(self, glyphs, _TTGlyphCFF)
if glyphs is None and "glyf" in self:
if location and 'gvar' in self:
glyphs = _TTVarGlyphSet(self, location=location, normalized=normalized)
glyphs = _TTVarGlyphSet(self, self["glyf"], _TTVarGlyphGlyf,
location, normalized)
else:
glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf)

View File

@ -3,6 +3,7 @@
from fontTools.misc.fixedTools import otRound
from copy import copy
class _TTGlyphSet(object):
"""Generic dict-like GlyphSet class that pulls metrics from hmtx and
@ -13,13 +14,13 @@ class _TTGlyphSet(object):
"""Construct a new glyphset.
Args:
font (TTFont): The font object (used to get metrics).
glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
font (TTFont): The font object (used to get metrics).
glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
"""
self._glyphs = glyphs
self._hmtx = ttFont['hmtx']
self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
self._hmtx = ttFont["hmtx"]
self._vmtx = ttFont["vmtx"] if "vmtx" in ttFont else None
self._glyphType = glyphType
def keys(self):
@ -34,7 +35,8 @@ class _TTGlyphSet(object):
horizontalMetrics = self._hmtx[glyphName]
verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
return self._glyphType(
self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics
)
def __len__(self):
return len(self._glyphs)
@ -45,6 +47,7 @@ class _TTGlyphSet(object):
except KeyError:
return default
class _TTGlyph(object):
"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
@ -56,17 +59,20 @@ class _TTGlyph(object):
attributes.
"""
def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
def __init__(self, glyphset, glyph, horizontalMetrics=None, verticalMetrics=None):
"""Construct a new _TTGlyph.
Args:
glyphset (_TTGlyphSet): A glyphset object used to resolve components.
glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
horizontalMetrics (int, int): The glyph's width and left sidebearing.
glyphset (_TTGlyphSet): A glyphset object used to resolve components.
glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
horizontalMetrics (int, int): The glyph's width and left sidebearing.
"""
self._glyphset = glyphset
self._glyph = glyph
self.width, self.lsb = horizontalMetrics
if horizontalMetrics:
self.width, self.lsb = horizontalMetrics
else:
self.width, self.lsb = None, None
if verticalMetrics:
self.height, self.tsb = verticalMetrics
else:
@ -80,13 +86,15 @@ class _TTGlyph(object):
def drawPoints(self, pen):
from fontTools.pens.pointPen import SegmentToPointPen
self.draw(SegmentToPointPen(pen))
class _TTGlyphCFF(_TTGlyph):
pass
class _TTGlyphGlyf(_TTGlyph):
class _TTGlyphGlyf(_TTGlyph):
def draw(self, pen):
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
how that works.
@ -106,20 +114,22 @@ class _TTGlyphGlyf(_TTGlyph):
glyph.drawPoints(pen, glyfTable, offset)
class _TTVarGlyphSet(_TTGlyphSet):
def __init__(self, font, location, normalized=False):
def __init__(self, font, glyphs, glyphType, location, normalized):
self._ttFont = font
self._glyphs = font['glyf']
self._glyphs = glyphs
self._glyphType = glyphType
if not normalized:
from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes}
axes = {
a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
for a in font["fvar"].axes
}
location = normalizeLocation(location, axes)
if 'avar' in font:
avar = font['avar']
if "avar" in font:
avar = font["avar"]
avarSegments = avar.segments
new_location = {}
for axis_tag, value in location.items():
@ -135,13 +145,13 @@ class _TTVarGlyphSet(_TTGlyphSet):
def __getitem__(self, glyphName):
if glyphName not in self._glyphs:
raise KeyError(glyphName)
return _TTVarGlyphGlyf(self._ttFont, glyphName, self.location)
return self._glyphType(self, glyphName, self.location)
def _setCoordinates(glyph, coord, glyfTable):
# Handle phantom points for (left, right, top, bottom) positions.
assert len(coord) >= 4
if not hasattr(glyph, 'xMin'):
if not hasattr(glyph, "xMin"):
glyph.recalcBounds(glyfTable)
leftSideX = coord[-4][0]
rightSideX = coord[-3][0]
@ -154,9 +164,9 @@ def _setCoordinates(glyph, coord, glyfTable):
if glyph.isComposite():
assert len(coord) == len(glyph.components)
glyph.components = [copy(comp) for comp in glyph.components]
for p,comp in zip(coord, glyph.components):
if hasattr(comp, 'x'):
comp.x,comp.y = p
for p, comp in zip(coord, glyph.components):
if hasattr(comp, "x"):
comp.x, comp.y = p
elif glyph.numberOfContours == 0:
assert len(coord) == 0
else:
@ -178,16 +188,47 @@ def _setCoordinates(glyph, coord, glyfTable):
class _TTVarGlyph(_TTGlyph):
def __init__(self, ttFont, glyphName, location):
self._ttFont = ttFont
def __init__(self, glyphSet, glyphName, location):
super().__init__(glyphSet._glyphs, glyphSet._glyphs[glyphName])
self._glyphSet = glyphSet
self._ttFont = glyphSet._ttFont
self._glyphs = glyphSet._glyphs
self._glyphName = glyphName
self._location = location
# draw() fills these in
self.width = self.height = self.lsb = self.tsb = None
class _TTVarGlyphCFF(_TTVarGlyph):
def draw(self, pen):
varStore = self._glyphs.varStore
if varStore is None:
blender = None
else:
from fontTools.varLib.varStore import VarStoreInstancer
vsInstancer = getattr(self._glyphSet, "vsInstancer", None)
if vsInstancer is None:
self._glyphSet.vsInstancer = vsInstancer = VarStoreInstancer(
varStore.otVarStore, self._ttFont["fvar"].axes, self._location
)
blender = vsInstancer.interpolateFromDeltas
self._glyph.draw(pen, blender)
self.width = self._ttFont["hmtx"][self._glyphName][0]
if "HVAR" in self._ttFont:
hvar = self._ttFont["HVAR"].table
varidx = self._ttFont.getGlyphID(self._glyphName)
if hvar.AdvWidthMap is not None:
varidx = hvar.AdvWidthMap.mapping[self._glyphName]
vsInstancer = VarStoreInstancer(
hvar.VarStore, self._ttFont["fvar"].axes, self._location
)
delta = vsInstancer[varidx]
self.width += delta
class _TTVarGlyphGlyf(_TTVarGlyph):
def draw(self, pen):
self._drawWithPen(pen, isPointPen=False)
@ -199,12 +240,14 @@ class _TTVarGlyphGlyf(_TTVarGlyph):
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib.models import supportScalar
glyf = self._ttFont['glyf']
hMetrics = self._ttFont['hmtx'].metrics
vMetrics = getattr(self._ttFont.get('vmtx'), 'metrics', None)
glyf = self._ttFont["glyf"]
hMetrics = self._ttFont["hmtx"].metrics
vMetrics = getattr(self._ttFont.get("vmtx"), "metrics", None)
variations = self._ttFont['gvar'].variations[self._glyphName]
coordinates, _ = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics)
variations = self._ttFont["gvar"].variations[self._glyphName]
coordinates, _ = glyf._getCoordinatesAndControls(
self._glyphName, hMetrics, vMetrics
)
origCoords, endPts = None, None
for var in variations:
scalar = supportScalar(self._location, var.axes)
@ -213,12 +256,16 @@ class _TTVarGlyphGlyf(_TTVarGlyph):
delta = var.coordinates
if None in delta:
if origCoords is None:
origCoords,control = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics)
endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
origCoords, control = glyf._getCoordinatesAndControls(
self._glyphName, hMetrics, vMetrics
)
endPts = (
control[1] if control[0] >= 1 else list(range(len(control[1])))
)
delta = iup_delta(delta, origCoords, endPts)
coordinates += GlyphCoordinates(delta) * scalar
glyph = copy(glyf[self._glyphName]) # Shallow copy
glyph = copy(glyf[self._glyphName]) # Shallow copy
width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyf)
self.width = width
self.lsb = lsb

View File

@ -448,9 +448,9 @@ class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
into a CFF2 variable font charstring."""
def __init__(self, pen, localSubrs, globalSubrs,
nominalWidthX, defaultWidthX, private=None):
nominalWidthX, defaultWidthX, private=None, blender=None):
super().__init__(pen, localSubrs,
globalSubrs, nominalWidthX, defaultWidthX, private)
globalSubrs, nominalWidthX, defaultWidthX, private, blender)
def countHints(self):
args = self.popallWidth()

BIN
Tests/ttLib/data/I.otf Normal file

Binary file not shown.

View File

@ -6,82 +6,116 @@ import pytest
class TTGlyphSetTest(object):
@staticmethod
def getpath(testfile):
path = os.path.dirname(__file__)
return os.path.join(path, "data", testfile)
@pytest.mark.parametrize(
"location, expected",
"fontfile, location, expected",
[
(
"I.ttf",
None,
[
('moveTo', ((175, 0),)),
('lineTo', ((367, 0),)),
('lineTo', ((367, 1456),)),
('lineTo', ((175, 1456),)),
('closePath', ())
]
("moveTo", ((175, 0),)),
("lineTo", ((367, 0),)),
("lineTo", ((367, 1456),)),
("lineTo", ((175, 1456),)),
("closePath", ()),
],
),
(
"I.ttf",
{},
[
('moveTo', ((175, 0),)),
('lineTo', ((367, 0),)),
('lineTo', ((367, 1456),)),
('lineTo', ((175, 1456),)),
('closePath', ())
]
("moveTo", ((175, 0),)),
("lineTo", ((367, 0),)),
("lineTo", ((367, 1456),)),
("lineTo", ((175, 1456),)),
("closePath", ()),
],
),
(
{'wght': 100},
"I.ttf",
{"wght": 100},
[
('moveTo', ((175, 0),)),
('lineTo', ((271, 0),)),
('lineTo', ((271, 1456),)),
('lineTo', ((175, 1456),)),
('closePath', ())
]
("moveTo", ((175, 0),)),
("lineTo", ((271, 0),)),
("lineTo", ((271, 1456),)),
("lineTo", ((175, 1456),)),
("closePath", ()),
],
),
(
{'wght': 1000},
"I.ttf",
{"wght": 1000},
[
('moveTo', ((128, 0),)),
('lineTo', ((550, 0),)),
('lineTo', ((550, 1456),)),
('lineTo', ((128, 1456),)),
('closePath', ())
]
("moveTo", ((128, 0),)),
("lineTo", ((550, 0),)),
("lineTo", ((550, 1456),)),
("lineTo", ((128, 1456),)),
("closePath", ()),
],
),
(
{'wght': 1000, 'wdth': 25},
"I.ttf",
{"wght": 1000, "wdth": 25},
[
('moveTo', ((140, 0),)),
('lineTo', ((553, 0),)),
('lineTo', ((553, 1456),)),
('lineTo', ((140, 1456),)),
('closePath', ())
]
("moveTo", ((140, 0),)),
("lineTo", ((553, 0),)),
("lineTo", ((553, 1456),)),
("lineTo", ((140, 1456),)),
("closePath", ()),
],
),
(
{'wght': 1000, 'wdth': 50},
"I.ttf",
{"wght": 1000, "wdth": 50},
[
('moveTo', ((136, 0),)),
('lineTo', ((552, 0),)),
('lineTo', ((552, 1456),)),
('lineTo', ((136, 1456),)),
('closePath', ())
]
("moveTo", ((136, 0),)),
("lineTo", ((552, 0),)),
("lineTo", ((552, 1456),)),
("lineTo", ((136, 1456),)),
("closePath", ()),
],
),
]
(
"I.otf",
{"wght": 1000},
[
("moveTo", ((179, 74),)),
("lineTo", ((28, 59),)),
("lineTo", ((28, 0),)),
("lineTo", ((367, 0),)),
("lineTo", ((367, 59),)),
("lineTo", ((212, 74),)),
("lineTo", ((179, 74),)),
("closePath", ()),
("moveTo", ((179, 578),)),
("lineTo", ((212, 578),)),
("lineTo", ((367, 593),)),
("lineTo", ((367, 652),)),
("lineTo", ((28, 652),)),
("lineTo", ((28, 593),)),
("lineTo", ((179, 578),)),
("closePath", ()),
("moveTo", ((98, 310),)),
("curveTo", ((98, 205), (98, 101), (95, 0))),
("lineTo", ((299, 0),)),
("curveTo", ((296, 103), (296, 207), (296, 311))),
("lineTo", ((296, 342),)),
("curveTo", ((296, 447), (296, 551), (299, 652))),
("lineTo", ((95, 652),)),
("curveTo", ((98, 549), (98, 445), (98, 342))),
("lineTo", ((98, 310),)),
("closePath", ()),
],
),
],
)
def test_glyphset(
self, location, expected
):
# TODO: also test loading CFF-flavored fonts
font = TTFont(self.getpath("I.ttf"))
def test_glyphset(self, fontfile, location, expected):
font = TTFont(self.getpath(fontfile))
glyphset = font.getGlyphSet(location=location)
assert isinstance(glyphset, ttGlyphSet._TTGlyphSet)
@ -96,17 +130,22 @@ class TTGlyphSetTest(object):
assert len(glyphset) == 2
pen = RecordingPen()
glyph = glyphset['I']
glyph = glyphset["I"]
assert glyphset.get("foobar") is None
assert isinstance(glyph, ttGlyphSet._TTGlyph)
is_glyf = fontfile.endswith(".ttf")
if location:
assert isinstance(glyph, ttGlyphSet._TTVarGlyphGlyf)
glyphType = (
ttGlyphSet._TTVarGlyphGlyf if is_glyf else ttGlyphSet._TTVarGlyphCFF
)
else:
assert isinstance(glyph, ttGlyphSet._TTGlyphGlyf)
glyphType = ttGlyphSet._TTGlyphGlyf if is_glyf else ttGlyphSet._TTGlyphCFF
assert isinstance(glyph, glyphType)
glyph.draw(pen)
actual = pen.value
print(actual)
assert actual == expected, (location, actual, expected)