aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2020-09-22 18:20:58 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-09-22 18:20:58 +0000
commita263eb264a3f5c23193c4f7b310ce99e9bedca7d (patch)
tree88bae7c0d397c0ef0353879c3be9ead6d38fd7e0
parent88f41f40cad9eafc09ce203d968fc2ef1c6c7d13 (diff)
parent2d54a34c1c619455a1b42dc127d334bb43ad15aa (diff)
downloadfonttools-a263eb264a3f5c23193c4f7b310ce99e9bedca7d.tar.gz
Upgrade fonttools to 4.15.0 am: c9804561ed am: 6baeafbb6d am: 4f502262a1 am: 2d54a34c1c
Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/1433028 Change-Id: Ia36ca2d20fbfa25340019540a86c1e46b49b98f8
-rw-r--r--.travis.yml3
-rw-r--r--Doc/docs-requirements.txt2
-rw-r--r--Doc/source/mtiLib.rst18
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/feaLib/__main__.py10
-rw-r--r--Lib/fontTools/feaLib/builder.py42
-rw-r--r--Lib/fontTools/feaLib/lookupDebugInfo.py10
-rw-r--r--Lib/fontTools/misc/plistlib/__init__.py (renamed from Lib/fontTools/misc/plistlib.py)299
-rw-r--r--Lib/fontTools/misc/plistlib/py.typed0
-rw-r--r--Lib/fontTools/mtiLib/__init__.py23
-rw-r--r--Lib/fontTools/subset/__init__.py2
-rw-r--r--Lib/fontTools/ttLib/sfnt.py2
-rw-r--r--Lib/fontTools/ttLib/tables/D__e_b_g.py17
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py39
-rw-r--r--Lib/fontTools/ttLib/ttFont.py46
-rw-r--r--Lib/fontTools/ttLib/woff2.py2
-rw-r--r--Lib/fontTools/varLib/interpolatable.py21
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO28
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt7
-rw-r--r--MANIFEST.in2
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst16
-rw-r--r--PKG-INFO28
-rw-r--r--Tests/feaLib/builder_test.py13
-rw-r--r--Tests/feaLib/data/lookup-debug.ttx80
-rw-r--r--Tests/ttLib/ttFont_test.py48
-rw-r--r--dev-requirements.txt1
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py2
-rw-r--r--tox.ini9
31 files changed, 618 insertions, 166 deletions
diff --git a/.travis.yml b/.travis.yml
index 7ff7e316..389d3372 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,6 +20,9 @@ matrix:
include:
- python: 3.6
env:
+ - TOXENV=mypy
+ - python: 3.6
+ env:
- TOXENV=py36-cov,package_readme
- BUILD_DIST=true
- python: 3.7
diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt
index 4ea38275..b877d4e4 100644
--- a/Doc/docs-requirements.txt
+++ b/Doc/docs-requirements.txt
@@ -1,3 +1,3 @@
sphinx==3.2.1
sphinx_rtd_theme==0.5.0
-reportlab==3.5.47
+reportlab==3.5.49
diff --git a/Doc/source/mtiLib.rst b/Doc/source/mtiLib.rst
index fdb35a67..1bf74e18 100644
--- a/Doc/source/mtiLib.rst
+++ b/Doc/source/mtiLib.rst
@@ -1,8 +1,14 @@
-######
-mtiLib
-######
+###########################################
+mtiLib: Read Monotype FontDame source files
+###########################################
+
+FontTools provides support for reading the OpenType layout tables produced by
+Monotype's FontDame and Font Chef font editors. These tables are written in a
+simple textual format. The ``mtiLib`` library parses these text files and creates
+table objects representing their contents.
+
+Additionally, ``fonttools mtiLib`` will convert a text file to TTX XML.
+
.. automodule:: fontTools.mtiLib
- :inherited-members:
- :members:
- :undoc-members:
+ :members: build, main
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 104792a2..a86bbae3 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.14.0"
+version = __version__ = "4.15.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/feaLib/__main__.py b/Lib/fontTools/feaLib/__main__.py
index 9c682fc1..348cf0a9 100644
--- a/Lib/fontTools/feaLib/__main__.py
+++ b/Lib/fontTools/feaLib/__main__.py
@@ -39,6 +39,12 @@ def main(args=None):
help="Specify the table(s) to be built.",
)
parser.add_argument(
+ "-d",
+ "--debug",
+ action="store_true",
+ help="Add source-level debugging information to font.",
+ )
+ parser.add_argument(
"-v",
"--verbose",
help="increase the logger verbosity. Multiple -v " "options are allowed.",
@@ -58,7 +64,9 @@ def main(args=None):
font = TTFont(options.input_font)
try:
- addOpenTypeFeatures(font, options.input_fea, tables=options.tables)
+ addOpenTypeFeatures(
+ font, options.input_fea, tables=options.tables, debug=options.debug
+ )
except FeatureLibError as e:
if options.traceback:
raise
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 00c6d85b..f8b6a33b 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -2,6 +2,7 @@ from fontTools.misc.py23 import *
from fontTools.misc import sstruct
from fontTools.misc.textTools import binary2num, safeEval
from fontTools.feaLib.error import FeatureLibError
+from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
from fontTools.feaLib.parser import Parser
from fontTools.feaLib.ast import FeatureFile
from fontTools.otlLib import builder as otl
@@ -34,7 +35,7 @@ import logging
log = logging.getLogger(__name__)
-def addOpenTypeFeatures(font, featurefile, tables=None):
+def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
"""Add features from a file to a font. Note that this replaces any features
currently present.
@@ -44,13 +45,17 @@ def addOpenTypeFeatures(font, featurefile, tables=None):
parse it into an AST), or a pre-parsed AST instance.
tables: If passed, restrict the set of affected tables to those in the
list.
+ debug: Whether to add source debugging information to the font in the
+ ``Debg`` table
"""
builder = Builder(font, featurefile)
- builder.build(tables=tables)
+ builder.build(tables=tables, debug=debug)
-def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
+def addOpenTypeFeaturesFromString(
+ font, features, filename=None, tables=None, debug=False
+):
"""Add features from a string to a font. Note that this replaces any
features currently present.
@@ -62,13 +67,15 @@ def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
directory is assumed.
tables: If passed, restrict the set of affected tables to those in the
list.
+ debug: Whether to add source debugging information to the font in the
+ ``Debg`` table
"""
featurefile = UnicodeIO(tounicode(features))
if filename:
featurefile.name = filename
- addOpenTypeFeatures(font, featurefile, tables=tables)
+ addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
class Builder(object):
@@ -108,6 +115,7 @@ class Builder(object):
self.cur_lookup_name_ = None
self.cur_feature_name_ = None
self.lookups_ = []
+ self.lookup_locations = {"GSUB": {}, "GPOS": {}}
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
# for feature 'aalt'
@@ -146,7 +154,7 @@ class Builder(object):
# for table 'vhea'
self.vhea_ = {}
- def build(self, tables=None):
+ def build(self, tables=None, debug=False):
if self.parseTree is None:
self.parseTree = Parser(self.file, self.glyphMap).parse()
self.parseTree.build(self)
@@ -201,6 +209,8 @@ class Builder(object):
self.font["BASE"] = base
elif "BASE" in self.font:
del self.font["BASE"]
+ if debug:
+ self.buildDebg()
def get_chained_lookup_(self, location, builder_class):
result = builder_class(self.font, location)
@@ -638,6 +648,12 @@ class Builder(object):
sets.append(glyphs)
return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
+ def buildDebg(self):
+ if "Debg" not in self.font:
+ self.font["Debg"] = newTable("Debg")
+ self.font["Debg"].data = {}
+ self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
+
def buildLookups_(self, tag):
assert tag in ("GPOS", "GSUB"), tag
for lookup in self.lookups_:
@@ -647,6 +663,11 @@ class Builder(object):
if lookup.table != tag:
continue
lookup.lookup_index = len(lookups)
+ self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
+ location=str(lookup.location),
+ name=self.get_lookup_name_(lookup),
+ feature=None,
+ )
lookups.append(lookup)
try:
otLookups = [l.build() for l in lookups]
@@ -685,6 +706,11 @@ class Builder(object):
if len(lookup_indices) == 0 and not size_feature:
continue
+ for ix in lookup_indices:
+ self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
+ str(ix)
+ ]._replace(feature=key)
+
feature_key = (feature_tag, lookup_indices)
feature_index = feature_indices.get(feature_key)
if feature_index is None:
@@ -737,6 +763,12 @@ class Builder(object):
table.LookupList.LookupCount = len(table.LookupList.Lookup)
return table
+ def get_lookup_name_(self, lookup):
+ rev = {v: k for k, v in self.named_lookups_.items()}
+ if lookup in rev:
+ return rev[lookup]
+ return None
+
def add_language_system(self, location, script, language):
# OpenType Feature File Specification, section 4.b.i
if script == "DFLT" and language == "dflt" and self.default_language_systems_:
diff --git a/Lib/fontTools/feaLib/lookupDebugInfo.py b/Lib/fontTools/feaLib/lookupDebugInfo.py
new file mode 100644
index 00000000..3b711f64
--- /dev/null
+++ b/Lib/fontTools/feaLib/lookupDebugInfo.py
@@ -0,0 +1,10 @@
+from typing import NamedTuple
+
+LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib"
+
+class LookupDebugInfo(NamedTuple):
+ """Information about where a lookup came from, to be embedded in a font"""
+
+ location: str
+ name: str
+ feature: list
diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib/__init__.py
index 0ee1e6f7..1335e8cb 100644
--- a/Lib/fontTools/misc/plistlib.py
+++ b/Lib/fontTools/misc/plistlib/__init__.py
@@ -1,13 +1,25 @@
+import collections.abc
import sys
import re
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ List,
+ Mapping,
+ MutableMapping,
+ Optional,
+ Sequence,
+ Type,
+ Union,
+ IO,
+)
import warnings
from io import BytesIO
from datetime import datetime
from base64 import b64encode, b64decode
from numbers import Integral
-
from types import SimpleNamespace
-from collections.abc import Mapping
from functools import singledispatch
from fontTools.misc import etree
@@ -17,7 +29,7 @@ from fontTools.misc.py23 import (
tobytes,
)
-# By default, we
+# By default, we
# - deserialize <data> elements as bytes and
# - serialize bytes as <data> elements.
# Before, on Python 2, we
@@ -38,6 +50,7 @@ PLIST_DOCTYPE = (
b'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
)
+
# Date should conform to a subset of ISO 8601:
# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'
_date_parser = re.compile(
@@ -48,23 +61,27 @@ _date_parser = re.compile(
r"(?::(?P<minute>\d\d)"
r"(?::(?P<second>\d\d))"
r"?)?)?)?)?Z",
- re.ASCII
+ re.ASCII,
)
-def _date_from_string(s):
+def _date_from_string(s: str) -> datetime:
order = ("year", "month", "day", "hour", "minute", "second")
- gd = _date_parser.match(s).groupdict()
+ m = _date_parser.match(s)
+ if m is None:
+ raise ValueError(f"Expected ISO 8601 date string, but got '{s:r}'.")
+ gd = m.groupdict()
lst = []
for key in order:
val = gd[key]
if val is None:
break
lst.append(int(val))
- return datetime(*lst)
+ # NOTE: mypy doesn't know that lst is 6 elements long.
+ return datetime(*lst) # type:ignore
-def _date_to_string(d):
+def _date_to_string(d: datetime) -> str:
return "%04d-%02d-%02dT%02d:%02d:%02dZ" % (
d.year,
d.month,
@@ -75,21 +92,6 @@ def _date_to_string(d):
)
-def _encode_base64(data, maxlinelength=76, indent_level=1):
- data = b64encode(data)
- if data and maxlinelength:
- # split into multiple lines right-justified to 'maxlinelength' chars
- indent = b"\n" + b" " * indent_level
- max_length = max(16, maxlinelength - len(indent))
- chunks = []
- for i in range(0, len(data), max_length):
- chunks.append(indent)
- chunks.append(data[i : i + max_length])
- chunks.append(indent)
- data = b"".join(chunks)
- return data
-
-
class Data:
"""Represents binary data when ``use_builtin_types=False.``
@@ -100,21 +102,21 @@ class Data:
The actual binary data is retrieved using the ``data`` attribute.
"""
- def __init__(self, data):
+ def __init__(self, data: bytes) -> None:
if not isinstance(data, bytes):
raise TypeError("Expected bytes, found %s" % type(data).__name__)
self.data = data
@classmethod
- def fromBase64(cls, data):
+ def fromBase64(cls, data: Union[bytes, str]) -> "Data":
return cls(b64decode(data))
- def asBase64(self, maxlinelength=76, indent_level=1):
+ def asBase64(self, maxlinelength: int = 76, indent_level: int = 1) -> bytes:
return _encode_base64(
self.data, maxlinelength=maxlinelength, indent_level=indent_level
)
- def __eq__(self, other):
+ def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.data == other.data
elif isinstance(other, bytes):
@@ -122,12 +124,45 @@ class Data:
else:
return NotImplemented
- def __repr__(self):
+ def __repr__(self) -> str:
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
+def _encode_base64(
+ data: bytes, maxlinelength: Optional[int] = 76, indent_level: int = 1
+) -> bytes:
+ data = b64encode(data)
+ if data and maxlinelength:
+ # split into multiple lines right-justified to 'maxlinelength' chars
+ indent = b"\n" + b" " * indent_level
+ max_length = max(16, maxlinelength - len(indent))
+ chunks = []
+ for i in range(0, len(data), max_length):
+ chunks.append(indent)
+ chunks.append(data[i : i + max_length])
+ chunks.append(indent)
+ data = b"".join(chunks)
+ return data
+
+
+# Mypy does not support recursive type aliases as of 0.782, Pylance does.
+# https://github.com/python/mypy/issues/731
+# https://devblogs.microsoft.com/python/pylance-introduces-five-new-features-that-enable-type-magic-for-python-developers/#1-support-for-recursive-type-aliases
+PlistEncodable = Union[
+ bool,
+ bytes,
+ Data,
+ datetime,
+ float,
+ int,
+ Mapping[str, Any],
+ Sequence[Any],
+ str,
+]
+
+
class PlistTarget:
- """ Event handler using the ElementTree Target API that can be
+ """Event handler using the ElementTree Target API that can be
passed to a XMLParser to produce property list objects from XML.
It is based on the CPython plistlib module's _PlistParser class,
but does not use the expat parser.
@@ -148,10 +183,14 @@ class PlistTarget:
http://lxml.de/parsing.html#the-target-parser-interface
"""
- def __init__(self, use_builtin_types=None, dict_type=dict):
- self.stack = []
- self.current_key = None
- self.root = None
+ def __init__(
+ self,
+ use_builtin_types: Optional[bool] = None,
+ dict_type: Type[MutableMapping[str, Any]] = dict,
+ ) -> None:
+ self.stack: List[PlistEncodable] = []
+ self.current_key: Optional[str] = None
+ self.root: Optional[PlistEncodable] = None
if use_builtin_types is None:
self._use_builtin_types = USE_BUILTIN_TYPES
else:
@@ -164,40 +203,44 @@ class PlistTarget:
self._use_builtin_types = use_builtin_types
self._dict_type = dict_type
- def start(self, tag, attrib):
- self._data = []
+ def start(self, tag: str, attrib: Mapping[str, str]) -> None:
+ self._data: List[str] = []
handler = _TARGET_START_HANDLERS.get(tag)
if handler is not None:
handler(self)
- def end(self, tag):
+ def end(self, tag: str) -> None:
handler = _TARGET_END_HANDLERS.get(tag)
if handler is not None:
handler(self)
- def data(self, data):
+ def data(self, data: str) -> None:
self._data.append(data)
- def close(self):
+ def close(self) -> PlistEncodable:
+ if self.root is None:
+ raise ValueError("No root set.")
return self.root
# helpers
- def add_object(self, value):
+ def add_object(self, value: PlistEncodable) -> None:
if self.current_key is not None:
- if not isinstance(self.stack[-1], type({})):
- raise ValueError("unexpected element: %r" % self.stack[-1])
- self.stack[-1][self.current_key] = value
+ stack_top = self.stack[-1]
+ if not isinstance(stack_top, collections.abc.MutableMapping):
+ raise ValueError("unexpected element: %r" % stack_top)
+ stack_top[self.current_key] = value
self.current_key = None
elif not self.stack:
# this is the root object
self.root = value
else:
- if not isinstance(self.stack[-1], type([])):
- raise ValueError("unexpected element: %r" % self.stack[-1])
- self.stack[-1].append(value)
+ stack_top = self.stack[-1]
+ if not isinstance(stack_top, list):
+ raise ValueError("unexpected element: %r" % stack_top)
+ stack_top.append(value)
- def get_data(self):
+ def get_data(self) -> str:
data = "".join(self._data)
self._data = []
return data
@@ -206,68 +249,71 @@ class PlistTarget:
# event handlers
-def start_dict(self):
+def start_dict(self: PlistTarget) -> None:
d = self._dict_type()
self.add_object(d)
self.stack.append(d)
-def end_dict(self):
+def end_dict(self: PlistTarget) -> None:
if self.current_key:
raise ValueError("missing value for key '%s'" % self.current_key)
self.stack.pop()
-def end_key(self):
- if self.current_key or not isinstance(self.stack[-1], type({})):
+def end_key(self: PlistTarget) -> None:
+ if self.current_key or not isinstance(self.stack[-1], collections.abc.Mapping):
raise ValueError("unexpected key")
self.current_key = self.get_data()
-def start_array(self):
- a = []
+def start_array(self: PlistTarget) -> None:
+ a: List[PlistEncodable] = []
self.add_object(a)
self.stack.append(a)
-def end_array(self):
+def end_array(self: PlistTarget) -> None:
self.stack.pop()
-def end_true(self):
+def end_true(self: PlistTarget) -> None:
self.add_object(True)
-def end_false(self):
+def end_false(self: PlistTarget) -> None:
self.add_object(False)
-def end_integer(self):
+def end_integer(self: PlistTarget) -> None:
self.add_object(int(self.get_data()))
-def end_real(self):
+def end_real(self: PlistTarget) -> None:
self.add_object(float(self.get_data()))
-def end_string(self):
+def end_string(self: PlistTarget) -> None:
self.add_object(self.get_data())
-def end_data(self):
+def end_data(self: PlistTarget) -> None:
if self._use_builtin_types:
self.add_object(b64decode(self.get_data()))
else:
self.add_object(Data.fromBase64(self.get_data()))
-def end_date(self):
+def end_date(self: PlistTarget) -> None:
self.add_object(_date_from_string(self.get_data()))
-_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array}
+_TARGET_START_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = {
+ "dict": start_dict,
+ "array": start_array,
+}
-_TARGET_END_HANDLERS = {
+_TARGET_END_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = {
"dict": end_dict,
"array": end_array,
"key": end_key,
@@ -284,39 +330,37 @@ _TARGET_END_HANDLERS = {
# functions to build element tree from plist data
-def _string_element(value, ctx):
+def _string_element(value: str, ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("string")
el.text = value
return el
-def _bool_element(value, ctx):
+def _bool_element(value: bool, ctx: SimpleNamespace) -> etree.Element:
if value:
return etree.Element("true")
- else:
- return etree.Element("false")
+ return etree.Element("false")
-def _integer_element(value, ctx):
+def _integer_element(value: int, ctx: SimpleNamespace) -> etree.Element:
if -1 << 63 <= value < 1 << 64:
el = etree.Element("integer")
el.text = "%d" % value
return el
- else:
- raise OverflowError(value)
+ raise OverflowError(value)
-def _real_element(value, ctx):
+def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("real")
el.text = repr(value)
return el
-def _dict_element(d, ctx):
+def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("dict")
items = d.items()
if ctx.sort_keys:
- items = sorted(items)
+ items = sorted(items) # type: ignore
ctx.indent_level += 1
for key, value in items:
if not isinstance(key, str):
@@ -330,7 +374,7 @@ def _dict_element(d, ctx):
return el
-def _array_element(array, ctx):
+def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("array")
if len(array) == 0:
return el
@@ -341,15 +385,16 @@ def _array_element(array, ctx):
return el
-def _date_element(date, ctx):
+def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("date")
el.text = _date_to_string(date)
return el
-def _data_element(data, ctx):
+def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element:
el = etree.Element("data")
- el.text = _encode_base64(
+ # NOTE: mypy is confused about whether el.text should be str or bytes.
+ el.text = _encode_base64( # type: ignore
data,
maxlinelength=(76 if ctx.pretty_print else None),
indent_level=ctx.indent_level,
@@ -357,7 +402,7 @@ def _data_element(data, ctx):
return el
-def _string_or_data_element(raw_bytes, ctx):
+def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Element:
if ctx.use_builtin_types:
return _data_element(raw_bytes, ctx)
else:
@@ -365,21 +410,26 @@ def _string_or_data_element(raw_bytes, ctx):
string = raw_bytes.decode(encoding="ascii", errors="strict")
except UnicodeDecodeError:
raise ValueError(
- "invalid non-ASCII bytes; use unicode string instead: %r"
- % raw_bytes
+ "invalid non-ASCII bytes; use unicode string instead: %r" % raw_bytes
)
return _string_element(string, ctx)
+# The following is probably not entirely correct. The signature should take `Any`
+# and return `NoReturn`. At the time of this writing, neither mypy nor Pyright
+# can deal with singledispatch properly and will apply the signature of the base
+# function to all others. Being slightly dishonest makes it type-check and return
+# usable typing information for the optimistic case.
@singledispatch
-def _make_element(value, ctx):
+def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element:
raise TypeError("unsupported type: %s" % type(value))
+
_make_element.register(str)(_string_element)
_make_element.register(bool)(_bool_element)
_make_element.register(Integral)(_integer_element)
_make_element.register(float)(_real_element)
-_make_element.register(Mapping)(_dict_element)
+_make_element.register(collections.abc.Mapping)(_dict_element)
_make_element.register(list)(_array_element)
_make_element.register(tuple)(_array_element)
_make_element.register(datetime)(_date_element)
@@ -393,13 +443,13 @@ _make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx))
def totree(
- value,
- sort_keys=True,
- skipkeys=False,
- use_builtin_types=None,
- pretty_print=True,
- indent_level=1,
-):
+ value: PlistEncodable,
+ sort_keys: bool = True,
+ skipkeys: bool = False,
+ use_builtin_types: Optional[bool] = None,
+ pretty_print: bool = True,
+ indent_level: int = 1,
+) -> etree.Element:
"""Convert a value derived from a plist into an XML tree.
Args:
@@ -439,7 +489,11 @@ def totree(
return _make_element(value, context)
-def fromtree(tree, use_builtin_types=None, dict_type=dict):
+def fromtree(
+ tree: etree.Element,
+ use_builtin_types: Optional[bool] = None,
+ dict_type: Type[MutableMapping[str, Any]] = dict,
+) -> Any:
"""Convert an XML tree to a plist structure.
Args:
@@ -451,9 +505,7 @@ def fromtree(tree, use_builtin_types=None, dict_type=dict):
Returns: An object (usually a dictionary).
"""
- target = PlistTarget(
- use_builtin_types=use_builtin_types, dict_type=dict_type
- )
+ target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type)
for action, element in etree.iterwalk(tree, events=("start", "end")):
if action == "start":
target.start(element.tag, element.attrib)
@@ -469,7 +521,11 @@ def fromtree(tree, use_builtin_types=None, dict_type=dict):
# python3 plistlib API
-def load(fp, use_builtin_types=None, dict_type=dict):
+def load(
+ fp: IO[bytes],
+ use_builtin_types: Optional[bool] = None,
+ dict_type: Type[MutableMapping[str, Any]] = dict,
+) -> Any:
"""Load a plist file into an object.
Args:
@@ -485,13 +541,9 @@ def load(fp, use_builtin_types=None, dict_type=dict):
"""
if not hasattr(fp, "read"):
- raise AttributeError(
- "'%s' object has no attribute 'read'" % type(fp).__name__
- )
- target = PlistTarget(
- use_builtin_types=use_builtin_types, dict_type=dict_type
- )
- parser = etree.XMLParser(target=target)
+ raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__)
+ target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type)
+ parser = etree.XMLParser(target=target) # type: ignore
result = etree.parse(fp, parser=parser)
# lxml returns the target object directly, while ElementTree wraps
# it as the root of an ElementTree object
@@ -501,11 +553,15 @@ def load(fp, use_builtin_types=None, dict_type=dict):
return result
-def loads(value, use_builtin_types=None, dict_type=dict):
+def loads(
+ value: bytes,
+ use_builtin_types: Optional[bool] = None,
+ dict_type: Type[MutableMapping[str, Any]] = dict,
+) -> Any:
"""Load a plist file from a string into an object.
Args:
- value: A string containing a plist.
+ value: A bytes string containing a plist.
use_builtin_types: If True, binary data is deserialized to
bytes strings. If False, it is wrapped in :py:class:`Data`
objects. Defaults to True if not provided. Deprecated.
@@ -521,13 +577,13 @@ def loads(value, use_builtin_types=None, dict_type=dict):
def dump(
- value,
- fp,
- sort_keys=True,
- skipkeys=False,
- use_builtin_types=None,
- pretty_print=True,
-):
+ value: PlistEncodable,
+ fp: IO[bytes],
+ sort_keys: bool = True,
+ skipkeys: bool = False,
+ use_builtin_types: Optional[bool] = None,
+ pretty_print: bool = True,
+) -> None:
"""Write a Python object to a plist file.
Args:
@@ -550,12 +606,10 @@ def dump(
``ValueError``
if non-representable binary data is present
and `use_builtin_types` is false.
- """
+ """
if not hasattr(fp, "write"):
- raise AttributeError(
- "'%s' object has no attribute 'write'" % type(fp).__name__
- )
+ raise AttributeError("'%s' object has no attribute 'write'" % type(fp).__name__)
root = etree.Element("plist", version="1.0")
el = totree(
value,
@@ -574,18 +628,21 @@ def dump(
else:
header = XML_DECLARATION + PLIST_DOCTYPE
fp.write(header)
- tree.write(
- fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False
+ tree.write( # type: ignore
+ fp,
+ encoding="utf-8",
+ pretty_print=pretty_print,
+ xml_declaration=False,
)
def dumps(
- value,
- sort_keys=True,
- skipkeys=False,
- use_builtin_types=None,
- pretty_print=True,
-):
+ value: PlistEncodable,
+ sort_keys: bool = True,
+ skipkeys: bool = False,
+ use_builtin_types: Optional[bool] = None,
+ pretty_print: bool = True,
+) -> bytes:
"""Write a Python object to a string in plist format.
Args:
diff --git a/Lib/fontTools/misc/plistlib/py.typed b/Lib/fontTools/misc/plistlib/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/Lib/fontTools/misc/plistlib/py.typed
diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py
index 4176fb25..8525754f 100644
--- a/Lib/fontTools/mtiLib/__init__.py
+++ b/Lib/fontTools/mtiLib/__init__.py
@@ -1146,12 +1146,33 @@ class Tokenizer(object):
return line
def build(f, font, tableTag=None):
+ """Convert a Monotype font layout file to an OpenType layout object
+
+ A font object must be passed, but this may be a "dummy" font; it is only
+ used for sorting glyph sets when making coverage tables and to hold the
+ OpenType layout table while it is being built.
+
+ Args:
+ f: A file object.
+ font (TTFont): A font object.
+ tableTag (string): If provided, asserts that the file contains data for the
+ given OpenType table.
+
+ Returns:
+ An object representing the table. (e.g. ``table_G_S_U_B_``)
+ """
lines = Tokenizer(f)
return parseTable(lines, font, tableTag=tableTag)
def main(args=None, font=None):
- """Convert a FontDame OTL file to TTX XML"""
+ """Convert a FontDame OTL file to TTX XML.
+
+ Writes XML output to stdout.
+
+ Args:
+ args: Command line arguments (``--font``, ``--table``, input files).
+ """
import sys
from fontTools import configLogger
from fontTools.misc.testTools import MockFont
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index aaa22f94..4aa97610 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -2711,7 +2711,7 @@ class Subsetter(object):
def load_font(fontFile,
options,
allowVID=False,
- checkChecksums=False,
+ checkChecksums=0,
dontLoadGlyphNames=False,
lazy=True):
diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py
index 2f3b6698..d609dc51 100644
--- a/Lib/fontTools/ttLib/sfnt.py
+++ b/Lib/fontTools/ttLib/sfnt.py
@@ -43,7 +43,7 @@ class SFNTReader(object):
# return default object
return object.__new__(cls)
- def __init__(self, file, checkChecksums=1, fontNumber=-1):
+ def __init__(self, file, checkChecksums=0, fontNumber=-1):
self.file = file
self.checkChecksums = checkChecksums
diff --git a/Lib/fontTools/ttLib/tables/D__e_b_g.py b/Lib/fontTools/ttLib/tables/D__e_b_g.py
new file mode 100644
index 00000000..ff64a9b5
--- /dev/null
+++ b/Lib/fontTools/ttLib/tables/D__e_b_g.py
@@ -0,0 +1,17 @@
+import json
+
+from . import DefaultTable
+
+
+class table_D__e_b_g(DefaultTable.DefaultTable):
+ def decompile(self, data, ttFont):
+ self.data = json.loads(data)
+
+ def compile(self, ttFont):
+ return json.dumps(self.data).encode("utf-8")
+
+ def toXML(self, writer, ttFont):
+ writer.writecdata(json.dumps(self.data))
+
+ def fromXML(self, name, attrs, content, ttFont):
+ self.data = json.loads(content)
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index b821fa36..31ff0122 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -12,6 +12,7 @@ from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
from fontTools.misc.textTools import pad, safeEval
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
+from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
import logging
import struct
@@ -1187,6 +1188,44 @@ class COLR(BaseTable):
}
+class LookupList(BaseTable):
+ @property
+ def table(self):
+ for l in self.Lookup:
+ for st in l.SubTable:
+ if type(st).__name__.endswith("Subst"):
+ return "GSUB"
+ if type(st).__name__.endswith("Pos"):
+ return "GPOS"
+ raise ValueError
+
+ def toXML2(self, xmlWriter, font):
+ if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data:
+ return super().toXML2(xmlWriter, font)
+ debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
+ for conv in self.getConverters():
+ if conv.repeat:
+ value = getattr(self, conv.name, [])
+ for lookupIndex, item in enumerate(value):
+ if str(lookupIndex) in debugData:
+ info = LookupDebugInfo(*debugData[str(lookupIndex)])
+ tag = info.location
+ if info.name:
+ tag = f'{info.name}: {tag}'
+ if info.feature:
+ script,language,feature = info.feature
+ tag = f'{tag} in {feature} ({script}/{language})'
+ xmlWriter.comment(tag)
+ xmlWriter.newline()
+
+ conv.xmlWrite(xmlWriter, font, item, conv.name,
+ [("index", lookupIndex)])
+ else:
+ if conv.aux and not eval(conv.aux, None, vars(self)):
+ continue
+ value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None!
+ conv.xmlWrite(xmlWriter, font, value, conv.name, [])
+
class BaseGlyphRecordArray(BaseTable):
def preWrite(self, font):
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index fd8f718f..ed1ec5e2 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -18,7 +18,7 @@ class TTFont(object):
"""
def __init__(self, file=None, res_name_or_index=None,
- sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False,
+ sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
_tableCache=None):
@@ -830,10 +830,48 @@ def getTableModule(tag):
return getattr(tables, pyTag)
-def getTableClass(tag):
- """Fetch the packer/unpacker class for a table.
- Return None when no class is found.
+# Registry for custom table packer/unpacker classes. Keys are table
+# tags, values are (moduleName, className) tuples.
+# See registerCustomTableClass() and getCustomTableClass()
+_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.
+
+ The registered table class should be a subclass of
+ fontTools.ttLib.tables.DefaultTable.DefaultTable
+ """
+ if className is None:
+ className = "table_" + tagToIdentifier(tag)
+ _customTableRegistry[tag] = (moduleName, className)
+
+
+def unregisterCustomTableClass(tag):
+ """Unregister the custom packer/unpacker class for a table."""
+ del _customTableRegistry[tag]
+
+
+def getCustomTableClass(tag):
+ """Return the custom table class for tag, if one has been registered
+ with 'registerCustomTableClass()'. Else return None.
"""
+ if tag not in _customTableRegistry:
+ return None
+ import importlib
+ moduleName, className = _customTableRegistry[tag]
+ module = importlib.import_module(moduleName)
+ return getattr(module, className)
+
+
+def getTableClass(tag):
+ """Fetch the packer/unpacker class for a table."""
+ tableClass = getCustomTableClass(tag)
+ if tableClass is not None:
+ return tableClass
module = getTableModule(tag)
if module is None:
from .tables.DefaultTable import DefaultTable
diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py
index e77ad9a4..67b1d1c1 100644
--- a/Lib/fontTools/ttLib/woff2.py
+++ b/Lib/fontTools/ttLib/woff2.py
@@ -29,7 +29,7 @@ class WOFF2Reader(SFNTReader):
flavor = "woff2"
- def __init__(self, file, checkChecksums=1, fontNumber=-1):
+ def __init__(self, file, checkChecksums=0, fontNumber=-1):
if not haveBrotli:
log.error(
'The WOFF2 decoder requires the Brotli Python extension, available at: '
diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py
index 6488022f..0aa941c9 100644
--- a/Lib/fontTools/varLib/interpolatable.py
+++ b/Lib/fontTools/varLib/interpolatable.py
@@ -104,6 +104,7 @@ def test(glyphsets, glyphs=None, names=None):
try:
allVectors = []
+ allNodeTypes = []
for glyphset,name in zip(glyphsets, names):
#print('.', end='')
glyph = glyphset[glyph_name]
@@ -114,8 +115,11 @@ def test(glyphsets, glyphs=None, names=None):
del perContourPen
contourVectors = []
+ nodeTypes = []
+ allNodeTypes.append(nodeTypes)
allVectors.append(contourVectors)
for contour in contourPens:
+ nodeTypes.append(tuple(instruction[0] for instruction in contour.value))
stats = StatisticsPen(glyphset=glyphset)
contour.replay(stats)
size = abs(stats.area) ** .5 * .5
@@ -131,6 +135,23 @@ def test(glyphsets, glyphs=None, names=None):
#print(vector)
# Check each master against the next one in the list.
+ for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
+ if len(m0) != len(m1):
+ print('%s: %s+%s: Glyphs not compatible (wrong number of paths %i+%i)!!!!!' % (glyph_name, names[i], names[i+1], len(m0), len(m1)))
+ if m0 == m1:
+ continue
+ for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
+ if nodes1 == nodes2:
+ continue
+ print('%s: %s+%s: Glyphs not compatible at path %i!!!!!' % (glyph_name, names[i], names[i+1], pathIx))
+ if len(nodes1) != len(nodes2):
+ print("%s has %i nodes, %s has %i nodes" % (names[i], len(nodes1), names[i+1], len(nodes2)))
+ continue
+ for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
+ if n1 != n2:
+ print("At node %i, %s has %s, %s has %s" % (nodeIx, names[i], n1, names[i+1], n2))
+ continue
+
for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])):
if len(m0) != len(m1):
print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1]))
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index c2bfa255..e2bad13a 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.14.0
+Version: 4.15.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -254,6 +254,22 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.15.0 (released 2020-09-21)
+ ----------------------------
+
+ - [plistlib] Added typing annotations to plistlib module. Set up mypy static
+ typechecker to run automatically on CI (#2061).
+ - [ttLib] Implement private ``Debg`` table, a reverse-DNS namespaced JSON dict.
+ - [feaLib] Optionally add an entry into the ``Debg`` table with the original
+ lookup name (if any), feature name / script / language combination (if any),
+ and original source filename and line location. Annotate the ttx output for
+ a lookup with the information from the Debg table (#2052).
+ - [sfnt] Disabled checksum checking by default in ``SFNTReader`` (#2058).
+ - [Docs] Document ``mtiLib`` module (#2027).
+ - [varLib.interpolatable] Added checks for contour node count and operation type
+ of each node (#2054).
+ - [ttLib] Added API to register custom table packer/unpacker classes (#2055).
+
4.14.0 (released 2020-08-19)
----------------------------
@@ -2021,13 +2037,13 @@ Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
+Provides-Extra: lxml
+Provides-Extra: symfont
Provides-Extra: ufo
-Provides-Extra: unicode
Provides-Extra: interpolatable
-Provides-Extra: plot
-Provides-Extra: symfont
Provides-Extra: all
-Provides-Extra: lxml
-Provides-Extra: woff
Provides-Extra: type1
+Provides-Extra: woff
+Provides-Extra: plot
+Provides-Extra: unicode
Provides-Extra: graphite
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 9fd87b8f..812fcb82 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -182,6 +182,7 @@ Lib/fontTools/feaLib/builder.py
Lib/fontTools/feaLib/error.py
Lib/fontTools/feaLib/lexer.py
Lib/fontTools/feaLib/location.py
+Lib/fontTools/feaLib/lookupDebugInfo.py
Lib/fontTools/feaLib/parser.py
Lib/fontTools/misc/__init__.py
Lib/fontTools/misc/arrayTools.py
@@ -199,7 +200,6 @@ Lib/fontTools/misc/intTools.py
Lib/fontTools/misc/loggingTools.py
Lib/fontTools/misc/macCreatorType.py
Lib/fontTools/misc/macRes.py
-Lib/fontTools/misc/plistlib.py
Lib/fontTools/misc/psCharStrings.py
Lib/fontTools/misc/psLib.py
Lib/fontTools/misc/psOperators.py
@@ -212,6 +212,8 @@ Lib/fontTools/misc/timeTools.py
Lib/fontTools/misc/transform.py
Lib/fontTools/misc/xmlReader.py
Lib/fontTools/misc/xmlWriter.py
+Lib/fontTools/misc/plistlib/__init__.py
+Lib/fontTools/misc/plistlib/py.typed
Lib/fontTools/mtiLib/__init__.py
Lib/fontTools/mtiLib/__main__.py
Lib/fontTools/otlLib/__init__.py
@@ -266,6 +268,7 @@ Lib/fontTools/ttLib/tables/C_F_F__2.py
Lib/fontTools/ttLib/tables/C_O_L_R_.py
Lib/fontTools/ttLib/tables/C_P_A_L_.py
Lib/fontTools/ttLib/tables/D_S_I_G_.py
+Lib/fontTools/ttLib/tables/D__e_b_g.py
Lib/fontTools/ttLib/tables/DefaultTable.py
Lib/fontTools/ttLib/tables/E_B_D_T_.py
Lib/fontTools/ttLib/tables/E_B_L_C_.py
@@ -670,6 +673,7 @@ Tests/feaLib/data/ignore_pos.ttx
Tests/feaLib/data/include0.fea
Tests/feaLib/data/language_required.fea
Tests/feaLib/data/language_required.ttx
+Tests/feaLib/data/lookup-debug.ttx
Tests/feaLib/data/lookup.fea
Tests/feaLib/data/lookup.ttx
Tests/feaLib/data/lookupflag.fea
@@ -954,6 +958,7 @@ Tests/t1Lib/data/TestT1-Regular.pfa
Tests/t1Lib/data/TestT1-Regular.pfb
Tests/t1Lib/data/TestT1-weird-zeros.pfa
Tests/ttLib/sfnt_test.py
+Tests/ttLib/ttFont_test.py
Tests/ttLib/woff2_test.py
Tests/ttLib/data/TestOTF-Regular.otx
Tests/ttLib/data/TestTTF-Regular.ttx
diff --git a/MANIFEST.in b/MANIFEST.in
index 5a55dfeb..5c4d1274 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -13,6 +13,8 @@ include *requirements.txt
include tox.ini
include run-tests.sh
+recursive-include Lib/fontTools py.typed
+
include .appveyor.yml
include .codecov.yml
include .coveragerc
diff --git a/METADATA b/METADATA
index 87611a58..36770a26 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/4.14.0/fonttools-4.14.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/4.15.0/fonttools-4.15.0.zip"
}
- version: "4.14.0"
+ version: "4.15.0"
license_type: NOTICE
last_upgrade_date {
year: 2020
- month: 8
- day: 19
+ month: 9
+ day: 21
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 31cb76bf..efc26ad0 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,19 @@
+4.15.0 (released 2020-09-21)
+----------------------------
+
+- [plistlib] Added typing annotations to plistlib module. Set up mypy static
+ typechecker to run automatically on CI (#2061).
+- [ttLib] Implement private ``Debg`` table, a reverse-DNS namespaced JSON dict.
+- [feaLib] Optionally add an entry into the ``Debg`` table with the original
+ lookup name (if any), feature name / script / language combination (if any),
+ and original source filename and line location. Annotate the ttx output for
+ a lookup with the information from the Debg table (#2052).
+- [sfnt] Disabled checksum checking by default in ``SFNTReader`` (#2058).
+- [Docs] Document ``mtiLib`` module (#2027).
+- [varLib.interpolatable] Added checks for contour node count and operation type
+ of each node (#2054).
+- [ttLib] Added API to register custom table packer/unpacker classes (#2055).
+
4.14.0 (released 2020-08-19)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index c2bfa255..e2bad13a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.14.0
+Version: 4.15.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -254,6 +254,22 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.15.0 (released 2020-09-21)
+ ----------------------------
+
+ - [plistlib] Added typing annotations to plistlib module. Set up mypy static
+ typechecker to run automatically on CI (#2061).
+ - [ttLib] Implement private ``Debg`` table, a reverse-DNS namespaced JSON dict.
+ - [feaLib] Optionally add an entry into the ``Debg`` table with the original
+ lookup name (if any), feature name / script / language combination (if any),
+ and original source filename and line location. Annotate the ttx output for
+ a lookup with the information from the Debg table (#2052).
+ - [sfnt] Disabled checksum checking by default in ``SFNTReader`` (#2058).
+ - [Docs] Document ``mtiLib`` module (#2027).
+ - [varLib.interpolatable] Added checks for contour node count and operation type
+ of each node (#2054).
+ - [ttLib] Added API to register custom table packer/unpacker classes (#2055).
+
4.14.0 (released 2020-08-19)
----------------------------
@@ -2021,13 +2037,13 @@ Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
+Provides-Extra: lxml
+Provides-Extra: symfont
Provides-Extra: ufo
-Provides-Extra: unicode
Provides-Extra: interpolatable
-Provides-Extra: plot
-Provides-Extra: symfont
Provides-Extra: all
-Provides-Extra: lxml
-Provides-Extra: woff
Provides-Extra: type1
+Provides-Extra: woff
+Provides-Extra: plot
+Provides-Extra: unicode
Provides-Extra: graphite
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 5a8c562d..8e987522 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -114,12 +114,16 @@ class BuilderTest(unittest.TestCase):
lines.append(line.rstrip() + os.linesep)
return lines
- def expect_ttx(self, font, expected_ttx):
+ def expect_ttx(self, font, expected_ttx, replace=None):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
'GPOS', 'OS/2', 'hhea', 'vhea'])
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
+ if replace:
+ for i in range(len(expected)):
+ for k, v in replace.items():
+ expected[i] = expected[i].replace(k, v)
if actual != expected:
for line in difflib.unified_diff(
expected, actual, fromfile=expected_ttx, tofile=path):
@@ -133,12 +137,17 @@ class BuilderTest(unittest.TestCase):
def check_feature_file(self, name):
font = makeTTFont()
- addOpenTypeFeatures(font, self.getpath("%s.fea" % name))
+ feapath = self.getpath("%s.fea" % name)
+ addOpenTypeFeatures(font, feapath)
self.expect_ttx(font, self.getpath("%s.ttx" % name))
# Make sure we can produce binary OpenType tables, not just XML.
for tag in ('GDEF', 'GSUB', 'GPOS'):
if tag in font:
font[tag].compile(font)
+ debugttx = self.getpath("%s-debug.ttx" % name)
+ if os.path.exists(debugttx):
+ addOpenTypeFeatures(font, feapath, debug=True)
+ self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath})
def check_fea2fea_file(self, name, base=None, parser=Parser):
font = makeTTFont()
diff --git a/Tests/feaLib/data/lookup-debug.ttx b/Tests/feaLib/data/lookup-debug.ttx
new file mode 100644
index 00000000..f8696179
--- /dev/null
+++ b/Tests/feaLib/data/lookup-debug.ttx
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=4 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ <FeatureIndex index="3" value="3"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=4 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="tst1"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="tst2"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="2">
+ <FeatureTag value="tst3"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="3">
+ <FeatureTag value="tst4"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=2 -->
+ <!-- SomeLookup: __PATH__:4:5 in tst2 (DFLT/dflt) -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <LigatureSubst index="0">
+ <LigatureSet glyph="f">
+ <Ligature components="f,i" glyph="f_f_i"/>
+ <Ligature components="i" glyph="f_i"/>
+ </LigatureSet>
+ </LigatureSubst>
+ </Lookup>
+ <!-- EmbeddedLookup: __PATH__:18:9 in tst4 (DFLT/dflt) -->
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="A" out="A.sc"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/ttLib/ttFont_test.py b/Tests/ttLib/ttFont_test.py
new file mode 100644
index 00000000..47cedeb7
--- /dev/null
+++ b/Tests/ttLib/ttFont_test.py
@@ -0,0 +1,48 @@
+import io
+from fontTools.ttLib import TTFont, newTable, registerCustomTableClass, unregisterCustomTableClass
+from fontTools.ttLib.tables.DefaultTable import DefaultTable
+
+
+class CustomTableClass(DefaultTable):
+
+ def decompile(self, data, ttFont):
+ self.numbers = list(data)
+
+ def compile(self, ttFont):
+ return bytes(self.numbers)
+
+ # not testing XML read/write
+
+
+table_C_U_S_T_ = CustomTableClass # alias for testing
+
+
+TABLETAG = "CUST"
+
+
+def test_registerCustomTableClass():
+ font = TTFont()
+ font[TABLETAG] = newTable(TABLETAG)
+ font[TABLETAG].data = b"\x00\x01\xff"
+ f = io.BytesIO()
+ font.save(f)
+ f.seek(0)
+ assert font[TABLETAG].data == b"\x00\x01\xff"
+ registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass")
+ try:
+ font = TTFont(f)
+ assert font[TABLETAG].numbers == [0, 1, 255]
+ assert font[TABLETAG].compile(font) == b"\x00\x01\xff"
+ finally:
+ unregisterCustomTableClass(TABLETAG)
+
+
+def test_registerCustomTableClassStandardName():
+ registerCustomTableClass(TABLETAG, "ttFont_test")
+ try:
+ font = TTFont()
+ font[TABLETAG] = newTable(TABLETAG)
+ font[TABLETAG].numbers = [4, 5, 6]
+ assert font[TABLETAG].compile(font) == b"\x04\x05\x06"
+ finally:
+ unregisterCustomTableClass(TABLETAG)
diff --git a/dev-requirements.txt b/dev-requirements.txt
index a34deb2e..73eae680 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -2,3 +2,4 @@ pytest>=3.0
tox>=2.5
bump2version>=0.5.6
sphinx>=1.5.5
+mypy>=0.782
diff --git a/requirements.txt b/requirements.txt
index 9e858085..0f2a1327 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
# we use the official Brotli module on CPython and the CFFI-based
# extension 'brotlipy' on PyPy
-brotli==1.0.7; platform_python_implementation != "PyPy"
+brotli==1.0.9; platform_python_implementation != "PyPy"
brotlipy==0.7.0; platform_python_implementation == "PyPy"
unicodedata2==13.0.0.post2; python_version < '3.9' and platform_python_implementation != "PyPy"
scipy==1.5.2; platform_python_implementation != "PyPy"
diff --git a/setup.cfg b/setup.cfg
index b4c626fb..2628580d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.14.0
+current_version = 4.15.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index e91eb5dc..73f2b11a 100755
--- a/setup.py
+++ b/setup.py
@@ -437,7 +437,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.14.0",
+ version="4.15.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",
diff --git a/tox.ini b/tox.ini
index df6358c2..5a8d9f20 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
minversion = 3.0
-envlist = py3{6,7,8}-cov, htmlcov
+envlist = mypy, py3{6,7,8}-cov, htmlcov
skip_missing_interpreters=true
[testenv]
@@ -33,6 +33,13 @@ commands =
coverage combine
coverage html
+[testenv:mypy]
+deps =
+ -r dev-requirements.txt
+skip_install = true
+commands =
+ mypy
+
[testenv:codecov]
passenv = *
deps =