aboutsummaryrefslogtreecommitdiffstats
path: root/contrib
diff options
context:
space:
mode:
authorrobot-piglet <robot-piglet@yandex-team.com>2024-06-08 08:51:43 +0300
committerrobot-piglet <robot-piglet@yandex-team.com>2024-06-08 09:02:27 +0300
commitc3763910c6f878b17102c94e0b62cab57c9954b8 (patch)
tree19de35706beeb1adc158f13ca87110e110f1a753 /contrib
parent63965219325c32d9b72b466b89fc613295746336 (diff)
downloadydb-c3763910c6f878b17102c94e0b62cab57c9954b8.tar.gz
Intermediate changes
Diffstat (limited to 'contrib')
-rw-r--r--contrib/python/fonttools/.dist-info/METADATA29
-rw-r--r--contrib/python/fonttools/fontTools/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py165
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py287
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/__init__.py225
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/specializer.py10
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/transforms.py482
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/width.py3
-rw-r--r--contrib/python/fonttools/fontTools/cu2qu/__main__.py2
-rw-r--r--contrib/python/fonttools/fontTools/cu2qu/benchmark.py1
-rw-r--r--contrib/python/fonttools/fontTools/cu2qu/cli.py2
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/builder.py7
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/variableScalar.py3
-rw-r--r--contrib/python/fonttools/fontTools/fontBuilder.py6
-rw-r--r--contrib/python/fonttools/fontTools/help.py3
-rw-r--r--contrib/python/fonttools/fontTools/merge/tables.py4
-rw-r--r--contrib/python/fonttools/fontTools/misc/bezierTools.py5
-rw-r--r--contrib/python/fonttools/fontTools/misc/iterTools.py12
-rw-r--r--contrib/python/fonttools/fontTools/misc/lazyTools.py42
-rw-r--r--contrib/python/fonttools/fontTools/misc/psCharStrings.py20
-rw-r--r--contrib/python/fonttools/fontTools/misc/sstruct.py21
-rw-r--r--contrib/python/fonttools/fontTools/misc/transform.py13
-rw-r--r--contrib/python/fonttools/fontTools/pens/svgPathPen.py10
-rw-r--r--contrib/python/fonttools/fontTools/qu2cu/__main__.py2
-rw-r--r--contrib/python/fonttools/fontTools/qu2cu/benchmark.py1
-rw-r--r--contrib/python/fonttools/fontTools/qu2cu/cli.py2
-rw-r--r--contrib/python/fonttools/fontTools/subset/__init__.py127
-rw-r--r--contrib/python/fonttools/fontTools/subset/cff.py368
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/__main__.py9
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/scaleUpem.py145
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py4
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py52
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py5
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py51
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py3
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py465
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py67
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py5
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/otBase.py5
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py235
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/otData.py166
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/otTables.py402
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/ttFont.py34
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py188
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/woff2.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttx.py2
-rw-r--r--contrib/python/fonttools/fontTools/varLib/__init__.py5
-rw-r--r--contrib/python/fonttools/fontTools/varLib/builder.py58
-rw-r--r--contrib/python/fonttools/fontTools/varLib/cff.py89
-rw-r--r--contrib/python/fonttools/fontTools/varLib/instancer/__init__.py405
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatable.py21
-rw-r--r--contrib/python/fonttools/fontTools/varLib/models.py11
-rw-r--r--contrib/python/fonttools/fontTools/varLib/multiVarStore.py253
-rw-r--r--contrib/python/fonttools/fontTools/varLib/mutator.py2
-rw-r--r--contrib/python/fonttools/fontTools/varLib/varStore.py82
-rw-r--r--contrib/python/fonttools/ya.make9
-rw-r--r--contrib/python/zope.interface/py3/.dist-info/METADATA13
-rw-r--r--contrib/python/zope.interface/py3/ya.make2
60 files changed, 3199 insertions, 1449 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA
index 60b6e6df80..6a00aa9e47 100644
--- a/contrib/python/fonttools/.dist-info/METADATA
+++ b/contrib/python/fonttools/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.51.0
+Version: 4.52.1
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -375,6 +375,33 @@ Have fun!
Changelog
~~~~~~~~~
+4.52.1 (released 2024-05-24)
+----------------------------
+
+- Fixed a small syntax error in the reStructuredText-formatted NEWS.rst file
+ which caused the upload to PyPI to fail for 4.52.0. No other code changes.
+
+4.52.0 (released 2024-05-24)
+----------------------------
+
+- Added support for the new ``VARC`` (Variable Composite) table that is being
+ proposed to OpenType spec (#3395). For more info:
+ https://github.com/harfbuzz/boring-expansion-spec/blob/main/VARC.md
+- [ttLib.__main__] Fixed decompiling all tables (90fed08).
+- [feaLib] Don't reference the same lookup index multiple times within the same
+ feature record, it is only applied once anyway (#3520).
+- [cffLib] Moved methods to desubroutinize, remove hints and unused subroutines
+ from subset module to cffLib (#3517).
+- [varLib.instancer] Added support for partial-instancing CFF2 tables! Also, added
+ method to down-convert from CFF2 to CFF 1.0, and CLI entry points to convert
+ CFF<->CFF2 (#3506).
+- [subset] Prune unused user name IDs even with --name-IDs='*' (#3410).
+- [ttx] use GNU-style getopt to intermix options and positional arguments (#3509).
+- [feaLib.variableScalar] Fixed ``value_at_location()`` method (#3491)
+- [psCharStrings] Shorten output of ``encodeFloat`` (#3492).
+- [bezierTools] Fix infinite-recursion in ``calcCubicArcLength`` (#3502).
+- [avar2] Implement ``avar2`` support in ``TTFont.getGlyphSet()`` (#3473).
+
4.51.0 (released 2024-04-05)
----------------------------
diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py
index 5621f391f9..56dd7303e0 100644
--- a/contrib/python/fonttools/fontTools/__init__.py
+++ b/contrib/python/fonttools/fontTools/__init__.py
@@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.51.0"
+version = __version__ = "4.52.1"
__all__ = ["version", "log", "configLogger"]
diff --git a/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py
new file mode 100644
index 0000000000..5dc48a7fcb
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py
@@ -0,0 +1,165 @@
+"""CFF2 to CFF converter."""
+
+from fontTools.ttLib import TTFont, newTable
+from fontTools.misc.cliTools import makeOutputFileName
+from fontTools.cffLib import TopDictIndex, buildOrder, topDictOperators
+from .width import optimizeWidths
+from collections import defaultdict
+import logging
+
+
+__all__ = ["convertCFF2ToCFF", "main"]
+
+
+log = logging.getLogger("fontTools.cffLib")
+
+
+def _convertCFF2ToCFF(cff, otFont):
+ """Converts this object from CFF2 format to CFF format. This conversion
+ is done 'in-place'. The conversion cannot be reversed.
+
+ The CFF2 font cannot be variable. (TODO Accept those and convert to the
+ default instance?)
+
+ This assumes a decompiled CFF table. (i.e. that the object has been
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
+
+ cff.major = 1
+
+ topDictData = TopDictIndex(None, isCFF2=True)
+ for item in cff.topDictIndex:
+ # Iterate over, such that all are decompiled
+ topDictData.append(item)
+ cff.topDictIndex = topDictData
+ topDict = topDictData[0]
+
+ if hasattr(topDict, "VarStore"):
+ raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
+
+ if hasattr(topDict, "Private"):
+ privateDict = topDict.Private
+ else:
+ privateDict = None
+ opOrder = buildOrder(topDictOperators)
+ topDict.order = opOrder
+
+ fdArray = topDict.FDArray
+ charStrings = topDict.CharStrings
+
+ for cs in charStrings.values():
+ cs.decompile()
+ cs.program.append("endchar")
+ for subrSets in [cff.GlobalSubrs] + [
+ getattr(fd.Private, "Subrs", []) for fd in fdArray
+ ]:
+ for cs in subrSets:
+ cs.program.append("return")
+
+ # Add (optimal) width to CharStrings that need it.
+ widths = defaultdict(list)
+ metrics = otFont["hmtx"].metrics
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ if fdIndex == None:
+ fdIndex = 0
+ widths[fdIndex].append(metrics[glyphName][0])
+ for fdIndex, widthList in widths.items():
+ bestDefault, bestNominal = optimizeWidths(widthList)
+ private = fdArray[fdIndex].Private
+ private.defaultWidthX = bestDefault
+ private.nominalWidthX = bestNominal
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ if fdIndex == None:
+ fdIndex = 0
+ private = fdArray[fdIndex].Private
+ width = metrics[glyphName][0]
+ if width != private.defaultWidthX:
+ cs.program.insert(0, width - private.nominalWidthX)
+
+
+def convertCFF2ToCFF(font, *, updatePostTable=True):
+ cff = font["CFF2"].cff
+ _convertCFF2ToCFF(cff, font)
+ del font["CFF2"]
+ table = font["CFF "] = newTable("CFF ")
+ table.cff = cff
+
+ if updatePostTable and "post" in font:
+ # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
+ post = font["post"]
+ if post.formatType == 2.0:
+ post.formatType = 3.0
+
+
+def main(args=None):
+ """Convert CFF OTF font to CFF2 OTF font"""
+ if args is None:
+ import sys
+
+ args = sys.argv[1:]
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.CFFToCFF2",
+ description="Upgrade a CFF font to CFF2.",
+ )
+ parser.add_argument(
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ metavar="OUTPUT.ttf",
+ default=None,
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
+ )
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
+ loggingGroup.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ loggingGroup.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ options = parser.parse_args(args)
+
+ from fontTools import configLogger
+
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ import os
+
+ infile = options.input
+ if not os.path.isfile(infile):
+ parser.error("No such file '{}'".format(infile))
+
+ outfile = (
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF")
+ if not options.output
+ else options.output
+ )
+
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
+
+ convertCFF2ToCFF(font)
+
+ log.info(
+ "Saving %s",
+ outfile,
+ )
+ font.save(outfile)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(main(sys.argv[1:]))
diff --git a/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py
new file mode 100644
index 0000000000..78347c6667
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py
@@ -0,0 +1,287 @@
+"""CFF to CFF2 converter."""
+
+from fontTools.ttLib import TTFont, newTable
+from fontTools.misc.cliTools import makeOutputFileName
+from fontTools.misc.psCharStrings import T2WidthExtractor
+from fontTools.cffLib import (
+ TopDictIndex,
+ FDArrayIndex,
+ FontDict,
+ buildOrder,
+ topDictOperators,
+ privateDictOperators,
+ topDictOperators2,
+ privateDictOperators2,
+)
+from io import BytesIO
+import logging
+
+__all__ = ["convertCFFToCFF2", "main"]
+
+
+log = logging.getLogger("fontTools.cffLib")
+
+
+class _NominalWidthUsedError(Exception):
+ def __add__(self, other):
+ raise self
+
+ def __radd__(self, other):
+ raise self
+
+
+def _convertCFFToCFF2(cff, otFont):
+ """Converts this object from CFF format to CFF2 format. This conversion
+ is done 'in-place'. The conversion cannot be reversed.
+
+ This assumes a decompiled CFF table. (i.e. that the object has been
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
+
+ # Clean up T2CharStrings
+
+ topDict = cff.topDictIndex[0]
+ fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None
+ charStrings = topDict.CharStrings
+ globalSubrs = cff.GlobalSubrs
+ localSubrs = [getattr(fd.Private, "Subrs", []) for fd in fdArray] if fdArray else []
+
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ cs.decompile()
+
+ # Clean up subroutines first
+ for subrs in [globalSubrs] + localSubrs:
+ for subr in subrs:
+ program = subr.program
+ i = j = len(program)
+ try:
+ i = program.index("return")
+ except ValueError:
+ pass
+ try:
+ j = program.index("endchar")
+ except ValueError:
+ pass
+ program[min(i, j) :] = []
+
+ # Clean up glyph charstrings
+ removeUnusedSubrs = False
+ nominalWidthXError = _NominalWidthUsedError()
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ program = cs.program
+ if fdIndex == None:
+ fdIndex = 0
+
+ # Intentionally use custom type for nominalWidthX, such that any
+ # CharString that has an explicit width encoded will throw back to us.
+ extractor = T2WidthExtractor(
+ localSubrs[fdIndex] if localSubrs else [],
+ globalSubrs,
+ nominalWidthXError,
+ 0,
+ )
+ try:
+ extractor.execute(cs)
+ except _NominalWidthUsedError:
+ # Program has explicit width. We want to drop it, but can't
+ # just pop the first number since it may be a subroutine call.
+ # Instead, when seeing that, we embed the subroutine and recurse.
+ # If this ever happened, we later prune unused subroutines.
+ while program[1] in ["callsubr", "callgsubr"]:
+ removeUnusedSubrs = True
+ subrNumber = program.pop(0)
+ op = program.pop(0)
+ bias = extractor.localBias if op == "callsubr" else extractor.globalBias
+ subrNumber += bias
+ subrSet = localSubrs[fdIndex] if op == "callsubr" else globalSubrs
+ subrProgram = subrSet[subrNumber].program
+ program[:0] = subrProgram
+ # Now pop the actual width
+ program.pop(0)
+
+ if program and program[-1] == "endchar":
+ program.pop()
+
+ if removeUnusedSubrs:
+ cff.remove_unused_subroutines()
+
+ # Upconvert TopDict
+
+ cff.major = 2
+ cff2GetGlyphOrder = cff.otFont.getGlyphOrder
+ topDictData = TopDictIndex(None, cff2GetGlyphOrder)
+ for item in cff.topDictIndex:
+ # Iterate over, such that all are decompiled
+ topDictData.append(item)
+ cff.topDictIndex = topDictData
+ topDict = topDictData[0]
+ if hasattr(topDict, "Private"):
+ privateDict = topDict.Private
+ else:
+ privateDict = None
+ opOrder = buildOrder(topDictOperators2)
+ topDict.order = opOrder
+ topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
+
+ if not hasattr(topDict, "FDArray"):
+ fdArray = topDict.FDArray = FDArrayIndex()
+ fdArray.strings = None
+ fdArray.GlobalSubrs = topDict.GlobalSubrs
+ topDict.GlobalSubrs.fdArray = fdArray
+ charStrings = topDict.CharStrings
+ if charStrings.charStringsAreIndexed:
+ charStrings.charStringsIndex.fdArray = fdArray
+ else:
+ charStrings.fdArray = fdArray
+ fontDict = FontDict()
+ fontDict.setCFF2(True)
+ fdArray.append(fontDict)
+ fontDict.Private = privateDict
+ privateOpOrder = buildOrder(privateDictOperators2)
+ if privateDict is not None:
+ for entry in privateDictOperators:
+ key = entry[1]
+ if key not in privateOpOrder:
+ if key in privateDict.rawDict:
+ # print "Removing private dict", key
+ del privateDict.rawDict[key]
+ if hasattr(privateDict, key):
+ delattr(privateDict, key)
+ # print "Removing privateDict attr", key
+ else:
+ # clean up the PrivateDicts in the fdArray
+ fdArray = topDict.FDArray
+ privateOpOrder = buildOrder(privateDictOperators2)
+ for fontDict in fdArray:
+ fontDict.setCFF2(True)
+ for key in list(fontDict.rawDict.keys()):
+ if key not in fontDict.order:
+ del fontDict.rawDict[key]
+ if hasattr(fontDict, key):
+ delattr(fontDict, key)
+
+ privateDict = fontDict.Private
+ for entry in privateDictOperators:
+ key = entry[1]
+ if key not in privateOpOrder:
+ if key in list(privateDict.rawDict.keys()):
+ # print "Removing private dict", key
+ del privateDict.rawDict[key]
+ if hasattr(privateDict, key):
+ delattr(privateDict, key)
+ # print "Removing privateDict attr", key
+
+ # Now delete up the deprecated topDict operators from CFF 1.0
+ for entry in topDictOperators:
+ key = entry[1]
+ # We seem to need to keep the charset operator for now,
+ # or we fail to compile with some fonts, like AdditionFont.otf.
+ # I don't know which kind of CFF font those are. But keeping
+ # charset seems to work. It will be removed when we save and
+ # read the font again.
+ #
+ # AdditionFont.otf has <Encoding name="StandardEncoding"/>.
+ if key == "charset":
+ continue
+ if key not in opOrder:
+ if key in topDict.rawDict:
+ del topDict.rawDict[key]
+ if hasattr(topDict, key):
+ delattr(topDict, key)
+
+ # TODO(behdad): What does the following comment even mean? Both CFF and CFF2
+ # use the same T2Charstring class. I *think* what it means is that the CharStrings
+ # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc
+ # on them. At least that's what I understand. It's probably safe to remove this
+ # and just set vstore where needed.
+ #
+ # See comment above about charset as well.
+
+ # At this point, the Subrs and Charstrings are all still T2Charstring class
+ # easiest to fix this by compiling, then decompiling again
+ file = BytesIO()
+ cff.compile(file, otFont, isCFF2=True)
+ file.seek(0)
+ cff.decompile(file, otFont, isCFF2=True)
+
+
+def convertCFFToCFF2(font):
+ cff = font["CFF "].cff
+ del font["CFF "]
+ _convertCFFToCFF2(cff, font)
+ table = font["CFF2"] = newTable("CFF2")
+ table.cff = cff
+
+
+def main(args=None):
+ """Convert CFF OTF font to CFF2 OTF font"""
+ if args is None:
+ import sys
+
+ args = sys.argv[1:]
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.CFFToCFF2",
+ description="Upgrade a CFF font to CFF2.",
+ )
+ parser.add_argument(
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ metavar="OUTPUT.ttf",
+ default=None,
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
+ )
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
+ loggingGroup.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ loggingGroup.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ options = parser.parse_args(args)
+
+ from fontTools import configLogger
+
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ import os
+
+ infile = options.input
+ if not os.path.isfile(infile):
+ parser.error("No such file '{}'".format(infile))
+
+ outfile = (
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF2")
+ if not options.output
+ else options.output
+ )
+
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
+
+ convertCFFToCFF2(font)
+
+ log.info(
+ "Saving %s",
+ outfile,
+ )
+ font.save(outfile)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(main(sys.argv[1:]))
diff --git a/contrib/python/fonttools/fontTools/cffLib/__init__.py b/contrib/python/fonttools/fontTools/cffLib/__init__.py
index 0ad41c5674..9cfdebaa11 100644
--- a/contrib/python/fonttools/fontTools/cffLib/__init__.py
+++ b/contrib/python/fonttools/fontTools/cffLib/__init__.py
@@ -45,96 +45,6 @@ maxStackLimit = 513
# maxstack operator has been deprecated. max stack is now always 513.
-class StopHintCountEvent(Exception):
- pass
-
-
-class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
- stop_hintcount_ops = (
- "op_hintmask",
- "op_cntrmask",
- "op_rmoveto",
- "op_hmoveto",
- "op_vmoveto",
- )
-
- def __init__(self, localSubrs, globalSubrs, private=None):
- psCharStrings.SimpleT2Decompiler.__init__(
- self, localSubrs, globalSubrs, private
- )
-
- def execute(self, charString):
- self.need_hintcount = True # until proven otherwise
- for op_name in self.stop_hintcount_ops:
- setattr(self, op_name, self.stop_hint_count)
-
- if hasattr(charString, "_desubroutinized"):
- # If a charstring has already been desubroutinized, we will still
- # need to execute it if we need to count hints in order to
- # compute the byte length for mask arguments, and haven't finished
- # counting hints pairs.
- if self.need_hintcount and self.callingStack:
- try:
- psCharStrings.SimpleT2Decompiler.execute(self, charString)
- except StopHintCountEvent:
- del self.callingStack[-1]
- return
-
- charString._patches = []
- psCharStrings.SimpleT2Decompiler.execute(self, charString)
- desubroutinized = charString.program[:]
- for idx, expansion in reversed(charString._patches):
- assert idx >= 2
- assert desubroutinized[idx - 1] in [
- "callsubr",
- "callgsubr",
- ], desubroutinized[idx - 1]
- assert type(desubroutinized[idx - 2]) == int
- if expansion[-1] == "return":
- expansion = expansion[:-1]
- desubroutinized[idx - 2 : idx] = expansion
- if not self.private.in_cff2:
- if "endchar" in desubroutinized:
- # Cut off after first endchar
- desubroutinized = desubroutinized[
- : desubroutinized.index("endchar") + 1
- ]
- else:
- if not len(desubroutinized) or desubroutinized[-1] != "return":
- desubroutinized.append("return")
-
- charString._desubroutinized = desubroutinized
- del charString._patches
-
- def op_callsubr(self, index):
- subr = self.localSubrs[self.operandStack[-1] + self.localBias]
- psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
- self.processSubr(index, subr)
-
- def op_callgsubr(self, index):
- subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
- psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
- self.processSubr(index, subr)
-
- def stop_hint_count(self, *args):
- self.need_hintcount = False
- for op_name in self.stop_hintcount_ops:
- setattr(self, op_name, None)
- cs = self.callingStack[-1]
- if hasattr(cs, "_desubroutinized"):
- raise StopHintCountEvent()
-
- def op_hintmask(self, index):
- psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
- if self.need_hintcount:
- self.stop_hint_count()
-
- def processSubr(self, index, subr):
- cs = self.callingStack[-1]
- if not hasattr(cs, "_desubroutinized"):
- cs._patches.append((index, subr._desubroutinized))
-
-
class CFFFontSet(object):
"""A CFF font "file" can contain more than one font, although this is
extremely rare (and not allowed within OpenType fonts).
@@ -389,115 +299,29 @@ class CFFFontSet(object):
self.minor = int(attrs["value"])
def convertCFFToCFF2(self, otFont):
- """Converts this object from CFF format to CFF2 format. This conversion
- is done 'in-place'. The conversion cannot be reversed.
-
- This assumes a decompiled CFF table. (i.e. that the object has been
- filled via :meth:`decompile`.)"""
- self.major = 2
- cff2GetGlyphOrder = self.otFont.getGlyphOrder
- topDictData = TopDictIndex(None, cff2GetGlyphOrder)
- topDictData.items = self.topDictIndex.items
- self.topDictIndex = topDictData
- topDict = topDictData[0]
- if hasattr(topDict, "Private"):
- privateDict = topDict.Private
- else:
- privateDict = None
- opOrder = buildOrder(topDictOperators2)
- topDict.order = opOrder
- topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
- for entry in topDictOperators:
- key = entry[1]
- if key not in opOrder:
- if key in topDict.rawDict:
- del topDict.rawDict[key]
- if hasattr(topDict, key):
- delattr(topDict, key)
-
- if not hasattr(topDict, "FDArray"):
- fdArray = topDict.FDArray = FDArrayIndex()
- fdArray.strings = None
- fdArray.GlobalSubrs = topDict.GlobalSubrs
- topDict.GlobalSubrs.fdArray = fdArray
- charStrings = topDict.CharStrings
- if charStrings.charStringsAreIndexed:
- charStrings.charStringsIndex.fdArray = fdArray
- else:
- charStrings.fdArray = fdArray
- fontDict = FontDict()
- fontDict.setCFF2(True)
- fdArray.append(fontDict)
- fontDict.Private = privateDict
- privateOpOrder = buildOrder(privateDictOperators2)
- for entry in privateDictOperators:
- key = entry[1]
- if key not in privateOpOrder:
- if key in privateDict.rawDict:
- # print "Removing private dict", key
- del privateDict.rawDict[key]
- if hasattr(privateDict, key):
- delattr(privateDict, key)
- # print "Removing privateDict attr", key
- else:
- # clean up the PrivateDicts in the fdArray
- fdArray = topDict.FDArray
- privateOpOrder = buildOrder(privateDictOperators2)
- for fontDict in fdArray:
- fontDict.setCFF2(True)
- for key in fontDict.rawDict.keys():
- if key not in fontDict.order:
- del fontDict.rawDict[key]
- if hasattr(fontDict, key):
- delattr(fontDict, key)
-
- privateDict = fontDict.Private
- for entry in privateDictOperators:
- key = entry[1]
- if key not in privateOpOrder:
- if key in privateDict.rawDict:
- # print "Removing private dict", key
- del privateDict.rawDict[key]
- if hasattr(privateDict, key):
- delattr(privateDict, key)
- # print "Removing privateDict attr", key
- # At this point, the Subrs and Charstrings are all still T2Charstring class
- # easiest to fix this by compiling, then decompiling again
- file = BytesIO()
- self.compile(file, otFont, isCFF2=True)
- file.seek(0)
- self.decompile(file, otFont, isCFF2=True)
+ from .CFFToCFF2 import _convertCFFToCFF2
+
+ _convertCFFToCFF2(self, otFont)
+
+ def convertCFF2ToCFF(self, otFont):
+ from .CFF2ToCFF import _convertCFF2ToCFF
+
+ _convertCFF2ToCFF(self, otFont)
def desubroutinize(self):
- for fontName in self.fontNames:
- font = self[fontName]
- cs = font.CharStrings
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- c.decompile()
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _DesubroutinizingT2Decompiler(
- subrs, c.globalSubrs, c.private
- )
- decompiler.execute(c)
- c.program = c._desubroutinized
- del c._desubroutinized
- # Delete all the local subrs
- if hasattr(font, "FDArray"):
- for fd in font.FDArray:
- pd = fd.Private
- if hasattr(pd, "Subrs"):
- del pd.Subrs
- if "Subrs" in pd.rawDict:
- del pd.rawDict["Subrs"]
- else:
- pd = font.Private
- if hasattr(pd, "Subrs"):
- del pd.Subrs
- if "Subrs" in pd.rawDict:
- del pd.rawDict["Subrs"]
- # as well as the global subrs
- self.GlobalSubrs.clear()
+ from .transforms import desubroutinize
+
+ desubroutinize(self)
+
+ def remove_hints(self):
+ from .transforms import remove_hints
+
+ remove_hints(self)
+
+ def remove_unused_subroutines(self):
+ from .transforms import remove_unused_subroutines
+
+ remove_unused_subroutines(self)
class CFFWriter(object):
@@ -764,8 +588,8 @@ class Index(object):
compilerClass = IndexCompiler
def __init__(self, file=None, isCFF2=None):
- assert (isCFF2 is None) == (file is None)
self.items = []
+ self.offsets = offsets = []
name = self.__class__.__name__
if file is None:
return
@@ -782,7 +606,6 @@ class Index(object):
offSize = readCard8(file)
log.log(DEBUG, " index count: %s offSize: %s", count, offSize)
assert offSize <= 4, "offSize too large: %s" % offSize
- self.offsets = offsets = []
pad = b"\0" * (4 - offSize)
for index in range(count + 1):
chunk = file.read(offSize)
@@ -960,7 +783,6 @@ class TopDictIndex(Index):
compilerClass = TopDictIndexCompiler
def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None):
- assert (isCFF2 is None) == (file is None)
self.cff2GetGlyphOrder = cff2GetGlyphOrder
if file is not None and isCFF2:
self._isCFF2 = isCFF2
@@ -1050,6 +872,7 @@ class VarStoreData(object):
reader = OTTableReader(self.data, globalState)
self.otVarStore = ot.VarStore()
self.otVarStore.decompile(reader, self.font)
+ self.data = None
return self
def compile(self):
@@ -2860,9 +2683,11 @@ class PrivateDict(BaseDict):
# Provide dummy values. This avoids needing to provide
# an isCFF2 state in a lot of places.
self.nominalWidthX = self.defaultWidthX = None
+ self._isCFF2 = True
else:
self.defaults = buildDefaults(privateDictOperators)
self.order = buildOrder(privateDictOperators)
+ self._isCFF2 = False
@property
def in_cff2(self):
diff --git a/contrib/python/fonttools/fontTools/cffLib/specializer.py b/contrib/python/fonttools/fontTools/cffLib/specializer.py
index efc15af704..bb7f89e4ff 100644
--- a/contrib/python/fonttools/fontTools/cffLib/specializer.py
+++ b/contrib/python/fonttools/fontTools/cffLib/specializer.py
@@ -43,10 +43,8 @@ def programToCommands(program, getNumRegions=None):
hintmask/cntrmask argument, as well as stray arguments at the end of the
program (🤷).
'getNumRegions' may be None, or a callable object. It must return the
- number of regions. 'getNumRegions' takes a single argument, vsindex. If
- the vsindex argument is None, getNumRegions returns the default number
- of regions for the charstring, else it returns the numRegions for
- the vsindex.
+ number of regions. 'getNumRegions' takes a single argument, vsindex. It
+ returns the numRegions for the vsindex.
The Charstring may or may not start with a width value. If the first
non-blend operator has an odd number of arguments, then the first argument is
a width, and is popped off. This is complicated with blend operators, as
@@ -61,7 +59,7 @@ def programToCommands(program, getNumRegions=None):
"""
seenWidthOp = False
- vsIndex = None
+ vsIndex = 0
lenBlendStack = 0
lastBlendIndex = 0
commands = []
@@ -813,7 +811,7 @@ if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
- "fonttools cffLib.specialer",
+ "fonttools cffLib.specializer",
description="CFF CharString generalizer/specializer",
)
parser.add_argument("program", metavar="command", nargs="*", help="Commands.")
diff --git a/contrib/python/fonttools/fontTools/cffLib/transforms.py b/contrib/python/fonttools/fontTools/cffLib/transforms.py
new file mode 100644
index 0000000000..0772d85e21
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/cffLib/transforms.py
@@ -0,0 +1,482 @@
+from fontTools.misc.psCharStrings import (
+ SimpleT2Decompiler,
+ T2WidthExtractor,
+ calcSubrBias,
+)
+
+
+def _uniq_sort(l):
+ return sorted(set(l))
+
+
+class StopHintCountEvent(Exception):
+ pass
+
+
+class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
+ stop_hintcount_ops = (
+ "op_hintmask",
+ "op_cntrmask",
+ "op_rmoveto",
+ "op_hmoveto",
+ "op_vmoveto",
+ )
+
+ def __init__(self, localSubrs, globalSubrs, private=None):
+ SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
+
+ def execute(self, charString):
+ self.need_hintcount = True # until proven otherwise
+ for op_name in self.stop_hintcount_ops:
+ setattr(self, op_name, self.stop_hint_count)
+
+ if hasattr(charString, "_desubroutinized"):
+ # If a charstring has already been desubroutinized, we will still
+ # need to execute it if we need to count hints in order to
+ # compute the byte length for mask arguments, and haven't finished
+ # counting hints pairs.
+ if self.need_hintcount and self.callingStack:
+ try:
+ SimpleT2Decompiler.execute(self, charString)
+ except StopHintCountEvent:
+ del self.callingStack[-1]
+ return
+
+ charString._patches = []
+ SimpleT2Decompiler.execute(self, charString)
+ desubroutinized = charString.program[:]
+ for idx, expansion in reversed(charString._patches):
+ assert idx >= 2
+ assert desubroutinized[idx - 1] in [
+ "callsubr",
+ "callgsubr",
+ ], desubroutinized[idx - 1]
+ assert type(desubroutinized[idx - 2]) == int
+ if expansion[-1] == "return":
+ expansion = expansion[:-1]
+ desubroutinized[idx - 2 : idx] = expansion
+ if not self.private.in_cff2:
+ if "endchar" in desubroutinized:
+ # Cut off after first endchar
+ desubroutinized = desubroutinized[
+ : desubroutinized.index("endchar") + 1
+ ]
+
+ charString._desubroutinized = desubroutinized
+ del charString._patches
+
+ def op_callsubr(self, index):
+ subr = self.localSubrs[self.operandStack[-1] + self.localBias]
+ SimpleT2Decompiler.op_callsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_callgsubr(self, index):
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
+ SimpleT2Decompiler.op_callgsubr(self, index)
+ self.processSubr(index, subr)
+
+ def stop_hint_count(self, *args):
+ self.need_hintcount = False
+ for op_name in self.stop_hintcount_ops:
+ setattr(self, op_name, None)
+ cs = self.callingStack[-1]
+ if hasattr(cs, "_desubroutinized"):
+ raise StopHintCountEvent()
+
+ def op_hintmask(self, index):
+ SimpleT2Decompiler.op_hintmask(self, index)
+ if self.need_hintcount:
+ self.stop_hint_count()
+
+ def processSubr(self, index, subr):
+ cs = self.callingStack[-1]
+ if not hasattr(cs, "_desubroutinized"):
+ cs._patches.append((index, subr._desubroutinized))
+
+
+def desubroutinize(cff):
+ for fontName in cff.fontNames:
+ font = cff[fontName]
+ cs = font.CharStrings
+ for c in cs.values():
+ c.decompile()
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
+ decompiler.execute(c)
+ c.program = c._desubroutinized
+ del c._desubroutinized
+ # Delete all the local subrs
+ if hasattr(font, "FDArray"):
+ for fd in font.FDArray:
+ pd = fd.Private
+ if hasattr(pd, "Subrs"):
+ del pd.Subrs
+ if "Subrs" in pd.rawDict:
+ del pd.rawDict["Subrs"]
+ else:
+ pd = font.Private
+ if hasattr(pd, "Subrs"):
+ del pd.Subrs
+ if "Subrs" in pd.rawDict:
+ del pd.rawDict["Subrs"]
+ # as well as the global subrs
+ cff.GlobalSubrs.clear()
+
+
+class _MarkingT2Decompiler(SimpleT2Decompiler):
+ def __init__(self, localSubrs, globalSubrs, private):
+ SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
+ for subrs in [localSubrs, globalSubrs]:
+ if subrs and not hasattr(subrs, "_used"):
+ subrs._used = set()
+
+ def op_callsubr(self, index):
+ self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
+ SimpleT2Decompiler.op_callsubr(self, index)
+
+ def op_callgsubr(self, index):
+ self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
+ SimpleT2Decompiler.op_callgsubr(self, index)
+
+
+class _DehintingT2Decompiler(T2WidthExtractor):
+ class Hints(object):
+ def __init__(self):
+ # Whether calling this charstring produces any hint stems
+ # Note that if a charstring starts with hintmask, it will
+ # have has_hint set to True, because it *might* produce an
+ # implicit vstem if called under certain conditions.
+ self.has_hint = False
+ # Index to start at to drop all hints
+ self.last_hint = 0
+ # Index up to which we know more hints are possible.
+ # Only relevant if status is 0 or 1.
+ self.last_checked = 0
+ # The status means:
+ # 0: after dropping hints, this charstring is empty
+ # 1: after dropping hints, there may be more hints
+ # continuing after this, or there might be
+ # other things. Not clear yet.
+ # 2: no more hints possible after this charstring
+ self.status = 0
+ # Has hintmask instructions; not recursive
+ self.has_hintmask = False
+ # List of indices of calls to empty subroutines to remove.
+ self.deletions = []
+
+ pass
+
+ def __init__(
+ self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
+ ):
+ self._css = css
+ T2WidthExtractor.__init__(
+ self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
+ )
+ self.private = private
+
+ def execute(self, charString):
+ old_hints = charString._hints if hasattr(charString, "_hints") else None
+ charString._hints = self.Hints()
+
+ T2WidthExtractor.execute(self, charString)
+
+ hints = charString._hints
+
+ if hints.has_hint or hints.has_hintmask:
+ self._css.add(charString)
+
+ if hints.status != 2:
+ # Check from last_check, make sure we didn't have any operators.
+ for i in range(hints.last_checked, len(charString.program) - 1):
+ if isinstance(charString.program[i], str):
+ hints.status = 2
+ break
+ else:
+ hints.status = 1 # There's *something* here
+ hints.last_checked = len(charString.program)
+
+ if old_hints:
+ assert hints.__dict__ == old_hints.__dict__
+
+ def op_callsubr(self, index):
+ subr = self.localSubrs[self.operandStack[-1] + self.localBias]
+ T2WidthExtractor.op_callsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_callgsubr(self, index):
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
+ T2WidthExtractor.op_callgsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_hstem(self, index):
+ T2WidthExtractor.op_hstem(self, index)
+ self.processHint(index)
+
+ def op_vstem(self, index):
+ T2WidthExtractor.op_vstem(self, index)
+ self.processHint(index)
+
+ def op_hstemhm(self, index):
+ T2WidthExtractor.op_hstemhm(self, index)
+ self.processHint(index)
+
+ def op_vstemhm(self, index):
+ T2WidthExtractor.op_vstemhm(self, index)
+ self.processHint(index)
+
+ def op_hintmask(self, index):
+ rv = T2WidthExtractor.op_hintmask(self, index)
+ self.processHintmask(index)
+ return rv
+
+ def op_cntrmask(self, index):
+ rv = T2WidthExtractor.op_cntrmask(self, index)
+ self.processHintmask(index)
+ return rv
+
+ def processHintmask(self, index):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ hints.has_hintmask = True
+ if hints.status != 2:
+ # Check from last_check, see if we may be an implicit vstem
+ for i in range(hints.last_checked, index - 1):
+ if isinstance(cs.program[i], str):
+ hints.status = 2
+ break
+ else:
+ # We are an implicit vstem
+ hints.has_hint = True
+ hints.last_hint = index + 1
+ hints.status = 0
+ hints.last_checked = index + 1
+
+ def processHint(self, index):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ hints.has_hint = True
+ hints.last_hint = index
+ hints.last_checked = index
+
+ def processSubr(self, index, subr):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ subr_hints = subr._hints
+
+ # Check from last_check, make sure we didn't have
+ # any operators.
+ if hints.status != 2:
+ for i in range(hints.last_checked, index - 1):
+ if isinstance(cs.program[i], str):
+ hints.status = 2
+ break
+ hints.last_checked = index
+
+ if hints.status != 2:
+ if subr_hints.has_hint:
+ hints.has_hint = True
+
+ # Decide where to chop off from
+ if subr_hints.status == 0:
+ hints.last_hint = index
+ else:
+ hints.last_hint = index - 2 # Leave the subr call in
+
+ elif subr_hints.status == 0:
+ hints.deletions.append(index)
+
+ hints.status = max(hints.status, subr_hints.status)
+
+
+def _cs_subset_subroutines(charstring, subrs, gsubrs):
+ p = charstring.program
+ for i in range(1, len(p)):
+ if p[i] == "callsubr":
+ assert isinstance(p[i - 1], int)
+ p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
+ elif p[i] == "callgsubr":
+ assert isinstance(p[i - 1], int)
+ p[i - 1] = (
+ gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
+ )
+
+
+def _cs_drop_hints(charstring):
+ hints = charstring._hints
+
+ if hints.deletions:
+ p = charstring.program
+ for idx in reversed(hints.deletions):
+ del p[idx - 2 : idx]
+
+ if hints.has_hint:
+ assert not hints.deletions or hints.last_hint <= hints.deletions[0]
+ charstring.program = charstring.program[hints.last_hint :]
+ if not charstring.program:
+ # TODO CFF2 no need for endchar.
+ charstring.program.append("endchar")
+ if hasattr(charstring, "width"):
+ # Insert width back if needed
+ if charstring.width != charstring.private.defaultWidthX:
+ # For CFF2 charstrings, this should never happen
+ assert (
+ charstring.private.defaultWidthX is not None
+ ), "CFF2 CharStrings must not have an initial width value"
+ charstring.program.insert(
+ 0, charstring.width - charstring.private.nominalWidthX
+ )
+
+ if hints.has_hintmask:
+ i = 0
+ p = charstring.program
+ while i < len(p):
+ if p[i] in ["hintmask", "cntrmask"]:
+ assert i + 1 <= len(p)
+ del p[i : i + 2]
+ continue
+ i += 1
+
+ assert len(charstring.program)
+
+ del charstring._hints
+
+
+def remove_hints(cff):
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ # This can be tricky, but doesn't have to. What we do is:
+ #
+ # - Run all used glyph charstrings and recurse into subroutines,
+ # - For each charstring (including subroutines), if it has any
+ # of the hint stem operators, we mark it as such.
+ # Upon returning, for each charstring we note all the
+ # subroutine calls it makes that (recursively) contain a stem,
+ # - Dropping hinting then consists of the following two ops:
+ # * Drop the piece of the program in each charstring before the
+ # last call to a stem op or a stem-calling subroutine,
+ # * Drop all hintmask operations.
+ # - It's trickier... A hintmask right after hints and a few numbers
+ # will act as an implicit vstemhm. As such, we track whether
+ # we have seen any non-hint operators so far and do the right
+ # thing, recursively... Good luck understanding that :(
+ css = set()
+ for c in cs.values():
+ c.decompile()
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _DehintingT2Decompiler(
+ css,
+ subrs,
+ c.globalSubrs,
+ c.private.nominalWidthX,
+ c.private.defaultWidthX,
+ c.private,
+ )
+ decompiler.execute(c)
+ c.width = decompiler.width
+ for charstring in css:
+ _cs_drop_hints(charstring)
+ del css
+
+ # Drop font-wide hinting values
+ all_privs = []
+ if hasattr(font, "FDArray"):
+ all_privs.extend(fd.Private for fd in font.FDArray)
+ else:
+ all_privs.append(font.Private)
+ for priv in all_privs:
+ for k in [
+ "BlueValues",
+ "OtherBlues",
+ "FamilyBlues",
+ "FamilyOtherBlues",
+ "BlueScale",
+ "BlueShift",
+ "BlueFuzz",
+ "StemSnapH",
+ "StemSnapV",
+ "StdHW",
+ "StdVW",
+ "ForceBold",
+ "LanguageGroup",
+ "ExpansionFactor",
+ ]:
+ if hasattr(priv, k):
+ setattr(priv, k, None)
+ remove_unused_subroutines(cff)
+
+
+def _pd_delete_empty_subrs(private_dict):
+ if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
+ if "Subrs" in private_dict.rawDict:
+ del private_dict.rawDict["Subrs"]
+ del private_dict.Subrs
+
+
+def remove_unused_subroutines(cff):
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ # Renumber subroutines to remove unused ones
+
+ # Mark all used subroutines
+ for c in cs.values():
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
+ decompiler.execute(c)
+
+ all_subrs = [font.GlobalSubrs]
+ if hasattr(font, "FDArray"):
+ all_subrs.extend(
+ fd.Private.Subrs
+ for fd in font.FDArray
+ if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
+ )
+ elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
+ all_subrs.append(font.Private.Subrs)
+
+ subrs = set(subrs) # Remove duplicates
+
+ # Prepare
+ for subrs in all_subrs:
+ if not hasattr(subrs, "_used"):
+ subrs._used = set()
+ subrs._used = _uniq_sort(subrs._used)
+ subrs._old_bias = calcSubrBias(subrs)
+ subrs._new_bias = calcSubrBias(subrs._used)
+
+ # Renumber glyph charstrings
+ for c in cs.values():
+ subrs = getattr(c.private, "Subrs", None)
+ _cs_subset_subroutines(c, subrs, font.GlobalSubrs)
+
+ # Renumber subroutines themselves
+ for subrs in all_subrs:
+ if subrs == font.GlobalSubrs:
+ if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
+ local_subrs = font.Private.Subrs
+ else:
+ local_subrs = None
+ else:
+ local_subrs = subrs
+
+ subrs.items = [subrs.items[i] for i in subrs._used]
+ if hasattr(subrs, "file"):
+ del subrs.file
+ if hasattr(subrs, "offsets"):
+ del subrs.offsets
+
+ for subr in subrs.items:
+ _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)
+
+ # Delete local SubrsIndex if empty
+ if hasattr(font, "FDArray"):
+ for fd in font.FDArray:
+ _pd_delete_empty_subrs(fd.Private)
+ else:
+ _pd_delete_empty_subrs(font.Private)
+
+ # Cleanup
+ for subrs in all_subrs:
+ del subrs._used, subrs._old_bias, subrs._new_bias
diff --git a/contrib/python/fonttools/fontTools/cffLib/width.py b/contrib/python/fonttools/fontTools/cffLib/width.py
index 0ba3ed39bf..78ff27e4fd 100644
--- a/contrib/python/fonttools/fontTools/cffLib/width.py
+++ b/contrib/python/fonttools/fontTools/cffLib/width.py
@@ -13,6 +13,9 @@ from operator import add
from functools import reduce
+__all__ = ["optimizeWidths", "main"]
+
+
class missingdict(dict):
def __init__(self, missing_func):
self.missing_func = missing_func
diff --git a/contrib/python/fonttools/fontTools/cu2qu/__main__.py b/contrib/python/fonttools/fontTools/cu2qu/__main__.py
index 084bf8f960..5205ffeef9 100644
--- a/contrib/python/fonttools/fontTools/cu2qu/__main__.py
+++ b/contrib/python/fonttools/fontTools/cu2qu/__main__.py
@@ -1,5 +1,5 @@
import sys
-from .cli import main
+from .cli import _main as main
if __name__ == "__main__":
diff --git a/contrib/python/fonttools/fontTools/cu2qu/benchmark.py b/contrib/python/fonttools/fontTools/cu2qu/benchmark.py
index 2ab1e966b1..007f75d887 100644
--- a/contrib/python/fonttools/fontTools/cu2qu/benchmark.py
+++ b/contrib/python/fonttools/fontTools/cu2qu/benchmark.py
@@ -45,7 +45,6 @@ def run_benchmark(module, function, setup_suffix="", repeat=5, number=1000):
def main():
- """Benchmark the cu2qu algorithm performance."""
run_benchmark("cu2qu", "curve_to_quadratic")
run_benchmark("cu2qu", "curves_to_quadratic")
diff --git a/contrib/python/fonttools/fontTools/cu2qu/cli.py b/contrib/python/fonttools/fontTools/cu2qu/cli.py
index 9144043ff1..ddc6450200 100644
--- a/contrib/python/fonttools/fontTools/cu2qu/cli.py
+++ b/contrib/python/fonttools/fontTools/cu2qu/cli.py
@@ -64,7 +64,7 @@ def _copytree(input_path, output_path):
shutil.copytree(input_path, output_path)
-def main(args=None):
+def _main(args=None):
"""Convert a UFO font from cubic to quadratic curves"""
parser = argparse.ArgumentParser(prog="cu2qu")
parser.add_argument("--version", action="version", version=fontTools.__version__)
diff --git a/contrib/python/fonttools/fontTools/feaLib/builder.py b/contrib/python/fonttools/fontTools/feaLib/builder.py
index 7921a3f179..a91381ddc1 100644
--- a/contrib/python/fonttools/fontTools/feaLib/builder.py
+++ b/contrib/python/fonttools/fontTools/feaLib/builder.py
@@ -880,8 +880,13 @@ class Builder(object):
# l.lookup_index will be None when a lookup is not needed
# for the table under construction. For example, substitution
# rules will have no lookup_index while building GPOS tables.
+ # We also deduplicate lookup indices, as they only get applied once
+ # within a given feature:
+ # https://github.com/fonttools/fonttools/issues/2946
lookup_indices = tuple(
- [l.lookup_index for l in lookups if l.lookup_index is not None]
+ dict.fromkeys(
+ l.lookup_index for l in lookups if l.lookup_index is not None
+ )
)
size_feature = tag == "GPOS" and feature_tag == "size"
diff --git a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
index c97b435429..96e7c07566 100644
--- a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
+++ b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
@@ -75,10 +75,11 @@ class VariableScalar:
return self.values[key]
def value_at_location(self, location, model_cache=None, avar=None):
- loc = location
+ loc = Location(location)
if loc in self.values.keys():
return self.values[loc]
values = list(self.values.values())
+ loc = dict(self._normalized_location(loc))
return self.model(model_cache, avar).interpolateFromMasters(loc, values)
def model(self, model_cache=None, avar=None):
diff --git a/contrib/python/fonttools/fontTools/fontBuilder.py b/contrib/python/fonttools/fontTools/fontBuilder.py
index dd57a0507d..16b7ee167d 100644
--- a/contrib/python/fonttools/fontTools/fontBuilder.py
+++ b/contrib/python/fonttools/fontTools/fontBuilder.py
@@ -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."
diff --git a/contrib/python/fonttools/fontTools/help.py b/contrib/python/fonttools/fontTools/help.py
index 2a238de3d6..4331a26dde 100644
--- a/contrib/python/fonttools/fontTools/help.py
+++ b/contrib/python/fonttools/fontTools/help.py
@@ -20,7 +20,8 @@ def main():
continue
try:
description = imports.main.__doc__
- if description:
+ # Cython modules seem to return "main()" as the docstring
+ if description and description != "main()":
pkg = pkg.replace("fontTools.", "").replace(".__main__", "")
# show the docstring's first line only
descriptions[pkg] = description.splitlines()[0]
diff --git a/contrib/python/fonttools/fontTools/merge/tables.py b/contrib/python/fonttools/fontTools/merge/tables.py
index 57ed64d337..208a5099ff 100644
--- a/contrib/python/fonttools/fontTools/merge/tables.py
+++ b/contrib/python/fonttools/fontTools/merge/tables.py
@@ -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)
@@ -294,6 +294,8 @@ def merge(self, m, tables):
extractor.execute(c)
width = extractor.width
if width is not defaultWidthXToken:
+ # The following will be wrong if the width is added
+ # by a subroutine. Ouch!
c.program.pop(0)
else:
width = defaultWidthX
diff --git a/contrib/python/fonttools/fontTools/misc/bezierTools.py b/contrib/python/fonttools/fontTools/misc/bezierTools.py
index a1a707b098..5411ff99fd 100644
--- a/contrib/python/fonttools/fontTools/misc/bezierTools.py
+++ b/contrib/python/fonttools/fontTools/misc/bezierTools.py
@@ -18,6 +18,9 @@ except (AttributeError, ImportError):
COMPILED = False
+EPSILON = 1e-9
+
+
Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
@@ -92,7 +95,7 @@ def _split_cubic_into_two(p0, p1, p2, p3):
def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
arch = abs(p0 - p3)
box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3)
- if arch * mult >= box:
+ if arch * mult + EPSILON >= box:
return (arch + box) * 0.5
else:
one, two = _split_cubic_into_two(p0, p1, p2, p3)
diff --git a/contrib/python/fonttools/fontTools/misc/iterTools.py b/contrib/python/fonttools/fontTools/misc/iterTools.py
new file mode 100644
index 0000000000..d7b8305322
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/misc/iterTools.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/misc/lazyTools.py b/contrib/python/fonttools/fontTools/misc/lazyTools.py
new file mode 100644
index 0000000000..91cb80c992
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/misc/lazyTools.py
@@ -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)
diff --git a/contrib/python/fonttools/fontTools/misc/psCharStrings.py b/contrib/python/fonttools/fontTools/misc/psCharStrings.py
index cc9ca01c7f..5d881c5816 100644
--- a/contrib/python/fonttools/fontTools/misc/psCharStrings.py
+++ b/contrib/python/fonttools/fontTools/misc/psCharStrings.py
@@ -275,6 +275,24 @@ def encodeFloat(f):
s = s[1:]
elif s[:3] == "-0.":
s = "-" + s[2:]
+ elif s.endswith("000"):
+ significantDigits = s.rstrip("0")
+ s = "%sE%d" % (significantDigits, len(s) - len(significantDigits))
+ else:
+ dotIndex = s.find(".")
+ eIndex = s.find("E")
+ if dotIndex != -1 and eIndex != -1:
+ integerPart = s[:dotIndex]
+ fractionalPart = s[dotIndex + 1 : eIndex]
+ exponent = int(s[eIndex + 1 :])
+ newExponent = exponent - len(fractionalPart)
+ if newExponent == 1:
+ s = "%s%s0" % (integerPart, fractionalPart)
+ else:
+ s = "%s%sE%d" % (integerPart, fractionalPart, newExponent)
+ if s.startswith((".0", "-.0")):
+ sign, s = s.split(".", 1)
+ s = "%s%sE-%d" % (sign, s.lstrip("0"), len(s))
nibbles = []
while s:
c = s[0]
@@ -286,6 +304,8 @@ def encodeFloat(f):
c = "E-"
elif c2 == "+":
s = s[1:]
+ if s.startswith("0"):
+ s = s[1:]
nibbles.append(realNibblesDict[c])
nibbles.append(0xF)
if len(nibbles) % 2:
diff --git a/contrib/python/fonttools/fontTools/misc/sstruct.py b/contrib/python/fonttools/fontTools/misc/sstruct.py
index d35bc9a5c8..92be275b89 100644
--- a/contrib/python/fonttools/fontTools/misc/sstruct.py
+++ b/contrib/python/fonttools/fontTools/misc/sstruct.py
@@ -64,7 +64,10 @@ def pack(fmt, obj):
elements = []
if not isinstance(obj, dict):
obj = obj.__dict__
- for name in names:
+ string_index = formatstring
+ if formatstring.startswith(">"):
+ string_index = formatstring[1:]
+ for ix, name in enumerate(names.keys()):
value = obj[name]
if name in fixes:
# fixed point conversion
@@ -72,6 +75,13 @@ def pack(fmt, obj):
elif isinstance(value, str):
value = tobytes(value)
elements.append(value)
+ # Check it fits
+ try:
+ struct.pack(names[name], value)
+ except Exception as e:
+ raise ValueError(
+ "Value %s does not fit in format %s for %s" % (value, names[name], name)
+ ) from e
data = struct.pack(*(formatstring,) + tuple(elements))
return data
@@ -87,7 +97,7 @@ def unpack(fmt, data, obj=None):
d = obj.__dict__
elements = struct.unpack(formatstring, data)
for i in range(len(names)):
- name = names[i]
+ name = list(names.keys())[i]
value = elements[i]
if name in fixes:
# fixed point conversion
@@ -141,7 +151,7 @@ def getformat(fmt, keep_pad_byte=False):
except KeyError:
lines = re.split("[\n;]", fmt)
formatstring = ""
- names = []
+ names = {}
fixes = {}
for line in lines:
if _emptyRE.match(line):
@@ -158,7 +168,7 @@ def getformat(fmt, keep_pad_byte=False):
name = m.group(1)
formatchar = m.group(2)
if keep_pad_byte or formatchar != "x":
- names.append(name)
+ names[name] = formatchar
if m.group(3):
# fixed point
before = int(m.group(3))
@@ -167,9 +177,10 @@ def getformat(fmt, keep_pad_byte=False):
if bits not in [8, 16, 32]:
raise Error("fixed point must be 8, 16 or 32 bits long")
formatchar = _fixedpointmappings[bits]
+ names[name] = formatchar
assert m.group(5) == "F"
fixes[name] = after
- formatstring = formatstring + formatchar
+ formatstring += formatchar
_formatcache[fmt] = formatstring, names, fixes
return formatstring, names, fixes
diff --git a/contrib/python/fonttools/fontTools/misc/transform.py b/contrib/python/fonttools/fontTools/misc/transform.py
index 0f9f3a5d8b..9025b79ec1 100644
--- a/contrib/python/fonttools/fontTools/misc/transform.py
+++ b/contrib/python/fonttools/fontTools/misc/transform.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/pens/svgPathPen.py b/contrib/python/fonttools/fontTools/pens/svgPathPen.py
index 29d41a8029..8231603f8a 100644
--- a/contrib/python/fonttools/fontTools/pens/svgPathPen.py
+++ b/contrib/python/fonttools/fontTools/pens/svgPathPen.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/qu2cu/__main__.py b/contrib/python/fonttools/fontTools/qu2cu/__main__.py
index 27728cc7aa..7c85f61b41 100644
--- a/contrib/python/fonttools/fontTools/qu2cu/__main__.py
+++ b/contrib/python/fonttools/fontTools/qu2cu/__main__.py
@@ -1,6 +1,6 @@
import sys
-from .cli import main
+from .cli import _main as main
if __name__ == "__main__":
diff --git a/contrib/python/fonttools/fontTools/qu2cu/benchmark.py b/contrib/python/fonttools/fontTools/qu2cu/benchmark.py
index cee55f5e7d..f28ad88cf9 100644
--- a/contrib/python/fonttools/fontTools/qu2cu/benchmark.py
+++ b/contrib/python/fonttools/fontTools/qu2cu/benchmark.py
@@ -48,7 +48,6 @@ def run_benchmark(module, function, setup_suffix="", repeat=25, number=1):
def main():
- """Benchmark the qu2cu algorithm performance."""
run_benchmark("qu2cu", "quadratic_to_curves")
diff --git a/contrib/python/fonttools/fontTools/qu2cu/cli.py b/contrib/python/fonttools/fontTools/qu2cu/cli.py
index a07fd6dcd0..101e938a6f 100644
--- a/contrib/python/fonttools/fontTools/qu2cu/cli.py
+++ b/contrib/python/fonttools/fontTools/qu2cu/cli.py
@@ -42,7 +42,7 @@ def _font_to_cubic(input_path, output_path=None, **kwargs):
font.save(output_path)
-def main(args=None):
+def _main(args=None):
"""Convert an OpenType font from quadratic to cubic curves"""
parser = argparse.ArgumentParser(prog="qu2cu")
parser.add_argument("--version", action="version", version=fontTools.__version__)
diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py
index 250a07ef1a..4aa60ad842 100644
--- a/contrib/python/fonttools/fontTools/subset/__init__.py
+++ b/contrib/python/fonttools/fontTools/subset/__init__.py
@@ -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:
@@ -2913,8 +2995,9 @@ def prune_post_subset(self, font, options):
visitor = NameRecordVisitor()
visitor.visit(font)
nameIDs = set(options.name_IDs) | visitor.seen
- if "*" not in options.name_IDs:
- self.names = [n for n in self.names if n.nameID in nameIDs]
+ if "*" in options.name_IDs:
+ nameIDs |= {n.nameID for n in self.names if n.nameID < 256}
+ self.names = [n for n in self.names if n.nameID in nameIDs]
if not options.name_legacy:
# TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman
# entry for Latin and no Unicode names.
@@ -3297,33 +3380,33 @@ 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'"):
+ if "MATH" in font:
+ with timer("close glyph list over 'MATH'"):
log.info(
- "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
+ "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
- font["GSUB"].closure_glyphs(self)
+ font["MATH"].closure_glyphs(self)
self.glyphs.intersection_update(realGlyphs)
log.info(
- "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
+ "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
- self.glyphs_gsubed = frozenset(self.glyphs)
+ self.glyphs_mathed = frozenset(self.glyphs)
- if "MATH" in font:
- with timer("close glyph list over 'MATH'"):
+ if self.options.layout_closure and "GSUB" in font:
+ with timer("close glyph list over 'GSUB'"):
log.info(
- "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs)
+ "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
- font["MATH"].closure_glyphs(self)
+ font["GSUB"].closure_glyphs(self)
self.glyphs.intersection_update(realGlyphs)
log.info(
- "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs)
+ "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
)
log.glyphs(self.glyphs, font=font)
- self.glyphs_mathed = frozenset(self.glyphs)
+ self.glyphs_gsubed = frozenset(self.glyphs)
for table in ("COLR", "bsln"):
if table in font:
@@ -3344,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(
diff --git a/contrib/python/fonttools/fontTools/subset/cff.py b/contrib/python/fonttools/fontTools/subset/cff.py
index 03fc565b31..cef0a36864 100644
--- a/contrib/python/fonttools/fontTools/subset/cff.py
+++ b/contrib/python/fonttools/fontTools/subset/cff.py
@@ -132,227 +132,6 @@ def subset_glyphs(self, s):
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
-@_add_method(psCharStrings.T2CharString)
-def subset_subroutines(self, subrs, gsubrs):
- p = self.program
- for i in range(1, len(p)):
- if p[i] == "callsubr":
- assert isinstance(p[i - 1], int)
- p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
- elif p[i] == "callgsubr":
- assert isinstance(p[i - 1], int)
- p[i - 1] = (
- gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
- )
-
-
-@_add_method(psCharStrings.T2CharString)
-def drop_hints(self):
- hints = self._hints
-
- if hints.deletions:
- p = self.program
- for idx in reversed(hints.deletions):
- del p[idx - 2 : idx]
-
- if hints.has_hint:
- assert not hints.deletions or hints.last_hint <= hints.deletions[0]
- self.program = self.program[hints.last_hint :]
- if not self.program:
- # TODO CFF2 no need for endchar.
- self.program.append("endchar")
- if hasattr(self, "width"):
- # Insert width back if needed
- if self.width != self.private.defaultWidthX:
- # For CFF2 charstrings, this should never happen
- assert (
- self.private.defaultWidthX is not None
- ), "CFF2 CharStrings must not have an initial width value"
- self.program.insert(0, self.width - self.private.nominalWidthX)
-
- if hints.has_hintmask:
- i = 0
- p = self.program
- while i < len(p):
- if p[i] in ["hintmask", "cntrmask"]:
- assert i + 1 <= len(p)
- del p[i : i + 2]
- continue
- i += 1
-
- assert len(self.program)
-
- del self._hints
-
-
-class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
- def __init__(self, localSubrs, globalSubrs, private):
- psCharStrings.SimpleT2Decompiler.__init__(
- self, localSubrs, globalSubrs, private
- )
- for subrs in [localSubrs, globalSubrs]:
- if subrs and not hasattr(subrs, "_used"):
- subrs._used = set()
-
- def op_callsubr(self, index):
- self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
- psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
-
- def op_callgsubr(self, index):
- self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
- psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
-
-
-class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
- class Hints(object):
- def __init__(self):
- # Whether calling this charstring produces any hint stems
- # Note that if a charstring starts with hintmask, it will
- # have has_hint set to True, because it *might* produce an
- # implicit vstem if called under certain conditions.
- self.has_hint = False
- # Index to start at to drop all hints
- self.last_hint = 0
- # Index up to which we know more hints are possible.
- # Only relevant if status is 0 or 1.
- self.last_checked = 0
- # The status means:
- # 0: after dropping hints, this charstring is empty
- # 1: after dropping hints, there may be more hints
- # continuing after this, or there might be
- # other things. Not clear yet.
- # 2: no more hints possible after this charstring
- self.status = 0
- # Has hintmask instructions; not recursive
- self.has_hintmask = False
- # List of indices of calls to empty subroutines to remove.
- self.deletions = []
-
- pass
-
- def __init__(
- self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
- ):
- self._css = css
- psCharStrings.T2WidthExtractor.__init__(
- self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
- )
- self.private = private
-
- def execute(self, charString):
- old_hints = charString._hints if hasattr(charString, "_hints") else None
- charString._hints = self.Hints()
-
- psCharStrings.T2WidthExtractor.execute(self, charString)
-
- hints = charString._hints
-
- if hints.has_hint or hints.has_hintmask:
- self._css.add(charString)
-
- if hints.status != 2:
- # Check from last_check, make sure we didn't have any operators.
- for i in range(hints.last_checked, len(charString.program) - 1):
- if isinstance(charString.program[i], str):
- hints.status = 2
- break
- else:
- hints.status = 1 # There's *something* here
- hints.last_checked = len(charString.program)
-
- if old_hints:
- assert hints.__dict__ == old_hints.__dict__
-
- def op_callsubr(self, index):
- subr = self.localSubrs[self.operandStack[-1] + self.localBias]
- psCharStrings.T2WidthExtractor.op_callsubr(self, index)
- self.processSubr(index, subr)
-
- def op_callgsubr(self, index):
- subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
- psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
- self.processSubr(index, subr)
-
- def op_hstem(self, index):
- psCharStrings.T2WidthExtractor.op_hstem(self, index)
- self.processHint(index)
-
- def op_vstem(self, index):
- psCharStrings.T2WidthExtractor.op_vstem(self, index)
- self.processHint(index)
-
- def op_hstemhm(self, index):
- psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
- self.processHint(index)
-
- def op_vstemhm(self, index):
- psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
- self.processHint(index)
-
- def op_hintmask(self, index):
- rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
- self.processHintmask(index)
- return rv
-
- def op_cntrmask(self, index):
- rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
- self.processHintmask(index)
- return rv
-
- def processHintmask(self, index):
- cs = self.callingStack[-1]
- hints = cs._hints
- hints.has_hintmask = True
- if hints.status != 2:
- # Check from last_check, see if we may be an implicit vstem
- for i in range(hints.last_checked, index - 1):
- if isinstance(cs.program[i], str):
- hints.status = 2
- break
- else:
- # We are an implicit vstem
- hints.has_hint = True
- hints.last_hint = index + 1
- hints.status = 0
- hints.last_checked = index + 1
-
- def processHint(self, index):
- cs = self.callingStack[-1]
- hints = cs._hints
- hints.has_hint = True
- hints.last_hint = index
- hints.last_checked = index
-
- def processSubr(self, index, subr):
- cs = self.callingStack[-1]
- hints = cs._hints
- subr_hints = subr._hints
-
- # Check from last_check, make sure we didn't have
- # any operators.
- if hints.status != 2:
- for i in range(hints.last_checked, index - 1):
- if isinstance(cs.program[i], str):
- hints.status = 2
- break
- hints.last_checked = index
-
- if hints.status != 2:
- if subr_hints.has_hint:
- hints.has_hint = True
-
- # Decide where to chop off from
- if subr_hints.status == 0:
- hints.last_hint = index
- else:
- hints.last_hint = index - 2 # Leave the subr call in
-
- elif subr_hints.status == 0:
- hints.deletions.append(index)
-
- hints.status = max(hints.status, subr_hints.status)
-
-
@_add_method(ttLib.getTableClass("CFF "))
def prune_post_subset(self, ttfFont, options):
cff = self.cff
@@ -381,13 +160,6 @@ def prune_post_subset(self, ttfFont, options):
return True
-def _delete_empty_subrs(private_dict):
- if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
- if "Subrs" in private_dict.rawDict:
- del private_dict.rawDict["Subrs"]
- del private_dict.Subrs
-
-
@deprecateFunction(
"use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning
)
@@ -396,141 +168,17 @@ def desubroutinize(self):
self.cff.desubroutinize()
+@deprecateFunction(
+ "use 'CFFFontSet.remove_hints()' instead", category=DeprecationWarning
+)
@_add_method(ttLib.getTableClass("CFF "))
def remove_hints(self):
- cff = self.cff
- for fontname in cff.keys():
- font = cff[fontname]
- cs = font.CharStrings
- # This can be tricky, but doesn't have to. What we do is:
- #
- # - Run all used glyph charstrings and recurse into subroutines,
- # - For each charstring (including subroutines), if it has any
- # of the hint stem operators, we mark it as such.
- # Upon returning, for each charstring we note all the
- # subroutine calls it makes that (recursively) contain a stem,
- # - Dropping hinting then consists of the following two ops:
- # * Drop the piece of the program in each charstring before the
- # last call to a stem op or a stem-calling subroutine,
- # * Drop all hintmask operations.
- # - It's trickier... A hintmask right after hints and a few numbers
- # will act as an implicit vstemhm. As such, we track whether
- # we have seen any non-hint operators so far and do the right
- # thing, recursively... Good luck understanding that :(
- css = set()
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- c.decompile()
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _DehintingT2Decompiler(
- css,
- subrs,
- c.globalSubrs,
- c.private.nominalWidthX,
- c.private.defaultWidthX,
- c.private,
- )
- decompiler.execute(c)
- c.width = decompiler.width
- for charstring in css:
- charstring.drop_hints()
- del css
-
- # Drop font-wide hinting values
- all_privs = []
- if hasattr(font, "FDArray"):
- all_privs.extend(fd.Private for fd in font.FDArray)
- else:
- all_privs.append(font.Private)
- for priv in all_privs:
- for k in [
- "BlueValues",
- "OtherBlues",
- "FamilyBlues",
- "FamilyOtherBlues",
- "BlueScale",
- "BlueShift",
- "BlueFuzz",
- "StemSnapH",
- "StemSnapV",
- "StdHW",
- "StdVW",
- "ForceBold",
- "LanguageGroup",
- "ExpansionFactor",
- ]:
- if hasattr(priv, k):
- setattr(priv, k, None)
- self.remove_unused_subroutines()
+ self.cff.remove_hints()
+@deprecateFunction(
+ "use 'CFFFontSet.remove_unused_subroutines' instead", category=DeprecationWarning
+)
@_add_method(ttLib.getTableClass("CFF "))
def remove_unused_subroutines(self):
- cff = self.cff
- for fontname in cff.keys():
- font = cff[fontname]
- cs = font.CharStrings
- # Renumber subroutines to remove unused ones
-
- # Mark all used subroutines
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
- decompiler.execute(c)
-
- all_subrs = [font.GlobalSubrs]
- if hasattr(font, "FDArray"):
- all_subrs.extend(
- fd.Private.Subrs
- for fd in font.FDArray
- if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
- )
- elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
- all_subrs.append(font.Private.Subrs)
-
- subrs = set(subrs) # Remove duplicates
-
- # Prepare
- for subrs in all_subrs:
- if not hasattr(subrs, "_used"):
- subrs._used = set()
- subrs._used = _uniq_sort(subrs._used)
- subrs._old_bias = psCharStrings.calcSubrBias(subrs)
- subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
-
- # Renumber glyph charstrings
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- subrs = getattr(c.private, "Subrs", None)
- c.subset_subroutines(subrs, font.GlobalSubrs)
-
- # Renumber subroutines themselves
- for subrs in all_subrs:
- if subrs == font.GlobalSubrs:
- if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
- local_subrs = font.Private.Subrs
- else:
- local_subrs = None
- else:
- local_subrs = subrs
-
- subrs.items = [subrs.items[i] for i in subrs._used]
- if hasattr(subrs, "file"):
- del subrs.file
- if hasattr(subrs, "offsets"):
- del subrs.offsets
-
- for subr in subrs.items:
- subr.subset_subroutines(local_subrs, font.GlobalSubrs)
-
- # Delete local SubrsIndex if empty
- if hasattr(font, "FDArray"):
- for fd in font.FDArray:
- _delete_empty_subrs(fd.Private)
- else:
- _delete_empty_subrs(font.Private)
-
- # Cleanup
- for subrs in all_subrs:
- del subrs._used, subrs._old_bias, subrs._new_bias
+ self.cff.remove_unused_subroutines()
diff --git a/contrib/python/fonttools/fontTools/ttLib/__main__.py b/contrib/python/fonttools/fontTools/ttLib/__main__.py
index 2733444d8b..a7ba7c76a9 100644
--- a/contrib/python/fonttools/fontTools/ttLib/__main__.py
+++ b/contrib/python/fonttools/fontTools/ttLib/__main__.py
@@ -77,7 +77,7 @@ def main(args=None):
outFile = options.output
lazy = options.lazy
flavor = options.flavor
- tables = options.table if options.table is not None else []
+ tables = options.table if options.table is not None else ["*"]
fonts = []
for f in options.font:
@@ -88,9 +88,10 @@ def main(args=None):
collection = TTCollection(f, lazy=lazy)
fonts.extend(collection.fonts)
- for font in fonts:
- for table in tables if "*" not in tables else font.keys():
- font[table] # Decompiles
+ if lazy is False:
+ for font in fonts:
+ for table in tables if "*" not in tables else font.keys():
+ font[table] # Decompiles
if outFile is not None:
if len(fonts) == 1:
diff --git a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py
index 4795320669..ea13d4734b 100644
--- a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py
+++ b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py
@@ -226,6 +226,8 @@ def removeOverlaps(
def main(args=None):
+ """Simplify glyphs in TTFont by merging overlapping contours."""
+
import sys
if args is None:
diff --git a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py
index 2909bfcb2c..68709825b3 100644
--- a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py
+++ b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py
@@ -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
+ for i, xy in enumerate(coordinates):
+ if xy is None:
+ continue
+ coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
- if not isVarComposite:
- for i, xy in enumerate(coordinates):
- if xy is None:
- continue
- coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
- continue
- # VarComposite glyph
-
- i = 0
- for component in glyph.components:
- if component.flags & VarComponentFlags.AXES_HAVE_VARIATION:
- i += len(component.location)
- if component.flags & (
- VarComponentFlags.HAVE_TRANSLATE_X
- | VarComponentFlags.HAVE_TRANSLATE_Y
- ):
- xy = coordinates[i]
- coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
- i += 1
- if component.flags & VarComponentFlags.HAVE_ROTATION:
- i += 1
- if component.flags & (
- VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
- ):
- i += 1
- if component.flags & (
- VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y
- ):
- i += 1
- if component.flags & (
- VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
- ):
- xy = coordinates[i]
- coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
- i += 1
-
- # Phantom points
- assert i + 4 == len(coordinates)
- for i in range(i, len(coordinates)):
- xy = coordinates[i]
- coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
+@ScalerVisitor.register_attr(ttLib.getTableClass("VARC"), "table")
+def visit(visitor, obj, attr, varc):
+ # VarComposite variations are a pain
+
+ fvar = visitor.font["fvar"]
+ fvarAxes = [a.axisTag for a in fvar.axes]
+
+ store = varc.MultiVarStore
+ storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes)
+
+ for g in varc.VarCompositeGlyphs.VarCompositeGlyph:
+ for component in g.components:
+ t = component.transform
+ t.translateX = visitor.scale(t.translateX)
+ t.translateY = visitor.scale(t.translateY)
+ t.tCenterX = visitor.scale(t.tCenterX)
+ t.tCenterY = visitor.scale(t.tCenterY)
+
+ if component.axisValuesVarIndex != otTables.NO_VARIATION_INDEX:
+ varIdx = component.axisValuesVarIndex
+ # TODO Move this code duplicated below to MultiVarStore.__getitem__,
+ # or a getDeltasAndSupports().
+ if varIdx != otTables.NO_VARIATION_INDEX:
+ major = varIdx >> 16
+ minor = varIdx & 0xFFFF
+ varData = store.MultiVarData[major]
+ vec = varData.Item[minor]
+ storeBuilder.setSupports(store.get_supports(major, fvar.axes))
+ if vec:
+ m = len(vec) // varData.VarRegionCount
+ vec = list(batched(vec, m))
+ vec = [Vector(v) for v in vec]
+ component.axisValuesVarIndex = storeBuilder.storeDeltas(vec)
+ else:
+ component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX
+
+ if component.transformVarIndex != otTables.NO_VARIATION_INDEX:
+ varIdx = component.transformVarIndex
+ if varIdx != otTables.NO_VARIATION_INDEX:
+ major = varIdx >> 16
+ minor = varIdx & 0xFFFF
+ vec = varData.Item[varIdx & 0xFFFF]
+ major = varIdx >> 16
+ minor = varIdx & 0xFFFF
+ varData = store.MultiVarData[major]
+ vec = varData.Item[minor]
+ storeBuilder.setSupports(store.get_supports(major, fvar.axes))
+ if vec:
+ m = len(vec) // varData.VarRegionCount
+ flags = component.flags
+ vec = list(batched(vec, m))
+ newVec = []
+ for v in vec:
+ v = list(v)
+ i = 0
+ ## Scale translate & tCenter
+ if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X:
+ v[i] = visitor.scale(v[i])
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y:
+ v[i] = visitor.scale(v[i])
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_ROTATION:
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_SCALE_X:
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_SCALE_Y:
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_SKEW_X:
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_SKEW_Y:
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_TCENTER_X:
+ v[i] = visitor.scale(v[i])
+ i += 1
+ if flags & otTables.VarComponentFlags.HAVE_TCENTER_Y:
+ v[i] = visitor.scale(v[i])
+ i += 1
+
+ newVec.append(Vector(v))
+ vec = newVec
+
+ component.transformVarIndex = storeBuilder.storeDeltas(vec)
+ else:
+ component.transformVarIndex = otTables.NO_VARIATION_INDEX
+
+ varc.MultiVarStore = storeBuilder.finish()
@ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables")
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py
index 6046d9100b..23d57964f6 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py
@@ -298,9 +298,9 @@ class BitmapSizeTable(object):
# cares about in terms of XML creation.
def _getXMLMetricNames(self):
dataNames = sstruct.getformat(bitmapSizeTableFormatPart1)[1]
- dataNames = dataNames + sstruct.getformat(bitmapSizeTableFormatPart2)[1]
+ dataNames = {**dataNames, **sstruct.getformat(bitmapSizeTableFormatPart2)[1]}
# Skip the first 3 data names because they are byte offsets and counts.
- return dataNames[3:]
+ return list(dataNames.keys())[3:]
def toXML(self, writer, ttFont):
writer.begintag("bitmapSizeTable")
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
index 027ac15342..a98bca2e0e 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py
new file mode 100644
index 0000000000..5a00887160
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py
@@ -0,0 +1,5 @@
+from .otBase import BaseTTXConverter
+
+
+class table_V_A_R_C_(BaseTTXConverter):
+ pass
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py
index 39039cf73a..6ea4132b3d 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py
@@ -6,6 +6,9 @@ from fontTools.misc.fixedTools import (
strToFixedToFloat as str2fl,
)
from fontTools.misc.textTools import bytesjoin, safeEval
+from fontTools.misc.roundTools import otRound
+from fontTools.varLib.models import piecewiseLinearMap
+from fontTools.varLib.varStore import VarStoreInstancer, NO_VARIATION_INDEX
from fontTools.ttLib import TTLibError
from . import DefaultTable
from . import otTables
@@ -74,9 +77,10 @@ class table__a_v_a_r(BaseTTXConverter):
def decompile(self, data, ttFont):
super().decompile(data, ttFont)
- assert self.table.Version >= 0x00010000
self.majorVersion = self.table.Version >> 16
self.minorVersion = self.table.Version & 0xFFFF
+ if self.majorVersion not in (1, 2):
+ raise NotImplementedError("Unknown avar table version")
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
for axis in axisTags:
self.segments[axis] = {}
@@ -136,3 +140,48 @@ class table__a_v_a_r(BaseTTXConverter):
segment[fromValue] = toValue
else:
super().fromXML(name, attrs, content, ttFont)
+
+ def renormalizeLocation(self, location, font):
+
+ if self.majorVersion not in (1, 2):
+ raise NotImplementedError("Unknown avar table version")
+
+ avarSegments = self.segments
+ mappedLocation = {}
+ for axisTag, value in location.items():
+ avarMapping = avarSegments.get(axisTag, None)
+ if avarMapping is not None:
+ value = piecewiseLinearMap(value, avarMapping)
+ mappedLocation[axisTag] = value
+
+ if self.majorVersion < 2:
+ return mappedLocation
+
+ # Version 2
+
+ varIdxMap = self.table.VarIdxMap
+ varStore = self.table.VarStore
+ axes = font["fvar"].axes
+ if varStore is not None:
+ instancer = VarStoreInstancer(varStore, axes, mappedLocation)
+
+ coords = list(fl2fi(mappedLocation.get(axis.axisTag, 0), 14) for axis in axes)
+
+ out = []
+ for varIdx, v in enumerate(coords):
+
+ if varIdxMap is not None:
+ varIdx = varIdxMap[varIdx]
+
+ if varStore is not None:
+ delta = instancer[varIdx]
+ v += otRound(delta)
+ v = min(max(v, -(1 << 14)), +(1 << 14))
+
+ out.append(v)
+
+ mappedLocation = {
+ axis.axisTag: fi2fl(v, 14) for v, axis in zip(out, axes) if v != 0
+ }
+
+ return mappedLocation
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
index 062a9aa429..a3bdacd4cc 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
@@ -110,6 +110,9 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
instance.fromXML(name, attrs, content, ttFont)
self.instances.append(instance)
+ def getAxes(self):
+ return {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in self.axes}
+
class Axis(object):
def __init__(self):
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
index 683912be97..fa11cf8f47 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
@@ -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.
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py
index 11485bf09a..044f65f716 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py
@@ -1,7 +1,8 @@
-from collections import UserDict, deque
+from collections import deque
from functools import partial
from fontTools.misc import sstruct
from fontTools.misc.textTools import safeEval
+from fontTools.misc.lazyTools import LazyDict
from . import DefaultTable
import array
import itertools
@@ -39,19 +40,6 @@ GVAR_HEADER_FORMAT = """
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
-class _LazyDict(UserDict):
- def __init__(self, data):
- super().__init__()
- self.data = data
-
- def __getitem__(self, k):
- v = self.data[k]
- if callable(v):
- v = v()
- self.data[k] = v
- return v
-
-
class table__g_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar", "glyf"]
@@ -116,11 +104,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
assert len(glyphs) == self.glyphCount
assert len(axisTags) == self.axisCount
- offsets = self.decompileOffsets_(
- data[GVAR_HEADER_SIZE:],
- tableFormat=(self.flags & 1),
- glyphCount=self.glyphCount,
- )
sharedCoords = tv.decompileSharedTuples(
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
)
@@ -128,20 +111,35 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
offsetToData = self.offsetToGlyphVariationData
glyf = ttFont["glyf"]
- def decompileVarGlyph(glyphName, gid):
- gvarData = data[
- offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
- ]
- if not gvarData:
- return []
- glyph = glyf[glyphName]
- numPointsInGlyph = self.getNumPoints_(glyph)
- return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
+ def get_read_item():
+ reverseGlyphMap = ttFont.getReverseGlyphMap()
+ tableFormat = self.flags & 1
+
+ def read_item(glyphName):
+ gid = reverseGlyphMap[glyphName]
+ offsetSize = 2 if tableFormat == 0 else 4
+ startOffset = GVAR_HEADER_SIZE + offsetSize * gid
+ endOffset = startOffset + offsetSize * 2
+ offsets = table__g_v_a_r.decompileOffsets_(
+ data[startOffset:endOffset],
+ tableFormat=tableFormat,
+ glyphCount=1,
+ )
+ gvarData = data[offsetToData + offsets[0] : offsetToData + offsets[1]]
+ if not gvarData:
+ return []
+ glyph = glyf[glyphName]
+ numPointsInGlyph = self.getNumPoints_(glyph)
+ return decompileGlyph_(
+ numPointsInGlyph, sharedCoords, axisTags, gvarData
+ )
+
+ return read_item
+
+ read_item = get_read_item()
+ l = LazyDict({glyphs[gid]: read_item for gid in range(self.glyphCount)})
- for gid in range(self.glyphCount):
- glyphName = glyphs[gid]
- variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
- self.variations = _LazyDict(variations)
+ 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
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py
index 5884cef45f..39c0c9e39b 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py
@@ -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",
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py
index f0e6c33ae3..95b6ab9335 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py
@@ -127,7 +127,7 @@ class table__m_a_x_p(DefaultTable.DefaultTable):
formatstring, names, fixes = sstruct.getformat(maxpFormat_0_5)
if self.tableVersion != 0x00005000:
formatstring, names_1_0, fixes = sstruct.getformat(maxpFormat_1_0_add)
- names = names + names_1_0
+ names = {**names, **names_1_0}
for name in names:
value = getattr(self, name)
if name == "tableVersion":
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py
index 53abd13b48..8df7c236b1 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py
@@ -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 = []
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
index a2f672567e..656836bd3c 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
@@ -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,
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otData.py b/contrib/python/fonttools/fontTools/ttLib/tables/otData.py
index 56716824ec..3a01f5934f 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/otData.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/otData.py
@@ -3169,6 +3169,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.",
),
],
),
@@ -3215,6 +3234,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",
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py
index 3505f42337..bc7fbad915 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py
@@ -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
@@ -703,6 +1097,9 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
assert inner <= 0xFFFF
mapping.insert(index, (outer << 16) | inner)
+ def __getitem__(self, i):
+ return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
+
class VarIdxMap(BaseTable):
def populateDefaults(self, propagator=None):
@@ -755,6 +1152,9 @@ class VarIdxMap(BaseTable):
assert inner <= 0xFFFF
mapping[glyph] = (outer << 16) | inner
+ def __getitem__(self, glyphName):
+ return self.mapping.get(glyphName, NO_VARIATION_INDEX)
+
class VarRegionList(BaseTable):
def preWrite(self, font):
diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
index 52e048b5f1..f4a539678b 100644
--- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py
+++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
@@ -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
@@ -537,7 +542,7 @@ class TTFont(object):
#
# Not enough names found in the 'post' table.
# Can happen when 'post' format 1 is improperly used on a font that
- # has more than 258 glyphs (the lenght of 'standardGlyphOrder').
+ # has more than 258 glyphs (the length of 'standardGlyphOrder').
#
log.warning(
"Not enough names found in the 'post' table, generating them from cmap instead"
@@ -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
@@ -781,26 +790,15 @@ class TTFont(object):
Raises ``TTLibError`` if the font is not a variable font.
"""
- from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
+ from fontTools.varLib.models import normalizeLocation
if "fvar" not in self:
raise TTLibError("Not a variable font")
- axes = {
- a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
- for a in self["fvar"].axes
- }
+ axes = self["fvar"].getAxes()
location = normalizeLocation(location, axes)
if "avar" in self:
- avar = self["avar"]
- avarSegments = avar.segments
- mappedLocation = {}
- for axisTag, value in location.items():
- avarMapping = avarSegments.get(axisTag, None)
- if avarMapping is not None:
- value = piecewiseLinearMap(value, avarMapping)
- mappedLocation[axisTag] = value
- location = mappedLocation
+ location = self["avar"].renormalizeLocation(location, self)
return location
def getBestCmap(
diff --git a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
index b4beb3e766..446c81e7db 100644
--- a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
+++ b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
@@ -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:
diff --git a/contrib/python/fonttools/fontTools/ttLib/woff2.py b/contrib/python/fonttools/fontTools/ttLib/woff2.py
index 9da2f7e6d6..03667e834b 100644
--- a/contrib/python/fonttools/fontTools/ttLib/woff2.py
+++ b/contrib/python/fonttools/fontTools/ttLib/woff2.py
@@ -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)
diff --git a/contrib/python/fonttools/fontTools/ttx.py b/contrib/python/fonttools/fontTools/ttx.py
index e7a068748b..0adda52d74 100644
--- a/contrib/python/fonttools/fontTools/ttx.py
+++ b/contrib/python/fonttools/fontTools/ttx.py
@@ -375,7 +375,7 @@ def guessFileType(fileName):
def parseOptions(args):
- rawOptions, files = getopt.getopt(
+ rawOptions, files = getopt.gnu_getopt(
args,
"ld:o:fvqht:x:sgim:z:baey:",
[
diff --git a/contrib/python/fonttools/fontTools/varLib/__init__.py b/contrib/python/fonttools/fontTools/varLib/__init__.py
index 1e0f2ec2f4..6d0e00ee10 100644
--- a/contrib/python/fonttools/fontTools/varLib/__init__.py
+++ b/contrib/python/fonttools/fontTools/varLib/__init__.py
@@ -845,9 +845,10 @@ def _add_CFF2(varFont, model, master_fonts):
glyphOrder = varFont.getGlyphOrder()
if "CFF2" not in varFont:
- from .cff import convertCFFtoCFF2
+ from fontTools.cffLib.CFFToCFF2 import convertCFFToCFF2
+
+ convertCFFToCFF2(varFont)
- convertCFFtoCFF2(varFont)
ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
# re-ordering the master list simplifies building the CFF2 data item lists.
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
diff --git a/contrib/python/fonttools/fontTools/varLib/builder.py b/contrib/python/fonttools/fontTools/varLib/builder.py
index 94cc5bf063..456c34c4dd 100644
--- a/contrib/python/fonttools/fontTools/varLib/builder.py
+++ b/contrib/python/fonttools/fontTools/varLib/builder.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/varLib/cff.py b/contrib/python/fonttools/fontTools/varLib/cff.py
index 52e6a8848d..393c793e36 100644
--- a/contrib/python/fonttools/fontTools/varLib/cff.py
+++ b/contrib/python/fonttools/fontTools/varLib/cff.py
@@ -49,95 +49,6 @@ def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
fontDict.Private.vstore = topDict.VarStore
-def lib_convertCFFToCFF2(cff, otFont):
- # This assumes a decompiled CFF table.
- cff2GetGlyphOrder = cff.otFont.getGlyphOrder
- topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
- topDictData.items = cff.topDictIndex.items
- cff.topDictIndex = topDictData
- topDict = topDictData[0]
- if hasattr(topDict, "Private"):
- privateDict = topDict.Private
- else:
- privateDict = None
- opOrder = buildOrder(topDictOperators2)
- topDict.order = opOrder
- topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
- if not hasattr(topDict, "FDArray"):
- fdArray = topDict.FDArray = FDArrayIndex()
- fdArray.strings = None
- fdArray.GlobalSubrs = topDict.GlobalSubrs
- topDict.GlobalSubrs.fdArray = fdArray
- charStrings = topDict.CharStrings
- if charStrings.charStringsAreIndexed:
- charStrings.charStringsIndex.fdArray = fdArray
- else:
- charStrings.fdArray = fdArray
- fontDict = FontDict()
- fontDict.setCFF2(True)
- fdArray.append(fontDict)
- fontDict.Private = privateDict
- privateOpOrder = buildOrder(privateDictOperators2)
- if privateDict is not None:
- for entry in privateDictOperators:
- key = entry[1]
- if key not in privateOpOrder:
- if key in privateDict.rawDict:
- # print "Removing private dict", key
- del privateDict.rawDict[key]
- if hasattr(privateDict, key):
- delattr(privateDict, key)
- # print "Removing privateDict attr", key
- else:
- # clean up the PrivateDicts in the fdArray
- fdArray = topDict.FDArray
- privateOpOrder = buildOrder(privateDictOperators2)
- for fontDict in fdArray:
- fontDict.setCFF2(True)
- for key in list(fontDict.rawDict.keys()):
- if key not in fontDict.order:
- del fontDict.rawDict[key]
- if hasattr(fontDict, key):
- delattr(fontDict, key)
-
- privateDict = fontDict.Private
- for entry in privateDictOperators:
- key = entry[1]
- if key not in privateOpOrder:
- if key in privateDict.rawDict:
- # print "Removing private dict", key
- del privateDict.rawDict[key]
- if hasattr(privateDict, key):
- delattr(privateDict, key)
- # print "Removing privateDict attr", key
- # Now delete up the deprecated topDict operators from CFF 1.0
- for entry in topDictOperators:
- key = entry[1]
- if key not in opOrder:
- if key in topDict.rawDict:
- del topDict.rawDict[key]
- if hasattr(topDict, key):
- delattr(topDict, key)
-
- # At this point, the Subrs and Charstrings are all still T2Charstring class
- # easiest to fix this by compiling, then decompiling again
- cff.major = 2
- file = BytesIO()
- cff.compile(file, otFont, isCFF2=True)
- file.seek(0)
- cff.decompile(file, otFont, isCFF2=True)
-
-
-def convertCFFtoCFF2(varFont):
- # Convert base font to a single master CFF2 font.
- cffTable = varFont["CFF "]
- lib_convertCFFToCFF2(cffTable.cff, varFont)
- newCFF2 = newTable("CFF2")
- newCFF2.cff = cffTable.cff
- varFont["CFF2"] = newCFF2
- del varFont["CFF "]
-
-
def conv_to_int(num):
if isinstance(num, float) and num.is_integer():
return int(num)
diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
index c5de81cad0..f8c43187c7 100644
--- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
+++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
@@ -89,7 +89,7 @@ from fontTools.misc.fixedTools import (
otRound,
)
from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
-from fontTools.ttLib import TTFont
+from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import _g_l_y_f
from fontTools import varLib
@@ -97,6 +97,13 @@ from fontTools import varLib
# we import the `subset` module because we use the `prune_lookups` method on the GSUB
# table class, and that method is only defined dynamically upon importing `subset`
from fontTools import subset # noqa: F401
+from fontTools.cffLib import privateDictOperators2
+from fontTools.cffLib.specializer import (
+ programToCommands,
+ commandsToProgram,
+ specializeCommands,
+ generalizeCommands,
+)
from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger
@@ -104,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
@@ -458,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
):
@@ -566,6 +610,259 @@ def changeTupleVariationAxisLimit(var, axisTag, axisLimit):
return out
+def instantiateCFF2(
+ varfont,
+ axisLimits,
+ *,
+ round=round,
+ specialize=True,
+ generalize=False,
+ downgrade=False,
+):
+ # The algorithm here is rather simple:
+ #
+ # Take all blend operations and store their deltas in the (otherwise empty)
+ # CFF2 VarStore. Then, instantiate the VarStore with the given axis limits,
+ # and read back the new deltas. This is done for both the CharStrings and
+ # the Private dicts.
+ #
+ # Then prune unused things and possibly drop the VarStore if it's empty.
+ # In which case, downgrade to CFF table if requested.
+
+ log.info("Instantiating CFF2 table")
+
+ fvarAxes = varfont["fvar"].axes
+
+ cff = varfont["CFF2"].cff
+ topDict = cff.topDictIndex[0]
+ varStore = topDict.VarStore.otVarStore
+ if not varStore:
+ if downgrade:
+ from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
+
+ convertCFF2ToCFF(varfont)
+ return
+
+ cff.desubroutinize()
+
+ def getNumRegions(vsindex):
+ return varStore.VarData[vsindex if vsindex is not None else 0].VarRegionCount
+
+ charStrings = topDict.CharStrings.values()
+
+ # Gather all unique private dicts
+ uniquePrivateDicts = set()
+ privateDicts = []
+ for fd in topDict.FDArray:
+ if fd.Private not in uniquePrivateDicts:
+ uniquePrivateDicts.add(fd.Private)
+ privateDicts.append(fd.Private)
+
+ allCommands = []
+ for cs in charStrings:
+ assert cs.private.vstore.otVarStore is varStore # Or in many places!!
+ commands = programToCommands(cs.program, getNumRegions=getNumRegions)
+ if generalize:
+ commands = generalizeCommands(commands)
+ if specialize:
+ commands = specializeCommands(commands, generalizeFirst=not generalize)
+ allCommands.append(commands)
+
+ def storeBlendsToVarStore(arg):
+ if not isinstance(arg, list):
+ return
+
+ if any(isinstance(subarg, list) for subarg in arg[:-1]):
+ raise NotImplementedError("Nested blend lists not supported (yet)")
+
+ count = arg[-1]
+ assert (len(arg) - 1) % count == 0
+ nRegions = (len(arg) - 1) // count - 1
+ assert nRegions == getNumRegions(vsindex)
+ for i in range(count, len(arg) - 1, nRegions):
+ deltas = arg[i : i + nRegions]
+ assert len(deltas) == nRegions
+ varData = varStore.VarData[vsindex]
+ varData.Item.append(deltas)
+ varData.ItemCount += 1
+
+ def fetchBlendsFromVarStore(arg):
+ if not isinstance(arg, list):
+ return [arg]
+
+ if any(isinstance(subarg, list) for subarg in arg[:-1]):
+ raise NotImplementedError("Nested blend lists not supported (yet)")
+
+ count = arg[-1]
+ assert (len(arg) - 1) % count == 0
+ numRegions = getNumRegions(vsindex)
+ newDefaults = []
+ newDeltas = []
+ for i in range(count):
+ defaultValue = arg[i]
+
+ major = vsindex
+ minor = varDataCursor[major]
+ varDataCursor[major] += 1
+
+ varIdx = (major << 16) + minor
+
+ defaultValue += round(defaultDeltas[varIdx])
+ newDefaults.append(defaultValue)
+
+ varData = varStore.VarData[major]
+ deltas = varData.Item[minor]
+ assert len(deltas) == numRegions
+ newDeltas.extend(deltas)
+
+ if not numRegions:
+ return newDefaults # No deltas, just return the defaults
+
+ return [newDefaults + newDeltas + [count]]
+
+ # Check VarData's are empty
+ for varData in varStore.VarData:
+ assert varData.Item == []
+ assert varData.ItemCount == 0
+
+ # Add charstring blend lists to VarStore so we can instantiate them
+ for commands in allCommands:
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ for arg in command[1]:
+ storeBlendsToVarStore(arg)
+
+ # Add private blend lists to VarStore so we can instantiate values
+ vsindex = 0
+ for opcode, name, arg_type, default, converter in privateDictOperators2:
+ if arg_type not in ("number", "delta", "array"):
+ continue
+
+ vsindex = 0
+ for private in privateDicts:
+ if not hasattr(private, name):
+ continue
+ values = getattr(private, name)
+
+ if name == "vsindex":
+ vsindex = values[0]
+ continue
+
+ if arg_type == "number":
+ values = [values]
+
+ for value in values:
+ if not isinstance(value, list):
+ continue
+
+ assert len(value) % (getNumRegions(vsindex) + 1) == 0
+ count = len(value) // (getNumRegions(vsindex) + 1)
+ storeBlendsToVarStore(value + [count])
+
+ # Instantiate VarStore
+ defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ # Read back new charstring blends from the instantiated VarStore
+ varDataCursor = [0] * len(varStore.VarData)
+ for commands in allCommands:
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ newArgs = []
+ for arg in command[1]:
+ newArgs.extend(fetchBlendsFromVarStore(arg))
+ command[1][:] = newArgs
+
+ # Read back new private blends from the instantiated VarStore
+ for opcode, name, arg_type, default, converter in privateDictOperators2:
+ if arg_type not in ("number", "delta", "array"):
+ continue
+
+ for private in privateDicts:
+ if not hasattr(private, name):
+ continue
+ values = getattr(private, name)
+ if arg_type == "number":
+ values = [values]
+
+ newValues = []
+ for value in values:
+ if not isinstance(value, list):
+ newValues.append(value)
+ continue
+
+ value.append(1)
+ value = fetchBlendsFromVarStore(value)
+ newValues.extend(v[:-1] if isinstance(v, list) else v for v in value)
+
+ if arg_type == "number":
+ newValues = newValues[0]
+
+ setattr(private, name, newValues)
+
+ # Empty out the VarStore
+ for i, varData in enumerate(varStore.VarData):
+ assert varDataCursor[i] == varData.ItemCount, (
+ varDataCursor[i],
+ varData.ItemCount,
+ )
+ varData.Item = []
+ varData.ItemCount = 0
+
+ # Remove vsindex commands that are no longer needed, collect those that are.
+ usedVsindex = set()
+ for commands in allCommands:
+ if any(isinstance(arg, list) for command in commands for arg in command[1]):
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ if any(isinstance(arg, list) for arg in command[1]):
+ usedVsindex.add(vsindex)
+ else:
+ commands[:] = [command for command in commands if command[0] != "vsindex"]
+
+ # Remove unused VarData and update vsindex values
+ vsindexMapping = {v: i for i, v in enumerate(sorted(usedVsindex))}
+ varStore.VarData = [
+ varData for i, varData in enumerate(varStore.VarData) if i in usedVsindex
+ ]
+ varStore.VarDataCount = len(varStore.VarData)
+ for commands in allCommands:
+ for command in commands:
+ if command[0] == "vsindex":
+ command[1][0] = vsindexMapping[command[1][0]]
+
+ # Remove initial vsindex commands that are implied
+ for commands in allCommands:
+ if commands and commands[0] == ("vsindex", [0]):
+ commands.pop(0)
+
+ # Ship the charstrings!
+ for cs, commands in zip(charStrings, allCommands):
+ cs.program = commandsToProgram(commands)
+
+ # Remove empty VarStore
+ if not varStore.VarData:
+ if "VarStore" in topDict.rawDict:
+ del topDict.rawDict["VarStore"]
+ del topDict.VarStore
+ del topDict.CharStrings.varStore
+ for private in privateDicts:
+ del private.vstore
+
+ if downgrade:
+ from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
+
+ convertCFF2ToCFF(varfont)
+
+
def _instantiateGvarGlyph(
glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=True
):
@@ -583,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
@@ -650,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,
@@ -765,22 +1045,57 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2
-def _instantiateVHVAR(varfont, axisLimits, tableFields):
+def _instantiateVHVAR(varfont, axisLimits, tableFields, *, round=round):
location = axisLimits.pinnedLocation()
tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes
- # Deltas from gvar table have already been applied to the hmtx/vmtx. For full
- # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
- if set(location).issuperset(axis.axisTag for axis in fvarAxes):
- log.info("Dropping %s table", tableTag)
- del varfont[tableTag]
- return
log.info("Instantiating %s table", tableTag)
vhvar = varfont[tableTag].table
varStore = vhvar.VarStore
- # since deltas were already applied, the return value here is ignored
- instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ if "glyf" in varfont:
+ # Deltas from gvar table have already been applied to the hmtx/vmtx. For full
+ # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
+ if set(location).issuperset(axis.axisTag for axis in fvarAxes):
+ log.info("Dropping %s table", tableTag)
+ del varfont[tableTag]
+ return
+
+ defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ if "glyf" not in varfont:
+ # CFF2 fonts need hmtx/vmtx updated here. For glyf fonts, the instantiateGvar
+ # function already updated the hmtx/vmtx from phantom points. Maybe remove
+ # that and do it here for both CFF2 and glyf fonts?
+ #
+ # Specially, if a font has glyf but not gvar, the hmtx/vmtx will not have been
+ # updated by instantiateGvar. Though one can call that a faulty font.
+ metricsTag = "vmtx" if tableTag == "VVAR" else "hmtx"
+ if metricsTag in varfont:
+ advMapping = getattr(vhvar, tableFields.advMapping)
+ metricsTable = varfont[metricsTag]
+ metrics = metricsTable.metrics
+ for glyphName, (advanceWidth, sb) in metrics.items():
+ if advMapping:
+ varIdx = advMapping.mapping[glyphName]
+ else:
+ varIdx = varfont.getGlyphID(glyphName)
+ metrics[glyphName] = (advanceWidth + round(defaultDeltas[varIdx]), sb)
+
+ if (
+ tableTag == "VVAR"
+ and getattr(vhvar, tableFields.vOrigMapping) is not None
+ ):
+ log.warning(
+ "VORG table not yet updated to reflect changes in VVAR table"
+ )
+
+ # For full instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
+ if set(location).issuperset(axis.axisTag for axis in fvarAxes):
+ log.info("Dropping %s table", tableTag)
+ del varfont[tableTag]
+ return
if varStore.VarRegionList.Region:
# Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap
@@ -923,6 +1238,8 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
newItemVarStore = tupleVarStore.asItemVarStore()
itemVarStore.VarRegionList = newItemVarStore.VarRegionList
+ if not hasattr(itemVarStore, "VarDataCount"): # Happens fromXML
+ itemVarStore.VarDataCount = len(newItemVarStore.VarData)
assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount
itemVarStore.VarData = newItemVarStore.VarData
@@ -1019,7 +1336,11 @@ def _isValidAvarSegmentMap(axisTag, segmentMap):
def instantiateAvar(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates.
- segments = varfont["avar"].segments
+ avar = varfont["avar"]
+ if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
+ raise NotImplementedError("avar table with VarStore is not supported")
+
+ segments = avar.segments
# drop table if we instantiate all the axes
pinnedAxes = set(axisLimits.pinnedLocation())
@@ -1080,7 +1401,7 @@ def instantiateAvar(varfont, axisLimits):
newSegments[axisTag] = newMapping
else:
newSegments[axisTag] = mapping
- varfont["avar"].segments = newSegments
+ avar.segments = newSegments
def isInstanceWithinAxisRanges(location, axisRanges):
@@ -1218,9 +1539,6 @@ def sanityCheckVariableTables(varfont):
if "gvar" in varfont:
if "glyf" not in varfont:
raise ValueError("Can't have gvar without glyf")
- # TODO(anthrotype) Remove once we do support partial instancing CFF2
- if "CFF2" in varfont:
- raise NotImplementedError("Instancing CFF2 variable fonts is not supported yet")
def instantiateVariableFont(
@@ -1230,6 +1548,8 @@ def instantiateVariableFont(
optimize=True,
overlap=OverlapMode.KEEP_AND_SET_FLAGS,
updateFontNames=False,
+ *,
+ downgradeCFF2=False,
):
"""Instantiate variable font, either fully or partially.
@@ -1269,6 +1589,11 @@ def instantiateVariableFont(
in the head and OS/2 table will be updated so they conform to the R/I/B/BI
model. If the STAT table is missing or an Axis Value table is missing for
a given axis coordinate, a ValueError will be raised.
+ downgradeCFF2 (bool): if True, downgrade the CFF2 table to CFF table when possible
+ ie. full instancing of all axes. This is useful for compatibility with older
+ software that does not support CFF2. Defaults to False. Note that this
+ operation also removes overlaps within glyph shapes, as CFF does not support
+ overlaps but CFF2 does.
"""
# 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
overlap = OverlapMode(int(overlap))
@@ -1293,6 +1618,12 @@ 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)
+
if "gvar" in varfont:
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
@@ -1485,6 +1816,11 @@ def parseArgs(args):
"a STAT table with Axis Value Tables",
)
parser.add_argument(
+ "--downgrade-cff2",
+ action="store_true",
+ help="If all axes are pinned, downgrade CFF2 to CFF table format",
+ )
+ parser.add_argument(
"--no-recalc-timestamp",
dest="recalc_timestamp",
action="store_false",
@@ -1545,7 +1881,7 @@ def main(args=None):
)
isFullInstance = {
- axisTag for axisTag, limit in axisLimits.items() if not isinstance(limit, tuple)
+ axisTag for axisTag, limit in axisLimits.items() if limit[0] == limit[2]
}.issuperset(axis.axisTag for axis in varfont["fvar"].axes)
instantiateVariableFont(
@@ -1555,6 +1891,7 @@ def main(args=None):
optimize=options.optimize,
overlap=options.overlap,
updateFontNames=options.update_name_table,
+ downgradeCFF2=options.downgrade_cff2,
)
suffix = "-instance" if isFullInstance else "-partial"
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
index 5fc12e04c9..e212ecf8d1 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
@@ -749,22 +749,27 @@ def main(args=None):
if "gvar" in font:
# Is variable font
- axisMapping = {}
fvar = font["fvar"]
+ axisMapping = {}
for axis in fvar.axes:
axisMapping[axis.axisTag] = {
-1: axis.minValue,
0: axis.defaultValue,
1: axis.maxValue,
}
+ normalized = False
if "avar" in font:
avar = font["avar"]
- for axisTag, segments in avar.segments.items():
- fvarMapping = axisMapping[axisTag].copy()
- for location, value in segments.items():
- axisMapping[axisTag][value] = piecewiseLinearMap(
- location, fvarMapping
- )
+ if getattr(avar.table, "VarStore", None):
+ axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
+ normalized = True
+ else:
+ for axisTag, segments in avar.segments.items():
+ fvarMapping = axisMapping[axisTag].copy()
+ for location, value in segments.items():
+ axisMapping[axisTag][value] = piecewiseLinearMap(
+ location, fvarMapping
+ )
gvar = font["gvar"]
glyf = font["glyf"]
@@ -811,6 +816,8 @@ def main(args=None):
)
+ "'"
)
+ if normalized:
+ name += " (normalized)"
names.append(name)
fonts.append(glyphsets[locTuple])
locations.append(dict(locTuple))
diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py
index 59815316f8..819596991f 100644
--- a/contrib/python/fonttools/fontTools/varLib/models.py
+++ b/contrib/python/fonttools/fontTools/varLib/models.py
@@ -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):
diff --git a/contrib/python/fonttools/fontTools/varLib/multiVarStore.py b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py
new file mode 100644
index 0000000000..f24a6e6f75
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py
@@ -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
diff --git a/contrib/python/fonttools/fontTools/varLib/mutator.py b/contrib/python/fonttools/fontTools/varLib/mutator.py
index c7c37dabca..6c327f9454 100644
--- a/contrib/python/fonttools/fontTools/varLib/mutator.py
+++ b/contrib/python/fonttools/fontTools/varLib/mutator.py
@@ -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,
diff --git a/contrib/python/fonttools/fontTools/varLib/varStore.py b/contrib/python/fonttools/fontTools/varLib/varStore.py
index 7805769074..f54fad2db3 100644
--- a/contrib/python/fonttools/fontTools/varLib/varStore.py
+++ b/contrib/python/fonttools/fontTools/varLib/varStore.py
@@ -32,7 +32,7 @@ class OnlineVarStoreBuilder(object):
self._supports = None
self._varDataIndices = {}
self._varDataCaches = {}
- self._cache = {}
+ self._cache = None
def setModel(self, model):
self.setSupports(model.supports)
@@ -43,7 +43,7 @@ class OnlineVarStoreBuilder(object):
self._supports = list(supports)
if not self._supports[0]:
del self._supports[0] # Drop base master support
- self._cache = {}
+ self._cache = None
self._data = None
def finish(self, optimize=True):
@@ -54,7 +54,7 @@ class OnlineVarStoreBuilder(object):
data.calculateNumShorts(optimize=optimize)
return self._store
- def _add_VarData(self):
+ def _add_VarData(self, num_items=1):
regionMap = self._regionMap
regionList = self._regionList
@@ -76,7 +76,7 @@ class OnlineVarStoreBuilder(object):
self._outer = varDataIdx
self._data = self._store.VarData[varDataIdx]
self._cache = self._varDataCaches[key]
- if len(self._data.Item) == 0xFFFF:
+ if len(self._data.Item) + num_items > 0xFFFF:
# This is full. Need new one.
varDataIdx = None
@@ -94,6 +94,14 @@ class OnlineVarStoreBuilder(object):
base = deltas.pop(0)
return base, self.storeDeltas(deltas, round=noRound)
+ def storeMastersMany(self, master_values_list, *, round=round):
+ deltas_list = [
+ self._model.getDeltas(master_values, round=round)
+ for master_values in master_values_list
+ ]
+ base_list = [deltas.pop(0) for deltas in deltas_list]
+ return base_list, self.storeDeltasMany(deltas_list, round=noRound)
+
def storeDeltas(self, deltas, *, round=round):
deltas = [round(d) for d in deltas]
if len(deltas) == len(self._supports) + 1:
@@ -102,23 +110,51 @@ class OnlineVarStoreBuilder(object):
assert len(deltas) == len(self._supports)
deltas = tuple(deltas)
+ if not self._data:
+ self._add_VarData()
+
varIdx = self._cache.get(deltas)
if varIdx is not None:
return varIdx
- if not self._data:
- self._add_VarData()
inner = len(self._data.Item)
if inner == 0xFFFF:
# Full array. Start new one.
self._add_VarData()
- return self.storeDeltas(deltas)
+ return self.storeDeltas(deltas, round=noRound)
self._data.addItem(deltas, round=noRound)
varIdx = (self._outer << 16) + inner
self._cache[deltas] = varIdx
return varIdx
+ def storeDeltasMany(self, deltas_list, *, round=round):
+ deltas_list = [[round(d) for d in deltas] for deltas in deltas_list]
+ deltas_list = tuple(tuple(deltas) for deltas in deltas_list)
+
+ if not self._data:
+ self._add_VarData(len(deltas_list))
+
+ varIdx = self._cache.get(deltas_list)
+ if varIdx is not None:
+ return varIdx
+
+ inner = len(self._data.Item)
+ if inner + len(deltas_list) > 0xFFFF:
+ # Full array. Start new one.
+ self._add_VarData(len(deltas_list))
+ return self.storeDeltasMany(deltas_list, round=noRound)
+ for i, deltas in enumerate(deltas_list):
+ self._data.addItem(deltas, round=noRound)
+
+ varIdx = (self._outer << 16) + inner + i
+ self._cache[deltas] = varIdx
+
+ varIdx = (self._outer << 16) + inner
+ self._cache[deltas_list] = varIdx
+
+ return varIdx
+
def VarData_addItem(self, deltas, *, round=round):
deltas = [round(d) for d in deltas]
@@ -210,26 +246,29 @@ class VarStoreInstancer(object):
def VarStore_subset_varidxes(
- self, varIdxes, optimize=True, retainFirstMap=False, advIdxes=set()
+ self,
+ varIdxes,
+ optimize=True,
+ retainFirstMap=False,
+ advIdxes=set(),
+ *,
+ VarData="VarData",
):
# Sort out used varIdxes by major/minor.
- used = {}
+ used = defaultdict(set)
for varIdx in varIdxes:
if varIdx == NO_VARIATION_INDEX:
continue
major = varIdx >> 16
minor = varIdx & 0xFFFF
- d = used.get(major)
- if d is None:
- d = used[major] = set()
- d.add(minor)
+ used[major].add(minor)
del varIdxes
#
# Subset VarData
#
- varData = self.VarData
+ varData = getattr(self, VarData)
newVarData = []
varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX}
for major, data in enumerate(varData):
@@ -260,10 +299,11 @@ def VarStore_subset_varidxes(
data.Item = newItems
data.ItemCount = len(data.Item)
- data.calculateNumShorts(optimize=optimize)
+ if VarData == "VarData":
+ data.calculateNumShorts(optimize=optimize)
- self.VarData = newVarData
- self.VarDataCount = len(self.VarData)
+ setattr(self, VarData, newVarData)
+ setattr(self, VarData + "Count", len(newVarData))
self.prune_regions()
@@ -273,7 +313,7 @@ def VarStore_subset_varidxes(
ot.VarStore.subset_varidxes = VarStore_subset_varidxes
-def VarStore_prune_regions(self):
+def VarStore_prune_regions(self, *, VarData="VarData", VarRegionList="VarRegionList"):
"""Remove unused VarRegions."""
#
# Subset VarRegionList
@@ -281,10 +321,10 @@ def VarStore_prune_regions(self):
# Collect.
usedRegions = set()
- for data in self.VarData:
+ for data in getattr(self, VarData):
usedRegions.update(data.VarRegionIndex)
# Subset.
- regionList = self.VarRegionList
+ regionList = getattr(self, VarRegionList)
regions = regionList.Region
newRegions = []
regionMap = {}
@@ -294,7 +334,7 @@ def VarStore_prune_regions(self):
regionList.Region = newRegions
regionList.RegionCount = len(regionList.Region)
# Map.
- for data in self.VarData:
+ for data in getattr(self, VarData):
data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex]
diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make
index 91be3261df..51c9d08b3b 100644
--- a/contrib/python/fonttools/ya.make
+++ b/contrib/python/fonttools/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(4.51.0)
+VERSION(4.52.1)
LICENSE(MIT)
@@ -23,8 +23,11 @@ PY_SRCS(
fontTools/__main__.py
fontTools/afmLib.py
fontTools/agl.py
+ fontTools/cffLib/CFF2ToCFF.py
+ fontTools/cffLib/CFFToCFF2.py
fontTools/cffLib/__init__.py
fontTools/cffLib/specializer.py
+ fontTools/cffLib/transforms.py
fontTools/cffLib/width.py
fontTools/colorLib/__init__.py
fontTools/colorLib/builder.py
@@ -84,6 +87,8 @@ PY_SRCS(
fontTools/misc/filenames.py
fontTools/misc/fixedTools.py
fontTools/misc/intTools.py
+ fontTools/misc/iterTools.py
+ fontTools/misc/lazyTools.py
fontTools/misc/loggingTools.py
fontTools/misc/macCreatorType.py
fontTools/misc/macRes.py
@@ -215,6 +220,7 @@ PY_SRCS(
fontTools/ttLib/tables/T_S_I__5.py
fontTools/ttLib/tables/T_T_F_A_.py
fontTools/ttLib/tables/TupleVariation.py
+ fontTools/ttLib/tables/V_A_R_C_.py
fontTools/ttLib/tables/V_D_M_X_.py
fontTools/ttLib/tables/V_O_R_G_.py
fontTools/ttLib/tables/V_V_A_R_.py
@@ -309,6 +315,7 @@ PY_SRCS(
fontTools/varLib/iup.py
fontTools/varLib/merger.py
fontTools/varLib/models.py
+ fontTools/varLib/multiVarStore.py
fontTools/varLib/mutator.py
fontTools/varLib/mvar.py
fontTools/varLib/plot.py
diff --git a/contrib/python/zope.interface/py3/.dist-info/METADATA b/contrib/python/zope.interface/py3/.dist-info/METADATA
index c903dd97fe..58a1b3d751 100644
--- a/contrib/python/zope.interface/py3/.dist-info/METADATA
+++ b/contrib/python/zope.interface/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: zope.interface
-Version: 6.4.post1
+Version: 6.4.post2
Summary: Interfaces for Python
Home-page: https://github.com/zopefoundation/zope.interface
Author: Zope Foundation and Contributors
@@ -75,6 +75,13 @@ For detailed documentation, please see https://zopeinterface.readthedocs.io/en/l
Changes
=========
+6.4.post2 (unreleased)
+======================
+
+- Publish missing Windows wheels, second attempt.
+ (`#295 <https://github.com/zopefoundation/zope.interface/issues/295>`_)
+
+
6.4.post1 (2024-05-23)
======================
@@ -89,14 +96,14 @@ For detailed documentation, please see https://zopeinterface.readthedocs.io/en/l
``zope_interface-6.4.tar.gz`` instead of ``zope.interface-6.4-py2.tar.gz``
which cannot be installed by ``zc.buildout``. This release is a re-release
of version 6.4 with the correct sdist name.
- (`#298 <https://github.com/zopefoundation/zope.interface/issues/298>`)
+ (`#298 <https://github.com/zopefoundation/zope.interface/issues/298>`_)
6.4 (2024-05-15)
================
- Adjust for incompatible changes in Python 3.13b1.
- (`#292 <https://github.com/zopefoundation/zope.interface/issues/292>`)
+ (`#292 <https://github.com/zopefoundation/zope.interface/issues/292>`_)
- Build windows wheels on GHA.
diff --git a/contrib/python/zope.interface/py3/ya.make b/contrib/python/zope.interface/py3/ya.make
index e7b5d1332a..c2b5dcb393 100644
--- a/contrib/python/zope.interface/py3/ya.make
+++ b/contrib/python/zope.interface/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(6.4.post1)
+VERSION(6.4.post2)
LICENSE(ZPL-2.1)