diff options
author | Haibo Huang <hhb@google.com> | 2020-09-22 18:20:58 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-09-22 18:20:58 +0000 |
commit | a263eb264a3f5c23193c4f7b310ce99e9bedca7d (patch) | |
tree | 88bae7c0d397c0ef0353879c3be9ead6d38fd7e0 | |
parent | 88f41f40cad9eafc09ce203d968fc2ef1c6c7d13 (diff) | |
parent | 2d54a34c1c619455a1b42dc127d334bb43ad15aa (diff) | |
download | fonttools-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
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 @@ -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 } } @@ -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) ---------------------------- @@ -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" @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.14.0 +current_version = 4.15.0 commit = True tag = False tag_name = {new_version} @@ -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", @@ -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 = |