aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttLib')
-rw-r--r--Lib/fontTools/ttLib/__init__.py43
-rw-r--r--Lib/fontTools/ttLib/removeOverlaps.py68
-rw-r--r--Lib/fontTools/ttLib/sfnt.py9
-rw-r--r--Lib/fontTools/ttLib/tables/C_B_D_T_.py2
-rw-r--r--Lib/fontTools/ttLib/tables/C_O_L_R_.py6
-rw-r--r--Lib/fontTools/ttLib/tables/C_P_A_L_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/D_S_I_G_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/DefaultTable.py2
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_D_T_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_L_C_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/F__e_a_t.py6
-rw-r--r--Lib/fontTools/ttLib/tables/G_M_A_P_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/G_P_K_G_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/M_E_T_A_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/S_I_N_G_.py3
-rw-r--r--Lib/fontTools/ttLib/tables/S_V_G_.py311
-rw-r--r--Lib/fontTools/ttLib/tables/S__i_l_f.py3
-rw-r--r--Lib/fontTools/ttLib/tables/T_S_I_V_.py2
-rw-r--r--Lib/fontTools/ttLib/tables/T_S_I__1.py2
-rw-r--r--Lib/fontTools/ttLib/tables/TupleVariation.py351
-rw-r--r--Lib/fontTools/ttLib/tables/V_O_R_G_.py11
-rw-r--r--Lib/fontTools/ttLib/tables/_a_n_k_r.py15
-rw-r--r--Lib/fontTools/ttLib/tables/_a_v_a_r.py24
-rw-r--r--Lib/fontTools/ttLib/tables/_c_i_d_g.py25
-rw-r--r--Lib/fontTools/ttLib/tables/_c_m_a_p.py129
-rw-r--r--Lib/fontTools/ttLib/tables/_c_v_a_r.py4
-rw-r--r--Lib/fontTools/ttLib/tables/_f_e_a_t.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_f_v_a_r.py3
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py575
-rw-r--r--Lib/fontTools/ttLib/tables/_g_v_a_r.py23
-rw-r--r--Lib/fontTools/ttLib/tables/_h_d_m_x.py2
-rw-r--r--Lib/fontTools/ttLib/tables/_l_t_a_g.py3
-rw-r--r--Lib/fontTools/ttLib/tables/_m_e_t_a.py3
-rw-r--r--Lib/fontTools/ttLib/tables/_n_a_m_e.py41
-rw-r--r--Lib/fontTools/ttLib/tables/_p_o_s_t.py38
-rw-r--r--Lib/fontTools/ttLib/tables/_t_r_a_k.py3
-rw-r--r--Lib/fontTools/ttLib/tables/asciiTable.py2
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py215
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py266
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py303
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py438
-rw-r--r--Lib/fontTools/ttLib/tables/ttProgram.py3
-rw-r--r--Lib/fontTools/ttLib/ttCollection.py18
-rw-r--r--Lib/fontTools/ttLib/ttFont.py318
-rw-r--r--Lib/fontTools/ttLib/woff2.py3
45 files changed, 1872 insertions, 1428 deletions
diff --git a/Lib/fontTools/ttLib/__init__.py b/Lib/fontTools/ttLib/__init__.py
index 16417e73..dadd7f20 100644
--- a/Lib/fontTools/ttLib/__init__.py
+++ b/Lib/fontTools/ttLib/__init__.py
@@ -1,45 +1,4 @@
-"""fontTools.ttLib -- a package for dealing with TrueType fonts.
-
-This package offers translators to convert TrueType fonts to Python
-objects and vice versa, and additionally from Python to TTX (an XML-based
-text format) and vice versa.
-
-Example interactive session:
-
-Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL]
-Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
->> from fontTools import ttLib
->> tt = ttLib.TTFont("afont.ttf")
->> tt['maxp'].numGlyphs
-242
->> tt['OS/2'].achVendID
-'B&H\000'
->> tt['head'].unitsPerEm
-2048
->> tt.saveXML("afont.ttx")
-Dumping 'LTSH' table...
-Dumping 'OS/2' table...
-Dumping 'VDMX' table...
-Dumping 'cmap' table...
-Dumping 'cvt ' table...
-Dumping 'fpgm' table...
-Dumping 'glyf' table...
-Dumping 'hdmx' table...
-Dumping 'head' table...
-Dumping 'hhea' table...
-Dumping 'hmtx' table...
-Dumping 'loca' table...
-Dumping 'maxp' table...
-Dumping 'name' table...
-Dumping 'post' table...
-Dumping 'prep' table...
->> tt2 = ttLib.TTFont()
->> tt2.importXML("afont.ttx")
->> tt2['maxp'].numGlyphs
-242
->>
-
-"""
+"""fontTools.ttLib -- a package for dealing with TrueType fonts."""
from fontTools.misc.loggingTools import deprecateFunction
import logging
diff --git a/Lib/fontTools/ttLib/removeOverlaps.py b/Lib/fontTools/ttLib/removeOverlaps.py
index fb5c77ab..624cd47b 100644
--- a/Lib/fontTools/ttLib/removeOverlaps.py
+++ b/Lib/fontTools/ttLib/removeOverlaps.py
@@ -5,8 +5,9 @@ Requires https://github.com/fonttools/skia-pathops
import itertools
import logging
-from typing import Iterable, Optional, Mapping
+from typing import Callable, Iterable, Optional, Mapping
+from fontTools.misc.roundTools import otRound
from fontTools.ttLib import ttFont
from fontTools.ttLib.tables import _g_l_y_f
from fontTools.ttLib.tables import _h_m_t_x
@@ -18,6 +19,10 @@ import pathops
__all__ = ["removeOverlaps"]
+class RemoveOverlapsError(Exception):
+ pass
+
+
log = logging.getLogger("fontTools.ttLib.removeOverlaps")
_TTGlyphMapping = Mapping[str, ttFont._TTGlyph]
@@ -76,6 +81,49 @@ def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph:
return glyph
+def _round_path(
+ path: pathops.Path, round: Callable[[float], float] = otRound
+) -> pathops.Path:
+ rounded_path = pathops.Path()
+ for verb, points in path:
+ rounded_path.add(verb, *((round(p[0]), round(p[1])) for p in points))
+ return rounded_path
+
+
+def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path:
+ # skia-pathops has a bug where it sometimes fails to simplify paths when there
+ # are float coordinates and control points are very close to one another.
+ # Rounding coordinates to integers works around the bug.
+ # Since we are going to round glyf coordinates later on anyway, here it is
+ # ok(-ish) to also round before simplify. Better than failing the whole process
+ # for the entire font.
+ # https://bugs.chromium.org/p/skia/issues/detail?id=11958
+ # https://github.com/google/fonts/issues/3365
+ # TODO(anthrotype): remove once this Skia bug is fixed
+ try:
+ return pathops.simplify(path, clockwise=path.clockwise)
+ except pathops.PathOpsError:
+ pass
+
+ path = _round_path(path)
+ try:
+ path = pathops.simplify(path, clockwise=path.clockwise)
+ log.debug(
+ "skia-pathops failed to simplify '%s' with float coordinates, "
+ "but succeded using rounded integer coordinates",
+ debugGlyphName,
+ )
+ return path
+ except pathops.PathOpsError as e:
+ if log.isEnabledFor(logging.DEBUG):
+ path.dump()
+ raise RemoveOverlapsError(
+ f"Failed to remove overlaps from glyph {debugGlyphName!r}"
+ ) from e
+
+ raise AssertionError("Unreachable")
+
+
def removeTTGlyphOverlaps(
glyphName: str,
glyphSet: _TTGlyphMapping,
@@ -93,7 +141,7 @@ def removeTTGlyphOverlaps(
path = skPathFromGlyph(glyphName, glyphSet)
# remove overlaps
- path2 = pathops.simplify(path, clockwise=path.clockwise)
+ path2 = _simplify(path, glyphName)
# replace TTGlyph if simplified path is different (ignoring contour order)
if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}:
@@ -115,6 +163,7 @@ def removeOverlaps(
font: ttFont.TTFont,
glyphNames: Optional[Iterable[str]] = None,
removeHinting: bool = True,
+ ignoreErrors=False,
) -> None:
"""Simplify glyphs in TTFont by merging overlapping contours.
@@ -132,6 +181,8 @@ def removeOverlaps(
glyphNames: optional iterable of glyph names (str) to remove overlaps from.
By default, all glyphs in the font are processed.
removeHinting (bool): set to False to keep hinting for unmodified glyphs.
+ ignoreErrors (bool): set to True to ignore errors while removing overlaps,
+ thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363).
"""
try:
glyfTable = font["glyf"]
@@ -159,10 +210,15 @@ def removeOverlaps(
)
modified = set()
for glyphName in glyphNames:
- if removeTTGlyphOverlaps(
- glyphName, glyphSet, glyfTable, hmtxTable, removeHinting
- ):
- modified.add(glyphName)
+ try:
+ if removeTTGlyphOverlaps(
+ glyphName, glyphSet, glyfTable, hmtxTable, removeHinting
+ ):
+ modified.add(glyphName)
+ except RemoveOverlapsError:
+ if not ignoreErrors:
+ raise
+ log.error("Failed to remove overlaps for '%s'", glyphName)
log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified))
diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py
index d609dc51..e7c06337 100644
--- a/Lib/fontTools/ttLib/sfnt.py
+++ b/Lib/fontTools/ttLib/sfnt.py
@@ -8,13 +8,13 @@ Defines two public classes:
used automatically by ttLib.TTFont.)
The reading and writing of sfnt files is separated in two distinct
-classes, since whenever to number of tables changes or whenever
-a table's length chages you need to rewrite the whole file anyway.
+classes, since whenever the number of tables changes or whenever
+a table's length changes you need to rewrite the whole file anyway.
"""
from io import BytesIO
from types import SimpleNamespace
-from fontTools.misc.py23 import Tag
+from fontTools.misc.textTools import Tag
from fontTools.misc import sstruct
from fontTools.ttLib import TTLibError
import struct
@@ -571,9 +571,6 @@ class WOFFFlavorData():
def calcChecksum(data):
"""Calculate the checksum for an arbitrary block of data.
- Optionally takes a 'start' argument, which allows you to
- calculate a checksum in chunks by feeding it a previous
- result.
If the data length is not a multiple of four, it assumes
it is to be padded with null byte.
diff --git a/Lib/fontTools/ttLib/tables/C_B_D_T_.py b/Lib/fontTools/ttLib/tables/C_B_D_T_.py
index 11bb60b8..adf5447f 100644
--- a/Lib/fontTools/ttLib/tables/C_B_D_T_.py
+++ b/Lib/fontTools/ttLib/tables/C_B_D_T_.py
@@ -3,7 +3,7 @@
# Google Author(s): Matt Fontaine
-from fontTools.misc.py23 import bytesjoin
+from fontTools.misc.textTools import bytesjoin
from fontTools.misc import sstruct
from . import E_B_D_T_
from .BitmapGlyphMetrics import BigGlyphMetrics, bigGlyphMetricsFormat, SmallGlyphMetrics, smallGlyphMetricsFormat
diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
index 4004d417..3528bf5b 100644
--- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py
+++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
@@ -9,8 +9,10 @@ from . import DefaultTable
class table_C_O_L_R_(DefaultTable.DefaultTable):
""" This table is structured so that you can treat it like a dictionary keyed by glyph name.
- ttFont['COLR'][<glyphName>] will return the color layers for any glyph
- ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
+
+ ``ttFont['COLR'][<glyphName>]`` will return the color layers for any glyph.
+
+ ``ttFont['COLR'][<glyphName>] = <value>`` will set the color layers for any glyph.
"""
@staticmethod
diff --git a/Lib/fontTools/ttLib/tables/C_P_A_L_.py b/Lib/fontTools/ttLib/tables/C_P_A_L_.py
index c095095e..1ad342f1 100644
--- a/Lib/fontTools/ttLib/tables/C_P_A_L_.py
+++ b/Lib/fontTools/ttLib/tables/C_P_A_L_.py
@@ -2,8 +2,7 @@
#
# Google Author(s): Behdad Esfahbod
-from fontTools.misc.py23 import bytesjoin
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, safeEval
from . import DefaultTable
import array
from collections import namedtuple
diff --git a/Lib/fontTools/ttLib/tables/D_S_I_G_.py b/Lib/fontTools/ttLib/tables/D_S_I_G_.py
index 1a520cab..02fddee6 100644
--- a/Lib/fontTools/ttLib/tables/D_S_I_G_.py
+++ b/Lib/fontTools/ttLib/tables/D_S_I_G_.py
@@ -1,5 +1,4 @@
-from fontTools.misc.py23 import bytesjoin, strjoin, tobytes, tostr
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, strjoin, tobytes, tostr, safeEval
from fontTools.misc import sstruct
from . import DefaultTable
import base64
diff --git a/Lib/fontTools/ttLib/tables/DefaultTable.py b/Lib/fontTools/ttLib/tables/DefaultTable.py
index c70480a3..dae83183 100644
--- a/Lib/fontTools/ttLib/tables/DefaultTable.py
+++ b/Lib/fontTools/ttLib/tables/DefaultTable.py
@@ -1,4 +1,4 @@
-from fontTools.misc.py23 import Tag
+from fontTools.misc.textTools import Tag
from fontTools.ttLib import getClassTag
class DefaultTable(object):
diff --git a/Lib/fontTools/ttLib/tables/E_B_D_T_.py b/Lib/fontTools/ttLib/tables/E_B_D_T_.py
index 5d9e7244..0bd2ab99 100644
--- a/Lib/fontTools/ttLib/tables/E_B_D_T_.py
+++ b/Lib/fontTools/ttLib/tables/E_B_D_T_.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import bytechr, byteord, bytesjoin, strjoin
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval, readHex, hexStr, deHexStr
+from fontTools.misc.textTools import bytechr, byteord, bytesjoin, strjoin, safeEval, readHex, hexStr, deHexStr
from .BitmapGlyphMetrics import BigGlyphMetrics, bigGlyphMetricsFormat, SmallGlyphMetrics, smallGlyphMetricsFormat
from . import DefaultTable
import itertools
diff --git a/Lib/fontTools/ttLib/tables/E_B_L_C_.py b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
index 94d40d96..cfdbca7b 100644
--- a/Lib/fontTools/ttLib/tables/E_B_L_C_.py
+++ b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
@@ -1,7 +1,6 @@
-from fontTools.misc.py23 import bytesjoin
from fontTools.misc import sstruct
from . import DefaultTable
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, safeEval
from .BitmapGlyphMetrics import BigGlyphMetrics, bigGlyphMetricsFormat, SmallGlyphMetrics, smallGlyphMetricsFormat
import struct
import itertools
diff --git a/Lib/fontTools/ttLib/tables/F__e_a_t.py b/Lib/fontTools/ttLib/tables/F__e_a_t.py
index 7e510614..a444c11d 100644
--- a/Lib/fontTools/ttLib/tables/F__e_a_t.py
+++ b/Lib/fontTools/ttLib/tables/F__e_a_t.py
@@ -11,6 +11,12 @@ Feat_hdr_format='''
'''
class table_F__e_a_t(DefaultTable.DefaultTable):
+ """The ``Feat`` table is used exclusively by the Graphite shaping engine
+ to store features and possible settings specified in GDL. Graphite features
+ determine what rules are applied to transform a glyph stream.
+
+ Not to be confused with ``feat``, or the OpenType Layout tables
+ ``GSUB``/``GPOS``."""
def __init__(self, tag=None):
DefaultTable.DefaultTable.__init__(self, tag)
diff --git a/Lib/fontTools/ttLib/tables/G_M_A_P_.py b/Lib/fontTools/ttLib/tables/G_M_A_P_.py
index 5b30dcfe..833890da 100644
--- a/Lib/fontTools/ttLib/tables/G_M_A_P_.py
+++ b/Lib/fontTools/ttLib/tables/G_M_A_P_.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import tobytes, tostr
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import tobytes, tostr, safeEval
from . import DefaultTable
GMAPFormat = """
diff --git a/Lib/fontTools/ttLib/tables/G_P_K_G_.py b/Lib/fontTools/ttLib/tables/G_P_K_G_.py
index 7598a62a..4f469c02 100644
--- a/Lib/fontTools/ttLib/tables/G_P_K_G_.py
+++ b/Lib/fontTools/ttLib/tables/G_P_K_G_.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import bytesjoin
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval, readHex
+from fontTools.misc.textTools import bytesjoin, safeEval, readHex
from . import DefaultTable
import sys
import array
diff --git a/Lib/fontTools/ttLib/tables/M_E_T_A_.py b/Lib/fontTools/ttLib/tables/M_E_T_A_.py
index d4f6bc8c..990bfd2d 100644
--- a/Lib/fontTools/ttLib/tables/M_E_T_A_.py
+++ b/Lib/fontTools/ttLib/tables/M_E_T_A_.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import byteord
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import byteord, safeEval
from . import DefaultTable
import pdb
import struct
diff --git a/Lib/fontTools/ttLib/tables/S_I_N_G_.py b/Lib/fontTools/ttLib/tables/S_I_N_G_.py
index dd9b63c4..73246df4 100644
--- a/Lib/fontTools/ttLib/tables/S_I_N_G_.py
+++ b/Lib/fontTools/ttLib/tables/S_I_N_G_.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import bytechr, byteord, tobytes, tostr
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytechr, byteord, tobytes, tostr, safeEval
from . import DefaultTable
SINGFormat = """
diff --git a/Lib/fontTools/ttLib/tables/S_V_G_.py b/Lib/fontTools/ttLib/tables/S_V_G_.py
index 135f2718..bc0e533d 100644
--- a/Lib/fontTools/ttLib/tables/S_V_G_.py
+++ b/Lib/fontTools/ttLib/tables/S_V_G_.py
@@ -1,10 +1,25 @@
-from fontTools.misc.py23 import bytesjoin, strjoin, tobytes, tostr
+"""Compiles/decompiles SVG table.
+
+https://docs.microsoft.com/en-us/typography/opentype/spec/svg
+
+The XML format is:
+
+.. code-block:: xml
+
+ <SVG>
+ <svgDoc endGlyphID="1" startGlyphID="1">
+ <![CDATA[ <complete SVG doc> ]]
+ </svgDoc>
+ ...
+ <svgDoc endGlyphID="n" startGlyphID="m">
+ <![CDATA[ <complete SVG doc> ]]
+ </svgDoc>
+ </SVG>
+"""
+
+from fontTools.misc.textTools import bytesjoin, strjoin, tobytes, tostr
from fontTools.misc import sstruct
from . import DefaultTable
-try:
- import xml.etree.cElementTree as ET
-except ImportError:
- import xml.etree.ElementTree as ET
from io import BytesIO
import struct
import logging
@@ -13,71 +28,15 @@ import logging
log = logging.getLogger(__name__)
-__doc__="""
-Compiles/decompiles version 0 and 1 SVG tables from/to XML.
-
-Version 1 is the first SVG definition, implemented in Mozilla before Aug 2013, now deprecated.
-This module will decompile this correctly, but will compile a version 1 table
-only if you add the secret element "<version1/>" to the SVG element in the TTF file.
-
-Version 0 is the joint Adobe-Mozilla proposal, which supports color palettes.
-
-The XML format is:
-<SVG>
- <svgDoc endGlyphID="1" startGlyphID="1">
- <![CDATA[ <complete SVG doc> ]]
- </svgDoc>
-...
- <svgDoc endGlyphID="n" startGlyphID="m">
- <![CDATA[ <complete SVG doc> ]]
- </svgDoc>
-
- <colorPalettes>
- <colorParamUINameID>n</colorParamUINameID>
- ...
- <colorParamUINameID>m</colorParamUINameID>
- <colorPalette uiNameID="n">
- <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" />
- ...
- <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" />
- </colorPalette>
- ...
- <colorPalette uiNameID="m">
- <colorRecord red="<int> green="<int>" blue="<int>" alpha="<int>" />
- ...
- <colorRecord red=<int>" green="<int>" blue="<int>" alpha="<int>" />
- </colorPalette>
- </colorPalettes>
-</SVG>
-
-Color values must be less than 256.
-
-The number of color records in each </colorPalette> must be the same as
-the number of <colorParamUINameID> elements.
-
-"""
-
-XML = ET.XML
-XMLElement = ET.Element
-xmlToString = ET.tostring
-
SVG_format_0 = """
> # big endian
version: H
offsetToSVGDocIndex: L
- offsetToColorPalettes: L
+ reserved: L
"""
SVG_format_0Size = sstruct.calcsize(SVG_format_0)
-SVG_format_1 = """
- > # big endian
- version: H
- numIndicies: H
-"""
-
-SVG_format_1Size = sstruct.calcsize(SVG_format_1)
-
doc_index_entry_format_0 = """
> # big endian
startGlyphID: H
@@ -88,84 +47,26 @@ doc_index_entry_format_0 = """
doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0)
-colorRecord_format_0 = """
- red: B
- green: B
- blue: B
- alpha: B
-"""
class table_S_V_G_(DefaultTable.DefaultTable):
- def __init__(self, tag=None):
- DefaultTable.DefaultTable.__init__(self, tag)
- self.colorPalettes = None
-
def decompile(self, data, ttFont):
- self.docList = None
- self.colorPalettes = None
- pos = 0
- self.version = struct.unpack(">H", data[pos:pos+2])[0]
-
- if self.version == 1:
- # This is pre-standardization version of the table; and obsolete. But we decompile it for now.
- # https://wiki.mozilla.org/SVGOpenTypeFonts
- self.decompile_format_1(data, ttFont)
- else:
- if self.version != 0:
- log.warning(
- "Unknown SVG table version '%s'. Decompiling as version 0.", self.version)
- # This is the standardized version of the table; and current.
- # https://www.microsoft.com/typography/otspec/svg.htm
- self.decompile_format_0(data, ttFont)
-
- def decompile_format_0(self, data, ttFont):
- dummy, data2 = sstruct.unpack2(SVG_format_0, data, self)
+ self.docList = []
+ # Version 0 is the standardized version of the table; and current.
+ # https://www.microsoft.com/typography/otspec/svg.htm
+ sstruct.unpack(SVG_format_0, data[:SVG_format_0Size], self)
+ if self.version != 0:
+ log.warning(
+ "Unknown SVG table version '%s'. Decompiling as version 0.", self.version)
# read in SVG Documents Index
- self.decompileEntryList(data)
-
- # read in colorPalettes table.
- self.colorPalettes = colorPalettes = ColorPalettes()
- pos = self.offsetToColorPalettes
- if pos > 0:
- colorPalettes.numColorParams = numColorParams = struct.unpack(">H", data[pos:pos+2])[0]
- if numColorParams > 0:
- colorPalettes.colorParamUINameIDs = colorParamUINameIDs = []
- pos = pos + 2
- for i in range(numColorParams):
- nameID = struct.unpack(">H", data[pos:pos+2])[0]
- colorParamUINameIDs.append(nameID)
- pos = pos + 2
-
- colorPalettes.numColorPalettes = numColorPalettes = struct.unpack(">H", data[pos:pos+2])[0]
- pos = pos + 2
- if numColorPalettes > 0:
- colorPalettes.colorPaletteList = colorPaletteList = []
- for i in range(numColorPalettes):
- colorPalette = ColorPalette()
- colorPaletteList.append(colorPalette)
- colorPalette.uiNameID = struct.unpack(">H", data[pos:pos+2])[0]
- pos = pos + 2
- colorPalette.paletteColors = paletteColors = []
- for j in range(numColorParams):
- colorRecord, colorPaletteData = sstruct.unpack2(colorRecord_format_0, data[pos:], ColorRecord())
- paletteColors.append(colorRecord)
- pos += 4
-
- def decompile_format_1(self, data, ttFont):
- self.offsetToSVGDocIndex = 2
- self.decompileEntryList(data)
-
- def decompileEntryList(self, data):
# data starts with the first entry of the entry list.
pos = subTableStart = self.offsetToSVGDocIndex
- self.numEntries = numEntries = struct.unpack(">H", data[pos:pos+2])[0]
+ self.numEntries = struct.unpack(">H", data[pos:pos+2])[0]
pos += 2
if self.numEntries > 0:
data2 = data[pos:]
- self.docList = []
- self.entries = entries = []
+ entries = []
for i in range(self.numEntries):
docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, DocumentIndexEntry())
entries.append(docIndexEntry)
@@ -185,13 +86,6 @@ class table_S_V_G_(DefaultTable.DefaultTable):
self.docList.append( [doc, entry.startGlyphID, entry.endGlyphID] )
def compile(self, ttFont):
- if hasattr(self, "version1"):
- data = self.compileFormat1(ttFont)
- else:
- data = self.compileFormat0(ttFont)
- return data
-
- def compileFormat0(self, ttFont):
version = 0
offsetToSVGDocIndex = SVG_format_0Size # I start the SVGDocIndex right after the header.
# get SGVDoc info.
@@ -201,8 +95,8 @@ class table_S_V_G_(DefaultTable.DefaultTable):
datum = struct.pack(">H",numEntries)
entryList.append(datum)
curOffset = len(datum) + doc_index_entry_format_0Size*numEntries
+ seenDocs = {}
for doc, startGlyphID, endGlyphID in self.docList:
- docOffset = curOffset
docBytes = tobytes(doc, encoding="utf_8")
if getattr(self, "compressed", False) and not docBytes.startswith(b"\x1f\x8b"):
import gzip
@@ -214,63 +108,25 @@ class table_S_V_G_(DefaultTable.DefaultTable):
docBytes = gzipped
del gzipped, bytesIO
docLength = len(docBytes)
- curOffset += docLength
+ if docBytes in seenDocs:
+ docOffset = seenDocs[docBytes]
+ else:
+ docOffset = curOffset
+ curOffset += docLength
+ seenDocs[docBytes] = docOffset
+ docList.append(docBytes)
entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength)
entryList.append(entry)
- docList.append(docBytes)
entryList.extend(docList)
svgDocData = bytesjoin(entryList)
- # get colorpalette info.
- if self.colorPalettes is None:
- offsetToColorPalettes = 0
- palettesData = ""
- else:
- offsetToColorPalettes = SVG_format_0Size + len(svgDocData)
- dataList = []
- numColorParams = len(self.colorPalettes.colorParamUINameIDs)
- datum = struct.pack(">H", numColorParams)
- dataList.append(datum)
- for uiNameId in self.colorPalettes.colorParamUINameIDs:
- datum = struct.pack(">H", uiNameId)
- dataList.append(datum)
- numColorPalettes = len(self.colorPalettes.colorPaletteList)
- datum = struct.pack(">H", numColorPalettes)
- dataList.append(datum)
- for colorPalette in self.colorPalettes.colorPaletteList:
- datum = struct.pack(">H", colorPalette.uiNameID)
- dataList.append(datum)
- for colorRecord in colorPalette.paletteColors:
- data = struct.pack(">BBBB", colorRecord.red, colorRecord.green, colorRecord.blue, colorRecord.alpha)
- dataList.append(data)
- palettesData = bytesjoin(dataList)
-
- header = struct.pack(">HLL", version, offsetToSVGDocIndex, offsetToColorPalettes)
- data = [header, svgDocData, palettesData]
+ reserved = 0
+ header = struct.pack(">HLL", version, offsetToSVGDocIndex, reserved)
+ data = [header, svgDocData]
data = bytesjoin(data)
return data
- def compileFormat1(self, ttFont):
- version = 1
- numEntries = len(self.docList)
- header = struct.pack(">HH", version, numEntries)
- dataList = [header]
- docList = []
- curOffset = SVG_format_1Size + doc_index_entry_format_0Size*numEntries
- for doc, startGlyphID, endGlyphID in self.docList:
- docOffset = curOffset
- docBytes = tobytes(doc, encoding="utf_8")
- docLength = len(docBytes)
- curOffset += docLength
- entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength)
- dataList.append(entry)
- docList.append(docBytes)
- dataList.extend(docList)
- data = bytesjoin(dataList)
- return data
-
def toXML(self, writer, ttFont):
- writer.newline()
for doc, startGID, endGID in self.docList:
writer.begintag("svgDoc", startGlyphID=startGID, endGlyphID=endGID)
writer.newline()
@@ -279,33 +135,6 @@ class table_S_V_G_(DefaultTable.DefaultTable):
writer.endtag("svgDoc")
writer.newline()
- if (self.colorPalettes is not None) and (self.colorPalettes.numColorParams is not None):
- writer.begintag("colorPalettes")
- writer.newline()
- for uiNameID in self.colorPalettes.colorParamUINameIDs:
- writer.begintag("colorParamUINameID")
- writer._writeraw(str(uiNameID))
- writer.endtag("colorParamUINameID")
- writer.newline()
- for colorPalette in self.colorPalettes.colorPaletteList:
- writer.begintag("colorPalette", [("uiNameID", str(colorPalette.uiNameID))])
- writer.newline()
- for colorRecord in colorPalette.paletteColors:
- colorAttributes = [
- ("red", hex(colorRecord.red)),
- ("green", hex(colorRecord.green)),
- ("blue", hex(colorRecord.blue)),
- ("alpha", hex(colorRecord.alpha)),
- ]
- writer.begintag("colorRecord", colorAttributes)
- writer.endtag("colorRecord")
- writer.newline()
- writer.endtag("colorPalette")
- writer.newline()
-
- writer.endtag("colorPalettes")
- writer.newline()
-
def fromXML(self, name, attrs, content, ttFont):
if name == "svgDoc":
if not hasattr(self, "docList"):
@@ -315,14 +144,10 @@ class table_S_V_G_(DefaultTable.DefaultTable):
startGID = int(attrs["startGlyphID"])
endGID = int(attrs["endGlyphID"])
self.docList.append( [doc, startGID, endGID] )
- elif name == "colorPalettes":
- self.colorPalettes = ColorPalettes()
- self.colorPalettes.fromXML(name, attrs, content, ttFont)
- if self.colorPalettes.numColorParams == 0:
- self.colorPalettes = None
else:
log.warning("Unknown %s %s", name, content)
+
class DocumentIndexEntry(object):
def __init__(self):
self.startGlyphID = None # USHORT
@@ -332,55 +157,3 @@ class DocumentIndexEntry(object):
def __repr__(self):
return "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s" % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength)
-
-class ColorPalettes(object):
- def __init__(self):
- self.numColorParams = None # USHORT
- self.colorParamUINameIDs = [] # list of name table name ID values that provide UI description of each color palette.
- self.numColorPalettes = None # USHORT
- self.colorPaletteList = [] # list of ColorPalette records
-
- def fromXML(self, name, attrs, content, ttFont):
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrib, content = element
- if name == "colorParamUINameID":
- uiNameID = int(content[0])
- self.colorParamUINameIDs.append(uiNameID)
- elif name == "colorPalette":
- colorPalette = ColorPalette()
- self.colorPaletteList.append(colorPalette)
- colorPalette.fromXML(name, attrib, content, ttFont)
-
- self.numColorParams = len(self.colorParamUINameIDs)
- self.numColorPalettes = len(self.colorPaletteList)
- for colorPalette in self.colorPaletteList:
- if len(colorPalette.paletteColors) != self.numColorParams:
- raise ValueError("Number of color records in a colorPalette ('%s') does not match the number of colorParamUINameIDs elements ('%s')." % (len(colorPalette.paletteColors), self.numColorParams))
-
-class ColorPalette(object):
- def __init__(self):
- self.uiNameID = None # USHORT. name table ID that describes user interface strings associated with this color palette.
- self.paletteColors = [] # list of ColorRecords
-
- def fromXML(self, name, attrs, content, ttFont):
- self.uiNameID = int(attrs["uiNameID"])
- for element in content:
- if isinstance(element, type("")):
- continue
- name, attrib, content = element
- if name == "colorRecord":
- colorRecord = ColorRecord()
- self.paletteColors.append(colorRecord)
- colorRecord.red = eval(attrib["red"])
- colorRecord.green = eval(attrib["green"])
- colorRecord.blue = eval(attrib["blue"])
- colorRecord.alpha = eval(attrib["alpha"])
-
-class ColorRecord(object):
- def __init__(self):
- self.red = 255 # all are one byte values.
- self.green = 255
- self.blue = 255
- self.alpha = 255
diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py
index 95880b07..f326c386 100644
--- a/Lib/fontTools/ttLib/tables/S__i_l_f.py
+++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py
@@ -1,7 +1,6 @@
-from fontTools.misc.py23 import byteord
from fontTools.misc import sstruct
from fontTools.misc.fixedTools import floatToFixedToStr
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import byteord, safeEval
# from itertools import *
from . import DefaultTable
from . import grUtils
diff --git a/Lib/fontTools/ttLib/tables/T_S_I_V_.py b/Lib/fontTools/ttLib/tables/T_S_I_V_.py
index 80214452..c1e244c6 100644
--- a/Lib/fontTools/ttLib/tables/T_S_I_V_.py
+++ b/Lib/fontTools/ttLib/tables/T_S_I_V_.py
@@ -1,4 +1,4 @@
-from fontTools.misc.py23 import strjoin, tobytes, tostr
+from fontTools.misc.textTools import strjoin, tobytes, tostr
from . import asciiTable
class table_T_S_I_V_(asciiTable.asciiTable):
diff --git a/Lib/fontTools/ttLib/tables/T_S_I__1.py b/Lib/fontTools/ttLib/tables/T_S_I__1.py
index 9ae7acd6..7f7608b2 100644
--- a/Lib/fontTools/ttLib/tables/T_S_I__1.py
+++ b/Lib/fontTools/ttLib/tables/T_S_I__1.py
@@ -4,9 +4,9 @@ tool to store its hinting source data.
TSI1 contains the text of the glyph programs in the form of low-level assembly
code, as well as the 'extra' programs 'fpgm', 'ppgm' (i.e. 'prep'), and 'cvt'.
"""
-from fontTools.misc.py23 import strjoin, tobytes, tostr
from . import DefaultTable
from fontTools.misc.loggingTools import LogMixin
+from fontTools.misc.textTools import strjoin, tobytes, tostr
class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable):
diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py
index a63fb6c6..9c2895e4 100644
--- a/Lib/fontTools/ttLib/tables/TupleVariation.py
+++ b/Lib/fontTools/ttLib/tables/TupleVariation.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import bytechr, byteord, bytesjoin
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
floatToFixed as fl2fi,
@@ -8,6 +7,7 @@ from fontTools.misc.fixedTools import (
)
from fontTools.misc.textTools import safeEval
import array
+from collections import Counter, defaultdict
import io
import logging
import struct
@@ -38,7 +38,7 @@ class TupleVariation(object):
def __init__(self, axes, coordinates):
self.axes = axes.copy()
- self.coordinates = coordinates[:]
+ self.coordinates = list(coordinates)
def __repr__(self):
axes = ",".join(sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()]))
@@ -48,11 +48,12 @@ class TupleVariation(object):
return self.coordinates == other.coordinates and self.axes == other.axes
def getUsedPoints(self):
- result = set()
- for i, point in enumerate(self.coordinates):
- if point is not None:
- result.add(i)
- return result
+ # Empty set means "all points used".
+ if None not in self.coordinates:
+ return frozenset()
+ used = frozenset([i for i,p in enumerate(self.coordinates) if p is not None])
+ # Return None if no points used.
+ return used if used else None
def hasImpact(self):
"""Returns True if this TupleVariation has any visible impact.
@@ -126,15 +127,21 @@ class TupleVariation(object):
log.warning("bad delta format: %s" %
", ".join(sorted(attrs.keys())))
- def compile(self, axisTags, sharedCoordIndices, sharedPoints):
+ def compile(self, axisTags, sharedCoordIndices={}, pointData=None):
+ assert set(self.axes.keys()) <= set(axisTags), ("Unknown axis tag found.", self.axes.keys(), axisTags)
+
tupleData = []
+ auxData = []
- assert all(tag in axisTags for tag in self.axes.keys()), ("Unknown axis tag found.", self.axes.keys(), axisTags)
+ if pointData is None:
+ usedPoints = self.getUsedPoints()
+ if usedPoints is None: # Nothing to encode
+ return b'', b''
+ pointData = self.compilePoints(usedPoints)
coord = self.compileCoord(axisTags)
- if coord in sharedCoordIndices:
- flags = sharedCoordIndices[coord]
- else:
+ flags = sharedCoordIndices.get(coord)
+ if flags is None:
flags = EMBEDDED_PEAK_TUPLE
tupleData.append(coord)
@@ -143,26 +150,27 @@ class TupleVariation(object):
flags |= INTERMEDIATE_REGION
tupleData.append(intermediateCoord)
- points = self.getUsedPoints()
- if sharedPoints == points:
- # Only use the shared points if they are identical to the actually used points
- auxData = self.compileDeltas(sharedPoints)
- usesSharedPoints = True
- else:
+ # pointData of b'' implies "use shared points".
+ if pointData:
flags |= PRIVATE_POINT_NUMBERS
- numPointsInGlyph = len(self.coordinates)
- auxData = self.compilePoints(points, numPointsInGlyph) + self.compileDeltas(points)
- usesSharedPoints = False
+ auxData.append(pointData)
- tupleData = struct.pack('>HH', len(auxData), flags) + bytesjoin(tupleData)
- return (tupleData, auxData, usesSharedPoints)
+ auxData.append(self.compileDeltas())
+ auxData = b''.join(auxData)
+
+ tupleData.insert(0, struct.pack('>HH', len(auxData), flags))
+ return b''.join(tupleData), auxData
def compileCoord(self, axisTags):
- result = []
+ result = bytearray()
+ axes = self.axes
for axis in axisTags:
- _minValue, value, _maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
- result.append(struct.pack(">h", fl2fi(value, 14)))
- return bytesjoin(result)
+ triple = axes.get(axis)
+ if triple is None:
+ result.extend(b'\0\0')
+ else:
+ result.extend(struct.pack(">h", fl2fi(triple[1], 14)))
+ return bytes(result)
def compileIntermediateCoord(self, axisTags):
needed = False
@@ -175,13 +183,13 @@ class TupleVariation(object):
break
if not needed:
return None
- minCoords = []
- maxCoords = []
+ minCoords = bytearray()
+ maxCoords = bytearray()
for axis in axisTags:
minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
- minCoords.append(struct.pack(">h", fl2fi(minValue, 14)))
- maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14)))
- return bytesjoin(minCoords + maxCoords)
+ minCoords.extend(struct.pack(">h", fl2fi(minValue, 14)))
+ maxCoords.extend(struct.pack(">h", fl2fi(maxValue, 14)))
+ return minCoords + maxCoords
@staticmethod
def decompileCoord_(axisTags, data, offset):
@@ -193,11 +201,15 @@ class TupleVariation(object):
return coord, pos
@staticmethod
- def compilePoints(points, numPointsInGlyph):
+ def compilePoints(points):
# If the set consists of all points in the glyph, it gets encoded with
# a special encoding: a single zero byte.
- if len(points) == numPointsInGlyph:
- return b"\0"
+ #
+ # To use this optimization, points passed in must be empty set.
+ # The following two lines are not strictly necessary as the main code
+ # below would emit the same. But this is most common and faster.
+ if not points:
+ return b'\0'
# In the 'gvar' table, the packing of point numbers is a little surprising.
# It consists of multiple runs, each being a delta-encoded list of integers.
@@ -209,19 +221,24 @@ class TupleVariation(object):
points.sort()
numPoints = len(points)
+ result = bytearray()
# The binary representation starts with the total number of points in the set,
# encoded into one or two bytes depending on the value.
if numPoints < 0x80:
- result = [bytechr(numPoints)]
+ result.append(numPoints)
else:
- result = [bytechr((numPoints >> 8) | 0x80) + bytechr(numPoints & 0xff)]
+ result.append((numPoints >> 8) | 0x80)
+ result.append(numPoints & 0xff)
MAX_RUN_LENGTH = 127
pos = 0
lastValue = 0
while pos < numPoints:
- run = io.BytesIO()
runLength = 0
+
+ headerPos = len(result)
+ result.append(0)
+
useByteEncoding = None
while pos < numPoints and runLength <= MAX_RUN_LENGTH:
curValue = points[pos]
@@ -234,38 +251,36 @@ class TupleVariation(object):
# TODO This never switches back to a byte-encoding from a short-encoding.
# That's suboptimal.
if useByteEncoding:
- run.write(bytechr(delta))
+ result.append(delta)
else:
- run.write(bytechr(delta >> 8))
- run.write(bytechr(delta & 0xff))
+ result.append(delta >> 8)
+ result.append(delta & 0xff)
lastValue = curValue
pos += 1
runLength += 1
if useByteEncoding:
- runHeader = bytechr(runLength - 1)
+ result[headerPos] = runLength - 1
else:
- runHeader = bytechr((runLength - 1) | POINTS_ARE_WORDS)
- result.append(runHeader)
- result.append(run.getvalue())
+ result[headerPos] = (runLength - 1) | POINTS_ARE_WORDS
- return bytesjoin(result)
+ return result
@staticmethod
def decompilePoints_(numPoints, data, offset, tableTag):
"""(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)"""
assert tableTag in ('cvar', 'gvar')
pos = offset
- numPointsInData = byteord(data[pos])
+ numPointsInData = data[pos]
pos += 1
if (numPointsInData & POINTS_ARE_WORDS) != 0:
- numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | byteord(data[pos])
+ numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | data[pos]
pos += 1
if numPointsInData == 0:
return (range(numPoints), pos)
result = []
while len(result) < numPointsInData:
- runHeader = byteord(data[pos])
+ runHeader = data[pos]
pos += 1
numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1
point = 0
@@ -298,23 +313,28 @@ class TupleVariation(object):
(",".join(sorted(badPoints)), tableTag))
return (result, pos)
- def compileDeltas(self, points):
+ def compileDeltas(self):
deltaX = []
deltaY = []
- for p in sorted(list(points)):
- c = self.coordinates[p]
- if type(c) is tuple and len(c) == 2:
+ if self.getCoordWidth() == 2:
+ for c in self.coordinates:
+ if c is None:
+ continue
deltaX.append(c[0])
deltaY.append(c[1])
- elif type(c) is int:
+ else:
+ for c in self.coordinates:
+ if c is None:
+ continue
deltaX.append(c)
- elif c is not None:
- raise TypeError("invalid type of delta: %s" % type(c))
- return self.compileDeltaValues_(deltaX) + self.compileDeltaValues_(deltaY)
+ bytearr = bytearray()
+ self.compileDeltaValues_(deltaX, bytearr)
+ self.compileDeltaValues_(deltaY, bytearr)
+ return bytearr
@staticmethod
- def compileDeltaValues_(deltas):
- """[value1, value2, value3, ...] --> bytestring
+ def compileDeltaValues_(deltas, bytearr=None):
+ """[value1, value2, value3, ...] --> bytearray
Emits a sequence of runs. Each run starts with a
byte-sized header whose 6 least significant bits
@@ -329,38 +349,41 @@ class TupleVariation(object):
bytes; if (header & 0x40) is set, the delta values are
signed 16-bit integers.
""" # Explaining the format because the 'gvar' spec is hard to understand.
- stream = io.BytesIO()
+ if bytearr is None:
+ bytearr = bytearray()
pos = 0
- while pos < len(deltas):
+ numDeltas = len(deltas)
+ while pos < numDeltas:
value = deltas[pos]
if value == 0:
- pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, stream)
- elif value >= -128 and value <= 127:
- pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, stream)
+ pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
+ elif -128 <= value <= 127:
+ pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr)
else:
- pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, stream)
- return stream.getvalue()
+ pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
+ return bytearr
@staticmethod
- def encodeDeltaRunAsZeroes_(deltas, offset, stream):
- runLength = 0
+ def encodeDeltaRunAsZeroes_(deltas, offset, bytearr):
pos = offset
numDeltas = len(deltas)
- while pos < numDeltas and runLength < 64 and deltas[pos] == 0:
+ while pos < numDeltas and deltas[pos] == 0:
pos += 1
- runLength += 1
- assert runLength >= 1 and runLength <= 64
- stream.write(bytechr(DELTAS_ARE_ZERO | (runLength - 1)))
+ runLength = pos - offset
+ while runLength >= 64:
+ bytearr.append(DELTAS_ARE_ZERO | 63)
+ runLength -= 64
+ if runLength:
+ bytearr.append(DELTAS_ARE_ZERO | (runLength - 1))
return pos
@staticmethod
- def encodeDeltaRunAsBytes_(deltas, offset, stream):
- runLength = 0
+ def encodeDeltaRunAsBytes_(deltas, offset, bytearr):
pos = offset
numDeltas = len(deltas)
- while pos < numDeltas and runLength < 64:
+ while pos < numDeltas:
value = deltas[pos]
- if value < -128 or value > 127:
+ if not (-128 <= value <= 127):
break
# Within a byte-encoded run of deltas, a single zero
# is best stored literally as 0x00 value. However,
@@ -373,19 +396,22 @@ class TupleVariation(object):
if value == 0 and pos+1 < numDeltas and deltas[pos+1] == 0:
break
pos += 1
- runLength += 1
- assert runLength >= 1 and runLength <= 64
- stream.write(bytechr(runLength - 1))
- for i in range(offset, pos):
- stream.write(struct.pack('b', otRound(deltas[i])))
+ runLength = pos - offset
+ while runLength >= 64:
+ bytearr.append(63)
+ bytearr.extend(array.array('b', deltas[offset:offset+64]))
+ offset += 64
+ runLength -= 64
+ if runLength:
+ bytearr.append(runLength - 1)
+ bytearr.extend(array.array('b', deltas[offset:pos]))
return pos
@staticmethod
- def encodeDeltaRunAsWords_(deltas, offset, stream):
- runLength = 0
+ def encodeDeltaRunAsWords_(deltas, offset, bytearr):
pos = offset
numDeltas = len(deltas)
- while pos < numDeltas and runLength < 64:
+ 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)
@@ -403,15 +429,22 @@ class TupleVariation(object):
# [0x6666, 2, 0x7777] becomes 7 bytes when storing
# the value literally (42 66 66 00 02 77 77), but 8 bytes
# when starting a new run (40 66 66 00 02 40 77 77).
- isByteEncodable = lambda value: value >= -128 and value <= 127
- if isByteEncodable(value) and pos+1 < numDeltas and isByteEncodable(deltas[pos+1]):
+ if (-128 <= value <= 127) and pos+1 < numDeltas and (-128 <= deltas[pos+1] <= 127):
break
pos += 1
- runLength += 1
- assert runLength >= 1 and runLength <= 64
- stream.write(bytechr(DELTAS_ARE_WORDS | (runLength - 1)))
- for i in range(offset, pos):
- stream.write(struct.pack('>h', otRound(deltas[i])))
+ runLength = pos - offset
+ while runLength >= 64:
+ bytearr.append(DELTAS_ARE_WORDS | 63)
+ a = array.array('h', deltas[offset:offset+64])
+ if sys.byteorder != "big": a.byteswap()
+ bytearr.extend(a)
+ offset += 64
+ runLength -= 64
+ if runLength:
+ bytearr.append(DELTAS_ARE_WORDS | (runLength - 1))
+ a = array.array('h', deltas[offset:pos])
+ if sys.byteorder != "big": a.byteswap()
+ bytearr.extend(a)
return pos
@staticmethod
@@ -420,7 +453,7 @@ class TupleVariation(object):
result = []
pos = offset
while len(result) < numDeltas:
- runHeader = byteord(data[pos])
+ runHeader = data[pos]
pos += 1
numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1
if (runHeader & DELTAS_ARE_ZERO) != 0:
@@ -523,9 +556,9 @@ class TupleVariation(object):
# Shouldn't matter that this is different from fvar...?
axisTags = sorted(self.axes.keys())
- tupleData, auxData, _ = self.compile(axisTags, [], None)
+ tupleData, auxData = self.compile(axisTags)
unoptimizedLength = len(tupleData) + len(auxData)
- tupleData, auxData, _ = varOpt.compile(axisTags, [], None)
+ tupleData, auxData = varOpt.compile(axisTags)
optimizedLength = len(tupleData) + len(auxData)
if optimizedLength < unoptimizedLength:
@@ -577,87 +610,77 @@ def decompileSharedTuples(axisTags, sharedTupleCount, data, offset):
return result
-def compileSharedTuples(axisTags, variations):
- coordCount = {}
+def compileSharedTuples(axisTags, variations,
+ MAX_NUM_SHARED_COORDS = TUPLE_INDEX_MASK + 1):
+ coordCount = Counter()
for var in variations:
coord = var.compileCoord(axisTags)
- coordCount[coord] = coordCount.get(coord, 0) + 1
- sharedCoords = [(count, coord)
- for (coord, count) in coordCount.items() if count > 1]
- sharedCoords.sort(reverse=True)
- MAX_NUM_SHARED_COORDS = TUPLE_INDEX_MASK + 1
- sharedCoords = sharedCoords[:MAX_NUM_SHARED_COORDS]
- return [c[1] for c in sharedCoords] # Strip off counts.
+ coordCount[coord] += 1
+ # In python < 3.7, most_common() ordering is non-deterministic
+ # so apply a sort to make sure the ordering is consistent.
+ sharedCoords = sorted(
+ coordCount.most_common(MAX_NUM_SHARED_COORDS),
+ key=lambda item: (-item[1], item[0]),
+ )
+ return [c[0] for c in sharedCoords if c[1] > 1]
def compileTupleVariationStore(variations, pointCount,
axisTags, sharedTupleIndices,
useSharedPoints=True):
- variations = [v for v in variations if v.hasImpact()]
- if len(variations) == 0:
- return (0, b"", b"")
+ newVariations = []
+ pointDatas = []
+ # Compile all points and figure out sharing if desired
+ sharedPoints = None
- # Each glyph variation tuples modifies a set of control points. To
- # indicate which exact points are getting modified, a single tuple
- # can either refer to a shared set of points, or the tuple can
- # supply its private point numbers. Because the impact of sharing
- # can be positive (no need for a private point list) or negative
- # (need to supply 0,0 deltas for unused points), it is not obvious
- # how to determine which tuples should take their points from the
- # shared pool versus have their own. Perhaps we should resort to
- # brute force, and try all combinations? However, if a glyph has n
- # variation tuples, we would need to try 2^n combinations (because
- # each tuple may or may not be part of the shared set). How many
- # variations tuples do glyphs have?
- #
- # Skia.ttf: {3: 1, 5: 11, 6: 41, 7: 62, 8: 387, 13: 1, 14: 3}
- # JamRegular.ttf: {3: 13, 4: 122, 5: 1, 7: 4, 8: 1, 9: 1, 10: 1}
- # BuffaloGalRegular.ttf: {1: 16, 2: 13, 4: 2, 5: 4, 6: 19, 7: 1, 8: 3, 9: 8}
- # (Reading example: In Skia.ttf, 41 glyphs have 6 variation tuples).
- #
-
- # Is this even worth optimizing? If we never use a shared point
- # list, the private lists will consume 112K for Skia, 5K for
- # BuffaloGalRegular, and 15K for JamRegular. If we always use a
- # shared point list, the shared lists will consume 16K for Skia,
- # 3K for BuffaloGalRegular, and 10K for JamRegular. However, in
- # the latter case the delta arrays will become larger, but I
- # haven't yet measured by how much. From gut feeling (which may be
- # wrong), the optimum is to share some but not all points;
- # however, then we would need to try all combinations.
- #
- # For the time being, we try two variants and then pick the better one:
- # (a) each tuple supplies its own private set of points;
- # (b) all tuples refer to a shared set of points, which consists of
- # "every control point in the glyph that has explicit deltas".
- usedPoints = set()
+ # Collect, count, and compile point-sets for all variation sets
+ pointSetCount = defaultdict(int)
for v in variations:
- usedPoints |= v.getUsedPoints()
+ points = v.getUsedPoints()
+ if points is None: # Empty variations
+ continue
+ pointSetCount[points] += 1
+ newVariations.append(v)
+ pointDatas.append(points)
+ variations = newVariations
+ del newVariations
+
+ if not variations:
+ return (0, b"", b"")
+
+ n = len(variations[0].coordinates)
+ assert all(len(v.coordinates) == n for v in variations), "Variation sets have different sizes"
+
+ compiledPoints = {pointSet:TupleVariation.compilePoints(pointSet)
+ for pointSet in pointSetCount}
+
+ tupleVariationCount = len(variations)
tuples = []
data = []
- someTuplesSharePoints = False
- sharedPointVariation = None # To keep track of a variation that uses shared points
- for v in variations:
- privateTuple, privateData, _ = v.compile(
- axisTags, sharedTupleIndices, sharedPoints=None)
- sharedTuple, sharedData, usesSharedPoints = v.compile(
- axisTags, sharedTupleIndices, sharedPoints=usedPoints)
- if useSharedPoints and (len(sharedTuple) + len(sharedData)) < (len(privateTuple) + len(privateData)):
- tuples.append(sharedTuple)
- data.append(sharedData)
- someTuplesSharePoints |= usesSharedPoints
- sharedPointVariation = v
- else:
- tuples.append(privateTuple)
- data.append(privateData)
- if someTuplesSharePoints:
- # Use the last of the variations that share points for compiling the packed point data
- data = sharedPointVariation.compilePoints(usedPoints, len(sharedPointVariation.coordinates)) + bytesjoin(data)
- tupleVariationCount = TUPLES_SHARE_POINT_NUMBERS | len(tuples)
- else:
- data = bytesjoin(data)
- tupleVariationCount = len(tuples)
- tuples = bytesjoin(tuples)
+
+ if useSharedPoints:
+ # Find point-set which saves most bytes.
+ def key(pn):
+ pointSet = pn[0]
+ count = pn[1]
+ return len(compiledPoints[pointSet]) * (count - 1)
+ sharedPoints = max(pointSetCount.items(), key=key)[0]
+
+ data.append(compiledPoints[sharedPoints])
+ tupleVariationCount |= TUPLES_SHARE_POINT_NUMBERS
+
+ # b'' implies "use shared points"
+ pointDatas = [compiledPoints[points] if points != sharedPoints else b''
+ for points in pointDatas]
+
+ for v,p in zip(variations, pointDatas):
+ thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p)
+
+ tuples.append(thisTuple)
+ data.append(thisData)
+
+ tuples = b''.join(tuples)
+ data = b''.join(data)
return tupleVariationCount, tuples, data
diff --git a/Lib/fontTools/ttLib/tables/V_O_R_G_.py b/Lib/fontTools/ttLib/tables/V_O_R_G_.py
index 0b7fe959..e03e164b 100644
--- a/Lib/fontTools/ttLib/tables/V_O_R_G_.py
+++ b/Lib/fontTools/ttLib/tables/V_O_R_G_.py
@@ -1,14 +1,15 @@
-from fontTools.misc.py23 import bytesjoin
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, safeEval
from . import DefaultTable
import struct
class table_V_O_R_G_(DefaultTable.DefaultTable):
- """ This table is structured so that you can treat it like a dictionary keyed by glyph name.
- ttFont['VORG'][<glyphName>] will return the vertical origin for any glyph
- ttFont['VORG'][<glyphName>] = <value> will set the vertical origin for any glyph.
+ """This table is structured so that you can treat it like a dictionary keyed by glyph name.
+
+ ``ttFont['VORG'][<glyphName>]`` will return the vertical origin for any glyph.
+
+ ``ttFont['VORG'][<glyphName>] = <value>`` will set the vertical origin for any glyph.
"""
def decompile(self, data, ttFont):
diff --git a/Lib/fontTools/ttLib/tables/_a_n_k_r.py b/Lib/fontTools/ttLib/tables/_a_n_k_r.py
index 1f2946c2..16f5c184 100644
--- a/Lib/fontTools/ttLib/tables/_a_n_k_r.py
+++ b/Lib/fontTools/ttLib/tables/_a_n_k_r.py
@@ -1,11 +1,12 @@
from .otBase import BaseTTXConverter
-
-# The anchor point table provides a way to define anchor points.
-# These are points within the coordinate space of a given glyph,
-# independent of the control points used to render the glyph.
-# Anchor points are used in conjunction with the 'kerx' table.
-#
-# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html
class table__a_n_k_r(BaseTTXConverter):
+ """
+ The anchor point table provides a way to define anchor points.
+ These are points within the coordinate space of a given glyph,
+ independent of the control points used to render the glyph.
+ Anchor points are used in conjunction with the 'kerx' table.
+
+ See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html
+ """
pass
diff --git a/Lib/fontTools/ttLib/tables/_a_v_a_r.py b/Lib/fontTools/ttLib/tables/_a_v_a_r.py
index 2b6a40ed..16f2a219 100644
--- a/Lib/fontTools/ttLib/tables/_a_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_a_v_a_r.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import bytesjoin
from fontTools.misc import sstruct
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
@@ -6,6 +5,7 @@ from fontTools.misc.fixedTools import (
floatToFixedToStr as fl2str,
strToFixedToFloat as str2fl,
)
+from fontTools.misc.textTools import bytesjoin
from fontTools.ttLib import TTLibError
from . import DefaultTable
import struct
@@ -28,6 +28,28 @@ assert sstruct.calcsize(AVAR_HEADER_FORMAT) == 8, sstruct.calcsize(AVAR_HEADER_F
class table__a_v_a_r(DefaultTable.DefaultTable):
+ """Axis Variations Table
+
+ This class represents the ``avar`` table of a variable font. The object has one
+ substantive attribute, ``segments``, which maps axis tags to a segments dictionary::
+
+ >>> font["avar"].segments # doctest: +SKIP
+ {'wght': {-1.0: -1.0,
+ 0.0: 0.0,
+ 0.125: 0.11444091796875,
+ 0.25: 0.23492431640625,
+ 0.5: 0.35540771484375,
+ 0.625: 0.5,
+ 0.75: 0.6566162109375,
+ 0.875: 0.81927490234375,
+ 1.0: 1.0},
+ 'ital': {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}}
+
+ Notice that the segments dictionary is made up of normalized values. A valid
+ ``avar`` segment mapping must contain the entries ``-1.0: -1.0, 0.0: 0.0, 1.0: 1.0``.
+ fontTools does not enforce this, so it is your responsibility to ensure that
+ mappings are valid.
+ """
dependencies = ["fvar"]
diff --git a/Lib/fontTools/ttLib/tables/_c_i_d_g.py b/Lib/fontTools/ttLib/tables/_c_i_d_g.py
index de83d4d6..2517e785 100644
--- a/Lib/fontTools/ttLib/tables/_c_i_d_g.py
+++ b/Lib/fontTools/ttLib/tables/_c_i_d_g.py
@@ -2,17 +2,18 @@
from .otBase import BaseTTXConverter
-# The AAT ‘cidg’ table has almost the same structure as ‘gidc’,
-# just mapping CIDs to GlyphIDs instead of the reverse direction.
-#
-# It is useful for fonts that may be used by a PDF renderer in lieu of
-# a font reference with a known glyph collection but no subsetted
-# glyphs. For instance, a PDF can say “please use a font conforming
-# to Adobe-Japan-1”; the ‘cidg’ mapping is necessary if the font is,
-# say, a TrueType font. ‘gidc’ is lossy for this purpose and is
-# obsoleted by ‘cidg’.
-#
-# For example, the first font in /System/Library/Fonts/PingFang.ttc
-# (which Apple ships pre-installed on MacOS 10.12.6) has a ‘cidg’ table.
class table__c_i_d_g(BaseTTXConverter):
+ """The AAT ``cidg`` table has almost the same structure as ``gidc``,
+just mapping CIDs to GlyphIDs instead of the reverse direction.
+
+It is useful for fonts that may be used by a PDF renderer in lieu of
+a font reference with a known glyph collection but no subsetted
+glyphs. For instance, a PDF can say “please use a font conforming
+to Adobe-Japan-1”; the ``cidg`` mapping is necessary if the font is,
+say, a TrueType font. ``gidc`` is lossy for this purpose and is
+obsoleted by ``cidg``.
+
+For example, the first font in ``/System/Library/Fonts/PingFang.ttc``
+(which Apple ships pre-installed on MacOS 10.12.6) has a ``cidg`` table.
+"""
pass
diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
index a65a0c25..a31b5059 100644
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -1,5 +1,4 @@
-from fontTools.misc.py23 import bytesjoin
-from fontTools.misc.textTools import safeEval, readHex
+from fontTools.misc.textTools import bytesjoin, safeEval, readHex
from fontTools.misc.encodingTools import getEncoding
from fontTools.ttLib import getSearchRange
from fontTools.unicode import Unicode
@@ -15,21 +14,61 @@ log = logging.getLogger(__name__)
def _make_map(font, chars, gids):
assert len(chars) == len(gids)
+ glyphNames = font.getGlyphNameMany(gids)
cmap = {}
- glyphOrder = font.getGlyphOrder()
- for char,gid in zip(chars,gids):
+ for char,gid,name in zip(chars,gids,glyphNames):
if gid == 0:
continue
- try:
- name = glyphOrder[gid]
- except IndexError:
- name = font.getGlyphName(gid)
cmap[char] = name
return cmap
class table__c_m_a_p(DefaultTable.DefaultTable):
+ """Character to Glyph Index Mapping Table
+
+ This class represents the `cmap <https://docs.microsoft.com/en-us/typography/opentype/spec/cmap>`_
+ table, which maps between input characters (in Unicode or other system encodings)
+ and glyphs within the font. The ``cmap`` table contains one or more subtables
+ which determine the mapping of of characters to glyphs across different platforms
+ and encoding systems.
+
+ ``table__c_m_a_p`` objects expose an accessor ``.tables`` which provides access
+ to the subtables, although it is normally easier to retrieve individual subtables
+ through the utility methods described below. To add new subtables to a font,
+ first determine the subtable format (if in doubt use format 4 for glyphs within
+ the BMP, format 12 for glyphs outside the BMP, and format 14 for Unicode Variation
+ Sequences) construct subtable objects with ``CmapSubtable.newSubtable(format)``,
+ and append them to the ``.tables`` list.
+
+ Within a subtable, the mapping of characters to glyphs is provided by the ``.cmap``
+ attribute.
+
+ Example::
+
+ cmap4_0_3 = CmapSubtable.newSubtable(4)
+ cmap4_0_3.platformID = 0
+ cmap4_0_3.platEncID = 3
+ cmap4_0_3.language = 0
+ cmap4_0_3.cmap = { 0xC1: "Aacute" }
+
+ cmap = newTable("cmap")
+ cmap.tableVersion = 0
+ cmap.tables = [cmap4_0_3]
+ """
def getcmap(self, platformID, platEncID):
+ """Returns the first subtable which matches the given platform and encoding.
+
+ Args:
+ platformID (int): The platform ID. Use 0 for Unicode, 1 for Macintosh
+ (deprecated for new fonts), 2 for ISO (deprecated) and 3 for Windows.
+ encodingID (int): Encoding ID. Interpretation depends on the platform ID.
+ See the OpenType specification for details.
+
+ Returns:
+ An object which is a subclass of :py:class:`CmapSubtable` if a matching
+ subtable is found within the font, or ``None`` otherwise.
+ """
+
for subtable in self.tables:
if (subtable.platformID == platformID and
subtable.platEncID == platEncID):
@@ -37,13 +76,22 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
return None # not found
def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))):
- """Return the 'best' unicode cmap dictionary available in the font,
- or None, if no unicode cmap subtable is available.
+ """Returns the 'best' Unicode cmap dictionary available in the font
+ or ``None``, if no Unicode cmap subtable is available.
By default it will search for the following (platformID, platEncID)
- pairs:
- (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0)
- This can be customized via the cmapPreferences argument.
+ pairs in order::
+
+ (3, 10), # Windows Unicode full repertoire
+ (0, 6), # Unicode full repertoire (format 13 subtable)
+ (0, 4), # Unicode 2.0 full repertoire
+ (3, 1), # Windows Unicode BMP
+ (0, 3), # Unicode 2.0 BMP
+ (0, 2), # Unicode ISO/IEC 10646
+ (0, 1), # Unicode 1.1
+ (0, 0) # Unicode 1.0
+
+ This order can be customized via the ``cmapPreferences`` argument.
"""
for platformID, platEncID in cmapPreferences:
cmapSubtable = self.getcmap(platformID, platEncID)
@@ -52,12 +100,20 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
return None # None of the requested cmap subtables were found
def buildReversed(self):
- """Returns a reverse cmap such as {'one':{0x31}, 'A':{0x41,0x391}}.
+ """Builds a reverse mapping dictionary
+
+ Iterates over all Unicode cmap tables and returns a dictionary mapping
+ glyphs to sets of codepoints, such as::
+
+ {
+ 'one': {0x31}
+ 'A': {0x41,0x391}
+ }
The values are sets of Unicode codepoints because
some fonts map different codepoints to the same glyph.
- For example, U+0041 LATIN CAPITAL LETTER A and U+0391
- GREEK CAPITAL LETTER ALPHA are sometimes the same glyph.
+ For example, ``U+0041 LATIN CAPITAL LETTER A`` and ``U+0391
+ GREEK CAPITAL LETTER ALPHA`` are sometimes the same glyph.
"""
result = {}
for subtable in self.tables:
@@ -100,6 +156,12 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
else:
seenOffsets[offset] = i
tables.append(table)
+ if ttFont.lazy is False: # Be lazy for None and True
+ self.ensureDecompiled()
+
+ def ensureDecompiled(self):
+ for st in self.tables:
+ st.ensureDecompiled()
def compile(self, ttFont):
self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__()
@@ -145,6 +207,16 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
class CmapSubtable(object):
+ """Base class for all cmap subtable formats.
+
+ Subclasses which handle the individual subtable formats are named
+ ``cmap_format_0``, ``cmap_format_2`` etc. Use :py:meth:`getSubtableClass`
+ to retrieve the concrete subclass, or :py:meth:`newSubtable` to get a
+ new subtable object for a given format.
+
+ The object exposes a ``.cmap`` attribute, which contains a dictionary mapping
+ character codepoints to glyph names.
+ """
@staticmethod
def getSubtableClass(format):
@@ -153,7 +225,8 @@ class CmapSubtable(object):
@staticmethod
def newSubtable(format):
- """Return a new instance of a subtable for format."""
+ """Return a new instance of a subtable for the given format
+ ."""
subtableClass = CmapSubtable.getSubtableClass(format)
return subtableClass(format)
@@ -161,6 +234,17 @@ class CmapSubtable(object):
self.format = format
self.data = None
self.ttFont = None
+ self.platformID = None #: The platform ID of this subtable
+ self.platEncID = None #: The encoding ID of this subtable (interpretation depends on ``platformID``)
+ self.language = None #: The language ID of this subtable (Macintosh platform only)
+
+ def ensureDecompiled(self):
+ if self.data is None:
+ return
+ self.decompile(None, None) # use saved data.
+ self.data = None # Once this table has been decompiled, make sure we don't
+ # just return the original data. Also avoids recursion when
+ # called with an attribute that the cmap subtable doesn't have.
def __getattr__(self, attr):
# allow lazy decompilation of subtables.
@@ -168,10 +252,7 @@ class CmapSubtable(object):
raise AttributeError(attr)
if self.data is None:
raise AttributeError(attr)
- self.decompile(None, None) # use saved data.
- self.data = None # Once this table has been decompiled, make sure we don't
- # just return the original data. Also avoids recursion when
- # called with an attribute that the cmap subtable doesn't have.
+ self.ensureDecompiled()
return getattr(self, attr)
def decompileHeader(self, data, ttFont):
@@ -198,20 +279,22 @@ class CmapSubtable(object):
def getEncoding(self, default=None):
"""Returns the Python encoding name for this cmap subtable based on its platformID,
platEncID, and language. If encoding for these values is not known, by default
- None is returned. That can be overriden by passing a value to the default
+ ``None`` is returned. That can be overridden by passing a value to the ``default``
argument.
Note that if you want to choose a "preferred" cmap subtable, most of the time
- self.isUnicode() is what you want as that one only returns true for the modern,
+ ``self.isUnicode()`` is what you want as that one only returns true for the modern,
commonly used, Unicode-compatible triplets, not the legacy ones.
"""
return getEncoding(self.platformID, self.platEncID, self.language, default)
def isUnicode(self):
+ """Returns true if the characters are interpreted as Unicode codepoints."""
return (self.platformID == 0 or
(self.platformID == 3 and self.platEncID in [0, 1, 10]))
def isSymbol(self):
+ """Returns true if the subtable is for the Symbol encoding (3,0)"""
return self.platformID == 3 and self.platEncID == 0
def _writeCodes(self, codes, writer):
diff --git a/Lib/fontTools/ttLib/tables/_c_v_a_r.py b/Lib/fontTools/ttLib/tables/_c_v_a_r.py
index 09b2c16c..a67efe02 100644
--- a/Lib/fontTools/ttLib/tables/_c_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_c_v_a_r.py
@@ -1,6 +1,6 @@
-from fontTools.misc.py23 import bytesjoin
from . import DefaultTable
from fontTools.misc import sstruct
+from fontTools.misc.textTools import bytesjoin
from fontTools.ttLib.tables.TupleVariation import \
compileTupleVariationStore, decompileTupleVariationStore, TupleVariation
@@ -41,7 +41,7 @@ class table__c_v_a_r(DefaultTable.DefaultTable):
"tupleVariationCount": tupleVariationCount,
"offsetToData": CVAR_HEADER_SIZE + len(tuples),
}
- return bytesjoin([
+ return b''.join([
sstruct.pack(CVAR_HEADER_FORMAT, header),
tuples,
data
diff --git a/Lib/fontTools/ttLib/tables/_f_e_a_t.py b/Lib/fontTools/ttLib/tables/_f_e_a_t.py
index eb03f8ba..079b514c 100644
--- a/Lib/fontTools/ttLib/tables/_f_e_a_t.py
+++ b/Lib/fontTools/ttLib/tables/_f_e_a_t.py
@@ -2,4 +2,10 @@ from .otBase import BaseTTXConverter
class table__f_e_a_t(BaseTTXConverter):
+ """The feature name table is an AAT (Apple Advanced Typography) table for
+ storing font features, settings, and their human-readable names. It should
+ not be confused with the ``Feat`` table or the OpenType Layout ``GSUB``/``GPOS``
+ tables. See `Feature Name Table <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6feat.html>`_
+ in the TrueType Reference Manual for more information on the structure and
+ purpose of this table."""
pass
diff --git a/Lib/fontTools/ttLib/tables/_f_v_a_r.py b/Lib/fontTools/ttLib/tables/_f_v_a_r.py
index 7487da62..d7409195 100644
--- a/Lib/fontTools/ttLib/tables/_f_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_f_v_a_r.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import Tag, bytesjoin
from fontTools.misc import sstruct
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
@@ -6,7 +5,7 @@ from fontTools.misc.fixedTools import (
floatToFixedToStr as fl2str,
strToFixedToFloat as str2fl,
)
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import Tag, bytesjoin, safeEval
from fontTools.ttLib import TTLibError
from . import DefaultTable
import struct
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
index 4680ddbf..14c4519d 100644
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -1,12 +1,11 @@
"""_g_l_y_f.py -- Converter classes for the 'glyf' table."""
from collections import namedtuple
-from fontTools.misc.py23 import bytechr, byteord, bytesjoin, tostr
from fontTools.misc import sstruct
from fontTools import ttLib
from fontTools import version
-from fontTools.misc.textTools import safeEval, pad
-from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect
+from fontTools.misc.textTools import tostr, safeEval, pad
+from fontTools.misc.arrayTools import calcIntBounds, pointInRect
from fontTools.misc.bezierTools import calcQuadraticBounds
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
@@ -25,6 +24,7 @@ import logging
import os
from fontTools.misc import xmlWriter
from fontTools.misc.filenames import userNameToFileName
+from fontTools.misc.loggingTools import deprecateFunction
log = logging.getLogger(__name__)
@@ -47,6 +47,35 @@ SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple
class table__g_l_y_f(DefaultTable.DefaultTable):
+ """Glyph Data Table
+
+ This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_
+ table, which contains outlines for glyphs in TrueType format. In many cases,
+ it is easier to access and manipulate glyph outlines through the ``GlyphSet``
+ object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`::
+
+ >> from fontTools.pens.boundsPen import BoundsPen
+ >> glyphset = font.getGlyphSet()
+ >> bp = BoundsPen(glyphset)
+ >> glyphset["A"].draw(bp)
+ >> bp.bounds
+ (19, 0, 633, 716)
+
+ However, this class can be used for low-level access to the ``glyf`` table data.
+ Objects of this class support dictionary-like access, mapping glyph names to
+ :py:class:`Glyph` objects::
+
+ >> glyf = font["glyf"]
+ >> len(glyf["Aacute"].components)
+ 2
+
+ Note that when adding glyphs to the font via low-level access to the ``glyf``
+ table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table::
+
+ >> font["glyf"]["divisionslash"] = Glyph()
+ >> font["hmtx"]["divisionslash"] = (640, 0)
+
+ """
# this attribute controls the amount of padding applied to glyph data upon compile.
# Glyph lenghts are aligned to multiples of the specified value.
@@ -81,8 +110,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
if noname:
log.warning('%s glyphs have no name', noname)
if ttFont.lazy is False: # Be lazy for None and True
- for glyph in self.glyphs.values():
- glyph.expand(self)
+ self.ensureDecompiled()
+
+ def ensureDecompiled(self):
+ for glyph in self.glyphs.values():
+ glyph.expand(self)
def compile(self, ttFont):
if not hasattr(self, "glyphOrder"):
@@ -117,7 +149,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
currentLocation += len(glyphData)
locations[len(dataList)] = currentLocation
- data = bytesjoin(dataList)
+ data = b''.join(dataList)
if 'loca' in ttFont:
ttFont['loca'].set(locations)
if 'maxp' in ttFont:
@@ -145,10 +177,10 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
path, ext = os.path.splitext(writer.file.name)
existingGlyphFiles = set()
for glyphName in glyphNames:
- if glyphName not in self:
+ glyph = self.get(glyphName)
+ if glyph is None:
log.warning("glyph '%s' does not exist in glyf table", glyphName)
continue
- glyph = self[glyphName]
if glyph.numberOfContours:
if splitGlyphs:
glyphPath = userNameToFileName(
@@ -215,16 +247,33 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
glyph.compact(self, 0)
def setGlyphOrder(self, glyphOrder):
+ """Sets the glyph order
+
+ Args:
+ glyphOrder ([str]): List of glyph names in order.
+ """
self.glyphOrder = glyphOrder
def getGlyphName(self, glyphID):
+ """Returns the name for the glyph with the given ID.
+
+ Raises a ``KeyError`` if the glyph name is not found in the font.
+ """
return self.glyphOrder[glyphID]
def getGlyphID(self, glyphName):
+ """Returns the ID of the glyph with the given name.
+
+ Raises a ``ValueError`` if the glyph is not found in the font.
+ """
# XXX optimize with reverse dict!!!
return self.glyphOrder.index(glyphName)
def removeHinting(self):
+ """Removes TrueType hints from all glyphs in the glyphset.
+
+ See :py:meth:`Glyph.removeHinting`.
+ """
for glyph in self.glyphs.values():
glyph.removeHinting()
@@ -236,6 +285,12 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
__contains__ = has_key
+ def get(self, glyphName, default=None):
+ glyph = self.glyphs.get(glyphName, default)
+ if glyph is not None:
+ glyph.expand(self)
+ return glyph
+
def __getitem__(self, glyphName):
glyph = self.glyphs[glyphName]
glyph.expand(self)
@@ -254,49 +309,33 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
assert len(self.glyphOrder) == len(self.glyphs)
return len(self.glyphs)
- def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None):
+ def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None):
"""Compute the four "phantom points" for the given glyph from its bounding box
and the horizontal and vertical advance widths and sidebearings stored in the
ttFont's "hmtx" and "vmtx" tables.
- If the ttFont doesn't contain a "vmtx" table, the hhea.ascent is used as the
- vertical origin, and the head.unitsPerEm as the vertical advance.
+ 'hMetrics' should be ttFont['hmtx'].metrics.
- The "defaultVerticalOrigin" (Optional[int]) is needed when the ttFont contains
- neither a "vmtx" nor an "hhea" table, as may happen with 'sparse' masters.
- The value should be the hhea.ascent of the default master.
+ 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise.
+ If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate.
https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms
"""
glyph = self[glyphName]
- assert glyphName in ttFont["hmtx"].metrics, ttFont["hmtx"].metrics
- horizontalAdvanceWidth, leftSideBearing = ttFont["hmtx"].metrics[glyphName]
if not hasattr(glyph, 'xMin'):
glyph.recalcBounds(self)
+
+ horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName]
leftSideX = glyph.xMin - leftSideBearing
rightSideX = leftSideX + horizontalAdvanceWidth
- if "vmtx" in ttFont:
- verticalAdvanceWidth, topSideBearing = ttFont["vmtx"].metrics[glyphName]
+
+ if vMetrics:
+ verticalAdvanceWidth, topSideBearing = vMetrics[glyphName]
topSideY = topSideBearing + glyph.yMax
+ bottomSideY = topSideY - verticalAdvanceWidth
else:
- # without vmtx, use ascent as vertical origin and UPEM as vertical advance
- # like HarfBuzz does
- verticalAdvanceWidth = ttFont["head"].unitsPerEm
- if "hhea" in ttFont:
- topSideY = ttFont["hhea"].ascent
- else:
- # sparse masters may not contain an hhea table; use the ascent
- # of the default master as the vertical origin
- if defaultVerticalOrigin is not None:
- topSideY = defaultVerticalOrigin
- else:
- log.warning(
- "font is missing both 'vmtx' and 'hhea' tables, "
- "and no 'defaultVerticalOrigin' was provided; "
- "the vertical phantom points may be incorrect."
- )
- topSideY = verticalAdvanceWidth
- bottomSideY = topSideY - verticalAdvanceWidth
+ bottomSideY = topSideY = 0
+
return [
(leftSideX, 0),
(rightSideX, 0),
@@ -304,7 +343,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
(0, bottomSideY),
]
- def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None):
+ def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None):
"""Return glyph coordinates and controls as expected by "gvar" table.
The coordinates includes four "phantom points" for the glyph metrics,
@@ -320,14 +359,14 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
- components: list of base glyph names (str) for each component in
composite glyphs (None for simple glyphs).
- The "ttFont" and "defaultVerticalOrigin" args are used to compute the
- "phantom points" (see "getPhantomPoints" method).
+ The "hMetrics" and vMetrics are used to compute the "phantom points" (see
+ the "_getPhantomPoints" method).
Return None if the requested glyphName is not present.
"""
- if glyphName not in self.glyphs:
+ glyph = self.get(glyphName)
+ if glyph is None:
return None
- glyph = self[glyphName]
if glyph.isComposite():
coords = GlyphCoordinates(
[(getattr(c, 'x', 0), getattr(c, 'y', 0)) for c in glyph.components]
@@ -348,13 +387,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
components=None,
)
# Add phantom points for (left, right, top, bottom) positions.
- phantomPoints = self.getPhantomPoints(
- glyphName, ttFont, defaultVerticalOrigin=defaultVerticalOrigin
- )
+ phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics)
coords.extend(phantomPoints)
return coords, controls
- def setCoordinates(self, glyphName, coord, ttFont):
+ def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None):
"""Set coordinates and metrics for the given glyph.
"coord" is an array of GlyphCoordinates which must include the "phantom
@@ -363,9 +400,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
Both the horizontal/vertical advances and left/top sidebearings in "hmtx"
and "vmtx" tables (if any) are updated from four phantom points and
the glyph's bounding boxes.
+
+ The "hMetrics" and vMetrics are used to propagate "phantom points"
+ into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints"
+ method).
"""
- # TODO: Create new glyph if not already present
- assert glyphName in self.glyphs
glyph = self[glyphName]
# Handle phantom points for (left, right, top, bottom) positions.
@@ -396,14 +435,61 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
# https://github.com/fonttools/fonttools/pull/1198
horizontalAdvanceWidth = 0
leftSideBearing = otRound(glyph.xMin - leftSideX)
- ttFont["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
+ hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
- if "vmtx" in ttFont:
+ if vMetrics is not None:
verticalAdvanceWidth = otRound(topSideY - bottomSideY)
if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal
verticalAdvanceWidth = 0
topSideBearing = otRound(topSideY - glyph.yMax)
- ttFont["vmtx"].metrics[glyphName] = verticalAdvanceWidth, topSideBearing
+ vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing
+
+
+ # Deprecated
+
+ def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin):
+ """This method is wrong and deprecated.
+ For rationale see:
+ https://github.com/fonttools/fonttools/pull/2266/files#r613569473
+ """
+ vMetrics = getattr(ttFont.get('vmtx'), 'metrics', None)
+ if vMetrics is None:
+ verticalAdvanceWidth = ttFont["head"].unitsPerEm
+ topSideY = getattr(ttFont.get('hhea'), 'ascent', None)
+ if topSideY is None:
+ if defaultVerticalOrigin is not None:
+ topSideY = defaultVerticalOrigin
+ else:
+ topSideY = verticalAdvanceWidth
+ glyph = self[glyphName]
+ glyph.recalcBounds(self)
+ topSideBearing = otRound(topSideY - glyph.yMax)
+ vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)}
+ return vMetrics
+
+ @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning)
+ def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None):
+ """Old public name for self._getPhantomPoints().
+ See: https://github.com/fonttools/fonttools/pull/2266"""
+ hMetrics = ttFont['hmtx'].metrics
+ vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
+ return self._getPhantomPoints(glyphName, hMetrics, vMetrics)
+
+ @deprecateFunction("use '_getCoordinatesAndControls' instead", category=DeprecationWarning)
+ def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None):
+ """Old public name for self._getCoordinatesAndControls().
+ See: https://github.com/fonttools/fonttools/pull/2266"""
+ hMetrics = ttFont['hmtx'].metrics
+ vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
+ return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics)
+
+ @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning)
+ def setCoordinates(self, glyphName, ttFont):
+ """Old public name for self._setCoordinates().
+ See: https://github.com/fonttools/fonttools/pull/2266"""
+ hMetrics = ttFont['hmtx'].metrics
+ vMetrics = getattr(ttFont.get('vmtx'), 'metrics', None)
+ self._setCoordinates(glyphName, hMetrics, vMetrics)
_GlyphControls = namedtuple(
@@ -488,8 +574,7 @@ def flagEncodeCoord(flag, mask, coord, coordBytes):
elif byteCount == -1:
coordBytes.append(-coord)
elif byteCount == 2:
- coordBytes.append((coord >> 8) & 0xFF)
- coordBytes.append(coord & 0xFF)
+ coordBytes.extend(struct.pack('>h', coord))
def flagEncodeCoords(flag, x, y, xBytes, yBytes):
flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes)
@@ -515,8 +600,29 @@ CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours',
class Glyph(object):
+ """This class represents an individual TrueType glyph.
+
+ TrueType glyph objects come in two flavours: simple and composite. Simple
+ glyph objects contain contours, represented via the ``.coordinates``,
+ ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes;
+ composite glyphs contain components, available through the ``.components``
+ attributes.
+
+ Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned
+ above) is only set on simple glyphs and the ``.components`` attribute is only
+ set on composite glyphs, it is necessary to use the :py:meth:`isComposite`
+ method to test whether a glyph is simple or composite before attempting to
+ access its data.
+
+ For a composite glyph, the components can also be accessed via array-like access::
- def __init__(self, data=""):
+ >> assert(font["glyf"]["Aacute"].isComposite())
+ >> font["glyf"]["Aacute"][0]
+ <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0>
+
+ """
+
+ def __init__(self, data=b""):
if not data:
# empty char
self.numberOfContours = 0
@@ -557,7 +663,7 @@ class Glyph(object):
else:
return self.data
if self.numberOfContours == 0:
- return ""
+ return b''
if recalcBBoxes:
self.recalcBounds(glyfTable)
data = sstruct.pack(glyphHeaderFormat, self)
@@ -608,7 +714,7 @@ class Glyph(object):
raise ttLib.TTLibError("can't mix composites and contours in glyph")
self.numberOfContours = self.numberOfContours + 1
coordinates = GlyphCoordinates()
- flags = []
+ flags = bytearray()
for element in content:
if not isinstance(element, tuple):
continue
@@ -616,11 +722,10 @@ class Glyph(object):
if name != "pt":
continue # ignore anything but "pt"
coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
- flag = not not safeEval(attrs["on"])
+ flag = bool(safeEval(attrs["on"]))
if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
flag |= flagOverlapSimple
flags.append(flag)
- flags = array.array("B", flags)
if not hasattr(self, "coordinates"):
self.coordinates = coordinates
self.flags = flags
@@ -695,16 +800,14 @@ class Glyph(object):
if sys.byteorder != "big": endPtsOfContours.byteswap()
self.endPtsOfContours = endPtsOfContours.tolist()
- data = data[2*self.numberOfContours:]
-
- instructionLength, = struct.unpack(">h", data[:2])
- data = data[2:]
+ pos = 2*self.numberOfContours
+ instructionLength, = struct.unpack(">h", data[pos:pos+2])
self.program = ttProgram.Program()
- self.program.fromBytecode(data[:instructionLength])
- data = data[instructionLength:]
+ self.program.fromBytecode(data[pos+2:pos+2+instructionLength])
+ pos += 2 + instructionLength
nCoordinates = self.endPtsOfContours[-1] + 1
flags, xCoordinates, yCoordinates = \
- self.decompileCoordinatesRaw(nCoordinates, data)
+ self.decompileCoordinatesRaw(nCoordinates, data, pos)
# fill in repetitions and apply signs
self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates)
@@ -741,24 +844,26 @@ class Glyph(object):
assert yIndex == len(yCoordinates)
coordinates.relativeToAbsolute()
# discard all flags except "keepFlags"
- self.flags = array.array("B", (f & keepFlags for f in flags))
+ for i in range(len(flags)):
+ flags[i] &= keepFlags
+ self.flags = flags
- def decompileCoordinatesRaw(self, nCoordinates, data):
+ def decompileCoordinatesRaw(self, nCoordinates, data, pos=0):
# unpack flags and prepare unpacking of coordinates
- flags = array.array("B", [0] * nCoordinates)
+ flags = bytearray(nCoordinates)
# Warning: deep Python trickery going on. We use the struct module to unpack
# the coordinates. We build a format string based on the flags, so we can
# unpack the coordinates in one struct.unpack() call.
xFormat = ">" # big endian
yFormat = ">" # big endian
- i = j = 0
+ j = 0
while True:
- flag = byteord(data[i])
- i = i + 1
+ flag = data[pos]
+ pos += 1
repeat = 1
if flag & flagRepeat:
- repeat = byteord(data[i]) + 1
- i = i + 1
+ repeat = data[pos] + 1
+ pos += 1
for k in range(repeat):
if flag & flagXShort:
xFormat = xFormat + 'B'
@@ -773,15 +878,14 @@ class Glyph(object):
if j >= nCoordinates:
break
assert j == nCoordinates, "bad glyph flags"
- data = data[i:]
# unpack raw coordinates, krrrrrr-tching!
xDataLen = struct.calcsize(xFormat)
yDataLen = struct.calcsize(yFormat)
- if len(data) - (xDataLen + yDataLen) >= 4:
+ if len(data) - pos - (xDataLen + yDataLen) >= 4:
log.warning(
- "too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen))
- xCoordinates = struct.unpack(xFormat, data[:xDataLen])
- yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen])
+ "too much glyph data: %d excess bytes", len(data) - pos - (xDataLen + yDataLen))
+ xCoordinates = struct.unpack(xFormat, data[pos:pos+xDataLen])
+ yCoordinates = struct.unpack(yFormat, data[pos+xDataLen:pos+xDataLen+yDataLen])
return flags, xCoordinates, yCoordinates
def compileComponents(self, glyfTable):
@@ -811,9 +915,7 @@ class Glyph(object):
data.append(instructions)
deltas = self.coordinates.copy()
- if deltas.isFloat():
- # Warn?
- deltas.toInt()
+ deltas.toInt()
deltas.absoluteToRelative()
# TODO(behdad): Add a configuration option for this?
@@ -821,14 +923,14 @@ class Glyph(object):
#deltas = self.compileDeltasOptimal(self.flags, deltas)
data.extend(deltas)
- return bytesjoin(data)
+ return b''.join(data)
def compileDeltasGreedy(self, flags, deltas):
# Implements greedy algorithm for packing coordinate deltas:
# uses shortest representation one coordinate at a time.
- compressedflags = []
- xPoints = []
- yPoints = []
+ compressedFlags = bytearray()
+ compressedXs = bytearray()
+ compressedYs = bytearray()
lastflag = None
repeat = 0
for flag,(x,y) in zip(flags, deltas):
@@ -842,9 +944,9 @@ class Glyph(object):
flag = flag | flagXsame
else:
x = -x
- xPoints.append(bytechr(x))
+ compressedXs.append(x)
else:
- xPoints.append(struct.pack(">h", x))
+ compressedXs.extend(struct.pack('>h', x))
# do y
if y == 0:
flag = flag | flagYsame
@@ -854,24 +956,21 @@ class Glyph(object):
flag = flag | flagYsame
else:
y = -y
- yPoints.append(bytechr(y))
+ compressedYs.append(y)
else:
- yPoints.append(struct.pack(">h", y))
+ compressedYs.extend(struct.pack('>h', y))
# handle repeating flags
if flag == lastflag and repeat != 255:
repeat = repeat + 1
if repeat == 1:
- compressedflags.append(flag)
+ compressedFlags.append(flag)
else:
- compressedflags[-2] = flag | flagRepeat
- compressedflags[-1] = repeat
+ compressedFlags[-2] = flag | flagRepeat
+ compressedFlags[-1] = repeat
else:
repeat = 0
- compressedflags.append(flag)
+ compressedFlags.append(flag)
lastflag = flag
- compressedFlags = array.array("B", compressedflags).tobytes()
- compressedXs = bytesjoin(xPoints)
- compressedYs = bytesjoin(yPoints)
return (compressedFlags, compressedXs, compressedYs)
def compileDeltasOptimal(self, flags, deltas):
@@ -902,9 +1001,9 @@ class Glyph(object):
flags.append(flag)
flags.reverse()
- compressedFlags = array.array("B")
- compressedXs = array.array("B")
- compressedYs = array.array("B")
+ compressedFlags = bytearray()
+ compressedXs = bytearray()
+ compressedYs = bytearray()
coords = iter(deltas)
ff = []
for flag in flags:
@@ -924,72 +1023,22 @@ class Glyph(object):
raise Exception("internal error")
except StopIteration:
pass
- compressedFlags = compressedFlags.tobytes()
- compressedXs = compressedXs.tobytes()
- compressedYs = compressedYs.tobytes()
return (compressedFlags, compressedXs, compressedYs)
def recalcBounds(self, glyfTable):
+ """Recalculates the bounds of the glyph.
+
+ Each glyph object stores its bounding box in the
+ ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
+ recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
+ must be provided to resolve component bounds.
+ """
coords, endPts, flags = self.getCoordinates(glyfTable)
- if len(coords) > 0:
- if 0:
- # This branch calculates exact glyph outline bounds
- # analytically, handling cases without on-curve
- # extremas, etc. However, the glyf table header
- # simply says that the bounds should be min/max x/y
- # "for coordinate data", so I suppose that means no
- # fancy thing here, just get extremas of all coord
- # points (on and off). As such, this branch is
- # disabled.
-
- # Collect on-curve points
- onCurveCoords = [coords[j] for j in range(len(coords))
- if flags[j] & flagOnCurve]
- # Add implicit on-curve points
- start = 0
- for end in endPts:
- last = end
- for j in range(start, end + 1):
- if not ((flags[j] | flags[last]) & flagOnCurve):
- x = (coords[last][0] + coords[j][0]) / 2
- y = (coords[last][1] + coords[j][1]) / 2
- onCurveCoords.append((x,y))
- last = j
- start = end + 1
- # Add bounds for curves without an explicit extrema
- start = 0
- for end in endPts:
- last = end
- for j in range(start, end + 1):
- if not (flags[j] & flagOnCurve):
- next = j + 1 if j < end else start
- bbox = calcBounds([coords[last], coords[next]])
- if not pointInRect(coords[j], bbox):
- # Ouch!
- log.warning("Outline has curve with implicit extrema.")
- # Ouch! Find analytical curve bounds.
- pthis = coords[j]
- plast = coords[last]
- if not (flags[last] & flagOnCurve):
- plast = ((pthis[0]+plast[0])/2, (pthis[1]+plast[1])/2)
- pnext = coords[next]
- if not (flags[next] & flagOnCurve):
- pnext = ((pthis[0]+pnext[0])/2, (pthis[1]+pnext[1])/2)
- bbox = calcQuadraticBounds(plast, pthis, pnext)
- onCurveCoords.append((bbox[0],bbox[1]))
- onCurveCoords.append((bbox[2],bbox[3]))
- last = j
- start = end + 1
-
- self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(onCurveCoords)
- else:
- self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
- else:
- self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0)
+ self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
def isComposite(self):
- """Can be called on compact or expanded glyph."""
+ """Test whether a glyph has components"""
if hasattr(self, "data") and self.data:
return struct.unpack(">h", self.data[:2])[0] == -1
else:
@@ -1001,12 +1050,27 @@ class Glyph(object):
return self.components[componentIndex]
def getCoordinates(self, glyfTable):
+ """Return the coordinates, end points and flags
+
+ This method returns three values: A :py:class:`GlyphCoordinates` object,
+ a list of the indexes of the final points of each contour (allowing you
+ to split up the coordinates list into contours) and a list of flags.
+
+ On simple glyphs, this method returns information from the glyph's own
+ contours; on composite glyphs, it "flattens" all components recursively
+ to return a list of coordinates representing all the components involved
+ in the glyph.
+
+ To interpret the flags for each point, see the "Simple Glyph Flags"
+ section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`.
+ """
+
if self.numberOfContours > 0:
return self.coordinates, self.endPtsOfContours, self.flags
elif self.isComposite():
# it's a composite
allCoords = GlyphCoordinates()
- allFlags = array.array("B")
+ allFlags = bytearray()
allEndPts = []
for compo in self.components:
g = glyfTable[compo.glyphName]
@@ -1051,9 +1115,14 @@ class Glyph(object):
allFlags.extend(flags)
return allCoords, allEndPts, allFlags
else:
- return GlyphCoordinates(), [], array.array("B")
+ return GlyphCoordinates(), [], bytearray()
def getComponentNames(self, glyfTable):
+ """Returns a list of names of component glyphs used in this glyph
+
+ This method can be used on simple glyphs (in which case it returns an
+ empty list) or composite glyphs.
+ """
if not hasattr(self, "data"):
if self.isComposite():
return [c.glyphName for c in self.components]
@@ -1101,7 +1170,7 @@ class Glyph(object):
if not self.data:
return
numContours = struct.unpack(">h", self.data[:2])[0]
- data = array.array("B", self.data)
+ data = bytearray(self.data)
i = 10
if numContours >= 0:
i += 2 * numContours # endPtsOfContours
@@ -1170,12 +1239,21 @@ class Glyph(object):
# Remove padding
data = data[:i]
- self.data = data.tobytes()
+ self.data = data
def removeHinting(self):
+ """Removes TrueType hinting instructions from the glyph."""
self.trim (remove_hinting=True)
def draw(self, pen, glyfTable, offset=0):
+ """Draws the glyph using the supplied pen object.
+
+ Arguments:
+ pen: An object conforming to the pen protocol.
+ glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components.
+ offset (int): A horizontal offset. If provided, all coordinates are
+ translated by this offset.
+ """
if self.isComposite():
for component in self.components:
@@ -1221,7 +1299,7 @@ class Glyph(object):
pen.closePath()
def drawPoints(self, pen, glyfTable, offset=0):
- """Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(),
+ """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(),
this will not change the point indices.
"""
@@ -1263,12 +1341,29 @@ class Glyph(object):
return result if result is NotImplemented else not result
class GlyphComponent(object):
+ """Represents a component within a composite glyph.
+
+ The component is represented internally with four attributes: ``glyphName``,
+ ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e
+ no scaling, reflection, or rotation; only translation), the ``transform``
+ attribute is not present.
+ """
+ # The above documentation is not *completely* true, but is *true enough* because
+ # the rare firstPt/lastPt attributes are not totally supported and nobody seems to
+ # mind - see below.
def __init__(self):
pass
def getComponentInfo(self):
- """Return the base glyph name and a transform."""
+ """Return information about the component
+
+ This method returns a tuple of two values: the glyph name of the component's
+ base glyph, and a transformation matrix. As opposed to accessing the attributes
+ directly, ``getComponentInfo`` always returns a six-element tuple of the
+ component's transformation matrix, even when the two-by-two ``.transform``
+ matrix is not present.
+ """
# XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
# something equivalent in fontTools.objects.glyph (I'd rather not
# convert it to an absolute offset, since it is valuable information).
@@ -1431,65 +1526,60 @@ class GlyphComponent(object):
return result if result is NotImplemented else not result
class GlyphCoordinates(object):
+ """A list of glyph coordinates.
- def __init__(self, iterable=[], typecode="h"):
- self._a = array.array(typecode)
+ Unlike an ordinary list, this is a numpy-like matrix object which supports
+ matrix addition, scalar multiplication and other operations described below.
+ """
+ def __init__(self, iterable=[]):
+ self._a = array.array('d')
self.extend(iterable)
@property
def array(self):
+ """Returns the underlying array of coordinates"""
return self._a
- def isFloat(self):
- return self._a.typecode == 'd'
-
- def _ensureFloat(self):
- if self.isFloat():
- return
- # The conversion to list() is to work around Jython bug
- self._a = array.array("d", list(self._a))
-
- def _checkFloat(self, p):
- if self.isFloat():
- return p
- if any(v > 0x7FFF or v < -0x8000 for v in p):
- self._ensureFloat()
- return p
- if any(isinstance(v, float) for v in p):
- p = [int(v) if int(v) == v else v for v in p]
- if any(isinstance(v, float) for v in p):
- self._ensureFloat()
- return p
-
@staticmethod
def zeros(count):
- return GlyphCoordinates([(0,0)] * count)
+ """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)"""
+ g = GlyphCoordinates()
+ g._a.frombytes(bytes(count * 2 * g._a.itemsize))
+ return g
def copy(self):
- c = GlyphCoordinates(typecode=self._a.typecode)
+ """Creates a new ``GlyphCoordinates`` object which is a copy of the current one."""
+ c = GlyphCoordinates()
c._a.extend(self._a)
return c
def __len__(self):
+ """Returns the number of coordinates in the array."""
return len(self._a) // 2
def __getitem__(self, k):
+ """Returns a two element tuple (x,y)"""
if isinstance(k, slice):
indices = range(*k.indices(len(self)))
return [self[i] for i in indices]
- return self._a[2*k],self._a[2*k+1]
+ a = self._a
+ x = a[2*k]
+ y = a[2*k+1]
+ return (int(x) if x.is_integer() else x,
+ int(y) if y.is_integer() else y)
def __setitem__(self, k, v):
+ """Sets a point's coordinates to a two element tuple (x,y)"""
if isinstance(k, slice):
indices = range(*k.indices(len(self)))
# XXX This only works if len(v) == len(indices)
for j,i in enumerate(indices):
self[i] = v[j]
return
- v = self._checkFloat(v)
self._a[2*k],self._a[2*k+1] = v
def __delitem__(self, i):
+ """Removes a point from the list"""
i = (2*i) % len(self._a)
del self._a[i]
del self._a[i]
@@ -1498,69 +1588,71 @@ class GlyphCoordinates(object):
return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])'
def append(self, p):
- p = self._checkFloat(p)
self._a.extend(tuple(p))
def extend(self, iterable):
for p in iterable:
- p = self._checkFloat(p)
self._a.extend(p)
def toInt(self, *, round=otRound):
- if not self.isFloat():
- return
- a = array.array("h")
- for n in self._a:
- a.append(round(n))
- self._a = a
+ a = self._a
+ for i in range(len(a)):
+ a[i] = round(a[i])
def relativeToAbsolute(self):
a = self._a
x,y = 0,0
- for i in range(len(a) // 2):
- x = a[2*i ] + x
- y = a[2*i+1] + y
- self[i] = (x, y)
+ for i in range(0, len(a), 2):
+ a[i ] = x = a[i ] + x
+ a[i+1] = y = a[i+1] + y
def absoluteToRelative(self):
a = self._a
x,y = 0,0
- for i in range(len(a) // 2):
- dx = a[2*i ] - x
- dy = a[2*i+1] - y
- x = a[2*i ]
- y = a[2*i+1]
- self[i] = (dx, dy)
+ for i in range(0, len(a), 2):
+ nx = a[i ]
+ ny = a[i+1]
+ a[i] = nx - x
+ a[i+1] = ny - y
+ x = nx
+ y = ny
def translate(self, p):
"""
>>> GlyphCoordinates([(1,2)]).translate((.5,0))
"""
- (x,y) = self._checkFloat(p)
+ x,y = p
+ if x == 0 and y == 0:
+ return
a = self._a
- for i in range(len(a) // 2):
- self[i] = (a[2*i] + x, a[2*i+1] + y)
+ for i in range(0, len(a), 2):
+ a[i] += x
+ a[i+1] += y
def scale(self, p):
"""
>>> GlyphCoordinates([(1,2)]).scale((.5,0))
"""
- (x,y) = self._checkFloat(p)
+ x,y = p
+ if x == 1 and y == 1:
+ return
a = self._a
- for i in range(len(a) // 2):
- self[i] = (a[2*i] * x, a[2*i+1] * y)
+ for i in range(0, len(a), 2):
+ a[i] *= x
+ a[i+1] *= y
def transform(self, t):
"""
>>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
"""
a = self._a
- for i in range(len(a) // 2):
- x = a[2*i ]
- y = a[2*i+1]
+ for i in range(0, len(a), 2):
+ x = a[i ]
+ y = a[i+1]
px = x * t[0][0] + y * t[1][0]
py = x * t[0][1] + y * t[1][1]
- self[i] = (px, py)
+ a[i] = px
+ a[i+1] = py
def __eq__(self, other):
"""
@@ -1645,23 +1737,22 @@ class GlyphCoordinates(object):
>>> g = GlyphCoordinates([(1,2)])
>>> g += (.5,0)
>>> g
- GlyphCoordinates([(1.5, 2.0)])
+ GlyphCoordinates([(1.5, 2)])
>>> g2 = GlyphCoordinates([(3,4)])
>>> g += g2
>>> g
- GlyphCoordinates([(4.5, 6.0)])
+ GlyphCoordinates([(4.5, 6)])
"""
if isinstance(other, tuple):
assert len(other) == 2
self.translate(other)
return self
if isinstance(other, GlyphCoordinates):
- if other.isFloat(): self._ensureFloat()
other = other._a
a = self._a
assert len(a) == len(other)
- for i in range(len(a) // 2):
- self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1])
+ for i in range(len(a)):
+ a[i] += other[i]
return self
return NotImplemented
@@ -1670,23 +1761,22 @@ class GlyphCoordinates(object):
>>> g = GlyphCoordinates([(1,2)])
>>> g -= (.5,0)
>>> g
- GlyphCoordinates([(0.5, 2.0)])
+ GlyphCoordinates([(0.5, 2)])
>>> g2 = GlyphCoordinates([(3,4)])
>>> g -= g2
>>> g
- GlyphCoordinates([(-2.5, -2.0)])
+ GlyphCoordinates([(-2.5, -2)])
"""
if isinstance(other, tuple):
assert len(other) == 2
self.translate((-other[0],-other[1]))
return self
if isinstance(other, GlyphCoordinates):
- if other.isFloat(): self._ensureFloat()
other = other._a
a = self._a
assert len(a) == len(other)
- for i in range(len(a) // 2):
- self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1])
+ for i in range(len(a)):
+ a[i] -= other[i]
return self
return NotImplemented
@@ -1696,20 +1786,23 @@ class GlyphCoordinates(object):
>>> g *= (2,.5)
>>> g *= 2
>>> g
- GlyphCoordinates([(4.0, 2.0)])
+ GlyphCoordinates([(4, 2)])
>>> g = GlyphCoordinates([(1,2)])
>>> g *= 2
>>> g
GlyphCoordinates([(2, 4)])
"""
- if isinstance(other, Number):
- other = (other, other)
if isinstance(other, tuple):
- if other == (1,1):
- return self
assert len(other) == 2
self.scale(other)
return self
+ if isinstance(other, Number):
+ if other == 1:
+ return self
+ a = self._a
+ for i in range(len(a)):
+ a[i] *= other
+ return self
return NotImplemented
def __itruediv__(self, other):
@@ -1718,7 +1811,7 @@ class GlyphCoordinates(object):
>>> g /= (.5,1.5)
>>> g /= 2
>>> g
- GlyphCoordinates([(1.0, 1.0)])
+ GlyphCoordinates([(1, 1)])
"""
if isinstance(other, Number):
other = (other, other)
@@ -1750,20 +1843,6 @@ class GlyphCoordinates(object):
__nonzero__ = __bool__
-def reprflag(flag):
- bin = ""
- if isinstance(flag, str):
- flag = byteord(flag)
- while flag:
- if flag & 0x01:
- bin = "1" + bin
- else:
- bin = "0" + bin
- flag = flag >> 1
- bin = (14 - len(bin)) * "0" + bin
- return bin
-
-
if __name__ == "__main__":
import doctest, sys
sys.exit(doctest.testmod().failed)
diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
index 8c9b530e..bc283cfe 100644
--- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import bytesjoin
from fontTools.misc import sstruct
from fontTools.misc.textTools import safeEval
from . import DefaultTable
@@ -76,12 +75,13 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
result = [compiledHeader, compiledOffsets]
result.extend(sharedTuples)
result.extend(compiledGlyphs)
- return bytesjoin(result)
+ return b''.join(result)
def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
result = []
+ glyf = ttFont['glyf']
for glyphName in ttFont.getGlyphOrder():
- glyph = ttFont["glyf"][glyphName]
+ glyph = glyf[glyphName]
pointCount = self.getNumPoints_(glyph)
variations = self.variations.get(glyphName, [])
result.append(compileGlyph_(variations, pointCount,
@@ -99,9 +99,10 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples)
self.variations = {}
offsetToData = self.offsetToGlyphVariationData
+ glyf = ttFont['glyf']
for i in range(self.glyphCount):
glyphName = glyphs[i]
- glyph = ttFont["glyf"][glyphName]
+ glyph = glyf[glyphName]
numPointsInGlyph = self.getNumPoints_(glyph)
gvarData = data[offsetToData + offsets[i] : offsetToData + offsets[i + 1]]
try:
@@ -214,12 +215,14 @@ def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
variations, pointCount, axisTags, sharedCoordIndices)
if tupleVariationCount == 0:
return b""
- result = (
- struct.pack(">HH", tupleVariationCount, 4 + len(tuples)) + tuples + data
- )
- if len(result) % 2 != 0:
- result = result + b"\0" # padding
- return result
+ result = [
+ struct.pack(">HH", tupleVariationCount, 4 + len(tuples)),
+ tuples,
+ data
+ ]
+ if (len(tuples) + len(data)) % 2 != 0:
+ result.append(b"\0") # padding
+ return b''.join(result)
def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
diff --git a/Lib/fontTools/ttLib/tables/_h_d_m_x.py b/Lib/fontTools/ttLib/tables/_h_d_m_x.py
index 954d1bc1..9f860d2a 100644
--- a/Lib/fontTools/ttLib/tables/_h_d_m_x.py
+++ b/Lib/fontTools/ttLib/tables/_h_d_m_x.py
@@ -1,5 +1,5 @@
-from fontTools.misc.py23 import bytechr, byteord, strjoin
from fontTools.misc import sstruct
+from fontTools.misc.textTools import bytechr, byteord, strjoin
from . import DefaultTable
import array
from collections.abc import Mapping
diff --git a/Lib/fontTools/ttLib/tables/_l_t_a_g.py b/Lib/fontTools/ttLib/tables/_l_t_a_g.py
index caec72a3..ce3c6b97 100644
--- a/Lib/fontTools/ttLib/tables/_l_t_a_g.py
+++ b/Lib/fontTools/ttLib/tables/_l_t_a_g.py
@@ -1,5 +1,4 @@
-from fontTools.misc.py23 import bytesjoin, tobytes
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, tobytes, safeEval
from . import DefaultTable
import struct
diff --git a/Lib/fontTools/ttLib/tables/_m_e_t_a.py b/Lib/fontTools/ttLib/tables/_m_e_t_a.py
index 1a125f82..3faf0a56 100644
--- a/Lib/fontTools/ttLib/tables/_m_e_t_a.py
+++ b/Lib/fontTools/ttLib/tables/_m_e_t_a.py
@@ -1,6 +1,5 @@
-from fontTools.misc.py23 import bytesjoin, strjoin
from fontTools.misc import sstruct
-from fontTools.misc.textTools import readHex
+from fontTools.misc.textTools import bytesjoin, strjoin, readHex
from fontTools.ttLib import TTLibError
from . import DefaultTable
diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
index 206469de..9558addb 100644
--- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py
+++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
-from fontTools.misc.py23 import bytechr, byteord, bytesjoin, strjoin, tobytes, tostr
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytechr, byteord, bytesjoin, strjoin, tobytes, tostr, safeEval
from fontTools.misc.encodingTools import getEncoding
from fontTools.ttLib import newTable
from . import DefaultTable
@@ -121,6 +120,44 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
else:
return None
+ def getFirstDebugName(self, nameIDs):
+ for nameID in nameIDs:
+ name = self.getDebugName(nameID)
+ if name is not None:
+ return name
+ return None
+
+ def getBestFamilyName(self):
+ # 21 = WWS Family Name
+ # 16 = Typographic Family Name
+ # 1 = Family Name
+ return self.getFirstDebugName((21, 16, 1))
+
+ def getBestSubFamilyName(self):
+ # 22 = WWS SubFamily Name
+ # 17 = Typographic SubFamily Name
+ # 2 = SubFamily Name
+ return self.getFirstDebugName((22, 17, 2))
+
+ def getBestFullName(self):
+ # 4 = Full Name
+ # 6 = PostScript Name
+ for nameIDs in ((21, 22), (16, 17), (1, 2), (4, ), (6, )):
+ if len(nameIDs) == 2:
+ name_fam = self.getDebugName(nameIDs[0])
+ name_subfam = self.getDebugName(nameIDs[1])
+ if None in [name_fam, name_subfam]:
+ continue # if any is None, skip
+ name = f"{name_fam} {name_subfam}"
+ if name_subfam.lower() == 'regular':
+ name = f"{name_fam}"
+ return name
+ else:
+ name = self.getDebugName(nameIDs[0])
+ if name is not None:
+ return name
+ return None
+
def setName(self, string, nameID, platformID, platEncID, langID):
""" Set the 'string' for the name record identified by 'nameID', 'platformID',
'platEncID' and 'langID'. If a record with that nameID doesn't exist, create it
diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py
index e26e81f8..c54b87f0 100644
--- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py
+++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py
@@ -1,13 +1,14 @@
-from fontTools.misc.py23 import bytechr, byteord, tobytes, tostr
from fontTools import ttLib
from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder
from fontTools.misc import sstruct
-from fontTools.misc.textTools import safeEval, readHex
+from fontTools.misc.textTools import bytechr, byteord, tobytes, tostr, safeEval, readHex
from . import DefaultTable
import sys
import struct
import array
+import logging
+log = logging.getLogger(__name__)
postFormat = """
>
@@ -85,7 +86,8 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
indices.frombytes(data[:2*numGlyphs])
if sys.byteorder != "big": indices.byteswap()
data = data[2*numGlyphs:]
- self.extraNames = extraNames = unpackPStrings(data)
+ maxIndex = max(indices)
+ self.extraNames = extraNames = unpackPStrings(data, maxIndex-257)
self.glyphOrder = glyphOrder = [""] * int(ttFont['maxp'].numGlyphs)
for glyphID in range(numGlyphs):
index = indices[glyphID]
@@ -252,14 +254,34 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
self.data = readHex(content)
-def unpackPStrings(data):
+def unpackPStrings(data, n):
+ # extract n Pascal strings from data.
+ # if there is not enough data, use ""
+
strings = []
index = 0
dataLen = len(data)
- while index < dataLen:
- length = byteord(data[index])
- strings.append(tostr(data[index+1:index+1+length], encoding="latin1"))
- index = index + 1 + length
+
+ for _ in range(n):
+ if dataLen <= index:
+ length = 0
+ else:
+ length = byteord(data[index])
+ index += 1
+
+ if dataLen <= index + length - 1:
+ name = ""
+ else:
+ name = tostr(data[index:index+length], encoding="latin1")
+ strings.append (name)
+ index += length
+
+ if index < dataLen:
+ log.warning("%d extra bytes in post.stringData array", dataLen - index)
+
+ elif dataLen < index:
+ log.warning("not enough data in post.stringData array")
+
return strings
diff --git a/Lib/fontTools/ttLib/tables/_t_r_a_k.py b/Lib/fontTools/ttLib/tables/_t_r_a_k.py
index 7f3227dc..3052496f 100644
--- a/Lib/fontTools/ttLib/tables/_t_r_a_k.py
+++ b/Lib/fontTools/ttLib/tables/_t_r_a_k.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import bytesjoin
from fontTools.misc import sstruct
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
@@ -6,7 +5,7 @@ from fontTools.misc.fixedTools import (
floatToFixedToStr as fl2str,
strToFixedToFloat as str2fl,
)
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import bytesjoin, safeEval
from fontTools.ttLib import TTLibError
from . import DefaultTable
import struct
diff --git a/Lib/fontTools/ttLib/tables/asciiTable.py b/Lib/fontTools/ttLib/tables/asciiTable.py
index 7b036c8e..a97d92df 100644
--- a/Lib/fontTools/ttLib/tables/asciiTable.py
+++ b/Lib/fontTools/ttLib/tables/asciiTable.py
@@ -1,4 +1,4 @@
-from fontTools.misc.py23 import strjoin, tobytes, tostr
+from fontTools.misc.textTools import strjoin, tobytes, tostr
from . import DefaultTable
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 3c07f9e1..bc2c9fba 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -1,9 +1,10 @@
-from fontTools.misc.py23 import Tag, bytesjoin
+from fontTools.misc.textTools import Tag, bytesjoin
from .DefaultTable import DefaultTable
import sys
import array
import struct
import logging
+from typing import Iterator, NamedTuple, Optional
log = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ class BaseTTXConverter(DefaultTable):
"""
def decompile(self, data, font):
+ """Create an object from the binary data. Called automatically on access."""
from . import otTables
reader = OTTableReader(data, tableTag=self.tableTag)
tableClass = getattr(otTables, self.tableTag)
@@ -41,26 +43,28 @@ class BaseTTXConverter(DefaultTable):
self.table.decompile(reader, font)
def compile(self, font):
- """ Create a top-level OTTableWriter for the GPOS/GSUB table.
- Call the compile method for the the table
- for each 'converter' record in the table converter list
- call converter's write method for each item in the value.
- - For simple items, the write method adds a string to the
- writer's self.items list.
- - For Struct/Table/Subtable items, it add first adds new writer to the
- to the writer's self.items, then calls the item's compile method.
- This creates a tree of writers, rooted at the GUSB/GPOS writer, with
- each writer representing a table, and the writer.items list containing
- the child data strings and writers.
- call the getAllData method
- call _doneWriting, which removes duplicates
- call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
- Traverse the flat list of tables, calling getDataLength on each to update their position
- Traverse the flat list of tables again, calling getData each get the data in the table, now that
- pos's and offset are known.
-
- If a lookup subtable overflows an offset, we have to start all over.
- """
+ """Compiles the table into binary. Called automatically on save."""
+
+ # General outline:
+ # Create a top-level OTTableWriter for the GPOS/GSUB table.
+ # Call the compile method for the the table
+ # for each 'converter' record in the table converter list
+ # call converter's write method for each item in the value.
+ # - For simple items, the write method adds a string to the
+ # writer's self.items list.
+ # - For Struct/Table/Subtable items, it add first adds new writer to the
+ # to the writer's self.items, then calls the item's compile method.
+ # This creates a tree of writers, rooted at the GUSB/GPOS writer, with
+ # each writer representing a table, and the writer.items list containing
+ # the child data strings and writers.
+ # call the getAllData method
+ # call _doneWriting, which removes duplicates
+ # call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
+ # Traverse the flat list of tables, calling getDataLength on each to update their position
+ # Traverse the flat list of tables again, calling getData each get the data in the table, now that
+ # pos's and offset are known.
+
+ # If a lookup subtable overflows an offset, we have to start all over.
overflowRecord = None
while True:
@@ -105,6 +109,13 @@ class BaseTTXConverter(DefaultTable):
self.table.fromXML(name, attrs, content, font)
self.table.populateDefaults()
+ def ensureDecompiled(self):
+ self.table.ensureDecompiled(recurse=True)
+
+
+# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
+assert len(struct.pack('i', 0)) == 4
+assert array.array('i').itemsize == 4, "Oops, file a bug against fonttools."
class OTTableReader(object):
@@ -140,32 +151,43 @@ class OTTableReader(object):
value, = struct.unpack(f">{typecode}", self.data[pos:newpos])
self.pos = newpos
return value
-
- def readUShort(self):
- return self.readValue("H", staticSize=2)
-
def readArray(self, typecode, staticSize, count):
pos = self.pos
newpos = pos + count * staticSize
value = array.array(typecode, self.data[pos:newpos])
if sys.byteorder != "big": value.byteswap()
self.pos = newpos
- return value
-
- def readUShortArray(self, count):
- return self.readArray("H", staticSize=2, count=count)
+ return value.tolist()
def readInt8(self):
return self.readValue("b", staticSize=1)
+ def readInt8Array(self, count):
+ return self.readArray("b", staticSize=1, count=count)
def readShort(self):
return self.readValue("h", staticSize=2)
+ def readShortArray(self, count):
+ return self.readArray("h", staticSize=2, count=count)
def readLong(self):
- return self.readValue("l", staticSize=4)
+ return self.readValue("i", staticSize=4)
+ def readLongArray(self, count):
+ return self.readArray("i", staticSize=4, count=count)
def readUInt8(self):
return self.readValue("B", staticSize=1)
+ def readUInt8Array(self, count):
+ return self.readArray("B", staticSize=1, count=count)
+
+ def readUShort(self):
+ return self.readValue("H", staticSize=2)
+ def readUShortArray(self, count):
+ return self.readArray("H", staticSize=2, count=count)
+
+ def readULong(self):
+ return self.readValue("I", staticSize=4)
+ def readULongArray(self, count):
+ return self.readArray("I", staticSize=4, count=count)
def readUInt24(self):
pos = self.pos
@@ -173,9 +195,8 @@ class OTTableReader(object):
value, = struct.unpack(">l", b'\0'+self.data[pos:newpos])
self.pos = newpos
return value
-
- def readULong(self):
- return self.readValue("L", staticSize=4)
+ def readUInt24Array(self, count):
+ return [self.readUInt24() for _ in range(count)]
def readTag(self):
pos = self.pos
@@ -316,6 +337,12 @@ class OTTableWriter(object):
items[i] = item.getCountData()
elif hasattr(item, "getData"):
item._doneWriting(internedTables)
+ # At this point, all subwriters are hashable based on their items.
+ # (See hash and comparison magic methods above.) So the ``setdefault``
+ # call here will return the first writer object we've seen with
+ # equal content, or store it in the dictionary if it's not been
+ # seen yet. We therefore replace the subwriter object with an equivalent
+ # object, which deduplicates the tree.
if not dontShare:
items[i] = item = internedTables.setdefault(item, item)
self.items = tuple(items)
@@ -344,13 +371,13 @@ class OTTableWriter(object):
tables, extTables, done = extTables, None, {}
# add Coverage table if it is sorted last.
- sortCoverageLast = 0
+ sortCoverageLast = False
if hasattr(self, "sortCoverageLast"):
# Find coverage table
for i in range(numItems):
item = self.items[i]
- if hasattr(item, "name") and (item.name == "Coverage"):
- sortCoverageLast = 1
+ if getattr(item, 'name', None) == "Coverage":
+ sortCoverageLast = True
break
if id(item) not in done:
item._gatherTables(tables, extTables, done)
@@ -363,7 +390,7 @@ class OTTableWriter(object):
if not hasattr(item, "getData"):
continue
- if sortCoverageLast and (i==1) and item.name == 'Coverage':
+ if sortCoverageLast and (i==1) and getattr(item, 'name', None) == 'Coverage':
# we've already 'gathered' it above
continue
@@ -419,33 +446,52 @@ class OTTableWriter(object):
def writeValue(self, typecode, value):
self.items.append(struct.pack(f">{typecode}", value))
+ def writeArray(self, typecode, values):
+ a = array.array(typecode, values)
+ if sys.byteorder != "big": a.byteswap()
+ self.items.append(a.tobytes())
- def writeUShort(self, value):
- assert 0 <= value < 0x10000, value
- self.items.append(struct.pack(">H", value))
+ def writeInt8(self, value):
+ assert -128 <= value < 128, value
+ self.items.append(struct.pack(">b", value))
+ def writeInt8Array(self, values):
+ self.writeArray('b', values)
def writeShort(self, value):
assert -32768 <= value < 32768, value
self.items.append(struct.pack(">h", value))
+ def writeShortArray(self, values):
+ self.writeArray('h', values)
+
+ def writeLong(self, value):
+ self.items.append(struct.pack(">i", value))
+ def writeLongArray(self, values):
+ self.writeArray('i', values)
def writeUInt8(self, value):
assert 0 <= value < 256, value
self.items.append(struct.pack(">B", value))
+ def writeUInt8Array(self, values):
+ self.writeArray('B', values)
- def writeInt8(self, value):
- assert -128 <= value < 128, value
- self.items.append(struct.pack(">b", value))
+ def writeUShort(self, value):
+ assert 0 <= value < 0x10000, value
+ self.items.append(struct.pack(">H", value))
+ def writeUShortArray(self, values):
+ self.writeArray('H', values)
+
+ def writeULong(self, value):
+ self.items.append(struct.pack(">I", value))
+ def writeULongArray(self, values):
+ self.writeArray('I', values)
def writeUInt24(self, value):
assert 0 <= value < 0x1000000, value
b = struct.pack(">L", value)
self.items.append(b[1:])
-
- def writeLong(self, value):
- self.items.append(struct.pack(">l", value))
-
- def writeULong(self, value):
- self.items.append(struct.pack(">L", value))
+ def writeUInt24Array(self, values):
+ for value in values:
+ self.writeUInt24(value)
def writeTag(self, tag):
tag = Tag(tag).tobytes()
@@ -532,11 +578,11 @@ def packUShort(value):
def packULong(value):
assert 0 <= value < 0x100000000, value
- return struct.pack(">L", value)
+ return struct.pack(">I", value)
def packUInt24(value):
assert 0 <= value < 0x1000000, value
- return struct.pack(">L", value)[1:]
+ return struct.pack(">I", value)[1:]
class BaseTable(object):
@@ -554,13 +600,16 @@ class BaseTable(object):
raise AttributeError(attr)
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=False):
reader = self.__dict__.get("reader")
if reader:
del self.reader
font = self.font
del self.font
self.decompile(reader, font)
+ if recurse:
+ for subtable in self.iterSubTables():
+ subtable.value.ensureDecompiled(recurse)
@classmethod
def getRecordSize(cls, reader):
@@ -571,7 +620,7 @@ class BaseTable(object):
countValue = 1
if conv.repeat:
if conv.repeat in reader:
- countValue = reader[conv.repeat]
+ countValue = reader[conv.repeat] + conv.aux
else:
return NotImplemented
totalSize += size * countValue
@@ -698,14 +747,11 @@ class BaseTable(object):
else:
# conv.repeat is a propagated count
writer[conv.repeat].setValue(countValue)
- values = value
- for i, value in enumerate(values):
- try:
- conv.write(writer, font, table, value, i)
- except Exception as e:
- name = value.__class__.__name__ if value is not None else conv.name
- e.args = e.args + (name+'['+str(i)+']',)
- raise
+ try:
+ conv.writeArray(writer, font, table, value)
+ except Exception as e:
+ e.args = e.args + (conv.name+'[]',)
+ raise
elif conv.isCount:
# Special-case Count values.
# Assumption: a Count field will *always* precede
@@ -812,6 +858,37 @@ class BaseTable(object):
return self.__dict__ == other.__dict__
+ class SubTableEntry(NamedTuple):
+ """See BaseTable.iterSubTables()"""
+ name: str
+ value: "BaseTable"
+ index: Optional[int] = None # index into given array, None for single values
+
+ def iterSubTables(self) -> Iterator[SubTableEntry]:
+ """Yield (name, value, index) namedtuples for all subtables of current table.
+
+ A sub-table is an instance of BaseTable (or subclass thereof) that is a child
+ of self, the current parent table.
+ The tuples also contain the attribute name (str) of the of parent table to get
+ a subtable, and optionally, for lists of subtables (i.e. attributes associated
+ with a converter that has a 'repeat'), an index into the list containing the
+ given subtable value.
+ This method can be useful to traverse trees of otTables.
+ """
+ for conv in self.getConverters():
+ name = conv.name
+ value = getattr(self, name, None)
+ if value is None:
+ continue
+ if isinstance(value, BaseTable):
+ yield self.SubTableEntry(name, value)
+ elif isinstance(value, list):
+ yield from (
+ self.SubTableEntry(name, v, index=i)
+ for i, v in enumerate(value)
+ if isinstance(v, BaseTable)
+ )
+
class FormatSwitchingBaseTable(BaseTable):
@@ -823,6 +900,15 @@ class FormatSwitchingBaseTable(BaseTable):
return NotImplemented
def getConverters(self):
+ try:
+ fmt = self.Format
+ except AttributeError:
+ # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format'
+ # attribute after fully decompiled, only gain one in preWrite before being
+ # recompiled. In the decompiled state, these hand-coded classes defined in
+ # otTables.py lose their format-specific nature and gain more high-level
+ # attributes that are not tied to converters.
+ return []
return self.converters.get(self.Format, [])
def getConverterByName(self, name):
@@ -970,6 +1056,13 @@ class ValueRecord(object):
format = format | valueRecordFormatDict[name][0]
return format
+ def getEffectiveFormat(self):
+ format = 0
+ for name,value in self.__dict__.items():
+ if value:
+ format = format | valueRecordFormatDict[name][0]
+ return format
+
def toXML(self, xmlWriter, font, valueName, attrs=None):
if attrs is None:
simpleItems = []
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 4af38acd..44fcd0ab 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import bytesjoin, tobytes, tostr
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
floatToFixed as fl2fi,
@@ -7,14 +6,15 @@ from fontTools.misc.fixedTools import (
ensureVersionIsLong as fi2ve,
versionToFixed as ve2fi,
)
-from fontTools.misc.textTools import pad, safeEval
+from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
+from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
from fontTools.ttLib import getSearchRange
from .otBase import (CountReference, FormatSwitchingBaseTable,
OTTableReader, OTTableWriter, ValueRecordFactory)
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
ContextualMorphAction, LigatureMorphAction,
- InsertionMorphAction, MorxSubtable, VariableFloat,
- VariableInt, ExtendMode as _ExtendMode,
+ InsertionMorphAction, MorxSubtable,
+ ExtendMode as _ExtendMode,
CompositeMode as _CompositeMode)
from itertools import zip_longest
from functools import partial
@@ -192,8 +192,12 @@ class BaseConverter(object):
raise NotImplementedError(self)
def writeArray(self, writer, font, tableDict, values):
- for i, value in enumerate(values):
- self.write(writer, font, tableDict, value, i)
+ try:
+ for i, value in enumerate(values):
+ self.write(writer, font, tableDict, value, i)
+ except Exception as e:
+ e.args = e.args + (i,)
+ raise
def write(self, writer, font, tableDict, value, repeatIndex=None):
"""Write a value to the writer."""
@@ -221,6 +225,18 @@ class SimpleValue(BaseConverter):
def xmlRead(self, attrs, content, font):
return self.fromString(attrs["value"])
+class OptionalValue(SimpleValue):
+ DEFAULT = None
+ def xmlWrite(self, xmlWriter, font, value, name, attrs):
+ if value != self.DEFAULT:
+ attrs.append(("value", self.toString(value)))
+ xmlWriter.simpletag(name, attrs)
+ xmlWriter.newline()
+ def xmlRead(self, attrs, content, font):
+ if "value" in attrs:
+ return self.fromString(attrs["value"])
+ return self.DEFAULT
+
class IntValue(SimpleValue):
@staticmethod
def fromString(value):
@@ -230,48 +246,75 @@ class Long(IntValue):
staticSize = 4
def read(self, reader, font, tableDict):
return reader.readLong()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readLongArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeLong(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeLongArray(values)
class ULong(IntValue):
staticSize = 4
def read(self, reader, font, tableDict):
return reader.readULong()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readULongArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeULong(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeULongArray(values)
class Flags32(ULong):
@staticmethod
def toString(value):
return "0x%08X" % value
+class VarIndex(OptionalValue, ULong):
+ DEFAULT = 0xFFFFFFFF
+
class Short(IntValue):
staticSize = 2
def read(self, reader, font, tableDict):
return reader.readShort()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readShortArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeShort(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeShortArray(values)
class UShort(IntValue):
staticSize = 2
def read(self, reader, font, tableDict):
return reader.readUShort()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readUShortArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUShort(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeUShortArray(values)
class Int8(IntValue):
staticSize = 1
def read(self, reader, font, tableDict):
return reader.readInt8()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readInt8Array(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeInt8(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeInt8Array(values)
class UInt8(IntValue):
staticSize = 1
def read(self, reader, font, tableDict):
return reader.readUInt8()
+ def readArray(self, reader, font, tableDict, count):
+ return reader.readUInt8Array(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUInt8(value)
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeUInt8Array(values)
class UInt24(IntValue):
staticSize = 3
@@ -304,16 +347,11 @@ class GlyphID(SimpleValue):
staticSize = 2
typecode = "H"
def readArray(self, reader, font, tableDict, count):
- glyphOrder = font.getGlyphOrder()
- gids = reader.readArray(self.typecode, self.staticSize, count)
- try:
- l = [glyphOrder[gid] for gid in gids]
- except IndexError:
- # Slower, but will not throw an IndexError on an invalid glyph id.
- l = [font.getGlyphName(gid) for gid in gids]
- return l
+ return font.getGlyphNameMany(reader.readArray(self.typecode, self.staticSize, count))
def read(self, reader, font, tableDict):
return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))
+ def writeArray(self, writer, font, tableDict, values):
+ writer.writeArray(self.typecode, font.getGlyphIDMany(values))
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeValue(self.typecode, font.getGlyphID(value))
@@ -390,6 +428,22 @@ class F2Dot14(FloatValue):
def toString(value):
return fl2str(value, 14)
+class Angle(F2Dot14):
+ # angles are specified in degrees, and encoded as F2Dot14 fractions of half
+ # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
+ factor = 1.0/(1<<14) * 180 # 0.010986328125
+ def read(self, reader, font, tableDict):
+ return super().read(reader, font, tableDict) * 180
+ def write(self, writer, font, tableDict, value, repeatIndex=None):
+ super().write(writer, font, tableDict, value / 180, repeatIndex=repeatIndex)
+ @classmethod
+ def fromString(cls, value):
+ # quantize to nearest multiples of minimum fixed-precision angle
+ return otRound(float(value) / cls.factor) * cls.factor
+ @classmethod
+ def toString(cls, value):
+ return nearestMultipleShortestRepr(value, cls.factor)
+
class Version(SimpleValue):
staticSize = 4
def read(self, reader, font, tableDict):
@@ -1155,8 +1209,7 @@ class STXHeader(BaseConverter):
def _readLigatures(self, reader, font):
limit = len(reader.data)
numLigatureGlyphs = (limit - reader.pos) // 2
- return [font.getGlyphName(g)
- for g in reader.readUShortArray(numLigatureGlyphs)]
+ return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs))
def _countPerGlyphLookups(self, table):
# Somewhat annoyingly, the morx table does not encode
@@ -1551,20 +1604,15 @@ class VarIdxMapValue(BaseConverter):
outerShift = 16 - innerBits
entrySize = 1 + ((fmt & 0x0030) >> 4)
- read = {
- 1: reader.readUInt8,
- 2: reader.readUShort,
- 3: reader.readUInt24,
- 4: reader.readULong,
+ readArray = {
+ 1: reader.readUInt8Array,
+ 2: reader.readUShortArray,
+ 3: reader.readUInt24Array,
+ 4: reader.readULongArray,
}[entrySize]
- mapping = []
- for i in range(nItems):
- raw = read()
- idx = ((raw & outerMask) << outerShift) | (raw & innerMask)
- mapping.append(idx)
-
- return mapping
+ return [(((raw & outerMask) << outerShift) | (raw & innerMask))
+ for raw in readArray(nItems)]
def write(self, writer, font, tableDict, value, repeatIndex=None):
fmt = tableDict['EntryFormat']
@@ -1576,16 +1624,15 @@ class VarIdxMapValue(BaseConverter):
outerShift = 16 - innerBits
entrySize = 1 + ((fmt & 0x0030) >> 4)
- write = {
- 1: writer.writeUInt8,
- 2: writer.writeUShort,
- 3: writer.writeUInt24,
- 4: writer.writeULong,
+ writeArray = {
+ 1: writer.writeUInt8Array,
+ 2: writer.writeUShortArray,
+ 3: writer.writeUInt24Array,
+ 4: writer.writeULongArray,
}[entrySize]
- for idx in mapping:
- raw = ((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)
- write(raw)
+ writeArray([(((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask))
+ for idx in mapping])
class VarDataValue(BaseConverter):
@@ -1594,27 +1641,43 @@ class VarDataValue(BaseConverter):
values = []
regionCount = tableDict["VarRegionCount"]
- shortCount = tableDict["NumShorts"]
+ wordCount = tableDict["NumShorts"]
- for i in range(min(regionCount, shortCount)):
- values.append(reader.readShort())
- for i in range(min(regionCount, shortCount), regionCount):
- values.append(reader.readInt8())
- for i in range(regionCount, shortCount):
- reader.readInt8()
+ # https://github.com/fonttools/fonttools/issues/2279
+ longWords = bool(wordCount & 0x8000)
+ wordCount = wordCount & 0x7FFF
+
+ if longWords:
+ readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray
+ else:
+ readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array
+
+ n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
+ values.extend(readBigArray(n1))
+ values.extend(readSmallArray(n2 - n1))
+ if n2 > regionCount: # Padding
+ del values[regionCount:]
return values
- def write(self, writer, font, tableDict, value, repeatIndex=None):
+ def write(self, writer, font, tableDict, values, repeatIndex=None):
regionCount = tableDict["VarRegionCount"]
- shortCount = tableDict["NumShorts"]
+ wordCount = tableDict["NumShorts"]
- for i in range(min(regionCount, shortCount)):
- writer.writeShort(value[i])
- for i in range(min(regionCount, shortCount), regionCount):
- writer.writeInt8(value[i])
- for i in range(regionCount, shortCount):
- writer.writeInt8(0)
+ # https://github.com/fonttools/fonttools/issues/2279
+ longWords = bool(wordCount & 0x8000)
+ wordCount = wordCount & 0x7FFF
+
+ (writeBigArray, writeSmallArray) = {
+ False: (writer.writeShortArray, writer.writeInt8Array),
+ True: (writer.writeLongArray, writer.writeShortArray),
+ }[longWords]
+
+ n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
+ writeBigArray(values[:n1])
+ writeSmallArray(values[n1:regionCount])
+ if n2 > regionCount: # Padding
+ writer.writeSmallArray([0] * (n2 - regionCount))
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
@@ -1637,99 +1700,6 @@ class LookupFlag(UShort):
xmlWriter.comment(" ".join(flags))
xmlWriter.newline()
-def _issubclass_namedtuple(x):
- return (
- issubclass(x, tuple)
- and getattr(x, "_fields", None) is not None
- )
-
-
-class _NamedTupleConverter(BaseConverter):
- # subclasses must override this
- tupleClass = NotImplemented
- # List[SimpleValue]
- converterClasses = NotImplemented
-
- def __init__(self, name, repeat, aux, tableClass=None):
- # we expect all converters to be subclasses of SimpleValue
- assert all(issubclass(klass, SimpleValue) for klass in self.converterClasses)
- assert _issubclass_namedtuple(self.tupleClass), repr(self.tupleClass)
- assert len(self.tupleClass._fields) == len(self.converterClasses)
- assert tableClass is None # tableClass is unused by SimplValues
- BaseConverter.__init__(self, name, repeat, aux)
- self.converters = [
- klass(name=name, repeat=None, aux=None)
- for name, klass in zip(self.tupleClass._fields, self.converterClasses)
- ]
- self.convertersByName = {conv.name: conv for conv in self.converters}
- # returned by getRecordSize method
- self.staticSize = sum(c.staticSize for c in self.converters)
-
- def read(self, reader, font, tableDict):
- kwargs = {
- conv.name: conv.read(reader, font, tableDict)
- for conv in self.converters
- }
- return self.tupleClass(**kwargs)
-
- def write(self, writer, font, tableDict, value, repeatIndex=None):
- for conv in self.converters:
- v = getattr(value, conv.name)
- # repeatIndex is unused for SimpleValues
- conv.write(writer, font, tableDict, v, repeatIndex=None)
-
- def xmlWrite(self, xmlWriter, font, value, name, attrs):
- assert value is not None
- defaults = value.__new__.__defaults__ or ()
- assert len(self.converters) >= len(defaults)
- values = {}
- required = object()
- for conv, default in zip_longest(
- reversed(self.converters),
- reversed(defaults),
- fillvalue=required,
- ):
- v = getattr(value, conv.name)
- if default is required or v != default:
- values[conv.name] = conv.toString(v)
- if attrs is None:
- attrs = []
- attrs.extend(
- (conv.name, values[conv.name])
- for conv in self.converters
- if conv.name in values
- )
- xmlWriter.simpletag(name, attrs)
- xmlWriter.newline()
-
- def xmlRead(self, attrs, content, font):
- converters = self.convertersByName
- kwargs = {
- k: converters[k].fromString(v)
- for k, v in attrs.items()
- }
- return self.tupleClass(**kwargs)
-
-
-class VarFixed(_NamedTupleConverter):
- tupleClass = VariableFloat
- converterClasses = [Fixed, ULong]
-
-
-class VarF2Dot14(_NamedTupleConverter):
- tupleClass = VariableFloat
- converterClasses = [F2Dot14, ULong]
-
-
-class VarInt16(_NamedTupleConverter):
- tupleClass = VariableInt
- converterClasses = [Short, ULong]
-
-
-class VarUInt16(_NamedTupleConverter):
- tupleClass = VariableInt
- converterClasses = [UShort, ULong]
-
class _UInt8Enum(UInt8):
enumClass = NotImplemented
@@ -1762,6 +1732,7 @@ converterMapping = {
"uint32": ULong,
"char64": Char64,
"Flags32": Flags32,
+ "VarIndex": VarIndex,
"Version": Version,
"Tag": Tag,
"GlyphID": GlyphID,
@@ -1770,6 +1741,7 @@ converterMapping = {
"DeciPoints": DeciPoints,
"Fixed": Fixed,
"F2Dot14": F2Dot14,
+ "Angle": Angle,
"struct": Struct,
"Offset": Table,
"LOffset": LTable,
@@ -1798,10 +1770,4 @@ converterMapping = {
"OffsetTo": lambda C: partial(Table, tableClass=C),
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
"LOffset24To": lambda C: partial(Table24, tableClass=C),
-
- # Variable types
- "VarFixed": VarFixed,
- "VarF2Dot14": VarF2Dot14,
- "VarInt16": VarInt16,
- "VarUInt16": VarUInt16,
}
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index c4294169..dd4033e4 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -988,6 +988,20 @@ otData = [
('VarIdxMapValue', 'mapping', '', 0, 'Array of compressed data'),
]),
+ ('DeltaSetIndexMapFormat0', [
+ ('uint8', 'Format', None, None, 'Format of the DeltaSetIndexMap = 0'),
+ ('uint8', 'EntryFormat', None, None, ''), # Automatically computed
+ ('uint16', 'MappingCount', None, None, ''), # Automatically computed
+ ('VarIdxMapValue', 'mapping', '', 0, 'Array of compressed data'),
+ ]),
+
+ ('DeltaSetIndexMapFormat1', [
+ ('uint8', 'Format', None, None, 'Format of the DeltaSetIndexMap = 1'),
+ ('uint8', 'EntryFormat', None, None, ''), # Automatically computed
+ ('uint32', 'MappingCount', None, None, ''), # Automatically computed
+ ('VarIdxMapValue', 'mapping', '', 0, 'Array of compressed data'),
+ ]),
+
# Glyph advance variations
('HVAR', [
@@ -1546,8 +1560,10 @@ otData = [
('LOffset', 'BaseGlyphRecordArray', None, None, 'Offset (from beginning of COLR table) to Base Glyph records.'),
('LOffset', 'LayerRecordArray', None, None, 'Offset (from beginning of COLR table) to Layer Records.'),
('uint16', 'LayerRecordCount', None, None, 'Number of Layer Records.'),
- ('LOffset', 'BaseGlyphV1List', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
- ('LOffset', 'LayerV1List', None, 'Version >= 1', 'Offset (from beginning of COLR table) to LayerV1List.'),
+ ('LOffset', 'BaseGlyphList', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
+ ('LOffset', 'LayerList', None, 'Version >= 1', 'Offset (from beginning of COLR table) to LayerList.'),
+ ('LOffset', 'ClipList', None, 'Version >= 1', 'Offset to ClipList table (may be NULL)'),
+ ('LOffsetTo(DeltaSetIndexMap)', 'VarIndexMap', None, 'Version >= 1', 'Offset to DeltaSetIndexMap table (may be NULL)'),
('LOffset', 'VarStore', None, 'Version >= 1', 'Offset to variation store (may be NULL)'),
]),
@@ -1570,19 +1586,48 @@ otData = [
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
]),
- ('BaseGlyphV1List', [
+ ('BaseGlyphList', [
('uint32', 'BaseGlyphCount', None, None, 'Number of Version-1 Base Glyph records'),
- ('struct', 'BaseGlyphV1Record', 'BaseGlyphCount', 0, 'Array of Version-1 Base Glyph records'),
+ ('struct', 'BaseGlyphPaintRecord', 'BaseGlyphCount', 0, 'Array of Version-1 Base Glyph records'),
]),
- ('BaseGlyphV1Record', [
+ ('BaseGlyphPaintRecord', [
('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph.'),
- ('LOffset', 'Paint', None, None, 'Offset (from beginning of BaseGlyphV1Record) to Paint, typically a PaintColrLayers.'),
+ ('LOffset', 'Paint', None, None, 'Offset (from beginning of BaseGlyphPaintRecord) to Paint, typically a PaintColrLayers.'),
]),
- ('LayerV1List', [
+ ('LayerList', [
('uint32', 'LayerCount', None, None, 'Number of Version-1 Layers'),
- ('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'),
+ ('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerList table.'),
+ ]),
+
+ ('ClipListFormat1', [
+ ('uint8', 'Format', None, None, 'Format for ClipList with 16bit glyph IDs: 1'),
+ ('uint32', 'ClipCount', None, None, 'Number of Clip records.'),
+ ('struct', 'ClipRecord', 'ClipCount', 0, 'Array of Clip records sorted by glyph ID.'),
+ ]),
+
+ ('ClipRecord', [
+ ('uint16', 'StartGlyphID', None, None, 'First glyph ID in the range.'),
+ ('uint16', 'EndGlyphID', None, None, 'Last glyph ID in the range.'),
+ ('Offset24', 'ClipBox', None, None, 'Offset to a ClipBox table.'),
+ ]),
+
+ ('ClipBoxFormat1', [
+ ('uint8', 'Format', None, None, 'Format for ClipBox without variation: set to 1.'),
+ ('int16', 'xMin', None, None, 'Minimum x of clip box.'),
+ ('int16', 'yMin', None, None, 'Minimum y of clip box.'),
+ ('int16', 'xMax', None, None, 'Maximum x of clip box.'),
+ ('int16', 'yMax', None, None, 'Maximum y of clip box.'),
+ ]),
+
+ ('ClipBoxFormat2', [
+ ('uint8', 'Format', None, None, 'Format for variable ClipBox: set to 2.'),
+ ('int16', 'xMin', None, None, 'Minimum x of clip box.'),
+ ('int16', 'yMin', None, None, 'Minimum y of clip box.'),
+ ('int16', 'xMax', None, None, 'Maximum x of clip box.'),
+ ('int16', 'yMax', None, None, 'Maximum y of clip box.'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
# COLRv1 Affine2x3 uses the same column-major order to serialize a 2D
@@ -1603,30 +1648,25 @@ otData = [
('Fixed', 'dy', None, None, 'Translation in y direction'),
]),
('VarAffine2x3', [
- ('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
- ('VarFixed', 'yx', None, None, 'y-part of x basis vector'),
- ('VarFixed', 'xy', None, None, 'x-part of y basis vector'),
- ('VarFixed', 'yy', None, None, 'y-part of y basis vector'),
- ('VarFixed', 'dx', None, None, 'Translation in x direction'),
- ('VarFixed', 'dy', None, None, 'Translation in y direction'),
- ]),
-
- ('ColorIndex', [
- ('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
- ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
- ]),
- ('VarColorIndex', [
- ('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
- ('VarF2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
+ ('Fixed', 'xx', None, None, 'x-part of x basis vector'),
+ ('Fixed', 'yx', None, None, 'y-part of x basis vector'),
+ ('Fixed', 'xy', None, None, 'x-part of y basis vector'),
+ ('Fixed', 'yy', None, None, 'y-part of y basis vector'),
+ ('Fixed', 'dx', None, None, 'Translation in x direction'),
+ ('Fixed', 'dy', None, None, 'Translation in y direction'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
('ColorStop', [
('F2Dot14', 'StopOffset', None, None, ''),
- ('ColorIndex', 'Color', None, None, ''),
+ ('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
]),
('VarColorStop', [
- ('VarF2Dot14', 'StopOffset', None, None, ''),
- ('VarColorIndex', 'Color', None, None, ''),
+ ('F2Dot14', 'StopOffset', None, None, 'VarIndexBase + 0'),
+ ('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 1'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
('ColorLine', [
@@ -1643,19 +1683,22 @@ otData = [
# PaintColrLayers
('PaintFormat1', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 1'),
- ('uint8', 'NumLayers', None, None, 'Number of offsets to Paint to read from LayerV1List.'),
- ('uint32', 'FirstLayerIndex', None, None, 'Index into LayerV1List.'),
+ ('uint8', 'NumLayers', None, None, 'Number of offsets to Paint to read from LayerList.'),
+ ('uint32', 'FirstLayerIndex', None, None, 'Index into LayerList.'),
]),
# PaintSolid
('PaintFormat2', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 2'),
- ('ColorIndex', 'Color', None, None, 'A solid color paint.'),
+ ('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
]),
# PaintVarSolid
('PaintFormat3', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
- ('VarColorIndex', 'Color', None, None, 'A solid color paint.'),
+ ('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 0'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
# PaintLinearGradient
@@ -1673,12 +1716,13 @@ otData = [
('PaintFormat5', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarLinearGradient table) to VarColorLine subtable.'),
- ('VarInt16', 'x0', None, None, ''),
- ('VarInt16', 'y0', None, None, ''),
- ('VarInt16', 'x1', None, None, ''),
- ('VarInt16', 'y1', None, None, ''),
- ('VarInt16', 'x2', None, None, ''),
- ('VarInt16', 'y2', None, None, ''),
+ ('int16', 'x0', None, None, ''),
+ ('int16', 'y0', None, None, ''),
+ ('int16', 'x1', None, None, ''),
+ ('int16', 'y1', None, None, ''),
+ ('int16', 'x2', None, None, ''),
+ ('int16', 'y2', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
# PaintRadialGradient
@@ -1696,12 +1740,13 @@ otData = [
('PaintFormat7', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarRadialGradient table) to VarColorLine subtable.'),
- ('VarInt16', 'x0', None, None, ''),
- ('VarInt16', 'y0', None, None, ''),
- ('VarUInt16', 'r0', None, None, ''),
- ('VarInt16', 'x1', None, None, ''),
- ('VarInt16', 'y1', None, None, ''),
- ('VarUInt16', 'r1', None, None, ''),
+ ('int16', 'x0', None, None, ''),
+ ('int16', 'y0', None, None, ''),
+ ('uint16', 'r0', None, None, ''),
+ ('int16', 'x1', None, None, ''),
+ ('int16', 'y1', None, None, ''),
+ ('uint16', 'r1', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
# PaintSweepGradient
@@ -1710,17 +1755,18 @@ otData = [
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintSweepGradient table) to ColorLine subtable.'),
('int16', 'centerX', None, None, 'Center x coordinate.'),
('int16', 'centerY', None, None, 'Center y coordinate.'),
- ('Fixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
- ('Fixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
+ ('Angle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
]),
# PaintVarSweepGradient
('PaintFormat9', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarSweepGradient table) to VarColorLine subtable.'),
- ('VarInt16', 'centerX', None, None, 'Center x coordinate.'),
- ('VarInt16', 'centerY', None, None, 'Center y coordinate.'),
- ('VarFixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
- ('VarFixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ('int16', 'centerX', None, None, 'Center x coordinate.'),
+ ('int16', 'centerY', None, None, 'Center y coordinate.'),
+ ('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
+ ('Angle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
# PaintGlyph
@@ -1733,76 +1779,177 @@ otData = [
# PaintColrGlyph
('PaintFormat11', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 11'),
- ('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'),
+ ('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphList base glyph.'),
]),
# PaintTransform
('PaintFormat12', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 12'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTransform table) to Paint subtable.'),
- ('Affine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
+ ('LOffset24To(Affine2x3)', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
]),
# PaintVarTransform
('PaintFormat13', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 13'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTransform table) to Paint subtable.'),
- ('VarAffine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
+ ('LOffset24To(VarAffine2x3)', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
]),
# PaintTranslate
('PaintFormat14', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 14'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTranslate table) to Paint subtable.'),
- ('Fixed', 'dx', None, None, 'Translation in x direction.'),
- ('Fixed', 'dy', None, None, 'Translation in y direction.'),
+ ('int16', 'dx', None, None, 'Translation in x direction.'),
+ ('int16', 'dy', None, None, 'Translation in y direction.'),
]),
# PaintVarTranslate
('PaintFormat15', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 15'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTranslate table) to Paint subtable.'),
- ('VarFixed', 'dx', None, None, 'Translation in x direction.'),
- ('VarFixed', 'dy', None, None, 'Translation in y direction.'),
+ ('int16', 'dx', None, None, 'Translation in x direction.'),
+ ('int16', 'dy', None, None, 'Translation in y direction.'),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
- # PaintRotate
+ # PaintScale
('PaintFormat16', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 16'),
- ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintRotate table) to Paint subtable.'),
- ('Fixed', 'angle', None, None, ''),
- ('Fixed', 'centerX', None, None, ''),
- ('Fixed', 'centerY', None, None, ''),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintScale table) to Paint subtable.'),
+ ('F2Dot14', 'scaleX', None, None, ''),
+ ('F2Dot14', 'scaleY', None, None, ''),
]),
- # PaintVarRotate
+ # PaintVarScale
('PaintFormat17', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 17'),
- ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
- ('VarFixed', 'angle', None, None, ''),
- ('VarFixed', 'centerX', None, None, ''),
- ('VarFixed', 'centerY', None, None, ''),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScale table) to Paint subtable.'),
+ ('F2Dot14', 'scaleX', None, None, ''),
+ ('F2Dot14', 'scaleY', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
- # PaintSkew
+ # PaintScaleAroundCenter
('PaintFormat18', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 18'),
- ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintSkew table) to Paint subtable.'),
- ('Fixed', 'xSkewAngle', None, None, ''),
- ('Fixed', 'ySkewAngle', None, None, ''),
- ('Fixed', 'centerX', None, None, ''),
- ('Fixed', 'centerY', None, None, ''),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintScaleAroundCenter table) to Paint subtable.'),
+ ('F2Dot14', 'scaleX', None, None, ''),
+ ('F2Dot14', 'scaleY', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
]),
- # PaintVarSkew
+ # PaintVarScaleAroundCenter
('PaintFormat19', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 19'),
- ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
- ('VarFixed', 'xSkewAngle', None, None, ''),
- ('VarFixed', 'ySkewAngle', None, None, ''),
- ('VarFixed', 'centerX', None, None, ''),
- ('VarFixed', 'centerY', None, None, ''),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleAroundCenter table) to Paint subtable.'),
+ ('F2Dot14', 'scaleX', None, None, ''),
+ ('F2Dot14', 'scaleY', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
- # PaintComposite
+ # PaintScaleUniform
('PaintFormat20', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 20'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintScaleUniform table) to Paint subtable.'),
+ ('F2Dot14', 'scale', None, None, ''),
+ ]),
+ # PaintVarScaleUniform
+ ('PaintFormat21', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 21'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniform table) to Paint subtable.'),
+ ('F2Dot14', 'scale', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintScaleUniformAroundCenter
+ ('PaintFormat22', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 22'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintScaleUniformAroundCenter table) to Paint subtable.'),
+ ('F2Dot14', 'scale', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ]),
+ # PaintVarScaleUniformAroundCenter
+ ('PaintFormat23', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 23'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniformAroundCenter table) to Paint subtable.'),
+ ('F2Dot14', 'scale', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintRotate
+ ('PaintFormat24', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 24'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintRotate table) to Paint subtable.'),
+ ('Angle', 'angle', None, None, ''),
+ ]),
+ # PaintVarRotate
+ ('PaintFormat25', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 25'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
+ ('Angle', 'angle', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintRotateAroundCenter
+ ('PaintFormat26', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 26'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintRotateAroundCenter table) to Paint subtable.'),
+ ('Angle', 'angle', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ]),
+ # PaintVarRotateAroundCenter
+ ('PaintFormat27', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 27'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotateAroundCenter table) to Paint subtable.'),
+ ('Angle', 'angle', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintSkew
+ ('PaintFormat28', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 28'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintSkew table) to Paint subtable.'),
+ ('Angle', 'xSkewAngle', None, None, ''),
+ ('Angle', 'ySkewAngle', None, None, ''),
+ ]),
+ # PaintVarSkew
+ ('PaintFormat29', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 29'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
+ ('Angle', 'xSkewAngle', None, None, ''),
+ ('Angle', 'ySkewAngle', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintSkewAroundCenter
+ ('PaintFormat30', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 30'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintSkewAroundCenter table) to Paint subtable.'),
+ ('Angle', 'xSkewAngle', None, None, ''),
+ ('Angle', 'ySkewAngle', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ]),
+ # PaintVarSkewAroundCenter
+ ('PaintFormat31', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 31'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkewAroundCenter table) to Paint subtable.'),
+ ('Angle', 'xSkewAngle', None, None, ''),
+ ('Angle', 'ySkewAngle', None, None, ''),
+ ('int16', 'centerX', None, None, ''),
+ ('int16', 'centerY', None, None, ''),
+ ('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
+ ]),
+
+ # PaintComposite
+ ('PaintFormat32', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 32'),
('LOffset24To(Paint)', 'SourcePaint', None, None, 'Offset (from beginning of PaintComposite table) to source Paint subtable.'),
('CompositeMode', 'CompositeMode', None, None, 'A CompositeMode enumeration value.'),
('LOffset24To(Paint)', 'BackdropPaint', None, None, 'Offset (from beginning of PaintComposite table) to backdrop Paint subtable.'),
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 85befb3b..fbd9db7b 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -5,12 +5,12 @@ OpenType subtables.
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
+import copy
from enum import IntEnum
import itertools
-from collections import namedtuple
-from fontTools.misc.py23 import bytesjoin
+from collections import defaultdict, namedtuple
from fontTools.misc.roundTools import otRound
-from fontTools.misc.textTools import pad, safeEval
+from fontTools.misc.textTools import bytesjoin, pad, safeEval
from .otBase import (
BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference,
getFormatSwitchingBaseTableClass,
@@ -425,8 +425,7 @@ class InsertionMorphAction(AATAction):
return []
reader = actionReader.getSubReader(
actionReader.pos + index * 2)
- return [font.getGlyphName(glyphID)
- for glyphID in reader.readUShortArray(count)]
+ return font.getGlyphNameMany(reader.readUShortArray(count))
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
@@ -521,12 +520,10 @@ class Coverage(FormatSwitchingBaseTable):
def postRead(self, rawTable, font):
if self.Format == 1:
- # TODO only allow glyphs that are valid?
self.glyphs = rawTable["GlyphArray"]
elif self.Format == 2:
glyphs = self.glyphs = []
ranges = rawTable["RangeRecord"]
- glyphOrder = font.getGlyphOrder()
# Some SIL fonts have coverage entries that don't have sorted
# StartCoverageIndex. If it is so, fixup and warn. We undo
# this when writing font out.
@@ -536,25 +533,11 @@ class Coverage(FormatSwitchingBaseTable):
ranges = sorted_ranges
del sorted_ranges
for r in ranges:
- assert r.StartCoverageIndex == len(glyphs), \
- (r.StartCoverageIndex, len(glyphs))
start = r.Start
end = r.End
- try:
- startID = font.getGlyphID(start, requireReal=True)
- except KeyError:
- log.warning("Coverage table has start glyph ID out of range: %s.", start)
- continue
- try:
- endID = font.getGlyphID(end, requireReal=True) + 1
- except KeyError:
- # Apparently some tools use 65535 to "match all" the range
- if end != 'glyph65535':
- log.warning("Coverage table has end glyph ID out of range: %s.", end)
- # NOTE: We clobber out-of-range things here. There are legit uses for those,
- # but none that we have seen in the wild.
- endID = len(glyphOrder)
- glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID))
+ startID = font.getGlyphID(start)
+ endID = font.getGlyphID(end) + 1
+ glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
else:
self.glyphs = []
log.warning("Unknown Coverage format: %s", self.Format)
@@ -566,10 +549,9 @@ class Coverage(FormatSwitchingBaseTable):
glyphs = self.glyphs = []
format = 1
rawTable = {"GlyphArray": glyphs}
- getGlyphID = font.getGlyphID
if glyphs:
# find out whether Format 2 is more compact or not
- glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ]
+ glyphIDs = font.getGlyphIDMany(glyphs)
brokenOrder = sorted(glyphIDs) != glyphIDs
last = glyphIDs[0]
@@ -618,32 +600,18 @@ class Coverage(FormatSwitchingBaseTable):
glyphs.append(attrs["value"])
-class VarIdxMap(BaseTable):
+class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
def populateDefaults(self, propagator=None):
if not hasattr(self, 'mapping'):
- self.mapping = {}
+ self.mapping = []
def postRead(self, rawTable, font):
assert (rawTable['EntryFormat'] & 0xFFC0) == 0
- glyphOrder = font.getGlyphOrder()
- mapList = rawTable['mapping']
- mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
- self.mapping = dict(zip(glyphOrder, mapList))
-
- def preWrite(self, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = self.mapping = {}
-
- glyphOrder = font.getGlyphOrder()
- mapping = [mapping[g] for g in glyphOrder]
- while len(mapping) > 1 and mapping[-2] == mapping[-1]:
- del mapping[-1]
-
- rawTable = { 'mapping': mapping }
- rawTable['MappingCount'] = len(mapping)
+ self.mapping = rawTable['mapping']
+ @staticmethod
+ def getEntryFormat(mapping):
ored = 0
for idx in mapping:
ored |= idx
@@ -666,9 +634,65 @@ class VarIdxMap(BaseTable):
else:
entrySize = 4
- entryFormat = ((entrySize - 1) << 4) | (innerBits - 1)
+ return ((entrySize - 1) << 4) | (innerBits - 1)
+
+ def preWrite(self, font):
+ mapping = getattr(self, "mapping", None)
+ if mapping is None:
+ mapping = self.mapping = []
+ self.Format = 1 if len(mapping) > 0xFFFF else 0
+ rawTable = self.__dict__.copy()
+ rawTable['MappingCount'] = len(mapping)
+ rawTable['EntryFormat'] = self.getEntryFormat(mapping)
+ return rawTable
+
+ def toXML2(self, xmlWriter, font):
+ for i, value in enumerate(getattr(self, "mapping", [])):
+ attrs = (
+ ('index', i),
+ ('outer', value >> 16),
+ ('inner', value & 0xFFFF),
+ )
+ xmlWriter.simpletag("Map", attrs)
+ xmlWriter.newline()
+
+ def fromXML(self, name, attrs, content, font):
+ mapping = getattr(self, "mapping", None)
+ if mapping is None:
+ self.mapping = mapping = []
+ index = safeEval(attrs['index'])
+ outer = safeEval(attrs['outer'])
+ inner = safeEval(attrs['inner'])
+ assert inner <= 0xFFFF
+ mapping.insert(index, (outer << 16) | inner)
+
+
+class VarIdxMap(BaseTable):
+
+ def populateDefaults(self, propagator=None):
+ if not hasattr(self, 'mapping'):
+ self.mapping = {}
+
+ def postRead(self, rawTable, font):
+ assert (rawTable['EntryFormat'] & 0xFFC0) == 0
+ glyphOrder = font.getGlyphOrder()
+ mapList = rawTable['mapping']
+ mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
+ self.mapping = dict(zip(glyphOrder, mapList))
+
+ def preWrite(self, font):
+ mapping = getattr(self, "mapping", None)
+ if mapping is None:
+ mapping = self.mapping = {}
+
+ glyphOrder = font.getGlyphOrder()
+ mapping = [mapping[g] for g in glyphOrder]
+ while len(mapping) > 1 and mapping[-2] == mapping[-1]:
+ del mapping[-1]
- rawTable['EntryFormat'] = entryFormat
+ rawTable = {'mapping': mapping}
+ rawTable['MappingCount'] = len(mapping)
+ rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping)
return rawTable
def toXML2(self, xmlWriter, font):
@@ -726,9 +750,9 @@ class SingleSubst(FormatSwitchingBaseTable):
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
if self.Format == 1:
delta = rawTable["DeltaGlyphID"]
- inputGIDS = [ font.getGlyphID(name) for name in input ]
+ inputGIDS = font.getGlyphIDMany(input)
outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ]
- outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ]
+ outNames = font.getGlyphNameMany(outGIDS)
for inp, out in zip(input, outNames):
mapping[inp] = out
elif self.Format == 2:
@@ -882,51 +906,30 @@ class ClassDef(FormatSwitchingBaseTable):
def postRead(self, rawTable, font):
classDefs = {}
- glyphOrder = font.getGlyphOrder()
if self.Format == 1:
start = rawTable["StartGlyph"]
classList = rawTable["ClassValueArray"]
- try:
- startID = font.getGlyphID(start, requireReal=True)
- except KeyError:
- log.warning("ClassDef table has start glyph ID out of range: %s.", start)
- startID = len(glyphOrder)
+ startID = font.getGlyphID(start)
endID = startID + len(classList)
- if endID > len(glyphOrder):
- log.warning("ClassDef table has entries for out of range glyph IDs: %s,%s.",
- start, len(classList))
- # NOTE: We clobber out-of-range things here. There are legit uses for those,
- # but none that we have seen in the wild.
- endID = len(glyphOrder)
-
- for glyphID, cls in zip(range(startID, endID), classList):
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
+ for glyphName, cls in zip(glyphNames, classList):
if cls:
- classDefs[glyphOrder[glyphID]] = cls
+ classDefs[glyphName] = cls
elif self.Format == 2:
records = rawTable["ClassRangeRecord"]
for rec in records:
- start = rec.Start
- end = rec.End
cls = rec.Class
- try:
- startID = font.getGlyphID(start, requireReal=True)
- except KeyError:
- log.warning("ClassDef table has start glyph ID out of range: %s.", start)
+ if not cls:
continue
- try:
- endID = font.getGlyphID(end, requireReal=True) + 1
- except KeyError:
- # Apparently some tools use 65535 to "match all" the range
- if end != 'glyph65535':
- log.warning("ClassDef table has end glyph ID out of range: %s.", end)
- # NOTE: We clobber out-of-range things here. There are legit uses for those,
- # but none that we have seen in the wild.
- endID = len(glyphOrder)
- for glyphID in range(startID, endID):
- if cls:
- classDefs[glyphOrder[glyphID]] = cls
+ start = rec.Start
+ end = rec.End
+ startID = font.getGlyphID(start)
+ endID = font.getGlyphID(end) + 1
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
+ for glyphName in glyphNames:
+ classDefs[glyphName] = cls
else:
log.warning("Unknown ClassDef format: %s", self.Format)
self.classDefs = classDefs
@@ -1179,7 +1182,6 @@ class COLR(BaseTable):
if conv.name != "LayerRecordCount":
subReader.advance(conv.staticSize)
continue
- conv = self.getConverterByName("LayerRecordCount")
reader[conv.name] = conv.read(subReader, font, tableDict={})
break
else:
@@ -1245,51 +1247,176 @@ class BaseGlyphRecordArray(BaseTable):
return self.__dict__.copy()
-class BaseGlyphV1List(BaseTable):
+class BaseGlyphList(BaseTable):
def preWrite(self, font):
- self.BaseGlyphV1Record = sorted(
- self.BaseGlyphV1Record,
+ self.BaseGlyphPaintRecord = sorted(
+ self.BaseGlyphPaintRecord,
key=lambda rec: font.getGlyphID(rec.BaseGlyph)
)
return self.__dict__.copy()
+class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
-class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])):
- __slots__ = ()
+ def as_tuple(self):
+ return tuple(getattr(self, conv.name) for conv in self.getConverters())
- _value_mapper = None
+ def __repr__(self):
+ return f"{self.__class__.__name__}{self.as_tuple()}"
- def __new__(cls, value, varIdx=0):
- return super().__new__(
- cls,
- cls._value_mapper(value) if cls._value_mapper else value,
- varIdx
- )
- @classmethod
- def _make(cls, iterable):
- if cls._value_mapper:
- it = iter(iterable)
- try:
- value = next(it)
- except StopIteration:
- pass
- else:
- value = cls._value_mapper(value)
- iterable = itertools.chain((value,), it)
- return super()._make(iterable)
+class ClipList(getFormatSwitchingBaseTableClass("uint8")):
+ def populateDefaults(self, propagator=None):
+ if not hasattr(self, "clips"):
+ self.clips = {}
-class VariableFloat(VariableValue):
- __slots__ = ()
- _value_mapper = float
+ def postRead(self, rawTable, font):
+ clips = {}
+ glyphOrder = font.getGlyphOrder()
+ for i, rec in enumerate(rawTable["ClipRecord"]):
+ if rec.StartGlyphID > rec.EndGlyphID:
+ log.warning(
+ "invalid ClipRecord[%i].StartGlyphID (%i) > "
+ "EndGlyphID (%i); skipped",
+ i,
+ rec.StartGlyphID,
+ rec.EndGlyphID,
+ )
+ continue
+ redefinedGlyphs = []
+ missingGlyphs = []
+ for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
+ try:
+ glyph = glyphOrder[glyphID]
+ except IndexError:
+ missingGlyphs.append(glyphID)
+ continue
+ if glyph not in clips:
+ clips[glyph] = copy.copy(rec.ClipBox)
+ else:
+ redefinedGlyphs.append(glyphID)
+ if redefinedGlyphs:
+ log.warning(
+ "ClipRecord[%i] overlaps previous records; "
+ "ignoring redefined clip boxes for the "
+ "following glyph ID range: [%i-%i]",
+ i,
+ min(redefinedGlyphs),
+ max(redefinedGlyphs),
+ )
+ if missingGlyphs:
+ log.warning(
+ "ClipRecord[%i] range references missing "
+ "glyph IDs: [%i-%i]",
+ i,
+ min(missingGlyphs),
+ max(missingGlyphs),
+ )
+ self.clips = clips
+
+ def groups(self):
+ glyphsByClip = defaultdict(list)
+ uniqueClips = {}
+ for glyphName, clipBox in self.clips.items():
+ key = clipBox.as_tuple()
+ glyphsByClip[key].append(glyphName)
+ if key not in uniqueClips:
+ uniqueClips[key] = clipBox
+ return {
+ frozenset(glyphs): uniqueClips[key]
+ for key, glyphs in glyphsByClip.items()
+ }
+
+ def preWrite(self, font):
+ if not hasattr(self, "clips"):
+ self.clips = {}
+ clipBoxRanges = {}
+ glyphMap = font.getReverseGlyphMap()
+ for glyphs, clipBox in self.groups().items():
+ glyphIDs = sorted(
+ glyphMap[glyphName] for glyphName in glyphs
+ if glyphName in glyphMap
+ )
+ if not glyphIDs:
+ continue
+ last = glyphIDs[0]
+ ranges = [[last]]
+ for glyphID in glyphIDs[1:]:
+ if glyphID != last + 1:
+ ranges[-1].append(last)
+ ranges.append([glyphID])
+ last = glyphID
+ ranges[-1].append(last)
+ for start, end in ranges:
+ assert (start, end) not in clipBoxRanges
+ clipBoxRanges[(start, end)] = clipBox
+
+ clipRecords = []
+ for (start, end), clipBox in sorted(clipBoxRanges.items()):
+ record = ClipRecord()
+ record.StartGlyphID = start
+ record.EndGlyphID = end
+ record.ClipBox = clipBox
+ clipRecords.append(record)
+ rawTable = {
+ "ClipCount": len(clipRecords),
+ "ClipRecord": clipRecords,
+ }
+ return rawTable
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
+ tableName = name if name else self.__class__.__name__
+ if attrs is None:
+ attrs = []
+ if hasattr(self, "Format"):
+ attrs.append(("Format", self.Format))
+ xmlWriter.begintag(tableName, attrs)
+ xmlWriter.newline()
+ # sort clips alphabetically to ensure deterministic XML dump
+ for glyphs, clipBox in sorted(
+ self.groups().items(), key=lambda item: min(item[0])
+ ):
+ xmlWriter.begintag("Clip")
+ xmlWriter.newline()
+ for glyphName in sorted(glyphs):
+ xmlWriter.simpletag("Glyph", value=glyphName)
+ xmlWriter.newline()
+ xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
+ xmlWriter.newline()
+ clipBox.toXML2(xmlWriter, font)
+ xmlWriter.endtag("ClipBox")
+ xmlWriter.newline()
+ xmlWriter.endtag("Clip")
+ xmlWriter.newline()
+ xmlWriter.endtag(tableName)
+ xmlWriter.newline()
-class VariableInt(VariableValue):
- __slots__ = ()
- _value_mapper = otRound
+ def fromXML(self, name, attrs, content, font):
+ clips = getattr(self, "clips", None)
+ if clips is None:
+ self.clips = clips = {}
+ assert name == "Clip"
+ glyphs = []
+ clipBox = None
+ for elem in content:
+ if not isinstance(elem, tuple):
+ continue
+ name, attrs, content = elem
+ if name == "Glyph":
+ glyphs.append(attrs["value"])
+ elif name == "ClipBox":
+ clipBox = ClipBox()
+ clipBox.Format = safeEval(attrs["Format"])
+ for elem in content:
+ if not isinstance(elem, tuple):
+ continue
+ name, attrs, content = elem
+ clipBox.fromXML(name, attrs, content, font)
+ if clipBox:
+ for glyphName in glyphs:
+ clips[glyphName] = clipBox
class ExtendMode(IntEnum):
@@ -1313,21 +1440,22 @@ class CompositeMode(IntEnum):
SRC_ATOP = 9
DEST_ATOP = 10
XOR = 11
- SCREEN = 12
- OVERLAY = 13
- DARKEN = 14
- LIGHTEN = 15
- COLOR_DODGE = 16
- COLOR_BURN = 17
- HARD_LIGHT = 18
- SOFT_LIGHT = 19
- DIFFERENCE = 20
- EXCLUSION = 21
- MULTIPLY = 22
- HSL_HUE = 23
- HSL_SATURATION = 24
- HSL_COLOR = 25
- HSL_LUMINOSITY = 26
+ PLUS = 12
+ SCREEN = 13
+ OVERLAY = 14
+ DARKEN = 15
+ LIGHTEN = 16
+ COLOR_DODGE = 17
+ COLOR_BURN = 18
+ HARD_LIGHT = 19
+ SOFT_LIGHT = 20
+ DIFFERENCE = 21
+ EXCLUSION = 22
+ MULTIPLY = 23
+ HSL_HUE = 24
+ HSL_SATURATION = 25
+ HSL_COLOR = 26
+ HSL_LUMINOSITY = 27
class PaintFormat(IntEnum):
@@ -1346,11 +1474,23 @@ class PaintFormat(IntEnum):
PaintVarTransform = 13
PaintTranslate = 14
PaintVarTranslate = 15
- PaintRotate = 16
- PaintVarRotate = 17
- PaintSkew = 18
- PaintVarSkew = 19
- PaintComposite = 20
+ PaintScale = 16
+ PaintVarScale = 17
+ PaintScaleAroundCenter = 18
+ PaintVarScaleAroundCenter = 19
+ PaintScaleUniform = 20
+ PaintVarScaleUniform = 21
+ PaintScaleUniformAroundCenter = 22
+ PaintVarScaleUniformAroundCenter = 23
+ PaintRotate = 24
+ PaintVarRotate = 25
+ PaintRotateAroundCenter = 26
+ PaintVarRotateAroundCenter = 27
+ PaintSkew = 28
+ PaintVarSkew = 29
+ PaintSkewAroundCenter = 30
+ PaintVarSkewAroundCenter = 31
+ PaintComposite = 32
class Paint(getFormatSwitchingBaseTableClass("uint8")):
@@ -1375,16 +1515,20 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
def getChildren(self, colr):
if self.Format == PaintFormat.PaintColrLayers:
- return colr.LayerV1List.Paint[
+ # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
+ layers = []
+ if colr.LayerList is not None:
+ layers = colr.LayerList.Paint
+ return layers[
self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers
]
if self.Format == PaintFormat.PaintColrGlyph:
- for record in colr.BaseGlyphV1List.BaseGlyphV1Record:
+ for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
if record.BaseGlyph == self.Glyph:
return [record.Paint]
else:
- raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List")
+ raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
children = []
for conv in self.getConverters():
@@ -1490,20 +1634,22 @@ def fixLookupOverFlows(ttf, overflowRecord):
return ok
lookup = lookups[lookupIndex]
- lookup.LookupType = extType
- for si in range(len(lookup.SubTable)):
- subTable = lookup.SubTable[si]
- extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
- extSubTable = extSubTableClass()
- extSubTable.Format = 1
- extSubTable.ExtSubTable = subTable
- lookup.SubTable[si] = extSubTable
+ for lookupIndex in range(lookupIndex, len(lookups)):
+ lookup = lookups[lookupIndex]
+ if lookup.LookupType != extType:
+ lookup.LookupType = extType
+ for si in range(len(lookup.SubTable)):
+ subTable = lookup.SubTable[si]
+ extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
+ extSubTable = extSubTableClass()
+ extSubTable.Format = 1
+ extSubTable.ExtSubTable = subTable
+ lookup.SubTable[si] = extSubTable
ok = 1
return ok
def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
- newSubTable.Format = oldSubTable.Format
oldMapping = sorted(oldSubTable.mapping.items())
oldLen = len(oldMapping)
@@ -1529,7 +1675,6 @@ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
- newSubTable.Format = oldSubTable.Format
if hasattr(oldSubTable, 'sortCoverageLast'):
newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
@@ -1559,7 +1704,6 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
- newSubTable.Format = oldSubTable.Format
oldLigs = sorted(oldSubTable.ligatures.items())
oldLen = len(oldLigs)
diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py
index a1dfa3c5..72377583 100644
--- a/Lib/fontTools/ttLib/tables/ttProgram.py
+++ b/Lib/fontTools/ttLib/tables/ttProgram.py
@@ -1,7 +1,6 @@
"""ttLib.tables.ttProgram.py -- Assembler/disassembler for TrueType bytecode programs."""
-from fontTools.misc.py23 import strjoin
-from fontTools.misc.textTools import num2binary, binary2num, readHex
+from fontTools.misc.textTools import num2binary, binary2num, readHex, strjoin
import array
from io import StringIO
import re
diff --git a/Lib/fontTools/ttLib/ttCollection.py b/Lib/fontTools/ttLib/ttCollection.py
index 3db4c8cd..f0922127 100644
--- a/Lib/fontTools/ttLib/ttCollection.py
+++ b/Lib/fontTools/ttLib/ttCollection.py
@@ -26,8 +26,10 @@ class TTCollection(object):
assert 'fontNumber' not in kwargs, kwargs
+ closeStream = False
if not hasattr(file, "read"):
file = open(file, "rb")
+ closeStream = True
tableCache = {} if shareTables else None
@@ -35,13 +37,21 @@ class TTCollection(object):
for i in range(header.numFonts):
font = TTFont(file, fontNumber=i, _tableCache=tableCache, **kwargs)
fonts.append(font)
-
+
+ # don't close file if lazy=True, as the TTFont hold a reference to the original
+ # file; the file will be closed once the TTFonts are closed in the
+ # TTCollection.close(). We still want to close the file if lazy is None or
+ # False, because in that case the TTFont no longer need the original file
+ # and we want to avoid 'ResourceWarning: unclosed file'.
+ if not kwargs.get("lazy") and closeStream:
+ file.close()
+
def __enter__(self):
return self
-
+
def __exit__(self, type, value, traceback):
self.close()
-
+
def close(self):
for font in self.fonts:
font.close()
@@ -76,7 +86,7 @@ class TTCollection(object):
final.write(file.getvalue())
file.close()
- def saveXML(self, fileOrPath, newlinestr=None, writeVersion=True, **kwargs):
+ def saveXML(self, fileOrPath, newlinestr="\n", writeVersion=True, **kwargs):
from fontTools.misc import xmlWriter
writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index 41a48751..3929e2f3 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -1,5 +1,5 @@
from fontTools.misc import xmlWriter
-from fontTools.misc.py23 import Tag, byteord, tostr
+from fontTools.misc.textTools import Tag, byteord, tostr
from fontTools.misc.loggingTools import deprecateArgument
from fontTools.ttLib import TTLibError
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
@@ -12,75 +12,84 @@ log = logging.getLogger(__name__)
class TTFont(object):
- """The main font object. It manages file input and output, and offers
- a convenient way of accessing tables.
- Tables will be only decompiled when necessary, ie. when they're actually
- accessed. This means that simple operations can be extremely fast.
+ """Represents a TrueType font.
+
+ The object manages file input and output, and offers a convenient way of
+ accessing tables. Tables will be only decompiled when necessary, ie. when
+ they're actually accessed. This means that simple operations can be extremely fast.
+
+ Example usage::
+
+ >> from fontTools import ttLib
+ >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file
+ >> tt['maxp'].numGlyphs
+ 242
+ >> tt['OS/2'].achVendID
+ 'B&H\000'
+ >> tt['head'].unitsPerEm
+ 2048
+
+ For details of the objects returned when accessing each table, see :ref:`tables`.
+ To add a table to the font, use the :py:func:`newTable` function::
+
+ >> os2 = newTable("OS/2")
+ >> os2.version = 4
+ >> # set other attributes
+ >> font["OS/2"] = os2
+
+ TrueType fonts can also be serialized to and from XML format (see also the
+ :ref:`ttx` binary)::
+
+ >> tt.saveXML("afont.ttx")
+ Dumping 'LTSH' table...
+ Dumping 'OS/2' table...
+ [...]
+
+ >> tt2 = ttLib.TTFont() # Create a new font object
+ >> tt2.importXML("afont.ttx")
+ >> tt2['maxp'].numGlyphs
+ 242
+
+ The TTFont object may be used as a context manager; this will cause the file
+ reader to be closed after the context ``with`` block is exited::
+
+ with TTFont(filename) as f:
+ # Do stuff
+
+ Args:
+ file: When reading a font from disk, either a pathname pointing to a file,
+ or a readable file object.
+ res_name_or_index: If running on a Macintosh, either a sfnt resource name or
+ an sfnt resource index number. If the index number is zero, TTLib will
+ autodetect whether the file is a flat file or a suitcase. (If it is a suitcase,
+ only the first 'sfnt' resource will be read.)
+ sfntVersion (str): When constructing a font object from scratch, sets the four-byte
+ sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create
+ an OpenType file, use ``OTTO``.
+ flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2
+ file.
+ checkChecksums (int): How checksum data should be treated. Default is 0
+ (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to
+ raise an exception if any wrong checksums are found.
+ recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``,
+ ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save.
+ Also compiles the glyphs on importing, which saves memory consumption and
+ time.
+ ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation
+ will be ignored, and the binary data will be returned for those tables instead.
+ recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in
+ the ``head`` table on save.
+ fontNumber (int): The index of the font in a TrueType Collection file.
+ lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon
+ access only. If it is set to False, many data structures are loaded immediately.
+ The default is ``lazy=None`` which is somewhere in between.
"""
def __init__(self, file=None, res_name_or_index=None,
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
- verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
+ verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False,
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
_tableCache=None):
-
- """The constructor can be called with a few different arguments.
- When reading a font from disk, 'file' should be either a pathname
- pointing to a file, or a readable file object.
-
- It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
- resource name or an sfnt resource index number or zero. The latter
- case will cause TTLib to autodetect whether the file is a flat file
- or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
- will be read!)
-
- The 'checkChecksums' argument is used to specify how sfnt
- checksums are treated upon reading a file from disk:
- 0: don't check (default)
- 1: check, print warnings if a wrong checksum is found
- 2: check, raise an exception if a wrong checksum is found.
-
- The TTFont constructor can also be called without a 'file'
- argument: this is the way to create a new empty font.
- In this case you can optionally supply the 'sfntVersion' argument,
- and a 'flavor' which can be None, 'woff', or 'woff2'.
-
- If the recalcBBoxes argument is false, a number of things will *not*
- be recalculated upon save/compile:
- 1) 'glyf' glyph bounding boxes
- 2) 'CFF ' font bounding box
- 3) 'head' font bounding box
- 4) 'hhea' min/max values
- 5) 'vhea' min/max values
- (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
- Additionally, upon importing an TTX file, this option cause glyphs
- to be compiled right away. This should reduce memory consumption
- greatly, and therefore should have some impact on the time needed
- to parse/compile large fonts.
-
- If the recalcTimestamp argument is false, the modified timestamp in the
- 'head' table will *not* be recalculated upon save/compile.
-
- If the allowVID argument is set to true, then virtual GID's are
- supported. Asking for a glyph ID with a glyph name or GID that is not in
- the font will return a virtual GID. This is valid for GSUB and cmap
- tables. For SING glyphlets, the cmap table is used to specify Unicode
- values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
- and does not exist in the font, or the glyphname has the form glyphN
- and does not exist in the font, then N is used as the virtual GID.
- Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new
- virtual GIDs, the next is one less than the previous.
-
- If ignoreDecompileErrors is set to True, exceptions raised in
- individual tables during decompilation will be ignored, falling
- back to the DefaultTable implementation, which simply keeps the
- binary data.
-
- If lazy is set to True, many data structures are loaded lazily, upon
- access only. If it is set to False, many data structures are loaded
- immediately. The default is lazy=None which is somewhere in between.
- """
-
for name in ("verbose", "quiet"):
val = locals().get(name)
if val is not None:
@@ -92,12 +101,6 @@ class TTFont(object):
self.recalcTimestamp = recalcTimestamp
self.tables = {}
self.reader = None
-
- # Permit the user to reference glyphs that are not int the font.
- self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value.
- self.reverseVIDDict = {}
- self.VIDDict = {}
- self.allowVID = allowVID
self.ignoreDecompileErrors = ignoreDecompileErrors
if not file:
@@ -154,9 +157,15 @@ class TTFont(object):
self.reader.close()
def save(self, file, reorderTables=True):
- """Save the font to disk. Similarly to the constructor,
- the 'file' argument can be either a pathname or a writable
- file object.
+ """Save the font to disk.
+
+ Args:
+ file: Similarly to the constructor, can be either a pathname or a writable
+ file object.
+ reorderTables (Option[bool]): If true (the default), reorder the tables,
+ sorting them by tag (recommended by the OpenType specification). If
+ false, retain the original font order. If None, reorder by table
+ dependency (fastest).
"""
if not hasattr(file, "write"):
if self.lazy and self.reader.file.name == file:
@@ -215,7 +224,7 @@ class TTFont(object):
return writer.reordersTables()
- def saveXML(self, fileOrPath, newlinestr=None, **kwargs):
+ def saveXML(self, fileOrPath, newlinestr="\n", **kwargs):
"""Export the font as TTX (an XML-based text file), or as a series of text
files when splitTables is true. In the latter case, the 'fileOrPath'
argument should be a path to a directory.
@@ -336,11 +345,15 @@ class TTFont(object):
reader.read()
def isLoaded(self, tag):
- """Return true if the table identified by 'tag' has been
+ """Return true if the table identified by ``tag`` has been
decompiled and loaded into memory."""
return tag in self.tables
def has_key(self, tag):
+ """Test if the table identified by ``tag`` is present in the font.
+
+ As well as this method, ``tag in font`` can also be used to determine the
+ presence of the table."""
if self.isLoaded(tag):
return True
elif self.reader and tag in self.reader:
@@ -353,6 +366,7 @@ class TTFont(object):
__contains__ = has_key
def keys(self):
+ """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
keys = list(self.tables.keys())
if self.reader:
for key in list(self.reader.keys()):
@@ -364,6 +378,14 @@ class TTFont(object):
keys = sortedTagList(keys)
return ["GlyphOrder"] + keys
+ def ensureDecompiled(self):
+ """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
+ for tag in self.keys():
+ table = self[tag]
+ if self.lazy is not False and hasattr(table, "ensureDecompiled"):
+ table.ensureDecompiled()
+ self.lazy = False
+
def __len__(self):
return len(list(self.keys()))
@@ -422,15 +444,26 @@ class TTFont(object):
del self.reader[tag]
def get(self, tag, default=None):
+ """Returns the table if it exists or (optionally) a default if it doesn't."""
try:
return self[tag]
except KeyError:
return default
def setGlyphOrder(self, glyphOrder):
+ """Set the glyph order
+
+ Args:
+ glyphOrder ([str]): List of glyph names in order.
+ """
self.glyphOrder = glyphOrder
+ if hasattr(self, '_reverseGlyphOrderDict'):
+ del self._reverseGlyphOrderDict
+ if self.isLoaded("glyf"):
+ self["glyf"].setGlyphOrder(glyphOrder)
def getGlyphOrder(self):
+ """Returns a list of glyph names ordered by their position in the font."""
try:
return self.glyphOrder
except AttributeError:
@@ -544,78 +577,55 @@ class TTFont(object):
from fontTools.misc import textTools
return textTools.caselessSort(self.getGlyphOrder())
- def getGlyphName(self, glyphID, requireReal=False):
+ def getGlyphName(self, glyphID):
+ """Returns the name for the glyph with the given ID.
+
+ If no name is available, synthesises one with the form ``glyphXXXXX``` where
+ ```XXXXX`` is the zero-padded glyph ID.
+ """
try:
return self.getGlyphOrder()[glyphID]
except IndexError:
- if requireReal or not self.allowVID:
- # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in
- # the cmap table than there are glyphs. I don't think it's legal...
- return "glyph%.5d" % glyphID
- else:
- # user intends virtual GID support
+ return "glyph%.5d" % glyphID
+
+ def getGlyphNameMany(self, lst):
+ """Converts a list of glyph IDs into a list of glyph names."""
+ glyphOrder = self.getGlyphOrder();
+ cnt = len(glyphOrder)
+ return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid
+ for gid in lst]
+
+ def getGlyphID(self, glyphName):
+ """Returns the ID of the glyph with the given name."""
+ try:
+ return self.getReverseGlyphMap()[glyphName]
+ except KeyError:
+ if glyphName[:5] == "glyph":
try:
- glyphName = self.VIDDict[glyphID]
- except KeyError:
- glyphName ="glyph%.5d" % glyphID
- self.last_vid = min(glyphID, self.last_vid )
- self.reverseVIDDict[glyphName] = glyphID
- self.VIDDict[glyphID] = glyphName
- return glyphName
-
- def getGlyphID(self, glyphName, requireReal=False):
- if not hasattr(self, "_reverseGlyphOrderDict"):
- self._buildReverseGlyphOrderDict()
- glyphOrder = self.getGlyphOrder()
- d = self._reverseGlyphOrderDict
- if glyphName not in d:
- if glyphName in glyphOrder:
- self._buildReverseGlyphOrderDict()
- return self.getGlyphID(glyphName)
- else:
- if requireReal:
+ return int(glyphName[5:])
+ except (NameError, ValueError):
raise KeyError(glyphName)
- elif not self.allowVID:
- # Handle glyphXXX only
- if glyphName[:5] == "glyph":
- try:
- return int(glyphName[5:])
- except (NameError, ValueError):
- raise KeyError(glyphName)
- else:
- # user intends virtual GID support
- try:
- glyphID = self.reverseVIDDict[glyphName]
- except KeyError:
- # if name is in glyphXXX format, use the specified name.
- if glyphName[:5] == "glyph":
- try:
- glyphID = int(glyphName[5:])
- except (NameError, ValueError):
- glyphID = None
- if glyphID is None:
- glyphID = self.last_vid -1
- self.last_vid = glyphID
- self.reverseVIDDict[glyphName] = glyphID
- self.VIDDict[glyphID] = glyphName
- return glyphID
-
- glyphID = d[glyphName]
- if glyphName != glyphOrder[glyphID]:
- self._buildReverseGlyphOrderDict()
- return self.getGlyphID(glyphName)
- return glyphID
+
+ def getGlyphIDMany(self, lst):
+ """Converts a list of glyph names into a list of glyph IDs."""
+ d = self.getReverseGlyphMap()
+ try:
+ return [d[glyphName] for glyphName in lst]
+ except KeyError:
+ getGlyphID = self.getGlyphID
+ return [getGlyphID(glyphName) for glyphName in lst]
def getReverseGlyphMap(self, rebuild=False):
+ """Returns a mapping of glyph names to glyph IDs."""
if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
self._buildReverseGlyphOrderDict()
return self._reverseGlyphOrderDict
def _buildReverseGlyphOrderDict(self):
self._reverseGlyphOrderDict = d = {}
- glyphOrder = self.getGlyphOrder()
- for glyphID in range(len(glyphOrder)):
- d[glyphOrder[glyphID]] = glyphID
+ for glyphID,glyphName in enumerate(self.getGlyphOrder()):
+ d[glyphName] = glyphID
+ return d
def _writeTable(self, tag, writer, done, tableCache=None):
"""Internal helper function for self.save(). Keeps track of
@@ -644,7 +654,11 @@ class TTFont(object):
tableCache[(Tag(tag), tabledata)] = writer[tag]
def getTableData(self, tag):
- """Returns raw table data, whether compiled or directly read from disk.
+ """Returns the binary representation of a table.
+
+ If the table is currently loaded and in memory, the data is compiled to
+ binary and returned; if it is not currently loaded, the binary data is
+ read from the font file and returned.
"""
tag = Tag(tag)
if self.isLoaded(tag):
@@ -688,9 +702,18 @@ class TTFont(object):
or None, if no unicode cmap subtable is available.
By default it will search for the following (platformID, platEncID)
- pairs:
- (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0)
- This can be customized via the cmapPreferences argument.
+ pairs::
+
+ (3, 10),
+ (0, 6),
+ (0, 4),
+ (3, 1),
+ (0, 3),
+ (0, 2),
+ (0, 1),
+ (0, 0)
+
+ This can be customized via the ``cmapPreferences`` argument.
"""
return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
@@ -820,9 +843,9 @@ class GlyphOrder(object):
def fromXML(self, name, attrs, content, ttFont):
if not hasattr(self, "glyphOrder"):
self.glyphOrder = []
- ttFont.setGlyphOrder(self.glyphOrder)
if name == "GlyphID":
self.glyphOrder.append(attrs["name"])
+ ttFont.setGlyphOrder(self.glyphOrder)
def getTableModule(tag):
@@ -854,12 +877,13 @@ _customTableRegistry = {}
def registerCustomTableClass(tag, moduleName, className=None):
"""Register a custom packer/unpacker class for a table.
+
The 'moduleName' must be an importable module. If no 'className'
is given, it is derived from the tag, for example it will be
- table_C_U_S_T_ for a 'CUST' tag.
+ ``table_C_U_S_T_`` for a 'CUST' tag.
The registered table class should be a subclass of
- fontTools.ttLib.tables.DefaultTable.DefaultTable
+ :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
"""
if className is None:
className = "table_" + tagToIdentifier(tag)
@@ -930,10 +954,14 @@ def tagToIdentifier(tag):
letters get an underscore after the letter. Trailing spaces are
trimmed. Illegal characters are escaped as two hex bytes. If the
result starts with a number (as the result of a hex escape), an
- extra underscore is prepended. Examples:
- 'glyf' -> '_g_l_y_f'
- 'cvt ' -> '_c_v_t'
- 'OS/2' -> 'O_S_2f_2'
+ extra underscore is prepended. Examples::
+
+ >>> tagToIdentifier('glyf')
+ '_g_l_y_f'
+ >>> tagToIdentifier('cvt ')
+ '_c_v_t'
+ >>> tagToIdentifier('OS/2')
+ 'O_S_2f_2'
"""
import re
tag = Tag(tag)
diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py
index cc58afa5..b66661ab 100644
--- a/Lib/fontTools/ttLib/woff2.py
+++ b/Lib/fontTools/ttLib/woff2.py
@@ -1,4 +1,3 @@
-from fontTools.misc.py23 import Tag, bytechr, byteord, bytesjoin
from io import BytesIO
import sys
import array
@@ -6,7 +5,7 @@ import struct
from collections import OrderedDict
from fontTools.misc import sstruct
from fontTools.misc.arrayTools import calcIntBounds
-from fontTools.misc.textTools import pad
+from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad
from fontTools.ttLib import (TTFont, TTLibError, getTableModule, getTableClass,
getSearchRange)
from fontTools.ttLib.sfnt import (SFNTReader, SFNTWriter, DirectoryEntry,