diff -Nru fonttools-3.21.2/.appveyor.yml fonttools-3.29.0/.appveyor.yml --- fonttools-3.21.2/.appveyor.yml 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/.appveyor.yml 2018-07-26 14:12:55.000000000 +0000 @@ -1,34 +1,13 @@ environment: matrix: - - JOB: "2.7.13 32-bit" + - JOB: "2.7 32-bit" PYTHON_HOME: "C:\\Python27" - TOXENV: "py27-cov" - TOXPYTHON: "C:\\Python27\\python.exe" - - JOB: "3.5.2 32-bit" - PYTHON_HOME: "C:\\Python35" - TOXENV: "py35-cov" - TOXPYTHON: "C:\\Python35\\python.exe" - - - JOB: "3.6.0 32-bit" - PYTHON_HOME: "C:\\Python36" - TOXENV: "py36-cov" - TOXPYTHON: "C:\\Python36\\python.exe" - - - JOB: "2.7.13 64-bit" - PYTHON_HOME: "C:\\Python27-x64" - TOXENV: "py27-cov" - TOXPYTHON: "C:\\Python27-x64\\python.exe" - - - JOB: "3.5.2 64-bit" - PYTHON_HOME: "C:\\Python35-x64" - TOXENV: "py35-cov" - TOXPYTHON: "C:\\Python35-x64\\python.exe" - - - JOB: "3.6.0 64-bit" + - JOB: "3.6 64-bit" PYTHON_HOME: "C:\\Python36-x64" - TOXENV: "py36-cov" - TOXPYTHON: "C:\\Python36-x64\\python.exe" + + - JOB: "3.7 64-bit" + PYTHON_HOME: "C:\\Python37-x64" install: # If there is a newer build queued for the same PR, cancel this one. @@ -53,14 +32,15 @@ # install the dependencies to run the tests - "python -m pip install tox" - build: false test_script: - - "tox" + # run tests with the current 'python' in %PATH%, and measure test coverage + - "tox -e py-cov" after_test: + # upload test coverage to Codecov.io - "tox -e codecov" notifications: diff -Nru fonttools-3.21.2/debian/changelog fonttools-3.29.0/debian/changelog --- fonttools-3.21.2/debian/changelog 2018-01-17 15:48:30.000000000 +0000 +++ fonttools-3.29.0/debian/changelog 2018-09-03 03:11:59.000000000 +0000 @@ -1,3 +1,19 @@ +fonttools (3.29.0-1) unstable; urgency=medium + + * New upstream release 3.29.0 (Closes: #903378) + * debian/control: + - Add Yao Wei (魏銘廷) as uploader + - Remove deprecated X-Python-Version field + - Change descriptions of binary packages + - Update mailing list address + - Bump debhelper to 11 + * debian/rules: + - Bump Standards-Version to 4.1.5 + * debian/compat: + - Bump debhelper to 11 + + -- Yao Wei (魏銘廷) Mon, 03 Sep 2018 11:11:59 +0800 + fonttools (3.21.2-1) unstable; urgency=medium * Team upload. @@ -64,7 +80,7 @@ fonttools (3.15.1-2) unstable; urgency=medium * Team upload. - * Upload to unstable + * Upload to unstable -- Hideki Yamane Sun, 03 Sep 2017 18:09:16 +0900 diff -Nru fonttools-3.21.2/debian/compat fonttools-3.29.0/debian/compat --- fonttools-3.21.2/debian/compat 2018-01-17 15:48:30.000000000 +0000 +++ fonttools-3.29.0/debian/compat 2018-09-03 03:11:59.000000000 +0000 @@ -1 +1 @@ -10 +11 diff -Nru fonttools-3.21.2/debian/control fonttools-3.29.0/debian/control --- fonttools-3.21.2/debian/control 2018-01-17 15:48:30.000000000 +0000 +++ fonttools-3.29.0/debian/control 2018-09-03 03:11:59.000000000 +0000 @@ -1,15 +1,17 @@ Source: fonttools Section: devel Priority: optional -Maintainer: Debian Fonts Task Force -Uploaders: Luke Faraone +Maintainer: Debian Fonts Task Force +Uploaders: + Luke Faraone , + Yao Wei (魏銘廷) Build-Depends: - debhelper (>= 10), + debhelper (>= 11), dh-python, gir1.2-atk-1.0 , gir1.2-gtk-3.0 , python-all, - python-brotli (>= 0.6.0) , + python-brotli (>= 1.0.1) , python-gi , python-numpy, python-pytest , @@ -17,7 +19,7 @@ python-setuptools, python-sympy , python3-all, - python3-brotli (>= 0.6.0) , + python3-brotli (>= 1.0.1) , python3-gi , python3-numpy, python3-pytest , @@ -26,12 +28,10 @@ python3-sympy , python3-sphinx, unicode-data -Standards-Version: 4.1.3 +Standards-Version: 4.1.5 Homepage: https://github.com/fonttools/fonttools Vcs-Git: https://salsa.debian.org/fonts-team/fonttools.git Vcs-Browser: https://salsa.debian.org/fonts-team/fonttools -X-Python-Version: >= 2.7 -X-Python3-Version: >= 3.4 Package: python3-fonttools Section: python @@ -39,7 +39,7 @@ Depends: gir1.2-atk-1.0, gir1.2-gtk-3.0, - python3-brotli (>= 0.6.0), + python3-brotli (>= 1.0.1), python3-gi, python3-numpy, python3-pkg-resources, @@ -50,7 +50,7 @@ ${python3:Depends} Replaces: fonttools (<< 3.15.1-3) Breaks: fonttools (<< 3.15.1-3) -Description: Converts OpenType and TrueType fonts to and from XML +Description: Converts OpenType and TrueType fonts to and from XML (Python 3 Library) FontTools/TTX is a library to manipulate font files from Python. It supports reading and writing of TrueType/OpenType fonts, reading and writing of AFM files, reading (and partially writing) of PS Type 1 @@ -65,7 +65,7 @@ Depends: gir1.2-atk-1.0, gir1.2-gtk-3.0, - python-brotli (>= 0.6.0), + python-brotli (>= 1.0.1), python-gi, python-numpy, python-pkg-resources, @@ -75,7 +75,7 @@ ${python:Depends} Replaces: fonttools (<< 3.15.1-1) Breaks: fonttools (<< 3.15.1-1) -Description: Converts OpenType and TrueType fonts to and from XML +Description: Converts OpenType and TrueType fonts to and from XML (Python 2 Library) FontTools/TTX is a library to manipulate font files from Python. It supports reading and writing of TrueType/OpenType fonts, reading and writing of AFM files, reading (and partially writing) of PS Type 1 @@ -92,7 +92,7 @@ ${misc:Depends} Replaces: fonttools (<< 3.15.1-1) Breaks: fonttools (<< 3.15.1-1) -Description: Converts OpenType and TrueType fonts to and from XML +Description: Converts OpenType and TrueType fonts to and from XML (Documentation) FontTools/TTX is a library to manipulate font files from Python. It supports reading and writing of TrueType/OpenType fonts, reading and writing of AFM files, reading (and partially writing) of PS Type 1 @@ -104,7 +104,7 @@ Package: fonttools Depends: python3, python3-fonttools, ${misc:Depends} Architecture: all -Description: Converts OpenType and TrueType fonts to and from XML +Description: Converts OpenType and TrueType fonts to and from XML (Executables) FontTools/TTX is a library to manipulate font files from Python. It supports reading and writing of TrueType/OpenType fonts, reading and writing of AFM files, reading (and partially writing) of PS Type 1 diff -Nru fonttools-3.21.2/debian/gbp.conf fonttools-3.29.0/debian/gbp.conf --- fonttools-3.21.2/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/debian/gbp.conf 2018-09-03 03:11:59.000000000 +0000 @@ -0,0 +1,3 @@ +[DEFAULT] +pristine-tar = True +patch-numbers = False diff -Nru fonttools-3.21.2/Doc/source/designspaceLib/index.rst fonttools-3.29.0/Doc/source/designspaceLib/index.rst --- fonttools-3.21.2/Doc/source/designspaceLib/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Doc/source/designspaceLib/index.rst 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,17 @@ +############## +designspaceLib +############## + +MutatorMath started out with its own reader and writer for designspaces. +Since then the use of designspace has broadened and it would be useful +to have a reader and writer that are independent of a specific system. + +.. toctree:: + :maxdepth: 1 + + readme + scripting + +.. automodule:: fontTools.designspaceLib + :members: + :undoc-members: diff -Nru fonttools-3.21.2/Doc/source/designspaceLib/readme.rst fonttools-3.29.0/Doc/source/designspaceLib/readme.rst --- fonttools-3.21.2/Doc/source/designspaceLib/readme.rst 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Doc/source/designspaceLib/readme.rst 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,1052 @@ +################################# +DesignSpaceDocument Specification +################################# + +An object to read, write and edit interpolation systems for typefaces. + +- the format was originally written for MutatorMath. +- the format is now also used in fontTools.varlib. +- Define sources, axes and instances. +- Not all values might be required by all applications. + +A couple of differences between things that use designspaces: + +- Varlib does not support anisotropic interpolations. +- MutatorMath and Superpolator will extrapolate over the boundaries of + the axes. Varlib can not (at the moment). +- Varlib requires much less data to define an instance than + MutatorMath. +- The goals of Varlib and MutatorMath are different, so not all + attributes are always needed. +- Need to expand the description of FDK use of designspace files. + +The DesignSpaceDocument object can read and write ``.designspace`` data. +It imports the axes, sources and instances to very basic **descriptor** +objects that store the data in attributes. Data is added to the document +by creating such descriptor objects, filling them with data and then +adding them to the document. This makes it easy to integrate this object +in different contexts. + +The **DesignSpaceDocument** object can be subclassed to work with +different objects, as long as they have the same attributes. + +.. code:: python + + from designSpaceDocument import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read("some/path/to/my.designspace") + doc.axes + doc.sources + doc.instances + +********** +Validation +********** + +Some validation is done when reading. + +Axes +==== + +- If the ``axes`` element is available in the document then all + locations will check their dimensions against the defined axes. If a + location uses an axis that is not defined it will be ignored. +- If there are no ``axes`` in the document, locations will accept all + axis names, so that we can.. +- Use ``doc.checkAxes()`` to reconstruct axes definitions based on the + ``source.location`` values. If you save the document the axes will be + there. + +Default font +============ + +- The source with the ``copyInfo`` flag indicates this is the default + font. +- In mutatorMath the default font is selected automatically. A warning + is printed if the mutatorMath default selection differs from the one + set by ``copyInfo``. But the ``copyInfo`` source will be used. +- If no source has a ``copyInfo`` flag, mutatorMath will be used to + select one. This source gets its ``copyInfo`` flag set. If you save + the document this flag will be set. +- Use ``doc.checkDefault()`` to set the default font. + +************ +Localisation +************ + +Some of the descriptors support localised names. The names are stored in +dictionaries using the language code as key. That means that there are +now two places to store names: the old attribute and the new localised +dictionary, ``obj.stylename`` and ``obj.localisedStyleName['en']``. + +***** +Rules +***** + +Rules describe designspace areas in which one glyph should be replaced by another. +A rule has a name and a number of conditionsets. The rule also contains a list of +glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered +**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset +**all** conditions need to be true, ``AND``. + +The ``sub`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**. + +UFO instances +============= + +- When making instances as UFOs however, we need to swap the glyphs so + that the original shape is still available. For instance, if a rule + swaps ``a`` for ``a.alt``, but a glyph that references ``a`` in a + component would then show the new ``a.alt``. +- But that can lead to unexpected results. So, if there are no rules + for ``adieresis`` (assuming it references ``a``) then that glyph + **should not change appearance**. That means that when the rule swaps + ``a`` and ``a.alt`` it also swaps all components that reference these + glyphs so they keep their appearance. +- The swap function also needs to take care of swapping the names in + kerning data. + +********** +Python API +********** + +SourceDescriptor object +======================= + +Attributes +---------- + +- ``filename``: string. A relative path to the source file, **as it is + in the document**. MutatorMath + Varlib. +- ``path``: string. Absolute path to the source file, calculated from + the document path and the string in the filename attr. MutatorMath + + Varlib. +- ``layerName``: string. The name of the layer in the source to look for + outline data. Default ``None`` which means ``foreground``. +- ``font``: Any Python object. Optional. Points to a representation of + this source font that is loaded in memory, as a Python object + (e.g. a ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). The default + document reader will not fill-in this attribute, and the default + writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by ``filename`` + and store it in this field, or write the contents of this field to the + disk and make ``filename`` point to that. +- ``name``: string. Optional. Unique identifier name for this source, + if there is one or more ``instance.glyph`` elements in the document. + MutatorMath. +- ``location``: dict. Axis values for this source. MutatorMath + Varlib +- ``copyLib``: bool. Indicates if the contents of the font.lib need to + be copied to the instances. MutatorMath. +- ``copyInfo`` bool. Indicates if the non-interpolating font.info needs + to be copied to the instances. Also indicates this source is expected + to be the default font. MutatorMath + Varlib +- ``copyGroups`` bool. Indicates if the groups need to be copied to the + instances. MutatorMath. +- ``copyFeatures`` bool. Indicates if the feature text needs to be + copied to the instances. MutatorMath. +- ``muteKerning``: bool. Indicates if the kerning data from this source + needs to be muted (i.e. not be part of the calculations). + MutatorMath. +- ``muteInfo``: bool. Indicated if the interpolating font.info data for + this source needs to be muted. MutatorMath. +- ``mutedGlyphNames``: list. Glyphnames that need to be muted in the + instances. MutatorMath. +- ``familyName``: string. Family name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. Varlib. +- ``styleName``: string. Style name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. Varlib. + +.. code:: python + + doc = DesignSpaceDocument() + s1 = SourceDescriptor() + s1.path = masterPath1 + s1.name = "master.ufo1" + s1.font = defcon.Font("master.ufo1") + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(weight=0) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + s1.mutedGlyphNames.append("A") + s1.mutedGlyphNames.append("Z") + doc.addSource(s1) + +.. _instance-descriptor-object: + +InstanceDescriptor object +========================= + +.. attributes-1: + +Attributes +---------- + +- ``filename``: string. Relative path to the instance file, **as it is + in the document**. The file may or may not exist. MutatorMath. +- ``path``: string. Absolute path to the source file, calculated from + the document path and the string in the filename attr. The file may + or may not exist. MutatorMath. +- ``name``: string. Unique identifier name of the instance, used to + identify it if it needs to be referenced from elsewhere in the + document. +- ``location``: dict. Axis values for this source. MutatorMath + + Varlib. +- ``familyName``: string. Family name of this instance. MutatorMath + + Varlib. +- ``localisedFamilyName``: dict. A dictionary of localised family name + strings, keyed by language code. +- ``styleName``: string. Style name of this source. MutatorMath + + Varlib. +- ``localisedStyleName``: dict. A dictionary of localised stylename + strings, keyed by language code. +- ``postScriptFontName``: string. Postscript fontname for this + instance. MutatorMath. +- ``styleMapFamilyName``: string. StyleMap familyname for this + instance. MutatorMath. +- ``localisedStyleMapFamilyName``: A dictionary of localised style map + familyname strings, keyed by language code. +- ``localisedStyleMapStyleName``: A dictionary of localised style map + stylename strings, keyed by language code. +- ``styleMapStyleName``: string. StyleMap stylename for this instance. + MutatorMath. +- ``glyphs``: dict for special master definitions for glyphs. If glyphs + need special masters (to record the results of executed rules for + example). MutatorMath. +- ``mutedGlyphNames``: list of glyphnames that should be suppressed in + the generation of this instance. +- ``kerning``: bool. Indicates if this instance needs its kerning + calculated. MutatorMath. +- ``info``: bool. Indicated if this instance needs the interpolating + font.info calculated. +- ``lib``: dict. Custom data associated with this instance. + +Methods +------- + +These methods give easier access to the localised names. + +- ``setStyleName(styleName, languageCode="en")`` +- ``getStyleName(languageCode="en")`` +- ``setFamilyName(familyName, languageCode="en")`` +- ``getFamilyName(self, languageCode="en")`` +- ``setStyleMapStyleName(styleMapStyleName, languageCode="en")`` +- ``getStyleMapStyleName(languageCode="en")`` +- ``setStyleMapFamilyName(styleMapFamilyName, languageCode="en")`` +- ``getStyleMapFamilyName(languageCode="en")`` + +Example +------- + +.. code:: python + + i2 = InstanceDescriptor() + i2.path = instancePath2 + i2.familyName = "InstanceFamilyName" + i2.styleName = "InstanceStyleName" + i2.name = "instance.ufo2" + # anisotropic location + i2.location = dict(weight=500, width=(400,300)) + i2.postScriptFontName = "InstancePostscriptName" + i2.styleMapFamilyName = "InstanceStyleMapFamilyName" + i2.styleMapStyleName = "InstanceStyleMapStyleName" + glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] + glyphData = dict(name="arrow", unicodeValue=1234) + glyphData['masters'] = glyphMasters + glyphData['note'] = "A note about this glyph" + glyphData['instanceLocation'] = dict(width=100, weight=120) + i2.glyphs['arrow'] = glyphData + i2.glyphs['arrow2'] = dict(mute=False) + i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever' + doc.addInstance(i2) + +.. _axis-descriptor-object: + +AxisDescriptor object +===================== + +- ``tag``: string. Four letter tag for this axis. Some might be + registered at the `OpenType + specification `__. + Privately-defined axis tags must begin with an uppercase letter and + use only uppercase letters or digits. +- ``name``: string. Name of the axis as it is used in the location + dicts. MutatorMath + Varlib. +- ``labelNames``: dict. When defining a non-registered axis, it will be + necessary to define user-facing readable names for the axis. Keyed by + xml:lang code. Varlib. +- ``minimum``: number. The minimum value for this axis. MutatorMath + + Varlib. +- ``maximum``: number. The maximum value for this axis. MutatorMath + + Varlib. +- ``default``: number. The default value for this axis, i.e. when a new + location is created, this is the value this axis will get. + MutatorMath + Varlib. +- ``map``: list of input / output values that can describe a warp of + user space to designspace coordinates. If no map values are present, + it is assumed it is [(minimum, minimum), (maximum, maximum)]. Varlib. + +.. code:: python + + a1 = AxisDescriptor() + a1.minimum = 1 + a1.maximum = 1000 + a1.default = 400 + a1.name = "weight" + a1.tag = "wght" + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)] + +RuleDescriptor object +===================== + +- ``name``: string. Unique name for this rule. Can be used to + reference this rule data. +- ``conditionSets``: a list of conditionsets +- Each conditionset is a list of conditions. +- Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys. +- ``subs``: list of substitutions +- Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt"). + +.. code:: python + + r1 = RuleDescriptor() + r1.name = "unique.rule.name" + r1.conditionsSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)]) + r1.conditionsSets.append([dict(...), dict(...)]) + r1.subs.append(("a", "a.alt")) + +.. _subclassing-descriptors: + +Subclassing descriptors +======================= + +The DesignSpaceDocument can take subclassed Reader and Writer objects. +This allows you to work with your own descriptors. You could subclass +the descriptors. But as long as they have the basic attributes the +descriptor does not need to be a subclass. + +.. code:: python + + class MyDocReader(BaseDocReader): + ruleDescriptorClass = MyRuleDescriptor + axisDescriptorClass = MyAxisDescriptor + sourceDescriptorClass = MySourceDescriptor + instanceDescriptorClass = MyInstanceDescriptor + + class MyDocWriter(BaseDocWriter): + ruleDescriptorClass = MyRuleDescriptor + axisDescriptorClass = MyAxisDescriptor + sourceDescriptorClass = MySourceDescriptor + instanceDescriptorClass = MyInstanceDescriptor + + myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter) + +********************** +Document xml structure +********************** + +- The ``axes`` element contains one or more ``axis`` elements. +- The ``sources`` element contains one or more ``source`` elements. +- The ``instances`` element contains one or more ``instance`` elements. +- The ``rules`` element contains one or more ``rule`` elements. +- The ``lib`` element contains arbitrary data. + +.. code:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + +.. 1-axis-element: + +1. axis element +=============== + +- Define a single axis +- Child element of ``axes`` + +.. attributes-2: + +Attributes +---------- + +- ``name``: required, string. Name of the axis that is used in the + location elements. +- ``tag``: required, string, 4 letters. Some axis tags are registered + in the OpenType Specification. +- ``minimum``: required, number. The minimum value for this axis. +- ``maximum``: required, number. The maximum value for this axis. +- ``default``: required, number. The default value for this axis. +- ``hidden``: optional, 0 or 1. Records whether this axis needs to be + hidden in interfaces. + +.. code:: xml + + + +.. 11-labelname-element: + +1.1 labelname element +===================== + +- Defines a human readable name for UI use. +- Optional for non-registered axis names. +- Can be localised with ``xml:lang`` +- Child element of ``axis`` + +.. attributes-3: + +Attributes +---------- + +- ``xml:lang``: required, string. `XML language + definition `__ + +Value +----- + +- The natural language name of this axis. + +.. example-1: + +Example +------- + +.. code:: xml + + قطر + Wéíght + +.. 12-map-element: + +1.2 map element +=============== + +- Defines a single node in a series of input value / output value + pairs. +- Together these values transform the designspace. +- Child of ``axis`` element. + +.. example-2: + +Example +------- + +.. code:: xml + + + + + +Example of all axis elements together: +-------------------------------------- + +.. code:: xml + + + + قطر + Wéíght + + + + + + + + +.. 2-location-element: + +2. location element +=================== + +- Defines a coordinate in the design space. +- Dictionary of axisname: axisvalue +- Used in ``source``, ``instance`` and ``glyph`` elements. + +.. 21-dimension-element: + +2.1 dimension element +===================== + +- Child element of ``location`` + +.. attributes-4: + +Attributes +---------- + +- ``name``: required, string. Name of the axis. +- ``xvalue``: required, number. The value on this axis. +- ``yvalue``: optional, number. Separate value for anisotropic + interpolations. + +.. example-3: + +Example +------- + +.. code:: xml + + + + + + +.. 3-source-element: + +3. source element +================= + +- Defines a single font that contributes to the designspace. +- Child element of ``sources`` + +.. attributes-5: + +Attributes +---------- + +- ``familyname``: optional, string. The family name of the source font. + While this could be extracted from the font data itself, it can be + more efficient to add it here. +- ``stylename``: optional, string. The style name of the source font. +- ``name``: required, string. A unique name that can be used to + identify this font if it needs to be referenced elsewhere. +- ``filename``: required, string. A path to the source file, relative + to the root path of this document. The path can be at the same level + as the document or lower. +- ``layer``: optional, string. The name of the layer in the source file. + If no layer attribute is given assume the foreground layer should be used. + +.. 31-lib-element: + +3.1 lib element +=============== + +There are two meanings for the ``lib`` element: + +1. Source lib + - Example: ```` + - Child element of ``source`` + - Defines if the instances can inherit the data in the lib of this + source. + - MutatorMath only + +2. Document and instance lib + - Example: + + .. code:: xml + + + + ... + The contents use the PLIST format. + + + + - Child element of ``designspace`` and ``instance`` + - Contains arbitrary data about the whole document or about a specific + instance. + - Items in the dict need to use **reverse domain name notation** __ + +.. 32-info-element: + +3.2 info element +================ + +- ```` +- Child element of ``source`` +- Defines if the instances can inherit the non-interpolating font info + from this source. +- MutatorMath + Varlib +- NOTE: **This presence of this element indicates this source is to be + the default font.** + +.. 33-features-element: + +3.3 features element +==================== + +- ```` +- Defines if the instances can inherit opentype feature text from this + source. +- Child element of ``source`` +- MutatorMath only + +.. 34-glyph-element: + +3.4 glyph element +================= + +- Can appear in ``source`` as well as in ``instance`` elements. +- In a ``source`` element this states if a glyph is to be excluded from + the calculation. +- MutatorMath only + +.. attributes-6: + +Attributes +---------- + +- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph + should be ignored as a master. +- ```` +- MutatorMath only + +.. 35-kerning-element: + +3.5 kerning element +=================== + +- ```` +- Can appear in ``source`` as well as in ``instance`` elements. + +.. attributes-7: + +Attributes +---------- + +- ``mute``: required attribute, number 1 or 0. Indicate if the kerning + data from this source is to be excluded from the calculation. +- If the kerning element is not present, assume ``mute=0``, yes, + include the kerning of this source in the calculation. +- MutatorMath only + +.. example-4: + +Example +------- + +.. code:: xml + + + + + + + + + + + + + +.. 4-instance-element: + +4. instance element +=================== + +- Defines a single font that can be calculated with the designspace. +- Child element of ``instances`` +- For use in Varlib the instance element really only needs the names + and the location. The ``glyphs`` element is not required. +- MutatorMath uses the ``glyphs`` element to describe how certain + glyphs need different masters, mainly to describe the effects of + conditional rules in Superpolator. + +.. attributes-8: + +Attributes +---------- + +- ``familyname``: required, string. The family name of the instance + font. Corresponds with ``font.info.familyName`` +- ``stylename``: required, string. The style name of the instance font. + Corresponds with ``font.info.styleName`` +- ``name``: required, string. A unique name that can be used to + identify this font if it needs to be referenced elsewhere. +- ``filename``: string. Required for MutatorMath. A path to the + instance file, relative to the root path of this document. The path + can be at the same level as the document or lower. +- ``postscriptfontname``: string. Optional for MutatorMath. Corresponds + with ``font.info.postscriptFontName`` +- ``stylemapfamilyname``: string. Optional for MutatorMath. Corresponds + with ``styleMapFamilyName`` +- ``stylemapstylename``: string. Optional for MutatorMath. Corresponds + with ``styleMapStyleName`` + +Example for varlib +------------------ + +.. code:: xml + + + + + + + + + + + com.coolDesignspaceApp.specimenText + Hamburgerwhatever + + + + +.. 41-glyphs-element: + +4.1 glyphs element +================== + +- Container for ``glyph`` elements. +- Optional +- MutatorMath only. + +.. 42-glyph-element: + +4.2 glyph element +================= + +- Child element of ``glyphs`` +- May contain a ``location`` element. + +.. attributes-9: + +Attributes +---------- + +- ``name``: string. The name of the glyph. +- ``unicode``: string. Unicode values for this glyph, in hexadecimal. + Multiple values should be separated with a space. +- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph + should be supressed in the output. + +.. 421-note-element: + +4.2.1 note element +================== + +- String. The value corresponds to glyph.note in UFO. + +.. 422-masters-element: + +4.2.2 masters element +===================== + +- Container for ``master`` elements +- These ``master`` elements define an alternative set of glyph masters + for this glyph. + +.. 4221-master-element: + +4.2.2.1 master element +====================== + +- Defines a single alternative master for this glyph. + +4.3 Localised names for instances +================================= + +Localised names for instances can be included with these simple elements +with an ``xml:lang`` attribute: +`XML language definition `__ + +- stylename +- familyname +- stylemapstylename +- stylemapfamilyname + +.. example-5: + +Example +------- + +.. code:: xml + + Demigras + 半ば + Montserrat + モンセラート + Standard + Montserrat Halbfett + モンセラート SemiBold + +.. attributes-10: + +Attributes +---------- + +- ``glyphname``: the name of the alternate master glyph. +- ``source``: the identifier name of the source this master glyph needs + to be loaded from + +.. example-6: + +Example +------- + +.. code:: xml + + + + + + + + + + + + + + A note about this glyph + + + + + + + + + + + + + + + com.coolDesignspaceApp.specimenText + Hamburgerwhatever + + + + +.. 50-rules-element: + +5.0 rules element +================= + +- Container for ``rule`` elements +- The rules are evaluated in this order. + +.. 51-rule-element: + +5.1 rule element +================ + +- Defines a named rule. +- Each ``rule`` element contains one or more ``conditionset`` elements. +- Only one ``conditionset`` needs to be true to trigger the rule. +- All conditions in a ``conditionset`` must be true to make the ``conditionset`` true. +- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``. Note: these conditions should be written wrapped in a conditionset. +- A rule element needs to contain one or more ``sub`` elements in order to be compiled to a variable font. +- Rules without sub elements should be ignored when compiling a font. +- For authoring tools it might be necessary to save designspace files without ``sub`` elements just because the work is incomplete. + +.. attributes-11: + +Attributes +---------- + +- ``name``: optional, string. A unique name that can be used to + identify this rule if it needs to be referenced elsewhere. The name + is not important for compiling variable fonts. + +5.1.1 conditionset element +======================= + +- Child element of ``rule`` +- Contains one or more ``condition`` elements. + +.. 512-condition-element: + +5.1.2 condition element +======================= + +- Child element of ``conditionset`` +- Between the ``minimum`` and ``maximum`` this rule is ``True``. +- If ``minimum`` is not available, assume it is ``axis.minimum``. +- If ``maximum`` is not available, assume it is ``axis.maximum``. +- The condition must contain at least a minimum or maximum or both. + +.. attributes-12: + +Attributes +---------- + +- ``name``: string, required. Must match one of the defined ``axis`` + name attributes. +- ``minimum``: number, required*. The low value. +- ``maximum``: number, required*. The high value. + +.. 513-sub-element: + +5.1.3 sub element +================= + +- Child element of ``rule``. +- Defines which glyph to replace when the rule evaluates to **True**. + +.. attributes-13: + +Attributes +---------- + +- ``name``: string, required. The name of the glyph this rule looks + for. +- ``with``: string, required. The name of the glyph it is replaced + with. + +.. example-7: + +Example +------- + +Example with an implied ``conditionset``. Here the conditions are not +contained in a conditionset. + +.. code:: xml + + + + + + + + + +Example with ``conditionsets``. All conditions in a conditionset must be true. + +.. code:: xml + + + + + + + + + + + + + + + +.. 6-notes: + +6 Notes +======= + +Paths and filenames +------------------- + +A designspace file needs to store many references to UFO files. + +- designspace files can be part of versioning systems and appear on + different computers. This means it is not possible to store absolute + paths. +- So, all paths are relative to the designspace document path. +- Using relative paths allows designspace files and UFO files to be + **near** each other, and that they can be **found** without enforcing + one particular structure. +- The **filename** attribute in the ``SourceDescriptor`` and + ``InstanceDescriptor`` classes stores the preferred relative path. +- The **path** attribute in these objects stores the absolute path. It + is calculated from the document path and the relative path in the + filename attribute when the object is created. +- Only the **filename** attribute is written to file. +- Both **filename** and **path** must use forward slashes (``/``) as + path separators, even on Windows. + +Right before we save we need to identify and respond to the following +situations: + +In each descriptor, we have to do the right thing for the filename +attribute. Before writing to file, the ``documentObject.updatePaths()`` +method prepares the paths as follows: + +**Case 1** + +:: + + descriptor.filename == None + descriptor.path == None + +**Action** + +- write as is, descriptors will not have a filename attr. Useless, but + no reason to interfere. + +**Case 2** + +:: + + descriptor.filename == "../something" + descriptor.path == None + +**Action** + +- write as is. The filename attr should not be touched. + +**Case 3** + +:: + + descriptor.filename == None + descriptor.path == "~/absolute/path/there" + +**Action** + +- calculate the relative path for filename. We're not overwriting some + other value for filename, it should be fine. + +**Case 4** + +:: + + descriptor.filename == '../somewhere' + descriptor.path == "~/absolute/path/there" + +**Action** + +- There is a conflict between the given filename, and the path. The + difference could have happened for any number of reasons. Assuming + the values were not in conflict when the object was created, either + could have changed. We can't guess. +- Assume the path attribute is more up to date. Calculate a new value + for filename based on the path and the document path. + +Recommendation for editors +-------------------------- + +- If you want to explicitly set the **filename** attribute, leave the + path attribute empty. +- If you want to explicitly set the **path** attribute, leave the + filename attribute empty. It will be recalculated. +- Use ``documentObject.updateFilenameFromPath()`` to explicitly set the + **filename** attributes for all instance and source descriptors. + +.. 7-this-document: + +7 This document +=============== + +- The package is rather new and changes are to be expected. diff -Nru fonttools-3.21.2/Doc/source/designspaceLib/scripting.rst fonttools-3.29.0/Doc/source/designspaceLib/scripting.rst --- fonttools-3.21.2/Doc/source/designspaceLib/scripting.rst 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Doc/source/designspaceLib/scripting.rst 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,253 @@ +####################### +Scripting a designspace +####################### + +It can be useful to build a designspace with a script rather than +construct one with an interface like +`Superpolator `__ or +`DesignSpaceEditor `__. + +`fontTools.designspaceLib` offers a some tools for building designspaces in +Python. This document shows an example. + +******************************** +Filling-in a DesignSpaceDocument +******************************** + +So, suppose you installed the `fontTools` package through your favorite +``git`` client. + +The ``DesignSpaceDocument`` object represents the document, whether it +already exists or not. Make a new one: + +.. code:: python + + from fontTools.designspaceLib import (DesignSpaceDocument, AxisDescriptor, + SourceDescriptor, InstanceDescriptor) + doc = DesignSpaceDocument() + +We want to create definitions for axes, sources and instances. That +means there are a lot of attributes to set. The **DesignSpaceDocument +object** uses objects to describe the axes, sources and instances. These +are relatively simple objects, think of these as collections of +attributes. + +- Attributes of the :ref:`source-descriptor-object` +- Attributes of the :ref:`instance-descriptor-object` +- Attributes of the :ref:`axis-descriptor-object` +- Read about :ref:`subclassing-descriptors` + +Make an axis object +=================== + +Make a descriptor object and add it to the document. + +.. code:: python + + a1 = AxisDescriptor() + a1.maximum = 1000 + a1.minimum = 0 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + doc.addAxis(a1) + +- You can add as many axes as you need. OpenType has a maximum of + around 64K. DesignSpaceEditor has a maximum of 5. +- The ``name`` attribute is the name you'll be using as the axis name + in the locations. +- The ``tag`` attribute is the one of the registered `OpenType + Variation Axis + Tags `__ +- The default master is expected at the intersection of all + default values of all axes. + +Option: add label names +----------------------- + +The **labelnames** attribute is intended to store localisable, human +readable names for this axis if this is not an axis that is registered +by OpenType. Think "The label next to the slider". The attribute is a +dictionary. The key is the `xml language +tag `__, the +value is a utf-8 string with the name. Whether or not this attribute is +used depends on the font building tool, the operating system and the +authoring software. This, at least, is the place to record it. + +.. code:: python + + a1.labelNames['fa-IR'] = u"قطر" + a1.labelNames['en'] = u"Wéíght" + +Option: add a map +----------------- + +The **map** attribute is a list of (input, output) mapping values +intended for `axis variations table of +OpenType `__. + +.. code:: python + + a1.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + +Make a source object +==================== + +A **source** is an object that points to a UFO file. It provides the +outline geometry, kerning and font.info that we want to work with. + +.. code:: python + + s0 = SourceDescriptor() + s0.path = "my/path/to/thin.ufo" + s0.name = "master.thin" + s0.location = dict(weight=0) + doc.addSource(s0) + +- You'll need to have at least 2 sources in your document, so go ahead + and add another one. +- The **location** attribute is a dictionary with the designspace + location for this master. +- The axis names in the location have to match one of the ``axis.name`` + values you defined before. +- The **path** attribute is the absolute path to an existing UFO. +- The **name** attribute is a unique name for this source used to keep + track it. +- The **layerName** attribute is the name of the UFO3 layer. Default None for ``foreground``. + +So go ahead and add another master: + +.. code:: python + + s1 = SourceDescriptor() + s1.path = "my/path/to/bold.ufo" + s1.name = "master.bold" + s1.location = dict(weight=1000) + doc.addSource(s1) + + +Option: exclude glyphs +---------------------- + +By default all glyphs in a source will be processed. If you want to +exclude certain glyphs, add their names to the ``mutedGlyphNames`` list. + +.. code:: python + + s1.mutedGlyphNames = ["A.test", "A.old"] + +Make an instance object +======================= + +An **instance** is description of a UFO that you want to generate with +the designspace. For an instance you can define more things. If you want +to generate UFO instances with MutatorMath then you can define different +names and set flags for if you want to generate kerning and font info +and so on. You can also set a path where to generate the instance. + +.. code:: python + + i0 = InstanceDescriptor() + i0.familyName = "MyVariableFontPrototype" + i0.styleName = "Medium" + i0.path = os.path.join(root, "instances","MyVariableFontPrototype-Medium.ufo") + i0.location = dict(weight=500) + i0.kerning = True + i0.info = True + doc.addInstance(i0) + +- The ``path`` attribute needs to be the absolute (real or intended) + path for the instance. When the document is saved this path will + written as relative to the path of the document. +- instance paths should be on the same level as the document, or in a + level below. +- Instances for MutatorMath will generate to UFO. +- Instances for variable fonts become **named instances**. + +Option: add more names +---------------------- + +If you want you can add a PostScript font name, a stylemap familyName +and a stylemap styleName. + +.. code:: python + + i0.postScriptFontName = "MyVariableFontPrototype-Medium" + i0.styleMapFamilyName = "MyVarProtoMedium" + i0.styleMapStyleName = "regular" + +Option: add glyph specific masters +---------------------------------- + +This bit is not supported by OpenType variable fonts, but it is needed +for some designspaces intended for generating instances with +MutatorMath. The code becomes a bit verbose, so you're invited to wrap +this into something clever. + +.. code:: python + + # we're making a dict with all sorts of + #(optional) settings for a glyph. + #In this example: the dollar. + glyphData = dict(name="dollar", unicodeValue=0x24) + + # you can specify a different location for a glyph + glyphData['instanceLocation'] = dict(weight=500) + + # You can specify different masters + # for this specific glyph. + # You can also give those masters new + # locations. It's a miniature designspace. + # Remember the "name" attribute we assigned to the sources? + glyphData['masters'] = [ + dict(font="master.thin", + glyphName="dollar.nostroke", + location=dict(weight=0)), + dict(font="master.bold", + glyphName="dollar.nostroke", + location=dict(weight=1000)), + ] + + # With all of that set up, store it in the instance. + i4.glyphs['dollar'] = glyphData + +****** +Saving +****** + +.. code:: python + + path = "myprototype.designspace" + doc.write(path) + +************************ +Reading old designspaces +************************ + +Old designspace files might not contain ``axes`` definitions. This is +how you reconstruct the axes from the extremes of the source locations + +.. code:: python + + doc.checkAxes() + +This is how you check the default font. + +.. code:: python + + doc.checkDefault() + +*********** +Generating? +*********** + +You can generate the UFO's with MutatorMath: + +.. code:: python + + from mutatorMath.ufo import build + build("whatevs/myprototype.designspace") + +- Assuming the outline data in the masters is compatible. + +Or you can use the file in making a **variable font** with varlib. diff -Nru fonttools-3.21.2/Doc/source/index.rst fonttools-3.29.0/Doc/source/index.rst --- fonttools-3.21.2/Doc/source/index.rst 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Doc/source/index.rst 2018-07-26 14:12:55.000000000 +0000 @@ -7,6 +7,7 @@ afmLib agl cffLib + designspaceLib/index inspect encodings feaLib diff -Nru fonttools-3.21.2/.gitignore fonttools-3.29.0/.gitignore --- fonttools-3.21.2/.gitignore 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/.gitignore 2018-07-26 14:12:55.000000000 +0000 @@ -23,3 +23,5 @@ # OSX Finder .DS_Store + +.pytest_cache Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconGreenCircle.pdf and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconGreenCircle.pdf differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconGreenCircle.png and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconGreenCircle.png differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconGreenSquare.pdf and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconGreenSquare.pdf differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconGreenSquare.png and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconGreenSquare.png differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconWhiteCircle.pdf and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconWhiteCircle.pdf differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconWhiteCircle.png and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconWhiteCircle.png differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconWhiteSquare.pdf and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconWhiteSquare.pdf differ Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Icons/FontToolsIconWhiteSquare.png and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Icons/FontToolsIconWhiteSquare.png differ diff -Nru fonttools-3.21.2/Lib/fontTools/cffLib/__init__.py fonttools-3.29.0/Lib/fontTools/cffLib/__init__.py --- fonttools-3.21.2/Lib/fontTools/cffLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/cffLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -144,7 +144,7 @@ writer.toFile(file) - def toXML(self, xmlWriter, progress=None): + def toXML(self, xmlWriter): xmlWriter.simpletag("major", value=self.major) xmlWriter.newline() xmlWriter.simpletag("minor", value=self.minor) @@ -153,13 +153,13 @@ xmlWriter.begintag("CFFFont", name=tostr(fontName)) xmlWriter.newline() font = self[fontName] - font.toXML(xmlWriter, progress) + font.toXML(xmlWriter) xmlWriter.endtag("CFFFont") xmlWriter.newline() xmlWriter.newline() xmlWriter.begintag("GlobalSubrs") xmlWriter.newline() - self.GlobalSubrs.toXML(xmlWriter, progress) + self.GlobalSubrs.toXML(xmlWriter) xmlWriter.endtag("GlobalSubrs") xmlWriter.newline() @@ -244,7 +244,7 @@ if key in topDict.rawDict: del topDict.rawDict[key] if hasattr(topDict, key): - exec("del topDict.%s" % (key)) + delattr(topDict, key) if not hasattr(topDict, "FDArray"): fdArray = topDict.FDArray = FDArrayIndex() @@ -257,6 +257,7 @@ else: charStrings.fdArray = fdArray fontDict = FontDict() + fontDict.setCFF2(True) fdArray.append(fontDict) fontDict.Private = privateDict privateOpOrder = buildOrder(privateDictOperators2) @@ -267,12 +268,20 @@ # print "Removing private dict", key del privateDict.rawDict[key] if hasattr(privateDict, key): - exec("del privateDict.%s" % (key)) + delattr(privateDict, key) # print "Removing privateDict attr", key else: # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray privateOpOrder = buildOrder(privateDictOperators2) for fontDict in fdArray: + fontDict.setCFF2(True) + for key in fontDict.rawDict.keys(): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + privateDict = fontDict.Private for entry in privateDictOperators: key = entry[1] @@ -281,7 +290,7 @@ # print "Removing private dict", key del privateDict.rawDict[key] if hasattr(privateDict, key): - exec("del privateDict.%s" % (key)) + delattr(privateDict, key) # print "Removing privateDict attr", key # At this point, the Subrs and Charstrings are all still T2Charstring class # easiest to fix this by compiling, then decompiling again @@ -633,7 +642,7 @@ private = None return self.subrClass(data, private=private, globalSubrs=self.globalSubrs) - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): xmlWriter.comment( "The 'index' attribute is only for humans; " "it is ignored when parsed.") @@ -698,11 +707,11 @@ top.decompile(data) return top - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for i in range(len(self)): xmlWriter.begintag("FontDict", index=i) xmlWriter.newline() - self[i].toXML(xmlWriter, progress) + self[i].toXML(xmlWriter) xmlWriter.endtag("FontDict") xmlWriter.newline() @@ -711,11 +720,11 @@ compilerClass = FDArrayIndexCompiler - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for i in range(len(self)): xmlWriter.begintag("FontDict", index=i) xmlWriter.newline() - self[i].toXML(xmlWriter, progress) + self[i].toXML(xmlWriter) xmlWriter.endtag("FontDict") xmlWriter.newline() @@ -906,11 +915,8 @@ sel = None return self.charStrings[name], sel - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): names = sorted(self.keys()) - i = 0 - step = 10 - numGlyphs = len(names) for name in names: charStr, fdSelectIndex = self.getItemAndSelector(name) if charStr.needsDecompilation(): @@ -927,10 +933,6 @@ charStr.toXML(xmlWriter) xmlWriter.endtag("CharString") xmlWriter.newline() - if not i % step and progress is not None: - progress.setLabel("Dumping 'CFF ' table... (%s)" % name) - progress.increment(step / numGlyphs) - i = i + 1 def fromXML(self, name, attrs, content): for element in content: @@ -1037,12 +1039,22 @@ class SimpleConverter(object): def read(self, parent, value): + if not hasattr(parent, "file"): + return self._read(parent, value) + file = parent.file + pos = file.tell() + try: + return self._read(parent, value) + finally: + file.seek(pos) + + def _read(self, parent, value): return value def write(self, parent, value): return value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, value=value) xmlWriter.newline() @@ -1052,13 +1064,13 @@ class ASCIIConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): return tostr(value, encoding='ascii') def write(self, parent, value): return tobytes(value, encoding='ascii') - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, value=tounicode(value, encoding="ascii")) xmlWriter.newline() @@ -1068,13 +1080,13 @@ class Latin1Converter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): return tostr(value, encoding='latin1') def write(self, parent, value): return tobytes(value, encoding='latin1') - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): value = tounicode(value, encoding="latin1") if name in ['Notice', 'Copyright']: value = re.sub(r"[\r\n]\s+", " ", value) @@ -1108,7 +1120,7 @@ class NumberConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if isinstance(value, list): xmlWriter.begintag(name) xmlWriter.newline() @@ -1133,7 +1145,7 @@ class ArrayConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if value and isinstance(value[0], list): xmlWriter.begintag(name) xmlWriter.newline() @@ -1162,10 +1174,10 @@ class TableConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.begintag(name) xmlWriter.newline() - value.toXML(xmlWriter, progress) + value.toXML(xmlWriter) xmlWriter.endtag(name) xmlWriter.newline() @@ -1184,7 +1196,7 @@ def getClass(self): return PrivateDict - def read(self, parent, value): + def _read(self, parent, value): size, offset = value file = parent.file isCFF2 = parent._isCFF2 @@ -1209,7 +1221,7 @@ def getClass(self): return SubrsIndex - def read(self, parent, value): + def _read(self, parent, value): file = parent.file isCFF2 = parent._isCFF2 file.seek(parent.offset + value) # Offset(self) @@ -1221,7 +1233,7 @@ class CharStringsConverter(TableConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file isCFF2 = parent._isCFF2 charset = parent.charset @@ -1265,8 +1277,8 @@ return charStrings -class CharsetConverter(object): - def read(self, parent, value): +class CharsetConverter(SimpleConverter): + def _read(self, parent, value): isCID = hasattr(parent, "ROS") if value > 2: numGlyphs = parent.numGlyphs @@ -1301,7 +1313,7 @@ def write(self, parent, value): return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): # XXX only write charset when not in OT/TTX context, where we # dump charset as a separate "GlyphOrder" table. # # xmlWriter.simpletag("charset") @@ -1471,7 +1483,7 @@ class EncodingConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): if value == 0: return "StandardEncoding" elif value == 1: @@ -1501,7 +1513,7 @@ return 1 return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if value in ("StandardEncoding", "ExpertEncoding"): xmlWriter.simpletag(name, name=value) xmlWriter.newline() @@ -1613,7 +1625,7 @@ class FDArrayConverter(TableConverter): - def read(self, parent, value): + def _read(self, parent, value): try: vstore = parent.VarStore except AttributeError: @@ -1640,9 +1652,9 @@ return fdArray -class FDSelectConverter(object): +class FDSelectConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file file.seek(value) fdSelect = FDSelect(file, parent.numGlyphs) @@ -1653,7 +1665,7 @@ # The FDSelect glyph data is written out to XML in the charstring keys, # so we write out only the format selector - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, [('format', value.format)]) xmlWriter.newline() @@ -1667,7 +1679,7 @@ class VarStoreConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file file.seek(value) varStore = VarStoreData(file) @@ -1677,7 +1689,7 @@ def write(self, parent, value): return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): value.writeXML(xmlWriter, name) def xmlRead(self, name, attrs, content, parent): @@ -1771,7 +1783,7 @@ class ROSConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): registry, order, supplement = value xmlWriter.simpletag( name, @@ -2245,7 +2257,7 @@ setattr(self, name, value) return value - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for name in self.order: if name in self.skipNames: continue @@ -2262,7 +2274,7 @@ if value is None and name != "charset": continue conv = self.converters[name] - conv.xmlWrite(xmlWriter, name, value, progress) + conv.xmlWrite(xmlWriter, name, value) ignoredNames = set(self.rawDict) - set(self.order) if ignoredNames: xmlWriter.comment( @@ -2310,9 +2322,9 @@ else: self.numGlyphs = readCard16(self.file) - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): if hasattr(self, "CharStrings"): - self.decompileAllCharStrings(progress) + self.decompileAllCharStrings() if hasattr(self, "ROS"): self.skipNames = ['Encoding'] if not hasattr(self, "ROS") or not hasattr(self, "CharStrings"): @@ -2320,25 +2332,21 @@ # in CID fonts. self.skipNames = [ 'CIDFontVersion', 'CIDFontRevision', 'CIDFontType', 'CIDCount'] - BaseDict.toXML(self, xmlWriter, progress) + BaseDict.toXML(self, xmlWriter) - def decompileAllCharStrings(self, progress): + def decompileAllCharStrings(self): # Make sure that all the Private Dicts have been instantiated. - i = 0 for charString in self.CharStrings.values(): try: charString.decompile() except: log.error("Error in charstring %s", i) raise - if not i % 30 and progress: - progress.increment(0) # update - i = i + 1 def recalcFontBBox(self): fontBBox = None for charString in self.CharStrings.values(): - bounds = charString.calcBounds() + bounds = charString.calcBounds(self.CharStrings) if bounds is not None: if fontBBox is not None: fontBBox = unionRect(fontBBox, bounds) @@ -2382,13 +2390,24 @@ defaults = {} converters = buildConverters(topDictOperators) compilerClass = FontDictCompiler - order = ['FontName', 'FontMatrix', 'Weight', 'Private'] + orderCFF = ['FontName', 'FontMatrix', 'Weight', 'Private'] + orderCFF2 = ['Private'] decompilerClass = TopDictDecompiler def __init__(self, strings=None, file=None, offset=None, GlobalSubrs=None, isCFF2=None, vstore=None): super(FontDict, self).__init__(strings, file, offset, isCFF2=isCFF2) self.vstore = vstore + self.setCFF2(isCFF2) + + def setCFF2(self, isCFF2): + # isCFF2 may be None. + if isCFF2: + self.order = self.orderCFF2 + self._isCFF2 = True + else: + self.order = self.orderCFF + self._isCFF2 = False class PrivateDict(BaseDict): diff -Nru fonttools-3.21.2/Lib/fontTools/cffLib/specializer.py fonttools-3.29.0/Lib/fontTools/cffLib/specializer.py --- fonttools-3.21.2/Lib/fontTools/cffLib/specializer.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/cffLib/specializer.py 2018-07-26 14:12:55.000000000 +0000 @@ -436,7 +436,6 @@ if op[2:] == 'curveto' and len(args) == 5 and prv == nxt == 'rrcurveto': assert (op[0] == 'r') ^ (op[1] == 'r') - args = list(args) if op[0] == 'v': pos = 0 elif op[0] != 'r': @@ -445,7 +444,8 @@ pos = 4 else: pos = 5 - args.insert(pos, 0) + # Insert, while maintaining the type of args (can be tuple or list). + args = args[:pos] + type(args)((0,)) + args[pos:] commands[i] = ('rrcurveto', args) continue @@ -493,7 +493,9 @@ if d0 is None: continue new_op = d0+d+'curveto' - if new_op and len(args1) + len(args2) <= maxstack: + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + if new_op and len(args1) + len(args2) < maxstack: commands[i-1] = (new_op, args1+args2) del commands[i] diff -Nru fonttools-3.21.2/Lib/fontTools/cffLib/width.py fonttools-3.29.0/Lib/fontTools/cffLib/width.py --- fonttools-3.21.2/Lib/fontTools/cffLib/width.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/cffLib/width.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +"""T2CharString glyph width optimizer.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, getTableClass +from collections import defaultdict +from operator import add +from functools import partial, reduce + + +class missingdict(dict): + def __init__(self, missing_func): + self.missing_func = missing_func + def __missing__(self, v): + return self.missing_func(v) + +def cumSum(f, op=add, start=0, decreasing=False): + + keys = sorted(f.keys()) + minx, maxx = keys[0], keys[-1] + + total = reduce(op, f.values(), start) + + if decreasing: + missing = lambda x: start if x > maxx else total + domain = range(maxx, minx - 1, -1) + else: + missing = lambda x: start if x < minx else total + domain = range(minx, maxx + 1) + + out = missingdict(missing) + + v = start + for x in domain: + v = op(v, f[x]) + out[x] = v + + return out + +def byteCost(widths, default, nominal): + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + cost = 0 + for w,freq in widths.items(): + if w == default: continue + diff = abs(w - nominal) + if diff <= 107: + cost += freq + elif diff <= 1131: + cost += freq * 2 + else: + cost += freq * 5 + return cost + + +def optimizeWidthsBruteforce(widths): + """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" + + d = defaultdict(int) + for w in widths: + d[w] += 1 + + # Maximum number of bytes using default can possibly save + maxDefaultAdvantage = 5 * max(d.values()) + + minw, maxw = min(widths), max(widths) + domain = list(range(minw, maxw+1)) + + bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) + + bestCost = len(widths) * 5 + 1 + for nominal in domain: + if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: + continue + for default in domain: + cost = byteCost(widths, default, nominal) + if cost < bestCost: + bestCost = cost + bestDefault = default + bestNominal = nominal + + return bestDefault, bestNominal + + +def optimizeWidths(widths): + """Given a list of glyph widths, or dictionary mapping glyph width to number of + glyphs having that, returns a tuple of best CFF default and nominal glyph widths. + + This algorithm is linear in UPEM+numGlyphs.""" + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + keys = sorted(widths.keys()) + minw, maxw = keys[0], keys[-1] + domain = list(range(minw, maxw+1)) + + # Cumulative sum/max forward/backward. + cumFrqU = cumSum(widths, op=add) + cumMaxU = cumSum(widths, op=max) + cumFrqD = cumSum(widths, op=add, decreasing=True) + cumMaxD = cumSum(widths, op=max, decreasing=True) + + # Cost per nominal choice, without default consideration. + nomnCostU = missingdict(lambda x: cumFrqU[x] + cumFrqU[x-108] + cumFrqU[x-1132]*3) + nomnCostD = missingdict(lambda x: cumFrqD[x] + cumFrqD[x+108] + cumFrqD[x+1132]*3) + nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) + + # Cost-saving per nominal choice, by best default choice. + dfltCostU = missingdict(lambda x: max(cumMaxU[x], cumMaxU[x-108]*2, cumMaxU[x-1132]*5)) + dfltCostD = missingdict(lambda x: max(cumMaxD[x], cumMaxD[x+108]*2, cumMaxD[x+1132]*5)) + dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) + + # Combined cost per nominal choice. + bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) + + # Best nominal. + nominal = min(domain, key=lambda x: bestCost[x]) + + # Work back the best default. + bestC = bestCost[nominal] + dfltC = nomnCost[nominal] - bestCost[nominal] + ends = [] + if dfltC == dfltCostU[nominal]: + starts = [nominal, nominal-108, nominal-1131] + for start in starts: + while cumMaxU[start] and cumMaxU[start] == cumMaxU[start-1]: + start -= 1 + ends.append(start) + else: + starts = [nominal, nominal+108, nominal+1131] + for start in starts: + while cumMaxD[start] and cumMaxD[start] == cumMaxD[start+1]: + start += 1 + ends.append(start) + default = min(ends, key=lambda default: byteCost(widths, default, nominal)) + + return default, nominal + + +if __name__ == '__main__': + import sys + if len(sys.argv) == 1: + import doctest + sys.exit(doctest.testmod().failed) + for fontfile in sys.argv[1:]: + font = TTFont(fontfile) + hmtx = font['hmtx'] + widths = [m[0] for m in hmtx.metrics.values()] + default, nominal = optimizeWidths(widths) + print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) + #default, nominal = optimizeWidthsBruteforce(widths) + #print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) diff -Nru fonttools-3.21.2/Lib/fontTools/designspaceLib/__init__.py fonttools-3.29.0/Lib/fontTools/designspaceLib/__init__.py --- fonttools-3.21.2/Lib/fontTools/designspaceLib/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/designspaceLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,1252 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import LogMixin +import collections +import os +import posixpath +import plistlib + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +""" + designSpaceDocument + + - read and write designspace files +""" + +__all__ = [ + 'DesignSpaceDocumentError', 'DesignSpaceDocument', 'SourceDescriptor', + 'InstanceDescriptor', 'AxisDescriptor', 'RuleDescriptor', 'BaseDocReader', + 'BaseDocWriter' +] + +# ElementTree allows to find namespace-prefixed elements, but not attributes +# so we have to do it ourselves for 'xml:lang' +XML_NS = "{http://www.w3.org/XML/1998/namespace}" +XML_LANG = XML_NS + "lang" + + +def to_plist(value): + try: + # Python 2 + string = plistlib.writePlistToString(value) + except AttributeError: + # Python 3 + string = plistlib.dumps(value).decode() + return ET.fromstring(string)[0] + + +def from_plist(element): + if element is None: + return {} + plist = ET.Element('plist') + plist.append(element) + string = ET.tostring(plist) + try: + # Python 2 + return plistlib.readPlistFromString(string) + except AttributeError: + # Python 3 + return plistlib.loads(string, fmt=plistlib.FMT_XML) + + +def posix(path): + """Normalize paths using forward slash to work also on Windows.""" + new_path = posixpath.join(*path.split(os.path.sep)) + if path.startswith('/'): + # The above transformation loses absolute paths + new_path = '/' + new_path + return new_path + + +def posixpath_property(private_name): + def getter(self): + # Normal getter + return getattr(self, private_name) + + def setter(self, value): + # The setter rewrites paths using forward slashes + if value is not None: + value = posix(value) + setattr(self, private_name, value) + + return property(getter, setter) + + +class DesignSpaceDocumentError(Exception): + def __init__(self, msg, obj=None): + self.msg = msg + self.obj = obj + + def __str__(self): + return str(self.msg) + ( + ": %r" % self.obj if self.obj is not None else "") + + +def _indent(elem, whitespace=" ", level=0): + # taken from http://effbot.org/zone/element-lib.htm#prettyprint + i = "\n" + level * whitespace + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + whitespace + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, whitespace, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class SimpleDescriptor(object): + """ Containers for a bunch of attributes""" + + # XXX this is ugly. The 'print' is inappropriate here, and instead of + # assert, it should simply return True/False + def compare(self, other): + # test if this object contains the same data as the other + for attr in self._attrs: + try: + assert(getattr(self, attr) == getattr(other, attr)) + except AssertionError: + print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr)) + + +class SourceDescriptor(SimpleDescriptor): + """Simple container for data related to the source""" + flavor = "source" + _attrs = ['filename', 'path', 'name', 'layerName', + 'location', 'copyLib', + 'copyGroups', 'copyFeatures', + 'muteKerning', 'muteInfo', + 'mutedGlyphNames', + 'familyName', 'styleName'] + + def __init__(self): + self.filename = None + """The original path as found in the document.""" + + self.path = None + """The absolute path, calculated from filename.""" + + self.font = None + """Any Python object. Optional. Points to a representation of this + source font that is loaded in memory, as a Python object (e.g. a + ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). + + The default document reader will not fill-in this attribute, and the + default writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by + ``filename`` and store it in this field, or write the contents of + this field to the disk and make ```filename`` point to that. + """ + + self.name = None + self.location = None + self.layerName = None + self.copyLib = False + self.copyInfo = False + self.copyGroups = False + self.copyFeatures = False + self.muteKerning = False + self.muteInfo = False + self.mutedGlyphNames = [] + self.familyName = None + self.styleName = None + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + +class RuleDescriptor(SimpleDescriptor): + """ + + + + + + + + + + + + """ + _attrs = ['name', 'conditionSets', 'subs'] # what do we need here + + def __init__(self): + self.name = None + self.conditionSets = [] # list of list of dict(name='aaaa', minimum=0, maximum=1000) + self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt") + + +def evaluateRule(rule, location): + """ Return True if any of the rule's conditionsets matches the given location.""" + return any(evaluateConditions(c, location) for c in rule.conditionSets) + + +def evaluateConditions(conditions, location): + """ Return True if all the conditions matches the given location. + If a condition has no minimum, check for < maximum. + If a condition has no maximum, check for > minimum. + """ + for cd in conditions: + value = location[cd['name']] + if cd.get('minimum') is None: + if value > cd['maximum']: + return False + elif cd.get('maximum') is None: + if cd['minimum'] > value: + return False + elif not cd['minimum'] <= value <= cd['maximum']: + return False + return True + + +def processRules(rules, location, glyphNames): + """ Apply these rules at this location to these glyphnames.minimum + - rule order matters + """ + newNames = [] + for rule in rules: + if evaluateRule(rule, location): + for name in glyphNames: + swap = False + for a, b in rule.subs: + if name == a: + swap = True + break + if swap: + newNames.append(b) + else: + newNames.append(name) + glyphNames = newNames + newNames = [] + return glyphNames + + +class InstanceDescriptor(SimpleDescriptor): + """Simple container for data related to the instance""" + flavor = "instance" + _defaultLanguageCode = "en" + _attrs = ['path', + 'name', + 'location', + 'familyName', + 'styleName', + 'postScriptFontName', + 'styleMapFamilyName', + 'styleMapStyleName', + 'kerning', + 'info', + 'lib'] + + def __init__(self): + self.filename = None # the original path as found in the document + self.path = None # the absolute path, calculated from filename + self.name = None + self.location = None + self.familyName = None + self.styleName = None + self.postScriptFontName = None + self.styleMapFamilyName = None + self.styleMapStyleName = None + self.localisedStyleName = {} + self.localisedFamilyName = {} + self.localisedStyleMapStyleName = {} + self.localisedStyleMapFamilyName = {} + self.glyphs = {} + self.mutedGlyphNames = [] + self.kerning = True + self.info = True + + self.lib = {} + """Custom data associated with this instance.""" + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + def setStyleName(self, styleName, languageCode="en"): + self.localisedStyleName[languageCode] = styleName + + def getStyleName(self, languageCode="en"): + return self.localisedStyleName.get(languageCode) + + def setFamilyName(self, familyName, languageCode="en"): + self.localisedFamilyName[languageCode] = familyName + + def getFamilyName(self, languageCode="en"): + return self.localisedFamilyName.get(languageCode) + + def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"): + self.localisedStyleMapStyleName[languageCode] = styleMapStyleName + + def getStyleMapStyleName(self, languageCode="en"): + return self.localisedStyleMapStyleName.get(languageCode) + + def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"): + self.localisedStyleMapFamilyName[languageCode] = styleMapFamilyName + + def getStyleMapFamilyName(self, languageCode="en"): + return self.localisedStyleMapFamilyName.get(languageCode) + + +def tagForAxisName(name): + # try to find or make a tag name for this axis name + names = { + 'weight': ('wght', dict(en = 'Weight')), + 'width': ('wdth', dict(en = 'Width')), + 'optical': ('opsz', dict(en = 'Optical Size')), + 'slant': ('slnt', dict(en = 'Slant')), + 'italic': ('ital', dict(en = 'Italic')), + } + if name.lower() in names: + return names[name.lower()] + if len(name) < 4: + tag = name + "*" * (4 - len(name)) + else: + tag = name[:4] + return tag, dict(en=name) + + +class AxisDescriptor(SimpleDescriptor): + """ Simple container for the axis data + Add more localisations? + """ + flavor = "axis" + _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map'] + + def __init__(self): + self.tag = None # opentype tag for this axis + self.name = None # name of the axis used in locations + self.labelNames = {} # names for UI purposes, if this is not a standard axis, + self.minimum = None + self.maximum = None + self.default = None + self.hidden = False + self.map = [] + + def serialize(self): + # output to a dict, used in testing + return dict( + tag=self.tag, + name=self.name, + labelNames=self.labelNames, + maximum=self.maximum, + minimum=self.minimum, + default=self.default, + hidden=self.hidden, + map=self.map, + ) + + +class BaseDocWriter(object): + _whiteSpace = " " + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + @classmethod + def getAxisDecriptor(cls): + return cls.axisDescriptorClass() + + @classmethod + def getSourceDescriptor(cls): + return cls.sourceDescriptorClass() + + @classmethod + def getInstanceDescriptor(cls): + return cls.instanceDescriptorClass() + + @classmethod + def getRuleDescriptor(cls): + return cls.ruleDescriptorClass() + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + self.documentVersion = "4.0" + self.root = ET.Element("designspace") + self.root.attrib['format'] = self.documentVersion + self._axes = [] # for use by the writer only + self._rules = [] # for use by the writer only + + def write(self, pretty=True): + if self.documentObject.axes: + self.root.append(ET.Element("axes")) + for axisObject in self.documentObject.axes: + self._addAxis(axisObject) + + if self.documentObject.rules: + self.root.append(ET.Element("rules")) + for ruleObject in self.documentObject.rules: + self._addRule(ruleObject) + + if self.documentObject.sources: + self.root.append(ET.Element("sources")) + for sourceObject in self.documentObject.sources: + self._addSource(sourceObject) + + if self.documentObject.instances: + self.root.append(ET.Element("instances")) + for instanceObject in self.documentObject.instances: + self._addInstance(instanceObject) + + if self.documentObject.lib: + self._addLib(self.documentObject.lib) + + if pretty: + _indent(self.root, whitespace=self._whiteSpace) + tree = ET.ElementTree(self.root) + tree.write(self.path, encoding="utf-8", method='xml', xml_declaration=True) + + def _makeLocationElement(self, locationObject, name=None): + """ Convert Location dict to a locationElement.""" + locElement = ET.Element("location") + if name is not None: + locElement.attrib['name'] = name + validatedLocation = self.documentObject.newDefaultLocation() + for axisName, axisValue in locationObject.items(): + if axisName in validatedLocation: + # only accept values we know + validatedLocation[axisName] = axisValue + for dimensionName, dimensionValue in validatedLocation.items(): + dimElement = ET.Element('dimension') + dimElement.attrib['name'] = dimensionName + if type(dimensionValue) == tuple: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue[0]) + dimElement.attrib['yvalue'] = self.intOrFloat(dimensionValue[1]) + else: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue) + locElement.append(dimElement) + return locElement, validatedLocation + + def intOrFloat(self, num): + if int(num) == num: + return "%d" % num + return "%f" % num + + def _addRule(self, ruleObject): + # if none of the conditions have minimum or maximum values, do not add the rule. + self._rules.append(ruleObject) + ruleElement = ET.Element('rule') + if ruleObject.name is not None: + ruleElement.attrib['name'] = ruleObject.name + for conditions in ruleObject.conditionSets: + conditionsetElement = ET.Element('conditionset') + for cond in conditions: + if cond.get('minimum') is None and cond.get('maximum') is None: + # neither is defined, don't add this condition + continue + conditionElement = ET.Element('condition') + conditionElement.attrib['name'] = cond.get('name') + if cond.get('minimum') is not None: + conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum')) + if cond.get('maximum') is not None: + conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum')) + conditionsetElement.append(conditionElement) + if len(conditionsetElement): + ruleElement.append(conditionsetElement) + for sub in ruleObject.subs: + subElement = ET.Element('sub') + subElement.attrib['name'] = sub[0] + subElement.attrib['with'] = sub[1] + ruleElement.append(subElement) + if len(ruleElement): + self.root.findall('.rules')[0].append(ruleElement) + + def _addAxis(self, axisObject): + self._axes.append(axisObject) + axisElement = ET.Element('axis') + axisElement.attrib['tag'] = axisObject.tag + axisElement.attrib['name'] = axisObject.name + axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum) + axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum) + axisElement.attrib['default'] = self.intOrFloat(axisObject.default) + if axisObject.hidden: + axisElement.attrib['hidden'] = "1" + for languageCode, labelName in sorted(axisObject.labelNames.items()): + languageElement = ET.Element('labelname') + languageElement.attrib[u'xml:lang'] = languageCode + languageElement.text = labelName + axisElement.append(languageElement) + if axisObject.map: + for inputValue, outputValue in axisObject.map: + mapElement = ET.Element('map') + mapElement.attrib['input'] = self.intOrFloat(inputValue) + mapElement.attrib['output'] = self.intOrFloat(outputValue) + axisElement.append(mapElement) + self.root.findall('.axes')[0].append(axisElement) + + def _addInstance(self, instanceObject): + instanceElement = ET.Element('instance') + if instanceObject.name is not None: + instanceElement.attrib['name'] = instanceObject.name + if instanceObject.familyName is not None: + instanceElement.attrib['familyname'] = instanceObject.familyName + if instanceObject.styleName is not None: + instanceElement.attrib['stylename'] = instanceObject.styleName + # add localisations + if instanceObject.localisedStyleName: + languageCodes = list(instanceObject.localisedStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedStyleNameElement = ET.Element('stylename') + localisedStyleNameElement.attrib["xml:lang"] = code + localisedStyleNameElement.text = instanceObject.getStyleName(code) + instanceElement.append(localisedStyleNameElement) + if instanceObject.localisedFamilyName: + languageCodes = list(instanceObject.localisedFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedFamilyNameElement = ET.Element('familyname') + localisedFamilyNameElement.attrib["xml:lang"] = code + localisedFamilyNameElement.text = instanceObject.getFamilyName(code) + instanceElement.append(localisedFamilyNameElement) + if instanceObject.localisedStyleMapStyleName: + languageCodes = list(instanceObject.localisedStyleMapStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapStyleNameElement = ET.Element('stylemapstylename') + localisedStyleMapStyleNameElement.attrib["xml:lang"] = code + localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code) + instanceElement.append(localisedStyleMapStyleNameElement) + if instanceObject.localisedStyleMapFamilyName: + languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname') + localisedStyleMapFamilyNameElement.attrib["xml:lang"] = code + localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code) + instanceElement.append(localisedStyleMapFamilyNameElement) + + if instanceObject.location is not None: + locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location) + instanceElement.append(locationElement) + if instanceObject.filename is not None: + instanceElement.attrib['filename'] = instanceObject.filename + if instanceObject.postScriptFontName is not None: + instanceElement.attrib['postscriptfontname'] = instanceObject.postScriptFontName + if instanceObject.styleMapFamilyName is not None: + instanceElement.attrib['stylemapfamilyname'] = instanceObject.styleMapFamilyName + if instanceObject.styleMapStyleName is not None: + instanceElement.attrib['stylemapstylename'] = instanceObject.styleMapStyleName + if instanceObject.glyphs: + if instanceElement.findall('.glyphs') == []: + glyphsElement = ET.Element('glyphs') + instanceElement.append(glyphsElement) + glyphsElement = instanceElement.findall('.glyphs')[0] + for glyphName, data in sorted(instanceObject.glyphs.items()): + glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data) + glyphsElement.append(glyphElement) + if instanceObject.kerning: + kerningElement = ET.Element('kerning') + instanceElement.append(kerningElement) + if instanceObject.info: + infoElement = ET.Element('info') + instanceElement.append(infoElement) + if instanceObject.lib: + libElement = ET.Element('lib') + libElement.append(to_plist(instanceObject.lib)) + instanceElement.append(libElement) + self.root.findall('.instances')[0].append(instanceElement) + + def _addSource(self, sourceObject): + sourceElement = ET.Element("source") + if sourceObject.filename is not None: + sourceElement.attrib['filename'] = sourceObject.filename + if sourceObject.name is not None: + if sourceObject.name.find("temp_master") != 0: + # do not save temporary source names + sourceElement.attrib['name'] = sourceObject.name + if sourceObject.familyName is not None: + sourceElement.attrib['familyname'] = sourceObject.familyName + if sourceObject.styleName is not None: + sourceElement.attrib['stylename'] = sourceObject.styleName + if sourceObject.layerName is not None: + sourceElement.attrib['layer'] = sourceObject.layerName + if sourceObject.copyLib: + libElement = ET.Element('lib') + libElement.attrib['copy'] = "1" + sourceElement.append(libElement) + if sourceObject.copyGroups: + groupsElement = ET.Element('groups') + groupsElement.attrib['copy'] = "1" + sourceElement.append(groupsElement) + if sourceObject.copyFeatures: + featuresElement = ET.Element('features') + featuresElement.attrib['copy'] = "1" + sourceElement.append(featuresElement) + if sourceObject.copyInfo or sourceObject.muteInfo: + infoElement = ET.Element('info') + if sourceObject.copyInfo: + infoElement.attrib['copy'] = "1" + if sourceObject.muteInfo: + infoElement.attrib['mute'] = "1" + sourceElement.append(infoElement) + if sourceObject.muteKerning: + kerningElement = ET.Element("kerning") + kerningElement.attrib["mute"] = '1' + sourceElement.append(kerningElement) + if sourceObject.mutedGlyphNames: + for name in sourceObject.mutedGlyphNames: + glyphElement = ET.Element("glyph") + glyphElement.attrib["name"] = name + glyphElement.attrib["mute"] = '1' + sourceElement.append(glyphElement) + locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location) + sourceElement.append(locationElement) + self.root.findall('.sources')[0].append(sourceElement) + + def _addLib(self, dict): + libElement = ET.Element('lib') + libElement.append(to_plist(dict)) + self.root.append(libElement) + + def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data): + glyphElement = ET.Element('glyph') + if data.get('mute'): + glyphElement.attrib['mute'] = "1" + if data.get('unicodes') is not None: + glyphElement.attrib['unicode'] = " ".join([hex(u) for u in data.get('unicodes')]) + if data.get('instanceLocation') is not None: + locationElement, data['instanceLocation'] = self._makeLocationElement(data.get('instanceLocation')) + glyphElement.append(locationElement) + if glyphName is not None: + glyphElement.attrib['name'] = glyphName + if data.get('note') is not None: + noteElement = ET.Element('note') + noteElement.text = data.get('note') + glyphElement.append(noteElement) + if data.get('masters') is not None: + mastersElement = ET.Element("masters") + for m in data.get('masters'): + masterElement = ET.Element("master") + if m.get('glyphName') is not None: + masterElement.attrib['glyphname'] = m.get('glyphName') + if m.get('font') is not None: + masterElement.attrib['source'] = m.get('font') + if m.get('location') is not None: + locationElement, m['location'] = self._makeLocationElement(m.get('location')) + masterElement.append(locationElement) + mastersElement.append(masterElement) + glyphElement.append(mastersElement) + return glyphElement + + +class BaseDocReader(LogMixin): + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + tree = ET.parse(self.path) + self.root = tree.getroot() + self.documentObject.formatVersion = self.root.attrib.get("format", "3.0") + self._axes = [] + self.rules = [] + self.sources = [] + self.instances = [] + self.axisDefaults = {} + self._strictAxisNames = True + + def read(self): + self.readAxes() + self.readRules() + self.readSources() + self.readInstances() + self.readLib() + + def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + paths = [] + for name in self.documentObject.sources.keys(): + paths.append(self.documentObject.sources[name][0].path) + return paths + + def readRules(self): + # we also need to read any conditions that are outside of a condition set. + rules = [] + for ruleElement in self.root.findall(".rules/rule"): + ruleObject = self.ruleDescriptorClass() + ruleName = ruleObject.name = ruleElement.attrib.get("name") + # read any stray conditions outside a condition set + externalConditions = self._readConditionElements( + ruleElement, + ruleName, + ) + if externalConditions: + ruleObject.conditionSets.append(externalConditions) + self.log.info( + "Found stray rule conditions outside a conditionset. " + "Wrapped them in a new conditionset." + ) + # read the conditionsets + for conditionSetElement in ruleElement.findall('.conditionset'): + conditionSet = self._readConditionElements( + conditionSetElement, + ruleName, + ) + if conditionSet is not None: + ruleObject.conditionSets.append(conditionSet) + for subElement in ruleElement.findall('.sub'): + a = subElement.attrib['name'] + b = subElement.attrib['with'] + ruleObject.subs.append((a, b)) + rules.append(ruleObject) + self.documentObject.rules = rules + + def _readConditionElements(self, parentElement, ruleName=None): + cds = [] + for conditionElement in parentElement.findall('.condition'): + cd = {} + cdMin = conditionElement.attrib.get("minimum") + if cdMin is not None: + cd['minimum'] = float(cdMin) + else: + # will allow these to be None, assume axis.minimum + cd['minimum'] = None + cdMax = conditionElement.attrib.get("maximum") + if cdMax is not None: + cd['maximum'] = float(cdMax) + else: + # will allow these to be None, assume axis.maximum + cd['maximum'] = None + cd['name'] = conditionElement.attrib.get("name") + # # test for things + if cd.get('minimum') is None and cd.get('maximum') is None: + raise DesignSpaceDocumentError( + "condition missing required minimum or maximum in rule" + + (" '%s'" % ruleName if ruleName is not None else "")) + cds.append(cd) + return cds + + def readAxes(self): + # read the axes elements, including the warp map. + if len(self.root.findall(".axes/axis")) == 0: + self._strictAxisNames = False + return + for axisElement in self.root.findall(".axes/axis"): + axisObject = self.axisDescriptorClass() + axisObject.name = axisElement.attrib.get("name") + axisObject.minimum = float(axisElement.attrib.get("minimum")) + axisObject.maximum = float(axisElement.attrib.get("maximum")) + if axisElement.attrib.get('hidden', False): + axisObject.hidden = True + axisObject.default = float(axisElement.attrib.get("default")) + axisObject.tag = axisElement.attrib.get("tag") + for mapElement in axisElement.findall('map'): + a = float(mapElement.attrib['input']) + b = float(mapElement.attrib['output']) + axisObject.map.append((a, b)) + for labelNameElement in axisElement.findall('labelname'): + # Note: elementtree reads the xml:lang attribute name as + # '{http://www.w3.org/XML/1998/namespace}lang' + for key, lang in labelNameElement.items(): + if key == XML_LANG: + labelName = labelNameElement.text + axisObject.labelNames[lang] = labelName + self.documentObject.axes.append(axisObject) + self.axisDefaults[axisObject.name] = axisObject.default + self.documentObject.defaultLoc = self.axisDefaults + + def readSources(self): + for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")): + filename = sourceElement.attrib.get('filename') + if filename is not None and self.path is not None: + sourcePath = os.path.abspath(os.path.join(os.path.dirname(self.path), filename)) + else: + sourcePath = None + sourceName = sourceElement.attrib.get('name') + if sourceName is None: + # add a temporary source name + sourceName = "temp_master.%d" % (sourceCount) + sourceObject = self.sourceDescriptorClass() + sourceObject.path = sourcePath # absolute path to the ufo source + sourceObject.filename = filename # path as it is stored in the document + sourceObject.name = sourceName + familyName = sourceElement.attrib.get("familyname") + if familyName is not None: + sourceObject.familyName = familyName + styleName = sourceElement.attrib.get("stylename") + if styleName is not None: + sourceObject.styleName = styleName + sourceObject.location = self.locationFromElement(sourceElement) + layerName = sourceElement.attrib.get('layer') + if layerName is not None: + sourceObject.layerName = layerName + for libElement in sourceElement.findall('.lib'): + if libElement.attrib.get('copy') == '1': + sourceObject.copyLib = True + for groupsElement in sourceElement.findall('.groups'): + if groupsElement.attrib.get('copy') == '1': + sourceObject.copyGroups = True + for infoElement in sourceElement.findall(".info"): + if infoElement.attrib.get('copy') == '1': + sourceObject.copyInfo = True + if infoElement.attrib.get('mute') == '1': + sourceObject.muteInfo = True + for featuresElement in sourceElement.findall(".features"): + if featuresElement.attrib.get('copy') == '1': + sourceObject.copyFeatures = True + for glyphElement in sourceElement.findall(".glyph"): + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + continue + if glyphElement.attrib.get('mute') == '1': + sourceObject.mutedGlyphNames.append(glyphName) + for kerningElement in sourceElement.findall(".kerning"): + if kerningElement.attrib.get('mute') == '1': + sourceObject.muteKerning = True + self.documentObject.sources.append(sourceObject) + + def locationFromElement(self, element): + elementLocation = None + for locationElement in element.findall('.location'): + elementLocation = self.readLocationElement(locationElement) + break + return elementLocation + + def readLocationElement(self, locationElement): + """ Format 0 location reader """ + if not self.documentObject.axes: + raise DesignSpaceDocumentError("No axes defined") + loc = {} + for dimensionElement in locationElement.findall(".dimension"): + dimName = dimensionElement.attrib.get("name") + if self._strictAxisNames and dimName not in self.axisDefaults: + # In case the document contains no axis definitions, + self.log.warning("Location with undefined axis: \"%s\".", dimName) + continue + xValue = yValue = None + try: + xValue = dimensionElement.attrib.get('xvalue') + xValue = float(xValue) + except ValueError: + self.log.warning("KeyError in readLocation xValue %3.3f", xValue) + try: + yValue = dimensionElement.attrib.get('yvalue') + if yValue is not None: + yValue = float(yValue) + except ValueError: + pass + if yValue is not None: + loc[dimName] = (xValue, yValue) + else: + loc[dimName] = xValue + return loc + + def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + instanceElements = self.root.findall('.instances/instance') + for instanceElement in instanceElements: + self._readSingleInstanceElement(instanceElement, makeGlyphs=makeGlyphs, makeKerning=makeKerning, makeInfo=makeInfo) + + def _readSingleInstanceElement(self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True): + filename = instanceElement.attrib.get('filename') + if filename is not None: + instancePath = os.path.join(os.path.dirname(self.documentObject.path), filename) + else: + instancePath = None + instanceObject = self.instanceDescriptorClass() + instanceObject.path = instancePath # absolute path to the instance + instanceObject.filename = filename # path as it is stored in the document + name = instanceElement.attrib.get("name") + if name is not None: + instanceObject.name = name + familyname = instanceElement.attrib.get('familyname') + if familyname is not None: + instanceObject.familyName = familyname + stylename = instanceElement.attrib.get('stylename') + if stylename is not None: + instanceObject.styleName = stylename + postScriptFontName = instanceElement.attrib.get('postscriptfontname') + if postScriptFontName is not None: + instanceObject.postScriptFontName = postScriptFontName + styleMapFamilyName = instanceElement.attrib.get('stylemapfamilyname') + if styleMapFamilyName is not None: + instanceObject.styleMapFamilyName = styleMapFamilyName + styleMapStyleName = instanceElement.attrib.get('stylemapstylename') + if styleMapStyleName is not None: + instanceObject.styleMapStyleName = styleMapStyleName + # read localised names + for styleNameElement in instanceElement.findall('stylename'): + for key, lang in styleNameElement.items(): + if key == XML_LANG: + styleName = styleNameElement.text + instanceObject.setStyleName(styleName, lang) + for familyNameElement in instanceElement.findall('familyname'): + for key, lang in familyNameElement.items(): + if key == XML_LANG: + familyName = familyNameElement.text + instanceObject.setFamilyName(familyName, lang) + for styleMapStyleNameElement in instanceElement.findall('stylemapstylename'): + for key, lang in styleMapStyleNameElement.items(): + if key == XML_LANG: + styleMapStyleName = styleMapStyleNameElement.text + instanceObject.setStyleMapStyleName(styleMapStyleName, lang) + for styleMapFamilyNameElement in instanceElement.findall('stylemapfamilyname'): + for key, lang in styleMapFamilyNameElement.items(): + if key == XML_LANG: + styleMapFamilyName = styleMapFamilyNameElement.text + instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang) + instanceLocation = self.locationFromElement(instanceElement) + if instanceLocation is not None: + instanceObject.location = instanceLocation + for glyphElement in instanceElement.findall('.glyphs/glyph'): + self.readGlyphElement(glyphElement, instanceObject) + for infoElement in instanceElement.findall("info"): + self.readInfoElement(infoElement, instanceObject) + for libElement in instanceElement.findall('lib'): + self.readLibElement(libElement, instanceObject) + self.documentObject.instances.append(instanceObject) + + def readLibElement(self, libElement, instanceObject): + """Read the lib element for the given instance.""" + instanceObject.lib = from_plist(libElement[0]) + + def readInfoElement(self, infoElement, instanceObject): + """ Read the info element.""" + instanceObject.info = True + + def readKerningElement(self, kerningElement, instanceObject): + """ Read the kerning element.""" + kerningLocation = self.locationFromElement(kerningElement) + instanceObject.addKerning(kerningLocation) + + def readGlyphElement(self, glyphElement, instanceObject): + """ + Read the glyph element. + + + + + + + This is an instance from an anisotropic interpolation. + + + """ + glyphData = {} + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + raise DesignSpaceDocumentError("Glyph object without name attribute") + mute = glyphElement.attrib.get("mute") + if mute == "1": + glyphData['mute'] = True + # unicode + unicodes = glyphElement.attrib.get('unicode') + if unicodes is not None: + try: + unicodes = [int(u, 16) for u in unicodes.split(" ")] + glyphData['unicodes'] = unicodes + except ValueError: + raise DesignSpaceDocumentError("unicode values %s are not integers" % unicodes) + + for noteElement in glyphElement.findall('.note'): + glyphData['note'] = noteElement.text + break + instanceLocation = self.locationFromElement(glyphElement) + if instanceLocation is not None: + glyphData['instanceLocation'] = instanceLocation + glyphSources = None + for masterElement in glyphElement.findall('.masters/master'): + fontSourceName = masterElement.attrib.get('source') + sourceLocation = self.locationFromElement(masterElement) + masterGlyphName = masterElement.attrib.get('glyphname') + if masterGlyphName is None: + # if we don't read a glyphname, use the one we have + masterGlyphName = glyphName + d = dict(font=fontSourceName, + location=sourceLocation, + glyphName=masterGlyphName) + if glyphSources is None: + glyphSources = [] + glyphSources.append(d) + if glyphSources is not None: + glyphData['masters'] = glyphSources + instanceObject.glyphs[glyphName] = glyphData + + def readLib(self): + """Read the lib element for the whole document.""" + for libElement in self.root.findall(".lib"): + self.documentObject.lib = from_plist(libElement[0]) + + +class DesignSpaceDocument(LogMixin): + """ Read, write data from the designspace file""" + def __init__(self, readerClass=None, writerClass=None): + self.path = None + self.filename = None + """String, optional. When the document is read from the disk, this is + its original file name, i.e. the last part of its path. + + When the document is produced by a Python script and still only exists + in memory, the producing script can write here an indication of a + possible "good" filename, in case one wants to save the file somewhere. + """ + + self.formatVersion = None + self.sources = [] + self.instances = [] + self.axes = [] + self.rules = [] + self.default = None # name of the default master + self.defaultLoc = None + + self.lib = {} + """Custom data associated with the whole document.""" + + # + if readerClass is not None: + self.readerClass = readerClass + else: + self.readerClass = BaseDocReader + if writerClass is not None: + self.writerClass = writerClass + else: + self.writerClass = BaseDocWriter + + def read(self, path): + self.path = path + self.filename = os.path.basename(path) + reader = self.readerClass(path, self) + reader.read() + if self.sources: + self.findDefault() + + def write(self, path): + self.path = path + self.filename = os.path.basename(path) + self.updatePaths() + writer = self.writerClass(path, self) + writer.write() + + def _posixRelativePath(self, otherPath): + relative = os.path.relpath(otherPath, os.path.dirname(self.path)) + return posix(relative) + + def updatePaths(self): + """ + Right before we save we need to identify and respond to the following situations: + In each descriptor, we have to do the right thing for the filename attribute. + + case 1. + descriptor.filename == None + descriptor.path == None + + -- action: + write as is, descriptors will not have a filename attr. + useless, but no reason to interfere. + + + case 2. + descriptor.filename == "../something" + descriptor.path == None + + -- action: + write as is. The filename attr should not be touched. + + + case 3. + descriptor.filename == None + descriptor.path == "~/absolute/path/there" + + -- action: + calculate the relative path for filename. + We're not overwriting some other value for filename, it should be fine + + + case 4. + descriptor.filename == '../somewhere' + descriptor.path == "~/absolute/path/there" + + -- action: + there is a conflict between the given filename, and the path. + So we know where the file is relative to the document. + Can't guess why they're different, we just choose for path to be correct and update filename. + + + """ + for descriptor in self.sources + self.instances: + # check what the relative path really should be? + expectedFilename = None + if descriptor.path is not None and self.path is not None: + expectedFilename = self._posixRelativePath(descriptor.path) + + # 3 + if descriptor.filename is None and descriptor.path is not None and self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + continue + + # 4 + if descriptor.filename is not None and descriptor.path is not None and self.path is not None: + if descriptor.filename is not expectedFilename: + descriptor.filename = expectedFilename + + def addSource(self, sourceDescriptor): + self.sources.append(sourceDescriptor) + + def addInstance(self, instanceDescriptor): + self.instances.append(instanceDescriptor) + + def addAxis(self, axisDescriptor): + self.axes.append(axisDescriptor) + + def addRule(self, ruleDescriptor): + self.rules.append(ruleDescriptor) + + def newDefaultLocation(self): + # Without OrderedDict, output XML would be non-deterministic. + # https://github.com/LettError/designSpaceDocument/issues/10 + loc = collections.OrderedDict() + for axisDescriptor in self.axes: + loc[axisDescriptor.name] = axisDescriptor.default + return loc + + def updateFilenameFromPath(self, masters=True, instances=True, force=False): + # set a descriptor filename attr from the path and this document path + # if the filename attribute is not None: skip it. + if masters: + for descriptor in self.sources: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + if instances: + for descriptor in self.instances: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + + def newAxisDescriptor(self): + # Ask the writer class to make us a new axisDescriptor + return self.writerClass.getAxisDecriptor() + + def newSourceDescriptor(self): + # Ask the writer class to make us a new sourceDescriptor + return self.writerClass.getSourceDescriptor() + + def newInstanceDescriptor(self): + # Ask the writer class to make us a new instanceDescriptor + return self.writerClass.getInstanceDescriptor() + + def getAxisOrder(self): + names = [] + for axisDescriptor in self.axes: + names.append(axisDescriptor.name) + return names + + def getAxis(self, name): + for axisDescriptor in self.axes: + if axisDescriptor.name == name: + return axisDescriptor + return None + + def findDefault(self): + # new default finder + # take the sourcedescriptor with the location at all the defaults + # if we can't find it, return None, let someone else figure it out + self.default = None + for sourceDescriptor in self.sources: + if sourceDescriptor.location == self.defaultLoc: + # we choose you! + self.default = sourceDescriptor + return sourceDescriptor + return None + + def normalizeLocation(self, location): + # adapted from fontTools.varlib.models.normalizeLocation because: + # - this needs to work with axis names, not tags + # - this needs to accomodate anisotropic locations + # - the axes are stored differently here, it's just math + new = {} + for axis in self.axes: + if axis.name not in location: + # skipping this dimension it seems + continue + v = location.get(axis.name, axis.default) + if type(v) == tuple: + v = v[0] + if v == axis.default: + v = 0.0 + elif v < axis.default: + if axis.default == axis.minimum: + v = 0.0 + else: + v = (max(v, axis.minimum) - axis.default) / (axis.default - axis.minimum) + else: + if axis.default == axis.maximum: + v = 0.0 + else: + v = (min(v, axis.maximum) - axis.default) / (axis.maximum - axis.default) + new[axis.name] = v + return new + + def normalize(self): + # Normalise the geometry of this designspace: + # scale all the locations of all masters and instances to the -1 - 0 - 1 value. + # we need the axis data to do the scaling, so we do those last. + # masters + for item in self.sources: + item.location = self.normalizeLocation(item.location) + # instances + for item in self.instances: + # glyph masters for this instance + for _, glyphData in item.glyphs.items(): + glyphData['instanceLocation'] = self.normalizeLocation(glyphData['instanceLocation']) + for glyphMaster in glyphData['masters']: + glyphMaster['location'] = self.normalizeLocation(glyphMaster['location']) + item.location = self.normalizeLocation(item.location) + # the axes + for axis in self.axes: + # scale the map first + newMap = [] + for inputValue, outputValue in axis.map: + newOutputValue = self.normalizeLocation({axis.name: outputValue}).get(axis.name) + newMap.append((inputValue, newOutputValue)) + if newMap: + axis.map = newMap + # finally the axis values + minimum = self.normalizeLocation({axis.name: axis.minimum}).get(axis.name) + maximum = self.normalizeLocation({axis.name: axis.maximum}).get(axis.name) + default = self.normalizeLocation({axis.name: axis.default}).get(axis.name) + # and set them in the axis.minimum + axis.minimum = minimum + axis.maximum = maximum + axis.default = default + # now the rules + for rule in self.rules: + newConditionSets = [] + for conditions in rule.conditionSets: + newConditions = [] + for cond in conditions: + if cond.get('minimum') is not None: + minimum = self.normalizeLocation({cond['name']: cond['minimum']}).get(cond['name']) + else: + minimum = None + if cond.get('maximum') is not None: + maximum = self.normalizeLocation({cond['name']: cond['maximum']}).get(cond['name']) + else: + maximum = None + newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum)) + newConditionSets.append(newConditions) + rule.conditionSets = newConditionSets diff -Nru fonttools-3.21.2/Lib/fontTools/feaLib/ast.py fonttools-3.29.0/Lib/fontTools/feaLib/ast.py --- fonttools-3.21.2/Lib/fontTools/feaLib/ast.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/feaLib/ast.py 2018-07-26 14:12:55.000000000 +0000 @@ -8,6 +8,76 @@ SHIFT = " " * 4 +__all__ = [ + 'AlternateSubstStatement', + 'Anchor', + 'AnchorDefinition', + 'AnonymousBlock', + 'AttachStatement', + 'BaseAxis', + 'Block', + 'BytesIO', + 'CVParametersNameStatement', + 'ChainContextPosStatement', + 'ChainContextSubstStatement', + 'CharacterStatement', + 'Comment', + 'CursivePosStatement', + 'Element', + 'Expression', + 'FeatureBlock', + 'FeatureFile', + 'FeatureLibError', + 'FeatureNameStatement', + 'FeatureReferenceStatement', + 'FontRevisionStatement', + 'GlyphClass', + 'GlyphClassDefStatement', + 'GlyphClassDefinition', + 'GlyphClassName', + 'GlyphName', + 'HheaField', + 'IgnorePosStatement', + 'IgnoreSubstStatement', + 'IncludeStatement', + 'LanguageStatement', + 'LanguageSystemStatement', + 'LigatureCaretByIndexStatement', + 'LigatureCaretByPosStatement', + 'LigatureSubstStatement', + 'LookupBlock', + 'LookupFlagStatement', + 'LookupReferenceStatement', + 'MarkBasePosStatement', + 'MarkClass', + 'MarkClassDefinition', + 'MarkClassName', + 'MarkLigPosStatement', + 'MarkMarkPosStatement', + 'MultipleSubstStatement', + 'NameRecord', + 'NestedBlock', + 'OS2Field', + 'OrderedDict', + 'PairPosStatement', + 'Py23Error', + 'ReverseChainSingleSubstStatement', + 'ScriptStatement', + 'SimpleNamespace', + 'SinglePosStatement', + 'SingleSubstStatement', + 'SizeParameters', + 'Statement', + 'StringIO', + 'SubtableStatement', + 'TableBlock', + 'Tag', + 'UnicodeIO', + 'ValueRecord', + 'ValueRecordDefinition', + 'VheaField', +] + def deviceToString(device): if device is None: @@ -49,7 +119,7 @@ class Element(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -71,7 +141,7 @@ class Comment(Element): - def __init__(self, location, text): + def __init__(self, text, location=None): super(Comment, self).__init__(location) self.text = text @@ -81,7 +151,7 @@ class GlyphName(Expression): """A single glyph name, such as cedilla.""" - def __init__(self, location, glyph): + def __init__(self, glyph, location=None): Expression.__init__(self, location) self.glyph = glyph @@ -94,7 +164,7 @@ class GlyphClass(Expression): """A glyph class, such as [acute cedilla grave].""" - def __init__(self, location, glyphs=None): + def __init__(self, glyphs=None, location=None): Expression.__init__(self, location) self.glyphs = glyphs if glyphs is not None else [] self.original = [] @@ -142,7 +212,7 @@ class GlyphClassName(Expression): """A glyph class name, such as @FRENCH_MARKS.""" - def __init__(self, location, glyphclass): + def __init__(self, glyphclass, location=None): Expression.__init__(self, location) assert isinstance(glyphclass, GlyphClassDefinition) self.glyphclass = glyphclass @@ -156,7 +226,7 @@ class MarkClassName(Expression): """A mark class name, such as @FRENCH_MARKS defined with markClass.""" - def __init__(self, location, markClass): + def __init__(self, markClass, location=None): Expression.__init__(self, location) assert isinstance(markClass, MarkClass) self.markClass = markClass @@ -169,7 +239,7 @@ class AnonymousBlock(Statement): - def __init__(self, tag, content, location): + def __init__(self, tag, content, location=None): Statement.__init__(self, location) self.tag, self.content = tag, content @@ -181,7 +251,7 @@ class Block(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) self.statements = [] @@ -205,7 +275,7 @@ class FeatureBlock(Block): - def __init__(self, location, name, use_extension): + def __init__(self, name, use_extension=False, location=None): Block.__init__(self, location) self.name, self.use_extension = name, use_extension @@ -229,19 +299,26 @@ return res -class FeatureNamesBlock(Block): - def __init__(self, location): +class NestedBlock(Block): + def __init__(self, tag, block_name, location=None): Block.__init__(self, location) + self.tag = tag + self.block_name = block_name + + def build(self, builder): + Block.build(self, builder) + if self.block_name == "ParamUILabelNameID": + builder.add_to_cv_num_named_params(self.tag) def asFea(self, indent=""): - res = indent + "featureNames {\n" + res = "{}{} {{\n".format(indent, self.block_name) res += Block.asFea(self, indent=indent) - res += indent + "};\n" + res += "{}}};\n".format(indent) return res class LookupBlock(Block): - def __init__(self, location, name, use_extension): + def __init__(self, name, use_extension=False, location=None): Block.__init__(self, location) self.name, self.use_extension = name, use_extension @@ -259,7 +336,7 @@ class TableBlock(Block): - def __init__(self, location, name): + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -272,7 +349,7 @@ class GlyphClassDefinition(Statement): """Example: @UPPERCASE = [A-Z];""" - def __init__(self, location, name, glyphs): + def __init__(self, name, glyphs, location=None): Statement.__init__(self, location) self.name = name self.glyphs = glyphs @@ -286,8 +363,8 @@ class GlyphClassDefStatement(Statement): """Example: GlyphClassDef @UPPERCASE, [B], [C], [D];""" - def __init__(self, location, baseGlyphs, markGlyphs, - ligatureGlyphs, componentGlyphs): + def __init__(self, baseGlyphs, markGlyphs, ligatureGlyphs, + componentGlyphs, location=None): Statement.__init__(self, location) self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs) self.ligatureGlyphs = ligatureGlyphs @@ -328,9 +405,13 @@ for glyph in definition.glyphSet(): if glyph in self.glyphs: otherLoc = self.glyphs[glyph].location + if otherLoc is None: + end = "" + else: + end = " at %s:%d:%d" % ( + otherLoc[0], otherLoc[1], otherLoc[2]) raise FeatureLibError( - "Glyph %s already defined at %s:%d:%d" % ( - glyph, otherLoc[0], otherLoc[1], otherLoc[2]), + "Glyph %s already defined%s" % (glyph, end), definition.location) self.glyphs[glyph] = definition @@ -343,7 +424,7 @@ class MarkClassDefinition(Statement): - def __init__(self, location, markClass, anchor, glyphs): + def __init__(self, markClass, anchor, glyphs, location=None): Statement.__init__(self, location) assert isinstance(markClass, MarkClass) assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression) @@ -359,7 +440,7 @@ class AlternateSubstStatement(Statement): - def __init__(self, location, prefix, glyph, suffix, replacement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): Statement.__init__(self, location) self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) self.replacement = replacement @@ -391,8 +472,8 @@ class Anchor(Expression): - def __init__(self, location, name, x, y, contourpoint, - xDeviceTable, yDeviceTable): + def __init__(self, x, y, name=None, contourpoint=None, + xDeviceTable=None, yDeviceTable=None, location=None): Expression.__init__(self, location) self.name = name self.x, self.y, self.contourpoint = x, y, contourpoint @@ -414,7 +495,7 @@ class AnchorDefinition(Statement): - def __init__(self, location, name, x, y, contourpoint): + def __init__(self, name, x, y, contourpoint=None, location=None): Statement.__init__(self, location) self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint @@ -427,7 +508,7 @@ class AttachStatement(Statement): - def __init__(self, location, glyphs, contourPoints): + def __init__(self, glyphs, contourPoints, location=None): Statement.__init__(self, location) self.glyphs, self.contourPoints = (glyphs, contourPoints) @@ -441,7 +522,7 @@ class ChainContextPosStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, lookups): + def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix self.lookups = lookups @@ -473,7 +554,7 @@ class ChainContextSubstStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, lookups): + def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix self.lookups = lookups @@ -505,7 +586,7 @@ class CursivePosStatement(Statement): - def __init__(self, location, glyphclass, entryAnchor, exitAnchor): + def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None): Statement.__init__(self, location) self.glyphclass = glyphclass self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor @@ -522,7 +603,7 @@ class FeatureReferenceStatement(Statement): """Example: feature salt;""" - def __init__(self, location, featureName): + def __init__(self, featureName, location=None): Statement.__init__(self, location) self.location, self.featureName = (location, featureName) @@ -534,7 +615,7 @@ class IgnorePosStatement(Statement): - def __init__(self, location, chainContexts): + def __init__(self, chainContexts, location=None): Statement.__init__(self, location) self.chainContexts = chainContexts @@ -563,7 +644,7 @@ class IgnoreSubstStatement(Statement): - def __init__(self, location, chainContexts): + def __init__(self, chainContexts, location=None): Statement.__init__(self, location) self.chainContexts = chainContexts @@ -591,8 +672,25 @@ return "ignore sub " + ", ".join(contexts) + ";" +class IncludeStatement(Statement): + def __init__(self, filename, location=None): + super(IncludeStatement, self).__init__(location) + self.filename = filename + + def build(self): + # TODO: consider lazy-loading the including parser/lexer? + raise FeatureLibError( + "Building an include statement is not implemented yet. " + "Instead, use Parser(..., followIncludes=True) for building.", + self.location) + + def asFea(self, indent=""): + return indent + "include(%s);" % self.filename + + class LanguageStatement(Statement): - def __init__(self, location, language, include_default, required): + def __init__(self, language, include_default=True, required=False, + location=None): Statement.__init__(self, location) assert(len(language) == 4) self.language = language @@ -615,7 +713,7 @@ class LanguageSystemStatement(Statement): - def __init__(self, location, script, language): + def __init__(self, script, language, location=None): Statement.__init__(self, location) self.script, self.language = (script, language) @@ -627,7 +725,7 @@ class FontRevisionStatement(Statement): - def __init__(self, location, revision): + def __init__(self, revision, location=None): Statement.__init__(self, location) self.revision = revision @@ -639,7 +737,7 @@ class LigatureCaretByIndexStatement(Statement): - def __init__(self, location, glyphs, carets): + def __init__(self, glyphs, carets, location=None): Statement.__init__(self, location) self.glyphs, self.carets = (glyphs, carets) @@ -653,7 +751,7 @@ class LigatureCaretByPosStatement(Statement): - def __init__(self, location, glyphs, carets): + def __init__(self, glyphs, carets, location=None): Statement.__init__(self, location) self.glyphs, self.carets = (glyphs, carets) @@ -667,8 +765,8 @@ class LigatureSubstStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, replacement, - forceChain): + def __init__(self, prefix, glyphs, suffix, replacement, + forceChain, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) self.replacement, self.forceChain = replacement, forceChain @@ -698,7 +796,8 @@ class LookupFlagStatement(Statement): - def __init__(self, location, value, markAttachment, markFilteringSet): + def __init__(self, value=0, markAttachment=None, markFilteringSet=None, + location=None): Statement.__init__(self, location) self.value = value self.markAttachment = markAttachment @@ -731,7 +830,7 @@ class LookupReferenceStatement(Statement): - def __init__(self, location, lookup): + def __init__(self, lookup, location=None): Statement.__init__(self, location) self.location, self.lookup = (location, lookup) @@ -743,7 +842,7 @@ class MarkBasePosStatement(Statement): - def __init__(self, location, base, marks): + def __init__(self, base, marks, location=None): Statement.__init__(self, location) self.base, self.marks = base, marks @@ -759,7 +858,7 @@ class MarkLigPosStatement(Statement): - def __init__(self, location, ligatures, marks): + def __init__(self, ligatures, marks, location=None): Statement.__init__(self, location) self.ligatures, self.marks = ligatures, marks @@ -783,7 +882,7 @@ class MarkMarkPosStatement(Statement): - def __init__(self, location, baseMarks, marks): + def __init__(self, baseMarks, marks, location=None): Statement.__init__(self, location) self.baseMarks, self.marks = baseMarks, marks @@ -799,7 +898,7 @@ class MultipleSubstStatement(Statement): - def __init__(self, location, prefix, glyph, suffix, replacement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): Statement.__init__(self, location) self.prefix, self.glyph, self.suffix = prefix, glyph, suffix self.replacement = replacement @@ -827,8 +926,8 @@ class PairPosStatement(Statement): - def __init__(self, location, enumerated, - glyphs1, valuerecord1, glyphs2, valuerecord2): + def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2, + enumerated=False, location=None): Statement.__init__(self, location) self.enumerated = enumerated self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 @@ -868,7 +967,8 @@ class ReverseChainSingleSubstStatement(Statement): - def __init__(self, location, old_prefix, old_suffix, glyphs, replacements): + def __init__(self, old_prefix, old_suffix, glyphs, replacements, + location=None): Statement.__init__(self, location) self.old_prefix, self.old_suffix = old_prefix, old_suffix self.glyphs = glyphs @@ -899,7 +999,8 @@ class SingleSubstStatement(Statement): - def __init__(self, location, glyphs, replace, prefix, suffix, forceChain): + def __init__(self, glyphs, replace, prefix, suffix, forceChain, + location=None): Statement.__init__(self, location) self.prefix, self.suffix = prefix, suffix self.forceChain = forceChain @@ -932,7 +1033,7 @@ class ScriptStatement(Statement): - def __init__(self, location, script): + def __init__(self, script, location=None): Statement.__init__(self, location) self.script = script @@ -944,7 +1045,7 @@ class SinglePosStatement(Statement): - def __init__(self, location, pos, prefix, suffix, forceChain): + def __init__(self, pos, prefix, suffix, forceChain, location=None): Statement.__init__(self, location) self.pos, self.prefix, self.suffix = pos, prefix, suffix self.forceChain = forceChain @@ -973,14 +1074,22 @@ class SubtableStatement(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) + def build(self, builder): + builder.add_subtable_break(self.location) + + def asFea(self, indent=""): + return indent + "subtable;" + class ValueRecord(Expression): - def __init__(self, location, vertical, - xPlacement, yPlacement, xAdvance, yAdvance, - xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice): + def __init__(self, xPlacement=None, yPlacement=None, + xAdvance=None, yAdvance=None, + xPlaDevice=None, yPlaDevice=None, + xAdvDevice=None, yAdvDevice=None, + vertical=False, location=None): Expression.__init__(self, location) self.xPlacement, self.yPlacement = (xPlacement, yPlacement) self.xAdvance, self.yAdvance = (xAdvance, yAdvance) @@ -1033,7 +1142,7 @@ class ValueRecordDefinition(Statement): - def __init__(self, location, name, value): + def __init__(self, name, value, location=None): Statement.__init__(self, location) self.name = name self.value = value @@ -1052,8 +1161,8 @@ class NameRecord(Statement): - def __init__(self, location, nameID, platformID, - platEncID, langID, string): + def __init__(self, nameID, platformID, platEncID, langID, string, + location=None): Statement.__init__(self, location) self.nameID = nameID self.platformID = platformID @@ -1093,7 +1202,7 @@ class FeatureNameStatement(NameRecord): def build(self, builder): NameRecord.build(self, builder) - builder.add_featureName(self.location, self.nameID) + builder.add_featureName(self.nameID) def asFea(self, indent=""): if self.nameID == "size": @@ -1107,8 +1216,8 @@ class SizeParameters(Statement): - def __init__(self, location, DesignSize, SubfamilyID, RangeStart, - RangeEnd): + def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, + location=None): Statement.__init__(self, location) self.DesignSize = DesignSize self.SubfamilyID = SubfamilyID @@ -1126,8 +1235,50 @@ return res + ";" +class CVParametersNameStatement(NameRecord): + def __init__(self, nameID, platformID, platEncID, langID, string, + block_name, location=None): + NameRecord.__init__(self, nameID, platformID, platEncID, langID, + string, location=location) + self.block_name = block_name + + def build(self, builder): + item = "" + if self.block_name == "ParamUILabelNameID": + item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) + builder.add_cv_parameter(self.nameID) + self.nameID = (self.nameID, self.block_name + item) + NameRecord.build(self, builder) + + def asFea(self, indent=""): + plat = simplify_name_attributes(self.platformID, self.platEncID, + self.langID) + if plat != "": + plat += " " + return "name {}\"{}\";".format(plat, self.string) + + +class CharacterStatement(Statement): + """ + Statement used in cvParameters blocks of Character Variant features (cvXX). + The Unicode value may be written with either decimal or hexadecimal + notation. The value must be preceded by '0x' if it is a hexadecimal value. + The largest Unicode value allowed is 0xFFFFFF. + """ + def __init__(self, character, tag, location=None): + Statement.__init__(self, location) + self.character = character + self.tag = tag + + def build(self, builder): + builder.add_cv_character(self.character, self.tag) + + def asFea(self, indent=""): + return "Character {:#x};".format(self.character) + + class BaseAxis(Statement): - def __init__(self, location, bases, scripts, vertical): + def __init__(self, bases, scripts, vertical, location=None): Statement.__init__(self, location) self.bases = bases self.scripts = scripts @@ -1144,7 +1295,7 @@ class OS2Field(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value @@ -1169,7 +1320,7 @@ class HheaField(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value @@ -1184,7 +1335,7 @@ class VheaField(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value diff -Nru fonttools-3.21.2/Lib/fontTools/feaLib/builder.py fonttools-3.29.0/Lib/fontTools/feaLib/builder.py --- fonttools-3.21.2/Lib/fontTools/feaLib/builder.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/feaLib/builder.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,51 +5,80 @@ from fontTools.misc.textTools import binary2num, safeEval from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser +from fontTools.feaLib.ast import FeatureFile from fontTools.otlLib import builder as otl from fontTools.ttLib import newTable, getTableModule from fontTools.ttLib.tables import otBase, otTables +from collections import defaultdict import itertools +import logging -def addOpenTypeFeatures(font, featurefile): +log = logging.getLogger(__name__) + + +def addOpenTypeFeatures(font, featurefile, tables=None): builder = Builder(font, featurefile) - builder.build() + builder.build(tables=tables) -def addOpenTypeFeaturesFromString(font, features, filename=None): +def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): featurefile = UnicodeIO(tounicode(features)) if filename: # the directory containing 'filename' is used as the root of relative # include paths; if None is provided, the current directory is assumed featurefile.name = filename - addOpenTypeFeatures(font, featurefile) + addOpenTypeFeatures(font, featurefile, tables=tables) class Builder(object): + + supportedTables = frozenset(Tag(tag) for tag in [ + "BASE", + "GDEF", + "GPOS", + "GSUB", + "OS/2", + "head", + "hhea", + "name", + "vhea", + ]) + def __init__(self, font, featurefile): self.font = font - self.file = featurefile + # 'featurefile' can be either a path or file object (in which case we + # parse it into an AST), or a pre-parsed AST instance + if isinstance(featurefile, FeatureFile): + self.parseTree, self.file = featurefile, None + else: + self.parseTree, self.file = None, featurefile self.glyphMap = font.getReverseGlyphMap() self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() + self.seen_non_DFLT_script_ = False self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] - self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} # for 'featureNames' - self.featureNames_ = [] + self.featureNames_ = set() self.featureNames_ids_ = {} + # for 'cvParameters' + self.cv_parameters_ = set() + self.cv_parameters_ids_ = {} + self.cv_num_named_params_ = {} + self.cv_characters_ = defaultdict(list) # for feature 'size' self.size_parameters_ = None # for table 'head' @@ -74,16 +103,32 @@ # for table 'vhea' self.vhea_ = {} - def build(self): - self.parseTree = Parser(self.file, self.glyphMap).parse() + def build(self, tables=None): + if self.parseTree is None: + self.parseTree = Parser(self.file, self.glyphMap).parse() self.parseTree.build(self) - self.build_feature_aalt_() - self.build_head() - self.build_hhea() - self.build_vhea() - self.build_name() - self.build_OS_2() + # by default, build all the supported tables + if tables is None: + tables = self.supportedTables + else: + tables = frozenset(tables) + unsupported = tables - self.supportedTables + assert not unsupported, unsupported + if "GSUB" in tables: + self.build_feature_aalt_() + if "head" in tables: + self.build_head() + if "hhea" in tables: + self.build_hhea() + if "vhea" in tables: + self.build_vhea() + if "name" in tables: + self.build_name() + if "OS/2" in tables: + self.build_OS_2() for tag in ('GPOS', 'GSUB'): + if tag not in tables: + continue table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or @@ -92,16 +137,18 @@ fontTable.table = table elif tag in self.font: del self.font[tag] - gdef = self.buildGDEF() - if gdef: - self.font["GDEF"] = gdef - elif "GDEF" in self.font: - del self.font["GDEF"] - base = self.buildBASE() - if base: - self.font["BASE"] = base - elif "BASE" in self.font: - del self.font["BASE"] + if "GDEF" in tables: + gdef = self.buildGDEF() + if gdef: + self.font["GDEF"] = gdef + elif "GDEF" in self.font: + del self.font["GDEF"] + if "BASE" in tables: + base = self.buildBASE() + if base: + self.font["BASE"] = base + elif "BASE" in self.font: + del self.font["BASE"] def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) @@ -243,10 +290,28 @@ else: params.SubfamilyNameID = 0 elif tag in self.featureNames_: - assert tag in self.featureNames_ids_ - params = otTables.FeatureParamsStylisticSet() - params.Version = 0 - params.UINameID = self.featureNames_ids_[tag] + if not self.featureNames_ids_: + # name table wasn't selected among the tables to build; skip + pass + else: + assert tag in self.featureNames_ids_ + params = otTables.FeatureParamsStylisticSet() + params.Version = 0 + params.UINameID = self.featureNames_ids_[tag] + elif tag in self.cv_parameters_: + params = otTables.FeatureParamsCharacterVariants() + params.Format = 0 + params.FeatUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUILabelNameID'), 0) + params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUITooltipTextNameID'), 0) + params.SampleTextNameID = self.cv_parameters_ids_.get( + (tag, 'SampleTextNameID'), 0) + params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0) + params.FirstParamUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'ParamUILabelNameID_0'), 0) + params.CharCount = len(self.cv_characters_[tag]) + params.Character = self.cv_characters_[tag] return params def build_name(self): @@ -258,13 +323,20 @@ table.names = [] for name in self.names_: nameID, platformID, platEncID, langID, string = name + # For featureNames block, nameID is 'feature tag' + # For cvParameters blocks, nameID is ('feature tag', 'block name') if not isinstance(nameID, int): - # A featureNames name and nameID is actually the tag tag = nameID - if tag not in self.featureNames_ids_: - self.featureNames_ids_[tag] = self.get_user_name_id(table) - assert self.featureNames_ids_[tag] is not None - nameID = self.featureNames_ids_[tag] + if tag in self.featureNames_: + if tag not in self.featureNames_ids_: + self.featureNames_ids_[tag] = self.get_user_name_id(table) + assert self.featureNames_ids_[tag] is not None + nameID = self.featureNames_ids_[tag] + elif tag[0] in self.cv_parameters_: + if tag not in self.cv_parameters_ids_: + self.cv_parameters_ids_[tag] = self.get_user_name_id(table) + assert self.cv_parameters_ids_[tag] is not None + nameID = self.cv_parameters_ids_[tag] table.setName(string, nameID, platformID, platEncID, langID) def build_OS_2(self): @@ -496,7 +568,7 @@ frec.Feature = otTables.Feature() frec.Feature.FeatureParams = self.buildFeatureParams( feature_tag) - frec.Feature.LookupListIndex = lookup_indices + frec.Feature.LookupListIndex = list(lookup_indices) frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index @@ -549,6 +621,15 @@ raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' 'the first of the languagesystem statements', location) + if script == "DFLT": + if self.seen_non_DFLT_script_: + raise FeatureLibError( + 'languagesystems using the "DFLT" script tag must ' + "precede all other languagesystems", + location + ) + else: + self.seen_non_DFLT_script_ = True if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % @@ -621,9 +702,6 @@ raise FeatureLibError( "Language statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) - if language != 'dflt' and self.script_ == 'DFLT': - raise FeatureLibError("Need non-DFLT script when using non-dflt " - "language (was: \"%s\")" % language, location) self.cur_lookup_ = None key = (self.script_, language, self.cur_feature_name_) @@ -640,12 +718,7 @@ if key[:2] in self.get_default_language_systems_(): lookups = [l for l in lookups if l not in dflt_lookups] self.features_.setdefault(key, []).extend(lookups) - if self.script_ == 'DFLT': - langsys = set(self.get_default_language_systems_()) - else: - langsys = set() - langsys.add((self.script_, language)) - self.language_systems = frozenset(langsys) + self.language_systems = frozenset([(self.script_, language)]) if required: key = (self.script_, language) @@ -764,8 +837,22 @@ location) self.aalt_features_.append((location, featureName)) - def add_featureName(self, location, tag): - self.featureNames_.append(tag) + def add_featureName(self, tag): + self.featureNames_.add(tag) + + def add_cv_parameter(self, tag): + self.cv_parameters_.add(tag) + + def add_to_cv_num_named_params(self, tag): + """Adds new items to self.cv_num_named_params_ + or increments the count of existing items.""" + if tag in self.cv_num_named_params_: + self.cv_num_named_params_[tag] += 1 + else: + self.cv_num_named_params_[tag] = 1 + + def add_cv_character(self, character, tag): + self.cv_characters_[tag].append(character) def set_base_axis(self, bases, scripts, vertical): if vertical: @@ -916,6 +1003,16 @@ lookup = self.get_lookup_(location, PairPosBuilder) lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2) + def add_subtable_break(self, location): + if type(self.cur_lookup_) is not PairPosBuilder: + raise FeatureLibError( + 'explicit "subtable" statement is intended for use with only ' + "Pair Adjustment Positioning Format 2 (i.e. pair class kerning)", + location + ) + lookup = self.get_lookup_(location, PairPosBuilder) + lookup.add_subtable_break(location) + def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addGlyphPair(location, glyph1, value1, glyph2, value2) @@ -1406,6 +1503,7 @@ st = otl.buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap) self.subtables_.append(st) + self.forceSubtableBreak_ = False class PairPosBuilder(LookupBuilder): @@ -1424,15 +1522,19 @@ key = (glyph1, glyph2) oldValue = self.glyphPairs.get(key, None) if oldValue is not None: + # the Feature File spec explicitly allows specific pairs generated + # by an 'enum' rule to be overridden by preceding single pairs; + # we emit a warning and use the previously defined value otherLoc = self.locations[key] - raise FeatureLibError( - 'Already defined position for pair %s %s at %s:%d:%d' - % (glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]), - location) - val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True) - val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True) - self.glyphPairs[key] = (val1, val2) - self.locations[key] = location + log.warning( + 'Already defined position for pair %s %s at %s:%d:%d; ' + 'choosing the first value', + glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]) + else: + val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True) + val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True) + self.glyphPairs[key] = (val1, val2) + self.locations[key] = location def add_subtable_break(self, location): self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, diff -Nru fonttools-3.21.2/Lib/fontTools/feaLib/error.py fonttools-3.29.0/Lib/fontTools/feaLib/error.py --- fonttools-3.21.2/Lib/fontTools/feaLib/error.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/feaLib/error.py 2018-07-26 14:12:55.000000000 +0000 @@ -14,3 +14,7 @@ return "%s:%d:%d: %s" % (path, line, column, message) else: return message + + +class IncludedFeaNotFound(FeatureLibError): + pass diff -Nru fonttools-3.21.2/Lib/fontTools/feaLib/lexer.py fonttools-3.29.0/Lib/fontTools/feaLib/lexer.py --- fonttools-3.21.2/Lib/fontTools/feaLib/lexer.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/feaLib/lexer.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.misc.py23 import * -from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound import re import os @@ -56,7 +56,7 @@ def location_(self): column = self.pos_ - self.line_start_ + 1 - return (self.filename_, self.line_, column) + return (self.filename_ or "", self.line_, column) def next_(self): self.scan_over_(Lexer.CHAR_WHITESPACE_) @@ -211,32 +211,51 @@ #semi_type, semi_token, semi_location = lexer.next() #if semi_type is not Lexer.SYMBOL or semi_token != ";": # raise FeatureLibError("Expected ';'", semi_location) - curpath = os.path.dirname(self.featurefilepath) - path = os.path.join(curpath, fname_token) + if os.path.isabs(fname_token): + path = fname_token + else: + if self.featurefilepath is not None: + curpath = os.path.dirname(self.featurefilepath) + else: + # if the IncludingLexer was initialized from an in-memory + # file-like stream, it doesn't have a 'name' pointing to + # its filesystem path, therefore we fall back to using the + # current working directory to resolve relative includes + curpath = os.getcwd() + path = os.path.join(curpath, fname_token) if len(self.lexers_) >= 5: raise FeatureLibError("Too many recursive includes", fname_location) - self.lexers_.append(self.make_lexer_(path, fname_location)) - continue + try: + self.lexers_.append(self.make_lexer_(path)) + except IOError as err: + # FileNotFoundError does not exist on Python < 3.3 + import errno + if err.errno == errno.ENOENT: + raise IncludedFeaNotFound(fname_token, fname_location) + raise # pragma: no cover else: return (token_type, token, location) raise StopIteration() @staticmethod - def make_lexer_(file_or_path, location=None): + def make_lexer_(file_or_path): if hasattr(file_or_path, "read"): fileobj, closing = file_or_path, False else: filename, closing = file_or_path, True - try: - fileobj = open(filename, "r", encoding="utf-8") - except IOError as err: - raise FeatureLibError(str(err), location) + fileobj = open(filename, "r", encoding="utf-8") data = fileobj.read() - filename = fileobj.name if hasattr(fileobj, "name") else "" + filename = getattr(fileobj, "name", None) if closing: fileobj.close() return Lexer(data, filename) def scan_anonymous_block(self, tag): return self.lexers_[-1].scan_anonymous_block(tag) + + +class NonIncludingLexer(IncludingLexer): + """Lexer that does not follow `include` statements, emits them as-is.""" + def __next__(self): # Python 3 + return next(self.lexers_[0]) diff -Nru fonttools-3.21.2/Lib/fontTools/feaLib/parser.py fonttools-3.29.0/Lib/fontTools/feaLib/parser.py --- fonttools-3.21.2/Lib/fontTools/feaLib/parser.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/feaLib/parser.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.feaLib.error import FeatureLibError -from fontTools.feaLib.lexer import Lexer, IncludingLexer +from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer from fontTools.misc.encodingTools import getEncoding from fontTools.misc.py23 import * import fontTools.feaLib.ast as ast @@ -16,8 +16,11 @@ class Parser(object): extensions = {} ast = ast + SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)} + CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99+1)} - def __init__(self, featurefile, glyphNames=(), **kwargs): + def __init__(self, featurefile, glyphNames=(), followIncludes=True, + **kwargs): if "glyphMap" in kwargs: from fontTools.misc.loggingTools import deprecateArgument deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") @@ -42,15 +45,20 @@ self.next_token_type_, self.next_token_ = (None, None) self.cur_comments_ = [] self.next_token_location_ = None - self.lexer_ = IncludingLexer(featurefile) + lexerClass = IncludingLexer if followIncludes else NonIncludingLexer + self.lexer_ = lexerClass(featurefile) self.advance_lexer_(comments=True) def parse(self): statements = self.doc_.statements - while self.next_token_type_ is not None: + while self.next_token_type_ is not None or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append( + self.ast.Comment(self.cur_token_, + location=self.cur_token_location_)) + elif self.is_cur_keyword_("include"): + statements.append(self.parse_include_()) elif self.cur_token_type_ is Lexer.GLYPHCLASS: statements.append(self.parse_glyphclass_definition_()) elif self.is_cur_keyword_(("anon", "anonymous")): @@ -99,9 +107,11 @@ 'Unknown anchor "%s"' % name, self.cur_token_location_) self.expect_symbol_(">") - return self.ast.Anchor(location, name, anchordef.x, anchordef.y, - anchordef.contourpoint, - xDeviceTable=None, yDeviceTable=None) + return self.ast.Anchor(anchordef.x, anchordef.y, + name=name, + contourpoint=anchordef.contourpoint, + xDeviceTable=None, yDeviceTable=None, + location=location) x, y = self.expect_number_(), self.expect_number_() @@ -117,8 +127,11 @@ xDeviceTable, yDeviceTable = None, None self.expect_symbol_(">") - return self.ast.Anchor(location, None, x, y, contourpoint, - xDeviceTable, yDeviceTable) + return self.ast.Anchor(x, y, name=None, + contourpoint=contourpoint, + xDeviceTable=xDeviceTable, + yDeviceTable=yDeviceTable, + location=location) def parse_anchor_marks_(self): """Parses a sequence of [ mark @MARKCLASS]*.""" @@ -142,7 +155,9 @@ contourpoint = self.expect_number_() name = self.expect_name_() self.expect_symbol_(";") - anchordef = self.ast.AnchorDefinition(location, name, x, y, contourpoint) + anchordef = self.ast.AnchorDefinition(name, x, y, + contourpoint=contourpoint, + location=location) self.anchors_.define(name, anchordef) return anchordef @@ -155,7 +170,7 @@ end_tag = self.expect_tag_() assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()" self.expect_symbol_(';') - return self.ast.AnonymousBlock(tag, content, location) + return self.ast.AnonymousBlock(tag, content, location=location) def parse_attach_(self): assert self.is_cur_keyword_("Attach") @@ -165,7 +180,8 @@ while self.next_token_ != ";": contourPoints.add(self.expect_number_()) self.expect_symbol_(";") - return self.ast.AttachStatement(location, glyphs, contourPoints) + return self.ast.AttachStatement(glyphs, contourPoints, + location=location) def parse_enumerate_(self, vertical): assert self.cur_token_ in {"enumerate", "enum"} @@ -196,8 +212,9 @@ else: componentGlyphs = None self.expect_symbol_(";") - return self.ast.GlyphClassDefStatement(location, baseGlyphs, markGlyphs, - ligatureGlyphs, componentGlyphs) + return self.ast.GlyphClassDefStatement(baseGlyphs, markGlyphs, + ligatureGlyphs, componentGlyphs, + location=location) def parse_glyphclass_definition_(self): """Parses glyph class definitions such as '@UPPERCASE = [A-Z];'""" @@ -205,7 +222,8 @@ self.expect_symbol_("=") glyphs = self.parse_glyphclass_(accept_glyphname=False) self.expect_symbol_(";") - glyphclass = self.ast.GlyphClassDefinition(location, name, glyphs) + glyphclass = self.ast.GlyphClassDefinition(name, glyphs, + location=location) self.glyphclasses_.define(name, glyphclass) return glyphclass @@ -252,7 +270,7 @@ if (accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID)): glyph = self.expect_glyph_() - return self.ast.GlyphName(self.cur_token_location_, glyph) + return self.ast.GlyphName(glyph, location=self.cur_token_location_) if self.next_token_type_ is Lexer.GLYPHCLASS: self.advance_lexer_() gc = self.glyphclasses_.resolve(self.cur_token_) @@ -261,13 +279,15 @@ "Unknown glyph class @%s" % self.cur_token_, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - return self.ast.MarkClassName(self.cur_token_location_, gc) + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - return self.ast.GlyphClassName(self.cur_token_location_, gc) + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) self.expect_symbol_("[") location = self.cur_token_location_ - glyphs = self.ast.GlyphClass(location) + glyphs = self.ast.GlyphClass(location=location) while self.next_token_ != "]": if self.next_token_type_ is Lexer.NAME: glyph = self.expect_glyph_() @@ -306,9 +326,11 @@ "Unknown glyph class @%s" % self.cur_token_, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - gc = self.ast.MarkClassName(self.cur_token_location_, gc) + gc = self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - gc = self.ast.GlyphClassName(self.cur_token_location_, gc) + gc = self.ast.GlyphClassName( + gc, location=self.cur_token_location_) glyphs.add_class(gc) else: raise FeatureLibError( @@ -326,9 +348,11 @@ "Unknown glyph class @%s" % name, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - return self.ast.MarkClassName(self.cur_token_location_, gc) + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - return self.ast.GlyphClassName(self.cur_token_location_, gc) + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) def parse_glyph_pattern_(self, vertical): prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) @@ -408,18 +432,27 @@ raise FeatureLibError( "No lookups can be specified for \"ignore sub\"", location) - return self.ast.IgnoreSubstStatement(location, chainContext) + return self.ast.IgnoreSubstStatement(chainContext, + location=location) if self.cur_token_ in ["position", "pos"]: chainContext, hasLookups = self.parse_chain_context_() if hasLookups: raise FeatureLibError( "No lookups can be specified for \"ignore pos\"", location) - return self.ast.IgnorePosStatement(location, chainContext) + return self.ast.IgnorePosStatement(chainContext, + location=location) raise FeatureLibError( "Expected \"substitute\" or \"position\"", self.cur_token_location_) + def parse_include_(self): + assert self.cur_token_ == "include" + location = self.cur_token_location_ + filename = self.expect_filename_() + # self.expect_symbol_(";") + return ast.IncludeStatement(filename, location=location) + def parse_language_(self): assert self.is_cur_keyword_("language") location = self.cur_token_location_ @@ -431,8 +464,9 @@ self.expect_keyword_("required") required = True self.expect_symbol_(";") - return self.ast.LanguageStatement(location, language, - include_default, required) + return self.ast.LanguageStatement(language, + include_default, required, + location=location) def parse_ligatureCaretByIndex_(self): assert self.is_cur_keyword_("LigatureCaretByIndex") @@ -442,7 +476,8 @@ while self.next_token_ != ";": carets.append(self.expect_number_()) self.expect_symbol_(";") - return self.ast.LigatureCaretByIndexStatement(location, glyphs, carets) + return self.ast.LigatureCaretByIndexStatement(glyphs, carets, + location=location) def parse_ligatureCaretByPos_(self): assert self.is_cur_keyword_("LigatureCaretByPos") @@ -452,7 +487,8 @@ while self.next_token_ != ";": carets.append(self.expect_number_()) self.expect_symbol_(";") - return self.ast.LigatureCaretByPosStatement(location, glyphs, carets) + return self.ast.LigatureCaretByPosStatement(glyphs, carets, + location=location) def parse_lookup_(self, vertical): assert self.is_cur_keyword_("lookup") @@ -464,14 +500,15 @@ raise FeatureLibError("Unknown lookup \"%s\"" % name, self.cur_token_location_) self.expect_symbol_(";") - return self.ast.LookupReferenceStatement(location, lookup) + return self.ast.LookupReferenceStatement(lookup, + location=location) use_extension = False if self.next_token_ == "useExtension": self.expect_keyword_("useExtension") use_extension = True - block = self.ast.LookupBlock(location, name, use_extension) + block = self.ast.LookupBlock(name, use_extension, location=location) self.parse_block_(block, vertical) self.lookups_.define(name, block) return block @@ -484,7 +521,7 @@ if self.next_token_type_ == Lexer.NUMBER: value = self.expect_number_() self.expect_symbol_(";") - return self.ast.LookupFlagStatement(location, value, None, None) + return self.ast.LookupFlagStatement(value, location=location) # format A: "lookupflag RightToLeft MarkAttachmentType @M;" value, markAttachment, markFilteringSet = 0, None, None @@ -512,8 +549,10 @@ '"%s" is not a recognized lookupflag' % self.next_token_, self.next_token_location_) self.expect_symbol_(";") - return self.ast.LookupFlagStatement(location, value, - markAttachment, markFilteringSet) + return self.ast.LookupFlagStatement(value, + markAttachment=markAttachment, + markFilteringSet=markFilteringSet, + location=location) def parse_markClass_(self): assert self.is_cur_keyword_("markClass") @@ -527,7 +566,8 @@ markClass = self.ast.MarkClass(name) self.doc_.markClasses[name] = markClass self.glyphclasses_.define(name, markClass) - mcdef = self.ast.MarkClassDefinition(location, markClass, anchor, glyphs) + mcdef = self.ast.MarkClassDefinition(markClass, anchor, glyphs, + location=location) markClass.addDefinition(mcdef) return mcdef @@ -554,7 +594,7 @@ "If \"lookup\" is present, no values must be specified", location) return self.ast.ChainContextPosStatement( - location, prefix, glyphs, suffix, lookups) + prefix, glyphs, suffix, lookups, location=location) # Pair positioning, format A: "pos V 10 A -10;" # Pair positioning, format B: "pos V A -20;" @@ -562,14 +602,16 @@ if values[0] is None: # Format B: "pos V A -20;" values.reverse() return self.ast.PairPosStatement( - location, enumerated, - glyphs[0], values[0], glyphs[1], values[1]) + glyphs[0], values[0], glyphs[1], values[1], + enumerated=enumerated, + location=location) if enumerated: raise FeatureLibError( '"enumerate" is only allowed with pair positionings', location) - return self.ast.SinglePosStatement(location, list(zip(glyphs, values)), - prefix, suffix, forceChain=hasMarks) + return self.ast.SinglePosStatement(list(zip(glyphs, values)), + prefix, suffix, forceChain=hasMarks, + location=location) def parse_position_cursive_(self, enumerated, vertical): location = self.cur_token_location_ @@ -584,7 +626,7 @@ exitAnchor = self.parse_anchor_() self.expect_symbol_(";") return self.ast.CursivePosStatement( - location, glyphclass, entryAnchor, exitAnchor) + glyphclass, entryAnchor, exitAnchor, location=location) def parse_position_base_(self, enumerated, vertical): location = self.cur_token_location_ @@ -597,7 +639,7 @@ base = self.parse_glyphclass_(accept_glyphname=True) marks = self.parse_anchor_marks_() self.expect_symbol_(";") - return self.ast.MarkBasePosStatement(location, base, marks) + return self.ast.MarkBasePosStatement(base, marks, location=location) def parse_position_ligature_(self, enumerated, vertical): location = self.cur_token_location_ @@ -613,7 +655,7 @@ self.expect_keyword_("ligComponent") marks.append(self.parse_anchor_marks_()) self.expect_symbol_(";") - return self.ast.MarkLigPosStatement(location, ligatures, marks) + return self.ast.MarkLigPosStatement(ligatures, marks, location=location) def parse_position_mark_(self, enumerated, vertical): location = self.cur_token_location_ @@ -626,13 +668,14 @@ baseMarks = self.parse_glyphclass_(accept_glyphname=True) marks = self.parse_anchor_marks_() self.expect_symbol_(";") - return self.ast.MarkMarkPosStatement(location, baseMarks, marks) + return self.ast.MarkMarkPosStatement(baseMarks, marks, + location=location) def parse_script_(self): assert self.is_cur_keyword_("script") location, script = self.cur_token_location_, self.expect_script_tag_() self.expect_symbol_(";") - return self.ast.ScriptStatement(location, script) + return self.ast.ScriptStatement(script, location=location) def parse_substitute_(self): assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"} @@ -676,7 +719,7 @@ 'Expected a single glyphclass after "from"', location) return self.ast.AlternateSubstStatement( - location, old_prefix, old[0], old_suffix, new[0]) + old_prefix, old[0], old_suffix, new[0], location=location) num_lookups = len([l for l in lookups if l is not None]) @@ -696,9 +739,10 @@ 'but found a glyph class with %d elements' % (len(glyphs), len(replacements)), location) return self.ast.SingleSubstStatement( - location, old, new, + old, new, old_prefix, old_suffix, - forceChain=hasMarks + forceChain=hasMarks, + location=location ) # GSUB lookup type 2: Multiple substitution. @@ -708,8 +752,8 @@ len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and num_lookups == 0): return self.ast.MultipleSubstStatement( - location, old_prefix, tuple(old[0].glyphSet())[0], old_suffix, - tuple([list(n.glyphSet())[0] for n in new])) + old_prefix, tuple(old[0].glyphSet())[0], old_suffix, + tuple([list(n.glyphSet())[0] for n in new]), location=location) # GSUB lookup type 4: Ligature substitution. # Format: "substitute f f i by f_f_i;" @@ -718,8 +762,9 @@ len(new[0].glyphSet()) == 1 and num_lookups == 0): return self.ast.LigatureSubstStatement( - location, old_prefix, old, old_suffix, - list(new[0].glyphSet())[0], forceChain=hasMarks) + old_prefix, old, old_suffix, + list(new[0].glyphSet())[0], forceChain=hasMarks, + location=location) # GSUB lookup type 8: Reverse chaining substitution. if reverse: @@ -747,19 +792,25 @@ 'but found a glyph class with %d elements' % (len(glyphs), len(replacements)), location) return self.ast.ReverseChainSingleSubstStatement( - location, old_prefix, old_suffix, old, new) + old_prefix, old_suffix, old, new, location=location) + + if len(old) > 1 and len(new) > 1: + raise FeatureLibError( + 'Direct substitution of multiple glyphs by multiple glyphs ' + 'is not supported', + location) # GSUB lookup type 6: Chaining contextual substitution. assert len(new) == 0, new rule = self.ast.ChainContextSubstStatement( - location, old_prefix, old, old_suffix, lookups) + old_prefix, old, old_suffix, lookups, location=location) return rule def parse_subtable_(self): assert self.is_cur_keyword_("subtable") location = self.cur_token_location_ self.expect_symbol_(";") - return self.ast.SubtableStatement(location) + return self.ast.SubtableStatement(location=location) def parse_size_parameters_(self): assert self.is_cur_keyword_("parameters") @@ -774,20 +825,22 @@ RangeEnd = self.expect_decipoint_() self.expect_symbol_(";") - return self.ast.SizeParameters(location, DesignSize, SubfamilyID, - RangeStart, RangeEnd) + return self.ast.SizeParameters(DesignSize, SubfamilyID, + RangeStart, RangeEnd, + location=location) def parse_size_menuname_(self): assert self.is_cur_keyword_("sizemenuname") location = self.cur_token_location_ platformID, platEncID, langID, string = self.parse_name_() - return self.ast.FeatureNameStatement(location, "size", platformID, - platEncID, langID, string) + return self.ast.FeatureNameStatement("size", platformID, + platEncID, langID, string, + location=location) def parse_table_(self): assert self.is_cur_keyword_("table") location, name = self.cur_token_location_, self.expect_tag_() - table = self.ast.TableBlock(location, name) + table = self.ast.TableBlock(name, location=location) self.expect_symbol_("{") handler = { "GDEF": self.parse_table_GDEF_, @@ -816,7 +869,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("Attach"): statements.append(self.parse_attach_()) elif self.is_cur_keyword_("GlyphClassDef"): @@ -838,7 +892,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("FontRevision"): statements.append(self.parse_FontRevision_()) elif self.cur_token_ == ";": @@ -853,12 +908,14 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: key = self.cur_token_.lower() value = self.expect_number_() statements.append( - self.ast.HheaField(self.cur_token_location_, key, value)) + self.ast.HheaField(key, value, + location=self.cur_token_location_)) if self.next_token_ != ";": raise FeatureLibError("Incomplete statement", self.next_token_location_) elif self.cur_token_ == ";": @@ -874,12 +931,14 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: key = self.cur_token_.lower() value = self.expect_number_() statements.append( - self.ast.VheaField(self.cur_token_location_, key, value)) + self.ast.VheaField(key, value, + location=self.cur_token_location_)) if self.next_token_ != ";": raise FeatureLibError("Incomplete statement", self.next_token_location_) elif self.cur_token_ == ";": @@ -894,7 +953,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("nameid"): statement = self.parse_nameid_() if statement: @@ -949,8 +1009,8 @@ return None platformID, platEncID, langID, string = self.parse_name_() - return self.ast.NameRecord(location, nameID, platformID, platEncID, - langID, string) + return self.ast.NameRecord(nameID, platformID, platEncID, + langID, string, location=location) def unescape_string_(self, string, encoding): if encoding == "utf_16_be": @@ -979,21 +1039,24 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("HorizAxis.BaseTagList"): horiz_bases = self.parse_base_tag_list_() elif self.is_cur_keyword_("HorizAxis.BaseScriptList"): horiz_scripts = self.parse_base_script_list_(len(horiz_bases)) statements.append( - self.ast.BaseAxis(self.cur_token_location_, horiz_bases, - horiz_scripts, False)) + self.ast.BaseAxis(horiz_bases, + horiz_scripts, False, + location=self.cur_token_location_)) elif self.is_cur_keyword_("VertAxis.BaseTagList"): vert_bases = self.parse_base_tag_list_() elif self.is_cur_keyword_("VertAxis.BaseScriptList"): vert_scripts = self.parse_base_script_list_(len(vert_bases)) statements.append( - self.ast.BaseAxis(self.cur_token_location_, vert_bases, - vert_scripts, True)) + self.ast.BaseAxis(vert_bases, + vert_scripts, True, + location=self.cur_token_location_)) elif self.cur_token_ == ";": continue @@ -1006,7 +1069,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME: key = self.cur_token_.lower() value = None @@ -1023,7 +1087,8 @@ elif self.is_cur_keyword_("Vendor"): value = self.expect_string_() statements.append( - self.ast.OS2Field(self.cur_token_location_, key, value)) + self.ast.OS2Field(key, value, + location=self.cur_token_location_)) elif self.cur_token_ == ";": continue @@ -1074,13 +1139,13 @@ if self.next_token_type_ is Lexer.NUMBER: number, location = self.expect_number_(), self.cur_token_location_ if vertical: - val = self.ast.ValueRecord(location, vertical, - None, None, None, number, - None, None, None, None) + val = self.ast.ValueRecord(yAdvance=number, + vertical=vertical, + location=location) else: - val = self.ast.ValueRecord(location, vertical, - None, None, number, None, - None, None, None, None) + val = self.ast.ValueRecord(xAdvance=number, + vertical=vertical, + location=location) return val self.expect_symbol_("<") location = self.cur_token_location_ @@ -1122,8 +1187,9 @@ self.expect_symbol_(">") return self.ast.ValueRecord( - location, vertical, xPlacement, yPlacement, xAdvance, yAdvance, - xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice) + xPlacement, yPlacement, xAdvance, yAdvance, + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice, + vertical=vertical, location=location) def parse_valuerecord_definition_(self, vertical): assert self.is_cur_keyword_("valueRecordDef") @@ -1131,7 +1197,7 @@ value = self.parse_valuerecord_(vertical) name = self.expect_name_() self.expect_symbol_(";") - vrd = self.ast.ValueRecordDefinition(location, name, value) + vrd = self.ast.ValueRecordDefinition(name, value, location=location) self.valuerecords_.define(name, vrd) return vrd @@ -1141,30 +1207,34 @@ script = self.expect_script_tag_() language = self.expect_language_tag_() self.expect_symbol_(";") - if script == "DFLT" and language != "dflt": - raise FeatureLibError( - 'For script "DFLT", the language must be "dflt"', - self.cur_token_location_) - return self.ast.LanguageSystemStatement(location, script, language) + return self.ast.LanguageSystemStatement(script, language, + location=location) def parse_feature_block_(self): assert self.cur_token_ == "feature" location = self.cur_token_location_ tag = self.expect_tag_() vertical = (tag in {"vkrn", "vpal", "vhal", "valt"}) + stylisticset = None - if tag in ["ss%02d" % i for i in range(1, 20+1)]: + cv_feature = None + size_feature = False + if tag in self.SS_FEATURE_TAGS: stylisticset = tag - - size_feature = (tag == "size") + elif tag in self.CV_FEATURE_TAGS: + cv_feature = tag + elif tag == "size": + size_feature = True use_extension = False if self.next_token_ == "useExtension": self.expect_keyword_("useExtension") use_extension = True - block = self.ast.FeatureBlock(location, tag, use_extension) - self.parse_block_(block, vertical, stylisticset, size_feature) + block = self.ast.FeatureBlock(tag, use_extension=use_extension, + location=location) + self.parse_block_(block, vertical, stylisticset, size_feature, + cv_feature) return block def parse_feature_reference_(self): @@ -1172,24 +1242,93 @@ location = self.cur_token_location_ featureName = self.expect_tag_() self.expect_symbol_(";") - return self.ast.FeatureReferenceStatement(location, featureName) + return self.ast.FeatureReferenceStatement(featureName, + location=location) def parse_featureNames_(self, tag): assert self.cur_token_ == "featureNames", self.cur_token_ - block = self.ast.FeatureNamesBlock(self.cur_token_location_) + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("name"): + location = self.cur_token_location_ + platformID, platEncID, langID, string = self.parse_name_() + block.statements.append( + self.ast.FeatureNameStatement(tag, platformID, + platEncID, langID, string, + location=location)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError('Expected "name"', + self.cur_token_location_) + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvParameters_(self, tag): + assert self.cur_token_ == "cvParameters", self.cur_token_ + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) self.expect_symbol_("{") for symtab in self.symbol_tables_: symtab.enter_scope() + + statements = block.statements while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - block.statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_({"FeatUILabelNameID", + "FeatUITooltipTextNameID", + "SampleTextNameID", + "ParamUILabelNameID"}): + statements.append(self.parse_cvNameIDs_(tag, self.cur_token_)) + elif self.is_cur_keyword_("Character"): + statements.append(self.parse_cvCharacter_(tag)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected statement: got {} {}".format( + self.cur_token_type_, self.cur_token_), + self.cur_token_location_) + + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvNameIDs_(self, tag, block_name): + assert self.cur_token_ == block_name, self.cur_token_ + block = self.ast.NestedBlock(tag, block_name, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("name"): location = self.cur_token_location_ platformID, platEncID, langID, string = self.parse_name_() block.statements.append( - self.ast.FeatureNameStatement(location, tag, platformID, - platEncID, langID, string)) + self.ast.CVParametersNameStatement( + tag, platformID, platEncID, langID, string, + block_name, location=location)) elif self.cur_token_ == ";": continue else: @@ -1201,6 +1340,16 @@ self.expect_symbol_(";") return block + def parse_cvCharacter_(self, tag): + assert self.cur_token_ == "Character", self.cur_token_ + location, character = self.cur_token_location_, self.expect_decimal_or_hexadecimal_() + self.expect_symbol_(";") + if not (0xFFFFFF >= character >= 0): + raise FeatureLibError("Character value must be between " + "{:#x} and {:#x}".format(0, 0xFFFFFF), + location) + return self.ast.CharacterStatement(character, tag, location=location) + def parse_FontRevision_(self): assert self.cur_token_ == "FontRevision", self.cur_token_ location, version = self.cur_token_location_, self.expect_float_() @@ -1208,10 +1357,10 @@ if version <= 0: raise FeatureLibError("Font revision numbers must be positive", location) - return self.ast.FontRevisionStatement(location, version) + return self.ast.FontRevisionStatement(version, location=location) def parse_block_(self, block, vertical, stylisticset=None, - size_feature=False): + size_feature=False, cv_feature=None): self.expect_symbol_("{") for symtab in self.symbol_tables_: symtab.enter_scope() @@ -1220,7 +1369,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.GLYPHCLASS: statements.append(self.parse_glyphclass_definition_()) elif self.is_cur_keyword_("anchorDef"): @@ -1253,6 +1403,8 @@ statements.append(self.parse_valuerecord_definition_(vertical)) elif stylisticset and self.is_cur_keyword_("featureNames"): statements.append(self.parse_featureNames_(stylisticset)) + elif cv_feature and self.is_cur_keyword_("cvParameters"): + statements.append(self.parse_cvParameters_(cv_feature)) elif size_feature and self.is_cur_keyword_("parameters"): statements.append(self.parse_size_parameters_()) elif size_feature and self.is_cur_keyword_("sizemenuname"): @@ -1294,9 +1446,10 @@ if has_single and has_multiple: for i, s in enumerate(statements): if isinstance(s, self.ast.SingleSubstStatement): - statements[i] = self.ast.MultipleSubstStatement(s.location, + statements[i] = self.ast.MultipleSubstStatement( s.prefix, s.glyphs[0].glyphSet()[0], s.suffix, - [r.glyphSet()[0] for r in s.replacements]) + [r.glyphSet()[0] for r in s.replacements], + location=s.location) def is_cur_keyword_(self, k): if self.cur_token_type_ is Lexer.NAME: @@ -1318,6 +1471,13 @@ return self.cur_token_ raise FeatureLibError("Expected a CID", self.cur_token_location_) + def expect_filename_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.FILENAME: + raise FeatureLibError("Expected file name", + self.cur_token_location_) + return self.cur_token_ + def expect_glyph_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.NAME: @@ -1388,6 +1548,7 @@ return self.cur_token_ raise FeatureLibError("Expected a name", self.cur_token_location_) + # TODO: Don't allow this method to accept hexadecimal values def expect_number_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.NUMBER: @@ -1401,6 +1562,7 @@ raise FeatureLibError("Expected a floating-point number", self.cur_token_location_) + # TODO: Don't allow this method to accept hexadecimal values def expect_decipoint_(self): if self.next_token_type_ == Lexer.FLOAT: return self.expect_float_() @@ -1410,6 +1572,18 @@ raise FeatureLibError("Expected an integer or floating-point number", self.cur_token_location_) + def expect_decimal_or_hexadecimal_(self): + # the lexer returns the same token type 'NUMBER' for either decimal or + # hexadecimal integers, and casts them both to a `int` type, so it's + # impossible to distinguish the two here. This method is implemented + # the same as `expect_number_`, only it gives a more informative + # error message + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NUMBER: + return self.cur_token_ + raise FeatureLibError("Expected a decimal or hexadecimal number", + self.cur_token_location_) + def expect_string_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.STRING: @@ -1424,7 +1598,6 @@ else: self.cur_token_type_, self.cur_token_, self.cur_token_location_ = ( self.next_token_type_, self.next_token_, self.next_token_location_) - self.cur_comments_ = [] while True: try: (self.next_token_type_, self.next_token_, diff -Nru fonttools-3.21.2/Lib/fontTools/__init__.py fonttools-3.29.0/Lib/fontTools/__init__.py --- fonttools-3.21.2/Lib/fontTools/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,6 +5,6 @@ log = logging.getLogger(__name__) -version = __version__ = "3.21.2" +version = __version__ = "3.29.0" __all__ = ["version", "log", "configLogger"] diff -Nru fonttools-3.21.2/Lib/fontTools/merge.py fonttools-3.29.0/Lib/fontTools/merge.py --- fonttools-3.21.2/Lib/fontTools/merge.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/merge.py 2018-07-26 14:12:55.000000000 +0000 @@ -12,6 +12,7 @@ from fontTools.ttLib.tables import otTables, _h_e_a_d from fontTools.ttLib.tables.DefaultTable import DefaultTable from fontTools.misc.loggingTools import Timer +from fontTools.pens.recordingPen import DecomposingRecordingPen from functools import reduce import sys import time @@ -351,6 +352,17 @@ ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable +def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2): + pen1 = DecomposingRecordingPen(glyphSet1) + pen2 = DecomposingRecordingPen(glyphSet2) + g1 = glyphSet1[glyph1] + g2 = glyphSet2[glyph2] + g1.draw(pen1) + g2.draw(pen2) + return (pen1.value == pen2.value and + g1.width == g2.width and + (not hasattr(g1, 'height') or g1.height == g2.height)) + @_add_method(ttLib.getTableClass('cmap')) def merge(self, m, tables): # TODO Handle format=14. @@ -373,19 +385,30 @@ # Build a unicode mapping, then decide which format is needed to store it. cmap = {} + fontIndexForGlyph = {} + glyphSets = [None for f in m.fonts] if hasattr(m, 'fonts') else None for table,fontIdx in cmapTables: # handle duplicates for uni,gid in table.cmap.items(): oldgid = cmap.get(uni, None) if oldgid is None: cmap[uni] = gid + fontIndexForGlyph[gid] = fontIdx elif oldgid != gid: # Char previously mapped to oldgid, now to gid. # Record, to fix up in GSUB 'locl' later. - if m.duplicateGlyphsPerFont[fontIdx].get(oldgid, gid) == gid: + if m.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None: + if glyphSets is not None: + oldFontIdx = fontIndexForGlyph[oldgid] + for idx in (fontIdx, oldFontIdx): + if glyphSets[idx] is None: + glyphSets[idx] = m.fonts[idx].getGlyphSet() + if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid): + continue m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid - else: - # Char previously mapped to oldgid but already remapped to a different gid. + elif m.duplicateGlyphsPerFont[fontIdx][oldgid] != gid: + # Char previously mapped to oldgid but oldgid is already remapped to a different + # gid, because of another Unicode character. # TODO: Try harder to do something about these. log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid) @@ -459,12 +482,22 @@ if len(lst) == 1: return lst[0] - # TODO Support merging LangSysRecords - assert all(not s.LangSysRecord for s in lst) + langSyses = {} + for sr in lst: + for lsr in sr.LangSysRecord: + if lsr.LangSysTag not in langSyses: + langSyses[lsr.LangSysTag] = [] + langSyses[lsr.LangSysTag].append(lsr.LangSys) + lsrecords = [] + for tag, langSys_list in sorted(langSyses.items()): + lsr = otTables.LangSysRecord() + lsr.LangSys = mergeLangSyses(langSys_list) + lsr.LangSysTag = tag + lsrecords.append(lsr) self = otTables.Script() - self.LangSysRecord = [] - self.LangSysCount = 0 + self.LangSysRecord = lsrecords + self.LangSysCount = len(lsrecords) self.DefaultLangSys = mergeLangSyses([s.DefaultLangSys for s in lst if s.DefaultLangSys]) return self @@ -604,6 +637,13 @@ synthLookup.LookupType = 1 synthLookup.SubTableCount = 1 synthLookup.SubTable = [subtable] + if table.table.LookupList is None: + # mtiLib uses None as default value for LookupList, + # while feaLib points to an empty array with count 0 + # TODO: make them do the same + table.table.LookupList = otTables.LookupList() + table.table.LookupList.Lookup = [] + table.table.LookupList.LookupCount = 0 table.table.LookupList.Lookup.append(synthLookup) table.table.LookupList.LookupCount += 1 @@ -824,9 +864,9 @@ return ret -class _AttendanceRecordingIdentityDict(dict): +class _AttendanceRecordingIdentityDict(object): """A dictionary-like object that records indices of items actually accessed - from a list.""" + from a list.""" def __init__(self, lst): self.l = lst @@ -837,7 +877,7 @@ self.s.add(self.d[id(v)]) return v -class _GregariousDict(dict): +class _GregariousIdentityDict(object): """A dictionary-like object that welcomes guests without reservations and adds them to the end of the guest list.""" @@ -851,14 +891,23 @@ self.l.append(v) return v -class _NonhashableDict(dict): - """A dictionary-like object mapping objects to their index within a list.""" +class _NonhashableDict(object): + """A dictionary-like object mapping objects to values.""" - def __init__(self, lst): - self.d = {id(v):i for i,v in enumerate(lst)} + def __init__(self, keys, values=None): + if values is None: + self.d = {id(v):i for i,v in enumerate(keys)} + else: + self.d = {id(k):v for k,v in zip(keys, values)} - def __getitem__(self, v): - return self.d[id(v)] + def __getitem__(self, k): + return self.d[id(k)] + + def __setitem__(self, k, v): + self.d[id(k)] = v + + def __delitem__(self, k): + del self.d[id(k)] class Merger(object): @@ -890,6 +939,7 @@ for font in fonts: self._preMerge(font) + self.fonts = fonts self.duplicateGlyphsPerFont = [{} for f in fonts] allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) @@ -919,6 +969,7 @@ log.info("Dropped '%s'.", tag) del self.duplicateGlyphsPerFont + del self.fonts self._postMerge(mega) @@ -997,7 +1048,7 @@ if t.table.FeatureList and t.table.ScriptList: # Collect unregistered (new) features. - featureMap = _GregariousDict(t.table.FeatureList.FeatureRecord) + featureMap = _GregariousIdentityDict(t.table.FeatureList.FeatureRecord) t.table.ScriptList.mapFeatures(featureMap) # Record used features. @@ -1017,7 +1068,7 @@ if t.table.LookupList: # Collect unregistered (new) lookups. - lookupMap = _GregariousDict(t.table.LookupList.Lookup) + lookupMap = _GregariousIdentityDict(t.table.LookupList.Lookup) t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/bezierTools.py fonttools-3.29.0/Lib/fontTools/misc/bezierTools.py --- fonttools-3.21.2/Lib/fontTools/misc/bezierTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/bezierTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -13,10 +13,12 @@ "approximateCubicArcLengthC", "approximateQuadraticArcLength", "approximateQuadraticArcLengthC", + "calcCubicArcLength", + "calcCubicArcLengthC", "calcQuadraticArcLength", "calcQuadraticArcLengthC", - "calcQuadraticBounds", "calcCubicBounds", + "calcQuadraticBounds", "splitLine", "splitQuadratic", "splitCubic", @@ -27,6 +29,32 @@ ] +def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment.""" + return calcCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance) + + +def _split_cubic_into_two(p0, p1, p2, p3): + mid = (p0 + 3 * (p1 + p2) + p3) * .125 + deriv3 = (p3 + p2 - p1 - p0) * .125 + return ((p0, (p0 + p1) * .5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * .5, p3)) + +def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): + arch = abs(p0-p3) + box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3) + if arch * mult >= box: + return (arch + box) * .5 + else: + one,two = _split_cubic_into_two(p0,p1,p2,p3) + return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(mult, *two) + +def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment using complex points.""" + mult = 1. + 1.5 * tolerance # The 1.5 is a empirical hack; no math + return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) + + epsilonDigits = 6 epsilon = 1e-10 @@ -41,7 +69,7 @@ return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2 -def calcQuadraticArcLength(pt1, pt2, pt3, approximate_fallback=False): +def calcQuadraticArcLength(pt1, pt2, pt3): """Return the arc length for a qudratic bezier segment. pt1 and pt3 are the "anchor" points, pt2 is the "handle". @@ -59,18 +87,18 @@ 120.21581243984076 >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50)) 102.53273816445825 - >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0), True) # collinear points, control point outside, exact result should be 66.6666666666667 - 69.41755572720999 - >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0), True) # collinear points, looping back, exact result should be 40 - 34.4265186329548 + >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside + 66.66666666666667 + >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back + 40.0 """ - return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), approximate_fallback) + return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) -def calcQuadraticArcLengthC(pt1, pt2, pt3, approximate_fallback=False): +def calcQuadraticArcLengthC(pt1, pt2, pt3): """Return the arc length for a qudratic bezier segment using complex points. pt1 and pt3 are the "anchor" points, pt2 is the "handle".""" - + # Analytical solution to the length of a quadratic bezier. # I'll explain how I arrived at this later. d0 = pt2 - pt1 @@ -81,12 +109,11 @@ if scale == 0.: return abs(pt3-pt1) origDist = _dot(n,d0) - if origDist == 0.: + if abs(origDist) < epsilon: if _dot(d0,d1) >= 0: return abs(pt3-pt1) - if approximate_fallback: - return approximateQuadraticArcLengthC(pt1, pt2, pt3) - assert 0 # TODO handle cusps + a, b = abs(d0), abs(d1) + return (a*a + b*b) / (a+b) x0 = _dot(d,d0) / origDist x1 = _dot(d,d1) / origDist Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0))) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/filenames.py fonttools-3.29.0/Lib/fontTools/misc/filenames.py --- fonttools-3.21.2/Lib/fontTools/misc/filenames.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/filenames.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,224 @@ +""" +User name to file name conversion based on the UFO 3 spec: +http://unifiedfontobject.org/versions/ufo3/conventions/ + +The code was copied from: +https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py + +Author: Tal Leming +Copyright (c) 2005-2016, The RoboFab Developers: + Erik van Blokland + Tal Leming + Just van Rossum +""" +from __future__ import unicode_literals +from fontTools.misc.py23 import basestring, unicode + + +illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") +illegalCharacters += [chr(i) for i in range(1, 32)] +illegalCharacters += [chr(0x7F)] +reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") +reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") +maxFileNameLength = 255 + + +class NameTranslationError(Exception): + pass + + +def userNameToFileName(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True + """ + # the incoming name must be a unicode string + if not isinstance(userName, unicode): + raise ValueError("The value for userName must be a unicode string.") + # establish the prefix and suffix lengths + prefixLength = len(prefix) + suffixLength = len(suffix) + # replace an initial period with an _ + # if no prefix is to be added + if not prefix and userName[0] == ".": + userName = "_" + userName[1:] + # filter the user name + filteredUserName = [] + for character in userName: + # replace illegal characters with _ + if character in illegalCharacters: + character = "_" + # add _ to all non-lower characters + elif character != character.lower(): + character += "_" + filteredUserName.append(character) + userName = "".join(filteredUserName) + # clip to 255 + sliceLength = maxFileNameLength - prefixLength - suffixLength + userName = userName[:sliceLength] + # test for illegal files names + parts = [] + for part in userName.split("."): + if part.lower() in reservedFileNames: + part = "_" + part + parts.append(part) + userName = ".".join(parts) + # test for clash + fullName = prefix + userName + suffix + if fullName.lower() in existing: + fullName = handleClash1(userName, existing, prefix, suffix) + # finished + return fullName + +def handleClash1(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + """ + # if the prefix length + user name length + suffix length + 15 is at + # or past the maximum length, silce 15 characters off of the user name + prefixLength = len(prefix) + suffixLength = len(suffix) + if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: + l = (prefixLength + len(userName) + suffixLength + 15) + sliceLength = maxFileNameLength - l + userName = userName[:sliceLength] + finalName = None + # try to add numbers to create a unique name + counter = 1 + while finalName is None: + name = userName + str(counter).zfill(15) + fullName = prefix + name + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= 999999999999999: + break + # if there is a clash, go to the next fallback + if finalName is None: + finalName = handleClash2(existing, prefix, suffix) + # finished + return finalName + +def handleClash2(existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True + """ + # calculate the longest possible string + maxLength = maxFileNameLength - len(prefix) - len(suffix) + maxValue = int("9" * maxLength) + # try to find a number + finalName = None + counter = 1 + while finalName is None: + fullName = prefix + str(counter) + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= maxValue: + break + # raise an error if nothing has been found + if finalName is None: + raise NameTranslationError("No unique name could be found.") + # finished + return finalName + +if __name__ == "__main__": + import doctest + import sys + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/fixedTools.py fonttools-3.29.0/Lib/fontTools/misc/fixedTools.py --- fonttools-3.21.2/Lib/fontTools/misc/fixedTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/fixedTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,11 +3,13 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +import math import logging log = logging.getLogger(__name__) __all__ = [ + "otRound", "fixedToFloat", "floatToFixed", "floatToFixedToFloat", @@ -15,6 +17,18 @@ "versionToFixed", ] + +def otRound(value): + """Round float value to nearest integer towards +Infinity. + For fractional values of 0.5 and higher, take the next higher integer; + for other fractional values, truncate. + + https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview + https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 + """ + return int(math.floor(value + 0.5)) + + def fixedToFloat(value, precisionBits): """Converts a fixed-point number to a float, choosing the float that has the shortest decimal reprentation. Eg. to convert a @@ -50,7 +64,7 @@ """Converts a float to a fixed-point number given the number of precisionBits. Ie. round(value * (1<= hdlr.level: + hdlr.handle(record) + if not c.propagate: + c = None # break out + else: + c = c.parent + if found == 0: + if logging.lastResort: + if record.levelno >= logging.lastResort.level: + logging.lastResort.handle(record) + elif ( + logging.raiseExceptions + and not self.manager.emittedNoHandlerWarning + ): + sys.stderr.write( + "No handlers could be found for logger" + ' "%s"\n' % self.name + ) + self.manager.emittedNoHandlerWarning = True + + +class StderrHandler(logging.StreamHandler): + """ This class is like a StreamHandler using sys.stderr, but always uses + whateve sys.stderr is currently set to rather than the value of + sys.stderr at handler construction time. + """ + + def __init__(self, level=logging.NOTSET): + """ + Initialize the handler. + """ + logging.Handler.__init__(self, level) + + @property + def stream(self): + # the try/execept avoids failures during interpreter shutdown, when + # globals are set to None + try: + return sys.stderr + except AttributeError: + return __import__("sys").stderr + + if __name__ == "__main__": import doctest sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/psCharStrings.py fonttools-3.29.0/Lib/fontTools/misc/psCharStrings.py --- fonttools-3.21.2/Lib/fontTools/misc/psCharStrings.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/psCharStrings.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import fixedToFloat +from fontTools.misc.fixedTools import fixedToFloat, otRound from fontTools.pens.boundsPen import BoundsPen import struct import logging @@ -216,8 +216,12 @@ encodeIntT2 = getIntEncoder("t2") def encodeFixed(f, pack=struct.pack): - # For T2 only - return b"\xff" + pack(">l", round(f * 65536)) + """For T2 only""" + value = otRound(f * 65536) # convert the float to fixed point + if value & 0xFFFF == 0: # check if the fractional part is zero + return encodeIntT2(value >> 16) # encode only the integer part + else: + return b"\xff" + pack(">l", value) # encode the entire fixed point value def encodeFloat(f): # For CFF only, used in cffLib @@ -944,7 +948,7 @@ decompilerClass = SimpleT2Decompiler outlineExtractor = T2OutlineExtractor isCFF2 = False - + def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): if program is None: program = [] @@ -979,8 +983,8 @@ extractor.execute(self) self.width = extractor.width - def calcBounds(self): - boundsPen = BoundsPen(None) + def calcBounds(self, glyphSet): + boundsPen = BoundsPen(glyphSet) self.draw(boundsPen) return boundsPen.bounds @@ -1006,8 +1010,7 @@ while i < end: token = program[i] i = i + 1 - tp = type(token) - if issubclass(tp, basestring): + if isinstance(token, basestring): try: bytecode.extend(bytechr(b) for b in opcodes[token]) except KeyError: @@ -1015,12 +1018,12 @@ if token in ('hintmask', 'cntrmask'): bytecode.append(program[i]) # hint mask i = i + 1 - elif tp == int: + elif isinstance(token, int): bytecode.append(encodeInt(token)) - elif tp == float: + elif isinstance(token, float): bytecode.append(encodeFixed(token)) else: - assert 0, "unsupported type: %s" % tp + assert 0, "unsupported type: %s" % type(token) try: bytecode = bytesjoin(bytecode) except TypeError: @@ -1259,12 +1262,12 @@ """ There may be non-blend args at the top of the stack. We first calculate where the blend args start in the stack. These are the last - numMasters*numBlends) +1 args. + numMasters*numBlends) +1 args. The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by numBlends list of values. Each of value in one of these lists is the Variable Font delta for the matching region. - - We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by + + We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by the delta values. We then convert the default values, the first item in each entry, to an absolute value. """ vsindex = self.dict.get('vsindex', 0) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/psLib.py fonttools-3.29.0/Lib/fontTools/misc/psLib.py --- fonttools-3.21.2/Lib/fontTools/misc/psLib.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/psLib.py 2018-07-26 14:12:55.000000000 +0000 @@ -43,13 +43,14 @@ class PSTokenizer(object): - def __init__(self, buf=b''): + def __init__(self, buf=b'', encoding="ascii"): # Force self.buf to be a byte string buf = tobytes(buf) self.buf = buf self.len = len(buf) self.pos = 0 self.closed = False + self.encoding = encoding def read(self, n=-1): """Read at most 'n' bytes from the buffer, or less if the read @@ -122,7 +123,7 @@ _, nextpos = m.span() token = buf[pos:nextpos] self.pos = pos + len(token) - token = tostr(token, encoding='ascii') + token = tostr(token, encoding=self.encoding) return tokentype, token def skipwhite(self, whitematch=skipwhiteRE.match): @@ -145,9 +146,10 @@ class PSInterpreter(PSOperators): - def __init__(self): + def __init__(self, encoding="ascii"): systemdict = {} userdict = {} + self.encoding = encoding self.dictstack = [systemdict, userdict] self.stack = [] self.proclevel = 0 @@ -174,7 +176,7 @@ self.suckoperators(systemdict, baseclass) def interpret(self, data, getattr=getattr): - tokenizer = self.tokenizer = PSTokenizer(data) + tokenizer = self.tokenizer = PSTokenizer(data, self.encoding) getnexttoken = tokenizer.getnexttoken do_token = self.do_token handle_object = self.handle_object @@ -345,13 +347,13 @@ newitem = item.value return newitem -def suckfont(data): +def suckfont(data, encoding="ascii"): m = re.search(br"/FontName\s+/([^ \t\n\r]+)\s+def", data) if m: fontName = m.group(1) else: fontName = None - interpreter = PSInterpreter() + interpreter = PSInterpreter(encoding=encoding) interpreter.interpret(b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop") interpreter.interpret(data) fontdir = interpreter.dictstack[0]['FontDirectory'].value diff -Nru fonttools-3.21.2/Lib/fontTools/misc/py23.py fonttools-3.29.0/Lib/fontTools/misc/py23.py --- fonttools-3.21.2/Lib/fontTools/misc/py23.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/py23.py 2018-07-26 14:12:55.000000000 +0000 @@ -6,7 +6,8 @@ __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', - 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error'] + 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error', + 'SimpleNamespace', 'zip'] class Py23Error(NotImplementedError): @@ -147,7 +148,7 @@ @staticmethod def transcode(blob): - if not isinstance(blob, str): + if isinstance(blob, bytes): blob = blob.decode('latin-1') return blob @@ -249,7 +250,7 @@ file, mode, buffering, encoding, errors, newline, closefd) -# always use iterator for 'range' on both py 2 and 3 +# always use iterator for 'range' and 'zip' on both py 2 and 3 try: range = xrange except NameError: @@ -258,6 +259,11 @@ def xrange(*args, **kwargs): raise Py23Error("'xrange' is not defined. Use 'range' instead.") +try: + from itertools import izip as zip +except ImportError: + zip = zip + import math as _math @@ -413,70 +419,6 @@ round = round3 -import logging - - -class _Logger(logging.Logger): - """ Add support for 'lastResort' handler introduced in Python 3.2. """ - - def callHandlers(self, record): - # this is the same as Python 3.5's logging.Logger.callHandlers - c = self - found = 0 - while c: - for hdlr in c.handlers: - found = found + 1 - if record.levelno >= hdlr.level: - hdlr.handle(record) - if not c.propagate: - c = None # break out - else: - c = c.parent - if (found == 0): - if logging.lastResort: - if record.levelno >= logging.lastResort.level: - logging.lastResort.handle(record) - elif logging.raiseExceptions and not self.manager.emittedNoHandlerWarning: - sys.stderr.write("No handlers could be found for logger" - " \"%s\"\n" % self.name) - self.manager.emittedNoHandlerWarning = True - - -class _StderrHandler(logging.StreamHandler): - """ This class is like a StreamHandler using sys.stderr, but always uses - whatever sys.stderr is currently set to rather than the value of - sys.stderr at handler construction time. - """ - def __init__(self, level=logging.NOTSET): - """ - Initialize the handler. - """ - logging.Handler.__init__(self, level) - - @property - def stream(self): - # the try/execept avoids failures during interpreter shutdown, when - # globals are set to None - try: - return sys.stderr - except AttributeError: - return __import__('sys').stderr - - -if not hasattr(logging, 'lastResort'): - # for Python pre-3.2, we need to define the "last resort" handler used when - # clients don't explicitly configure logging (in Python 3.2 and above this is - # already defined). The handler prints the bare message to sys.stderr, only - # for events of severity WARNING or greater. - # To obtain the pre-3.2 behaviour, you can set logging.lastResort to None. - # https://docs.python.org/3.5/howto/logging.html#what-happens-if-no-configuration-is-provided - logging.lastResort = _StderrHandler(logging.WARNING) - # Also, we need to set the Logger class to one which supports the last resort - # handler. All new loggers instantiated after this call will use the custom - # logger class (the already existing ones, like the 'root' logger, will not) - logging.setLoggerClass(_Logger) - - try: from types import SimpleNamespace except ImportError: diff -Nru fonttools-3.21.2/Lib/fontTools/misc/testTools.py fonttools-3.29.0/Lib/fontTools/misc/testTools.py --- fonttools-3.21.2/Lib/fontTools/misc/testTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/testTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,8 +1,13 @@ """Helpers for writing unit tests.""" -from __future__ import print_function, division, absolute_import -from __future__ import unicode_literals +from __future__ import (print_function, division, absolute_import, + unicode_literals) import collections +import os +import shutil +import sys +import tempfile +from unittest import TestCase as _TestCase from fontTools.misc.py23 import * from fontTools.misc.xmlWriter import XMLWriter @@ -37,7 +42,7 @@ class FakeFont: def __init__(self, glyphs): self.glyphOrder_ = glyphs - self.reverseGlyphOrderDict_ = {g:i for i,g in enumerate(glyphs)} + self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)} self.lazy = False self.tables = {} @@ -114,29 +119,65 @@ class MockFont(object): - """A font-like object that automatically adds any looked up glyphname - to its glyphOrder.""" + """A font-like object that automatically adds any looked up glyphname + to its glyphOrder.""" - def __init__(self): - self._glyphOrder = ['.notdef'] - class AllocatingDict(dict): - def __missing__(reverseDict, key): - self._glyphOrder.append(key) - gid = len(reverseDict) - reverseDict[key] = gid - return gid - self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) - self.lazy = False - - def getGlyphID(self, glyph, requireReal=None): - gid = self._reverseGlyphOrder[glyph] - return gid + def __init__(self): + self._glyphOrder = ['.notdef'] + + class AllocatingDict(dict): + def __missing__(reverseDict, key): + self._glyphOrder.append(key) + gid = len(reverseDict) + reverseDict[key] = gid + return gid + self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) + self.lazy = False + + def getGlyphID(self, glyph, requireReal=None): + gid = self._reverseGlyphOrder[glyph] + return gid + + def getReverseGlyphMap(self): + return self._reverseGlyphOrder + + def getGlyphName(self, gid): + return self._glyphOrder[gid] + + def getGlyphOrder(self): + return self._glyphOrder + + +class TestCase(_TestCase): + + def __init__(self, methodName): + _TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + +class DataFilesHandler(TestCase): + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) - def getReverseGlyphMap(self): - return self._reverseGlyphOrder + def getpath(self, testfile): + folder = os.path.dirname(sys.modules[self.__module__].__file__) + return os.path.join(folder, "data", testfile) - def getGlyphName(self, gid): - return self._glyphOrder[gid] + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() - def getGlyphOrder(self): - return self._glyphOrder + def temp_font(self, font_path, file_name): + self.temp_dir() + temppath = os.path.join(self.tempdir, file_name) + shutil.copy2(font_path, temppath) + return temppath diff -Nru fonttools-3.21.2/Lib/fontTools/misc/textTools.py fonttools-3.29.0/Lib/fontTools/misc/textTools.py --- fonttools-3.21.2/Lib/fontTools/misc/textTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/textTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,18 +3,19 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +import ast import string -def safeEval(data, eval=eval): - """A (kindof) safe replacement for eval.""" - return eval(data, {"__builtins__":{"True":True,"False":False}}) +# alias kept for backward compatibility +safeEval = ast.literal_eval def readHex(content): """Convert a list of hex strings to binary data.""" return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, basestring))) + def deHexStr(hexdata): """Convert a hex string to binary data.""" hexdata = strjoin(hexdata.split()) diff -Nru fonttools-3.21.2/Lib/fontTools/misc/xmlReader.py fonttools-3.29.0/Lib/fontTools/misc/xmlReader.py --- fonttools-3.21.2/Lib/fontTools/misc/xmlReader.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/xmlReader.py 2018-07-26 14:12:55.000000000 +0000 @@ -17,7 +17,7 @@ class XMLReader(object): - def __init__(self, fileOrPath, ttFont, progress=None, quiet=None): + def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): if fileOrPath == '-': fileOrPath = sys.stdin if not hasattr(fileOrPath, "read"): @@ -35,6 +35,7 @@ self.quiet = quiet self.root = None self.contentStack = [] + self.contentOnly = contentOnly self.stackSize = 0 def read(self, rootless=False): @@ -73,8 +74,24 @@ parser.Parse(chunk, 0) def _startElementHandler(self, name, attrs): + if self.stackSize == 1 and self.contentOnly: + # We already know the table we're parsing, skip + # parsing the table tag and continue to + # stack '2' which begins parsing content + self.contentStack.append([]) + self.stackSize = 2 + return stackSize = self.stackSize self.stackSize = stackSize + 1 + subFile = attrs.get("src") + if subFile is not None: + if hasattr(self.file, 'name'): + # if file has a name, get its parent directory + dirname = os.path.dirname(self.file.name) + else: + # else fall back to using the current working directory + dirname = os.getcwd() + subFile = os.path.join(dirname, subFile) if not stackSize: if name != "ttFont": raise TTXParseError("illegal root tag: %s" % name) @@ -85,15 +102,7 @@ self.ttFont.sfntVersion = sfntVersion self.contentStack.append([]) elif stackSize == 1: - subFile = attrs.get("src") if subFile is not None: - if hasattr(self.file, 'name'): - # if file has a name, get its parent directory - dirname = os.path.dirname(self.file.name) - else: - # else fall back to using the current working directory - dirname = os.getcwd() - subFile = os.path.join(dirname, subFile) subReader = XMLReader(subFile, self.ttFont, self.progress) subReader.read() self.contentStack.append([]) @@ -119,6 +128,11 @@ self.currentTable = tableClass(tag) self.ttFont[tag] = self.currentTable self.contentStack.append([]) + elif stackSize == 2 and subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) + subReader.read() + self.contentStack.append([]) + self.root = subReader.root elif stackSize == 2: self.contentStack.append([]) self.root = (name, attrs, self.contentStack[-1]) @@ -134,12 +148,13 @@ def _endElementHandler(self, name): self.stackSize = self.stackSize - 1 del self.contentStack[-1] - if self.stackSize == 1: - self.root = None - elif self.stackSize == 2: - name, attrs, content = self.root - self.currentTable.fromXML(name, attrs, content, self.ttFont) - self.root = None + if not self.contentOnly: + if self.stackSize == 1: + self.root = None + elif self.stackSize == 2: + name, attrs, content = self.root + self.currentTable.fromXML(name, attrs, content, self.ttFont) + self.root = None class ProgressPrinter(object): diff -Nru fonttools-3.21.2/Lib/fontTools/misc/xmlWriter.py fonttools-3.29.0/Lib/fontTools/misc/xmlWriter.py --- fonttools-3.21.2/Lib/fontTools/misc/xmlWriter.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/misc/xmlWriter.py 2018-07-26 14:12:55.000000000 +0000 @@ -18,10 +18,14 @@ if fileOrPath == '-': fileOrPath = sys.stdout if not hasattr(fileOrPath, "write"): + self.filename = fileOrPath self.file = open(fileOrPath, "wb") + self._closeStream = True else: + self.filename = None # assume writable file object self.file = fileOrPath + self._closeStream = False # Figure out if writer expects bytes or unicodes try: @@ -46,8 +50,15 @@ self._writeraw('') self.newline() + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.close() + def close(self): - self.file.close() + if self._closeStream: + self.file.close() def write(self, string, indent=True): """Writes text.""" diff -Nru fonttools-3.21.2/Lib/fontTools/pens/perimeterPen.py fonttools-3.29.0/Lib/fontTools/pens/perimeterPen.py --- fonttools-3.21.2/Lib/fontTools/pens/perimeterPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/pens/perimeterPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.pens.basePen import BasePen -from fontTools.misc.bezierTools import splitQuadraticAtT, splitCubicAtT, approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC +from fontTools.misc.bezierTools import approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC, calcCubicArcLengthC import math @@ -14,18 +14,12 @@ def _distance(p0, p1): return math.hypot(p0[0] - p1[0], p0[1] - p1[1]) -def _split_cubic_into_two(p0, p1, p2, p3): - mid = (p0 + 3 * (p1 + p2) + p3) * .125 - deriv3 = (p3 + p2 - p1 - p0) * .125 - return ((p0, (p0 + p1) * .5, mid - deriv3, mid), - (mid, mid + deriv3, (p2 + p3) * .5, p3)) - class PerimeterPen(BasePen): def __init__(self, glyphset=None, tolerance=0.005): BasePen.__init__(self, glyphset) self.value = 0 - self._mult = 1.+1.5*tolerance # The 1.5 is a empirical hack; no math + self.tolerance = tolerance # Choose which algorithm to use for quadratic and for cubic. # Quadrature is faster but has fixed error characteristic with no strong @@ -55,15 +49,8 @@ p0 = self._getCurrentPoint() self._addQuadratic(complex(*p0), complex(*p1), complex(*p2)) - def _addCubicRecursive(self, p0, p1, p2, p3): - arch = abs(p0-p3) - box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3) - if arch * self._mult >= box: - self.value += (arch + box) * .5 - else: - one,two = _split_cubic_into_two(p0,p1,p2,p3) - self._addCubicRecursive(*one) - self._addCubicRecursive(*two) + def _addCubicRecursive(self, c0, c1, c2, c3): + self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance) def _addCubicQuadrature(self, c0, c1, c2, c3): self.value += approximateCubicArcLengthC(c0, c1, c2, c3) diff -Nru fonttools-3.21.2/Lib/fontTools/pens/t2CharStringPen.py fonttools-3.29.0/Lib/fontTools/pens/t2CharStringPen.py --- fonttools-3.21.2/Lib/fontTools/pens/t2CharStringPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/pens/t2CharStringPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,6 +3,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc.psCharStrings import T2CharString from fontTools.pens.basePen import BasePen from fontTools.cffLib.specializer import specializeCommands, commandsToProgram @@ -15,7 +16,7 @@ def _round(number): if tolerance == 0: return number # no-op - rounded = round(number) + rounded = otRound(number) # return rounded integer if the tolerance >= 0.5, or if the absolute # difference between the original float and the rounded integer is # within the tolerance @@ -82,7 +83,7 @@ program = commandsToProgram(commands) if self._width is not None: assert not self._CFF2, "CFF2 does not allow encoding glyph width in CharString." - program.insert(0, round(self._width)) + program.insert(0, otRound(self._width)) if not self._CFF2: program.append('endchar') charString = T2CharString( diff -Nru fonttools-3.21.2/Lib/fontTools/pens/ttGlyphPen.py fonttools-3.29.0/Lib/fontTools/pens/ttGlyphPen.py --- fonttools-3.21.2/Lib/fontTools/pens/ttGlyphPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/pens/ttGlyphPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from array import array -from fontTools.pens.basePen import AbstractPen +from fontTools.pens.basePen import LoggingPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib.tables import ttProgram from fontTools.ttLib.tables._g_l_y_f import Glyph @@ -12,11 +12,32 @@ __all__ = ["TTGlyphPen"] -class TTGlyphPen(AbstractPen): - """Pen used for drawing to a TrueType glyph.""" +# the max value that can still fit in an F2Dot14: +# 1.99993896484375 +MAX_F2DOT14 = 0x7FFF / (1 << 14) + + +class TTGlyphPen(LoggingPen): + """Pen used for drawing to a TrueType glyph. + + If `handleOverflowingTransforms` is True, the components' transform values + are checked that they don't overflow the limits of a F2Dot14 number: + -2.0 <= v < +2.0. If any transform value exceeds these, the composite + glyph is decomposed. + An exception to this rule is done for values that are very close to +2.0 + (both for consistency with the -2.0 case, and for the relative frequency + these occur in real fonts). When almost +2.0 values occur (and all other + values are within the range -2.0 <= x <= +2.0), they are clamped to the + maximum positive value that can still be encoded as an F2Dot14: i.e. + 1.99993896484375. + If False, no check is done and all components are translated unmodified + into the glyf table, followed by an inevitable `struct.error` once an + attempt is made to compile them. + """ - def __init__(self, glyphSet): + def __init__(self, glyphSet, handleOverflowingTransforms=True): self.glyphSet = glyphSet + self.handleOverflowingTransforms = handleOverflowingTransforms self.init() def init(self): @@ -79,24 +100,46 @@ def addComponent(self, glyphName, transformation): self.components.append((glyphName, transformation)) - def glyph(self, componentFlags=0x4): - assert self._isClosed(), "Didn't close last contour." - + def _buildComponents(self, componentFlags): + if self.handleOverflowingTransforms: + # we can't encode transform values > 2 or < -2 in F2Dot14, + # so we must decompose the glyph if any transform exceeds these + overflowing = any(s > 2 or s < -2 + for (glyphName, transformation) in self.components + for s in transformation[:4]) components = [] for glyphName, transformation in self.components: - if self.points: - # can't have both, so decompose the glyph + if glyphName not in self.glyphSet: + self.log.warning( + "skipped non-existing component '%s'", glyphName + ) + continue + if (self.points or + (self.handleOverflowingTransforms and overflowing)): + # can't have both coordinates and components, so decompose tpen = TransformPen(self, transformation) self.glyphSet[glyphName].draw(tpen) continue component = GlyphComponent() component.glyphName = glyphName - if transformation[:4] != (1, 0, 0, 1): - component.transform = (transformation[:2], transformation[2:4]) component.x, component.y = transformation[4:] + transformation = transformation[:4] + if transformation != (1, 0, 0, 1): + if (self.handleOverflowingTransforms and + any(MAX_F2DOT14 < s <= 2 for s in transformation)): + # clamp values ~= +2.0 so we can keep the component + transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 + else s for s in transformation) + component.transform = (transformation[:2], transformation[2:]) component.flags = componentFlags components.append(component) + return components + + def glyph(self, componentFlags=0x4): + assert self._isClosed(), "Didn't close last contour." + + components = self._buildComponents(componentFlags) glyph = Glyph() glyph.coordinates = GlyphCoordinates(self.points) diff -Nru fonttools-3.21.2/Lib/fontTools/subset/__init__.py fonttools-3.29.0/Lib/fontTools/subset/__init__.py --- fonttools-3.21.2/Lib/fontTools/subset/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/subset/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,11 +4,13 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools import ttLib from fontTools.ttLib.tables import otTables from fontTools.misc import psCharStrings from fontTools.pens.basePen import NullPen from fontTools.misc.loggingTools import Timer +from fontTools.varLib import varStore import sys import struct import array @@ -100,7 +102,7 @@ $ pyftsubset --glyph-names? Current setting for 'glyph-names' is: False $ ./pyftsubset --name-IDs=? - Current setting for 'name-IDs' is: [1, 2] + Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] $ ./pyftsubset --hinting? --no-hinting --hinting? Current setting for 'hinting' is: True Current setting for 'hinting' is: False @@ -211,8 +213,8 @@ Add to the set of tables that will not be subsetted. By default, the following tables are included in this list, as they do not need subsetting (ignore the fact that 'loca' is listed - here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', - 'name', 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'STAT'. + here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', + 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'. By default, tables that the tool does not know how to subset and are not specified here will be dropped from the font, unless --passthrough-tables option is passed. @@ -242,11 +244,11 @@ codes, see: http://www.microsoft.com/typography/otspec/name.htm --name-IDs[+|-]=[,...] Specify (=), add to (+=) or exclude from (-=) the set of 'name' table - entry nameIDs that will be preserved. By default only nameID 1 (Family) - and nameID 2 (Style) are preserved. Use '*' to keep all entries. + entry nameIDs that will be preserved. By default, only nameIDs between 0 + and 6 are preserved, the rest are dropped. Use '*' to keep all entries. Examples: - --name-IDs+=0,4,6 - * Also keep Copyright, Full name and PostScript name entry. + --name-IDs+=7,8,9 + * Also keep Trademark, Manufacturer and Designer name entries. --name-IDs='' * Drop all 'name' table entries. --name-IDs='*' @@ -310,6 +312,8 @@ Update the 'OS/2 xAvgCharWidth' field after subsetting. --no-recalc-average-width Don't change the 'OS/2 xAvgCharWidth' field. [default] + --font-number= + Select font number for TrueType Collection (.ttc/.otc), starting from 0. Application options: --verbose @@ -333,10 +337,10 @@ log = logging.getLogger("fontTools.subset") def _log_glyphs(self, glyphs, font=None): - self.info("Glyph names: %s", sorted(glyphs)) - if font: - reverseGlyphMap = font.getReverseGlyphMap() - self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) + self.info("Glyph names: %s", sorted(glyphs)) + if font: + reverseGlyphMap = font.getReverseGlyphMap() + self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) # bind "glyphs" function to 'log' object log.glyphs = MethodType(_log_glyphs, log) @@ -347,2203 +351,2349 @@ def _add_method(*clazzes): - """Returns a decorator function that adds a new method to one or - more classes.""" - def wrapper(method): - done = [] - for clazz in clazzes: - if clazz in done: continue # Support multiple names of a clazz - done.append(clazz) - assert clazz.__name__ != 'DefaultTable', \ - 'Oops, table class not found.' - assert not hasattr(clazz, method.__name__), \ - "Oops, class '%s' has method '%s'." % (clazz.__name__, - method.__name__) - setattr(clazz, method.__name__, method) - return None - return wrapper + """Returns a decorator function that adds a new method to one or + more classes.""" + def wrapper(method): + done = [] + for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) + assert clazz.__name__ != 'DefaultTable', \ + 'Oops, table class not found.' + assert not hasattr(clazz, method.__name__), \ + "Oops, class '%s' has method '%s'." % (clazz.__name__, + method.__name__) + setattr(clazz, method.__name__, method) + return None + return wrapper def _uniq_sort(l): - return sorted(set(l)) + return sorted(set(l)) def _set_update(s, *others): - # Jython's set.update only takes one other argument. - # Emulate real set.update... - for other in others: - s.update(other) + # Jython's set.update only takes one other argument. + # Emulate real set.update... + for other in others: + s.update(other) def _dict_subset(d, glyphs): - return {g:d[g] for g in glyphs} + return {g:d[g] for g in glyphs} @_add_method(otTables.Coverage) def intersect(self, glyphs): - """Returns ascending list of matching coverage values.""" - return [i for i,g in enumerate(self.glyphs) if g in glyphs] + """Returns ascending list of matching coverage values.""" + return [i for i,g in enumerate(self.glyphs) if g in glyphs] @_add_method(otTables.Coverage) def intersect_glyphs(self, glyphs): - """Returns set of intersecting glyphs.""" - return set(g for g in self.glyphs if g in glyphs) + """Returns set of intersecting glyphs.""" + return set(g for g in self.glyphs if g in glyphs) @_add_method(otTables.Coverage) def subset(self, glyphs): - """Returns ascending list of remaining coverage values.""" - indices = self.intersect(glyphs) - self.glyphs = [g for g in self.glyphs if g in glyphs] - return indices + """Returns ascending list of remaining coverage values.""" + indices = self.intersect(glyphs) + self.glyphs = [g for g in self.glyphs if g in glyphs] + return indices @_add_method(otTables.Coverage) def remap(self, coverage_map): - """Remaps coverage.""" - self.glyphs = [self.glyphs[i] for i in coverage_map] + """Remaps coverage.""" + self.glyphs = [self.glyphs[i] for i in coverage_map] @_add_method(otTables.ClassDef) def intersect(self, glyphs): - """Returns ascending list of matching class values.""" - return _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - [v for g,v in self.classDefs.items() if g in glyphs]) + """Returns ascending list of matching class values.""" + return _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + [v for g,v in self.classDefs.items() if g in glyphs]) @_add_method(otTables.ClassDef) def intersect_class(self, glyphs, klass): - """Returns set of glyphs matching class.""" - if klass == 0: - return set(g for g in glyphs if g not in self.classDefs) - return set(g for g,v in self.classDefs.items() - if v == klass and g in glyphs) + """Returns set of glyphs matching class.""" + if klass == 0: + return set(g for g in glyphs if g not in self.classDefs) + return set(g for g,v in self.classDefs.items() + if v == klass and g in glyphs) @_add_method(otTables.ClassDef) def subset(self, glyphs, remap=False): - """Returns ascending list of remaining classes.""" - self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs} - # Note: while class 0 has the special meaning of "not matched", - # if no glyph will ever /not match/, we can optimize class 0 out too. - indices = _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - list(self.classDefs.values())) - if remap: - self.remap(indices) - return indices + """Returns ascending list of remaining classes.""" + self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs} + # Note: while class 0 has the special meaning of "not matched", + # if no glyph will ever /not match/, we can optimize class 0 out too. + indices = _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + list(self.classDefs.values())) + if remap: + self.remap(indices) + return indices @_add_method(otTables.ClassDef) def remap(self, class_map): - """Remaps classes.""" - self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()} + """Remaps classes.""" + self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()} @_add_method(otTables.SingleSubst) def closure_glyphs(self, s, cur_glyphs): - s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) + s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) @_add_method(otTables.SingleSubst) def subset_glyphs(self, s): - self.mapping = {g:v for g,v in self.mapping.items() - if g in s.glyphs and v in s.glyphs} - return bool(self.mapping) + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and v in s.glyphs} + return bool(self.mapping) @_add_method(otTables.MultipleSubst) def closure_glyphs(self, s, cur_glyphs): - for glyph, subst in self.mapping.items(): - if glyph in cur_glyphs: - _set_update(s.glyphs, subst) + for glyph, subst in self.mapping.items(): + if glyph in cur_glyphs: + _set_update(s.glyphs, subst) @_add_method(otTables.MultipleSubst) def subset_glyphs(self, s): - self.mapping = {g:v for g,v in self.mapping.items() - if g in s.glyphs and all(sub in s.glyphs for sub in v)} - return bool(self.mapping) + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and all(sub in s.glyphs for sub in v)} + return bool(self.mapping) @_add_method(otTables.AlternateSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() - if g in cur_glyphs)) + _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() + if g in cur_glyphs)) @_add_method(otTables.AlternateSubst) def subset_glyphs(self, s): - self.alternates = {g:vlist - for g,vlist in self.alternates.items() - if g in s.glyphs and - all(v in s.glyphs for v in vlist)} - return bool(self.alternates) + self.alternates = {g:vlist + for g,vlist in self.alternates.items() + if g in s.glyphs and + all(v in s.glyphs for v in vlist)} + return bool(self.alternates) @_add_method(otTables.LigatureSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs - if all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items() - if g in cur_glyphs)) + _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs + if all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items() + if g in cur_glyphs)) @_add_method(otTables.LigatureSubst) def subset_glyphs(self, s): - self.ligatures = {g:v for g,v in self.ligatures.items() - if g in s.glyphs} - self.ligatures = {g:[seq for seq in seqs - if seq.LigGlyph in s.glyphs and - all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items()} - self.ligatures = {g:v for g,v in self.ligatures.items() if v} - return bool(self.ligatures) + self.ligatures = {g:v for g,v in self.ligatures.items() + if g in s.glyphs} + self.ligatures = {g:[seq for seq in seqs + if seq.LigGlyph in s.glyphs and + all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items()} + self.ligatures = {g:v for g,v in self.ligatures.items() if v} + return bool(self.ligatures) @_add_method(otTables.ReverseChainSingleSubst) def closure_glyphs(self, s, cur_glyphs): - if self.Format == 1: - indices = self.Coverage.intersect(cur_glyphs) - if(not indices or - not all(c.intersect(s.glyphs) - for c in self.LookAheadCoverage + self.BacktrackCoverage)): - return - s.glyphs.update(self.Substitute[i] for i in indices) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.intersect(cur_glyphs) + if(not indices or + not all(c.intersect(s.glyphs) + for c in self.LookAheadCoverage + self.BacktrackCoverage)): + return + s.glyphs.update(self.Substitute[i] for i in indices) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ReverseChainSingleSubst) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - self.Substitute = [self.Substitute[i] for i in indices] - # Now drop rules generating glyphs we don't want - indices = [i for i,sub in enumerate(self.Substitute) - if sub in s.glyphs] - self.Substitute = [self.Substitute[i] for i in indices] - self.Coverage.remap(indices) - self.GlyphCount = len(self.Substitute) - return bool(self.GlyphCount and - all(c.subset(s.glyphs) - for c in self.LookAheadCoverage+self.BacktrackCoverage)) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + self.Substitute = [self.Substitute[i] for i in indices] + # Now drop rules generating glyphs we don't want + indices = [i for i,sub in enumerate(self.Substitute) + if sub in s.glyphs] + self.Substitute = [self.Substitute[i] for i in indices] + self.Coverage.remap(indices) + self.GlyphCount = len(self.Substitute) + return bool(self.GlyphCount and + all(c.subset(s.glyphs) + for c in self.LookAheadCoverage+self.BacktrackCoverage)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.SinglePos) def subset_glyphs(self, s): - if self.Format == 1: - return len(self.Coverage.subset(s.glyphs)) - elif self.Format == 2: - indices = self.Coverage.subset(s.glyphs) - values = self.Value - count = len(values) - self.Value = [values[i] for i in indices if i < count] - self.ValueCount = len(self.Value) - return bool(self.ValueCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return len(self.Coverage.subset(s.glyphs)) + elif self.Format == 2: + indices = self.Coverage.subset(s.glyphs) + values = self.Value + count = len(values) + self.Value = [values[i] for i in indices if i < count] + self.ValueCount = len(self.Value) + return bool(self.ValueCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.SinglePos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat &= ~0x00F0 - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat &= ~0x00F0 + return True @_add_method(otTables.PairPos) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - pairs = self.PairSet - count = len(pairs) - self.PairSet = [pairs[i] for i in indices if i < count] - for p in self.PairSet: - p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs] - p.PairValueCount = len(p.PairValueRecord) - # Remove empty pairsets - indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] - self.Coverage.remap(indices) - self.PairSet = [self.PairSet[i] for i in indices] - self.PairSetCount = len(self.PairSet) - return bool(self.PairSetCount) - elif self.Format == 2: - class1_map = [c for c in self.ClassDef1.subset(s.glyphs, remap=True) if c < self.Class1Count] - class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True) if c < self.Class2Count] - self.Class1Record = [self.Class1Record[i] for i in class1_map] - for c in self.Class1Record: - c.Class2Record = [c.Class2Record[i] for i in class2_map] - self.Class1Count = len(class1_map) - self.Class2Count = len(class2_map) - return bool(self.Class1Count and - self.Class2Count and - self.Coverage.subset(s.glyphs)) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + pairs = self.PairSet + count = len(pairs) + self.PairSet = [pairs[i] for i in indices if i < count] + for p in self.PairSet: + p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs] + p.PairValueCount = len(p.PairValueRecord) + # Remove empty pairsets + indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] + self.Coverage.remap(indices) + self.PairSet = [self.PairSet[i] for i in indices] + self.PairSetCount = len(self.PairSet) + return bool(self.PairSetCount) + elif self.Format == 2: + class1_map = [c for c in self.ClassDef1.subset(s.glyphs, remap=True) if c < self.Class1Count] + class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True) if c < self.Class2Count] + self.Class1Record = [self.Class1Record[i] for i in class1_map] + for c in self.Class1Record: + c.Class2Record = [c.Class2Record[i] for i in class2_map] + self.Class1Count = len(class1_map) + self.Class2Count = len(class2_map) + return bool(self.Class1Count and + self.Class2Count and + self.Coverage.subset(s.glyphs)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.PairPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat1 &= ~0x00F0 - self.ValueFormat2 &= ~0x00F0 - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat1 &= ~0x00F0 + self.ValueFormat2 &= ~0x00F0 + return True @_add_method(otTables.CursivePos) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - records = self.EntryExitRecord - count = len(records) - self.EntryExitRecord = [records[i] for i in indices if i < count] - self.EntryExitCount = len(self.EntryExitRecord) - return bool(self.EntryExitCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + records = self.EntryExitRecord + count = len(records) + self.EntryExitRecord = [records[i] for i in indices if i < count] + self.EntryExitCount = len(self.EntryExitRecord) + return bool(self.EntryExitCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.Anchor) def prune_hints(self): - # Drop device tables / contour anchor point - self.ensureDecompiled() - self.Format = 1 + # Drop device tables / contour anchor point + self.ensureDecompiled() + self.Format = 1 @_add_method(otTables.CursivePos) -def prune_post_subset(self, options): - if not options.hinting: - for rec in self.EntryExitRecord: - if rec.EntryAnchor: rec.EntryAnchor.prune_hints() - if rec.ExitAnchor: rec.ExitAnchor.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for rec in self.EntryExitRecord: + if rec.EntryAnchor: rec.EntryAnchor.prune_hints() + if rec.ExitAnchor: rec.ExitAnchor.prune_hints() + return True @_add_method(otTables.MarkBasePos) def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - base_indices = self.BaseCoverage.subset(s.glyphs) - self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] for i in base_indices] - self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.BaseArray.BaseRecord: - b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.BaseArray.BaseCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + base_indices = self.BaseCoverage.subset(s.glyphs) + self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] for i in base_indices] + self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.BaseArray.BaseRecord: + b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.BaseArray.BaseCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkBasePos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.BaseArray.BaseRecord: - for a in b.BaseAnchor: - if a: - a.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.BaseArray.BaseRecord: + for a in b.BaseAnchor: + if a: + a.prune_hints() + return True @_add_method(otTables.MarkLigPos) def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - ligature_indices = self.LigatureCoverage.subset(s.glyphs) - self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] for i in ligature_indices] - self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.LigatureArray.LigatureCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + ligature_indices = self.LigatureCoverage.subset(s.glyphs) + self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] for i in ligature_indices] + self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.LigatureArray.LigatureCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkLigPos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - for a in c.LigatureAnchor: - if a: - a.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + for a in c.LigatureAnchor: + if a: + a.prune_hints() + return True @_add_method(otTables.MarkMarkPos) def subset_glyphs(self, s): - if self.Format == 1: - mark1_indices = self.Mark1Coverage.subset(s.glyphs) - self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] for i in mark1_indices] - self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) - mark2_indices = self.Mark2Coverage.subset(s.glyphs) - self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] for i in mark2_indices] - self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.Mark1Array.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.Mark2Array.Mark2Record: - b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] - return bool(self.ClassCount and - self.Mark1Array.MarkCount and - self.Mark2Array.MarkCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark1_indices = self.Mark1Coverage.subset(s.glyphs) + self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] for i in mark1_indices] + self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) + mark2_indices = self.Mark2Coverage.subset(s.glyphs) + self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] for i in mark2_indices] + self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.Mark1Array.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.Mark2Array.Mark2Record: + b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] + return bool(self.ClassCount and + self.Mark1Array.MarkCount and + self.Mark2Array.MarkCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkMarkPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables or contour anchor point - for m in self.Mark1Array.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.Mark2Array.Mark2Record: - for m in b.Mark2Anchor: - if m: - m.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables or contour anchor point + for m in self.Mark1Array.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.Mark2Array.Mark2Record: + for m in b.Mark2Anchor: + if m: + m.prune_hints() + return True @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) def subset_lookups(self, lookup_indices): - pass + pass @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) def collect_lookups(self): - return [] + return [] @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def prune_post_subset(self, options): - return True + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def prune_post_subset(self, font, options): + return True @_add_method(otTables.SingleSubst, - otTables.AlternateSubst, - otTables.ReverseChainSingleSubst) + otTables.AlternateSubst, + otTables.ReverseChainSingleSubst) def may_have_non_1to1(self): - return False + return False @_add_method(otTables.MultipleSubst, - otTables.LigatureSubst, - otTables.ContextSubst, - otTables.ChainContextSubst) + otTables.LigatureSubst, + otTables.ContextSubst, + otTables.ChainContextSubst) def may_have_non_1to1(self): - return True + return True @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def __subset_classify_context(self): - class ContextHelper(object): - def __init__(self, klass, Format): - if klass.__name__.endswith('Subst'): - Typ = 'Sub' - Type = 'Subst' - else: - Typ = 'Pos' - Type = 'Pos' - if klass.__name__.startswith('Chain'): - Chain = 'Chain' - InputIdx = 1 - DataLen = 3 - else: - Chain = '' - InputIdx = 0 - DataLen = 1 - ChainTyp = Chain+Typ - - self.Typ = Typ - self.Type = Type - self.Chain = Chain - self.ChainTyp = ChainTyp - self.InputIdx = InputIdx - self.DataLen = DataLen - - self.LookupRecord = Type+'LookupRecord' - - if Format == 1: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(None,) - ChainContextData = lambda r:(None, None, None) - SetContextData = None - SetChainContextData = None - RuleData = lambda r:(r.Input,) - ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) - def SetRuleData(r, d): - (r.Input,) = d - (r.GlyphCount,) = (len(x)+1 for x in d) - def ChainSetRuleData(r, d): - (r.Backtrack, r.Input, r.LookAhead) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) - elif Format == 2: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(r.ClassDef,) - ChainContextData = lambda r:(r.BacktrackClassDef, - r.InputClassDef, - r.LookAheadClassDef) - def SetContextData(r, d): - (r.ClassDef,) = d - def SetChainContextData(r, d): - (r.BacktrackClassDef, - r.InputClassDef, - r.LookAheadClassDef) = d - RuleData = lambda r:(r.Class,) - ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) - def SetRuleData(r, d): - (r.Class,) = d - (r.GlyphCount,) = (len(x)+1 for x in d) - def ChainSetRuleData(r, d): - (r.Backtrack, r.Input, r.LookAhead) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) - elif Format == 3: - Coverage = lambda r: r.Coverage[0] - ChainCoverage = lambda r: r.InputCoverage[0] - ContextData = None - ChainContextData = None - SetContextData = None - SetChainContextData = None - RuleData = lambda r: r.Coverage - ChainRuleData = lambda r:(r.BacktrackCoverage + - r.InputCoverage + - r.LookAheadCoverage) - def SetRuleData(r, d): - (r.Coverage,) = d - (r.GlyphCount,) = (len(x) for x in d) - def ChainSetRuleData(r, d): - (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) - else: - assert 0, "unknown format: %s" % Format - - if Chain: - self.Coverage = ChainCoverage - self.ContextData = ChainContextData - self.SetContextData = SetChainContextData - self.RuleData = ChainRuleData - self.SetRuleData = ChainSetRuleData - else: - self.Coverage = Coverage - self.ContextData = ContextData - self.SetContextData = SetContextData - self.RuleData = RuleData - self.SetRuleData = SetRuleData - - if Format == 1: - self.Rule = ChainTyp+'Rule' - self.RuleCount = ChainTyp+'RuleCount' - self.RuleSet = ChainTyp+'RuleSet' - self.RuleSetCount = ChainTyp+'RuleSetCount' - self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] - elif Format == 2: - self.Rule = ChainTyp+'ClassRule' - self.RuleCount = ChainTyp+'ClassRuleCount' - self.RuleSet = ChainTyp+'ClassSet' - self.RuleSetCount = ChainTyp+'ClassSetCount' - self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c - else (set(glyphs) if r == 0 else set())) - - self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' - self.ClassDefIndex = 1 if Chain else 0 - self.Input = 'Input' if Chain else 'Class' - - if self.Format not in [1, 2, 3]: - return None # Don't shoot the messenger; let it go - if not hasattr(self.__class__, "__ContextHelpers"): - self.__class__.__ContextHelpers = {} - if self.Format not in self.__class__.__ContextHelpers: - helper = ContextHelper(self.__class__, self.Format) - self.__class__.__ContextHelpers[self.Format] = helper - return self.__class__.__ContextHelpers[self.Format] + class ContextHelper(object): + def __init__(self, klass, Format): + if klass.__name__.endswith('Subst'): + Typ = 'Sub' + Type = 'Subst' + else: + Typ = 'Pos' + Type = 'Pos' + if klass.__name__.startswith('Chain'): + Chain = 'Chain' + InputIdx = 1 + DataLen = 3 + else: + Chain = '' + InputIdx = 0 + DataLen = 1 + ChainTyp = Chain+Typ + + self.Typ = Typ + self.Type = Type + self.Chain = Chain + self.ChainTyp = ChainTyp + self.InputIdx = InputIdx + self.DataLen = DataLen + + self.LookupRecord = Type+'LookupRecord' + + if Format == 1: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(None,) + ChainContextData = lambda r:(None, None, None) + SetContextData = None + SetChainContextData = None + RuleData = lambda r:(r.Input,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Input,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 2: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(r.ClassDef,) + ChainContextData = lambda r:(r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) + def SetContextData(r, d): + (r.ClassDef,) = d + def SetChainContextData(r, d): + (r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) = d + RuleData = lambda r:(r.Class,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Class,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 3: + Coverage = lambda r: r.Coverage[0] + ChainCoverage = lambda r: r.InputCoverage[0] + ContextData = None + ChainContextData = None + SetContextData = None + SetChainContextData = None + RuleData = lambda r: r.Coverage + ChainRuleData = lambda r:(r.BacktrackCoverage + + r.InputCoverage + + r.LookAheadCoverage) + def SetRuleData(r, d): + (r.Coverage,) = d + (r.GlyphCount,) = (len(x) for x in d) + def ChainSetRuleData(r, d): + (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) + else: + assert 0, "unknown format: %s" % Format + + if Chain: + self.Coverage = ChainCoverage + self.ContextData = ChainContextData + self.SetContextData = SetChainContextData + self.RuleData = ChainRuleData + self.SetRuleData = ChainSetRuleData + else: + self.Coverage = Coverage + self.ContextData = ContextData + self.SetContextData = SetContextData + self.RuleData = RuleData + self.SetRuleData = SetRuleData + + if Format == 1: + self.Rule = ChainTyp+'Rule' + self.RuleCount = ChainTyp+'RuleCount' + self.RuleSet = ChainTyp+'RuleSet' + self.RuleSetCount = ChainTyp+'RuleSetCount' + self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] + elif Format == 2: + self.Rule = ChainTyp+'ClassRule' + self.RuleCount = ChainTyp+'ClassRuleCount' + self.RuleSet = ChainTyp+'ClassSet' + self.RuleSetCount = ChainTyp+'ClassSetCount' + self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c + else (set(glyphs) if r == 0 else set())) + + self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' + self.ClassDefIndex = 1 if Chain else 0 + self.Input = 'Input' if Chain else 'Class' + + if self.Format not in [1, 2, 3]: + return None # Don't shoot the messenger; let it go + if not hasattr(self.__class__, "__ContextHelpers"): + self.__class__.__ContextHelpers = {} + if self.Format not in self.__class__.__ContextHelpers: + helper = ContextHelper(self.__class__, self.Format) + self.__class__.__ContextHelpers[self.Format] = helper + return self.__class__.__ContextHelpers[self.Format] @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst) + otTables.ChainContextSubst) def closure_glyphs(self, s, cur_glyphs): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - indices = c.Coverage(self).intersect(cur_glyphs) - if not indices: - return [] - cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) - - if self.Format == 1: - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - continue - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) - else: - pos_glyphs = frozenset([r.Input[seqi - 1]]) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(r.Input)+2)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 2: - ClassDef = getattr(self, c.ClassDef) - indices = ClassDef.intersect(cur_glyphs) - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - continue - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i)) - else: - pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1])) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(getattr(r, c.Input))+2)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 3: - if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): - return [] - r = self - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset(cur_glyphs) - else: - pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs)) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(r.InputCoverage)+1)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - else: - assert 0, "unknown format: %s" % self.Format + indices = c.Coverage(self).intersect(cur_glyphs) + if not indices: + return [] + cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) + + if self.Format == 1: + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) + else: + pos_glyphs = frozenset([r.Input[seqi - 1]]) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.Input)+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 2: + ClassDef = getattr(self, c.ClassDef) + indices = ClassDef.intersect(cur_glyphs) + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i)) + else: + pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1])) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(getattr(r, c.Input))+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 3: + if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): + return [] + r = self + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(cur_glyphs) + else: + pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs)) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.InputCoverage)+1)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ContextPos, - otTables.ChainContextSubst, - otTables.ChainContextPos) -def subset_glyphs(self, s): - c = self.__subset_classify_context() - - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - rss = [rss[i] for i in indices if i < rssCount] - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(g in s.glyphs for g in glist) - for glist in c.RuleData(r))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - # Prune empty rulesets - indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)] - self.Coverage.remap(indices) - rss = [rss[i] for i in indices] - setattr(self, c.RuleSet, rss) - setattr(self, c.RuleSetCount, len(rss)) - return bool(rss) - elif self.Format == 2: - if not self.Coverage.subset(s.glyphs): - return False - ContextData = c.ContextData(self) - klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData] - - # Keep rulesets for class numbers that survived. - indices = klass_maps[c.ClassDefIndex] - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - rss = [rss[i] for i in indices if i < rssCount] - del rssCount - # Delete, but not renumber, unreachable rulesets. - indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) - rss = [rss if i in indices else None for i,rss in enumerate(rss)] - - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(k in klass_map for k in klist) - for klass_map,klist in zip(klass_maps, c.RuleData(r)))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - - # Remap rule classes - for r in ss: - c.SetRuleData(r, [[klass_map.index(k) for k in klist] - for klass_map,klist in zip(klass_maps, c.RuleData(r))]) - - # Prune empty rulesets - rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] - while rss and rss[-1] is None: - del rss[-1] - setattr(self, c.RuleSet, rss) - setattr(self, c.RuleSetCount, len(rss)) - - # TODO: We can do a second round of remapping class values based - # on classes that are actually used in at least one rule. Right - # now we subset classes to c.glyphs only. Or better, rewrite - # the above to do that. - - return bool(rss) - elif self.Format == 3: - return all(x.subset(s.glyphs) for x in c.RuleData(self)) - else: - assert 0, "unknown format: %s" % self.Format + otTables.ContextPos, + otTables.ChainContextSubst, + otTables.ChainContextPos) +def subset_glyphs(self, s): + c = self.__subset_classify_context() + + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(g in s.glyphs for g in glist) + for glist in c.RuleData(r))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + # Prune empty rulesets + indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)] + self.Coverage.remap(indices) + rss = [rss[i] for i in indices] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + return bool(rss) + elif self.Format == 2: + if not self.Coverage.subset(s.glyphs): + return False + ContextData = c.ContextData(self) + klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData] + + # Keep rulesets for class numbers that survived. + indices = klass_maps[c.ClassDefIndex] + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + del rssCount + # Delete, but not renumber, unreachable rulesets. + indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) + rss = [rss if i in indices else None for i,rss in enumerate(rss)] + + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(k in klass_map for k in klist) + for klass_map,klist in zip(klass_maps, c.RuleData(r)))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + + # Remap rule classes + for r in ss: + c.SetRuleData(r, [[klass_map.index(k) for k in klist] + for klass_map,klist in zip(klass_maps, c.RuleData(r))]) + + # Prune empty rulesets + rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] + while rss and rss[-1] is None: + del rss[-1] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + + # TODO: We can do a second round of remapping class values based + # on classes that are actually used in at least one rule. Right + # now we subset classes to c.glyphs only. Or better, rewrite + # the above to do that. + + return bool(rss) + elif self.Format == 3: + return all(x.subset(s.glyphs) for x in c.RuleData(self)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def subset_lookups(self, lookup_indices): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - if self.Format in [1, 2]: - for rs in getattr(self, c.RuleSet): - if not rs: continue - for r in getattr(rs, c.Rule): - if not r: continue - setattr(r, c.LookupRecord, - [ll for ll in getattr(r, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(r, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - elif self.Format == 3: - setattr(self, c.LookupRecord, - [ll for ll in getattr(self, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(self, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format in [1, 2]: + for rs in getattr(self, c.RuleSet): + if not rs: continue + for r in getattr(rs, c.Rule): + if not r: continue + setattr(r, c.LookupRecord, + [ll for ll in getattr(r, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(r, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + elif self.Format == 3: + setattr(self, c.LookupRecord, + [ll for ll in getattr(self, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(self, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def collect_lookups(self): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - if self.Format in [1, 2]: - return [ll.LookupListIndex - for rs in getattr(self, c.RuleSet) if rs - for r in getattr(rs, c.Rule) if r - for ll in getattr(r, c.LookupRecord) if ll] - elif self.Format == 3: - return [ll.LookupListIndex - for ll in getattr(self, c.LookupRecord) if ll] - else: - assert 0, "unknown format: %s" % self.Format + if self.Format in [1, 2]: + return [ll.LookupListIndex + for rs in getattr(self, c.RuleSet) if rs + for r in getattr(rs, c.Rule) if r + for ll in getattr(r, c.LookupRecord) if ll] + elif self.Format == 3: + return [ll.LookupListIndex + for ll in getattr(self, c.LookupRecord) if ll] + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst) def closure_glyphs(self, s, cur_glyphs): - if self.Format == 1: - self.ExtSubTable.closure_glyphs(s, cur_glyphs) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + self.ExtSubTable.closure_glyphs(s, cur_glyphs) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst) def may_have_non_1to1(self): - if self.Format == 1: - return self.ExtSubTable.may_have_non_1to1() - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.may_have_non_1to1() + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def subset_glyphs(self, s): - if self.Format == 1: - return self.ExtSubTable.subset_glyphs(s) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.subset_glyphs(s) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def prune_post_subset(self, options): - if self.Format == 1: - return self.ExtSubTable.prune_post_subset(options) - else: - assert 0, "unknown format: %s" % self.Format + otTables.ExtensionPos) +def prune_post_subset(self, font, options): + if self.Format == 1: + return self.ExtSubTable.prune_post_subset(font, options) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def subset_lookups(self, lookup_indices): - if self.Format == 1: - return self.ExtSubTable.subset_lookups(lookup_indices) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.subset_lookups(lookup_indices) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def collect_lookups(self): - if self.Format == 1: - return self.ExtSubTable.collect_lookups() - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.collect_lookups() + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.Lookup) def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: - cur_glyphs = frozenset(s.glyphs) + if cur_glyphs is None: + cur_glyphs = frozenset(s.glyphs) - # Memoize - if (id(self), cur_glyphs) in s._doneLookups: - return - s._doneLookups.add((id(self), cur_glyphs)) - - if self in s._activeLookups: - raise Exception("Circular loop in lookup recursion") - s._activeLookups.append(self) - for st in self.SubTable: - if not st: continue - st.closure_glyphs(s, cur_glyphs) - assert(s._activeLookups[-1] == self) - del s._activeLookups[-1] + # Memoize + key = id(self) + doneLookups = s._doneLookups + count,covered = doneLookups.get(key, (0, None)) + if count != len(s.glyphs): + count,covered = doneLookups[key] = (len(s.glyphs), set()) + if cur_glyphs.issubset(covered): + return + covered.update(cur_glyphs) + + for st in self.SubTable: + if not st: continue + st.closure_glyphs(s, cur_glyphs) @_add_method(otTables.Lookup) def subset_glyphs(self, s): - self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] - self.SubTableCount = len(self.SubTable) - return bool(self.SubTableCount) + self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] + self.SubTableCount = len(self.SubTable) + return bool(self.SubTableCount) @_add_method(otTables.Lookup) -def prune_post_subset(self, options): - ret = False - for st in self.SubTable: - if not st: continue - if st.prune_post_subset(options): ret = True - return ret +def prune_post_subset(self, font, options): + ret = False + for st in self.SubTable: + if not st: continue + if st.prune_post_subset(font, options): ret = True + return ret @_add_method(otTables.Lookup) def subset_lookups(self, lookup_indices): - for s in self.SubTable: - s.subset_lookups(lookup_indices) + for s in self.SubTable: + s.subset_lookups(lookup_indices) @_add_method(otTables.Lookup) def collect_lookups(self): - return sum((st.collect_lookups() for st in self.SubTable if st), []) + return sum((st.collect_lookups() for st in self.SubTable if st), []) @_add_method(otTables.Lookup) def may_have_non_1to1(self): - return any(st.may_have_non_1to1() for st in self.SubTable if st) + return any(st.may_have_non_1to1() for st in self.SubTable if st) @_add_method(otTables.LookupList) def subset_glyphs(self, s): - """Returns the indices of nonempty lookups.""" - return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] + """Returns the indices of nonempty lookups.""" + return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] @_add_method(otTables.LookupList) -def prune_post_subset(self, options): - ret = False - for l in self.Lookup: - if not l: continue - if l.prune_post_subset(options): ret = True - return ret +def prune_post_subset(self, font, options): + ret = False + for l in self.Lookup: + if not l: continue + if l.prune_post_subset(font, options): ret = True + return ret @_add_method(otTables.LookupList) def subset_lookups(self, lookup_indices): - self.ensureDecompiled() - self.Lookup = [self.Lookup[i] for i in lookup_indices - if i < self.LookupCount] - self.LookupCount = len(self.Lookup) - for l in self.Lookup: - l.subset_lookups(lookup_indices) + self.ensureDecompiled() + self.Lookup = [self.Lookup[i] for i in lookup_indices + if i < self.LookupCount] + self.LookupCount = len(self.Lookup) + for l in self.Lookup: + l.subset_lookups(lookup_indices) @_add_method(otTables.LookupList) def neuter_lookups(self, lookup_indices): - """Sets lookups not in lookup_indices to None.""" - self.ensureDecompiled() - self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)] + """Sets lookups not in lookup_indices to None.""" + self.ensureDecompiled() + self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)] @_add_method(otTables.LookupList) def closure_lookups(self, lookup_indices): - """Returns sorted index of all lookups reachable from lookup_indices.""" - lookup_indices = _uniq_sort(lookup_indices) - recurse = lookup_indices - while True: - recurse_lookups = sum((self.Lookup[i].collect_lookups() - for i in recurse if i < self.LookupCount), []) - recurse_lookups = [l for l in recurse_lookups - if l not in lookup_indices and l < self.LookupCount] - if not recurse_lookups: - return _uniq_sort(lookup_indices) - recurse_lookups = _uniq_sort(recurse_lookups) - lookup_indices.extend(recurse_lookups) - recurse = recurse_lookups + """Returns sorted index of all lookups reachable from lookup_indices.""" + lookup_indices = _uniq_sort(lookup_indices) + recurse = lookup_indices + while True: + recurse_lookups = sum((self.Lookup[i].collect_lookups() + for i in recurse if i < self.LookupCount), []) + recurse_lookups = [l for l in recurse_lookups + if l not in lookup_indices and l < self.LookupCount] + if not recurse_lookups: + return _uniq_sort(lookup_indices) + recurse_lookups = _uniq_sort(recurse_lookups) + lookup_indices.extend(recurse_lookups) + recurse = recurse_lookups @_add_method(otTables.Feature) def subset_lookups(self, lookup_indices): - """"Returns True if feature is non-empty afterwards.""" - self.LookupListIndex = [l for l in self.LookupListIndex - if l in lookup_indices] - # Now map them. - self.LookupListIndex = [lookup_indices.index(l) - for l in self.LookupListIndex] - self.LookupCount = len(self.LookupListIndex) - return self.LookupCount or self.FeatureParams + """"Returns True if feature is non-empty afterwards.""" + self.LookupListIndex = [l for l in self.LookupListIndex + if l in lookup_indices] + # Now map them. + self.LookupListIndex = [lookup_indices.index(l) + for l in self.LookupListIndex] + self.LookupCount = len(self.LookupListIndex) + return self.LookupCount or self.FeatureParams @_add_method(otTables.FeatureList) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - # Note: Never ever drop feature 'pref', even if it's empty. - # HarfBuzz chooses shaper for Khmer based on presence of this - # feature. See thread at: - # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html - return [i for i,f in enumerate(self.FeatureRecord) - if (f.Feature.subset_lookups(lookup_indices) or - f.FeatureTag == 'pref')] + """Returns the indices of nonempty features.""" + # Note: Never ever drop feature 'pref', even if it's empty. + # HarfBuzz chooses shaper for Khmer based on presence of this + # feature. See thread at: + # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html + return [i for i,f in enumerate(self.FeatureRecord) + if (f.Feature.subset_lookups(lookup_indices) or + f.FeatureTag == 'pref')] @_add_method(otTables.FeatureList) def collect_lookups(self, feature_indices): - return sum((self.FeatureRecord[i].Feature.LookupListIndex - for i in feature_indices - if i < self.FeatureCount), []) + return sum((self.FeatureRecord[i].Feature.LookupListIndex + for i in feature_indices + if i < self.FeatureCount), []) @_add_method(otTables.FeatureList) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] - self.FeatureCount = len(self.FeatureRecord) - return bool(self.FeatureCount) + self.ensureDecompiled() + self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] + self.FeatureCount = len(self.FeatureRecord) + return bool(self.FeatureCount) @_add_method(otTables.FeatureTableSubstitution) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - return [r.FeatureIndex for r in self.SubstitutionRecord - if r.Feature.subset_lookups(lookup_indices)] + """Returns the indices of nonempty features.""" + return [r.FeatureIndex for r in self.SubstitutionRecord + if r.Feature.subset_lookups(lookup_indices)] @_add_method(otTables.FeatureVariations) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - return sum((f.FeatureTableSubstitution.subset_lookups(lookup_indices) - for f in self.FeatureVariationRecord), []) + """Returns the indices of nonempty features.""" + return sum((f.FeatureTableSubstitution.subset_lookups(lookup_indices) + for f in self.FeatureVariationRecord), []) @_add_method(otTables.FeatureVariations) def collect_lookups(self, feature_indices): - return sum((r.Feature.LookupListIndex - for vr in self.FeatureVariationRecord - for r in vr.FeatureTableSubstitution.SubstitutionRecord - if r.FeatureIndex in feature_indices), []) + return sum((r.Feature.LookupListIndex + for vr in self.FeatureVariationRecord + for r in vr.FeatureTableSubstitution.SubstitutionRecord + if r.FeatureIndex in feature_indices), []) @_add_method(otTables.FeatureTableSubstitution) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.SubstitutionRecord = [r for r in self.SubstitutionRecord - if r.FeatureIndex in feature_indices] - self.SubstitutionCount = len(self.SubstitutionRecord) - return bool(self.SubstitutionCount) + self.ensureDecompiled() + self.SubstitutionRecord = [r for r in self.SubstitutionRecord + if r.FeatureIndex in feature_indices] + self.SubstitutionCount = len(self.SubstitutionRecord) + return bool(self.SubstitutionCount) @_add_method(otTables.FeatureVariations) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.FeaturVariationRecord = [r for r in self.FeatureVariationRecord - if r.FeatureTableSubstitution.subset_features(feature_indices)] - self.FeatureVariationCount = len(self.FeatureVariationRecord) - return bool(self.FeatureVariationCount) + self.ensureDecompiled() + self.FeaturVariationRecord = [r for r in self.FeatureVariationRecord + if r.FeatureTableSubstitution.subset_features(feature_indices)] + self.FeatureVariationCount = len(self.FeatureVariationRecord) + return bool(self.FeatureVariationCount) @_add_method(otTables.DefaultLangSys, - otTables.LangSys) + otTables.LangSys) def subset_features(self, feature_indices): - if self.ReqFeatureIndex in feature_indices: - self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) - else: - self.ReqFeatureIndex = 65535 - self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] - # Now map them. - self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex - if f in feature_indices] - self.FeatureCount = len(self.FeatureIndex) - return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) + if self.ReqFeatureIndex in feature_indices: + self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) + else: + self.ReqFeatureIndex = 65535 + self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] + # Now map them. + self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex + if f in feature_indices] + self.FeatureCount = len(self.FeatureIndex) + return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) @_add_method(otTables.DefaultLangSys, - otTables.LangSys) + otTables.LangSys) def collect_features(self): - feature_indices = self.FeatureIndex[:] - if self.ReqFeatureIndex != 65535: - feature_indices.append(self.ReqFeatureIndex) - return _uniq_sort(feature_indices) + feature_indices = self.FeatureIndex[:] + if self.ReqFeatureIndex != 65535: + feature_indices.append(self.ReqFeatureIndex) + return _uniq_sort(feature_indices) @_add_method(otTables.Script) def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False): - if(self.DefaultLangSys and - not self.DefaultLangSys.subset_features(feature_indices) and - not keepEmptyDefaultLangSys): - self.DefaultLangSys = None - self.LangSysRecord = [l for l in self.LangSysRecord - if l.LangSys.subset_features(feature_indices)] - self.LangSysCount = len(self.LangSysRecord) - return bool(self.LangSysCount or self.DefaultLangSys) + if(self.DefaultLangSys and + not self.DefaultLangSys.subset_features(feature_indices) and + not keepEmptyDefaultLangSys): + self.DefaultLangSys = None + self.LangSysRecord = [l for l in self.LangSysRecord + if l.LangSys.subset_features(feature_indices)] + self.LangSysCount = len(self.LangSysRecord) + return bool(self.LangSysCount or self.DefaultLangSys) @_add_method(otTables.Script) def collect_features(self): - feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] - if self.DefaultLangSys: - feature_indices.append(self.DefaultLangSys.collect_features()) - return _uniq_sort(sum(feature_indices, [])) + feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] + if self.DefaultLangSys: + feature_indices.append(self.DefaultLangSys.collect_features()) + return _uniq_sort(sum(feature_indices, [])) @_add_method(otTables.ScriptList) def subset_features(self, feature_indices, retain_empty): - # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 - self.ScriptRecord = [s for s in self.ScriptRecord - if s.Script.subset_features(feature_indices, s.ScriptTag=='DFLT') or - retain_empty] - self.ScriptCount = len(self.ScriptRecord) - return bool(self.ScriptCount) + # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 + self.ScriptRecord = [s for s in self.ScriptRecord + if s.Script.subset_features(feature_indices, s.ScriptTag=='DFLT') or + retain_empty] + self.ScriptCount = len(self.ScriptRecord) + return bool(self.ScriptCount) @_add_method(otTables.ScriptList) def collect_features(self): - return _uniq_sort(sum((s.Script.collect_features() - for s in self.ScriptRecord), [])) + return _uniq_sort(sum((s.Script.collect_features() + for s in self.ScriptRecord), [])) # CBLC will inherit it @_add_method(ttLib.getTableClass('EBLC')) def subset_glyphs(self, s): - for strike in self.strikes: - for indexSubTable in strike.indexSubTables: - indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] - strike.indexSubTables = [i for i in strike.indexSubTables if i.names] - self.strikes = [s for s in self.strikes if s.indexSubTables] + for strike in self.strikes: + for indexSubTable in strike.indexSubTables: + indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] + strike.indexSubTables = [i for i in strike.indexSubTables if i.names] + self.strikes = [s for s in self.strikes if s.indexSubTables] - return True + return True # CBDC will inherit it @_add_method(ttLib.getTableClass('EBDT')) def subset_glyphs(self, s): self.strikeData = [{g: strike[g] for g in s.glyphs if g in strike} - for strike in self.strikeData] + for strike in self.strikeData] return True @_add_method(ttLib.getTableClass('GSUB')) def closure_glyphs(self, s): - s.table = self.table - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if getattr(self.table, 'FeatureVariations', None): - lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) - lookup_indices = _uniq_sort(lookup_indices) - if self.table.LookupList: - while True: - orig_glyphs = frozenset(s.glyphs) - s._activeLookups = [] - s._doneLookups = set() - for i in lookup_indices: - if i >= self.table.LookupList.LookupCount: continue - if not self.table.LookupList.Lookup[i]: continue - self.table.LookupList.Lookup[i].closure_glyphs(s) - del s._activeLookups, s._doneLookups - if orig_glyphs == s.glyphs: - break - del s.table + s.table = self.table + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + s._doneLookups = {} + while True: + orig_glyphs = frozenset(s.glyphs) + for i in lookup_indices: + if i >= self.table.LookupList.LookupCount: continue + if not self.table.LookupList.Lookup[i]: continue + self.table.LookupList.Lookup[i].closure_glyphs(s) + if orig_glyphs == s.glyphs: + break + del s._doneLookups + del s.table @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_glyphs(self, s): - s.glyphs = s.glyphs_gsubed - if self.table.LookupList: - lookup_indices = self.table.LookupList.subset_glyphs(s) - else: - lookup_indices = [] - self.subset_lookups(lookup_indices) - return True + s.glyphs = s.glyphs_gsubed + if self.table.LookupList: + lookup_indices = self.table.LookupList.subset_glyphs(s) + else: + lookup_indices = [] + self.subset_lookups(lookup_indices) + return True @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def retain_empty_scripts(self): - # https://github.com/behdad/fonttools/issues/518 - # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 - return self.__class__ == ttLib.getTableClass('GSUB') + # https://github.com/behdad/fonttools/issues/518 + # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 + return self.__class__ == ttLib.getTableClass('GSUB') @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_lookups(self, lookup_indices): - """Retains specified lookups, then removes empty features, language - systems, and scripts.""" - if self.table.LookupList: - self.table.LookupList.subset_lookups(lookup_indices) - if self.table.FeatureList: - feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) - else: - feature_indices = [] - if getattr(self.table, 'FeatureVariations', None): - feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) - feature_indices = _uniq_sort(feature_indices) - if self.table.FeatureList: - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + """Retains specified lookups, then removes empty features, language + systems, and scripts.""" + if self.table.LookupList: + self.table.LookupList.subset_lookups(lookup_indices) + if self.table.FeatureList: + feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) + else: + feature_indices = [] + if getattr(self.table, 'FeatureVariations', None): + feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) + feature_indices = _uniq_sort(feature_indices) + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def neuter_lookups(self, lookup_indices): - """Sets lookups not in lookup_indices to None.""" - if self.table.LookupList: - self.table.LookupList.neuter_lookups(lookup_indices) + """Sets lookups not in lookup_indices to None.""" + if self.table.LookupList: + self.table.LookupList.neuter_lookups(lookup_indices) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_lookups(self, remap=True): - """Remove (default) or neuter unreferenced lookups""" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if getattr(self.table, 'FeatureVariations', None): - lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) - lookup_indices = _uniq_sort(lookup_indices) - if self.table.LookupList: - lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) - else: - lookup_indices = [] - if remap: - self.subset_lookups(lookup_indices) - else: - self.neuter_lookups(lookup_indices) + """Remove (default) or neuter unreferenced lookups""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) + else: + lookup_indices = [] + if remap: + self.subset_lookups(lookup_indices) + else: + self.neuter_lookups(lookup_indices) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_feature_tags(self, feature_tags): - if self.table.FeatureList: - feature_indices = \ - [i for i,f in enumerate(self.table.FeatureList.FeatureRecord) - if f.FeatureTag in feature_tags] - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - else: - feature_indices = [] - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + if self.table.FeatureList: + feature_indices = \ + [i for i,f in enumerate(self.table.FeatureList.FeatureRecord) + if f.FeatureTag in feature_tags] + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + else: + feature_indices = [] + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_features(self): - """Remove unreferenced features""" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + """Remove unreferenced features""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_pre_subset(self, font, options): - # Drop undesired features - if '*' not in options.layout_features: - self.subset_feature_tags(options.layout_features) - # Neuter unreferenced lookups - self.prune_lookups(remap=False) - return True + # Drop undesired features + if '*' not in options.layout_features: + self.subset_feature_tags(options.layout_features) + # Neuter unreferenced lookups + self.prune_lookups(remap=False) + return True @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def remove_redundant_langsys(self): - table = self.table - if not table.ScriptList or not table.FeatureList: - return - - features = table.FeatureList.FeatureRecord - - for s in table.ScriptList.ScriptRecord: - d = s.Script.DefaultLangSys - if not d: - continue - for lr in s.Script.LangSysRecord[:]: - l = lr.LangSys - # Compare d and l - if len(d.FeatureIndex) != len(l.FeatureIndex): - continue - if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): - continue - - if d.ReqFeatureIndex != 65535: - if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: - continue - - for i in range(len(d.FeatureIndex)): - if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: - break - else: - # LangSys and default are equal; delete LangSys - s.Script.LangSysRecord.remove(lr) + table = self.table + if not table.ScriptList or not table.FeatureList: + return + + features = table.FeatureList.FeatureRecord + + for s in table.ScriptList.ScriptRecord: + d = s.Script.DefaultLangSys + if not d: + continue + for lr in s.Script.LangSysRecord[:]: + l = lr.LangSys + # Compare d and l + if len(d.FeatureIndex) != len(l.FeatureIndex): + continue + if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): + continue + + if d.ReqFeatureIndex != 65535: + if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: + continue + + for i in range(len(d.FeatureIndex)): + if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: + break + else: + # LangSys and default are equal; delete LangSys + s.Script.LangSysRecord.remove(lr) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_post_subset(self, options): - table = self.table - - self.prune_lookups() # XXX Is this actually needed?! - - if table.LookupList: - table.LookupList.prune_post_subset(options) - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if not table.LookupList.Lookup: - # table.LookupList = None - - if not table.LookupList: - table.FeatureList = None - - - if table.FeatureList: - self.remove_redundant_langsys() - # Remove unreferenced features - self.prune_features() - - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if table.FeatureList and not table.FeatureList.FeatureRecord: - # table.FeatureList = None - - # Never drop scripts themselves as them just being available - # holds semantic significance. - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if table.ScriptList and not table.ScriptList.ScriptRecord: - # table.ScriptList = None - - if not table.FeatureList and hasattr(table, 'FeatureVariations'): - table.FeatureVariations = None - - if hasattr(table, 'FeatureVariations') and not table.FeatureVariations: - if table.Version == 0x00010001: - table.Version = 0x00010000 + ttLib.getTableClass('GPOS')) +def prune_post_subset(self, font, options): + table = self.table + + self.prune_lookups() # XXX Is this actually needed?! + + if table.LookupList: + table.LookupList.prune_post_subset(font, options) + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if not table.LookupList.Lookup: + # table.LookupList = None + + if not table.LookupList: + table.FeatureList = None + + + if table.FeatureList: + self.remove_redundant_langsys() + # Remove unreferenced features + self.prune_features() + + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.FeatureList and not table.FeatureList.FeatureRecord: + # table.FeatureList = None + + # Never drop scripts themselves as them just being available + # holds semantic significance. + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.ScriptList and not table.ScriptList.ScriptRecord: + # table.ScriptList = None + + if not table.FeatureList and hasattr(table, 'FeatureVariations'): + table.FeatureVariations = None + + if hasattr(table, 'FeatureVariations') and not table.FeatureVariations: + if table.Version == 0x00010001: + table.Version = 0x00010000 - return True + return True @_add_method(ttLib.getTableClass('GDEF')) def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - table = self.table - if table.LigCaretList: - indices = table.LigCaretList.Coverage.subset(glyphs) - table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] for i in indices] - table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) - if table.MarkAttachClassDef: - table.MarkAttachClassDef.classDefs = \ - {g:v for g,v in table.MarkAttachClassDef.classDefs.items() - if g in glyphs} - if table.GlyphClassDef: - table.GlyphClassDef.classDefs = \ - {g:v for g,v in table.GlyphClassDef.classDefs.items() - if g in glyphs} - if table.AttachList: - indices = table.AttachList.Coverage.subset(glyphs) - GlyphCount = table.AttachList.GlyphCount - table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] - for i in indices if i < GlyphCount] - table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) - if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: - for coverage in table.MarkGlyphSetsDef.Coverage: - coverage.subset(glyphs) - # TODO: The following is disabled. If enabling, we need to go fixup all - # lookups that use MarkFilteringSet and map their set. - # indices = table.MarkGlyphSetsDef.Coverage = \ - # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] - return True + glyphs = s.glyphs_gsubed + table = self.table + if table.LigCaretList: + indices = table.LigCaretList.Coverage.subset(glyphs) + table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] for i in indices] + table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) + if table.MarkAttachClassDef: + table.MarkAttachClassDef.classDefs = \ + {g:v for g,v in table.MarkAttachClassDef.classDefs.items() + if g in glyphs} + if table.GlyphClassDef: + table.GlyphClassDef.classDefs = \ + {g:v for g,v in table.GlyphClassDef.classDefs.items() + if g in glyphs} + if table.AttachList: + indices = table.AttachList.Coverage.subset(glyphs) + GlyphCount = table.AttachList.GlyphCount + table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] + for i in indices if i < GlyphCount] + table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) + if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: + for coverage in table.MarkGlyphSetsDef.Coverage: + if coverage: + coverage.subset(glyphs) + + # TODO: The following is disabled. If enabling, we need to go fixup all + # lookups that use MarkFilteringSet and map their set. + # indices = table.MarkGlyphSetsDef.Coverage = \ + # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] + # TODO: The following is disabled, as ots doesn't like it. Phew... + # https://github.com/khaledhosny/ots/issues/172 + # table.MarkGlyphSetsDef.Coverage = [c if c.glyphs else None for c in table.MarkGlyphSetsDef.Coverage] + return True + + +def _pruneGDEF(font): + if 'GDEF' not in font: return + gdef = font['GDEF'] + table = gdef.table + if not hasattr(table, 'VarStore'): return + + store = table.VarStore + + usedVarIdxes = set() + + # Collect. + table.collect_device_varidxes(usedVarIdxes) + if 'GPOS' in font: + font['GPOS'].table.collect_device_varidxes(usedVarIdxes) + + # Subset. + varidx_map = store.subset_varidxes(usedVarIdxes) + + # Map. + table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) @_add_method(ttLib.getTableClass('GDEF')) -def prune_post_subset(self, options): - table = self.table - # XXX check these against OTS - if table.LigCaretList and not table.LigCaretList.LigGlyphCount: - table.LigCaretList = None - if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: - table.MarkAttachClassDef = None - if table.GlyphClassDef and not table.GlyphClassDef.classDefs: - table.GlyphClassDef = None - if table.AttachList and not table.AttachList.GlyphCount: - table.AttachList = None - if (hasattr(table, "MarkGlyphSetsDef") and - table.MarkGlyphSetsDef and - not table.MarkGlyphSetsDef.Coverage): - table.MarkGlyphSetsDef = None - if table.Version == 0x00010002: - table.Version = 0x00010000 - return bool(table.LigCaretList or - table.MarkAttachClassDef or - table.GlyphClassDef or - table.AttachList or - (table.Version >= 0x00010002 and table.MarkGlyphSetsDef)) +def prune_post_subset(self, font, options): + table = self.table + # XXX check these against OTS + if table.LigCaretList and not table.LigCaretList.LigGlyphCount: + table.LigCaretList = None + if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: + table.MarkAttachClassDef = None + if table.GlyphClassDef and not table.GlyphClassDef.classDefs: + table.GlyphClassDef = None + if table.AttachList and not table.AttachList.GlyphCount: + table.AttachList = None + if hasattr(table, "VarStore"): + _pruneGDEF(font) + if table.VarStore.VarDataCount == 0: + if table.Version == 0x00010003: + table.Version = 0x00010002 + if (not hasattr(table, "MarkGlyphSetsDef") or + not table.MarkGlyphSetsDef or + not table.MarkGlyphSetsDef.Coverage): + table.MarkGlyphSetsDef = None + if table.Version == 0x00010002: + table.Version = 0x00010000 + return bool(table.LigCaretList or + table.MarkAttachClassDef or + table.GlyphClassDef or + table.AttachList or + (table.Version >= 0x00010002 and table.MarkGlyphSetsDef) or + (table.Version >= 0x00010003 and table.VarStore)) @_add_method(ttLib.getTableClass('kern')) def prune_pre_subset(self, font, options): - # Prune unknown kern table types - self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] - return bool(self.kernTables) + # Prune unknown kern table types + self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] + return bool(self.kernTables) @_add_method(ttLib.getTableClass('kern')) def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - for t in self.kernTables: - t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items() - if a in glyphs and b in glyphs} - self.kernTables = [t for t in self.kernTables if t.kernTable] - return bool(self.kernTables) + glyphs = s.glyphs_gsubed + for t in self.kernTables: + t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items() + if a in glyphs and b in glyphs} + self.kernTables = [t for t in self.kernTables if t.kernTable] + return bool(self.kernTables) @_add_method(ttLib.getTableClass('vmtx')) def subset_glyphs(self, s): - self.metrics = _dict_subset(self.metrics, s.glyphs) - return bool(self.metrics) + self.metrics = _dict_subset(self.metrics, s.glyphs) + return bool(self.metrics) @_add_method(ttLib.getTableClass('hmtx')) def subset_glyphs(self, s): - self.metrics = _dict_subset(self.metrics, s.glyphs) - return True # Required table + self.metrics = _dict_subset(self.metrics, s.glyphs) + return True # Required table @_add_method(ttLib.getTableClass('hdmx')) def subset_glyphs(self, s): - self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()} - return bool(self.hdmx) + self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()} + return bool(self.hdmx) @_add_method(ttLib.getTableClass('ankr')) def subset_glyphs(self, s): - table = self.table.AnchorPoints - assert table.Format == 0, "unknown 'ankr' format %s" % table.Format - table.Anchors = {glyph: table.Anchors[glyph] for glyph in s.glyphs - if glyph in table.Anchors} - return len(table.Anchors) > 0 + table = self.table.AnchorPoints + assert table.Format == 0, "unknown 'ankr' format %s" % table.Format + table.Anchors = {glyph: table.Anchors[glyph] for glyph in s.glyphs + if glyph in table.Anchors} + return len(table.Anchors) > 0 @_add_method(ttLib.getTableClass('bsln')) def closure_glyphs(self, s): - table = self.table.Baseline - if table.Format in (2, 3): - s.glyphs.add(table.StandardGlyph) + table = self.table.Baseline + if table.Format in (2, 3): + s.glyphs.add(table.StandardGlyph) @_add_method(ttLib.getTableClass('bsln')) def subset_glyphs(self, s): - table = self.table.Baseline - if table.Format in (1, 3): - baselines = {glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) - for glyph in s.glyphs} - if len(baselines) > 0: - mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] - table.DefaultBaseline = mostCommon - baselines = {glyph: b for glyph, b in baselines.items() - if b != mostCommon} - if len(baselines) > 0: - table.BaselineValues = baselines - else: - table.Format = {1: 0, 3: 2}[table.Format] - del table.BaselineValues - return True + table = self.table.Baseline + if table.Format in (1, 3): + baselines = {glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) + for glyph in s.glyphs} + if len(baselines) > 0: + mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] + table.DefaultBaseline = mostCommon + baselines = {glyph: b for glyph, b in baselines.items() + if b != mostCommon} + if len(baselines) > 0: + table.BaselineValues = baselines + else: + table.Format = {1: 0, 3: 2}[table.Format] + del table.BaselineValues + return True @_add_method(ttLib.getTableClass('lcar')) def subset_glyphs(self, s): - table = self.table.LigatureCarets - if table.Format in (0, 1): - table.Carets = {glyph: table.Carets[glyph] for glyph in s.glyphs - if glyph in table.Carets} - return len(table.Carets) > 0 - else: - assert False, "unknown 'lcar' format %s" % table.Format + table = self.table.LigatureCarets + if table.Format in (0, 1): + table.Carets = {glyph: table.Carets[glyph] for glyph in s.glyphs + if glyph in table.Carets} + return len(table.Carets) > 0 + else: + assert False, "unknown 'lcar' format %s" % table.Format @_add_method(ttLib.getTableClass('gvar')) def prune_pre_subset(self, font, options): - if options.notdef_glyph and not options.notdef_outline: - self.variations[font.glyphOrder[0]] = [] - return True + if options.notdef_glyph and not options.notdef_outline: + self.variations[font.glyphOrder[0]] = [] + return True @_add_method(ttLib.getTableClass('gvar')) def subset_glyphs(self, s): - self.variations = _dict_subset(self.variations, s.glyphs) - self.glyphCount = len(self.variations) - return bool(self.variations) + self.variations = _dict_subset(self.variations, s.glyphs) + self.glyphCount = len(self.variations) + return bool(self.variations) + +@_add_method(ttLib.getTableClass('HVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs) + used.update(table.AdvWidthMap.mapping.values()) + else: + assert table.LsbMap is None and table.RsbMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + + if table.LsbMap: + table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs) + used.update(table.LsbMap.mapping.values()) + if table.RsbMap: + table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs) + used.update(table.RsbMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = {k:varidx_map[v] for k,v in table.AdvWidthMap.mapping.items()} + if table.LsbMap: + table.LsbMap.mapping = {k:varidx_map[v] for k,v in table.LsbMap.mapping.items()} + if table.RsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True + +@_add_method(ttLib.getTableClass('VVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs) + used.update(table.AdvHeightMap.mapping.values()) + else: + assert table.TsbMap is None and table.BsbMap is None and table.VOrgMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + if table.TsbMap: + table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs) + used.update(table.TsbMap.mapping.values()) + if table.BsbMap: + table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs) + used.update(table.BsbMap.mapping.values()) + if table.VOrgMap: + table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs) + used.update(table.VOrgMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = {k:varidx_map[v] for k,v in table.AdvHeightMap.mapping.items()} + if table.TsbMap: + table.TsbMap.mapping = {k:varidx_map[v] for k,v in table.TsbMap.mapping.items()} + if table.BsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + if table.VOrgMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True @_add_method(ttLib.getTableClass('VORG')) def subset_glyphs(self, s): - self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() - if g in s.glyphs} - self.numVertOriginYMetrics = len(self.VOriginRecords) - return True # Never drop; has default metrics + self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() + if g in s.glyphs} + self.numVertOriginYMetrics = len(self.VOriginRecords) + return True # Never drop; has default metrics @_add_method(ttLib.getTableClass('opbd')) def subset_glyphs(self, s): - table = self.table.OpticalBounds - if table.Format == 0: - table.OpticalBoundsDeltas = {glyph: table.OpticalBoundsDeltas[glyph] - for glyph in s.glyphs - if glyph in table.OpticalBoundsDeltas} - return len(table.OpticalBoundsDeltas) > 0 - elif table.Format == 1: - table.OpticalBoundsPoints = {glyph: table.OpticalBoundsPoints[glyph] - for glyph in s.glyphs - if glyph in table.OpticalBoundsPoints} - return len(table.OpticalBoundsPoints) > 0 - else: - assert False, "unknown 'opbd' format %s" % table.Format + table = self.table.OpticalBounds + if table.Format == 0: + table.OpticalBoundsDeltas = {glyph: table.OpticalBoundsDeltas[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsDeltas} + return len(table.OpticalBoundsDeltas) > 0 + elif table.Format == 1: + table.OpticalBoundsPoints = {glyph: table.OpticalBoundsPoints[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsPoints} + return len(table.OpticalBoundsPoints) > 0 + else: + assert False, "unknown 'opbd' format %s" % table.Format @_add_method(ttLib.getTableClass('post')) def prune_pre_subset(self, font, options): - if not options.glyph_names: - self.formatType = 3.0 - return True # Required table + if not options.glyph_names: + self.formatType = 3.0 + return True # Required table @_add_method(ttLib.getTableClass('post')) def subset_glyphs(self, s): - self.extraNames = [] # This seems to do it - return True # Required table + self.extraNames = [] # This seems to do it + return True # Required table @_add_method(ttLib.getTableClass('prop')) def subset_glyphs(self, s): - prop = self.table.GlyphProperties - if prop.Format == 0: - return prop.DefaultProperties != 0 - elif prop.Format == 1: - prop.Properties = {g: prop.Properties.get(g, prop.DefaultProperties) - for g in s.glyphs} - mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] - prop.DefaultProperties = mostCommon - prop.Properties = {g: prop for g, prop in prop.Properties.items() - if prop != mostCommon} - if len(prop.Properties) == 0: - del prop.Properties - prop.Format = 0 - return prop.DefaultProperties != 0 - return True - else: - assert False, "unknown 'prop' format %s" % prop.Format + prop = self.table.GlyphProperties + if prop.Format == 0: + return prop.DefaultProperties != 0 + elif prop.Format == 1: + prop.Properties = {g: prop.Properties.get(g, prop.DefaultProperties) + for g in s.glyphs} + mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] + prop.DefaultProperties = mostCommon + prop.Properties = {g: prop for g, prop in prop.Properties.items() + if prop != mostCommon} + if len(prop.Properties) == 0: + del prop.Properties + prop.Format = 0 + return prop.DefaultProperties != 0 + return True + else: + assert False, "unknown 'prop' format %s" % prop.Format @_add_method(ttLib.getTableClass('COLR')) def closure_glyphs(self, s): - decompose = s.glyphs - while decompose: - layers = set() - for g in decompose: - for l in self.ColorLayers.get(g, []): - layers.add(l.name) - layers -= s.glyphs - s.glyphs.update(layers) - decompose = layers + decompose = s.glyphs + while decompose: + layers = set() + for g in decompose: + for l in self.ColorLayers.get(g, []): + layers.add(l.name) + layers -= s.glyphs + s.glyphs.update(layers) + decompose = layers @_add_method(ttLib.getTableClass('COLR')) def subset_glyphs(self, s): - self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers} - return bool(self.ColorLayers) + self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers} + return bool(self.ColorLayers) # TODO: prune unused palettes @_add_method(ttLib.getTableClass('CPAL')) -def prune_post_subset(self, options): - return True +def prune_post_subset(self, font, options): + return True @_add_method(otTables.MathGlyphConstruction) def closure_glyphs(self, glyphs): - variants = set() - for v in self.MathGlyphVariantRecord: - variants.add(v.VariantGlyph) - if self.GlyphAssembly: - for p in self.GlyphAssembly.PartRecords: - variants.add(p.glyph) - return variants + variants = set() + for v in self.MathGlyphVariantRecord: + variants.add(v.VariantGlyph) + if self.GlyphAssembly: + for p in self.GlyphAssembly.PartRecords: + variants.add(p.glyph) + return variants @_add_method(otTables.MathVariants) def closure_glyphs(self, s): - glyphs = frozenset(s.glyphs) - variants = set() + glyphs = frozenset(s.glyphs) + variants = set() - if self.VertGlyphCoverage: - indices = self.VertGlyphCoverage.intersect(glyphs) - for i in indices: - variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) - - if self.HorizGlyphCoverage: - indices = self.HorizGlyphCoverage.intersect(glyphs) - for i in indices: - variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) - s.glyphs.update(variants) + s.glyphs.update(variants) @_add_method(ttLib.getTableClass('MATH')) def closure_glyphs(self, s): - self.table.MathVariants.closure_glyphs(s) + self.table.MathVariants.closure_glyphs(s) @_add_method(otTables.MathItalicsCorrectionInfo) def subset_glyphs(self, s): - indices = self.Coverage.subset(s.glyphs) - self.ItalicsCorrection = [self.ItalicsCorrection[i] for i in indices] - self.ItalicsCorrectionCount = len(self.ItalicsCorrection) - return bool(self.ItalicsCorrectionCount) + indices = self.Coverage.subset(s.glyphs) + self.ItalicsCorrection = [self.ItalicsCorrection[i] for i in indices] + self.ItalicsCorrectionCount = len(self.ItalicsCorrection) + return bool(self.ItalicsCorrectionCount) @_add_method(otTables.MathTopAccentAttachment) def subset_glyphs(self, s): - indices = self.TopAccentCoverage.subset(s.glyphs) - self.TopAccentAttachment = [self.TopAccentAttachment[i] for i in indices] - self.TopAccentAttachmentCount = len(self.TopAccentAttachment) - return bool(self.TopAccentAttachmentCount) + indices = self.TopAccentCoverage.subset(s.glyphs) + self.TopAccentAttachment = [self.TopAccentAttachment[i] for i in indices] + self.TopAccentAttachmentCount = len(self.TopAccentAttachment) + return bool(self.TopAccentAttachmentCount) @_add_method(otTables.MathKernInfo) def subset_glyphs(self, s): - indices = self.MathKernCoverage.subset(s.glyphs) - self.MathKernInfoRecords = [self.MathKernInfoRecords[i] for i in indices] - self.MathKernCount = len(self.MathKernInfoRecords) - return bool(self.MathKernCount) + indices = self.MathKernCoverage.subset(s.glyphs) + self.MathKernInfoRecords = [self.MathKernInfoRecords[i] for i in indices] + self.MathKernCount = len(self.MathKernInfoRecords) + return bool(self.MathKernCount) @_add_method(otTables.MathGlyphInfo) def subset_glyphs(self, s): - if self.MathItalicsCorrectionInfo: - self.MathItalicsCorrectionInfo.subset_glyphs(s) - if self.MathTopAccentAttachment: - self.MathTopAccentAttachment.subset_glyphs(s) - if self.MathKernInfo: - self.MathKernInfo.subset_glyphs(s) - if self.ExtendedShapeCoverage: - self.ExtendedShapeCoverage.subset(s.glyphs) - return True + if self.MathItalicsCorrectionInfo: + self.MathItalicsCorrectionInfo.subset_glyphs(s) + if self.MathTopAccentAttachment: + self.MathTopAccentAttachment.subset_glyphs(s) + if self.MathKernInfo: + self.MathKernInfo.subset_glyphs(s) + if self.ExtendedShapeCoverage: + self.ExtendedShapeCoverage.subset(s.glyphs) + return True @_add_method(otTables.MathVariants) def subset_glyphs(self, s): - if self.VertGlyphCoverage: - indices = self.VertGlyphCoverage.subset(s.glyphs) - self.VertGlyphConstruction = [self.VertGlyphConstruction[i] for i in indices] - self.VertGlyphCount = len(self.VertGlyphConstruction) - - if self.HorizGlyphCoverage: - indices = self.HorizGlyphCoverage.subset(s.glyphs) - self.HorizGlyphConstruction = [self.HorizGlyphConstruction[i] for i in indices] - self.HorizGlyphCount = len(self.HorizGlyphConstruction) + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.subset(s.glyphs) + self.VertGlyphConstruction = [self.VertGlyphConstruction[i] for i in indices] + self.VertGlyphCount = len(self.VertGlyphConstruction) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.subset(s.glyphs) + self.HorizGlyphConstruction = [self.HorizGlyphConstruction[i] for i in indices] + self.HorizGlyphCount = len(self.HorizGlyphConstruction) - return True + return True @_add_method(ttLib.getTableClass('MATH')) def subset_glyphs(self, s): - s.glyphs = s.glyphs_mathed - self.table.MathGlyphInfo.subset_glyphs(s) - self.table.MathVariants.subset_glyphs(s) - return True + s.glyphs = s.glyphs_mathed + self.table.MathGlyphInfo.subset_glyphs(s) + self.table.MathVariants.subset_glyphs(s) + return True @_add_method(ttLib.getTableModule('glyf').Glyph) def remapComponentsFast(self, indices): - if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: - return # Not composite - data = array.array("B", self.data) - i = 10 - more = 1 - while more: - flags =(data[i] << 8) | data[i+1] - glyphID =(data[i+2] << 8) | data[i+3] - # Remap - glyphID = indices.index(glyphID) - data[i+2] = glyphID >> 8 - data[i+3] = glyphID & 0xFF - i += 4 - flags = int(flags) - - if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS - else: i += 2 - if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE - elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE - elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO - more = flags & 0x0020 # MORE_COMPONENTS + if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: + return # Not composite + data = array.array("B", self.data) + i = 10 + more = 1 + while more: + flags =(data[i] << 8) | data[i+1] + glyphID =(data[i+2] << 8) | data[i+3] + # Remap + glyphID = indices.index(glyphID) + data[i+2] = glyphID >> 8 + data[i+3] = glyphID & 0xFF + i += 4 + flags = int(flags) + + if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS + else: i += 2 + if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE + elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE + elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO + more = flags & 0x0020 # MORE_COMPONENTS - self.data = data.tostring() + self.data = data.tostring() @_add_method(ttLib.getTableClass('glyf')) def closure_glyphs(self, s): - decompose = s.glyphs - while decompose: - components = set() - for g in decompose: - if g not in self.glyphs: - continue - gl = self.glyphs[g] - for c in gl.getComponentNames(self): - components.add(c) - components -= s.glyphs - s.glyphs.update(components) - decompose = components + glyphSet = self.glyphs + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + for c in gl.getComponentNames(self): + components.add(c) + components -= s.glyphs + s.glyphs.update(components) + decompose = components @_add_method(ttLib.getTableClass('glyf')) def prune_pre_subset(self, font, options): - if options.notdef_glyph and not options.notdef_outline: - g = self[self.glyphOrder[0]] - # Yay, easy! - g.__dict__.clear() - g.data = "" - return True + if options.notdef_glyph and not options.notdef_outline: + g = self[self.glyphOrder[0]] + # Yay, easy! + g.__dict__.clear() + g.data = "" + return True @_add_method(ttLib.getTableClass('glyf')) def subset_glyphs(self, s): - self.glyphs = _dict_subset(self.glyphs, s.glyphs) - indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] - for v in self.glyphs.values(): - if hasattr(v, "data"): - v.remapComponentsFast(indices) - else: - pass # No need - self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] - # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. - return True + self.glyphs = _dict_subset(self.glyphs, s.glyphs) + indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] + for v in self.glyphs.values(): + if hasattr(v, "data"): + v.remapComponentsFast(indices) + else: + pass # No need + self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] + # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. + return True @_add_method(ttLib.getTableClass('glyf')) -def prune_post_subset(self, options): - remove_hinting = not options.hinting - for v in self.glyphs.values(): - v.trim(remove_hinting=remove_hinting) - return True +def prune_post_subset(self, font, options): + remove_hinting = not options.hinting + for v in self.glyphs.values(): + v.trim(remove_hinting=remove_hinting) + return True + + +class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, components, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + self.components = components + + def op_endchar(self, index): + args = self.popall() + if len(args) >= 4: + from fontTools.encodings.StandardEncoding import StandardEncoding + # endchar can do seac accent bulding; The T2 spec says it's deprecated, + # but recent software that shall remain nameless does output it. + adx, ady, bchar, achar = args[-4:] + baseGlyph = StandardEncoding[bchar] + accentGlyph = StandardEncoding[achar] + self.components.add(baseGlyph) + self.components.add(accentGlyph) + +@_add_method(ttLib.getTableClass('CFF ')) +def closure_glyphs(self, s): + cff = self.cff + assert len(cff) == 1 + font = cff[cff.keys()[0]] + glyphSet = font.CharStrings + + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + + subrs = getattr(gl.private, "Subrs", []) + decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) + decompiler.execute(gl) + components -= s.glyphs + s.glyphs.update(components) + decompose = components @_add_method(ttLib.getTableClass('CFF ')) def prune_pre_subset(self, font, options): - cff = self.cff - # CFF table must have one font only - cff.fontNames = cff.fontNames[:1] - - if options.notdef_glyph and not options.notdef_outline: - for fontname in cff.keys(): - font = cff[fontname] - c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') - if hasattr(font, 'FDArray') and font.FDArray is not None: - private = font.FDArray[fdSelectIndex].Private - else: - private = font.Private - dfltWdX = private.defaultWidthX - nmnlWdX = private.nominalWidthX - pen = NullPen() - c.draw(pen) # this will set the charstring's width - if c.width != dfltWdX: - c.program = [c.width - nmnlWdX, 'endchar'] - else: - c.program = ['endchar'] - - # Clear useless Encoding - for fontname in cff.keys(): - font = cff[fontname] - # https://github.com/behdad/fonttools/issues/620 - font.Encoding = "StandardEncoding" + cff = self.cff + # CFF table must have one font only + cff.fontNames = cff.fontNames[:1] + + if options.notdef_glyph and not options.notdef_outline: + for fontname in cff.keys(): + font = cff[fontname] + c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') + if hasattr(font, 'FDArray') and font.FDArray is not None: + private = font.FDArray[fdSelectIndex].Private + else: + private = font.Private + dfltWdX = private.defaultWidthX + nmnlWdX = private.nominalWidthX + pen = NullPen() + c.draw(pen) # this will set the charstring's width + if c.width != dfltWdX: + c.program = [c.width - nmnlWdX, 'endchar'] + else: + c.program = ['endchar'] + + # Clear useless Encoding + for fontname in cff.keys(): + font = cff[fontname] + # https://github.com/behdad/fonttools/issues/620 + font.Encoding = "StandardEncoding" - return True # bool(cff.fontNames) + return True # bool(cff.fontNames) @_add_method(ttLib.getTableClass('CFF ')) def subset_glyphs(self, s): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Load all glyphs - for g in font.charset: - if g not in s.glyphs: continue - c, _ = cs.getItemAndSelector(g) - - if cs.charStringsAreIndexed: - indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] - csi = cs.charStringsIndex - csi.items = [csi.items[i] for i in indices] - del csi.file, csi.offsets - if hasattr(font, "FDSelect"): - sel = font.FDSelect - # XXX We want to set sel.format to None, such that the - # most compact format is selected. However, OTS was - # broken and couldn't parse a FDSelect format 0 that - # happened before CharStrings. As such, always force - # format 3 until we fix cffLib to always generate - # FDSelect after CharStrings. - # https://github.com/khaledhosny/ots/pull/31 - #sel.format = None - sel.format = 3 - sel.gidArray = [sel.gidArray[i] for i in indices] - cs.charStrings = {g:indices.index(v) - for g,v in cs.charStrings.items() - if g in s.glyphs} - else: - cs.charStrings = {g:v - for g,v in cs.charStrings.items() - if g in s.glyphs} - font.charset = [g for g in font.charset if g in s.glyphs] - font.numGlyphs = len(font.charset) + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Load all glyphs + for g in font.charset: + if g not in s.glyphs: continue + c, _ = cs.getItemAndSelector(g) + + if cs.charStringsAreIndexed: + indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] + csi = cs.charStringsIndex + csi.items = [csi.items[i] for i in indices] + del csi.file, csi.offsets + if hasattr(font, "FDSelect"): + sel = font.FDSelect + # XXX We want to set sel.format to None, such that the + # most compact format is selected. However, OTS was + # broken and couldn't parse a FDSelect format 0 that + # happened before CharStrings. As such, always force + # format 3 until we fix cffLib to always generate + # FDSelect after CharStrings. + # https://github.com/khaledhosny/ots/pull/31 + #sel.format = None + sel.format = 3 + sel.gidArray = [sel.gidArray[i] for i in indices] + cs.charStrings = {g:indices.index(v) + for g,v in cs.charStrings.items() + if g in s.glyphs} + else: + cs.charStrings = {g:v + for g,v in cs.charStrings.items() + if g in s.glyphs} + font.charset = [g for g in font.charset if g in s.glyphs] + font.numGlyphs = len(font.charset) - return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) + return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) @_add_method(psCharStrings.T2CharString) def subset_subroutines(self, subrs, gsubrs): - p = self.program - assert len(p) - for i in range(1, len(p)): - if p[i] == 'callsubr': - assert isinstance(p[i-1], int) - p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias - elif p[i] == 'callgsubr': - assert isinstance(p[i-1], int) - p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias + p = self.program + assert len(p) + for i in range(1, len(p)): + if p[i] == 'callsubr': + assert isinstance(p[i-1], int) + p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias + elif p[i] == 'callgsubr': + assert isinstance(p[i-1], int) + p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias @_add_method(psCharStrings.T2CharString) def drop_hints(self): - hints = self._hints + hints = self._hints - if hints.deletions: - p = self.program - for idx in reversed(hints.deletions): - del p[idx-2:idx] - - if hints.has_hint: - assert not hints.deletions or hints.last_hint <= hints.deletions[0] - self.program = self.program[hints.last_hint:] - if hasattr(self, 'width'): - # Insert width back if needed - if self.width != self.private.defaultWidthX: - self.program.insert(0, self.width - self.private.nominalWidthX) - - if hints.has_hintmask: - i = 0 - p = self.program - while i < len(p): - if p[i] in ['hintmask', 'cntrmask']: - assert i + 1 <= len(p) - del p[i:i+2] - continue - i += 1 + if hints.deletions: + p = self.program + for idx in reversed(hints.deletions): + del p[idx-2:idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + self.program = self.program[hints.last_hint:] + if hasattr(self, 'width'): + # Insert width back if needed + if self.width != self.private.defaultWidthX: + self.program.insert(0, self.width - self.private.nominalWidthX) + + if hints.has_hintmask: + i = 0 + p = self.program + while i < len(p): + if p[i] in ['hintmask', 'cntrmask']: + assert i + 1 <= len(p) + del p[i:i+2] + continue + i += 1 - assert len(self.program) + assert len(self.program) - del self._hints + del self._hints class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - for subrs in [localSubrs, globalSubrs]: - if subrs and not hasattr(subrs, "_used"): - subrs._used = set() - - def op_callsubr(self, index): - self.localSubrs._used.add(self.operandStack[-1]+self.localBias) - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - - def op_callgsubr(self, index): - self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1]+self.localBias) + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): - class Hints(object): - def __init__(self): - # Whether calling this charstring produces any hint stems - # Note that if a charstring starts with hintmask, it will - # have has_hint set to True, because it *might* produce an - # implicit vstem if called under certain conditions. - self.has_hint = False - # Index to start at to drop all hints - self.last_hint = 0 - # Index up to which we know more hints are possible. - # Only relevant if status is 0 or 1. - self.last_checked = 0 - # The status means: - # 0: after dropping hints, this charstring is empty - # 1: after dropping hints, there may be more hints - # continuing after this - # 2: no more hints possible after this charstring - self.status = 0 - # Has hintmask instructions; not recursive - self.has_hintmask = False - # List of indices of calls to empty subroutines to remove. - self.deletions = [] - pass - - def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): - self._css = css - psCharStrings.T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) - - def execute(self, charString): - old_hints = charString._hints if hasattr(charString, '_hints') else None - charString._hints = self.Hints() - - psCharStrings.T2WidthExtractor.execute(self, charString) - - hints = charString._hints - - if hints.has_hint or hints.has_hintmask: - self._css.add(charString) - - if hints.status != 2: - # Check from last_check, make sure we didn't have any operators. - for i in range(hints.last_checked, len(charString.program) - 1): - if isinstance(charString.program[i], str): - hints.status = 2 - break - else: - hints.status = 1 # There's *something* here - hints.last_checked = len(charString.program) - - if old_hints: - assert hints.__dict__ == old_hints.__dict__ - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.T2WidthExtractor.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.T2WidthExtractor.op_callgsubr(self, index) - self.processSubr(index, subr) - - def op_hstem(self, index): - psCharStrings.T2WidthExtractor.op_hstem(self, index) - self.processHint(index) - def op_vstem(self, index): - psCharStrings.T2WidthExtractor.op_vstem(self, index) - self.processHint(index) - def op_hstemhm(self, index): - psCharStrings.T2WidthExtractor.op_hstemhm(self, index) - self.processHint(index) - def op_vstemhm(self, index): - psCharStrings.T2WidthExtractor.op_vstemhm(self, index) - self.processHint(index) - def op_hintmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) - self.processHintmask(index) - return rv - def op_cntrmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) - self.processHintmask(index) - return rv - - def processHintmask(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hintmask = True - if hints.status != 2: - # Check from last_check, see if we may be an implicit vstem - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - else: - # We are an implicit vstem - hints.has_hint = True - hints.last_hint = index + 1 - hints.status = 0 - hints.last_checked = index + 1 - - def processHint(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hint = True - hints.last_hint = index - hints.last_checked = index - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - hints = cs._hints - subr_hints = subr._hints - - # Check from last_check, make sure we didn't have - # any operators. - if hints.status != 2: - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - hints.last_checked = index - - if hints.status != 2: - if subr_hints.has_hint: - hints.has_hint = True - - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - elif subr_hints.status == 0: - hints.deletions.append(index) + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + pass + + def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + self._css = css + psCharStrings.T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, '_hints') else None + charString._hints = self.Hints() + + psCharStrings.T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + psCharStrings.T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + def op_vstem(self, index): + psCharStrings.T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + def op_hstemhm(self, index): + psCharStrings.T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + def op_vstemhm(self, index): + psCharStrings.T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + def op_hintmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + def op_cntrmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + elif subr_hints.status == 0: + hints.deletions.append(index) - hints.status = max(hints.status, subr_hints.status) + hints.status = max(hints.status, subr_hints.status) class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - - def execute(self, charString): - # Note: Currently we recompute _desubroutinized each time. - # This is more robust in some cases, but in other places we assume - # that each subroutine always expands to the same code, so - # maybe it doesn't matter. To speed up we can just not - # recompute _desubroutinized if it's there. For now I just - # double-check that it desubroutinized to the same thing. - old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None - - charString._patches = [] - psCharStrings.SimpleT2Decompiler.execute(self, charString) - desubroutinized = charString.program[:] - for idx,expansion in reversed (charString._patches): - assert idx >= 2 - assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] - assert type(desubroutinized[idx - 2]) == int - if expansion[-1] == 'return': - expansion = expansion[:-1] - desubroutinized[idx-2:idx] = expansion - if 'endchar' in desubroutinized: - # Cut off after first endchar - desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] - else: - if not len(desubroutinized) or desubroutinized[-1] != 'return': - desubroutinized.append('return') - - charString._desubroutinized = desubroutinized - del charString._patches - - if old_desubroutinized: - assert desubroutinized == old_desubroutinized - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - self.processSubr(index, subr) - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - cs._patches.append((index, subr._desubroutinized)) + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + + def execute(self, charString): + # Note: Currently we recompute _desubroutinized each time. + # This is more robust in some cases, but in other places we assume + # that each subroutine always expands to the same code, so + # maybe it doesn't matter. To speed up we can just not + # recompute _desubroutinized if it's there. For now I just + # double-check that it desubroutinized to the same thing. + old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None + + charString._patches = [] + psCharStrings.SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx,expansion in reversed (charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == 'return': + expansion = expansion[:-1] + desubroutinized[idx-2:idx] = expansion + if 'endchar' in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] + else: + if not len(desubroutinized) or desubroutinized[-1] != 'return': + desubroutinized.append('return') + + charString._desubroutinized = desubroutinized + del charString._patches + + if old_desubroutinized: + assert desubroutinized == old_desubroutinized + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + cs._patches.append((index, subr._desubroutinized)) @_add_method(ttLib.getTableClass('CFF ')) -def prune_post_subset(self, options): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Drop unused FontDictionaries - if hasattr(font, "FDSelect"): - sel = font.FDSelect - indices = _uniq_sort(sel.gidArray) - sel.gidArray = [indices.index (ss) for ss in sel.gidArray] - arr = font.FDArray - arr.items = [arr[i] for i in indices] - del arr.file, arr.offsets - - # Desubroutinize if asked for - if options.desubroutinize: - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - c.program = c._desubroutinized - - # Drop hints if not needed - if not options.hinting: - - # This can be tricky, but doesn't have to. What we do is: - # - # - Run all used glyph charstrings and recurse into subroutines, - # - For each charstring (including subroutines), if it has any - # of the hint stem operators, we mark it as such. - # Upon returning, for each charstring we note all the - # subroutine calls it makes that (recursively) contain a stem, - # - Dropping hinting then consists of the following two ops: - # * Drop the piece of the program in each charstring before the - # last call to a stem op or a stem-calling subroutine, - # * Drop all hintmask operations. - # - It's trickier... A hintmask right after hints and a few numbers - # will act as an implicit vstemhm. As such, we track whether - # we have seen any non-hint operators so far and do the right - # thing, recursively... Good luck understanding that :( - css = set() - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, - c.private.nominalWidthX, - c.private.defaultWidthX) - decompiler.execute(c) - c.width = decompiler.width - for charstring in css: - charstring.drop_hints() - del css - - # Drop font-wide hinting values - all_privs = [] - if hasattr(font, 'FDSelect'): - all_privs.extend(fd.Private for fd in font.FDArray) - else: - all_privs.append(font.Private) - for priv in all_privs: - for k in ['BlueValues', 'OtherBlues', - 'FamilyBlues', 'FamilyOtherBlues', - 'BlueScale', 'BlueShift', 'BlueFuzz', - 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: - if hasattr(priv, k): - setattr(priv, k, None) - - # Renumber subroutines to remove unused ones - - # Mark all used subroutines - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - - all_subrs = [font.GlobalSubrs] - if hasattr(font, 'FDSelect'): - all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) - elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: - all_subrs.append(font.Private.Subrs) - - subrs = set(subrs) # Remove duplicates - - # Prepare - for subrs in all_subrs: - if not hasattr(subrs, '_used'): - subrs._used = set() - subrs._used = _uniq_sort(subrs._used) - subrs._old_bias = psCharStrings.calcSubrBias(subrs) - subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) - - # Renumber glyph charstrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - c.subset_subroutines (subrs, font.GlobalSubrs) - - # Renumber subroutines themselves - for subrs in all_subrs: - if subrs == font.GlobalSubrs: - if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): - local_subrs = font.Private.Subrs - else: - local_subrs = [] - else: - local_subrs = subrs - - subrs.items = [subrs.items[i] for i in subrs._used] - if hasattr(subrs, 'file'): - del subrs.file - if hasattr(subrs, 'offsets'): - del subrs.offsets - - for subr in subrs.items: - subr.subset_subroutines (local_subrs, font.GlobalSubrs) - - # Delete local SubrsIndex if empty - if hasattr(font, 'FDSelect'): - for fd in font.FDArray: - _delete_empty_subrs(fd.Private) - else: - _delete_empty_subrs(font.Private) - - # Cleanup - for subrs in all_subrs: - del subrs._used, subrs._old_bias, subrs._new_bias +def prune_post_subset(self, font, options): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Drop unused FontDictionaries + if hasattr(font, "FDSelect"): + sel = font.FDSelect + indices = _uniq_sort(sel.gidArray) + sel.gidArray = [indices.index (ss) for ss in sel.gidArray] + arr = font.FDArray + arr.items = [arr[i] for i in indices] + del arr.file, arr.offsets + + # Desubroutinize if asked for + if options.desubroutinize: + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + c.program = c._desubroutinized + + # Drop hints if not needed + if not options.hinting: + + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + charstring.drop_hints() + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, 'FDSelect'): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in ['BlueValues', 'OtherBlues', + 'FamilyBlues', 'FamilyOtherBlues', + 'BlueScale', 'BlueShift', 'BlueFuzz', + 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: + if hasattr(priv, k): + setattr(priv, k, None) + + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, 'FDSelect'): + all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) + elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, '_used'): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = psCharStrings.calcSubrBias(subrs) + subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + c.subset_subroutines (subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): + local_subrs = font.Private.Subrs + else: + local_subrs = [] + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, 'file'): + del subrs.file + if hasattr(subrs, 'offsets'): + del subrs.offsets + + for subr in subrs.items: + subr.subset_subroutines (local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, 'FDSelect'): + for fd in font.FDArray: + _delete_empty_subrs(fd.Private) + else: + _delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias - return True + return True def _delete_empty_subrs(private_dict): - if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: - if 'Subrs' in private_dict.rawDict: - del private_dict.rawDict['Subrs'] - del private_dict.Subrs + if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: + if 'Subrs' in private_dict.rawDict: + del private_dict.rawDict['Subrs'] + del private_dict.Subrs @_add_method(ttLib.getTableClass('cmap')) def closure_glyphs(self, s): - tables = [t for t in self.tables if t.isUnicode()] + tables = [t for t in self.tables if t.isUnicode()] - # Close glyphs - for table in tables: - if table.format == 14: - for cmap in table.uvsDict.values(): - glyphs = {g for u,g in cmap if u in s.unicodes_requested} - if None in glyphs: - glyphs.remove(None) - s.glyphs.update(glyphs) - else: - cmap = table.cmap - intersection = s.unicodes_requested.intersection(cmap.keys()) - s.glyphs.update(cmap[u] for u in intersection) - - # Calculate unicodes_missing - s.unicodes_missing = s.unicodes_requested.copy() - for table in tables: - s.unicodes_missing.difference_update(table.cmap) + # Close glyphs + for table in tables: + if table.format == 14: + for cmap in table.uvsDict.values(): + glyphs = {g for u,g in cmap if u in s.unicodes_requested} + if None in glyphs: + glyphs.remove(None) + s.glyphs.update(glyphs) + else: + cmap = table.cmap + intersection = s.unicodes_requested.intersection(cmap.keys()) + s.glyphs.update(cmap[u] for u in intersection) + + # Calculate unicodes_missing + s.unicodes_missing = s.unicodes_requested.copy() + for table in tables: + s.unicodes_missing.difference_update(table.cmap) @_add_method(ttLib.getTableClass('cmap')) def prune_pre_subset(self, font, options): - if not options.legacy_cmap: - # Drop non-Unicode / non-Symbol cmaps - self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] - if not options.symbol_cmap: - self.tables = [t for t in self.tables if not t.isSymbol()] - # TODO(behdad) Only keep one subtable? - # For now, drop format=0 which can't be subset_glyphs easily? - self.tables = [t for t in self.tables if t.format != 0] - self.numSubTables = len(self.tables) - return True # Required table + if not options.legacy_cmap: + # Drop non-Unicode / non-Symbol cmaps + self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] + if not options.symbol_cmap: + self.tables = [t for t in self.tables if not t.isSymbol()] + # TODO(behdad) Only keep one subtable? + # For now, drop format=0 which can't be subset_glyphs easily? + self.tables = [t for t in self.tables if t.format != 0] + self.numSubTables = len(self.tables) + return True # Required table @_add_method(ttLib.getTableClass('cmap')) def subset_glyphs(self, s): - s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only - for t in self.tables: - if t.format == 14: - # TODO(behdad) We drop all the default-UVS mappings - # for glyphs_requested. So it's the caller's responsibility to make - # sure those are included. - t.uvsDict = {v:[(u,g) for u,g in l - if g in s.glyphs_requested or u in s.unicodes_requested] - for v,l in t.uvsDict.items()} - t.uvsDict = {v:l for v,l in t.uvsDict.items() if l} - elif t.isUnicode(): - t.cmap = {u:g for u,g in t.cmap.items() - if g in s.glyphs_requested or u in s.unicodes_requested} - else: - t.cmap = {u:g for u,g in t.cmap.items() - if g in s.glyphs_requested} - self.tables = [t for t in self.tables - if (t.cmap if t.format != 14 else t.uvsDict)] - self.numSubTables = len(self.tables) - # TODO(behdad) Convert formats when needed. - # In particular, if we have a format=12 without non-BMP - # characters, either drop format=12 one or convert it - # to format=4 if there's not one. - return True # Required table + s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only + for t in self.tables: + if t.format == 14: + # TODO(behdad) We drop all the default-UVS mappings + # for glyphs_requested. So it's the caller's responsibility to make + # sure those are included. + t.uvsDict = {v:[(u,g) for u,g in l + if g in s.glyphs_requested or u in s.unicodes_requested] + for v,l in t.uvsDict.items()} + t.uvsDict = {v:l for v,l in t.uvsDict.items() if l} + elif t.isUnicode(): + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested or u in s.unicodes_requested} + else: + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested} + self.tables = [t for t in self.tables + if (t.cmap if t.format != 14 else t.uvsDict)] + self.numSubTables = len(self.tables) + # TODO(behdad) Convert formats when needed. + # In particular, if we have a format=12 without non-BMP + # characters, either drop format=12 one or convert it + # to format=4 if there's not one. + return True # Required table @_add_method(ttLib.getTableClass('DSIG')) def prune_pre_subset(self, font, options): - # Drop all signatures since they will be invalid - self.usNumSigs = 0 - self.signatureRecords = [] - return True + # Drop all signatures since they will be invalid + self.usNumSigs = 0 + self.signatureRecords = [] + return True @_add_method(ttLib.getTableClass('maxp')) def prune_pre_subset(self, font, options): - if not options.hinting: - if self.tableVersion == 0x00010000: - self.maxZones = 1 - self.maxTwilightPoints = 0 - self.maxStorage = 0 - self.maxFunctionDefs = 0 - self.maxInstructionDefs = 0 - self.maxStackElements = 0 - self.maxSizeOfInstructions = 0 - return True + if not options.hinting: + if self.tableVersion == 0x00010000: + self.maxZones = 1 + self.maxTwilightPoints = 0 + self.maxStorage = 0 + self.maxFunctionDefs = 0 + self.maxInstructionDefs = 0 + self.maxStackElements = 0 + self.maxSizeOfInstructions = 0 + return True @_add_method(ttLib.getTableClass('name')) def prune_pre_subset(self, font, options): - nameIDs = set(options.name_IDs) - fvar = font.get('fvar') - if fvar: - nameIDs.update([axis.axisNameID for axis in fvar.axes]) - nameIDs.update([inst.subfamilyNameID for inst in fvar.instances]) - nameIDs.update([inst.postscriptNameID for inst in fvar.instances - if inst.postscriptNameID != 0xFFFF]) - if '*' not in options.name_IDs: - self.names = [n for n in self.names if n.nameID in nameIDs] - if not options.name_legacy: - # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman - # entry for Latin and no Unicode names. - self.names = [n for n in self.names if n.isUnicode()] - # TODO(behdad) Option to keep only one platform's - if '*' not in options.name_languages: - # TODO(behdad) This is Windows-platform specific! - self.names = [n for n in self.names - if n.langID in options.name_languages] - if options.obfuscate_names: - namerecs = [] - for n in self.names: - if n.nameID in [1, 4]: - n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f" - elif n.nameID in [2, 6]: - n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f" - elif n.nameID == 3: - n.string = "" - elif n.nameID in [16, 17, 18]: - continue - namerecs.append(n) - self.names = namerecs - return True # Required table + nameIDs = set(options.name_IDs) + fvar = font.get('fvar') + if fvar: + nameIDs.update([axis.axisNameID for axis in fvar.axes]) + nameIDs.update([inst.subfamilyNameID for inst in fvar.instances]) + nameIDs.update([inst.postscriptNameID for inst in fvar.instances + if inst.postscriptNameID != 0xFFFF]) + if '*' not in options.name_IDs: + self.names = [n for n in self.names if n.nameID in nameIDs] + if not options.name_legacy: + # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman + # entry for Latin and no Unicode names. + self.names = [n for n in self.names if n.isUnicode()] + # TODO(behdad) Option to keep only one platform's + if '*' not in options.name_languages: + # TODO(behdad) This is Windows-platform specific! + self.names = [n for n in self.names + if n.langID in options.name_languages] + if options.obfuscate_names: + namerecs = [] + for n in self.names: + if n.nameID in [1, 4]: + n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f" + elif n.nameID in [2, 6]: + n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f" + elif n.nameID == 3: + n.string = "" + elif n.nameID in [16, 17, 18]: + continue + namerecs.append(n) + self.names = namerecs + return True # Required table # TODO(behdad) OS/2 ulCodePageRange? @@ -2561,586 +2711,608 @@ class Options(object): - class OptionError(Exception): pass - class UnknownOptionError(OptionError): pass + class OptionError(Exception): pass + class UnknownOptionError(OptionError): pass - # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser - _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', - 'EBSC', 'SVG', 'PCLT', 'LTSH'] - _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite - _drop_tables_default += ['sbix'] # Color - _no_subset_tables_default = ['avar', 'fvar', - 'gasp', 'head', 'hhea', 'maxp', - 'vhea', 'OS/2', 'loca', 'name', 'cvt', - 'fpgm', 'prep', 'VDMX', 'DSIG', 'CPAL', - 'MVAR', 'STAT'] - _hinting_tables_default = ['cvar', 'cvt', 'fpgm', 'prep', 'hdmx', 'VDMX'] - - # Based on HarfBuzz shapers - _layout_features_groups = { - # Default shaper - 'common': ['rvrn', 'ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], - 'fractions': ['frac', 'numr', 'dnom'], - 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], - 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], - 'ltr': ['ltra', 'ltrm'], - 'rtl': ['rtla', 'rtlm'], - # Complex shapers - 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', - 'cswh', 'mset', 'stch'], - 'hangul': ['ljmo', 'vjmo', 'tjmo'], - 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], - 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', - 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', - 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], - } - _layout_features_default = _uniq_sort(sum( - iter(_layout_features_groups.values()), [])) - - def __init__(self, **kwargs): - - self.drop_tables = self._drop_tables_default[:] - self.no_subset_tables = self._no_subset_tables_default[:] - self.passthrough_tables = False # keep/drop tables we can't subset - self.hinting_tables = self._hinting_tables_default[:] - self.legacy_kern = False # drop 'kern' table if GPOS available - self.layout_features = self._layout_features_default[:] - self.ignore_missing_glyphs = False - self.ignore_missing_unicodes = True - self.hinting = True - self.glyph_names = False - self.legacy_cmap = False - self.symbol_cmap = False - self.name_IDs = [1, 2] # Family and Style - self.name_legacy = False - self.name_languages = [0x0409] # English - self.obfuscate_names = False # to make webfont unusable as a system font - self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF - self.notdef_outline = False # No need for notdef to have an outline really - self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType - self.recalc_bounds = False # Recalculate font bounding boxes - self.recalc_timestamp = False # Recalculate font modified timestamp - self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits - self.recalc_average_width = False # update 'xAvgCharWidth' - self.canonical_order = None # Order tables as recommended - self.flavor = None # May be 'woff' or 'woff2' - self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 - self.desubroutinize = False # Desubroutinize CFF CharStrings - self.verbose = False - self.timing = False - self.xml = False - - self.set(**kwargs) - - def set(self, **kwargs): - for k,v in kwargs.items(): - if not hasattr(self, k): - raise self.UnknownOptionError("Unknown option '%s'" % k) - setattr(self, k, v) - - def parse_opts(self, argv, ignore_unknown=[]): - posargs = [] - passthru_options = [] - for a in argv: - orig_a = a - if not a.startswith('--'): - posargs.append(a) - continue - a = a[2:] - i = a.find('=') - op = '=' - if i == -1: - if a.startswith("no-"): - k = a[3:] - if k == "canonical-order": - # reorderTables=None is faster than False (the latter - # still reorders to "keep" the original table order) - v = None - else: - v = False - else: - k = a - v = True - if k.endswith("?"): - k = k[:-1] - v = '?' - else: - k = a[:i] - if k[-1] in "-+": - op = k[-1]+'=' # Op is '-=' or '+=' now. - k = k[:-1] - v = a[i+1:] - ok = k - k = k.replace('-', '_') - if not hasattr(self, k): - if ignore_unknown is True or ok in ignore_unknown: - passthru_options.append(orig_a) - continue - else: - raise self.UnknownOptionError("Unknown option '%s'" % a) - - ov = getattr(self, k) - if v == '?': - print("Current setting for '%s' is: %s" % (ok, ov)) - continue - if isinstance(ov, bool): - v = bool(v) - elif isinstance(ov, int): - v = int(v) - elif isinstance(ov, str): - v = str(v) # redundant - elif isinstance(ov, list): - if isinstance(v, bool): - raise self.OptionError("Option '%s' requires values to be specified using '='" % a) - vv = v.replace(',', ' ').split() - if vv == ['']: - vv = [] - vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] - if op == '=': - v = vv - elif op == '+=': - v = ov - v.extend(vv) - elif op == '-=': - v = ov - for x in vv: - if x in v: - v.remove(x) - else: - assert False + # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser + _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', + 'EBSC', 'SVG', 'PCLT', 'LTSH'] + _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite + _drop_tables_default += ['sbix'] # Color + _no_subset_tables_default = ['avar', 'fvar', + 'gasp', 'head', 'hhea', 'maxp', + 'vhea', 'OS/2', 'loca', 'name', 'cvt', + 'fpgm', 'prep', 'VDMX', 'DSIG', 'CPAL', + 'MVAR', 'cvar', 'STAT'] + _hinting_tables_default = ['cvt', 'cvar', 'fpgm', 'prep', 'hdmx', 'VDMX'] + + # Based on HarfBuzz shapers + _layout_features_groups = { + # Default shaper + 'common': ['rvrn', 'ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], + 'fractions': ['frac', 'numr', 'dnom'], + 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], + 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], + 'ltr': ['ltra', 'ltrm'], + 'rtl': ['rtla', 'rtlm'], + # Complex shapers + 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', + 'cswh', 'mset', 'stch'], + 'hangul': ['ljmo', 'vjmo', 'tjmo'], + 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], + 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', + 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', + 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], + } + _layout_features_default = _uniq_sort(sum( + iter(_layout_features_groups.values()), [])) + + def __init__(self, **kwargs): + + self.drop_tables = self._drop_tables_default[:] + self.no_subset_tables = self._no_subset_tables_default[:] + self.passthrough_tables = False # keep/drop tables we can't subset + self.hinting_tables = self._hinting_tables_default[:] + self.legacy_kern = False # drop 'kern' table if GPOS available + self.layout_features = self._layout_features_default[:] + self.ignore_missing_glyphs = False + self.ignore_missing_unicodes = True + self.hinting = True + self.glyph_names = False + self.legacy_cmap = False + self.symbol_cmap = False + self.name_IDs = [0, 1, 2, 3, 4, 5, 6] # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225 + self.name_legacy = False + self.name_languages = [0x0409] # English + self.obfuscate_names = False # to make webfont unusable as a system font + self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF + self.notdef_outline = False # No need for notdef to have an outline really + self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType + self.recalc_bounds = False # Recalculate font bounding boxes + self.recalc_timestamp = False # Recalculate font modified timestamp + self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits + self.recalc_average_width = False # update 'xAvgCharWidth' + self.canonical_order = None # Order tables as recommended + self.flavor = None # May be 'woff' or 'woff2' + self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 + self.desubroutinize = False # Desubroutinize CFF CharStrings + self.verbose = False + self.timing = False + self.xml = False + self.font_number = -1 + + self.set(**kwargs) + + def set(self, **kwargs): + for k,v in kwargs.items(): + if not hasattr(self, k): + raise self.UnknownOptionError("Unknown option '%s'" % k) + setattr(self, k, v) + + def parse_opts(self, argv, ignore_unknown=[]): + posargs = [] + passthru_options = [] + for a in argv: + orig_a = a + if not a.startswith('--'): + posargs.append(a) + continue + a = a[2:] + i = a.find('=') + op = '=' + if i == -1: + if a.startswith("no-"): + k = a[3:] + if k == "canonical-order": + # reorderTables=None is faster than False (the latter + # still reorders to "keep" the original table order) + v = None + else: + v = False + else: + k = a + v = True + if k.endswith("?"): + k = k[:-1] + v = '?' + else: + k = a[:i] + if k[-1] in "-+": + op = k[-1]+'=' # Op is '-=' or '+=' now. + k = k[:-1] + v = a[i+1:] + ok = k + k = k.replace('-', '_') + if not hasattr(self, k): + if ignore_unknown is True or ok in ignore_unknown: + passthru_options.append(orig_a) + continue + else: + raise self.UnknownOptionError("Unknown option '%s'" % a) + + ov = getattr(self, k) + if v == '?': + print("Current setting for '%s' is: %s" % (ok, ov)) + continue + if isinstance(ov, bool): + v = bool(v) + elif isinstance(ov, int): + v = int(v) + elif isinstance(ov, str): + v = str(v) # redundant + elif isinstance(ov, list): + if isinstance(v, bool): + raise self.OptionError("Option '%s' requires values to be specified using '='" % a) + vv = v.replace(',', ' ').split() + if vv == ['']: + vv = [] + vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] + if op == '=': + v = vv + elif op == '+=': + v = ov + v.extend(vv) + elif op == '-=': + v = ov + for x in vv: + if x in v: + v.remove(x) + else: + assert False - setattr(self, k, v) + setattr(self, k, v) - return posargs + passthru_options + return posargs + passthru_options class Subsetter(object): - class SubsettingError(Exception): pass - class MissingGlyphsSubsettingError(SubsettingError): pass - class MissingUnicodesSubsettingError(SubsettingError): pass - - def __init__(self, options=None): - - if not options: - options = Options() - - self.options = options - self.unicodes_requested = set() - self.glyph_names_requested = set() - self.glyph_ids_requested = set() - - def populate(self, glyphs=[], gids=[], unicodes=[], text=""): - self.unicodes_requested.update(unicodes) - if isinstance(text, bytes): - text = text.decode("utf_8") - text_utf32 = text.encode("utf-32-be") - nchars = len(text_utf32)//4 - for u in struct.unpack('>%dL' % nchars, text_utf32): - self.unicodes_requested.add(u) - self.glyph_names_requested.update(glyphs) - self.glyph_ids_requested.update(gids) - - def _prune_pre_subset(self, font): - for tag in self._sort_tables(font): - if(tag.strip() in self.options.drop_tables or - (tag.strip() in self.options.hinting_tables and not self.options.hinting) or - (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))): - log.info("%s dropped", tag) - del font[tag] - continue - - clazz = ttLib.getTableClass(tag) - - if hasattr(clazz, 'prune_pre_subset'): - with timer("load '%s'" % tag): - table = font[tag] - with timer("prune '%s'" % tag): - retain = table.prune_pre_subset(font, self.options) - if not retain: - log.info("%s pruned to empty; dropped", tag) - del font[tag] - continue - else: - log.info("%s pruned", tag) - - def _closure_glyphs(self, font): - - realGlyphs = set(font.getGlyphOrder()) - glyph_order = font.getGlyphOrder() - - self.glyphs_requested = set() - self.glyphs_requested.update(self.glyph_names_requested) - self.glyphs_requested.update(glyph_order[i] - for i in self.glyph_ids_requested - if i < len(glyph_order)) - - self.glyphs_missing = set() - self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) - self.glyphs_missing.update(i for i in self.glyph_ids_requested - if i >= len(glyph_order)) - if self.glyphs_missing: - log.info("Missing requested glyphs: %s", self.glyphs_missing) - if not self.options.ignore_missing_glyphs: - raise self.MissingGlyphsSubsettingError(self.glyphs_missing) - - self.glyphs = self.glyphs_requested.copy() - - self.unicodes_missing = set() - if 'cmap' in font: - with timer("close glyph list over 'cmap'"): - font['cmap'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - self.glyphs_cmaped = frozenset(self.glyphs) - if self.unicodes_missing: - missing = ["U+%04X" % u for u in self.unicodes_missing] - log.info("Missing glyphs for requested Unicodes: %s", missing) - if not self.options.ignore_missing_unicodes: - raise self.MissingUnicodesSubsettingError(missing) - del missing - - if self.options.notdef_glyph: - if 'glyf' in font: - self.glyphs.add(font.getGlyphName(0)) - log.info("Added gid0 to subset") - else: - self.glyphs.add('.notdef') - log.info("Added .notdef to subset") - if self.options.recommended_glyphs: - if 'glyf' in font: - for i in range(min(4, len(font.getGlyphOrder()))): - self.glyphs.add(font.getGlyphName(i)) - log.info("Added first four glyphs to subset") - - if 'GSUB' in font: - with timer("close glyph list over 'GSUB'"): - log.info("Closing glyph list over 'GSUB': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['GSUB'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'GSUB': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_gsubed = frozenset(self.glyphs) - - if 'MATH' in font: - with timer("close glyph list over 'MATH'"): - log.info("Closing glyph list over 'MATH': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['MATH'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'MATH': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_mathed = frozenset(self.glyphs) - - for table in ('COLR', 'bsln'): - if table in font: - with timer("close glyph list over '%s'" % table): - log.info("Closing glyph list over '%s': %d glyphs before", - table, len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font[table].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over '%s': %d glyphs after", - table, len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - - if 'glyf' in font: - with timer("close glyph list over 'glyf'"): - log.info("Closing glyph list over 'glyf': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['glyf'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'glyf': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_glyfed = frozenset(self.glyphs) - - self.glyphs_all = frozenset(self.glyphs) - - log.info("Retaining %d glyphs", len(self.glyphs_all)) - - del self.glyphs - - def _subset_glyphs(self, font): - for tag in self._sort_tables(font): - clazz = ttLib.getTableClass(tag) - - if tag.strip() in self.options.no_subset_tables: - log.info("%s subsetting not needed", tag) - elif hasattr(clazz, 'subset_glyphs'): - with timer("subset '%s'" % tag): - table = font[tag] - self.glyphs = self.glyphs_all - retain = table.subset_glyphs(self) - del self.glyphs - if not retain: - log.info("%s subsetted to empty; dropped", tag) - del font[tag] - else: - log.info("%s subsetted", tag) - elif self.options.passthrough_tables: - log.info("%s NOT subset; don't know how to subset", tag) - else: - log.info("%s NOT subset; don't know how to subset; dropped", tag) - del font[tag] - - with timer("subset GlyphOrder"): - glyphOrder = font.getGlyphOrder() - glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] - font.setGlyphOrder(glyphOrder) - font._buildReverseGlyphOrderDict() - - def _prune_post_subset(self, font): - for tag in font.keys(): - if tag == 'GlyphOrder': continue - if tag == 'OS/2' and self.options.prune_unicode_ranges: - old_uniranges = font[tag].getUnicodeRanges() - new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) - if old_uniranges != new_uniranges: - log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)) - if self.options.recalc_average_width: - widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0] - avg_width = round(sum(widths) / len(widths)) - if avg_width != font[tag].xAvgCharWidth: - font[tag].xAvgCharWidth = avg_width - log.info("%s xAvgCharWidth updated: %d", tag, avg_width) - clazz = ttLib.getTableClass(tag) - if hasattr(clazz, 'prune_post_subset'): - with timer("prune '%s'" % tag): - table = font[tag] - retain = table.prune_post_subset(self.options) - if not retain: - log.info("%s pruned to empty; dropped", tag) - del font[tag] - else: - log.info("%s pruned", tag) - - def _sort_tables(self, font): - tagOrder = ['fvar', 'avar', 'gvar', 'name', 'glyf'] - tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} - tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) - return [t for t in tags if t != 'GlyphOrder'] - - def subset(self, font): - self._prune_pre_subset(font) - self._closure_glyphs(font) - self._subset_glyphs(font) - self._prune_post_subset(font) + class SubsettingError(Exception): pass + class MissingGlyphsSubsettingError(SubsettingError): pass + class MissingUnicodesSubsettingError(SubsettingError): pass + + def __init__(self, options=None): + + if not options: + options = Options() + + self.options = options + self.unicodes_requested = set() + self.glyph_names_requested = set() + self.glyph_ids_requested = set() + + def populate(self, glyphs=[], gids=[], unicodes=[], text=""): + self.unicodes_requested.update(unicodes) + if isinstance(text, bytes): + text = text.decode("utf_8") + text_utf32 = text.encode("utf-32-be") + nchars = len(text_utf32)//4 + for u in struct.unpack('>%dL' % nchars, text_utf32): + self.unicodes_requested.add(u) + self.glyph_names_requested.update(glyphs) + self.glyph_ids_requested.update(gids) + + def _prune_pre_subset(self, font): + for tag in self._sort_tables(font): + if (tag.strip() in self.options.drop_tables or + (tag.strip() in self.options.hinting_tables and not self.options.hinting) or + (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))): + log.info("%s dropped", tag) + del font[tag] + continue + + clazz = ttLib.getTableClass(tag) + + if hasattr(clazz, 'prune_pre_subset'): + with timer("load '%s'" % tag): + table = font[tag] + with timer("prune '%s'" % tag): + retain = table.prune_pre_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + continue + else: + log.info("%s pruned", tag) + + def _closure_glyphs(self, font): + + realGlyphs = set(font.getGlyphOrder()) + glyph_order = font.getGlyphOrder() + + self.glyphs_requested = set() + self.glyphs_requested.update(self.glyph_names_requested) + self.glyphs_requested.update(glyph_order[i] + for i in self.glyph_ids_requested + if i < len(glyph_order)) + + self.glyphs_missing = set() + self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) + self.glyphs_missing.update(i for i in self.glyph_ids_requested + if i >= len(glyph_order)) + if self.glyphs_missing: + log.info("Missing requested glyphs: %s", self.glyphs_missing) + if not self.options.ignore_missing_glyphs: + raise self.MissingGlyphsSubsettingError(self.glyphs_missing) + + self.glyphs = self.glyphs_requested.copy() + + self.unicodes_missing = set() + if 'cmap' in font: + with timer("close glyph list over 'cmap'"): + font['cmap'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + self.glyphs_cmaped = frozenset(self.glyphs) + if self.unicodes_missing: + missing = ["U+%04X" % u for u in self.unicodes_missing] + log.info("Missing glyphs for requested Unicodes: %s", missing) + if not self.options.ignore_missing_unicodes: + raise self.MissingUnicodesSubsettingError(missing) + del missing + + if self.options.notdef_glyph: + if 'glyf' in font: + self.glyphs.add(font.getGlyphName(0)) + log.info("Added gid0 to subset") + else: + self.glyphs.add('.notdef') + log.info("Added .notdef to subset") + if self.options.recommended_glyphs: + if 'glyf' in font: + for i in range(min(4, len(font.getGlyphOrder()))): + self.glyphs.add(font.getGlyphName(i)) + log.info("Added first four glyphs to subset") + + if 'GSUB' in font: + with timer("close glyph list over 'GSUB'"): + log.info("Closing glyph list over 'GSUB': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['GSUB'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'GSUB': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_gsubed = frozenset(self.glyphs) + + if 'MATH' in font: + with timer("close glyph list over 'MATH'"): + log.info("Closing glyph list over 'MATH': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['MATH'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'MATH': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_mathed = frozenset(self.glyphs) + + for table in ('COLR', 'bsln'): + if table in font: + with timer("close glyph list over '%s'" % table): + log.info("Closing glyph list over '%s': %d glyphs before", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font[table].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over '%s': %d glyphs after", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + + if 'glyf' in font: + with timer("close glyph list over 'glyf'"): + log.info("Closing glyph list over 'glyf': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['glyf'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'glyf': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_glyfed = frozenset(self.glyphs) + + if 'CFF ' in font: + with timer("close glyph list over 'CFF '"): + log.info("Closing glyph list over 'CFF ': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['CFF '].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'CFF ': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_cffed = frozenset(self.glyphs) + + self.glyphs_all = frozenset(self.glyphs) + + order = font.getReverseGlyphMap() + self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_all} + + log.info("Retaining %d glyphs", len(self.glyphs_all)) + + del self.glyphs + + def _subset_glyphs(self, font): + for tag in self._sort_tables(font): + clazz = ttLib.getTableClass(tag) + + if tag.strip() in self.options.no_subset_tables: + log.info("%s subsetting not needed", tag) + elif hasattr(clazz, 'subset_glyphs'): + with timer("subset '%s'" % tag): + table = font[tag] + self.glyphs = self.glyphs_all + retain = table.subset_glyphs(self) + del self.glyphs + if not retain: + log.info("%s subsetted to empty; dropped", tag) + del font[tag] + else: + log.info("%s subsetted", tag) + elif self.options.passthrough_tables: + log.info("%s NOT subset; don't know how to subset", tag) + else: + log.warning("%s NOT subset; don't know how to subset; dropped", tag) + del font[tag] + + with timer("subset GlyphOrder"): + glyphOrder = font.getGlyphOrder() + glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] + font.setGlyphOrder(glyphOrder) + font._buildReverseGlyphOrderDict() + + def _prune_post_subset(self, font): + for tag in font.keys(): + if tag == 'GlyphOrder': continue + if tag == 'OS/2' and self.options.prune_unicode_ranges: + old_uniranges = font[tag].getUnicodeRanges() + new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) + if old_uniranges != new_uniranges: + log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)) + if self.options.recalc_average_width: + widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0] + avg_width = otRound(sum(widths) / len(widths)) + if avg_width != font[tag].xAvgCharWidth: + font[tag].xAvgCharWidth = avg_width + log.info("%s xAvgCharWidth updated: %d", tag, avg_width) + clazz = ttLib.getTableClass(tag) + if hasattr(clazz, 'prune_post_subset'): + with timer("prune '%s'" % tag): + table = font[tag] + retain = table.prune_post_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + else: + log.info("%s pruned", tag) + + def _sort_tables(self, font): + tagOrder = ['fvar', 'avar', 'gvar', 'name', 'glyf'] + tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} + tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) + return [t for t in tags if t != 'GlyphOrder'] + + def subset(self, font): + self._prune_pre_subset(font) + self._closure_glyphs(font) + self._subset_glyphs(font) + self._prune_post_subset(font) @timer("load font") def load_font(fontFile, - options, - allowVID=False, - checkChecksums=False, - dontLoadGlyphNames=False, - lazy=True): - - font = ttLib.TTFont(fontFile, - allowVID=allowVID, - checkChecksums=checkChecksums, - recalcBBoxes=options.recalc_bounds, - recalcTimestamp=options.recalc_timestamp, - lazy=lazy) - - # Hack: - # - # If we don't need glyph names, change 'post' class to not try to - # load them. It avoid lots of headache with broken fonts as well - # as loading time. - # - # Ideally ttLib should provide a way to ask it to skip loading - # glyph names. But it currently doesn't provide such a thing. - # - if dontLoadGlyphNames: - post = ttLib.getTableClass('post') - saved = post.decode_format_2_0 - post.decode_format_2_0 = post.decode_format_3_0 - f = font['post'] - if f.formatType == 2.0: - f.formatType = 3.0 - post.decode_format_2_0 = saved + options, + allowVID=False, + checkChecksums=False, + dontLoadGlyphNames=False, + lazy=True): + + font = ttLib.TTFont(fontFile, + allowVID=allowVID, + checkChecksums=checkChecksums, + recalcBBoxes=options.recalc_bounds, + recalcTimestamp=options.recalc_timestamp, + lazy=lazy, + fontNumber=options.font_number) + + # Hack: + # + # If we don't need glyph names, change 'post' class to not try to + # load them. It avoid lots of headache with broken fonts as well + # as loading time. + # + # Ideally ttLib should provide a way to ask it to skip loading + # glyph names. But it currently doesn't provide such a thing. + # + if dontLoadGlyphNames: + post = ttLib.getTableClass('post') + saved = post.decode_format_2_0 + post.decode_format_2_0 = post.decode_format_3_0 + f = font['post'] + if f.formatType == 2.0: + f.formatType = 3.0 + post.decode_format_2_0 = saved - return font + return font @timer("compile and save font") def save_font(font, outfile, options): - if options.flavor and not hasattr(font, 'flavor'): - raise Exception("fonttools version does not support flavors.") - if options.with_zopfli and options.flavor == "woff": - from fontTools.ttLib import sfnt - sfnt.USE_ZOPFLI = True - font.flavor = options.flavor - font.save(outfile, reorderTables=options.canonical_order) + if options.with_zopfli and options.flavor == "woff": + from fontTools.ttLib import sfnt + sfnt.USE_ZOPFLI = True + font.flavor = options.flavor + font.save(outfile, reorderTables=options.canonical_order) def parse_unicodes(s): - import re - s = re.sub (r"0[xX]", " ", s) - s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s) - l = [] - for item in s.split(): - fields = item.split('-') - if len(fields) == 1: - l.append(int(item, 16)) - else: - start,end = fields - l.extend(range(int(start, 16), int(end, 16)+1)) - return l + import re + s = re.sub (r"0[xX]", " ", s) + s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s) + l = [] + for item in s.split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(item, 16)) + else: + start,end = fields + l.extend(range(int(start, 16), int(end, 16)+1)) + return l def parse_gids(s): - l = [] - for item in s.replace(',', ' ').split(): - fields = item.split('-') - if len(fields) == 1: - l.append(int(fields[0])) - else: - l.extend(range(int(fields[0]), int(fields[1])+1)) - return l + l = [] + for item in s.replace(',', ' ').split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(fields[0])) + else: + l.extend(range(int(fields[0]), int(fields[1])+1)) + return l def parse_glyphs(s): - return s.replace(',', ' ').split() + return s.replace(',', ' ').split() def usage(): - print("usage:", __usage__, file=sys.stderr) - print("Try pyftsubset --help for more information.\n", file=sys.stderr) + print("usage:", __usage__, file=sys.stderr) + print("Try pyftsubset --help for more information.\n", file=sys.stderr) @timer("make one with everything (TOTAL TIME)") def main(args=None): - from os.path import splitext - from fontTools import configLogger + from os.path import splitext + from fontTools import configLogger - if args is None: - args = sys.argv[1:] + if args is None: + args = sys.argv[1:] - if '--help' in args: - print(__doc__) - return 0 - - options = Options() - try: - args = options.parse_opts(args, - ignore_unknown=['gids', 'gids-file', - 'glyphs', 'glyphs-file', - 'text', 'text-file', - 'unicodes', 'unicodes-file', - 'output-file']) - except options.OptionError as e: - usage() - print("ERROR:", e, file=sys.stderr) - return 2 - - if len(args) < 2: - usage() - return 1 - - configLogger(level=logging.INFO if options.verbose else logging.WARNING) - if options.timing: - timer.logger.setLevel(logging.DEBUG) - else: - timer.logger.disabled = True - - fontfile = args[0] - args = args[1:] - - subsetter = Subsetter(options=options) - basename, extension = splitext(fontfile) - outfile = basename + '.subset' + extension - glyphs = [] - gids = [] - unicodes = [] - wildcard_glyphs = False - wildcard_unicodes = False - text = "" - for g in args: - if g == '*': - wildcard_glyphs = True - continue - if g.startswith('--output-file='): - outfile = g[14:] - continue - if g.startswith('--text='): - text += g[7:] - continue - if g.startswith('--text-file='): - text += open(g[12:], encoding='utf-8').read().replace('\n', '') - continue - if g.startswith('--unicodes='): - if g[11:] == '*': - wildcard_unicodes = True - else: - unicodes.extend(parse_unicodes(g[11:])) - continue - if g.startswith('--unicodes-file='): - for line in open(g[16:]).readlines(): - unicodes.extend(parse_unicodes(line.split('#')[0])) - continue - if g.startswith('--gids='): - gids.extend(parse_gids(g[7:])) - continue - if g.startswith('--gids-file='): - for line in open(g[12:]).readlines(): - gids.extend(parse_gids(line.split('#')[0])) - continue - if g.startswith('--glyphs='): - if g[9:] == '*': - wildcard_glyphs = True - else: - glyphs.extend(parse_glyphs(g[9:])) - continue - if g.startswith('--glyphs-file='): - for line in open(g[14:]).readlines(): - glyphs.extend(parse_glyphs(line.split('#')[0])) - continue - glyphs.append(g) - - dontLoadGlyphNames = not options.glyph_names and not glyphs - font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) - - with timer("compile glyph list"): - if wildcard_glyphs: - glyphs.extend(font.getGlyphOrder()) - if wildcard_unicodes: - for t in font['cmap'].tables: - if t.isUnicode(): - unicodes.extend(t.cmap.keys()) - assert '' not in glyphs - - log.info("Text: '%s'" % text) - log.info("Unicodes: %s", unicodes) - log.info("Glyphs: %s", glyphs) - log.info("Gids: %s", gids) - - subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) - subsetter.subset(font) - - save_font(font, outfile, options) - - if options.verbose: - import os - log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) - log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) + if '--help' in args: + print(__doc__) + return 0 + + options = Options() + try: + args = options.parse_opts(args, + ignore_unknown=['gids', 'gids-file', + 'glyphs', 'glyphs-file', + 'text', 'text-file', + 'unicodes', 'unicodes-file', + 'output-file']) + except options.OptionError as e: + usage() + print("ERROR:", e, file=sys.stderr) + return 2 + + if len(args) < 2: + usage() + return 1 + + configLogger(level=logging.INFO if options.verbose else logging.WARNING) + if options.timing: + timer.logger.setLevel(logging.DEBUG) + else: + timer.logger.disabled = True + + fontfile = args[0] + args = args[1:] + + subsetter = Subsetter(options=options) + outfile = None + glyphs = [] + gids = [] + unicodes = [] + wildcard_glyphs = False + wildcard_unicodes = False + text = "" + for g in args: + if g == '*': + wildcard_glyphs = True + continue + if g.startswith('--output-file='): + outfile = g[14:] + continue + if g.startswith('--text='): + text += g[7:] + continue + if g.startswith('--text-file='): + text += open(g[12:], encoding='utf-8').read().replace('\n', '') + continue + if g.startswith('--unicodes='): + if g[11:] == '*': + wildcard_unicodes = True + else: + unicodes.extend(parse_unicodes(g[11:])) + continue + if g.startswith('--unicodes-file='): + for line in open(g[16:]).readlines(): + unicodes.extend(parse_unicodes(line.split('#')[0])) + continue + if g.startswith('--gids='): + gids.extend(parse_gids(g[7:])) + continue + if g.startswith('--gids-file='): + for line in open(g[12:]).readlines(): + gids.extend(parse_gids(line.split('#')[0])) + continue + if g.startswith('--glyphs='): + if g[9:] == '*': + wildcard_glyphs = True + else: + glyphs.extend(parse_glyphs(g[9:])) + continue + if g.startswith('--glyphs-file='): + for line in open(g[14:]).readlines(): + glyphs.extend(parse_glyphs(line.split('#')[0])) + continue + glyphs.append(g) + + dontLoadGlyphNames = not options.glyph_names and not glyphs + font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) + + if outfile is None: + basename, _ = splitext(fontfile) + if options.flavor is not None: + ext = "." + options.flavor.lower() + else: + ext = ".ttf" if font.sfntVersion == "\0\1\0\0" else ".otf" + outfile = basename + ".subset" + ext + + with timer("compile glyph list"): + if wildcard_glyphs: + glyphs.extend(font.getGlyphOrder()) + if wildcard_unicodes: + for t in font['cmap'].tables: + if t.isUnicode(): + unicodes.extend(t.cmap.keys()) + assert '' not in glyphs + + log.info("Text: '%s'" % text) + log.info("Unicodes: %s", unicodes) + log.info("Glyphs: %s", glyphs) + log.info("Gids: %s", gids) + + subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) + subsetter.subset(font) + + save_font(font, outfile, options) + + if options.verbose: + import os + log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) + log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) - if options.xml: - font.saveXML(sys.stdout) + if options.xml: + font.saveXML(sys.stdout) - font.close() + font.close() __all__ = [ - 'Options', - 'Subsetter', - 'load_font', - 'save_font', - 'parse_gids', - 'parse_glyphs', - 'parse_unicodes', - 'main' + 'Options', + 'Subsetter', + 'load_font', + 'save_font', + 'parse_gids', + 'parse_glyphs', + 'parse_unicodes', + 'main' ] if __name__ == '__main__': - sys.exit(main()) + sys.exit(main()) diff -Nru fonttools-3.21.2/Lib/fontTools/t1Lib/__init__.py fonttools-3.29.0/Lib/fontTools/t1Lib/__init__.py --- fonttools-3.21.2/Lib/fontTools/t1Lib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/t1Lib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -49,11 +49,18 @@ Type 1 fonts. """ - def __init__(self, path=None): - if path is not None: - self.data, type = read(path) + def __init__(self, path, encoding="ascii", kind=None): + if kind is None: + self.data, _ = read(path) + elif kind == "LWFN": + self.data = readLWFN(path) + elif kind == "PFB": + self.data = readPFB(path) + elif kind == "OTHER": + self.data = readOther(path) else: - pass # XXX + raise ValueError(kind) + self.encoding = encoding def saveAs(self, path, type, dohex=False): write(path, self.getData(), type, dohex) @@ -82,7 +89,7 @@ def parse(self): from fontTools.misc import psLib from fontTools.misc import psCharStrings - self.font = psLib.suckfont(self.data) + self.font = psLib.suckfont(self.data, self.encoding) charStrings = self.font["CharStrings"] lenIV = self.font["Private"].get("lenIV", 4) assert lenIV >= 0 diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/__init__.py fonttools-3.29.0/Lib/fontTools/ttLib/__init__.py --- fonttools-3.21.2/Lib/fontTools/ttLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -43,9 +43,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.loggingTools import deprecateArgument, deprecateFunction -import os -import sys +from fontTools.misc.loggingTools import deprecateFunction import logging @@ -53,971 +51,10 @@ class TTLibError(Exception): pass - -class TTFont(object): - - """The main font object. It manages file input and output, and offers - a convenient way of accessing tables. - Tables will be only decompiled when necessary, ie. when they're actually - accessed. This means that simple operations can be extremely fast. - """ - - def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, - verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, - recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None): - - """The constructor can be called with a few different arguments. - When reading a font from disk, 'file' should be either a pathname - pointing to a file, or a readable file object. - - It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt - resource name or an sfnt resource index number or zero. The latter - case will cause TTLib to autodetect whether the file is a flat file - or a suitcase. (If it's a suitcase, only the first 'sfnt' resource - will be read!) - - The 'checkChecksums' argument is used to specify how sfnt - checksums are treated upon reading a file from disk: - 0: don't check (default) - 1: check, print warnings if a wrong checksum is found - 2: check, raise an exception if a wrong checksum is found. - - The TTFont constructor can also be called without a 'file' - argument: this is the way to create a new empty font. - In this case you can optionally supply the 'sfntVersion' argument, - and a 'flavor' which can be None, 'woff', or 'woff2'. - - If the recalcBBoxes argument is false, a number of things will *not* - be recalculated upon save/compile: - 1) 'glyf' glyph bounding boxes - 2) 'CFF ' font bounding box - 3) 'head' font bounding box - 4) 'hhea' min/max values - 5) 'vhea' min/max values - (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). - Additionally, upon importing an TTX file, this option cause glyphs - to be compiled right away. This should reduce memory consumption - greatly, and therefore should have some impact on the time needed - to parse/compile large fonts. - - If the recalcTimestamp argument is false, the modified timestamp in the - 'head' table will *not* be recalculated upon save/compile. - - If the allowVID argument is set to true, then virtual GID's are - supported. Asking for a glyph ID with a glyph name or GID that is not in - the font will return a virtual GID. This is valid for GSUB and cmap - tables. For SING glyphlets, the cmap table is used to specify Unicode - values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested - and does not exist in the font, or the glyphname has the form glyphN - and does not exist in the font, then N is used as the virtual GID. - Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new - virtual GIDs, the next is one less than the previous. - - If ignoreDecompileErrors is set to True, exceptions raised in - individual tables during decompilation will be ignored, falling - back to the DefaultTable implementation, which simply keeps the - binary data. - - If lazy is set to True, many data structures are loaded lazily, upon - access only. If it is set to False, many data structures are loaded - immediately. The default is lazy=None which is somewhere in between. - """ - - from fontTools.ttLib import sfnt - - for name in ("verbose", "quiet"): - val = locals().get(name) - if val is not None: - deprecateArgument(name, "configure logging instead") - setattr(self, name, val) - - self.lazy = lazy - self.recalcBBoxes = recalcBBoxes - self.recalcTimestamp = recalcTimestamp - self.tables = {} - self.reader = None - - # Permit the user to reference glyphs that are not int the font. - self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. - self.reverseVIDDict = {} - self.VIDDict = {} - self.allowVID = allowVID - self.ignoreDecompileErrors = ignoreDecompileErrors - - if not file: - self.sfntVersion = sfntVersion - self.flavor = flavor - self.flavorData = None - return - if not hasattr(file, "read"): - closeStream = True - # assume file is a string - if res_name_or_index is not None: - # see if it contains 'sfnt' resources in the resource or data fork - from . import macUtils - if res_name_or_index == 0: - if macUtils.getSFNTResIndices(file): - # get the first available sfnt font. - file = macUtils.SFNTResourceReader(file, 1) - else: - file = open(file, "rb") - else: - file = macUtils.SFNTResourceReader(file, res_name_or_index) - else: - file = open(file, "rb") - - else: - # assume "file" is a readable file object - closeStream = False - if not self.lazy: - # read input file in memory and wrap a stream around it to allow overwriting - tmp = BytesIO(file.read()) - if hasattr(file, 'name'): - # save reference to input file name - tmp.name = file.name - if closeStream: - file.close() - file = tmp - self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) - self.sfntVersion = self.reader.sfntVersion - self.flavor = self.reader.flavor - self.flavorData = self.reader.flavorData - - def close(self): - """If we still have a reader object, close it.""" - if self.reader is not None: - self.reader.close() - - def save(self, file, reorderTables=True): - """Save the font to disk. Similarly to the constructor, - the 'file' argument can be either a pathname or a writable - file object. - """ - from fontTools.ttLib import sfnt - if not hasattr(file, "write"): - if self.lazy and self.reader.file.name == file: - raise TTLibError( - "Can't overwrite TTFont when 'lazy' attribute is True") - closeStream = True - file = open(file, "wb") - else: - # assume "file" is a writable file object - closeStream = False - - if self.recalcTimestamp and 'head' in self: - self['head'] # make sure 'head' is loaded so the recalculation is actually done - - tags = list(self.keys()) - if "GlyphOrder" in tags: - tags.remove("GlyphOrder") - numTables = len(tags) - # write to a temporary stream to allow saving to unseekable streams - tmp = BytesIO() - writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData) - - done = [] - for tag in tags: - self._writeTable(tag, writer, done) - - writer.close() - - if (reorderTables is None or writer.reordersTables() or - (reorderTables is False and self.reader is None)): - # don't reorder tables and save as is - file.write(tmp.getvalue()) - tmp.close() - else: - if reorderTables is False: - # sort tables using the original font's order - tableOrder = list(self.reader.keys()) - else: - # use the recommended order from the OpenType specification - tableOrder = None - tmp.flush() - tmp.seek(0) - tmp2 = BytesIO() - reorderFontTables(tmp, tmp2, tableOrder) - file.write(tmp2.getvalue()) - tmp.close() - tmp2.close() - - if closeStream: - file.close() - - def saveXML(self, fileOrPath, progress=None, quiet=None, - tables=None, skipTables=None, splitTables=False, disassembleInstructions=True, - bitmapGlyphDataFormat='raw', newlinestr=None): - """Export the font as TTX (an XML-based text file), or as a series of text - files when splitTables is true. In the latter case, the 'fileOrPath' - argument should be a path to a directory. - The 'tables' argument must either be false (dump all tables) or a - list of tables to dump. The 'skipTables' argument may be a list of tables - to skip, but only when the 'tables' argument is false. - """ - from fontTools import version - from fontTools.misc import xmlWriter - - # only write the MAJOR.MINOR version in the 'ttLibVersion' attribute of - # TTX files' root element (without PATCH or .dev suffixes) - version = ".".join(version.split('.')[:2]) - - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - self.disassembleInstructions = disassembleInstructions - self.bitmapGlyphDataFormat = bitmapGlyphDataFormat - if not tables: - tables = list(self.keys()) - if "GlyphOrder" not in tables: - tables = ["GlyphOrder"] + tables - if skipTables: - for tag in skipTables: - if tag in tables: - tables.remove(tag) - numTables = len(tables) - if progress: - progress.set(0, numTables) - idlefunc = getattr(progress, "idle", None) - else: - idlefunc = None - - writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc, - newlinestr=newlinestr) - writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], - ttLibVersion=version) - writer.newline() - - if not splitTables: - writer.newline() - else: - # 'fileOrPath' must now be a path - path, ext = os.path.splitext(fileOrPath) - fileNameTemplate = path + ".%s" + ext - - for i in range(numTables): - if progress: - progress.set(i) - tag = tables[i] - if splitTables: - tablePath = fileNameTemplate % tagToIdentifier(tag) - tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc, - newlinestr=newlinestr) - tableWriter.begintag("ttFont", ttLibVersion=version) - tableWriter.newline() - tableWriter.newline() - writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) - writer.newline() - else: - tableWriter = writer - self._tableToXML(tableWriter, tag, progress) - if splitTables: - tableWriter.endtag("ttFont") - tableWriter.newline() - tableWriter.close() - if progress: - progress.set((i + 1)) - writer.endtag("ttFont") - writer.newline() - # close if 'fileOrPath' is a path; leave it open if it's a file. - # The special string "-" means standard output so leave that open too - if not hasattr(fileOrPath, "write") and fileOrPath != "-": - writer.close() - - def _tableToXML(self, writer, tag, progress, quiet=None): - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - if tag in self: - table = self[tag] - report = "Dumping '%s' table..." % tag - else: - report = "No '%s' table found." % tag - if progress: - progress.setLabel(report) - log.info(report) - if tag not in self: - return - xmlTag = tagToXML(tag) - attrs = dict() - if hasattr(table, "ERROR"): - attrs['ERROR'] = "decompilation error" - from .tables.DefaultTable import DefaultTable - if table.__class__ == DefaultTable: - attrs['raw'] = True - writer.begintag(xmlTag, **attrs) - writer.newline() - if tag in ("glyf", "CFF "): - table.toXML(writer, self, progress) - else: - table.toXML(writer, self) - writer.endtag(xmlTag) - writer.newline() - writer.newline() - - def importXML(self, fileOrPath, progress=None, quiet=None): - """Import a TTX file (an XML-based text format), so as to recreate - a font object. - """ - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - if "maxp" in self and "post" in self: - # Make sure the glyph order is loaded, as it otherwise gets - # lost if the XML doesn't contain the glyph order, yet does - # contain the table which was originally used to extract the - # glyph names from (ie. 'post', 'cmap' or 'CFF '). - self.getGlyphOrder() - - from fontTools.misc import xmlReader - - reader = xmlReader.XMLReader(fileOrPath, self, progress) - reader.read() - - def isLoaded(self, tag): - """Return true if the table identified by 'tag' has been - decompiled and loaded into memory.""" - return tag in self.tables - - def has_key(self, tag): - if self.isLoaded(tag): - return True - elif self.reader and tag in self.reader: - return True - elif tag == "GlyphOrder": - return True - else: - return False - - __contains__ = has_key - - def keys(self): - keys = list(self.tables.keys()) - if self.reader: - for key in list(self.reader.keys()): - if key not in keys: - keys.append(key) - - if "GlyphOrder" in keys: - keys.remove("GlyphOrder") - keys = sortedTagList(keys) - return ["GlyphOrder"] + keys - - def __len__(self): - return len(list(self.keys())) - - def __getitem__(self, tag): - tag = Tag(tag) - try: - return self.tables[tag] - except KeyError: - if tag == "GlyphOrder": - table = GlyphOrder(tag) - self.tables[tag] = table - return table - if self.reader is not None: - import traceback - log.debug("Reading '%s' table from disk", tag) - data = self.reader[tag] - tableClass = getTableClass(tag) - table = tableClass(tag) - self.tables[tag] = table - log.debug("Decompiling '%s' table", tag) - try: - table.decompile(data, self) - except: - if not self.ignoreDecompileErrors: - raise - # fall back to DefaultTable, retaining the binary table data - log.exception( - "An exception occurred during the decompilation of the '%s' table", tag) - from .tables.DefaultTable import DefaultTable - file = StringIO() - traceback.print_exc(file=file) - table = DefaultTable(tag) - table.ERROR = file.getvalue() - self.tables[tag] = table - table.decompile(data, self) - return table - else: - raise KeyError("'%s' table not found" % tag) - - def __setitem__(self, tag, table): - self.tables[Tag(tag)] = table - - def __delitem__(self, tag): - if tag not in self: - raise KeyError("'%s' table not found" % tag) - if tag in self.tables: - del self.tables[tag] - if self.reader and tag in self.reader: - del self.reader[tag] - - def get(self, tag, default=None): - try: - return self[tag] - except KeyError: - return default - - def setGlyphOrder(self, glyphOrder): - self.glyphOrder = glyphOrder - - def getGlyphOrder(self): - try: - return self.glyphOrder - except AttributeError: - pass - if 'CFF ' in self: - cff = self['CFF '] - self.glyphOrder = cff.getGlyphOrder() - elif 'post' in self: - # TrueType font - glyphOrder = self['post'].getGlyphOrder() - if glyphOrder is None: - # - # No names found in the 'post' table. - # Try to create glyph names from the unicode cmap (if available) - # in combination with the Adobe Glyph List (AGL). - # - self._getGlyphNamesFromCmap() - else: - self.glyphOrder = glyphOrder - else: - self._getGlyphNamesFromCmap() - return self.glyphOrder - - def _getGlyphNamesFromCmap(self): - # - # This is rather convoluted, but then again, it's an interesting problem: - # - we need to use the unicode values found in the cmap table to - # build glyph names (eg. because there is only a minimal post table, - # or none at all). - # - but the cmap parser also needs glyph names to work with... - # So here's what we do: - # - make up glyph names based on glyphID - # - load a temporary cmap table based on those names - # - extract the unicode values, build the "real" glyph names - # - unload the temporary cmap table - # - if self.isLoaded("cmap"): - # Bootstrapping: we're getting called by the cmap parser - # itself. This means self.tables['cmap'] contains a partially - # loaded cmap, making it impossible to get at a unicode - # subtable here. We remove the partially loaded cmap and - # restore it later. - # This only happens if the cmap table is loaded before any - # other table that does f.getGlyphOrder() or f.getGlyphName(). - cmapLoading = self.tables['cmap'] - del self.tables['cmap'] - else: - cmapLoading = None - # Make up glyph names based on glyphID, which will be used by the - # temporary cmap and by the real cmap in case we don't find a unicode - # cmap. - numGlyphs = int(self['maxp'].numGlyphs) - glyphOrder = [None] * numGlyphs - glyphOrder[0] = ".notdef" - for i in range(1, numGlyphs): - glyphOrder[i] = "glyph%.5d" % i - # Set the glyph order, so the cmap parser has something - # to work with (so we don't get called recursively). - self.glyphOrder = glyphOrder - - # Make up glyph names based on the reversed cmap table. Because some - # glyphs (eg. ligatures or alternates) may not be reachable via cmap, - # this naming table will usually not cover all glyphs in the font. - # If the font has no Unicode cmap table, reversecmap will be empty. - reversecmap = self['cmap'].buildReversed() - useCount = {} - for i in range(numGlyphs): - tempName = glyphOrder[i] - if tempName in reversecmap: - # If a font maps both U+0041 LATIN CAPITAL LETTER A and - # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, - # we prefer naming the glyph as "A". - glyphName = self._makeGlyphName(min(reversecmap[tempName])) - numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 - if numUses > 1: - glyphName = "%s.alt%d" % (glyphName, numUses - 1) - glyphOrder[i] = glyphName - - # Delete the temporary cmap table from the cache, so it can - # be parsed again with the right names. - del self.tables['cmap'] - self.glyphOrder = glyphOrder - if cmapLoading: - # restore partially loaded cmap, so it can continue loading - # using the proper names. - self.tables['cmap'] = cmapLoading - - @staticmethod - def _makeGlyphName(codepoint): - from fontTools import agl # Adobe Glyph List - if codepoint in agl.UV2AGL: - return agl.UV2AGL[codepoint] - elif codepoint <= 0xFFFF: - return "uni%04X" % codepoint - else: - return "u%X" % codepoint - - def getGlyphNames(self): - """Get a list of glyph names, sorted alphabetically.""" - glyphNames = sorted(self.getGlyphOrder()) - return glyphNames - - def getGlyphNames2(self): - """Get a list of glyph names, sorted alphabetically, - but not case sensitive. - """ - from fontTools.misc import textTools - return textTools.caselessSort(self.getGlyphOrder()) - - def getGlyphName(self, glyphID, requireReal=False): - try: - return self.getGlyphOrder()[glyphID] - except IndexError: - if requireReal or not self.allowVID: - # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in - # the cmap table than there are glyphs. I don't think it's legal... - return "glyph%.5d" % glyphID - else: - # user intends virtual GID support - try: - glyphName = self.VIDDict[glyphID] - except KeyError: - glyphName ="glyph%.5d" % glyphID - self.last_vid = min(glyphID, self.last_vid ) - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphName - - def getGlyphID(self, glyphName, requireReal=False): - if not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - glyphOrder = self.getGlyphOrder() - d = self._reverseGlyphOrderDict - if glyphName not in d: - if glyphName in glyphOrder: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - else: - if requireReal: - raise KeyError(glyphName) - elif not self.allowVID: - # Handle glyphXXX only - if glyphName[:5] == "glyph": - try: - return int(glyphName[5:]) - except (NameError, ValueError): - raise KeyError(glyphName) - else: - # user intends virtual GID support - try: - glyphID = self.reverseVIDDict[glyphName] - except KeyError: - # if name is in glyphXXX format, use the specified name. - if glyphName[:5] == "glyph": - try: - glyphID = int(glyphName[5:]) - except (NameError, ValueError): - glyphID = None - if glyphID is None: - glyphID = self.last_vid -1 - self.last_vid = glyphID - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphID - - glyphID = d[glyphName] - if glyphName != glyphOrder[glyphID]: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - return glyphID - - def getReverseGlyphMap(self, rebuild=False): - if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - return self._reverseGlyphOrderDict - - def _buildReverseGlyphOrderDict(self): - self._reverseGlyphOrderDict = d = {} - glyphOrder = self.getGlyphOrder() - for glyphID in range(len(glyphOrder)): - d[glyphOrder[glyphID]] = glyphID - - def _writeTable(self, tag, writer, done): - """Internal helper function for self.save(). Keeps track of - inter-table dependencies. - """ - if tag in done: - return - tableClass = getTableClass(tag) - for masterTable in tableClass.dependencies: - if masterTable not in done: - if masterTable in self: - self._writeTable(masterTable, writer, done) - else: - done.append(masterTable) - tabledata = self.getTableData(tag) - log.debug("writing '%s' table to disk", tag) - writer[tag] = tabledata - done.append(tag) - - def getTableData(self, tag): - """Returns raw table data, whether compiled or directly read from disk. - """ - tag = Tag(tag) - if self.isLoaded(tag): - log.debug("compiling '%s' table", tag) - return self.tables[tag].compile(self) - elif self.reader and tag in self.reader: - log.debug("Reading '%s' table from disk", tag) - return self.reader[tag] - else: - raise KeyError(tag) - - def getGlyphSet(self, preferCFF=True): - """Return a generic GlyphSet, which is a dict-like object - mapping glyph names to glyph objects. The returned glyph objects - have a .draw() method that supports the Pen protocol, and will - have an attribute named 'width'. - - If the font is CFF-based, the outlines will be taken from the 'CFF ' or - 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. - If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use - the 'preferCFF' argument to specify which one should be taken. If the - font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. - """ - glyphs = None - if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or - ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): - table_tag = "CFF2" if "CFF2" in self else "CFF " - glyphs = _TTGlyphSet(self, - list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) - - if glyphs is None and "glyf" in self: - glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) - - if glyphs is None: - raise TTLibError("Font contains no outlines") - - return glyphs - - def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): - """Return the 'best' unicode cmap dictionary available in the font, - or None, if no unicode cmap subtable is available. - - By default it will search for the following (platformID, platEncID) - pairs: - (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) - This can be customized via the cmapPreferences argument. - """ - return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) - - -class _TTGlyphSet(object): - - """Generic dict-like GlyphSet class that pulls metrics from hmtx and - glyph shape from TrueType or CFF. - """ - - def __init__(self, ttFont, glyphs, glyphType): - self._glyphs = glyphs - self._hmtx = ttFont['hmtx'] - self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None - self._glyphType = glyphType - - def keys(self): - return list(self._glyphs.keys()) - - def has_key(self, glyphName): - return glyphName in self._glyphs - - __contains__ = has_key - - def __getitem__(self, glyphName): - horizontalMetrics = self._hmtx[glyphName] - verticalMetrics = self._vmtx[glyphName] if self._vmtx else None - return self._glyphType( - self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) - - def get(self, glyphName, default=None): - try: - return self[glyphName] - except KeyError: - return default - -class _TTGlyph(object): - - """Wrapper for a TrueType glyph that supports the Pen protocol, meaning - that it has a .draw() method that takes a pen object as its only - argument. Additionally there are 'width' and 'lsb' attributes, read from - the 'hmtx' table. - - If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' - attributes. - """ - - def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): - self._glyphset = glyphset - self._glyph = glyph - self.width, self.lsb = horizontalMetrics - if verticalMetrics: - self.height, self.tsb = verticalMetrics - else: - self.height, self.tsb = None, None - - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - self._glyph.draw(pen) - -class _TTGlyphCFF(_TTGlyph): - pass - -class _TTGlyphGlyf(_TTGlyph): - - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - glyfTable = self._glyphset._glyphs - glyph = self._glyph - offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 - glyph.draw(pen, glyfTable, offset) - - -class GlyphOrder(object): - - """A pseudo table. The glyph order isn't in the font as a separate - table, but it's nice to present it as such in the TTX format. - """ - - def __init__(self, tag=None): - pass - - def toXML(self, writer, ttFont): - glyphOrder = ttFont.getGlyphOrder() - writer.comment("The 'id' attribute is only for humans; " - "it is ignored when parsed.") - writer.newline() - for i in range(len(glyphOrder)): - glyphName = glyphOrder[i] - writer.simpletag("GlyphID", id=i, name=glyphName) - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "glyphOrder"): - self.glyphOrder = [] - ttFont.setGlyphOrder(self.glyphOrder) - if name == "GlyphID": - self.glyphOrder.append(attrs["name"]) - - -def getTableModule(tag): - """Fetch the packer/unpacker module for a table. - Return None when no module is found. - """ - from . import tables - pyTag = tagToIdentifier(tag) - try: - __import__("fontTools.ttLib.tables." + pyTag) - except ImportError as err: - # If pyTag is found in the ImportError message, - # means table is not implemented. If it's not - # there, then some other module is missing, don't - # suppress the error. - if str(err).find(pyTag) >= 0: - return None - else: - raise err - else: - return getattr(tables, pyTag) - - -def getTableClass(tag): - """Fetch the packer/unpacker class for a table. - Return None when no class is found. - """ - module = getTableModule(tag) - if module is None: - from .tables.DefaultTable import DefaultTable - return DefaultTable - pyTag = tagToIdentifier(tag) - tableClass = getattr(module, "table_" + pyTag) - return tableClass - - -def getClassTag(klass): - """Fetch the table tag for a class object.""" - name = klass.__name__ - assert name[:6] == 'table_' - name = name[6:] # Chop 'table_' - return identifierToTag(name) - - -def newTable(tag): - """Return a new instance of a table.""" - tableClass = getTableClass(tag) - return tableClass(tag) - - -def _escapechar(c): - """Helper function for tagToIdentifier()""" - import re - if re.match("[a-z0-9]", c): - return "_" + c - elif re.match("[A-Z]", c): - return c + "_" - else: - return hex(byteord(c))[2:] - - -def tagToIdentifier(tag): - """Convert a table tag to a valid (but UGLY) python identifier, - as well as a filename that's guaranteed to be unique even on a - caseless file system. Each character is mapped to two characters. - Lowercase letters get an underscore before the letter, uppercase - letters get an underscore after the letter. Trailing spaces are - trimmed. Illegal characters are escaped as two hex bytes. If the - result starts with a number (as the result of a hex escape), an - extra underscore is prepended. Examples: - 'glyf' -> '_g_l_y_f' - 'cvt ' -> '_c_v_t' - 'OS/2' -> 'O_S_2f_2' - """ - import re - tag = Tag(tag) - if tag == "GlyphOrder": - return tag - assert len(tag) == 4, "tag should be 4 characters long" - while len(tag) > 1 and tag[-1] == ' ': - tag = tag[:-1] - ident = "" - for c in tag: - ident = ident + _escapechar(c) - if re.match("[0-9]", ident): - ident = "_" + ident - return ident - - -def identifierToTag(ident): - """the opposite of tagToIdentifier()""" - if ident == "GlyphOrder": - return ident - if len(ident) % 2 and ident[0] == "_": - ident = ident[1:] - assert not (len(ident) % 2) - tag = "" - for i in range(0, len(ident), 2): - if ident[i] == "_": - tag = tag + ident[i+1] - elif ident[i+1] == "_": - tag = tag + ident[i] - else: - # assume hex - tag = tag + chr(int(ident[i:i+2], 16)) - # append trailing spaces - tag = tag + (4 - len(tag)) * ' ' - return Tag(tag) - - -def tagToXML(tag): - """Similarly to tagToIdentifier(), this converts a TT tag - to a valid XML element name. Since XML element names are - case sensitive, this is a fairly simple/readable translation. - """ - import re - tag = Tag(tag) - if tag == "OS/2": - return "OS_2" - elif tag == "GlyphOrder": - return tag - if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): - return tag.strip() - else: - return tagToIdentifier(tag) - - -def xmlToTag(tag): - """The opposite of tagToXML()""" - if tag == "OS_2": - return Tag("OS/2") - if len(tag) == 8: - return identifierToTag(tag) - else: - return Tag(tag + " " * (4 - len(tag))) - - @deprecateFunction("use logging instead", category=DeprecationWarning) def debugmsg(msg): import time print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) - -# Table order as recommended in the OpenType specification 1.4 -TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", - "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", - "kern", "name", "post", "gasp", "PCLT"] - -OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", - "CFF "] - -def sortedTagList(tagList, tableOrder=None): - """Return a sorted copy of tagList, sorted according to the OpenType - specification, or according to a custom tableOrder. If given and not - None, tableOrder needs to be a list of tag names. - """ - tagList = sorted(tagList) - if tableOrder is None: - if "DSIG" in tagList: - # DSIG should be last (XXX spec reference?) - tagList.remove("DSIG") - tagList.append("DSIG") - if "CFF " in tagList: - tableOrder = OTFTableOrder - else: - tableOrder = TTFTableOrder - orderedTables = [] - for tag in tableOrder: - if tag in tagList: - orderedTables.append(tag) - tagList.remove(tag) - orderedTables.extend(tagList) - return orderedTables - - -def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): - """Rewrite a font file, ordering the tables as recommended by the - OpenType specification 1.4. - """ - from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter - reader = SFNTReader(inFile, checkChecksums=checkChecksums) - writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) - tables = list(reader.keys()) - for tag in sortedTagList(tables, tableOrder): - writer[tag] = reader[tag] - writer.close() - - -def maxPowerOfTwo(x): - """Return the highest exponent of two, so that - (2 ** exponent) <= x. Return 0 if x is 0. - """ - exponent = 0 - while x: - x = x >> 1 - exponent = exponent + 1 - return max(exponent - 1, 0) - - -def getSearchRange(n, itemSize=16): - """Calculate searchRange, entrySelector, rangeShift. - """ - # itemSize defaults to 16, for backward compatibility - # with upstream fonttools. - exponent = maxPowerOfTwo(n) - searchRange = (2 ** exponent) * itemSize - entrySelector = exponent - rangeShift = max(0, n * itemSize - searchRange) - return searchRange, entrySelector, rangeShift +from fontTools.ttLib.ttFont import * +from fontTools.ttLib.ttCollection import TTCollection diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/sfnt.py fonttools-3.29.0/Lib/fontTools/ttLib/sfnt.py --- fonttools-3.21.2/Lib/fontTools/ttLib/sfnt.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/sfnt.py 2018-07-26 14:12:55.000000000 +0000 @@ -15,7 +15,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct -from fontTools.ttLib import getSearchRange +from fontTools.ttLib import TTLibError import struct from collections import OrderedDict import logging @@ -32,6 +32,7 @@ """ if args and cls is SFNTReader: infile = args[0] + infile.seek(0) sfntVersion = Tag(infile.read(4)) infile.seek(0) if sfntVersion == "wOF2": @@ -48,46 +49,36 @@ self.flavor = None self.flavorData = None self.DirectoryEntry = SFNTDirectoryEntry + self.file.seek(0) self.sfntVersion = self.file.read(4) self.file.seek(0) if self.sfntVersion == b"ttcf": - data = self.file.read(ttcHeaderSize) - if len(data) != ttcHeaderSize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a Font Collection (not enough data)") - sstruct.unpack(ttcHeaderFormat, data, self) - assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version - if not 0 <= fontNumber < self.numFonts: - from fontTools import ttLib - raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)) - offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4)) - if self.Version == 0x00020000: - pass # ignoring version 2.0 signatures - self.file.seek(offsetTable[fontNumber]) + header = readTTCHeader(self.file) + numFonts = header.numFonts + if not 0 <= fontNumber < numFonts: + raise TTLibError("specify a font number between 0 and %d (inclusive)" % (numFonts - 1)) + self.numFonts = numFonts + self.file.seek(header.offsetTable[fontNumber]) data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a Font Collection (not enough data)") + raise TTLibError("Not a Font Collection (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) elif self.sfntVersion == b"wOFF": self.flavor = "woff" self.DirectoryEntry = WOFFDirectoryEntry data = self.file.read(woffDirectorySize) if len(data) != woffDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a WOFF font (not enough data)") + raise TTLibError("Not a WOFF font (not enough data)") sstruct.unpack(woffDirectoryFormat, data, self) else: data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a TrueType or OpenType font (not enough data)") + raise TTLibError("Not a TrueType or OpenType font (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) self.sfntVersion = Tag(self.sfntVersion) if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): - from fontTools import ttLib - raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") + raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") tables = {} for i in range(self.numTables): entry = self.DirectoryEntry() @@ -215,20 +206,27 @@ self.directorySize = sfntDirectorySize self.DirectoryEntry = SFNTDirectoryEntry + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables, 16) - self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize + self.directoryOffset = self.file.tell() + self.nextTableOffset = self.directoryOffset + self.directorySize + numTables * self.DirectoryEntry.formatSize # clear out directory area self.file.seek(self.nextTableOffset) # make sure we're actually where we want to be. (old cStringIO bug) self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) self.tables = OrderedDict() + def setEntry(self, tag, entry): + if tag in self.tables: + raise TTLibError("cannot rewrite '%s' table" % tag) + + self.tables[tag] = entry + def __setitem__(self, tag, data): """Write raw table data to disk.""" if tag in self.tables: - from fontTools import ttLib - raise ttLib.TTLibError("cannot rewrite '%s' table" % tag) + raise TTLibError("cannot rewrite '%s' table" % tag) entry = self.DirectoryEntry() entry.tag = tag @@ -253,7 +251,10 @@ self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) assert self.nextTableOffset == self.file.tell() - self.tables[tag] = entry + self.setEntry(tag, entry) + + def __getitem__(self, tag): + return self.tables[tag] def close(self): """All tables must have been written to disk. Now write the @@ -261,8 +262,7 @@ """ tables = sorted(self.tables.items()) if len(tables) != self.numTables: - from fontTools import ttLib - raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) + raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) if self.flavor == "woff": self.signature = b"wOFF" @@ -311,7 +311,7 @@ directory = sstruct.pack(self.directoryFormat, self) - self.file.seek(self.directorySize) + self.file.seek(self.directoryOffset + self.directorySize) seenHead = 0 for tag, entry in tables: if tag == "head": @@ -319,7 +319,7 @@ directory = directory + entry.toString() if seenHead: self.writeMasterChecksum(directory) - self.file.seek(0) + self.file.seek(self.directoryOffset) self.file.write(directory) def _calcMasterChecksum(self, directory): @@ -331,6 +331,7 @@ if self.DirectoryEntry != SFNTDirectoryEntry: # Create a SFNT directory for checksum calculation purposes + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) @@ -566,6 +567,31 @@ value = (value + sum(longs)) & 0xffffffff return value +def readTTCHeader(file): + file.seek(0) + data = file.read(ttcHeaderSize) + if len(data) != ttcHeaderSize: + raise TTLibError("Not a Font Collection (not enough data)") + self = SimpleNamespace() + sstruct.unpack(ttcHeaderFormat, data, self) + if self.TTCTag != "ttcf": + raise TTLibError("Not a Font Collection") + assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version + self.offsetTable = struct.unpack(">%dL" % self.numFonts, file.read(self.numFonts * 4)) + if self.Version == 0x00020000: + pass # ignoring version 2.0 signatures + return self + +def writeTTCHeader(file, numFonts): + self = SimpleNamespace() + self.TTCTag = 'ttcf' + self.Version = 0x00010000 + self.numFonts = numFonts + file.seek(0) + file.write(sstruct.pack(ttcHeaderFormat, self)) + offset = file.tell() + file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) + return offset if __name__ == "__main__": import sys diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_a_v_a_r.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_a_v_a_r.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_a_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_a_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -70,7 +70,7 @@ segments[fixedToFloat(fromValue, 14)] = fixedToFloat(toValue, 14) pos = pos + 4 - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] for axis in axisTags: writer.begintag("segment", axis=axis) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/C_F_F_.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/C_F_F_.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/C_F_F_.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/C_F_F_.py 2018-07-26 14:12:55.000000000 +0000 @@ -38,8 +38,8 @@ # XXX #self.cff[self.cff.fontNames[0]].setGlyphOrder(glyphOrder) - def toXML(self, writer, otFont, progress=None): - self.cff.toXML(writer, progress) + def toXML(self, writer, otFont): + self.cff.toXML(writer) def fromXML(self, name, attrs, content, otFont): if not hasattr(self, "cff"): diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_c_v_a_r.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_c_v_a_r.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_c_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_c_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -75,7 +75,7 @@ tupleName, tupleAttrs, tupleContent = tupleElement var.fromXML(tupleName, tupleAttrs, tupleContent) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] writer.simpletag("version", major=self.majorVersion, minor=self.minorVersion) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/DefaultTable.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/DefaultTable.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/DefaultTable.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/DefaultTable.py 2018-07-26 14:12:55.000000000 +0000 @@ -17,7 +17,7 @@ def compile(self, ttFont): return self.data - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): if hasattr(self, "ERROR"): writer.comment("An error occurred during the decompilation of this table") writer.newline() diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_f_v_a_r.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_f_v_a_r.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_f_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_f_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -89,7 +89,7 @@ self.instances.append(instance) pos += instanceSize - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): for axis in self.axes: axis.toXML(writer, ttFont) for instance in self.instances: diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_g_l_y_f.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_g_l_y_f.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_g_l_y_f.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_g_l_y_f.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,10 +5,15 @@ from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools import ttLib +from fontTools import version from fontTools.misc.textTools import safeEval, pad from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds -from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + otRound, +) from numbers import Number from . import DefaultTable from . import ttProgram @@ -16,10 +21,17 @@ import struct import array import logging - +import os +from fontTools.misc import xmlWriter +from fontTools.misc.filenames import userNameToFileName log = logging.getLogger(__name__) +# We compute the version the same as is computed in ttlib/__init__ +# so that we can write 'ttLibVersion' attribute of the glyf TTX files +# when glyf is written to separate files. +version = ".".join(version.split('.')[:2]) + # # The Apple and MS rasterizers behave differently for # scaled composite components: one does scale first and then translate @@ -110,37 +122,64 @@ ttFont['maxp'].numGlyphs = len(self.glyphs) return data - def toXML(self, writer, ttFont, progress=None): - writer.newline() + def toXML(self, writer, ttFont, splitGlyphs=False): + notice = ( + "The xMin, yMin, xMax and yMax values\n" + "will be recalculated by the compiler.") glyphNames = ttFont.getGlyphNames() - writer.comment("The xMin, yMin, xMax and yMax values\nwill be recalculated by the compiler.") - writer.newline() - writer.newline() - counter = 0 - progressStep = 10 + if not splitGlyphs: + writer.newline() + writer.comment(notice) + writer.newline() + writer.newline() numGlyphs = len(glyphNames) + if splitGlyphs: + path, ext = os.path.splitext(writer.file.name) + existingGlyphFiles = set() for glyphName in glyphNames: - if not counter % progressStep and progress is not None: - progress.setLabel("Dumping 'glyf' table... (%s)" % glyphName) - progress.increment(progressStep / numGlyphs) - counter = counter + 1 glyph = self[glyphName] if glyph.numberOfContours: - writer.begintag('TTGlyph', [ - ("name", glyphName), - ("xMin", glyph.xMin), - ("yMin", glyph.yMin), - ("xMax", glyph.xMax), - ("yMax", glyph.yMax), - ]) - writer.newline() - glyph.toXML(writer, ttFont) - writer.endtag('TTGlyph') - writer.newline() + if splitGlyphs: + glyphPath = userNameToFileName( + tounicode(glyphName, 'utf-8'), + existingGlyphFiles, + prefix=path + ".", + suffix=ext) + existingGlyphFiles.add(glyphPath.lower()) + glyphWriter = xmlWriter.XMLWriter( + glyphPath, idlefunc=writer.idlefunc, + newlinestr=writer.newlinestr) + glyphWriter.begintag("ttFont", ttLibVersion=version) + glyphWriter.newline() + glyphWriter.begintag("glyf") + glyphWriter.newline() + glyphWriter.comment(notice) + glyphWriter.newline() + writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) + else: + glyphWriter = writer + glyphWriter.begintag('TTGlyph', [ + ("name", glyphName), + ("xMin", glyph.xMin), + ("yMin", glyph.yMin), + ("xMax", glyph.xMax), + ("yMax", glyph.yMax), + ]) + glyphWriter.newline() + glyph.toXML(glyphWriter, ttFont) + glyphWriter.endtag('TTGlyph') + glyphWriter.newline() + if splitGlyphs: + glyphWriter.endtag("glyf") + glyphWriter.newline() + glyphWriter.endtag("ttFont") + glyphWriter.newline() + glyphWriter.close() else: writer.simpletag('TTGlyph', name=glyphName) writer.comment("contains no outline data") - writer.newline() + if not splitGlyphs: + writer.newline() writer.newline() def fromXML(self, name, attrs, content, ttFont): @@ -1079,8 +1118,8 @@ data = data + struct.pack(">HH", self.firstPt, self.secondPt) flags = flags | ARG_1_AND_2_ARE_WORDS else: - x = round(self.x) - y = round(self.y) + x = otRound(self.x) + y = otRound(self.y) flags = flags | ARGS_ARE_XY_VALUES if (-128 <= x <= 127) and (-128 <= y <= 127): data = data + struct.pack(">bb", x, y) @@ -1185,6 +1224,9 @@ def _checkFloat(self, p): if self.isFloat(): return p + if any(v > 0x7FFF or v < -0x8000 for v in p): + self._ensureFloat() + return p if any(isinstance(v, float) for v in p): p = [int(v) if int(v) == v else v for v in p] if any(isinstance(v, float) for v in p): @@ -1224,7 +1266,6 @@ del self._a[i] del self._a[i] - def __repr__(self): return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' @@ -1242,15 +1283,16 @@ return a = array.array("h") for n in self._a: - a.append(round(n)) + a.append(otRound(n)) self._a = a def relativeToAbsolute(self): a = self._a x,y = 0,0 for i in range(len(a) // 2): - a[2*i ] = x = a[2*i ] + x - a[2*i+1] = y = a[2*i+1] + y + x = a[2*i ] + x + y = a[2*i+1] + y + self[i] = (x, y) def absoluteToRelative(self): a = self._a @@ -1260,8 +1302,7 @@ dy = a[2*i+1] - y x = a[2*i ] y = a[2*i+1] - a[2*i ] = dx - a[2*i+1] = dy + self[i] = (dx, dy) def translate(self, p): """ @@ -1270,8 +1311,7 @@ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): - a[2*i ] += x - a[2*i+1] += y + self[i] = (a[2*i] + x, a[2*i+1] + y) def scale(self, p): """ @@ -1280,8 +1320,7 @@ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): - a[2*i ] *= x - a[2*i+1] *= y + self[i] = (a[2*i] * x, a[2*i+1] * y) def transform(self, t): """ @@ -1397,8 +1436,8 @@ other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] += other[i] + for i in range(len(a) // 2): + self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) return self return NotImplemented @@ -1422,8 +1461,8 @@ other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] -= other[i] + for i in range(len(a) // 2): + self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) return self return NotImplemented diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_g_v_a_r.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_g_v_a_r.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_g_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_g_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -156,7 +156,7 @@ packed.byteswap() return (packed.tostring(), tableFormat) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): writer.simpletag("version", value=self.version) writer.newline() writer.simpletag("reserved", value=self.reserved) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_h_h_e_a.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_h_h_e_a.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_h_h_e_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_h_h_e_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -64,9 +64,10 @@ boundsWidthDict[name] = g.xMax - g.xMin elif 'CFF ' in ttFont: topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings for name in ttFont.getGlyphOrder(): - cs = topDict.CharStrings[name] - bounds = cs.calcBounds() + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) if bounds is not None: boundsWidthDict[name] = int( math.ceil(bounds[2]) - math.floor(bounds[0])) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_h_m_t_x.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_h_m_t_x.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_h_m_t_x.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_h_m_t_x.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,5 +1,6 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools import ttLib from fontTools.misc.textTools import safeEval from . import DefaultTable @@ -78,14 +79,14 @@ lastIndex = 1 break additionalMetrics = metrics[lastIndex:] - additionalMetrics = [round(sb) for _, sb in additionalMetrics] + additionalMetrics = [otRound(sb) for _, sb in additionalMetrics] metrics = metrics[:lastIndex] numberOfMetrics = len(metrics) setattr(ttFont[self.headerTag], self.numberOfMetricsName, numberOfMetrics) allMetrics = [] for advance, sb in metrics: - allMetrics.extend([round(advance), round(sb)]) + allMetrics.extend([otRound(advance), otRound(sb)]) metricsFmt = ">" + self.longMetricFormat * numberOfMetrics try: data = struct.pack(metricsFmt, *allMetrics) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_m_e_t_a.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_m_e_t_a.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_m_e_t_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_m_e_t_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -74,7 +74,7 @@ dataOffset += len(data) return bytesjoin([header] + dataMaps + dataBlocks) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): for tag in sorted(self.data.keys()): if tag in ["dlng", "slng"]: writer.begintag("text", tag=tag) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/otBase.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/otBase.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/otBase.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/otBase.py 2018-07-26 14:12:55.000000000 +0000 @@ -42,7 +42,7 @@ self.table.decompile(reader, font) def compile(self, font): - """ Create a top-level OTFWriter for the GPOS/GSUB table. + """ Create a top-level OTTableWriter for the GPOS/GSUB table. Call the compile method for the the table for each 'converter' record in the table converter list call converter's write method for each item in the value. @@ -87,7 +87,13 @@ from .otTables import fixSubTableOverFlows ok = fixSubTableOverFlows(font, overflowRecord) if not ok: - raise + # Try upgrading lookup to Extension and hope + # that cross-lookup sharing not happening would + # fix overflow... + from .otTables import fixLookupOverFlows + ok = fixLookupOverFlows(font, overflowRecord) + if not ok: + raise def toXML(self, writer, font): self.table.toXML2(writer, font) @@ -283,7 +289,7 @@ def __eq__(self, other): if type(self) != type(other): return NotImplemented - return self.items == other.items + return self.longOffset == other.longOffset and self.items == other.items def _doneWriting(self, internedTables): # Convert CountData references to data string items @@ -331,7 +337,6 @@ iRange.reverse() isExtension = hasattr(self, "Extension") - dontShare = hasattr(self, 'DontShare') selfTables = tables @@ -610,22 +615,27 @@ if conv.name == "SubStruct": conv = conv.getConverter(reader.tableTag, table["MorphType"]) - if conv.repeat: - if isinstance(conv.repeat, int): - countValue = conv.repeat - elif conv.repeat in table: - countValue = table[conv.repeat] + try: + if conv.repeat: + if isinstance(conv.repeat, int): + countValue = conv.repeat + elif conv.repeat in table: + countValue = table[conv.repeat] + else: + # conv.repeat is a propagated count + countValue = reader[conv.repeat] + countValue += conv.aux + table[conv.name] = conv.readArray(reader, font, table, countValue) else: - # conv.repeat is a propagated count - countValue = reader[conv.repeat] - countValue += conv.aux - table[conv.name] = conv.readArray(reader, font, table, countValue) - else: - if conv.aux and not eval(conv.aux, None, table): - continue - table[conv.name] = conv.read(reader, font, table) - if conv.isPropagated: - reader[conv.name] = table[conv.name] + if conv.aux and not eval(conv.aux, None, table): + continue + table[conv.name] = conv.read(reader, font, table) + if conv.isPropagated: + reader[conv.name] = table[conv.name] + except Exception as e: + name = conv.name + e.args = e.args + (name,) + raise if hasattr(self, 'postRead'): self.postRead(table, font) @@ -894,7 +904,8 @@ setattr(self, name, None if isDevice else 0) if src is not None: for key,val in src.__dict__.items(): - assert hasattr(self, key) + if not hasattr(self, key): + continue setattr(self, key, val) elif src is not None: self.__dict__ = src.__dict__.copy() diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/otConverters.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/otConverters.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/otConverters.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/otConverters.py 2018-07-26 14:12:55.000000000 +0000 @@ -285,15 +285,16 @@ class NameID(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) - nameTable = font.get("name") if font else None - if nameTable: - name = nameTable.getDebugName(value) - xmlWriter.write(" ") - if name: - xmlWriter.comment(name) - else: - xmlWriter.comment("missing from name table") - log.warning("name id %d missing from name table" % value) + if font and value: + nameTable = font.get("name") + if nameTable: + name = nameTable.getDebugName(value) + xmlWriter.write(" ") + if name: + xmlWriter.comment(name) + else: + xmlWriter.comment("missing from name table") + log.warning("name id %d missing from name table" % value) xmlWriter.newline() @@ -1239,7 +1240,6 @@ actionIndex.setdefault( suffix, suffixIndex) result += a - assert len(result) % self.tableClass.staticSize == 0 return (result, actionIndex) def _compileLigComponents(self, table, font): diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/otData.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/otData.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/otData.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/otData.py 2018-07-26 14:12:55.000000000 +0000 @@ -397,16 +397,16 @@ ('LOffset', 'ExtSubTable', None, None, 'Offset to SubTable'), ]), - ('ValueRecord', [ - ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), - ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), - ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), - ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), - ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), - ]), +# ('ValueRecord', [ +# ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), +# ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), +# ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), +# ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), +# ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), +# ]), ('AnchorFormat1', [ ('uint16', 'AnchorFormat', None, None, 'Format identifier-format = 1'), @@ -970,7 +970,7 @@ ('VarData', [ ('uint16', 'ItemCount', None, None, ''), - ('uint16', 'NumShorts', None, None, ''), # Automatically computed + ('uint16', 'NumShorts', None, None, ''), ('uint16', 'VarRegionCount', None, None, ''), ('uint16', 'VarRegionIndex', 'VarRegionCount', 0, ''), ('VarDataValue', 'Item', 'ItemCount', 0, ''), diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/otTables.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/otTables.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/otTables.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/otTables.py 2018-07-26 14:12:55.000000000 +0000 @@ -8,7 +8,7 @@ from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import * from fontTools.misc.textTools import safeEval -from .otBase import BaseTable, FormatSwitchingBaseTable +from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord import operator import logging import struct @@ -482,7 +482,7 @@ glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID)) else: self.glyphs = [] - log.warning("Unknown Coverage format: %s" % self.Format) + log.warning("Unknown Coverage format: %s", self.Format) def preWrite(self, font): glyphs = getattr(self, "glyphs", None) @@ -546,21 +546,28 @@ def populateDefaults(self, propagator=None): if not hasattr(self, 'mapping'): - self.mapping = [] + self.mapping = {} def postRead(self, rawTable, font): assert (rawTable['EntryFormat'] & 0xFFC0) == 0 - self.mapping = rawTable['mapping'] + glyphOrder = font.getGlyphOrder() + mapList = rawTable['mapping'] + mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) + self.mapping = dict(zip(glyphOrder, mapList)) def preWrite(self, font): mapping = getattr(self, "mapping", None) if mapping is None: - mapping = self.mapping = [] + mapping = self.mapping = {} + + glyphOrder = font.getGlyphOrder() + mapping = [mapping[g] for g in glyphOrder] + while len(mapping) > 1 and mapping[-2] == mapping[-1]: + del mapping[-1] + rawTable = { 'mapping': mapping } rawTable['MappingCount'] = len(mapping) - # TODO Remove this abstraction/optimization and move it varLib.builder? - ored = 0 for idx in mapping: ored |= idx @@ -589,9 +596,9 @@ return rawTable def toXML2(self, xmlWriter, font): - for i, value in enumerate(getattr(self, "mapping", [])): + for glyph, value in sorted(getattr(self, "mapping", {}).items()): attrs = ( - ('index', i), + ('glyph', glyph), ('outer', value >> 16), ('inner', value & 0xFFFF), ) @@ -601,12 +608,16 @@ def fromXML(self, name, attrs, content, font): mapping = getattr(self, "mapping", None) if mapping is None: - mapping = [] + mapping = {} self.mapping = mapping + try: + glyph = attrs['glyph'] + except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 + glyph = font.getGlyphOrder()[attrs['index']] outer = safeEval(attrs['outer']) inner = safeEval(attrs['inner']) assert inner <= 0xFFFF - mapping.append((outer << 16) | inner) + mapping[glyph] = (outer << 16) | inner class SingleSubst(FormatSwitchingBaseTable): @@ -819,7 +830,7 @@ if cls: classDefs[glyphOrder[glyphID]] = cls else: - assert 0, "unknown format: %s" % self.Format + log.warning("Unknown ClassDef format: %s", self.Format) self.classDefs = classDefs def _getClassRanges(self, font): @@ -1270,6 +1281,67 @@ return ok +def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): + # split half of the mark classes to the new subtable + classCount = oldSubTable.ClassCount + if classCount < 2: + # oh well, not much left to split... + return False + + oldClassCount = classCount // 2 + newClassCount = classCount - oldClassCount + + oldMarkCoverage, oldMarkRecords = [], [] + newMarkCoverage, newMarkRecords = [], [] + for glyphName, markRecord in zip( + oldSubTable.MarkCoverage.glyphs, + oldSubTable.MarkArray.MarkRecord + ): + if markRecord.Class < oldClassCount: + oldMarkCoverage.append(glyphName) + oldMarkRecords.append(markRecord) + else: + newMarkCoverage.append(glyphName) + newMarkRecords.append(markRecord) + + oldBaseRecords, newBaseRecords = [], [] + for rec in oldSubTable.BaseArray.BaseRecord: + oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() + oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] + newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] + oldBaseRecords.append(oldBaseRecord) + newBaseRecords.append(newBaseRecord) + + newSubTable.Format = oldSubTable.Format + + oldSubTable.MarkCoverage.glyphs = oldMarkCoverage + newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() + newSubTable.MarkCoverage.Format = oldSubTable.MarkCoverage.Format + newSubTable.MarkCoverage.glyphs = newMarkCoverage + + # share the same BaseCoverage in both halves + newSubTable.BaseCoverage = oldSubTable.BaseCoverage + + oldSubTable.ClassCount = oldClassCount + newSubTable.ClassCount = newClassCount + + oldSubTable.MarkArray.MarkRecord = oldMarkRecords + newSubTable.MarkArray = oldSubTable.MarkArray.__class__() + newSubTable.MarkArray.MarkRecord = newMarkRecords + + oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) + newSubTable.MarkArray.MarkCount = len(newMarkRecords) + + oldSubTable.BaseArray.BaseRecord = oldBaseRecords + newSubTable.BaseArray = oldSubTable.BaseArray.__class__() + newSubTable.BaseArray.BaseRecord = newBaseRecords + + oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) + newSubTable.BaseArray.BaseCount = len(newBaseRecords) + + return True + + splitTable = { 'GSUB': { # 1: splitSingleSubst, # 2: splitMultipleSubst, @@ -1284,7 +1356,7 @@ # 1: splitSinglePos, 2: splitPairPos, # 3: splitCursivePos, -# 4: splitMarkBasePos, + 4: splitMarkBasePos, # 5: splitMarkLigPos, # 6: splitMarkMarkPos, # 7: splitContextPos, @@ -1298,7 +1370,6 @@ """ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. """ - ok = 0 table = ttf[overflowRecord.tableType].table lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] subIndex = overflowRecord.SubTableIndex @@ -1319,7 +1390,7 @@ newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType] newExtSubTable = newExtSubTableClass() newExtSubTable.Format = extSubTable.Format - lookup.SubTable.insert(subIndex + 1, newExtSubTable) + toInsert = newExtSubTable newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] newSubTable = newSubTableClass() @@ -1328,7 +1399,7 @@ subTableType = subtable.__class__.LookupType newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] newSubTable = newSubTableClass() - lookup.SubTable.insert(subIndex + 1, newSubTable) + toInsert = newSubTable if hasattr(lookup, 'SubTableCount'): # may not be defined yet. lookup.SubTableCount = lookup.SubTableCount + 1 @@ -1336,9 +1407,16 @@ try: splitFunc = splitTable[overflowRecord.tableType][subTableType] except KeyError: - return ok + log.error( + "Don't know how to split %s lookup type %s", + overflowRecord.tableType, + subTableType, + ) + return False ok = splitFunc(subtable, newSubTable, overflowRecord) + if ok: + lookup.SubTable.insert(subIndex + 1, toInsert) return ok # End of OverFlow logic diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/sbixGlyph.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/sbixGlyph.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/sbixGlyph.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/sbixGlyph.py 2018-07-26 14:12:55.000000000 +0000 @@ -75,7 +75,7 @@ # (needed if you just want to compile the sbix table on its own) self.gid = struct.pack(">H", ttFont.getGlyphID(self.glyphName)) if self.graphicType is None: - self.rawdata = "" + self.rawdata = b"" else: self.rawdata = sstruct.pack(sbixGlyphHeaderFormat, self) + self.imageData diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_s_b_i_x.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_s_b_i_x.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_s_b_i_x.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_s_b_i_x.py 2018-07-26 14:12:55.000000000 +0000 @@ -69,7 +69,7 @@ del self.numStrikes def compile(self, ttFont): - sbixData = "" + sbixData = b"" self.numStrikes = len(self.strikes) sbixHeader = sstruct.pack(sbixHeaderFormat, self) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/sbixStrike.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/sbixStrike.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/sbixStrike.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/sbixStrike.py 2018-07-26 14:12:55.000000000 +0000 @@ -65,8 +65,8 @@ del self.data def compile(self, ttFont): - self.glyphDataOffsets = "" - self.bitmapData = "" + self.glyphDataOffsets = b"" + self.bitmapData = b"" glyphOrder = ttFont.getGlyphOrder() diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_t_r_a_k.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_t_r_a_k.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_t_r_a_k.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_t_r_a_k.py 2018-07-26 14:12:55.000000000 +0000 @@ -92,7 +92,7 @@ trackData.decompile(data, offset) setattr(self, direction + 'Data', trackData) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): writer.simpletag('version', value=self.version) writer.newline() writer.simpletag('format', value=self.format) @@ -194,7 +194,7 @@ self[entry.track] = entry offset += TRACK_TABLE_ENTRY_FORMAT_SIZE - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): nTracks = len(self) nSizes = len(self.sizes()) writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes)) @@ -254,7 +254,7 @@ self.nameIndex = nameIndex self._map = dict(values) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): name = ttFont["name"].getDebugName(self.nameIndex) writer.begintag( "trackEntry", diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/TupleVariation.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/TupleVariation.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/TupleVariation.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/TupleVariation.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,6 +1,6 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed, otRound from fontTools.misc.textTools import safeEval import array import io @@ -369,7 +369,7 @@ assert runLength >= 1 and runLength <= 64 stream.write(bytechr(runLength - 1)) for i in range(offset, pos): - stream.write(struct.pack('b', round(deltas[i]))) + stream.write(struct.pack('b', otRound(deltas[i]))) return pos @staticmethod @@ -403,7 +403,7 @@ assert runLength >= 1 and runLength <= 64 stream.write(bytechr(DELTAS_ARE_WORDS | (runLength - 1))) for i in range(offset, pos): - stream.write(struct.pack('>h', round(deltas[i]))) + stream.write(struct.pack('>h', otRound(deltas[i]))) return pos @staticmethod diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/tables/_v_h_e_a.py fonttools-3.29.0/Lib/fontTools/ttLib/tables/_v_h_e_a.py --- fonttools-3.21.2/Lib/fontTools/ttLib/tables/_v_h_e_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/tables/_v_h_e_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -63,9 +63,10 @@ boundsHeightDict[name] = g.yMax - g.yMin elif 'CFF ' in ttFont: topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings for name in ttFont.getGlyphOrder(): - cs = topDict.CharStrings[name] - bounds = cs.calcBounds() + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) if bounds is not None: boundsHeightDict[name] = int( math.ceil(bounds[3]) - math.floor(bounds[1])) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/ttCollection.py fonttools-3.29.0/Lib/fontTools/ttLib/ttCollection.py --- fonttools-3.21.2/Lib/fontTools/ttLib/ttCollection.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/ttCollection.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,107 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib.ttFont import TTFont +from fontTools.ttLib.sfnt import readTTCHeader, writeTTCHeader +import struct +import logging + +log = logging.getLogger(__name__) + + +class TTCollection(object): + + """Object representing a TrueType Collection / OpenType Collection. + The main API is self.fonts being a list of TTFont instances. + + If shareTables is True, then different fonts in the collection + might point to the same table object if the data for the table was + the same in the font file. Note, however, that this might result + in suprises and incorrect behavior if the different fonts involved + have different GlyphOrder. Use only if you know what you are doing. + """ + + def __init__(self, file=None, shareTables=False, **kwargs): + fonts = self.fonts = [] + if file is None: + return + + assert 'fontNumber' not in kwargs, kwargs + + if not hasattr(file, "read"): + file = open(file, "rb") + + tableCache = {} if shareTables else None + + header = readTTCHeader(file) + for i in range(header.numFonts): + font = TTFont(file, fontNumber=i, _tableCache=tableCache, **kwargs) + fonts.append(font) + + def save(self, file, shareTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + final = None + file = open(file, "wb") + else: + # assume "file" is a writable file object + # write to a temporary stream to allow saving to unseekable streams + final = file + file = BytesIO() + + tableCache = {} if shareTables else None + + offsets_offset = writeTTCHeader(file, len(self.fonts)) + offsets = [] + for font in self.fonts: + offsets.append(file.tell()) + font._save(file, tableCache=tableCache) + file.seek(0,2) + + file.seek(offsets_offset) + file.write(struct.pack(">%dL" % len(self.fonts), *offsets)) + + if final: + final.write(file.getvalue()) + file.close() + + def saveXML(self, fileOrPath, newlinestr=None, writeVersion=True, **kwargs): + + from fontTools.misc import xmlWriter + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttCollection", ttLibVersion=version) + else: + writer.begintag("ttCollection") + writer.newline() + writer.newline() + + for font in self.fonts: + font._saveXML(writer, writeVersion=False, **kwargs) + writer.newline() + + writer.endtag("ttCollection") + writer.newline() + + writer.close() + + + def __getitem__(self, item): + return self.fonts[item] + + def __setitem__(self, item, value): + self.fonts[item] = values + + def __delitem__(self, item): + return self.fonts[item] + + def __len__(self): + return len(self.fonts) + + def __iter__(self): + return iter(self.fonts) diff -Nru fonttools-3.21.2/Lib/fontTools/ttLib/ttFont.py fonttools-3.29.0/Lib/fontTools/ttLib/ttFont.py --- fonttools-3.21.2/Lib/fontTools/ttLib/ttFont.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttLib/ttFont.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,1002 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc import xmlWriter +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import deprecateArgument +from fontTools.ttLib import TTLibError +from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter +import os +import logging +import itertools + +log = logging.getLogger(__name__) + +class TTFont(object): + + """The main font object. It manages file input and output, and offers + a convenient way of accessing tables. + Tables will be only decompiled when necessary, ie. when they're actually + accessed. This means that simple operations can be extremely fast. + """ + + def __init__(self, file=None, res_name_or_index=None, + sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, + verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, + recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, + _tableCache=None): + + """The constructor can be called with a few different arguments. + When reading a font from disk, 'file' should be either a pathname + pointing to a file, or a readable file object. + + It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt + resource name or an sfnt resource index number or zero. The latter + case will cause TTLib to autodetect whether the file is a flat file + or a suitcase. (If it's a suitcase, only the first 'sfnt' resource + will be read!) + + The 'checkChecksums' argument is used to specify how sfnt + checksums are treated upon reading a file from disk: + 0: don't check (default) + 1: check, print warnings if a wrong checksum is found + 2: check, raise an exception if a wrong checksum is found. + + The TTFont constructor can also be called without a 'file' + argument: this is the way to create a new empty font. + In this case you can optionally supply the 'sfntVersion' argument, + and a 'flavor' which can be None, 'woff', or 'woff2'. + + If the recalcBBoxes argument is false, a number of things will *not* + be recalculated upon save/compile: + 1) 'glyf' glyph bounding boxes + 2) 'CFF ' font bounding box + 3) 'head' font bounding box + 4) 'hhea' min/max values + 5) 'vhea' min/max values + (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). + Additionally, upon importing an TTX file, this option cause glyphs + to be compiled right away. This should reduce memory consumption + greatly, and therefore should have some impact on the time needed + to parse/compile large fonts. + + If the recalcTimestamp argument is false, the modified timestamp in the + 'head' table will *not* be recalculated upon save/compile. + + If the allowVID argument is set to true, then virtual GID's are + supported. Asking for a glyph ID with a glyph name or GID that is not in + the font will return a virtual GID. This is valid for GSUB and cmap + tables. For SING glyphlets, the cmap table is used to specify Unicode + values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested + and does not exist in the font, or the glyphname has the form glyphN + and does not exist in the font, then N is used as the virtual GID. + Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new + virtual GIDs, the next is one less than the previous. + + If ignoreDecompileErrors is set to True, exceptions raised in + individual tables during decompilation will be ignored, falling + back to the DefaultTable implementation, which simply keeps the + binary data. + + If lazy is set to True, many data structures are loaded lazily, upon + access only. If it is set to False, many data structures are loaded + immediately. The default is lazy=None which is somewhere in between. + """ + + for name in ("verbose", "quiet"): + val = locals().get(name) + if val is not None: + deprecateArgument(name, "configure logging instead") + setattr(self, name, val) + + self.lazy = lazy + self.recalcBBoxes = recalcBBoxes + self.recalcTimestamp = recalcTimestamp + self.tables = {} + self.reader = None + + # Permit the user to reference glyphs that are not int the font. + self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. + self.reverseVIDDict = {} + self.VIDDict = {} + self.allowVID = allowVID + self.ignoreDecompileErrors = ignoreDecompileErrors + + if not file: + self.sfntVersion = sfntVersion + self.flavor = flavor + self.flavorData = None + return + if not hasattr(file, "read"): + closeStream = True + # assume file is a string + if res_name_or_index is not None: + # see if it contains 'sfnt' resources in the resource or data fork + from . import macUtils + if res_name_or_index == 0: + if macUtils.getSFNTResIndices(file): + # get the first available sfnt font. + file = macUtils.SFNTResourceReader(file, 1) + else: + file = open(file, "rb") + else: + file = macUtils.SFNTResourceReader(file, res_name_or_index) + else: + file = open(file, "rb") + else: + # assume "file" is a readable file object + closeStream = False + file.seek(0) + + if not self.lazy: + # read input file in memory and wrap a stream around it to allow overwriting + file.seek(0) + tmp = BytesIO(file.read()) + if hasattr(file, 'name'): + # save reference to input file name + tmp.name = file.name + if closeStream: + file.close() + file = tmp + self._tableCache = _tableCache + self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) + self.sfntVersion = self.reader.sfntVersion + self.flavor = self.reader.flavor + self.flavorData = self.reader.flavorData + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + """If we still have a reader object, close it.""" + if self.reader is not None: + self.reader.close() + + def save(self, file, reorderTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + if self.lazy and self.reader.file.name == file: + raise TTLibError( + "Can't overwrite TTFont when 'lazy' attribute is True") + closeStream = True + file = open(file, "wb") + else: + # assume "file" is a writable file object + closeStream = False + + tmp = BytesIO() + + writer_reordersTables = self._save(tmp) + + if (reorderTables is None or writer_reordersTables or + (reorderTables is False and self.reader is None)): + # don't reorder tables and save as is + file.write(tmp.getvalue()) + tmp.close() + else: + if reorderTables is False: + # sort tables using the original font's order + tableOrder = list(self.reader.keys()) + else: + # use the recommended order from the OpenType specification + tableOrder = None + tmp.flush() + tmp2 = BytesIO() + reorderFontTables(tmp, tmp2, tableOrder) + file.write(tmp2.getvalue()) + tmp.close() + tmp2.close() + + if closeStream: + file.close() + + def _save(self, file, tableCache=None): + """Internal function, to be shared by save() and TTCollection.save()""" + + if self.recalcTimestamp and 'head' in self: + self['head'] # make sure 'head' is loaded so the recalculation is actually done + + tags = list(self.keys()) + if "GlyphOrder" in tags: + tags.remove("GlyphOrder") + numTables = len(tags) + # write to a temporary stream to allow saving to unseekable streams + writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData) + + done = [] + for tag in tags: + self._writeTable(tag, writer, done, tableCache) + + writer.close() + + return writer.reordersTables() + + def saveXML(self, fileOrPath, newlinestr=None, **kwargs): + """Export the font as TTX (an XML-based text file), or as a series of text + files when splitTables is true. In the latter case, the 'fileOrPath' + argument should be a path to a directory. + The 'tables' argument must either be false (dump all tables) or a + list of tables to dump. The 'skipTables' argument may be a list of tables + to skip, but only when the 'tables' argument is false. + """ + + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + self._saveXML(writer, **kwargs) + writer.close() + + def _saveXML(self, writer, + writeVersion=True, + quiet=None, tables=None, skipTables=None, splitTables=False, + splitGlyphs=False, disassembleInstructions=True, + bitmapGlyphDataFormat='raw'): + + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + self.disassembleInstructions = disassembleInstructions + self.bitmapGlyphDataFormat = bitmapGlyphDataFormat + if not tables: + tables = list(self.keys()) + if "GlyphOrder" not in tables: + tables = ["GlyphOrder"] + tables + if skipTables: + for tag in skipTables: + if tag in tables: + tables.remove(tag) + numTables = len(tables) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], + ttLibVersion=version) + else: + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) + writer.newline() + + # always splitTables if splitGlyphs is enabled + splitTables = splitTables or splitGlyphs + + if not splitTables: + writer.newline() + else: + path, ext = os.path.splitext(writer.filename) + fileNameTemplate = path + ".%s" + ext + + for i in range(numTables): + tag = tables[i] + if splitTables: + tablePath = fileNameTemplate % tagToIdentifier(tag) + tableWriter = xmlWriter.XMLWriter(tablePath, + newlinestr=writer.newlinestr) + tableWriter.begintag("ttFont", ttLibVersion=version) + tableWriter.newline() + tableWriter.newline() + writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) + writer.newline() + else: + tableWriter = writer + self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) + if splitTables: + tableWriter.endtag("ttFont") + tableWriter.newline() + tableWriter.close() + writer.endtag("ttFont") + writer.newline() + + def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + if tag in self: + table = self[tag] + report = "Dumping '%s' table..." % tag + else: + report = "No '%s' table found." % tag + log.info(report) + if tag not in self: + return + xmlTag = tagToXML(tag) + attrs = dict() + if hasattr(table, "ERROR"): + attrs['ERROR'] = "decompilation error" + from .tables.DefaultTable import DefaultTable + if table.__class__ == DefaultTable: + attrs['raw'] = True + writer.begintag(xmlTag, **attrs) + writer.newline() + if tag == "glyf": + table.toXML(writer, self, splitGlyphs=splitGlyphs) + else: + table.toXML(writer, self) + writer.endtag(xmlTag) + writer.newline() + writer.newline() + + def importXML(self, fileOrPath, quiet=None): + """Import a TTX file (an XML-based text format), so as to recreate + a font object. + """ + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + if "maxp" in self and "post" in self: + # Make sure the glyph order is loaded, as it otherwise gets + # lost if the XML doesn't contain the glyph order, yet does + # contain the table which was originally used to extract the + # glyph names from (ie. 'post', 'cmap' or 'CFF '). + self.getGlyphOrder() + + from fontTools.misc import xmlReader + + reader = xmlReader.XMLReader(fileOrPath, self) + reader.read() + + def isLoaded(self, tag): + """Return true if the table identified by 'tag' has been + decompiled and loaded into memory.""" + return tag in self.tables + + def has_key(self, tag): + if self.isLoaded(tag): + return True + elif self.reader and tag in self.reader: + return True + elif tag == "GlyphOrder": + return True + else: + return False + + __contains__ = has_key + + def keys(self): + keys = list(self.tables.keys()) + if self.reader: + for key in list(self.reader.keys()): + if key not in keys: + keys.append(key) + + if "GlyphOrder" in keys: + keys.remove("GlyphOrder") + keys = sortedTagList(keys) + return ["GlyphOrder"] + keys + + def __len__(self): + return len(list(self.keys())) + + def __getitem__(self, tag): + tag = Tag(tag) + try: + return self.tables[tag] + except KeyError: + if tag == "GlyphOrder": + table = GlyphOrder(tag) + self.tables[tag] = table + return table + if self.reader is not None: + import traceback + log.debug("Reading '%s' table from disk", tag) + data = self.reader[tag] + if self._tableCache is not None: + table = self._tableCache.get((Tag(tag), data)) + if table is not None: + return table + tableClass = getTableClass(tag) + table = tableClass(tag) + self.tables[tag] = table + log.debug("Decompiling '%s' table", tag) + try: + table.decompile(data, self) + except: + if not self.ignoreDecompileErrors: + raise + # fall back to DefaultTable, retaining the binary table data + log.exception( + "An exception occurred during the decompilation of the '%s' table", tag) + from .tables.DefaultTable import DefaultTable + file = StringIO() + traceback.print_exc(file=file) + table = DefaultTable(tag) + table.ERROR = file.getvalue() + self.tables[tag] = table + table.decompile(data, self) + if self._tableCache is not None: + self._tableCache[(Tag(tag), data)] = table + return table + else: + raise KeyError("'%s' table not found" % tag) + + def __setitem__(self, tag, table): + self.tables[Tag(tag)] = table + + def __delitem__(self, tag): + if tag not in self: + raise KeyError("'%s' table not found" % tag) + if tag in self.tables: + del self.tables[tag] + if self.reader and tag in self.reader: + del self.reader[tag] + + def get(self, tag, default=None): + try: + return self[tag] + except KeyError: + return default + + def setGlyphOrder(self, glyphOrder): + self.glyphOrder = glyphOrder + + def getGlyphOrder(self): + try: + return self.glyphOrder + except AttributeError: + pass + if 'CFF ' in self: + cff = self['CFF '] + self.glyphOrder = cff.getGlyphOrder() + elif 'post' in self: + # TrueType font + glyphOrder = self['post'].getGlyphOrder() + if glyphOrder is None: + # + # No names found in the 'post' table. + # Try to create glyph names from the unicode cmap (if available) + # in combination with the Adobe Glyph List (AGL). + # + self._getGlyphNamesFromCmap() + else: + self.glyphOrder = glyphOrder + else: + self._getGlyphNamesFromCmap() + return self.glyphOrder + + def _getGlyphNamesFromCmap(self): + # + # This is rather convoluted, but then again, it's an interesting problem: + # - we need to use the unicode values found in the cmap table to + # build glyph names (eg. because there is only a minimal post table, + # or none at all). + # - but the cmap parser also needs glyph names to work with... + # So here's what we do: + # - make up glyph names based on glyphID + # - load a temporary cmap table based on those names + # - extract the unicode values, build the "real" glyph names + # - unload the temporary cmap table + # + if self.isLoaded("cmap"): + # Bootstrapping: we're getting called by the cmap parser + # itself. This means self.tables['cmap'] contains a partially + # loaded cmap, making it impossible to get at a unicode + # subtable here. We remove the partially loaded cmap and + # restore it later. + # This only happens if the cmap table is loaded before any + # other table that does f.getGlyphOrder() or f.getGlyphName(). + cmapLoading = self.tables['cmap'] + del self.tables['cmap'] + else: + cmapLoading = None + # Make up glyph names based on glyphID, which will be used by the + # temporary cmap and by the real cmap in case we don't find a unicode + # cmap. + numGlyphs = int(self['maxp'].numGlyphs) + glyphOrder = [None] * numGlyphs + glyphOrder[0] = ".notdef" + for i in range(1, numGlyphs): + glyphOrder[i] = "glyph%.5d" % i + # Set the glyph order, so the cmap parser has something + # to work with (so we don't get called recursively). + self.glyphOrder = glyphOrder + + # Make up glyph names based on the reversed cmap table. Because some + # glyphs (eg. ligatures or alternates) may not be reachable via cmap, + # this naming table will usually not cover all glyphs in the font. + # If the font has no Unicode cmap table, reversecmap will be empty. + if 'cmap' in self: + reversecmap = self['cmap'].buildReversed() + else: + reversecmap = {} + useCount = {} + for i in range(numGlyphs): + tempName = glyphOrder[i] + if tempName in reversecmap: + # If a font maps both U+0041 LATIN CAPITAL LETTER A and + # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, + # we prefer naming the glyph as "A". + glyphName = self._makeGlyphName(min(reversecmap[tempName])) + numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 + if numUses > 1: + glyphName = "%s.alt%d" % (glyphName, numUses - 1) + glyphOrder[i] = glyphName + + if 'cmap' in self: + # Delete the temporary cmap table from the cache, so it can + # be parsed again with the right names. + del self.tables['cmap'] + self.glyphOrder = glyphOrder + if cmapLoading: + # restore partially loaded cmap, so it can continue loading + # using the proper names. + self.tables['cmap'] = cmapLoading + + @staticmethod + def _makeGlyphName(codepoint): + from fontTools import agl # Adobe Glyph List + if codepoint in agl.UV2AGL: + return agl.UV2AGL[codepoint] + elif codepoint <= 0xFFFF: + return "uni%04X" % codepoint + else: + return "u%X" % codepoint + + def getGlyphNames(self): + """Get a list of glyph names, sorted alphabetically.""" + glyphNames = sorted(self.getGlyphOrder()) + return glyphNames + + def getGlyphNames2(self): + """Get a list of glyph names, sorted alphabetically, + but not case sensitive. + """ + from fontTools.misc import textTools + return textTools.caselessSort(self.getGlyphOrder()) + + def getGlyphName(self, glyphID, requireReal=False): + try: + return self.getGlyphOrder()[glyphID] + except IndexError: + if requireReal or not self.allowVID: + # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in + # the cmap table than there are glyphs. I don't think it's legal... + return "glyph%.5d" % glyphID + else: + # user intends virtual GID support + try: + glyphName = self.VIDDict[glyphID] + except KeyError: + glyphName ="glyph%.5d" % glyphID + self.last_vid = min(glyphID, self.last_vid ) + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphName + + def getGlyphID(self, glyphName, requireReal=False): + if not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + glyphOrder = self.getGlyphOrder() + d = self._reverseGlyphOrderDict + if glyphName not in d: + if glyphName in glyphOrder: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + else: + if requireReal: + raise KeyError(glyphName) + elif not self.allowVID: + # Handle glyphXXX only + if glyphName[:5] == "glyph": + try: + return int(glyphName[5:]) + except (NameError, ValueError): + raise KeyError(glyphName) + else: + # user intends virtual GID support + try: + glyphID = self.reverseVIDDict[glyphName] + except KeyError: + # if name is in glyphXXX format, use the specified name. + if glyphName[:5] == "glyph": + try: + glyphID = int(glyphName[5:]) + except (NameError, ValueError): + glyphID = None + if glyphID is None: + glyphID = self.last_vid -1 + self.last_vid = glyphID + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphID + + glyphID = d[glyphName] + if glyphName != glyphOrder[glyphID]: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + return glyphID + + def getReverseGlyphMap(self, rebuild=False): + if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + return self._reverseGlyphOrderDict + + def _buildReverseGlyphOrderDict(self): + self._reverseGlyphOrderDict = d = {} + glyphOrder = self.getGlyphOrder() + for glyphID in range(len(glyphOrder)): + d[glyphOrder[glyphID]] = glyphID + + def _writeTable(self, tag, writer, done, tableCache=None): + """Internal helper function for self.save(). Keeps track of + inter-table dependencies. + """ + if tag in done: + return + tableClass = getTableClass(tag) + for masterTable in tableClass.dependencies: + if masterTable not in done: + if masterTable in self: + self._writeTable(masterTable, writer, done, tableCache) + else: + done.append(masterTable) + done.append(tag) + tabledata = self.getTableData(tag) + if tableCache is not None: + entry = tableCache.get((Tag(tag), tabledata)) + if entry is not None: + log.debug("reusing '%s' table", tag) + writer.setEntry(tag, entry) + return + log.debug("writing '%s' table to disk", tag) + writer[tag] = tabledata + if tableCache is not None: + tableCache[(Tag(tag), tabledata)] = writer[tag] + + def getTableData(self, tag): + """Returns raw table data, whether compiled or directly read from disk. + """ + tag = Tag(tag) + if self.isLoaded(tag): + log.debug("compiling '%s' table", tag) + return self.tables[tag].compile(self) + elif self.reader and tag in self.reader: + log.debug("Reading '%s' table from disk", tag) + return self.reader[tag] + else: + raise KeyError(tag) + + def getGlyphSet(self, preferCFF=True): + """Return a generic GlyphSet, which is a dict-like object + mapping glyph names to glyph objects. The returned glyph objects + have a .draw() method that supports the Pen protocol, and will + have an attribute named 'width'. + + If the font is CFF-based, the outlines will be taken from the 'CFF ' or + 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. + If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use + the 'preferCFF' argument to specify which one should be taken. If the + font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. + """ + glyphs = None + if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or + ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): + table_tag = "CFF2" if "CFF2" in self else "CFF " + glyphs = _TTGlyphSet(self, + list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) + + if glyphs is None and "glyf" in self: + glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) + + if glyphs is None: + raise TTLibError("Font contains no outlines") + + return glyphs + + def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): + """Return the 'best' unicode cmap dictionary available in the font, + or None, if no unicode cmap subtable is available. + + By default it will search for the following (platformID, platEncID) + pairs: + (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) + This can be customized via the cmapPreferences argument. + """ + return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) + + +class _TTGlyphSet(object): + + """Generic dict-like GlyphSet class that pulls metrics from hmtx and + glyph shape from TrueType or CFF. + """ + + def __init__(self, ttFont, glyphs, glyphType): + self._glyphs = glyphs + self._hmtx = ttFont['hmtx'] + self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None + self._glyphType = glyphType + + def keys(self): + return list(self._glyphs.keys()) + + def has_key(self, glyphName): + return glyphName in self._glyphs + + __contains__ = has_key + + def __getitem__(self, glyphName): + horizontalMetrics = self._hmtx[glyphName] + verticalMetrics = self._vmtx[glyphName] if self._vmtx else None + return self._glyphType( + self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) + + def __len__(self): + return len(self._glyphs) + + def get(self, glyphName, default=None): + try: + return self[glyphName] + except KeyError: + return default + +class _TTGlyph(object): + + """Wrapper for a TrueType glyph that supports the Pen protocol, meaning + that it has a .draw() method that takes a pen object as its only + argument. Additionally there are 'width' and 'lsb' attributes, read from + the 'hmtx' table. + + If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' + attributes. + """ + + def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): + self._glyphset = glyphset + self._glyph = glyph + self.width, self.lsb = horizontalMetrics + if verticalMetrics: + self.height, self.tsb = verticalMetrics + else: + self.height, self.tsb = None, None + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + self._glyph.draw(pen) + +class _TTGlyphCFF(_TTGlyph): + pass + +class _TTGlyphGlyf(_TTGlyph): + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + glyfTable = self._glyphset._glyphs + glyph = self._glyph + offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 + glyph.draw(pen, glyfTable, offset) + + +class GlyphOrder(object): + + """A pseudo table. The glyph order isn't in the font as a separate + table, but it's nice to present it as such in the TTX format. + """ + + def __init__(self, tag=None): + pass + + def toXML(self, writer, ttFont): + glyphOrder = ttFont.getGlyphOrder() + writer.comment("The 'id' attribute is only for humans; " + "it is ignored when parsed.") + writer.newline() + for i in range(len(glyphOrder)): + glyphName = glyphOrder[i] + writer.simpletag("GlyphID", id=i, name=glyphName) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "glyphOrder"): + self.glyphOrder = [] + ttFont.setGlyphOrder(self.glyphOrder) + if name == "GlyphID": + self.glyphOrder.append(attrs["name"]) + + +def getTableModule(tag): + """Fetch the packer/unpacker module for a table. + Return None when no module is found. + """ + from . import tables + pyTag = tagToIdentifier(tag) + try: + __import__("fontTools.ttLib.tables." + pyTag) + except ImportError as err: + # If pyTag is found in the ImportError message, + # means table is not implemented. If it's not + # there, then some other module is missing, don't + # suppress the error. + if str(err).find(pyTag) >= 0: + return None + else: + raise err + else: + return getattr(tables, pyTag) + + +def getTableClass(tag): + """Fetch the packer/unpacker class for a table. + Return None when no class is found. + """ + module = getTableModule(tag) + if module is None: + from .tables.DefaultTable import DefaultTable + return DefaultTable + pyTag = tagToIdentifier(tag) + tableClass = getattr(module, "table_" + pyTag) + return tableClass + + +def getClassTag(klass): + """Fetch the table tag for a class object.""" + name = klass.__name__ + assert name[:6] == 'table_' + name = name[6:] # Chop 'table_' + return identifierToTag(name) + + +def newTable(tag): + """Return a new instance of a table.""" + tableClass = getTableClass(tag) + return tableClass(tag) + + +def _escapechar(c): + """Helper function for tagToIdentifier()""" + import re + if re.match("[a-z0-9]", c): + return "_" + c + elif re.match("[A-Z]", c): + return c + "_" + else: + return hex(byteord(c))[2:] + + +def tagToIdentifier(tag): + """Convert a table tag to a valid (but UGLY) python identifier, + as well as a filename that's guaranteed to be unique even on a + caseless file system. Each character is mapped to two characters. + Lowercase letters get an underscore before the letter, uppercase + letters get an underscore after the letter. Trailing spaces are + trimmed. Illegal characters are escaped as two hex bytes. If the + result starts with a number (as the result of a hex escape), an + extra underscore is prepended. Examples: + 'glyf' -> '_g_l_y_f' + 'cvt ' -> '_c_v_t' + 'OS/2' -> 'O_S_2f_2' + """ + import re + tag = Tag(tag) + if tag == "GlyphOrder": + return tag + assert len(tag) == 4, "tag should be 4 characters long" + while len(tag) > 1 and tag[-1] == ' ': + tag = tag[:-1] + ident = "" + for c in tag: + ident = ident + _escapechar(c) + if re.match("[0-9]", ident): + ident = "_" + ident + return ident + + +def identifierToTag(ident): + """the opposite of tagToIdentifier()""" + if ident == "GlyphOrder": + return ident + if len(ident) % 2 and ident[0] == "_": + ident = ident[1:] + assert not (len(ident) % 2) + tag = "" + for i in range(0, len(ident), 2): + if ident[i] == "_": + tag = tag + ident[i+1] + elif ident[i+1] == "_": + tag = tag + ident[i] + else: + # assume hex + tag = tag + chr(int(ident[i:i+2], 16)) + # append trailing spaces + tag = tag + (4 - len(tag)) * ' ' + return Tag(tag) + + +def tagToXML(tag): + """Similarly to tagToIdentifier(), this converts a TT tag + to a valid XML element name. Since XML element names are + case sensitive, this is a fairly simple/readable translation. + """ + import re + tag = Tag(tag) + if tag == "OS/2": + return "OS_2" + elif tag == "GlyphOrder": + return tag + if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): + return tag.strip() + else: + return tagToIdentifier(tag) + + +def xmlToTag(tag): + """The opposite of tagToXML()""" + if tag == "OS_2": + return Tag("OS/2") + if len(tag) == 8: + return identifierToTag(tag) + else: + return Tag(tag + " " * (4 - len(tag))) + + + +# Table order as recommended in the OpenType specification 1.4 +TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", + "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", + "kern", "name", "post", "gasp", "PCLT"] + +OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", + "CFF "] + +def sortedTagList(tagList, tableOrder=None): + """Return a sorted copy of tagList, sorted according to the OpenType + specification, or according to a custom tableOrder. If given and not + None, tableOrder needs to be a list of tag names. + """ + tagList = sorted(tagList) + if tableOrder is None: + if "DSIG" in tagList: + # DSIG should be last (XXX spec reference?) + tagList.remove("DSIG") + tagList.append("DSIG") + if "CFF " in tagList: + tableOrder = OTFTableOrder + else: + tableOrder = TTFTableOrder + orderedTables = [] + for tag in tableOrder: + if tag in tagList: + orderedTables.append(tag) + tagList.remove(tag) + orderedTables.extend(tagList) + return orderedTables + + +def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): + """Rewrite a font file, ordering the tables as recommended by the + OpenType specification 1.4. + """ + inFile.seek(0) + outFile.seek(0) + reader = SFNTReader(inFile, checkChecksums=checkChecksums) + writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) + tables = list(reader.keys()) + for tag in sortedTagList(tables, tableOrder): + writer[tag] = reader[tag] + writer.close() + + +def maxPowerOfTwo(x): + """Return the highest exponent of two, so that + (2 ** exponent) <= x. Return 0 if x is 0. + """ + exponent = 0 + while x: + x = x >> 1 + exponent = exponent + 1 + return max(exponent - 1, 0) + + +def getSearchRange(n, itemSize=16): + """Calculate searchRange, entrySelector, rangeShift. + """ + # itemSize defaults to 16, for backward compatibility + # with upstream fonttools. + exponent = maxPowerOfTwo(n) + searchRange = (2 ** exponent) * itemSize + entrySelector = exponent + rangeShift = max(0, n * itemSize - searchRange) + return searchRange, entrySelector, rangeShift diff -Nru fonttools-3.21.2/Lib/fontTools/ttx.py fonttools-3.29.0/Lib/fontTools/ttx.py --- fonttools-3.21.2/Lib/fontTools/ttx.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/ttx.py 2018-07-26 14:12:55.000000000 +0000 @@ -38,6 +38,10 @@ to the individual table dumps. This file can be used as input to ttx, as long as the table files are in the same directory. + -g Split glyf table: Save the glyf data into separate TTX files + per glyph and write a small TTX for the glyf table which + contains references to the individual TTGlyph elements. + NOTE: specifying -g implies -s (no need for -s together with -g) -i Do NOT disassemble TT instructions: when this option is given, all TrueType programs (glyph programs, the font program and the pre-program) will be written to the TTX file as hex data @@ -110,6 +114,7 @@ verbose = False quiet = False splitTables = False + splitGlyphs = False disassembleInstructions = True mergeFile = None recalcBBoxes = True @@ -160,6 +165,10 @@ self.skipTables.append(value) elif option == "-s": self.splitTables = True + elif option == "-g": + # -g implies (and forces) splitTables + self.splitGlyphs = True + self.splitTables = True elif option == "-i": self.disassembleInstructions = False elif option == "-z": @@ -209,7 +218,6 @@ self.logLevel = logging.INFO if self.mergeFile and self.flavor: raise getopt.GetoptError("-m and --flavor options are mutually exclusive") - sys.exit(2) if self.onlyTables and self.skipTables: raise getopt.GetoptError("-t and -x options are mutually exclusive") if self.mergeFile and numFiles > 1: @@ -223,9 +231,9 @@ reader = ttf.reader tags = sorted(reader.keys()) print('Listing table info for "%s":' % input) - format = " %4s %10s %7s %7s" - print(format % ("tag ", " checksum", " length", " offset")) - print(format % ("----", "----------", "-------", "-------")) + format = " %4s %10s %8s %8s" + print(format % ("tag ", " checksum", " length", " offset")) + print(format % ("----", "----------", "--------", "--------")) for tag in tags: entry = reader.tables[tag] if ttf.flavor == "woff2": @@ -255,6 +263,7 @@ tables=options.onlyTables, skipTables=options.skipTables, splitTables=options.splitTables, + splitGlyphs=options.splitGlyphs, disassembleInstructions=options.disassembleInstructions, bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, newlinestr=options.newlinestr) @@ -318,7 +327,7 @@ def parseOptions(args): - rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sim:z:baey:", + rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", ['unicodedata=', "recalc-timestamp", 'flavor=', 'version', 'with-zopfli', 'newline=']) diff -Nru fonttools-3.21.2/Lib/fontTools/unicodedata/__init__.py fonttools-3.29.0/Lib/fontTools/unicodedata/__init__.py --- fonttools-3.21.2/Lib/fontTools/unicodedata/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/unicodedata/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -13,7 +13,7 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * -from . import Blocks, Scripts, ScriptExtensions +from . import Blocks, Scripts, ScriptExtensions, OTTags __all__ = [tostr(s) for s in ( @@ -38,6 +38,9 @@ "script_extension", "script_name", "script_code", + "script_horizontal_direction", + "ot_tags_from_script", + "ot_tag_to_script", )] @@ -72,7 +75,7 @@ >>> script_extension("a") == {'Latn'} True - >>> script_extension(unichr(0x060C)) == {'Arab', 'Syrc', 'Thaa'} + >>> script_extension(unichr(0x060C)) == {'Arab', 'Rohg', 'Syrc', 'Thaa'} True >>> script_extension(unichr(0x10FFFF)) == {'Zzzz'} True @@ -133,6 +136,75 @@ return default +# The data on script direction is taken from harfbuzz's "hb-common.cc": +# https://goo.gl/X5FDXC +# It matches the CLDR "scriptMetadata.txt as of January 2018: +# http://unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt +RTL_SCRIPTS = { + # Unicode-1.1 additions + 'Arab', # Arabic + 'Hebr', # Hebrew + + # Unicode-3.0 additions + 'Syrc', # Syriac + 'Thaa', # Thaana + + # Unicode-4.0 additions + 'Cprt', # Cypriot + + # Unicode-4.1 additions + 'Khar', # Kharoshthi + + # Unicode-5.0 additions + 'Phnx', # Phoenician + 'Nkoo', # Nko + + # Unicode-5.1 additions + 'Lydi', # Lydian + + # Unicode-5.2 additions + 'Avst', # Avestan + 'Armi', # Imperial Aramaic + 'Phli', # Inscriptional Pahlavi + 'Prti', # Inscriptional Parthian + 'Sarb', # Old South Arabian + 'Orkh', # Old Turkic + 'Samr', # Samaritan + + # Unicode-6.0 additions + 'Mand', # Mandaic + + # Unicode-6.1 additions + 'Merc', # Meroitic Cursive + 'Mero', # Meroitic Hieroglyphs + + # Unicode-7.0 additions + 'Mani', # Manichaean + 'Mend', # Mende Kikakui + 'Nbat', # Nabataean + 'Narb', # Old North Arabian + 'Palm', # Palmyrene + 'Phlp', # Psalter Pahlavi + + # Unicode-8.0 additions + 'Hatr', # Hatran + 'Hung', # Old Hungarian + + # Unicode-9.0 additions + 'Adlm', # Adlam +} + +def script_horizontal_direction(script_code, default=KeyError): + """ Return "RTL" for scripts that contain right-to-left characters + according to the Bidi_Class property. Otherwise return "LTR". + """ + if script_code not in Scripts.NAMES: + if isinstance(default, type) and issubclass(default, KeyError): + raise default(script_code) + return default + return str("RTL") if script_code in RTL_SCRIPTS else str("LTR") + + def block(char): """ Return the block property assigned to the Unicode character 'char' as a string. @@ -147,3 +219,58 @@ code = byteord(char) i = bisect_right(Blocks.RANGES, code) return Blocks.VALUES[i-1] + + +def ot_tags_from_script(script_code): + """ Return a list of OpenType script tags associated with a given + Unicode script code. + Return ['DFLT'] script tag for invalid/unknown script codes. + """ + if script_code not in Scripts.NAMES: + return [OTTags.DEFAULT_SCRIPT] + + script_tags = [ + OTTags.SCRIPT_EXCEPTIONS.get( + script_code, + script_code[0].lower() + script_code[1:] + ) + ] + if script_code in OTTags.NEW_SCRIPT_TAGS: + script_tags.extend(OTTags.NEW_SCRIPT_TAGS[script_code]) + script_tags.reverse() # last in, first out + + return script_tags + + +def ot_tag_to_script(tag): + """ Return the Unicode script code for the given OpenType script tag, or + None for "DFLT" tag or if there is no Unicode script associated with it. + Raises ValueError if the tag is invalid. + """ + tag = tostr(tag).strip() + if not tag or " " in tag or len(tag) > 4: + raise ValueError("invalid OpenType tag: %r" % tag) + + while len(tag) != 4: + tag += str(" ") # pad with spaces + + if tag == OTTags.DEFAULT_SCRIPT: + # it's unclear which Unicode script the "DFLT" OpenType tag maps to, + # so here we return None + return None + + if tag in OTTags.NEW_SCRIPT_TAGS_REVERSED: + return OTTags.NEW_SCRIPT_TAGS_REVERSED[tag] + + # This side of the conversion is fully algorithmic + + # Any spaces at the end of the tag are replaced by repeating the last + # letter. Eg 'nko ' -> 'Nkoo'. + # Change first char to uppercase + script_code = tag[0].upper() + tag[1] + for i in range(2, 4): + script_code += (script_code[i-1] if tag[i] == " " else tag[i]) + + if script_code not in Scripts.NAMES: + return None + return script_code diff -Nru fonttools-3.21.2/Lib/fontTools/unicodedata/OTTags.py fonttools-3.29.0/Lib/fontTools/unicodedata/OTTags.py --- fonttools-3.21.2/Lib/fontTools/unicodedata/OTTags.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/unicodedata/OTTags.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,41 @@ +# Data updated to OpenType 1.8.2 as of January 2018. + +# Complete list of OpenType script tags at: +# https://www.microsoft.com/typography/otspec/scripttags.htm + +# Most of the script tags are the same as the ISO 15924 tag but lowercased, +# so we only have to handle the exceptional cases: +# - KATAKANA and HIRAGANA both map to 'kana'; +# - spaces at the end are preserved, unlike ISO 15924; +# - we map special script codes for Inherited, Common and Unknown to DFLT. + +DEFAULT_SCRIPT = "DFLT" + +SCRIPT_EXCEPTIONS = { + "Hira": "kana", + "Hrkt": "kana", + "Laoo": "lao ", + "Yiii": "yi ", + "Nkoo": "nko ", + "Vaii": "vai ", + "Zinh": DEFAULT_SCRIPT, + "Zyyy": DEFAULT_SCRIPT, + "Zzzz": DEFAULT_SCRIPT, +} + +NEW_SCRIPT_TAGS = { + "Beng": ("bng2",), + "Deva": ("dev2",), + "Gujr": ("gjr2",), + "Guru": ("gur2",), + "Knda": ("knd2",), + "Mlym": ("mlm2",), + "Orya": ("ory2",), + "Taml": ("tml2",), + "Telu": ("tel2",), + "Mymr": ("mym2",), +} + +NEW_SCRIPT_TAGS_REVERSED = { + value: key for key, values in NEW_SCRIPT_TAGS.items() for value in values +} diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/builder.py fonttools-3.29.0/Lib/fontTools/varLib/builder.py --- fonttools-3.21.2/Lib/fontTools/varLib/builder.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/builder.py 2018-07-26 14:12:55.000000000 +0000 @@ -28,31 +28,35 @@ return self -def _reorderItem(lst, narrows): +def _reorderItem(lst, narrows, zeroes): out = [] count = len(lst) for i in range(count): if i not in narrows: out.append(lst[i]) for i in range(count): - if i in narrows: + if i in narrows and i not in zeroes: out.append(lst[i]) return out -def varDataCalculateNumShorts(self, optimize=True): +def VarData_CalculateNumShorts(self, optimize=True): count = self.VarRegionCount items = self.Item narrows = set(range(count)) + zeroes = set(range(count)) for item in items: wides = [i for i in narrows if not (-128 <= item[i] <= 127)] narrows.difference_update(wides) - if not narrows: + nonzeroes = [i for i in zeroes if item[i]] + zeroes.difference_update(nonzeroes) + if not narrows and not zeroes: break if optimize: # Reorder columns such that all SHORT columns come before UINT8 - self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows) + self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes) + self.VarRegionCount = len(self.VarRegionIndex) for i in range(self.ItemCount): - items[i] = _reorderItem(items[i], narrows) + items[i] = _reorderItem(items[i], narrows, zeroes) self.NumShorts = count - len(narrows) else: wides = set(range(count)) - narrows @@ -69,7 +73,7 @@ assert len(item) == regionCount records.append(list(item)) self.ItemCount = len(self.Item) - varDataCalculateNumShorts(self, optimize=optimize) + VarData_CalculateNumShorts(self, optimize=optimize) return self @@ -84,10 +88,9 @@ # Variation helpers -def buildVarIdxMap(varIdxes): - # TODO Change VarIdxMap mapping to hold separate outer,inner indices +def buildVarIdxMap(varIdxes, glyphOrder): self = ot.VarIdxMap() - self.mapping = list(varIdxes) + self.mapping = {g:v for g,v in zip(glyphOrder, varIdxes)} return self def buildVarDevTable(varIdx): diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/featureVars.py fonttools-3.29.0/Lib/fontTools/varLib/featureVars.py --- fonttools-3.21.2/Lib/fontTools/varLib/featureVars.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/featureVars.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,392 @@ +"""Module to build FeatureVariation tables: +https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table + +NOTE: The API is experimental and subject to change. +""" +from __future__ import print_function, absolute_import, division + +from fontTools.ttLib import newTable +from fontTools.ttLib.tables import otTables as ot +from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable +import itertools + + +def addFeatureVariations(font, conditionalSubstitutions): + """Add conditional substitutions to a Variable Font. + + The `conditionalSubstitutions` argument is a list of (Region, Substitutions) + tuples. + + A Region is a list of Spaces. A Space is a dict mapping axisTags to + (minValue, maxValue) tuples. Irrelevant axes may be omitted. + A Space represents a 'rectangular' subset of an N-dimensional design space. + A Region represents a more complex subset of an N-dimensional design space, + ie. the union of all the Spaces in the Region. + For efficiency, Spaces within a Region should ideally not overlap, but + functionality is not compromised if they do. + + The minimum and maximum values are expressed in normalized coordinates. + + A Substitution is a dict mapping source glyph names to substitute glyph names. + """ + + # Example: + # + # >>> f = TTFont(srcPath) + # >>> condSubst = [ + # ... # A list of (Region, Substitution) tuples. + # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), + # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), + # ... ] + # >>> addFeatureVariations(f, condSubst) + # >>> f.save(dstPath) + + # Since the FeatureVariations table will only ever match one rule at a time, + # we will make new rules for all possible combinations of our input, so we + # can indirectly support overlapping rules. + explodedConditionalSubstitutions = [] + for combination in iterAllCombinations(len(conditionalSubstitutions)): + regions = [] + lookups = [] + for index in combination: + regions.append(conditionalSubstitutions[index][0]) + lookups.append(conditionalSubstitutions[index][1]) + if not regions: + continue + intersection = regions[0] + for region in regions[1:]: + intersection = intersectRegions(intersection, region) + for space in intersection: + # Remove default values, so we don't generate redundant ConditionSets + space = cleanupSpace(space) + if space: + explodedConditionalSubstitutions.append((space, lookups)) + + addFeatureVariationsRaw(font, explodedConditionalSubstitutions) + + +def iterAllCombinations(numRules): + """Given a number of rules, yield all the combinations of indices, sorted + by decreasing length, so we get the most specialized rules first. + + >>> list(iterAllCombinations(0)) + [] + >>> list(iterAllCombinations(1)) + [(0,)] + >>> list(iterAllCombinations(2)) + [(0, 1), (0,), (1,)] + >>> list(iterAllCombinations(3)) + [(0, 1, 2), (0, 1), (0, 2), (1, 2), (0,), (1,), (2,)] + """ + indices = range(numRules) + for length in range(numRules, 0, -1): + for combinations in itertools.combinations(indices, length): + yield combinations + + +# +# Region and Space support +# +# Terminology: +# +# A 'Space' is a dict representing a "rectangular" bit of N-dimensional space. +# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. +# Missing dimensions (keys) are substituted by the default min and max values +# from the corresponding axes. +# +# A 'Region' is a list of Space dicts, representing the union of the Spaces, +# therefore representing a more complex subset of design space. +# + +def intersectRegions(region1, region2): + """Return the region intersecting `region1` and `region2`. + + >>> intersectRegions([], []) + [] + >>> intersectRegions([{'wdth': (0.0, 1.0)}], []) + [] + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-1.0, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0)}], [{'wght': (-1.0, 0.0)}]) + True + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], [{'wght': (-1.0, 0.0)}]) + True + >>> intersectRegions( + ... [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], + ... [{'wdth': (-1.0, 0.0), 'wght': (-1.0, 0.0)}]) + [] + + """ + region = [] + for space1 in region1: + for space2 in region2: + space = intersectSpaces(space1, space2) + if space is not None: + region.append(space) + return region + + +def intersectSpaces(space1, space2): + """Return the space intersected by `space1` and `space2`, or None if there + is no intersection. + + >>> intersectSpaces({}, {}) + {} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {}) + {'wdth': (-0.5, 0.5)} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 0.5)} + >>> expected = {'wdth': (0.0, 0.5), 'wght': (0.25, 0.5)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5), 'wght': (0.0, 0.5)}, {'wdth': (0.0, 1.0), 'wght': (0.25, 0.75)}) + True + >>> expected = {'wdth': (-0.5, 0.5), 'wght': (0.0, 1.0)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5)}, {'wght': (0.0, 1.0)}) + True + >>> intersectSpaces({'wdth': (-0.5, 0)}, {'wdth': (0.1, 0.5)}) + + """ + space = {} + space.update(space1) + space.update(space2) + for axisTag in set(space1) & set(space2): + min1, max1 = space1[axisTag] + min2, max2 = space2[axisTag] + minimum = max(min1, min2) + maximum = min(max1, max2) + if not minimum < maximum: + return None + space[axisTag] = minimum, maximum + return space + + +def cleanupSpace(space): + """Return a sparse copy of `space`, without redundant (default) values. + + >>> cleanupSpace({}) + {} + >>> cleanupSpace({'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 1.0)} + >>> cleanupSpace({'wdth': (-1.0, 1.0)}) + {} + + """ + return {tag: limit for tag, limit in space.items() if limit != (-1.0, 1.0)} + + +# +# Low level implementation +# + +def addFeatureVariationsRaw(font, conditionalSubstitutions): + """Low level implementation of addFeatureVariations that directly + models the possibilities of the FeatureVariations table.""" + + # + # assert there is no 'rvrn' feature + # make dummy 'rvrn' feature with no lookups + # sort features, get 'rvrn' feature index + # add 'rvrn' feature to all scripts + # make lookups + # add feature variations + # + + if "GSUB" not in font: + font["GSUB"] = buildGSUB() + + gsub = font["GSUB"].table + + if gsub.Version < 0x00010001: + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.FeatureVariations = None # delete any existing FeatureVariations + + for feature in gsub.FeatureList.FeatureRecord: + assert feature.FeatureTag != 'rvrn' + + rvrnFeature = buildFeatureRecord('rvrn', []) + gsub.FeatureList.FeatureRecord.append(rvrnFeature) + + sortFeatureList(gsub) + rvrnFeatureIndex = gsub.FeatureList.FeatureRecord.index(rvrnFeature) + + for scriptRecord in gsub.ScriptList.ScriptRecord: + for langSys in [scriptRecord.Script.DefaultLangSys] + scriptRecord.Script.LangSysRecord: + langSys.FeatureIndex.append(rvrnFeatureIndex) + + # setup lookups + + # turn substitution dicts into tuples of tuples, so they are hashable + conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(conditionalSubstitutions) + + lookupMap = buildSubstitutionLookups(gsub, allSubstitutions) + + axisIndices = {axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)} + + featureVariationRecords = [] + for conditionSet, substitutions in conditionalSubstitutions: + conditionTable = [] + for axisTag, (minValue, maxValue) in sorted(conditionSet.items()): + assert minValue < maxValue + ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) + conditionTable.append(ct) + + lookupIndices = [lookupMap[subst] for subst in substitutions] + record = buildFeatureTableSubstitutionRecord(rvrnFeatureIndex, lookupIndices) + featureVariationRecords.append(buildFeatureVariationRecord(conditionTable, [record])) + + gsub.FeatureVariations = buildFeatureVariations(featureVariationRecords) + + +# +# Building GSUB/FeatureVariations internals +# + +def buildGSUB(): + """Build a GSUB table from scratch.""" + fontTable = newTable("GSUB") + gsub = fontTable.table = ot.GSUB() + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.ScriptList = ot.ScriptList() + gsub.ScriptList.ScriptRecord = [] + gsub.FeatureList = ot.FeatureList() + gsub.FeatureList.FeatureRecord = [] + gsub.LookupList = ot.LookupList() + gsub.LookupList.Lookup = [] + + srec = ot.ScriptRecord() + srec.ScriptTag = 'DFLT' + srec.Script = ot.Script() + srec.Script.DefaultLangSys = None + srec.Script.LangSysRecord = [] + + langrec = ot.LangSysRecord() + langrec.LangSys = ot.LangSys() + langrec.LangSys.ReqFeatureIndex = 0xFFFF + langrec.LangSys.FeatureIndex = [0] + srec.Script.DefaultLangSys = langrec.LangSys + + gsub.ScriptList.ScriptRecord.append(srec) + gsub.FeatureVariations = None + + return fontTable + + +def makeSubstitutionsHashable(conditionalSubstitutions): + """Turn all the substitution dictionaries in sorted tuples of tuples so + they are hashable, to detect duplicates so we don't write out redundant + data.""" + allSubstitutions = set() + condSubst = [] + for conditionSet, substitutionMaps in conditionalSubstitutions: + substitutions = [] + for substitutionMap in substitutionMaps: + subst = tuple(sorted(substitutionMap.items())) + substitutions.append(subst) + allSubstitutions.add(subst) + condSubst.append((conditionSet, substitutions)) + return condSubst, sorted(allSubstitutions) + + +def buildSubstitutionLookups(gsub, allSubstitutions): + """Build the lookups for the glyph substitutions, return a dict mapping + the substitution to lookup indices.""" + firstIndex = len(gsub.LookupList.Lookup) + lookupMap = {} + for i, substitutionMap in enumerate(allSubstitutions): + lookupMap[substitutionMap] = i + firstIndex + + for subst in allSubstitutions: + substMap = dict(subst) + lookup = buildLookup([buildSingleSubstSubtable(substMap)]) + gsub.LookupList.Lookup.append(lookup) + assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup + return lookupMap + + +def buildFeatureVariations(featureVariationRecords): + """Build the FeatureVariations subtable.""" + fv = ot.FeatureVariations() + fv.Version = 0x00010000 + fv.FeatureVariationRecord = featureVariationRecords + return fv + + +def buildFeatureRecord(featureTag, lookupListIndices): + """Build a FeatureRecord.""" + fr = ot.FeatureRecord() + fr.FeatureTag = featureTag + fr.Feature = ot.Feature() + fr.Feature.LookupListIndex = lookupListIndices + return fr + + +def buildFeatureVariationRecord(conditionTable, substitutionRecords): + """Build a FeatureVariationRecord.""" + fvr = ot.FeatureVariationRecord() + fvr.ConditionSet = ot.ConditionSet() + fvr.ConditionSet.ConditionTable = conditionTable + fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() + fvr.FeatureTableSubstitution.Version = 0x00010001 + fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords + return fvr + + +def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices): + """Build a FeatureTableSubstitutionRecord.""" + ftsr = ot.FeatureTableSubstitutionRecord() + ftsr.FeatureIndex = featureIndex + ftsr.Feature = ot.Feature() + ftsr.Feature.LookupListIndex = lookupListIndices + return ftsr + + +def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue): + """Build a ConditionTable.""" + ct = ot.ConditionTable() + ct.Format = 1 + ct.AxisIndex = axisIndex + ct.FilterRangeMinValue = filterRangeMinValue + ct.FilterRangeMaxValue = filterRangeMaxValue + return ct + + +def sortFeatureList(table): + """Sort the feature list by feature tag, and remap the feature indices + elsewhere. This is needed after the feature list has been modified. + """ + # decorate, sort, undecorate, because we need to make an index remapping table + tagIndexFea = [(fea.FeatureTag, index, fea) for index, fea in enumerate(table.FeatureList.FeatureRecord)] + tagIndexFea.sort() + table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea] + featureRemap = dict(zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))) + + # Remap the feature indices + remapFeatures(table, featureRemap) + + +def remapFeatures(table, featureRemap): + """Go through the scripts list, and remap feature indices.""" + for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord): + defaultLangSys = script.Script.DefaultLangSys + if defaultLangSys is not None: + _remapLangSys(defaultLangSys, featureRemap) + for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord): + langSys = langSysRec.LangSys + _remapLangSys(langSys, featureRemap) + + if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: + for fvr in table.FeatureVariations.FeatureVariationRecord: + for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: + ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex] + + +def _remapLangSys(langSys, featureRemap): + if langSys.ReqFeatureIndex != 0xffff: + langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex] + langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex] + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/__init__.py fonttools-3.29.0/Lib/fontTools/varLib/__init__.py --- fonttools-3.21.2/Lib/fontTools/varLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -10,17 +10,18 @@ them. Such ttf-interpolatable and designspace files can be generated from a Glyphs source, eg., using noto-source as an example: - $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs + $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs Then you can make a variable-font this way: - $ fonttools varLib master_ufo/NotoSansArabic.designspace + $ fonttools varLib master_ufo/NotoSansArabic.designspace API *will* change in near future. """ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc.arrayTools import Vector from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.tables._n_a_m_e import NameRecord @@ -29,6 +30,7 @@ from fontTools.ttLib.tables.ttProgram import Program from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import OTTableWriter from fontTools.varLib import builder, designspace, models, varStore from fontTools.varLib.merger import VariationMerger, _all_equal from fontTools.varLib.mvar import MVAR_ENTRIES @@ -168,20 +170,25 @@ return avar def _add_stat(font, axes): + # for now we just get the axis tags and nameIDs from the fvar, + # so we can reuse the same nameIDs which were defined in there. + # TODO make use of 'axes' once it adds style attributes info: + # https://github.com/LettError/designSpaceDocument/issues/8 - nameTable = font['name'] + if "STAT" in font: + return + + fvarTable = font['fvar'] - assert "STAT" not in font STAT = font["STAT"] = newTable('STAT') stat = STAT.table = ot.STAT() - stat.Version = 0x00010000 + stat.Version = 0x00010002 axisRecords = [] - for i,a in enumerate(axes.values()): + for i, a in enumerate(fvarTable.axes): axis = ot.AxisRecord() - axis.AxisTag = Tag(a.tag) - # Meh. Reuse fvar nameID! - axis.AxisNameID = nameTable.addName(tounicode(a.labelname['en'])) + axis.AxisTag = Tag(a.axisTag) + axis.AxisNameID = a.axisNameID axis.AxisOrdering = i axisRecords.append(axis) @@ -192,6 +199,10 @@ stat.DesignAxisCount = len(axisRecords) stat.DesignAxisRecord = axisRecordArray + # for the elided fallback name, we default to the base style name. + # TODO make this user-configurable via designspace document + stat.ElidedFallbackNameID = 2 + # TODO Move to glyf or gvar table proper def _GetCoordinates(font, glyphName): """font, glyphName --> glyph coordinates as expected by "gvar" table @@ -258,8 +269,12 @@ glyph.recalcBounds(glyf) - horizontalAdvanceWidth = round(rightSideX - leftSideX) - leftSideBearing = round(glyph.xMin - leftSideX) + horizontalAdvanceWidth = otRound(rightSideX - leftSideX) + if horizontalAdvanceWidth < 0: + # unlikely, but it can happen, see: + # https://github.com/fonttools/fonttools/pull/1198 + horizontalAdvanceWidth = 0 + leftSideBearing = otRound(glyph.xMin - leftSideX) # XXX Handle vertical font["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing @@ -397,7 +412,7 @@ deltas = model.getDeltas(all_cvs) supports = model.supports for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): - delta = [round(d) for d in delta] + delta = [otRound(d) for d in delta] if all(abs(v) <= tolerance for v in delta): continue var = TupleVariation(support, delta) @@ -412,46 +427,58 @@ for glyph in font.getGlyphOrder(): hAdvances = [metrics[glyph][0] for metrics in metricses] # TODO move round somewhere else? - hAdvanceDeltas[glyph] = tuple(round(d) for d in model.getDeltas(hAdvances)[1:]) - - # We only support the direct mapping right now. + hAdvanceDeltas[glyph] = tuple(otRound(d) for d in model.getDeltas(hAdvances)[1:]) + # Direct mapping supports = model.supports[1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) n = len(supports) items = [] - zeroes = [0]*n for glyphName in font.getGlyphOrder(): - items.append(hAdvanceDeltas.get(glyphName, zeroes)) - while items and items[-1] is zeroes: - del items[-1] - - advanceMapping = None - # Add indirect mapping to save on duplicates - uniq = set(items) - # TODO Improve heuristic - if (len(items) - len(uniq)) * len(varTupleIndexes) > len(items): - newItems = sorted(uniq) - mapper = {v:i for i,v in enumerate(newItems)} - mapping = [mapper[item] for item in items] - while len(mapping) > 1 and mapping[-1] == mapping[-2]: - del mapping[-1] - advanceMapping = builder.buildVarIdxMap(mapping) - items = newItems - del mapper, mapping, newItems - del uniq + items.append(hAdvanceDeltas[glyphName]) + + # Build indirect mapping to save on duplicates, compare both sizes + uniq = list(set(items)) + mapper = {v:i for i,v in enumerate(uniq)} + mapping = [mapper[item] for item in items] + advanceMapping = builder.buildVarIdxMap(mapping, font.getGlyphOrder()) + # Direct varData = builder.buildVarData(varTupleIndexes, items) - varstore = builder.buildVarStore(varTupleList, [varData]) + directStore = builder.buildVarStore(varTupleList, [varData]) + + # Indirect + varData = builder.buildVarData(varTupleIndexes, uniq) + indirectStore = builder.buildVarStore(varTupleList, [varData]) + mapping = indirectStore.optimize() + advanceMapping.mapping = {k:mapping[v] for k,v in advanceMapping.mapping.items()} + + # Compile both, see which is more compact + + writer = OTTableWriter() + directStore.compile(writer, font) + directSize = len(writer.getAllData()) + + writer = OTTableWriter() + indirectStore.compile(writer, font) + advanceMapping.compile(writer, font) + indirectSize = len(writer.getAllData()) + + use_direct = directSize < indirectSize + # Done; put it all together. assert "HVAR" not in font HVAR = font["HVAR"] = newTable('HVAR') hvar = HVAR.table = ot.HVAR() hvar.Version = 0x00010000 - hvar.VarStore = varstore - hvar.AdvWidthMap = advanceMapping hvar.LsbMap = hvar.RsbMap = None + if use_direct: + hvar.VarStore = directStore + hvar.AdvWidthMap = None + else: + hvar.VarStore = indirectStore + hvar.AdvWidthMap = advanceMapping def _add_MVAR(font, model, master_ttfs, axisTags): @@ -494,11 +521,17 @@ assert "MVAR" not in font if records: + store = store_builder.finish() + # Optimize + mapping = store.optimize() + for rec in records: + rec.VarIdx = mapping[rec.VarIdx] + MVAR = font["MVAR"] = newTable('MVAR') mvar = MVAR.table = ot.MVAR() mvar.Version = 0x00010000 mvar.Reserved = 0 - mvar.VarStore = store_builder.finish() + mvar.VarStore = store # XXX these should not be hard-coded but computed automatically mvar.ValueRecordSize = 8 mvar.ValueRecordCount = len(records) @@ -511,7 +544,11 @@ merger = VariationMerger(model, axisTags, font) merger.mergeTables(font, master_fonts, ['GPOS']) + # TODO Merge GSUB + # TODO Merge GDEF itself! store = merger.store_builder.finish() + if not store.VarData: + return try: GDEF = font['GDEF'].table assert GDEF.Version <= 0x00010002 @@ -522,6 +559,12 @@ GDEF.Version = 0x00010003 GDEF.VarStore = store + # Optimize + varidx_map = store.optimize() + GDEF.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + # Pretty much all of this file should be redesigned and moved inot submodules... @@ -653,8 +696,8 @@ # Normalize master locations - normalized_master_locs = [o['location'] for o in masters] - log.info("Internal master locations:\n%s", pformat(normalized_master_locs)) + internal_master_locs = [o['location'] for o in masters] + log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} @@ -663,7 +706,7 @@ internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) - normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs] + normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) @@ -679,7 +722,7 @@ return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances -def build(designspace_filename, master_finder=lambda s:s): +def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True): """ Build variation font from a designspace file. @@ -700,8 +743,10 @@ # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(vf, axes, instances) - _add_stat(vf, axes) - _add_avar(vf, axes) + if 'STAT' not in exclude: + _add_stat(vf, axes) + if 'avar' not in exclude: + _add_avar(vf, axes) del instances # Map from axis names to axis tags... @@ -715,32 +760,102 @@ assert 0 == model.mapping[base_idx] log.info("Building variations tables") - _add_MVAR(vf, model, master_fonts, axisTags) - _add_HVAR(vf, model, master_fonts, axisTags) - _merge_OTL(vf, model, master_fonts, axisTags) - if 'glyf' in vf: - _add_gvar(vf, model, master_fonts) + if 'MVAR' not in exclude: + _add_MVAR(vf, model, master_fonts, axisTags) + if 'HVAR' not in exclude: + _add_HVAR(vf, model, master_fonts, axisTags) + if 'GDEF' not in exclude or 'GPOS' not in exclude: + _merge_OTL(vf, model, master_fonts, axisTags) + if 'gvar' not in exclude and 'glyf' in vf: + _add_gvar(vf, model, master_fonts, optimize=optimize) + if 'cvar' not in exclude and 'glyf' in vf: _merge_TTHinting(vf, model, master_fonts) + for tag in exclude: + if tag in vf: + del vf[tag] + return vf, model, master_ttfs +class MasterFinder(object): + + def __init__(self, template): + self.template = template + + def __call__(self, src_path): + fullname = os.path.abspath(src_path) + dirname, basename = os.path.split(fullname) + stem, ext = os.path.splitext(basename) + path = self.template.format( + fullname=fullname, + dirname=dirname, + basename=basename, + stem=stem, + ext=ext, + ) + return os.path.normpath(path) + + def main(args=None): from argparse import ArgumentParser from fontTools import configLogger parser = ArgumentParser(prog='varLib') parser.add_argument('designspace') + parser.add_argument( + '-o', + metavar='OUTPUTFILE', + dest='outfile', + default=None, + help='output file' + ) + parser.add_argument( + '-x', + metavar='TAG', + dest='exclude', + action='append', + default=[], + help='exclude table' + ) + parser.add_argument( + '--disable-iup', + dest='optimize', + action='store_false', + help='do not perform IUP optimization' + ) + parser.add_argument( + '--master-finder', + default='master_ttf_interpolatable/{stem}.ttf', + help=( + 'templated string used for finding binary font ' + 'files given the source file names defined in the ' + 'designspace document. The following special strings ' + 'are defined: {fullname} is the absolute source file ' + 'name; {basename} is the file name without its ' + 'directory; {stem} is the basename without the file ' + 'extension; {ext} is the source file extension; ' + '{dirname} is the directory of the absolute file ' + 'name. The default value is "%(default)s".' + ) + ) options = parser.parse_args(args) # TODO: allow user to configure logging via command-line options configLogger(level="INFO") designspace_filename = options.designspace - finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf') - outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' - - vf, model, master_ttfs = build(designspace_filename, finder) + finder = MasterFinder(options.master_finder) + outfile = options.outfile + if outfile is None: + outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' + + vf, model, master_ttfs = build( + designspace_filename, + finder, + exclude=options.exclude, + optimize=options.optimize + ) log.info("Saving variation font %s", outfile) vf.save(outfile) diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/merger.py fonttools-3.29.0/Lib/fontTools/varLib/merger.py --- fonttools-3.21.2/Lib/fontTools/varLib/merger.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/merger.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,6 +3,7 @@ """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc import classifyTools from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otBase as otBase @@ -660,8 +661,8 @@ YCoords = [a.YCoordinate for a in lst] model = merger.model scalars = merger.scalars - self.XCoordinate = round(model.interpolateFromMastersAndScalars(XCoords, scalars)) - self.YCoordinate = round(model.interpolateFromMastersAndScalars(YCoords, scalars)) + self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) + self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) @InstancerMerger.merger(otBase.ValueRecord) def merge(merger, self, lst): @@ -677,7 +678,7 @@ if hasattr(self, name): values = [getattr(a, name, 0) for a in lst] - value = round(model.interpolateFromMastersAndScalars(values, scalars)) + value = otRound(model.interpolateFromMastersAndScalars(values, scalars)) setattr(self, name, value) @@ -738,7 +739,7 @@ assert dev.DeltaFormat == 0x8000 varidx = (dev.StartSize << 16) + dev.EndSize - delta = round(instancer[varidx]) + delta = otRound(instancer[varidx]) attr = v+'Coordinate' setattr(self, attr, getattr(self, attr) + delta) @@ -769,7 +770,7 @@ assert dev.DeltaFormat == 0x8000 varidx = (dev.StartSize << 16) + dev.EndSize - delta = round(instancer[varidx]) + delta = otRound(instancer[varidx]) setattr(self, name, getattr(self, name) + delta) diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/models.py fonttools-3.29.0/Lib/fontTools/varLib/models.py --- fonttools-3.21.2/Lib/fontTools/varLib/models.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/models.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,6 +4,7 @@ __all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel'] + def normalizeValue(v, triple): """Normalizes value based on a min/default/max triple. >>> normalizeValue(400, (100, 400, 900)) @@ -152,12 +153,12 @@ {0: 1.0}, {0: 1.0}, {0: 1.0, 4: 1.0, 5: 1.0}, - {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, {0: 1.0, 3: 0.75, 4: 0.25, 5: 0.6666666666666667, - 6: 0.16666666666666669, + 6: 0.4444444444444445, 7: 0.6666666666666667}] """ @@ -170,7 +171,7 @@ self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above - self._computeMasterSupports(axisPoints) + self._computeMasterSupports(axisPoints, axisOrder) @staticmethod def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): @@ -183,7 +184,9 @@ value = loc[axis] if axis not in axisPoints: axisPoints[axis] = {0.} - assert value not in axisPoints[axis] + assert value not in axisPoints[axis], ( + 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints) + ) axisPoints[axis].add(value) def getKey(axisPoints, axisOrder): @@ -221,7 +224,7 @@ else: return value - def _computeMasterSupports(self, axisPoints): + def _computeMasterSupports(self, axisPoints, axisOrder): supports = [] deltaWeights = [] locations = self.locations @@ -229,11 +232,15 @@ box = {} # Account for axisPoints first + # TODO Use axis min/max instead? Isn't that always -1/+1? for axis,values in axisPoints.items(): if not axis in loc: continue locV = loc[axis] - box[axis] = (self.lowerBound(locV, values), locV, self.upperBound(locV, values)) + if locV > 0: + box[axis] = (0, locV, max({locV}|values)) + else: + box[axis] = (min({locV}|values), locV, 0) locAxes = set(loc.keys()) # Walk over previous masters now @@ -243,21 +250,42 @@ continue # If it's NOT in the current box, it does not participate relevant = True - for axis, (lower,_,upper) in box.items(): - if axis in m and not (lower < m[axis] < upper): + for axis, (lower,peak,upper) in box.items(): + if axis not in m or not (m[axis] == peak or lower < m[axis] < upper): relevant = False break if not relevant: continue - # Split the box for new master - for axis,val in m.items(): + + # Split the box for new master; split in whatever direction + # that has largest range ratio. See commit for details. + orderedAxes = [axis for axis in axisOrder if axis in m.keys()] + orderedAxes.extend([axis for axis in sorted(m.keys()) if axis not in axisOrder]) + bestAxis = None + bestRatio = -1 + for axis in orderedAxes: + val = m[axis] assert axis in box lower,locV,upper = box[axis] + newLower, newUpper = lower, upper if val < locV: - lower = val + newLower = val + ratio = (val - locV) / (lower - locV) elif locV < val: - upper = val - box[axis] = (lower,locV,upper) + newUpper = val + ratio = (val - locV) / (upper - locV) + else: # val == locV + # Can't split box in this direction. + continue + if ratio > bestRatio: + bestRatio = ratio + bestAxis = axis + bestLower = newLower + bestUpper = newUpper + bestLocV = locV + + if bestAxis: + box[bestAxis] = (bestLower,bestLocV,bestUpper) supports.append(box) deltaWeight = {} @@ -311,6 +339,46 @@ return self.interpolateFromDeltasAndScalars(deltas, scalars) +def main(args): + from fontTools import configLogger + + args = args[1:] + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + if len(args) < 1: + print("usage: fonttools varLib.models source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr) + sys.exit(1) + + from pprint import pprint + + if len(args) == 1 and args[0].endswith('.designspace'): + from fontTools.designspaceLib import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read(args[0]) + locs = [s.location for s in doc.sources] + print("Original locations:") + pprint(locs) + doc.normalize() + print("Normalized locations:") + pprint(locs) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + + model = VariationModel(locs) + print("Sorted locations:") + pprint(model.locations) + print("Supports:") + pprint(model.supports) + if __name__ == "__main__": import doctest, sys + + if len(sys.argv) > 1: + sys.exit(main(sys.argv)) + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/mutator.py fonttools-3.29.0/Lib/fontTools/varLib/mutator.py --- fonttools-3.21.2/Lib/fontTools/varLib/mutator.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/mutator.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,11 +1,11 @@ """ Instantiate a variation font. Run, eg: -$ python mutator.py ./NotoSansArabic-VF.ttf wght=140 wdth=85 +$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85 """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import floatToFixedToFloat +from fontTools.misc.fixedTools import floatToFixedToFloat, otRound from fontTools.ttLib import TTFont from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.varLib import _GetCoordinates, _SetCoordinates, _DesignspaceAxis @@ -20,6 +20,13 @@ log = logging.getLogger("fontTools.varlib.mutator") +# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest +OS2_WIDTH_CLASS_VALUES = {} +percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0] +for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1): + half = (prev + curr) / 2 + OS2_WIDTH_CLASS_VALUES[half] = i + def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary @@ -87,7 +94,7 @@ if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): - cvt[i] += round(delta) + cvt[i] += otRound(delta) if 'MVAR' in varfont: log.info("Mutating MVAR table") @@ -99,7 +106,7 @@ if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] - delta = round(varStoreInstancer[rec.VarIdx]) + delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, @@ -112,6 +119,32 @@ log.info("Building interpolated tables") merger.instantiate() + if 'name' in varfont: + log.info("Pruning name table") + exclude = {a.axisNameID for a in fvar.axes} + for i in fvar.instances: + exclude.add(i.subfamilyNameID) + exclude.add(i.postscriptNameID) + varfont['name'].names[:] = [ + n for n in varfont['name'].names + if n.nameID not in exclude + ] + + if "wght" in location and "OS/2" in varfont: + varfont["OS/2"].usWeightClass = otRound( + max(1, min(location["wght"], 1000)) + ) + if "wdth" in location: + wdth = location["wdth"] + for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): + if wdth < percent: + varfont["OS/2"].usWidthClass = widthClass + break + else: + varfont["OS/2"].usWidthClass = 9 + if "slnt" in location and "post" in varfont: + varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) + log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: @@ -122,23 +155,44 @@ def main(args=None): from fontTools import configLogger + import argparse - if args is None: - import sys - args = sys.argv[1:] - - varfilename = args[0] - locargs = args[1:] - outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' - - # TODO Allow to specify logging verbosity as command line option - configLogger(level=logging.INFO) + parser = argparse.ArgumentParser( + "fonttools varLib.mutator", description="Instantiate a variable font") + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input variable TTF file.") + parser.add_argument( + "locargs", metavar="AXIS=LOC", nargs="*", + help="List of space separated locations. A location consist in " + "the name of a variation axis, followed by '=' and a number. E.g.: " + " wght=700 wdth=80. The default is the location of the base master.") + parser.add_argument( + "-o", "--output", metavar="OUTPUT.ttf", default=None, + help="Output instance TTF file (default: INPUT-instance.ttf).") + logging_group = parser.add_mutually_exclusive_group(required=False) + logging_group.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely.") + logging_group.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off.") + options = parser.parse_args(args) + + varfilename = options.input + outfile = ( + os.path.splitext(varfilename)[0] + '-instance.ttf' + if not options.output else options.output) + configLogger(level=( + "DEBUG" if options.verbose else + "ERROR" if options.quiet else + "INFO")) loc = {} - for arg in locargs: - tag,val = arg.split('=') - assert len(tag) <= 4 - loc[tag.ljust(4)] = float(val) + for arg in options.locargs: + try: + tag, val = arg.split('=') + assert len(tag) <= 4 + loc[tag.ljust(4)] = float(val) + except (ValueError, AssertionError): + parser.error("invalid location argument format: %r" % arg) log.info("Location: %s", loc) log.info("Loading variable font") diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/plot.py fonttools-3.29.0/Lib/fontTools/varLib/plot.py --- fonttools-3.21.2/Lib/fontTools/varLib/plot.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/plot.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,118 @@ +"""Visualize DesignSpaceDocument and resulting VariationModel.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.varLib.models import VariationModel, supportScalar +from fontTools.designspaceLib import DesignSpaceDocument +from mpl_toolkits.mplot3d import axes3d +from matplotlib import pyplot +from itertools import cycle +import math +import logging +import sys + +log = logging.getLogger(__name__) + + +def stops(support, count=10): + a,b,c = support + + return [a + (b - a) * i / count for i in range(count)] + \ + [b + (c - b) * i / count for i in range(count)] + \ + [c] + +def plotLocations(locations, axes, axis3D, **kwargs): + for loc,color in zip(locations, cycle(pyplot.cm.Set1.colors)): + axis3D.plot([loc.get(axes[0], 0)], + [loc.get(axes[1], 0)], + [1.], + 'o', + color=color, + **kwargs) + +def plotLocationsSurfaces(locations, fig, names=None, **kwargs): + + assert len(locations[0].keys()) == 2 + + if names is None: + names = [''] + + n = len(locations) + cols = math.ceil(n**.5) + rows = math.ceil(n / cols) + + model = VariationModel(locations) + names = [names[model.reverseMapping[i]] for i in range(len(names))] + + ax1, ax2 = sorted(locations[0].keys()) + for i, (support,color, name) in enumerate(zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))): + + axis3D = fig.add_subplot(rows, cols, i + 1, projection='3d') + axis3D.set_title(name) + axis3D.set_xlabel(ax1) + axis3D.set_ylabel(ax2) + pyplot.xlim(-1.,+1.) + pyplot.ylim(-1.,+1.) + + Xs = support.get(ax1, (-1.,0.,+1.)) + Ys = support.get(ax2, (-1.,0.,+1.)) + for x in stops(Xs): + X, Y, Z = [], [], [] + for y in Ys: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + for y in stops(Ys): + X, Y, Z = [], [], [] + for x in Xs: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + + plotLocations(model.locations, [ax1, ax2], axis3D) + + +def plotDocument(doc, fig, **kwargs): + doc.normalize() + locations = [s.location for s in doc.sources] + names = [s.name for s in doc.sources] + plotLocationsSurfaces(locations, fig, names, **kwargs) + + +def main(args=None): + from fontTools import configLogger + + if args is None: + args = sys.argv[1:] + + # configure the library logger (for >= WARNING) + configLogger() + # comment this out to enable debug messages from logger + # log.setLevel(logging.DEBUG) + + if len(args) < 1: + print("usage: fonttools varLib.plot source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.plot location1 location2 ...", file=sys.stderr) + sys.exit(1) + + fig = pyplot.figure() + + if len(args) == 1 and args[0].endswith('.designspace'): + doc = DesignSpaceDocument() + doc.read(args[0]) + plotDocument(doc, fig) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + plotLocationsSurfaces(locs, fig) + + pyplot.show() + +if __name__ == '__main__': + import sys + sys.exit(main()) diff -Nru fonttools-3.21.2/Lib/fontTools/varLib/varStore.py fonttools-3.29.0/Lib/fontTools/varLib/varStore.py --- fonttools-3.21.2/Lib/fontTools/varLib/varStore.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/varLib/varStore.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,10 +1,14 @@ from __future__ import print_function, division, absolute_import -from __future__ import unicode_literals from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.ttLib.tables import otTables as ot from fontTools.varLib.models import supportScalar from fontTools.varLib.builder import (buildVarRegionList, buildVarStore, buildVarRegion, buildVarData, - varDataCalculateNumShorts) + VarData_CalculateNumShorts) +from functools import partial +from collections import defaultdict +from array import array def _getLocationKey(loc): @@ -18,14 +22,27 @@ self._regionMap = {} self._regionList = buildVarRegionList([], axisTags) self._store = buildVarStore(self._regionList, []) + self._data = None + self._model = None + self._cache = {} def setModel(self, model): self._model = model + self._cache = {} # Empty cached items + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.VarDataCount = len(self._store.VarData) + for data in self._store.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data, optimize) + return self._store + + def _add_VarData(self): regionMap = self._regionMap regionList = self._regionList - regions = model.supports[1:] + regions = self._model.supports[1:] regionIndices = [] for region in regions: key = _getLocationKey(region) @@ -40,21 +57,26 @@ self._outer = len(self._store.VarData) self._store.VarData.append(data) - def finish(self, optimize=True): - self._regionList.RegionCount = len(self._regionList.Region) - self._store.VarDataCount = len(self._store.VarData) - for data in self._store.VarData: - data.ItemCount = len(data.Item) - varDataCalculateNumShorts(data, optimize) - return self._store - def storeMasters(self, master_values): - deltas = [round(d) for d in self._model.getDeltas(master_values)] + deltas = [otRound(d) for d in self._model.getDeltas(master_values)] base = deltas.pop(0) + deltas = tuple(deltas) + varIdx = self._cache.get(deltas) + if varIdx is not None: + return base, varIdx + + if not self._data: + self._add_VarData() inner = len(self._data.Item) + if inner == 0xFFFF: + # Full array. Start new one. + self._add_VarData() + return self.storeMasters(master_values) self._data.Item.append(deltas) - # TODO Check for full data array? - return base, (self._outer << 16) + inner + + varIdx = (self._outer << 16) + inner + self._cache[deltas] = varIdx + return base, varIdx def VarRegion_get_support(self, fvar_axes): @@ -98,3 +120,401 @@ delta += d * s return delta + +# +# Optimizations +# + +def VarStore_subset_varidxes(self, varIdxes, optimize=True): + + # Sort out used varIdxes by major/minor. + used = {} + for varIdx in varIdxes: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + d = used.get(major) + if d is None: + d = used[major] = set() + d.add(minor) + del varIdxes + + # + # Subset VarData + # + + varData = self.VarData + newVarData = [] + varDataMap = {} + for major,data in enumerate(varData): + usedMinors = used.get(major) + if usedMinors is None: + continue + newMajor = varDataMap[major] = len(newVarData) + newVarData.append(data) + + items = data.Item + newItems = [] + for minor in sorted(usedMinors): + newMinor = len(newItems) + newItems.append(items[minor]) + varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor + + data.Item = newItems + data.ItemCount = len(data.Item) + + if optimize: + VarData_CalculateNumShorts(data) + + self.VarData = newVarData + self.VarDataCount = len(self.VarData) + + self.prune_regions() + + return varDataMap + +ot.VarStore.subset_varidxes = VarStore_subset_varidxes + +def VarStore_prune_regions(self): + """Remove unused VarRegions.""" + # + # Subset VarRegionList + # + + # Collect. + usedRegions = set() + for data in self.VarData: + usedRegions.update(data.VarRegionIndex) + # Subset. + regionList = self.VarRegionList + regions = regionList.Region + newRegions = [] + regionMap = {} + for i in sorted(usedRegions): + regionMap[i] = len(newRegions) + newRegions.append(regions[i]) + regionList.Region = newRegions + regionList.RegionCount = len(regionList.Region) + # Map. + for data in self.VarData: + data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] + +ot.VarStore.prune_regions = VarStore_prune_regions + + +def _visit(self, objType, func): + """Recurse down from self, if type of an object is objType, + call func() on it. Only works for otData-style classes.""" + + if type(self) == objType: + func(self) + return # We don't recurse down; don't need to. + + if isinstance(self, list): + for that in self: + _visit(that, objType, func) + + if hasattr(self, 'getConverters'): + for conv in self.getConverters(): + that = getattr(self, conv.name, None) + if that is not None: + _visit(that, objType, func) + + if isinstance(self, ot.ValueRecord): + for that in self.__dict__.values(): + _visit(that, objType, func) + +def _Device_recordVarIdx(self, s): + """Add VarIdx in this Device table (if any) to the set s.""" + if self.DeltaFormat == 0x8000: + s.add((self.StartSize<<16)+self.EndSize) + +def Object_collect_device_varidxes(self, varidxes): + adder = partial(_Device_recordVarIdx, s=varidxes) + _visit(self, ot.Device, adder) + +ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes +ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes + +def _Device_mapVarIdx(self, mapping, done): + """Add VarIdx in this Device table (if any) to the set s.""" + if id(self) in done: + return + done.add(id(self)) + if self.DeltaFormat == 0x8000: + varIdx = mapping[(self.StartSize<<16)+self.EndSize] + self.StartSize = varIdx >> 16 + self.EndSize = varIdx & 0xFFFF + +def Object_remap_device_varidxes(self, varidxes_map): + mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set()) + _visit(self, ot.Device, mapper) + +ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes +ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes + + +class _Encoding(object): + + def __init__(self, chars): + self.chars = chars + self.width = self._popcount(chars) + self.overhead = self._characteristic_overhead(chars) + self.items = set() + + def append(self, row): + self.items.add(row) + + def extend(self, lst): + self.items.update(lst) + + def get_room(self): + """Maximum number of bytes that can be added to characteristic + while still being beneficial to merge it into another one.""" + count = len(self.items) + return max(0, (self.overhead - 1) // count - self.width) + room = property(get_room) + + @property + def gain(self): + """Maximum possible byte gain from merging this into another + characteristic.""" + count = len(self.items) + return max(0, self.overhead - count * (self.width + 1)) + + def sort_key(self): + return self.width, self.chars + + def __len__(self): + return len(self.items) + + def can_encode(self, chars): + return not (chars & ~self.chars) + + def __sub__(self, other): + return self._popcount(self.chars & ~other.chars) + + @staticmethod + def _popcount(n): + # Apparently this is the fastest native way to do it... + # https://stackoverflow.com/a/9831671 + return bin(n).count('1') + + @staticmethod + def _characteristic_overhead(chars): + """Returns overhead in bytes of encoding this characteristic + as a VarData.""" + c = 6 + while chars: + if chars & 3: + c += 2 + chars >>= 2 + return c + + + def _find_yourself_best_new_encoding(self, done_by_width): + self.best_new_encoding = None + for new_width in range(self.width+1, self.width+self.room+1): + for new_encoding in done_by_width[new_width]: + if new_encoding.can_encode(self.chars): + break + else: + new_encoding = None + self.best_new_encoding = new_encoding + + +class _EncodingDict(dict): + + def __missing__(self, chars): + r = self[chars] = _Encoding(chars) + return r + + def add_row(self, row): + chars = self._row_characteristics(row) + self[chars].append(row) + + @staticmethod + def _row_characteristics(row): + """Returns encoding characteristics for a row.""" + chars = 0 + i = 1 + for v in row: + if v: + chars += i + if not (-128 <= v <= 127): + chars += i * 2 + i <<= 2 + return chars + + +def VarStore_optimize(self): + """Optimize storage. Returns mapping from old VarIdxes to new ones.""" + + # TODO + # Check that no two VarRegions are the same; if they are, fold them. + + n = len(self.VarRegionList.Region) # Number of columns + zeroes = array('h', [0]*n) + + front_mapping = {} # Map from old VarIdxes to full row tuples + + encodings = _EncodingDict() + + # Collect all items into a set of full rows (with lots of zeroes.) + for major,data in enumerate(self.VarData): + regionIndices = data.VarRegionIndex + + for minor,item in enumerate(data.Item): + + row = array('h', zeroes) + for regionIdx,v in zip(regionIndices, item): + row[regionIdx] += v + row = tuple(row) + + encodings.add_row(row) + front_mapping[(major<<16)+minor] = row + + # Separate encodings that have no gain (are decided) and those having + # possible gain (possibly to be merged into others.) + encodings = sorted(encodings.values(), key=_Encoding.__len__, reverse=True) + done_by_width = defaultdict(list) + todo = [] + for encoding in encodings: + if not encoding.gain: + done_by_width[encoding.width].append(encoding) + else: + todo.append(encoding) + + # For each encoding that is possibly to be merged, find the best match + # in the decided encodings, and record that. + todo.sort(key=_Encoding.get_room) + for encoding in todo: + encoding._find_yourself_best_new_encoding(done_by_width) + + # Walk through todo encodings, for each, see if merging it with + # another todo encoding gains more than each of them merging with + # their best decided encoding. If yes, merge them and add resulting + # encoding back to todo queue. If not, move the enconding to decided + # list. Repeat till done. + while todo: + encoding = todo.pop() + best_idx = None + best_gain = 0 + for i,other_encoding in enumerate(todo): + combined_chars = other_encoding.chars | encoding.chars + combined_width = _Encoding._popcount(combined_chars) + combined_overhead = _Encoding._characteristic_overhead(combined_chars) + combined_gain = ( + + encoding.overhead + + other_encoding.overhead + - combined_overhead + - (combined_width - encoding.width) * len(encoding) + - (combined_width - other_encoding.width) * len(other_encoding) + ) + this_gain = 0 if encoding.best_new_encoding is None else ( + + encoding.overhead + - (encoding.best_new_encoding.width - encoding.width) * len(encoding) + ) + other_gain = 0 if other_encoding.best_new_encoding is None else ( + + other_encoding.overhead + - (other_encoding.best_new_encoding.width - other_encoding.width) * len(other_encoding) + ) + separate_gain = this_gain + other_gain + + if combined_gain > separate_gain: + best_idx = i + best_gain = combined_gain - separate_gain + + if best_idx is None: + # Encoding is decided as is + done_by_width[encoding.width].append(encoding) + else: + other_encoding = todo[best_idx] + combined_chars = other_encoding.chars | encoding.chars + combined_encoding = _Encoding(combined_chars) + combined_encoding.extend(encoding.items) + combined_encoding.extend(other_encoding.items) + combined_encoding._find_yourself_best_new_encoding(done_by_width) + del todo[best_idx] + todo.append(combined_encoding) + + # Assemble final store. + back_mapping = {} # Mapping from full rows to new VarIdxes + encodings = sum(done_by_width.values(), []) + encodings.sort(key=_Encoding.sort_key) + self.VarData = [] + for major,encoding in enumerate(encodings): + data = ot.VarData() + self.VarData.append(data) + data.VarRegionIndex = range(n) + data.VarRegionCount = len(data.VarRegionIndex) + data.Item = sorted(encoding.items) + for minor,item in enumerate(data.Item): + back_mapping[item] = (major<<16)+minor + + # Compile final mapping. + varidx_map = {} + for k,v in front_mapping.items(): + varidx_map[k] = back_mapping[v] + + # Remove unused regions. + self.prune_regions() + + # Recalculate things and go home. + self.VarRegionList.RegionCount = len(self.VarRegionList.Region) + self.VarDataCount = len(self.VarData) + for data in self.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data) + + return varidx_map + +ot.VarStore.optimize = VarStore_optimize + + +def main(args=None): + from argparse import ArgumentParser + from fontTools import configLogger + from fontTools.ttLib import TTFont + from fontTools.ttLib.tables.otBase import OTTableWriter + + parser = ArgumentParser(prog='varLib.varStore') + parser.add_argument('fontfile') + parser.add_argument('outfile', nargs='?') + options = parser.parse_args(args) + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + fontfile = options.fontfile + outfile = options.outfile + + font = TTFont(fontfile) + gdef = font['GDEF'] + store = gdef.table.VarStore + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("Before: %7d bytes" % size) + + varidx_map = store.optimize() + + gdef.table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("After: %7d bytes" % size) + + if outfile is not None: + font.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Lib/fontTools/voltLib/ast.py fonttools-3.29.0/Lib/fontTools/voltLib/ast.py --- fonttools-3.21.2/Lib/fontTools/voltLib/ast.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/voltLib/ast.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ class Statement(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -12,7 +12,7 @@ class Expression(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -20,7 +20,7 @@ class Block(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) self.statements = [] @@ -35,7 +35,7 @@ class LookupBlock(Block): - def __init__(self, location, name): + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -46,7 +46,7 @@ class GlyphDefinition(Statement): - def __init__(self, location, name, gid, gunicode, gtype, components): + def __init__(self, name, gid, gunicode, gtype, components, location=None): Statement.__init__(self, location) self.name = name self.id = gid @@ -56,7 +56,7 @@ class GroupDefinition(Statement): - def __init__(self, location, name, enum): + def __init__(self, name, enum, location=None): Statement.__init__(self, location) self.name = name self.enum = enum @@ -78,7 +78,7 @@ class GlyphName(Expression): """A single glyph name, such as cedilla.""" - def __init__(self, location, glyph): + def __init__(self, glyph, location=None): Expression.__init__(self, location) self.glyph = glyph @@ -88,7 +88,7 @@ class Enum(Expression): """An enum""" - def __init__(self, location, enum): + def __init__(self, enum, location=None): Expression.__init__(self, location) self.enum = enum @@ -108,7 +108,7 @@ class GroupName(Expression): """A glyph group""" - def __init__(self, location, group, parser): + def __init__(self, group, parser, location=None): Expression.__init__(self, location) self.group = group self.parser_ = parser @@ -126,7 +126,7 @@ class Range(Expression): """A glyph range""" - def __init__(self, location, start, end, parser): + def __init__(self, start, end, parser, location=None): Expression.__init__(self, location) self.start = start self.end = end @@ -138,7 +138,7 @@ class ScriptDefinition(Statement): - def __init__(self, location, name, tag, langs): + def __init__(self, name, tag, langs, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -146,7 +146,7 @@ class LangSysDefinition(Statement): - def __init__(self, location, name, tag, features): + def __init__(self, name, tag, features, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -154,7 +154,7 @@ class FeatureDefinition(Statement): - def __init__(self, location, name, tag, lookups): + def __init__(self, name, tag, lookups, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -162,8 +162,8 @@ class LookupDefinition(Statement): - def __init__(self, location, name, process_base, process_marks, direction, - reversal, comments, context, sub, pos): + def __init__(self, name, process_base, process_marks, direction, + reversal, comments, context, sub, pos, location=None): Statement.__init__(self, location) self.name = name self.process_base = process_base @@ -177,47 +177,43 @@ class SubstitutionDefinition(Statement): - def __init__(self, location, mapping): + def __init__(self, mapping, location=None): Statement.__init__(self, location) self.mapping = mapping class SubstitutionSingleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionMultipleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionLigatureDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class PositionAttachDefinition(Statement): - def __init__(self, location, coverage, coverage_to): + def __init__(self, coverage, coverage_to, location=None): Statement.__init__(self, location) self.coverage = coverage self.coverage_to = coverage_to class PositionAttachCursiveDefinition(Statement): - def __init__(self, location, coverages_exit, coverages_enter): + def __init__(self, coverages_exit, coverages_enter, location=None): Statement.__init__(self, location) self.coverages_exit = coverages_exit self.coverages_enter = coverages_enter class PositionAdjustPairDefinition(Statement): - def __init__(self, location, coverages_1, coverages_2, adjust_pair): + def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): Statement.__init__(self, location) self.coverages_1 = coverages_1 self.coverages_2 = coverages_2 @@ -225,22 +221,22 @@ class PositionAdjustSingleDefinition(Statement): - def __init__(self, location, adjust_single): + def __init__(self, adjust_single, location=None): Statement.__init__(self, location) self.adjust_single = adjust_single class ContextDefinition(Statement): - def __init__(self, location, ex_or_in, left=[], right=[]): + def __init__(self, ex_or_in, left=None, right=None, location=None): Statement.__init__(self, location) self.ex_or_in = ex_or_in - self.left = left - self.right = right + self.left = left if left is not None else [] + self.right = right if right is not None else [] class AnchorDefinition(Statement): - def __init__(self, location, name, gid, glyph_name, component, locked, - pos): + def __init__(self, name, gid, glyph_name, component, locked, + pos, location=None): Statement.__init__(self, location) self.name = name self.gid = gid @@ -251,7 +247,7 @@ class SettingDefinition(Statement): - def __init__(self, location, name, value): + def __init__(self, name, value, location=None): Statement.__init__(self, location) self.name = name self.value = value diff -Nru fonttools-3.21.2/Lib/fontTools/voltLib/parser.py fonttools-3.29.0/Lib/fontTools/voltLib/parser.py --- fonttools-3.21.2/Lib/fontTools/voltLib/parser.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Lib/fontTools/voltLib/parser.py 2018-07-26 14:12:55.000000000 +0000 @@ -87,8 +87,9 @@ 'Glyph "%s" (gid %i) already defined' % (name, gid), location ) - def_glyph = ast.GlyphDefinition(location, name, gid, - gunicode, gtype, components) + def_glyph = ast.GlyphDefinition(name, gid, + gunicode, gtype, components, + location=location) self.glyphs_.define(name, def_glyph) return def_glyph @@ -107,7 +108,8 @@ 'group names are case insensitive' % name, location ) - def_group = ast.GroupDefinition(location, name, enum) + def_group = ast.GroupDefinition(name, enum, + location=location) self.groups_.define(name, def_group) return def_group @@ -142,7 +144,7 @@ langs.append(lang) self.expect_keyword_("END_SCRIPT") self.langs_.exit_scope() - def_script = ast.ScriptDefinition(location, name, tag, langs) + def_script = ast.ScriptDefinition(name, tag, langs, location=location) self.scripts_.define(tag, def_script) return def_script @@ -161,7 +163,8 @@ feature = self.parse_feature_() self.expect_keyword_("END_FEATURE") features.append(feature) - def_langsys = ast.LangSysDefinition(location, name, tag, features) + def_langsys = ast.LangSysDefinition(name, tag, features, + location=location) return def_langsys def parse_feature_(self): @@ -177,7 +180,8 @@ self.expect_keyword_("LOOKUP") lookup = self.expect_string_() lookups.append(lookup) - feature = ast.FeatureDefinition(location, name, tag, lookups) + feature = ast.FeatureDefinition(name, tag, lookups, + location=location) return feature def parse_def_lookup_(self): @@ -248,8 +252,8 @@ "Got %s" % (as_pos_or_sub), location) def_lookup = ast.LookupDefinition( - location, name, process_base, process_marks, direction, reversal, - comments, context, sub, pos) + name, process_base, process_marks, direction, reversal, + comments, context, sub, pos, location=location) self.lookups_.define(name, def_lookup) return def_lookup @@ -272,8 +276,8 @@ else: right.append(coverage) self.expect_keyword_("END_CONTEXT") - context = ast.ContextDefinition(location, ex_or_in, left, - right) + context = ast.ContextDefinition(ex_or_in, left, + right, location=location) contexts.append(context) else: self.expect_keyword_("END_CONTEXT") @@ -305,13 +309,16 @@ if max_src == 1 and max_dest == 1: if reversal: sub = ast.SubstitutionReverseChainingSingleDefinition( - location, mapping) + mapping, location=location) else: - sub = ast.SubstitutionSingleDefinition(location, mapping) + sub = ast.SubstitutionSingleDefinition(mapping, + location=location) elif max_src == 1 and max_dest > 1: - sub = ast.SubstitutionMultipleDefinition(location, mapping) + sub = ast.SubstitutionMultipleDefinition(mapping, + location=location) elif max_src > 1 and max_dest == 1: - sub = ast.SubstitutionLigatureDefinition(location, mapping) + sub = ast.SubstitutionLigatureDefinition(mapping, + location=location) return sub def parse_position_(self): @@ -348,7 +355,7 @@ coverage_to.append((cov, anchor_name)) self.expect_keyword_("END_ATTACH") position = ast.PositionAttachDefinition( - location, coverage, coverage_to) + coverage, coverage_to, location=location) return position def parse_attach_cursive_(self): @@ -364,7 +371,7 @@ coverages_enter.append(self.parse_coverage_()) self.expect_keyword_("END_ATTACH") position = ast.PositionAttachCursiveDefinition( - location, coverages_exit, coverages_enter) + coverages_exit, coverages_enter, location=location) return position def parse_adjust_pair_(self): @@ -390,7 +397,7 @@ adjust_pair[(id_1, id_2)] = (pos_1, pos_2) self.expect_keyword_("END_ADJUST") position = ast.PositionAdjustPairDefinition( - location, coverages_1, coverages_2, adjust_pair) + coverages_1, coverages_2, adjust_pair, location=location) return position def parse_adjust_single_(self): @@ -404,7 +411,7 @@ adjust_single.append((coverages, pos)) self.expect_keyword_("END_ADJUST") position = ast.PositionAdjustSingleDefinition( - location, adjust_single) + adjust_single, location=location) return position def parse_def_anchor_(self): @@ -433,8 +440,9 @@ self.expect_keyword_("AT") pos = self.parse_pos_() self.expect_keyword_("END_ANCHOR") - anchor = ast.AnchorDefinition(location, name, gid, glyph_name, - component, locked, pos) + anchor = ast.AnchorDefinition(name, gid, glyph_name, + component, locked, pos, + location=location) if glyph_name not in self.anchors_: self.anchors_[glyph_name] = SymbolTable() self.anchors_[glyph_name].define(name, anchor) @@ -493,7 +501,6 @@ def parse_enum_(self): assert self.is_cur_keyword_("ENUM") - location = self.cur_token_location_ enum = self.parse_coverage_() self.expect_keyword_("END_ENUM") return enum @@ -544,14 +551,14 @@ location = self.cur_token_location_ ppem_name = self.cur_token_ value = self.expect_number_() - setting = ast.SettingDefinition(location, ppem_name, value) + setting = ast.SettingDefinition(ppem_name, value, location=location) return setting def parse_compiler_flag_(self): location = self.cur_token_location_ flag_name = self.cur_token_ value = True - setting = ast.SettingDefinition(location, flag_name, value) + setting = ast.SettingDefinition(flag_name, value, location=location) return setting def parse_cmap_format(self): @@ -559,7 +566,7 @@ name = self.cur_token_ value = (self.expect_number_(), self.expect_number_(), self.expect_number_()) - setting = ast.SettingDefinition(location, name, value) + setting = ast.SettingDefinition(name, value, location=location) return setting def is_cur_keyword_(self, k): diff -Nru fonttools-3.21.2/NEWS.rst fonttools-3.29.0/NEWS.rst --- fonttools-3.21.2/NEWS.rst 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/NEWS.rst 2018-07-26 14:12:55.000000000 +0000 @@ -1,3 +1,235 @@ +3.29.0 (released 2018-07-26) +---------------------------- + +- [feaLib] In the OTL table builder, when the ``name`` table is excluded + from the list of tables to be build, skip compiling ``featureNames`` blocks, + as the records referenced in ``FeatureParams`` table don't exist (68951b7). +- [otBase] Try ``ExtensionLookup`` if other offset-overflow methods fail + (05f95f0). +- [feaLib] Added support for explicit ``subtable;`` break statements in + PairPos lookups; previously these were ignored (#1279, #1300, #1302). +- [cffLib.specializer] Make sure the stack depth does not exceed maxstack - 1, + so that a subroutinizer can insert subroutine calls (#1301, + https://github.com/googlei18n/ufo2ft/issues/266). +- [otTables] Added support for fixing offset overflow errors occurring inside + ``MarkBasePos`` subtables (#1297). +- [subset] Write the default output file extension based on ``--flavor`` option, + or the value of ``TTFont.sfntVersion`` (d7ac0ad). +- [unicodedata] Updated Blocks, Scripts and ScriptExtensions for Unicode 11 + (452c85e). +- [xmlWriter] Added context manager to XMLWriter class to autoclose file + descriptor on exit (#1290). +- [psCharStrings] Optimize the charstring's bytecode by encoding as integers + all float values that have no decimal portion (8d7774a). +- [ttFont] Fixed missing import of ``TTLibError`` exception (#1285). +- [feaLib] Allow any languages other than ``dflt`` under ``DFLT`` script + (#1278, #1292). + +3.28.0 (released 2018-06-19) +---------------------------- + +- [featureVars] Added experimental module to build ``FeatureVariations`` + tables. Still needs to be hooked up to ``varLib.build`` (#1240). +- [fixedTools] Added ``otRound`` to round floats to nearest integer towards + positive Infinity. This is now used where we deal with visual data like X/Y + coordinates, advance widths/heights, variation deltas, and similar (#1274, + #1248). +- [subset] Improved GSUB closure memoize algorithm. +- [varLib.models] Fixed regression in model resolution (180124, #1269). +- [feaLib.ast] Fixed error when converting ``SubtableStatement`` to string + (#1275). +- [varLib.mutator] Set ``OS/2.usWeightClass`` and ``usWidthClass``, and + ``post.italicAngle`` based on the 'wght', 'wdth' and 'slnt' axis values + (#1276, #1264). +- [py23/loggingTools] Don't automatically set ``logging.lastResort`` handler + on py27. Moved ``LastResortLogger`` to the ``loggingTools`` module (#1277). + +3.27.1 (released 2018-06-11) +---------------------------- + +- [ttGlyphPen] Issue a warning and skip building non-existing components + (https://github.com/googlei18n/fontmake/issues/411). +- [tests] Fixed issue running ttx_test.py from a tagged commit. + +3.27.0 (released 2018-06-11) +---------------------------- + +- [designspaceLib] Added new ``conditionSet`` element to ``rule`` element in + designspace document. Bumped ``format`` attribute to ``4.0`` (previously, + it was formatted as an integer). Removed ``checkDefault``, ``checkAxes`` + methods, and any kind of guessing about the axes when the ```` element + is missing. The default master is expected at the intersection of all default + values for each axis (#1254, #1255, #1267). +- [cffLib] Fixed issues when compiling CFF2 or converting from CFF when the + font has an FDArray (#1211, #1271). +- [varLib] Avoid attempting to build ``cvar`` table when ``glyf`` table is not + present, as is the case for CFF2 fonts. +- [subset] Handle None coverages in MarkGlyphSets; revert commit 02616ab that + sets empty Coverage tables in MarkGlyphSets to None, to make OTS happy. +- [ttFont] Allow to build glyph order from ``maxp.numGlyphs`` when ``post`` or + ``cmap`` are missing. +- [ttFont] Added ``__len__`` method to ``_TTGlyphSet``. +- [glyf] Ensure ``GlyphCoordinates`` never overflow signed shorts (#1230). +- [py23] Added alias for ``itertools.izip`` shadowing the built-in ``zip``. +- [loggingTools] Memoize ``log`` property of ``LogMixin`` class (fbab12). +- [ttx] Impoved test coverage (#1261). +- [Snippets] Addded script to append a suffix to all family names in a font. +- [varLib.plot] Make it work with matplotlib >= 2.1 (b38e2b). + +3.26.0 (released 2018-05-03) +---------------------------- + +- [designspace] Added a new optional ``layer`` attribute to the source element, + and a corresponding ``layerName`` attribute to the ``SourceDescriptor`` + object (#1253). + Added ``conditionset`` element to the ``rule`` element to the spec, but not + implemented in designspace reader/writer yet (#1254). +- [varLib.models] Refine modeling one last time (0ecf5c5). +- [otBase] Fixed sharing of tables referred to by different offset sizes + (795f2f9). +- [subset] Don't drop a GDEF that only has VarStore (fc819d6). Set to None + empty Coverage tables in MarkGlyphSets (02616ab). +- [varLib]: Added ``--master-finder`` command-line option (#1249). +- [varLib.mutator] Prune fvar nameIDs from instance's name table (#1245). +- [otTables] Allow decompiling bad ClassDef tables with invalid format, with + warning (#1236). +- [varLib] Make STAT v1.2 and reuse nameIDs from fvar table (#1242). +- [varLib.plot] Show master locations. Set axis limits to -1, +1. +- [subset] Handle HVAR direct mapping. Passthrough 'cvar'. + Added ``--font-number`` command-line option for collections. +- [t1Lib] Allow a text encoding to be specified when parsing a Type 1 font + (#1234). Added ``kind`` argument to T1Font constructor (c5c161c). +- [ttLib] Added context manager API to ``TTFont`` class, so it can be used in + ``with`` statements to auto-close the file when exiting the context (#1232). + +3.25.0 (released 2018-04-03) +---------------------------- + +- [varLib] Improved support-resolution algorithm. Previously, the on-axis + masters would always cut the space. They don't anymore. That's more + consistent, and fixes the main issue Erik showed at TYPO Labs 2017. + Any varfont built that had an unusual master configuration will change + when rebuilt (42bef17, a523a697, + https://github.com/googlei18n/fontmake/issues/264). +- [varLib.models] Added a ``main()`` entry point, that takes positions and + prints model results. +- [varLib.plot] Added new module to plot a designspace's + VariationModel. Requires ``matplotlib``. +- [varLib.mutator] Added -o option to specify output file path (2ef60fa). +- [otTables] Fixed IndexError while pruning of HVAR pre-write (6b6c34a). +- [varLib.models] Convert delta array to floats if values overflows signed + short integer (0055f94). + +3.24.2 (released 2018-03-26) +---------------------------- + +- [otBase] Don't fail during ``ValueRecord`` copy if src has more items. + We drop hinting in the subsetter by simply changing ValueFormat, without + cleaning up the actual ValueRecords. This was causing assertion error if + a variable font was subsetted without hinting and then passed directly to + the mutator for instantiation without first it saving to disk. + +3.24.1 (released 2018-03-06) +---------------------------- + +- [varLib] Don't remap the same ``DeviceTable`` twice in VarStore optimizer + (#1206). +- [varLib] Add ``--disable-iup`` option to ``fonttools varLib`` script, + and a ``optimize=True`` keyword argument to ``varLib.build`` function, + to optionally disable IUP optimization while building varfonts. +- [ttCollection] Fixed issue while decompiling ttc with python3 (#1207). + +3.24.0 (released 2018-03-01) +---------------------------- + +- [ttGlyphPen] Decompose composite glyphs if any components' transform is too + large to fit a ``F2Dot14`` value, or clamp transform values that are + (almost) equal to +2.0 to make them fit and avoid decomposing (#1200, + #1204, #1205). +- [ttx] Added new ``-g`` option to dump glyphs from the ``glyf`` table + splitted as individual ttx files (#153, #1035, #1132, #1202). +- Copied ``ufoLib.filenames`` module to ``fontTools.misc.filenames``, used + for the ttx split-glyphs option (#1202). +- [feaLib] Added support for ``cvParameters`` blocks in Character Variant + feautures ``cv01-cv99`` (#860, #1169). +- [Snippets] Added ``checksum.py`` script to generate/check SHA1 hash of + ttx files (#1197). +- [varLib.mutator] Fixed issue while instantiating some variable fonts + whereby the horizontal advance width computed from ``gvar`` phantom points + could turn up to be negative (#1198). +- [varLib/subset] Fixed issue with subsetting GPOS variation data not + picking up ``ValueRecord`` ``Device`` objects (54fd71f). +- [feaLib/voltLib] In all AST elements, the ``location`` is no longer a + required positional argument, but an optional kewyord argument (defaults + to ``None``). This will make it easier to construct feature AST from + code (#1201). + + +3.23.0 (released 2018-02-26) +---------------------------- + +- [designspaceLib] Added an optional ``lib`` element to the designspace as a + whole, as well as to the instance elements, to store arbitrary data in a + property list dictionary, similar to the UFO's ``lib``. Added an optional + ``font`` attribute to the ``SourceDescriptor``, to allow operating on + in-memory font objects (#1175). +- [cffLib] Fixed issue with lazy-loading of attributes when attempting to + set the CFF TopDict.Encoding (#1177, #1187). +- [ttx] Fixed regression introduced in 3.22.0 that affected the split tables + ``-s`` option (#1188). +- [feaLib] Added ``IncludedFeaNotFound`` custom exception subclass, raised + when an included feature file cannot be found (#1186). +- [otTables] Changed ``VarIdxMap`` to use glyph names internally instead of + glyph indexes. The old ttx dumps of HVAR/VVAR tables that contain indexes + can still be imported (21cbab8, 38a0ffb). +- [varLib] Implemented VarStore optimizer (#1184). +- [subset] Implemented pruning of GDEF VarStore, HVAR and MVAR (#1179). +- [sfnt] Restore backward compatiblity with ``numFonts`` attribute of + ``SFNTReader`` object (#1181). +- [merge] Initial support for merging ``LangSysRecords`` (#1180). +- [ttCollection] don't seek(0) when writing to possibly unseekable strems. +- [subset] Keep all ``--name-IDs`` from 0 to 6 by default (#1170, #605, #114). +- [cffLib] Added ``width`` module to calculate optimal CFF default and + nominal glyph widths. +- [varLib] Don’t fail if STAT already in the master fonts (#1166). + +3.22.0 (released 2018-02-04) +---------------------------- + +- [subset] Support subsetting ``endchar`` acting as ``seac``-like components + in ``CFF`` (fixes #1162). +- [feaLib] Allow to build from pre-parsed ``ast.FeatureFile`` object. + Added ``tables`` argument to only build some tables instead of all (#1159, + #1163). +- [textTools] Replaced ``safeEval`` with ``ast.literal_eval`` (#1139). +- [feaLib] Added option to the parser to not resolve ``include`` statements + (#1154). +- [ttLib] Added new ``ttCollection`` module to read/write TrueType and + OpenType Collections. Exports a ``TTCollection`` class with a ``fonts`` + attribute containing a list of ``TTFont`` instances, the methods ``save`` + and ``saveXML``, plus some list-like methods. The ``importXML`` method is + not implemented yet (#17). +- [unicodeadata] Added ``ot_tag_to_script`` function that converts from + OpenType script tag to Unicode script code. +- Added new ``designspaceLib`` subpackage, originally from Erik Van Blokland's + ``designSpaceDocument``: https://github.com/LettError/designSpaceDocument + NOTE: this is not yet used internally by varLib, and the API may be subject + to changes (#911, #1110, LettError/designSpaceDocument#28). +- Added new FontTools icon images (8ee7c32). +- [unicodedata] Added ``script_horizontal_direction`` function that returns + either "LTR" or "RTL" given a unicode script code. +- [otConverters] Don't write descriptive name string as XML comment if the + NameID value is 0 (== NULL) (#1151, #1152). +- [unicodedata] Add ``ot_tags_from_script`` function to get the list of + OpenType script tags associated with unicode script code (#1150). +- [feaLib] Don't error when "enumerated" kern pairs conflict with preceding + single pairs; emit warning and chose the first value (#1147, #1148). +- [loggingTools] In ``CapturingLogHandler.assertRegex`` method, match the + fully formatted log message. +- [sbix] Fixed TypeError when concatenating str and bytes (#1154). +- [bezierTools] Implemented cusp support and removed ``approximate_fallback`` + arg in ``calcQuadraticArcLength``. Added ``calcCubicArcLength`` (#1142). + 3.21.2 (released 2018-01-08) ---------------------------- diff -Nru fonttools-3.21.2/README.rst fonttools-3.29.0/README.rst --- fonttools-3.21.2/README.rst 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/README.rst 2018-07-26 14:12:55.000000000 +0000 @@ -1,5 +1,5 @@ |Travis Build Status| |Appveyor Build status| |Health| |Coverage Status| -|PyPI| +|PyPI| |Gitter Chat| What is this? ~~~~~~~~~~~~~ @@ -360,3 +360,6 @@ :target: https://codecov.io/gh/fonttools/fonttools .. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg :target: https://pypi.org/project/FontTools +.. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg + :alt: Join the chat at https://gitter.im/fonttools-dev/Lobby + :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge diff -Nru fonttools-3.21.2/requirements.txt fonttools-3.29.0/requirements.txt --- fonttools-3.21.2/requirements.txt 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/requirements.txt 2018-07-26 14:12:55.000000000 +0000 @@ -2,5 +2,6 @@ # extension 'brotlipy' on PyPy brotli==1.0.1; platform_python_implementation != "PyPy" brotlipy==0.7.0; platform_python_implementation == "PyPy" -unicodedata2==10.0.0; python_version < '3.7' and platform_python_implementation != "PyPy" +unicodedata2==11.0.0; python_version < '3.7' and platform_python_implementation != "PyPy" munkres==1.0.10 +zopfli==0.1.4 diff -Nru fonttools-3.21.2/run-tests.sh fonttools-3.29.0/run-tests.sh --- fonttools-3.21.2/run-tests.sh 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/run-tests.sh 2018-07-26 14:12:55.000000000 +0000 @@ -5,13 +5,13 @@ # Choose python version if test "x$1" = x-3; then - PYTHON=python3 + PYTHON=py3-nocov shift elif test "x$1" = x-2; then - PYTHON=python2 + PYTHON=py2-nocov shift fi -test "x$PYTHON" = x && PYTHON=python +test "x$PYTHON" = x && PYTHON=py-nocov # Find tests FILTERS= @@ -22,7 +22,7 @@ # Run tests if [ -z "$FILTERS" ]; then - $PYTHON setup.py test + tox --develop -e $PYTHON else - $PYTHON setup.py test --addopts="-k \"$FILTERS\"" + tox --develop -e $PYTHON -- -k "$FILTERS" fi diff -Nru fonttools-3.21.2/setup.cfg fonttools-3.29.0/setup.cfg --- fonttools-3.21.2/setup.cfg 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/setup.cfg 2018-07-26 14:12:55.000000000 +0000 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.21.2 +current_version = 3.29.0 commit = True tag = False tag_name = {new_version} @@ -46,7 +46,6 @@ python_classes = *Test addopts = - -v -r a --doctest-modules --doctest-ignore-import-errors diff -Nru fonttools-3.21.2/setup.py fonttools-3.29.0/setup.py --- fonttools-3.21.2/setup.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/setup.py 2018-07-26 14:12:55.000000000 +0000 @@ -309,7 +309,7 @@ setup( name="fonttools", - version="3.21.2", + version="3.29.0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", diff -Nru fonttools-3.21.2/Snippets/checksum.py fonttools-3.29.0/Snippets/checksum.py --- fonttools-3.21.2/Snippets/checksum.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/checksum.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import hashlib +import os +import sys + +from os.path import basename + +from fontTools.ttLib import TTFont + + +def write_checksum(filepaths, stdout_write=False, use_ttx=False, include_tables=None, exclude_tables=None, do_not_cleanup=False): + checksum_dict = {} + for path in filepaths: + if not os.path.exists(path): + sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid file path" + os.linesep) + sys.exit(1) + + if use_ttx: + # append a .ttx extension to existing extension to maintain data about the binary that + # was used to generate the .ttx XML dump. This creates unique checksum path values for + # paths that would otherwise not be unique with a file extension replacement with .ttx + # An example is woff and woff2 web font files that share the same base file name: + # + # coolfont-regular.woff ==> coolfont-regular.ttx + # coolfont-regular.woff2 ==> coolfont-regular.ttx (KAPOW! checksum data lost as this would overwrite dict value) + temp_ttx_path = path + ".ttx" + + tt = TTFont(path) + # important to keep the newlinestr value defined here as hash values will change across platforms + # if platform specific newline values are assumed + tt.saveXML(temp_ttx_path, newlinestr="\n", skipTables=exclude_tables, tables=include_tables) + checksum_path = temp_ttx_path + else: + if include_tables is not None: + sys.stderr.write("[checksum.py] -i and --include are not supported for font binary filepaths. \ + Use these flags for checksums with the --ttx flag.") + sys.exit(1) + if exclude_tables is not None: + sys.stderr.write("[checksum.py] -e and --exclude are not supported for font binary filepaths. \ + Use these flags for checksums with the --ttx flag.") + sys.exit(1) + checksum_path = path + + file_contents = _read_binary(checksum_path) + + # store SHA1 hash data and associated file path basename in the checksum_dict dictionary + checksum_dict[basename(checksum_path)] = hashlib.sha1(file_contents).hexdigest() + + # remove temp ttx files when present + if use_ttx and do_not_cleanup is False: + os.remove(temp_ttx_path) + + # generate the checksum list string for writes + checksum_out_data = "" + for key in checksum_dict.keys(): + checksum_out_data += checksum_dict[key] + " " + key + "\n" + + # write to stdout stream or file based upon user request (default = file write) + if stdout_write: + sys.stdout.write(checksum_out_data) + else: + checksum_report_filepath = "checksum.txt" + with open(checksum_report_filepath, "w") as file: + file.write(checksum_out_data) + + +def check_checksum(filepaths): + check_failed = False + for path in filepaths: + if not os.path.exists(path): + sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid filepath" + os.linesep) + sys.exit(1) + + with open(path, mode='r') as file: + for line in file.readlines(): + cleaned_line = line.rstrip() + line_list = cleaned_line.split(" ") + # eliminate empty strings parsed from > 1 space characters + line_list = list(filter(None, line_list)) + if len(line_list) == 2: + expected_sha1 = line_list[0] + test_path = line_list[1] + else: + sys.stderr.write("[checksum.py] ERROR: failed to parse checksum file values" + os.linesep) + sys.exit(1) + + if not os.path.exists(test_path): + print(test_path + ": Filepath is not valid, ignored") + else: + file_contents = _read_binary(test_path) + observed_sha1 = hashlib.sha1(file_contents).hexdigest() + if observed_sha1 == expected_sha1: + print(test_path + ": OK") + else: + print("-" * 80) + print(test_path + ": === FAIL ===") + print("Expected vs. Observed:") + print(expected_sha1) + print(observed_sha1) + print("-" * 80) + check_failed = True + + # exit with status code 1 if any fails detected across all tests in the check + if check_failed is True: + sys.exit(1) + + +def _read_binary(filepath): + with open(filepath, mode='rb') as file: + return file.read() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="checksum.py", description="A SHA1 hash checksum list generator and checksum testing script") + parser.add_argument("-t", "--ttx", help="Calculate from ttx file", action="store_true") + parser.add_argument("-s", "--stdout", help="Write output to stdout stream", action="store_true") + parser.add_argument("-n", "--noclean", help="Do not discard *.ttx files used to calculate SHA1 hashes", action="store_true") + parser.add_argument("-c", "--check", help="Verify checksum values vs. files", action="store_true") + parser.add_argument("filepaths", nargs="+", help="One or more file paths. Use checksum file path for -c/--check. Use paths\ + to font files for all other commands.") + + parser.add_argument("-i", "--include", action="append", help="Included OpenType tables for ttx data dump") + parser.add_argument("-e", "--exclude", action="append", help="Excluded OpenType tables for ttx data dump") + + args = parser.parse_args(sys.argv[1:]) + + if args.check is True: + check_checksum(args.filepaths) + else: + write_checksum(args.filepaths, stdout_write=args.stdout, use_ttx=args.ttx, do_not_cleanup=args.noclean, include_tables=args.include, exclude_tables=args.exclude) diff -Nru fonttools-3.21.2/Snippets/edit_raw_table_data.py fonttools-3.29.0/Snippets/edit_raw_table_data.py --- fonttools-3.21.2/Snippets/edit_raw_table_data.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/edit_raw_table_data.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,31 @@ +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables.DefaultTable import DefaultTable + +font_path = "myfont.ttf" +output_path = "myfont_patched.ttf" + +table_tag = "DSIG" + + +# Get raw table data from the source font + +font = TTFont(font_path) +raw_data = font.getTableData(table_tag) + + +# Do something with the raw table data +# This example just sets an empty DSIG table. + +raw_data = b"\0\0\0\1\0\0\0\0" + + +# Write the data back to the font + +# We could re-use the existing table when the source and target font are +# identical, but let's make a new empty table to be more universal. +table = DefaultTable(table_tag) +table.data = raw_data + +# Add the new table back into the source font and save under a new name. +font[table_tag] = table +font.save(output_path) diff -Nru fonttools-3.21.2/Snippets/fontTools/cffLib/__init__.py fonttools-3.29.0/Snippets/fontTools/cffLib/__init__.py --- fonttools-3.21.2/Snippets/fontTools/cffLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/cffLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -144,7 +144,7 @@ writer.toFile(file) - def toXML(self, xmlWriter, progress=None): + def toXML(self, xmlWriter): xmlWriter.simpletag("major", value=self.major) xmlWriter.newline() xmlWriter.simpletag("minor", value=self.minor) @@ -153,13 +153,13 @@ xmlWriter.begintag("CFFFont", name=tostr(fontName)) xmlWriter.newline() font = self[fontName] - font.toXML(xmlWriter, progress) + font.toXML(xmlWriter) xmlWriter.endtag("CFFFont") xmlWriter.newline() xmlWriter.newline() xmlWriter.begintag("GlobalSubrs") xmlWriter.newline() - self.GlobalSubrs.toXML(xmlWriter, progress) + self.GlobalSubrs.toXML(xmlWriter) xmlWriter.endtag("GlobalSubrs") xmlWriter.newline() @@ -244,7 +244,7 @@ if key in topDict.rawDict: del topDict.rawDict[key] if hasattr(topDict, key): - exec("del topDict.%s" % (key)) + delattr(topDict, key) if not hasattr(topDict, "FDArray"): fdArray = topDict.FDArray = FDArrayIndex() @@ -257,6 +257,7 @@ else: charStrings.fdArray = fdArray fontDict = FontDict() + fontDict.setCFF2(True) fdArray.append(fontDict) fontDict.Private = privateDict privateOpOrder = buildOrder(privateDictOperators2) @@ -267,12 +268,20 @@ # print "Removing private dict", key del privateDict.rawDict[key] if hasattr(privateDict, key): - exec("del privateDict.%s" % (key)) + delattr(privateDict, key) # print "Removing privateDict attr", key else: # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray privateOpOrder = buildOrder(privateDictOperators2) for fontDict in fdArray: + fontDict.setCFF2(True) + for key in fontDict.rawDict.keys(): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + privateDict = fontDict.Private for entry in privateDictOperators: key = entry[1] @@ -281,7 +290,7 @@ # print "Removing private dict", key del privateDict.rawDict[key] if hasattr(privateDict, key): - exec("del privateDict.%s" % (key)) + delattr(privateDict, key) # print "Removing privateDict attr", key # At this point, the Subrs and Charstrings are all still T2Charstring class # easiest to fix this by compiling, then decompiling again @@ -633,7 +642,7 @@ private = None return self.subrClass(data, private=private, globalSubrs=self.globalSubrs) - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): xmlWriter.comment( "The 'index' attribute is only for humans; " "it is ignored when parsed.") @@ -698,11 +707,11 @@ top.decompile(data) return top - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for i in range(len(self)): xmlWriter.begintag("FontDict", index=i) xmlWriter.newline() - self[i].toXML(xmlWriter, progress) + self[i].toXML(xmlWriter) xmlWriter.endtag("FontDict") xmlWriter.newline() @@ -711,11 +720,11 @@ compilerClass = FDArrayIndexCompiler - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for i in range(len(self)): xmlWriter.begintag("FontDict", index=i) xmlWriter.newline() - self[i].toXML(xmlWriter, progress) + self[i].toXML(xmlWriter) xmlWriter.endtag("FontDict") xmlWriter.newline() @@ -906,11 +915,8 @@ sel = None return self.charStrings[name], sel - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): names = sorted(self.keys()) - i = 0 - step = 10 - numGlyphs = len(names) for name in names: charStr, fdSelectIndex = self.getItemAndSelector(name) if charStr.needsDecompilation(): @@ -927,10 +933,6 @@ charStr.toXML(xmlWriter) xmlWriter.endtag("CharString") xmlWriter.newline() - if not i % step and progress is not None: - progress.setLabel("Dumping 'CFF ' table... (%s)" % name) - progress.increment(step / numGlyphs) - i = i + 1 def fromXML(self, name, attrs, content): for element in content: @@ -1037,12 +1039,22 @@ class SimpleConverter(object): def read(self, parent, value): + if not hasattr(parent, "file"): + return self._read(parent, value) + file = parent.file + pos = file.tell() + try: + return self._read(parent, value) + finally: + file.seek(pos) + + def _read(self, parent, value): return value def write(self, parent, value): return value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, value=value) xmlWriter.newline() @@ -1052,13 +1064,13 @@ class ASCIIConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): return tostr(value, encoding='ascii') def write(self, parent, value): return tobytes(value, encoding='ascii') - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, value=tounicode(value, encoding="ascii")) xmlWriter.newline() @@ -1068,13 +1080,13 @@ class Latin1Converter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): return tostr(value, encoding='latin1') def write(self, parent, value): return tobytes(value, encoding='latin1') - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): value = tounicode(value, encoding="latin1") if name in ['Notice', 'Copyright']: value = re.sub(r"[\r\n]\s+", " ", value) @@ -1108,7 +1120,7 @@ class NumberConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if isinstance(value, list): xmlWriter.begintag(name) xmlWriter.newline() @@ -1133,7 +1145,7 @@ class ArrayConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if value and isinstance(value[0], list): xmlWriter.begintag(name) xmlWriter.newline() @@ -1162,10 +1174,10 @@ class TableConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.begintag(name) xmlWriter.newline() - value.toXML(xmlWriter, progress) + value.toXML(xmlWriter) xmlWriter.endtag(name) xmlWriter.newline() @@ -1184,7 +1196,7 @@ def getClass(self): return PrivateDict - def read(self, parent, value): + def _read(self, parent, value): size, offset = value file = parent.file isCFF2 = parent._isCFF2 @@ -1209,7 +1221,7 @@ def getClass(self): return SubrsIndex - def read(self, parent, value): + def _read(self, parent, value): file = parent.file isCFF2 = parent._isCFF2 file.seek(parent.offset + value) # Offset(self) @@ -1221,7 +1233,7 @@ class CharStringsConverter(TableConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file isCFF2 = parent._isCFF2 charset = parent.charset @@ -1265,8 +1277,8 @@ return charStrings -class CharsetConverter(object): - def read(self, parent, value): +class CharsetConverter(SimpleConverter): + def _read(self, parent, value): isCID = hasattr(parent, "ROS") if value > 2: numGlyphs = parent.numGlyphs @@ -1301,7 +1313,7 @@ def write(self, parent, value): return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): # XXX only write charset when not in OT/TTX context, where we # dump charset as a separate "GlyphOrder" table. # # xmlWriter.simpletag("charset") @@ -1471,7 +1483,7 @@ class EncodingConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): if value == 0: return "StandardEncoding" elif value == 1: @@ -1501,7 +1513,7 @@ return 1 return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if value in ("StandardEncoding", "ExpertEncoding"): xmlWriter.simpletag(name, name=value) xmlWriter.newline() @@ -1613,7 +1625,7 @@ class FDArrayConverter(TableConverter): - def read(self, parent, value): + def _read(self, parent, value): try: vstore = parent.VarStore except AttributeError: @@ -1640,9 +1652,9 @@ return fdArray -class FDSelectConverter(object): +class FDSelectConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file file.seek(value) fdSelect = FDSelect(file, parent.numGlyphs) @@ -1653,7 +1665,7 @@ # The FDSelect glyph data is written out to XML in the charstring keys, # so we write out only the format selector - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, [('format', value.format)]) xmlWriter.newline() @@ -1667,7 +1679,7 @@ class VarStoreConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file file.seek(value) varStore = VarStoreData(file) @@ -1677,7 +1689,7 @@ def write(self, parent, value): return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): value.writeXML(xmlWriter, name) def xmlRead(self, name, attrs, content, parent): @@ -1771,7 +1783,7 @@ class ROSConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): registry, order, supplement = value xmlWriter.simpletag( name, @@ -2245,7 +2257,7 @@ setattr(self, name, value) return value - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): for name in self.order: if name in self.skipNames: continue @@ -2262,7 +2274,7 @@ if value is None and name != "charset": continue conv = self.converters[name] - conv.xmlWrite(xmlWriter, name, value, progress) + conv.xmlWrite(xmlWriter, name, value) ignoredNames = set(self.rawDict) - set(self.order) if ignoredNames: xmlWriter.comment( @@ -2310,9 +2322,9 @@ else: self.numGlyphs = readCard16(self.file) - def toXML(self, xmlWriter, progress): + def toXML(self, xmlWriter): if hasattr(self, "CharStrings"): - self.decompileAllCharStrings(progress) + self.decompileAllCharStrings() if hasattr(self, "ROS"): self.skipNames = ['Encoding'] if not hasattr(self, "ROS") or not hasattr(self, "CharStrings"): @@ -2320,25 +2332,21 @@ # in CID fonts. self.skipNames = [ 'CIDFontVersion', 'CIDFontRevision', 'CIDFontType', 'CIDCount'] - BaseDict.toXML(self, xmlWriter, progress) + BaseDict.toXML(self, xmlWriter) - def decompileAllCharStrings(self, progress): + def decompileAllCharStrings(self): # Make sure that all the Private Dicts have been instantiated. - i = 0 for charString in self.CharStrings.values(): try: charString.decompile() except: log.error("Error in charstring %s", i) raise - if not i % 30 and progress: - progress.increment(0) # update - i = i + 1 def recalcFontBBox(self): fontBBox = None for charString in self.CharStrings.values(): - bounds = charString.calcBounds() + bounds = charString.calcBounds(self.CharStrings) if bounds is not None: if fontBBox is not None: fontBBox = unionRect(fontBBox, bounds) @@ -2382,13 +2390,24 @@ defaults = {} converters = buildConverters(topDictOperators) compilerClass = FontDictCompiler - order = ['FontName', 'FontMatrix', 'Weight', 'Private'] + orderCFF = ['FontName', 'FontMatrix', 'Weight', 'Private'] + orderCFF2 = ['Private'] decompilerClass = TopDictDecompiler def __init__(self, strings=None, file=None, offset=None, GlobalSubrs=None, isCFF2=None, vstore=None): super(FontDict, self).__init__(strings, file, offset, isCFF2=isCFF2) self.vstore = vstore + self.setCFF2(isCFF2) + + def setCFF2(self, isCFF2): + # isCFF2 may be None. + if isCFF2: + self.order = self.orderCFF2 + self._isCFF2 = True + else: + self.order = self.orderCFF + self._isCFF2 = False class PrivateDict(BaseDict): diff -Nru fonttools-3.21.2/Snippets/fontTools/cffLib/specializer.py fonttools-3.29.0/Snippets/fontTools/cffLib/specializer.py --- fonttools-3.21.2/Snippets/fontTools/cffLib/specializer.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/cffLib/specializer.py 2018-07-26 14:12:55.000000000 +0000 @@ -436,7 +436,6 @@ if op[2:] == 'curveto' and len(args) == 5 and prv == nxt == 'rrcurveto': assert (op[0] == 'r') ^ (op[1] == 'r') - args = list(args) if op[0] == 'v': pos = 0 elif op[0] != 'r': @@ -445,7 +444,8 @@ pos = 4 else: pos = 5 - args.insert(pos, 0) + # Insert, while maintaining the type of args (can be tuple or list). + args = args[:pos] + type(args)((0,)) + args[pos:] commands[i] = ('rrcurveto', args) continue @@ -493,7 +493,9 @@ if d0 is None: continue new_op = d0+d+'curveto' - if new_op and len(args1) + len(args2) <= maxstack: + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + if new_op and len(args1) + len(args2) < maxstack: commands[i-1] = (new_op, args1+args2) del commands[i] diff -Nru fonttools-3.21.2/Snippets/fontTools/cffLib/width.py fonttools-3.29.0/Snippets/fontTools/cffLib/width.py --- fonttools-3.21.2/Snippets/fontTools/cffLib/width.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/cffLib/width.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +"""T2CharString glyph width optimizer.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, getTableClass +from collections import defaultdict +from operator import add +from functools import partial, reduce + + +class missingdict(dict): + def __init__(self, missing_func): + self.missing_func = missing_func + def __missing__(self, v): + return self.missing_func(v) + +def cumSum(f, op=add, start=0, decreasing=False): + + keys = sorted(f.keys()) + minx, maxx = keys[0], keys[-1] + + total = reduce(op, f.values(), start) + + if decreasing: + missing = lambda x: start if x > maxx else total + domain = range(maxx, minx - 1, -1) + else: + missing = lambda x: start if x < minx else total + domain = range(minx, maxx + 1) + + out = missingdict(missing) + + v = start + for x in domain: + v = op(v, f[x]) + out[x] = v + + return out + +def byteCost(widths, default, nominal): + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + cost = 0 + for w,freq in widths.items(): + if w == default: continue + diff = abs(w - nominal) + if diff <= 107: + cost += freq + elif diff <= 1131: + cost += freq * 2 + else: + cost += freq * 5 + return cost + + +def optimizeWidthsBruteforce(widths): + """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" + + d = defaultdict(int) + for w in widths: + d[w] += 1 + + # Maximum number of bytes using default can possibly save + maxDefaultAdvantage = 5 * max(d.values()) + + minw, maxw = min(widths), max(widths) + domain = list(range(minw, maxw+1)) + + bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) + + bestCost = len(widths) * 5 + 1 + for nominal in domain: + if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: + continue + for default in domain: + cost = byteCost(widths, default, nominal) + if cost < bestCost: + bestCost = cost + bestDefault = default + bestNominal = nominal + + return bestDefault, bestNominal + + +def optimizeWidths(widths): + """Given a list of glyph widths, or dictionary mapping glyph width to number of + glyphs having that, returns a tuple of best CFF default and nominal glyph widths. + + This algorithm is linear in UPEM+numGlyphs.""" + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + keys = sorted(widths.keys()) + minw, maxw = keys[0], keys[-1] + domain = list(range(minw, maxw+1)) + + # Cumulative sum/max forward/backward. + cumFrqU = cumSum(widths, op=add) + cumMaxU = cumSum(widths, op=max) + cumFrqD = cumSum(widths, op=add, decreasing=True) + cumMaxD = cumSum(widths, op=max, decreasing=True) + + # Cost per nominal choice, without default consideration. + nomnCostU = missingdict(lambda x: cumFrqU[x] + cumFrqU[x-108] + cumFrqU[x-1132]*3) + nomnCostD = missingdict(lambda x: cumFrqD[x] + cumFrqD[x+108] + cumFrqD[x+1132]*3) + nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) + + # Cost-saving per nominal choice, by best default choice. + dfltCostU = missingdict(lambda x: max(cumMaxU[x], cumMaxU[x-108]*2, cumMaxU[x-1132]*5)) + dfltCostD = missingdict(lambda x: max(cumMaxD[x], cumMaxD[x+108]*2, cumMaxD[x+1132]*5)) + dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) + + # Combined cost per nominal choice. + bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) + + # Best nominal. + nominal = min(domain, key=lambda x: bestCost[x]) + + # Work back the best default. + bestC = bestCost[nominal] + dfltC = nomnCost[nominal] - bestCost[nominal] + ends = [] + if dfltC == dfltCostU[nominal]: + starts = [nominal, nominal-108, nominal-1131] + for start in starts: + while cumMaxU[start] and cumMaxU[start] == cumMaxU[start-1]: + start -= 1 + ends.append(start) + else: + starts = [nominal, nominal+108, nominal+1131] + for start in starts: + while cumMaxD[start] and cumMaxD[start] == cumMaxD[start+1]: + start += 1 + ends.append(start) + default = min(ends, key=lambda default: byteCost(widths, default, nominal)) + + return default, nominal + + +if __name__ == '__main__': + import sys + if len(sys.argv) == 1: + import doctest + sys.exit(doctest.testmod().failed) + for fontfile in sys.argv[1:]: + font = TTFont(fontfile) + hmtx = font['hmtx'] + widths = [m[0] for m in hmtx.metrics.values()] + default, nominal = optimizeWidths(widths) + print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) + #default, nominal = optimizeWidthsBruteforce(widths) + #print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) diff -Nru fonttools-3.21.2/Snippets/fontTools/designspaceLib/__init__.py fonttools-3.29.0/Snippets/fontTools/designspaceLib/__init__.py --- fonttools-3.21.2/Snippets/fontTools/designspaceLib/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/designspaceLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,1252 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import LogMixin +import collections +import os +import posixpath +import plistlib + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +""" + designSpaceDocument + + - read and write designspace files +""" + +__all__ = [ + 'DesignSpaceDocumentError', 'DesignSpaceDocument', 'SourceDescriptor', + 'InstanceDescriptor', 'AxisDescriptor', 'RuleDescriptor', 'BaseDocReader', + 'BaseDocWriter' +] + +# ElementTree allows to find namespace-prefixed elements, but not attributes +# so we have to do it ourselves for 'xml:lang' +XML_NS = "{http://www.w3.org/XML/1998/namespace}" +XML_LANG = XML_NS + "lang" + + +def to_plist(value): + try: + # Python 2 + string = plistlib.writePlistToString(value) + except AttributeError: + # Python 3 + string = plistlib.dumps(value).decode() + return ET.fromstring(string)[0] + + +def from_plist(element): + if element is None: + return {} + plist = ET.Element('plist') + plist.append(element) + string = ET.tostring(plist) + try: + # Python 2 + return plistlib.readPlistFromString(string) + except AttributeError: + # Python 3 + return plistlib.loads(string, fmt=plistlib.FMT_XML) + + +def posix(path): + """Normalize paths using forward slash to work also on Windows.""" + new_path = posixpath.join(*path.split(os.path.sep)) + if path.startswith('/'): + # The above transformation loses absolute paths + new_path = '/' + new_path + return new_path + + +def posixpath_property(private_name): + def getter(self): + # Normal getter + return getattr(self, private_name) + + def setter(self, value): + # The setter rewrites paths using forward slashes + if value is not None: + value = posix(value) + setattr(self, private_name, value) + + return property(getter, setter) + + +class DesignSpaceDocumentError(Exception): + def __init__(self, msg, obj=None): + self.msg = msg + self.obj = obj + + def __str__(self): + return str(self.msg) + ( + ": %r" % self.obj if self.obj is not None else "") + + +def _indent(elem, whitespace=" ", level=0): + # taken from http://effbot.org/zone/element-lib.htm#prettyprint + i = "\n" + level * whitespace + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + whitespace + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, whitespace, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class SimpleDescriptor(object): + """ Containers for a bunch of attributes""" + + # XXX this is ugly. The 'print' is inappropriate here, and instead of + # assert, it should simply return True/False + def compare(self, other): + # test if this object contains the same data as the other + for attr in self._attrs: + try: + assert(getattr(self, attr) == getattr(other, attr)) + except AssertionError: + print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr)) + + +class SourceDescriptor(SimpleDescriptor): + """Simple container for data related to the source""" + flavor = "source" + _attrs = ['filename', 'path', 'name', 'layerName', + 'location', 'copyLib', + 'copyGroups', 'copyFeatures', + 'muteKerning', 'muteInfo', + 'mutedGlyphNames', + 'familyName', 'styleName'] + + def __init__(self): + self.filename = None + """The original path as found in the document.""" + + self.path = None + """The absolute path, calculated from filename.""" + + self.font = None + """Any Python object. Optional. Points to a representation of this + source font that is loaded in memory, as a Python object (e.g. a + ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). + + The default document reader will not fill-in this attribute, and the + default writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by + ``filename`` and store it in this field, or write the contents of + this field to the disk and make ```filename`` point to that. + """ + + self.name = None + self.location = None + self.layerName = None + self.copyLib = False + self.copyInfo = False + self.copyGroups = False + self.copyFeatures = False + self.muteKerning = False + self.muteInfo = False + self.mutedGlyphNames = [] + self.familyName = None + self.styleName = None + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + +class RuleDescriptor(SimpleDescriptor): + """ + + + + + + + + + + + + """ + _attrs = ['name', 'conditionSets', 'subs'] # what do we need here + + def __init__(self): + self.name = None + self.conditionSets = [] # list of list of dict(name='aaaa', minimum=0, maximum=1000) + self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt") + + +def evaluateRule(rule, location): + """ Return True if any of the rule's conditionsets matches the given location.""" + return any(evaluateConditions(c, location) for c in rule.conditionSets) + + +def evaluateConditions(conditions, location): + """ Return True if all the conditions matches the given location. + If a condition has no minimum, check for < maximum. + If a condition has no maximum, check for > minimum. + """ + for cd in conditions: + value = location[cd['name']] + if cd.get('minimum') is None: + if value > cd['maximum']: + return False + elif cd.get('maximum') is None: + if cd['minimum'] > value: + return False + elif not cd['minimum'] <= value <= cd['maximum']: + return False + return True + + +def processRules(rules, location, glyphNames): + """ Apply these rules at this location to these glyphnames.minimum + - rule order matters + """ + newNames = [] + for rule in rules: + if evaluateRule(rule, location): + for name in glyphNames: + swap = False + for a, b in rule.subs: + if name == a: + swap = True + break + if swap: + newNames.append(b) + else: + newNames.append(name) + glyphNames = newNames + newNames = [] + return glyphNames + + +class InstanceDescriptor(SimpleDescriptor): + """Simple container for data related to the instance""" + flavor = "instance" + _defaultLanguageCode = "en" + _attrs = ['path', + 'name', + 'location', + 'familyName', + 'styleName', + 'postScriptFontName', + 'styleMapFamilyName', + 'styleMapStyleName', + 'kerning', + 'info', + 'lib'] + + def __init__(self): + self.filename = None # the original path as found in the document + self.path = None # the absolute path, calculated from filename + self.name = None + self.location = None + self.familyName = None + self.styleName = None + self.postScriptFontName = None + self.styleMapFamilyName = None + self.styleMapStyleName = None + self.localisedStyleName = {} + self.localisedFamilyName = {} + self.localisedStyleMapStyleName = {} + self.localisedStyleMapFamilyName = {} + self.glyphs = {} + self.mutedGlyphNames = [] + self.kerning = True + self.info = True + + self.lib = {} + """Custom data associated with this instance.""" + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + def setStyleName(self, styleName, languageCode="en"): + self.localisedStyleName[languageCode] = styleName + + def getStyleName(self, languageCode="en"): + return self.localisedStyleName.get(languageCode) + + def setFamilyName(self, familyName, languageCode="en"): + self.localisedFamilyName[languageCode] = familyName + + def getFamilyName(self, languageCode="en"): + return self.localisedFamilyName.get(languageCode) + + def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"): + self.localisedStyleMapStyleName[languageCode] = styleMapStyleName + + def getStyleMapStyleName(self, languageCode="en"): + return self.localisedStyleMapStyleName.get(languageCode) + + def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"): + self.localisedStyleMapFamilyName[languageCode] = styleMapFamilyName + + def getStyleMapFamilyName(self, languageCode="en"): + return self.localisedStyleMapFamilyName.get(languageCode) + + +def tagForAxisName(name): + # try to find or make a tag name for this axis name + names = { + 'weight': ('wght', dict(en = 'Weight')), + 'width': ('wdth', dict(en = 'Width')), + 'optical': ('opsz', dict(en = 'Optical Size')), + 'slant': ('slnt', dict(en = 'Slant')), + 'italic': ('ital', dict(en = 'Italic')), + } + if name.lower() in names: + return names[name.lower()] + if len(name) < 4: + tag = name + "*" * (4 - len(name)) + else: + tag = name[:4] + return tag, dict(en=name) + + +class AxisDescriptor(SimpleDescriptor): + """ Simple container for the axis data + Add more localisations? + """ + flavor = "axis" + _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map'] + + def __init__(self): + self.tag = None # opentype tag for this axis + self.name = None # name of the axis used in locations + self.labelNames = {} # names for UI purposes, if this is not a standard axis, + self.minimum = None + self.maximum = None + self.default = None + self.hidden = False + self.map = [] + + def serialize(self): + # output to a dict, used in testing + return dict( + tag=self.tag, + name=self.name, + labelNames=self.labelNames, + maximum=self.maximum, + minimum=self.minimum, + default=self.default, + hidden=self.hidden, + map=self.map, + ) + + +class BaseDocWriter(object): + _whiteSpace = " " + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + @classmethod + def getAxisDecriptor(cls): + return cls.axisDescriptorClass() + + @classmethod + def getSourceDescriptor(cls): + return cls.sourceDescriptorClass() + + @classmethod + def getInstanceDescriptor(cls): + return cls.instanceDescriptorClass() + + @classmethod + def getRuleDescriptor(cls): + return cls.ruleDescriptorClass() + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + self.documentVersion = "4.0" + self.root = ET.Element("designspace") + self.root.attrib['format'] = self.documentVersion + self._axes = [] # for use by the writer only + self._rules = [] # for use by the writer only + + def write(self, pretty=True): + if self.documentObject.axes: + self.root.append(ET.Element("axes")) + for axisObject in self.documentObject.axes: + self._addAxis(axisObject) + + if self.documentObject.rules: + self.root.append(ET.Element("rules")) + for ruleObject in self.documentObject.rules: + self._addRule(ruleObject) + + if self.documentObject.sources: + self.root.append(ET.Element("sources")) + for sourceObject in self.documentObject.sources: + self._addSource(sourceObject) + + if self.documentObject.instances: + self.root.append(ET.Element("instances")) + for instanceObject in self.documentObject.instances: + self._addInstance(instanceObject) + + if self.documentObject.lib: + self._addLib(self.documentObject.lib) + + if pretty: + _indent(self.root, whitespace=self._whiteSpace) + tree = ET.ElementTree(self.root) + tree.write(self.path, encoding="utf-8", method='xml', xml_declaration=True) + + def _makeLocationElement(self, locationObject, name=None): + """ Convert Location dict to a locationElement.""" + locElement = ET.Element("location") + if name is not None: + locElement.attrib['name'] = name + validatedLocation = self.documentObject.newDefaultLocation() + for axisName, axisValue in locationObject.items(): + if axisName in validatedLocation: + # only accept values we know + validatedLocation[axisName] = axisValue + for dimensionName, dimensionValue in validatedLocation.items(): + dimElement = ET.Element('dimension') + dimElement.attrib['name'] = dimensionName + if type(dimensionValue) == tuple: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue[0]) + dimElement.attrib['yvalue'] = self.intOrFloat(dimensionValue[1]) + else: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue) + locElement.append(dimElement) + return locElement, validatedLocation + + def intOrFloat(self, num): + if int(num) == num: + return "%d" % num + return "%f" % num + + def _addRule(self, ruleObject): + # if none of the conditions have minimum or maximum values, do not add the rule. + self._rules.append(ruleObject) + ruleElement = ET.Element('rule') + if ruleObject.name is not None: + ruleElement.attrib['name'] = ruleObject.name + for conditions in ruleObject.conditionSets: + conditionsetElement = ET.Element('conditionset') + for cond in conditions: + if cond.get('minimum') is None and cond.get('maximum') is None: + # neither is defined, don't add this condition + continue + conditionElement = ET.Element('condition') + conditionElement.attrib['name'] = cond.get('name') + if cond.get('minimum') is not None: + conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum')) + if cond.get('maximum') is not None: + conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum')) + conditionsetElement.append(conditionElement) + if len(conditionsetElement): + ruleElement.append(conditionsetElement) + for sub in ruleObject.subs: + subElement = ET.Element('sub') + subElement.attrib['name'] = sub[0] + subElement.attrib['with'] = sub[1] + ruleElement.append(subElement) + if len(ruleElement): + self.root.findall('.rules')[0].append(ruleElement) + + def _addAxis(self, axisObject): + self._axes.append(axisObject) + axisElement = ET.Element('axis') + axisElement.attrib['tag'] = axisObject.tag + axisElement.attrib['name'] = axisObject.name + axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum) + axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum) + axisElement.attrib['default'] = self.intOrFloat(axisObject.default) + if axisObject.hidden: + axisElement.attrib['hidden'] = "1" + for languageCode, labelName in sorted(axisObject.labelNames.items()): + languageElement = ET.Element('labelname') + languageElement.attrib[u'xml:lang'] = languageCode + languageElement.text = labelName + axisElement.append(languageElement) + if axisObject.map: + for inputValue, outputValue in axisObject.map: + mapElement = ET.Element('map') + mapElement.attrib['input'] = self.intOrFloat(inputValue) + mapElement.attrib['output'] = self.intOrFloat(outputValue) + axisElement.append(mapElement) + self.root.findall('.axes')[0].append(axisElement) + + def _addInstance(self, instanceObject): + instanceElement = ET.Element('instance') + if instanceObject.name is not None: + instanceElement.attrib['name'] = instanceObject.name + if instanceObject.familyName is not None: + instanceElement.attrib['familyname'] = instanceObject.familyName + if instanceObject.styleName is not None: + instanceElement.attrib['stylename'] = instanceObject.styleName + # add localisations + if instanceObject.localisedStyleName: + languageCodes = list(instanceObject.localisedStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedStyleNameElement = ET.Element('stylename') + localisedStyleNameElement.attrib["xml:lang"] = code + localisedStyleNameElement.text = instanceObject.getStyleName(code) + instanceElement.append(localisedStyleNameElement) + if instanceObject.localisedFamilyName: + languageCodes = list(instanceObject.localisedFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedFamilyNameElement = ET.Element('familyname') + localisedFamilyNameElement.attrib["xml:lang"] = code + localisedFamilyNameElement.text = instanceObject.getFamilyName(code) + instanceElement.append(localisedFamilyNameElement) + if instanceObject.localisedStyleMapStyleName: + languageCodes = list(instanceObject.localisedStyleMapStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapStyleNameElement = ET.Element('stylemapstylename') + localisedStyleMapStyleNameElement.attrib["xml:lang"] = code + localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code) + instanceElement.append(localisedStyleMapStyleNameElement) + if instanceObject.localisedStyleMapFamilyName: + languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname') + localisedStyleMapFamilyNameElement.attrib["xml:lang"] = code + localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code) + instanceElement.append(localisedStyleMapFamilyNameElement) + + if instanceObject.location is not None: + locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location) + instanceElement.append(locationElement) + if instanceObject.filename is not None: + instanceElement.attrib['filename'] = instanceObject.filename + if instanceObject.postScriptFontName is not None: + instanceElement.attrib['postscriptfontname'] = instanceObject.postScriptFontName + if instanceObject.styleMapFamilyName is not None: + instanceElement.attrib['stylemapfamilyname'] = instanceObject.styleMapFamilyName + if instanceObject.styleMapStyleName is not None: + instanceElement.attrib['stylemapstylename'] = instanceObject.styleMapStyleName + if instanceObject.glyphs: + if instanceElement.findall('.glyphs') == []: + glyphsElement = ET.Element('glyphs') + instanceElement.append(glyphsElement) + glyphsElement = instanceElement.findall('.glyphs')[0] + for glyphName, data in sorted(instanceObject.glyphs.items()): + glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data) + glyphsElement.append(glyphElement) + if instanceObject.kerning: + kerningElement = ET.Element('kerning') + instanceElement.append(kerningElement) + if instanceObject.info: + infoElement = ET.Element('info') + instanceElement.append(infoElement) + if instanceObject.lib: + libElement = ET.Element('lib') + libElement.append(to_plist(instanceObject.lib)) + instanceElement.append(libElement) + self.root.findall('.instances')[0].append(instanceElement) + + def _addSource(self, sourceObject): + sourceElement = ET.Element("source") + if sourceObject.filename is not None: + sourceElement.attrib['filename'] = sourceObject.filename + if sourceObject.name is not None: + if sourceObject.name.find("temp_master") != 0: + # do not save temporary source names + sourceElement.attrib['name'] = sourceObject.name + if sourceObject.familyName is not None: + sourceElement.attrib['familyname'] = sourceObject.familyName + if sourceObject.styleName is not None: + sourceElement.attrib['stylename'] = sourceObject.styleName + if sourceObject.layerName is not None: + sourceElement.attrib['layer'] = sourceObject.layerName + if sourceObject.copyLib: + libElement = ET.Element('lib') + libElement.attrib['copy'] = "1" + sourceElement.append(libElement) + if sourceObject.copyGroups: + groupsElement = ET.Element('groups') + groupsElement.attrib['copy'] = "1" + sourceElement.append(groupsElement) + if sourceObject.copyFeatures: + featuresElement = ET.Element('features') + featuresElement.attrib['copy'] = "1" + sourceElement.append(featuresElement) + if sourceObject.copyInfo or sourceObject.muteInfo: + infoElement = ET.Element('info') + if sourceObject.copyInfo: + infoElement.attrib['copy'] = "1" + if sourceObject.muteInfo: + infoElement.attrib['mute'] = "1" + sourceElement.append(infoElement) + if sourceObject.muteKerning: + kerningElement = ET.Element("kerning") + kerningElement.attrib["mute"] = '1' + sourceElement.append(kerningElement) + if sourceObject.mutedGlyphNames: + for name in sourceObject.mutedGlyphNames: + glyphElement = ET.Element("glyph") + glyphElement.attrib["name"] = name + glyphElement.attrib["mute"] = '1' + sourceElement.append(glyphElement) + locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location) + sourceElement.append(locationElement) + self.root.findall('.sources')[0].append(sourceElement) + + def _addLib(self, dict): + libElement = ET.Element('lib') + libElement.append(to_plist(dict)) + self.root.append(libElement) + + def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data): + glyphElement = ET.Element('glyph') + if data.get('mute'): + glyphElement.attrib['mute'] = "1" + if data.get('unicodes') is not None: + glyphElement.attrib['unicode'] = " ".join([hex(u) for u in data.get('unicodes')]) + if data.get('instanceLocation') is not None: + locationElement, data['instanceLocation'] = self._makeLocationElement(data.get('instanceLocation')) + glyphElement.append(locationElement) + if glyphName is not None: + glyphElement.attrib['name'] = glyphName + if data.get('note') is not None: + noteElement = ET.Element('note') + noteElement.text = data.get('note') + glyphElement.append(noteElement) + if data.get('masters') is not None: + mastersElement = ET.Element("masters") + for m in data.get('masters'): + masterElement = ET.Element("master") + if m.get('glyphName') is not None: + masterElement.attrib['glyphname'] = m.get('glyphName') + if m.get('font') is not None: + masterElement.attrib['source'] = m.get('font') + if m.get('location') is not None: + locationElement, m['location'] = self._makeLocationElement(m.get('location')) + masterElement.append(locationElement) + mastersElement.append(masterElement) + glyphElement.append(mastersElement) + return glyphElement + + +class BaseDocReader(LogMixin): + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + tree = ET.parse(self.path) + self.root = tree.getroot() + self.documentObject.formatVersion = self.root.attrib.get("format", "3.0") + self._axes = [] + self.rules = [] + self.sources = [] + self.instances = [] + self.axisDefaults = {} + self._strictAxisNames = True + + def read(self): + self.readAxes() + self.readRules() + self.readSources() + self.readInstances() + self.readLib() + + def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + paths = [] + for name in self.documentObject.sources.keys(): + paths.append(self.documentObject.sources[name][0].path) + return paths + + def readRules(self): + # we also need to read any conditions that are outside of a condition set. + rules = [] + for ruleElement in self.root.findall(".rules/rule"): + ruleObject = self.ruleDescriptorClass() + ruleName = ruleObject.name = ruleElement.attrib.get("name") + # read any stray conditions outside a condition set + externalConditions = self._readConditionElements( + ruleElement, + ruleName, + ) + if externalConditions: + ruleObject.conditionSets.append(externalConditions) + self.log.info( + "Found stray rule conditions outside a conditionset. " + "Wrapped them in a new conditionset." + ) + # read the conditionsets + for conditionSetElement in ruleElement.findall('.conditionset'): + conditionSet = self._readConditionElements( + conditionSetElement, + ruleName, + ) + if conditionSet is not None: + ruleObject.conditionSets.append(conditionSet) + for subElement in ruleElement.findall('.sub'): + a = subElement.attrib['name'] + b = subElement.attrib['with'] + ruleObject.subs.append((a, b)) + rules.append(ruleObject) + self.documentObject.rules = rules + + def _readConditionElements(self, parentElement, ruleName=None): + cds = [] + for conditionElement in parentElement.findall('.condition'): + cd = {} + cdMin = conditionElement.attrib.get("minimum") + if cdMin is not None: + cd['minimum'] = float(cdMin) + else: + # will allow these to be None, assume axis.minimum + cd['minimum'] = None + cdMax = conditionElement.attrib.get("maximum") + if cdMax is not None: + cd['maximum'] = float(cdMax) + else: + # will allow these to be None, assume axis.maximum + cd['maximum'] = None + cd['name'] = conditionElement.attrib.get("name") + # # test for things + if cd.get('minimum') is None and cd.get('maximum') is None: + raise DesignSpaceDocumentError( + "condition missing required minimum or maximum in rule" + + (" '%s'" % ruleName if ruleName is not None else "")) + cds.append(cd) + return cds + + def readAxes(self): + # read the axes elements, including the warp map. + if len(self.root.findall(".axes/axis")) == 0: + self._strictAxisNames = False + return + for axisElement in self.root.findall(".axes/axis"): + axisObject = self.axisDescriptorClass() + axisObject.name = axisElement.attrib.get("name") + axisObject.minimum = float(axisElement.attrib.get("minimum")) + axisObject.maximum = float(axisElement.attrib.get("maximum")) + if axisElement.attrib.get('hidden', False): + axisObject.hidden = True + axisObject.default = float(axisElement.attrib.get("default")) + axisObject.tag = axisElement.attrib.get("tag") + for mapElement in axisElement.findall('map'): + a = float(mapElement.attrib['input']) + b = float(mapElement.attrib['output']) + axisObject.map.append((a, b)) + for labelNameElement in axisElement.findall('labelname'): + # Note: elementtree reads the xml:lang attribute name as + # '{http://www.w3.org/XML/1998/namespace}lang' + for key, lang in labelNameElement.items(): + if key == XML_LANG: + labelName = labelNameElement.text + axisObject.labelNames[lang] = labelName + self.documentObject.axes.append(axisObject) + self.axisDefaults[axisObject.name] = axisObject.default + self.documentObject.defaultLoc = self.axisDefaults + + def readSources(self): + for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")): + filename = sourceElement.attrib.get('filename') + if filename is not None and self.path is not None: + sourcePath = os.path.abspath(os.path.join(os.path.dirname(self.path), filename)) + else: + sourcePath = None + sourceName = sourceElement.attrib.get('name') + if sourceName is None: + # add a temporary source name + sourceName = "temp_master.%d" % (sourceCount) + sourceObject = self.sourceDescriptorClass() + sourceObject.path = sourcePath # absolute path to the ufo source + sourceObject.filename = filename # path as it is stored in the document + sourceObject.name = sourceName + familyName = sourceElement.attrib.get("familyname") + if familyName is not None: + sourceObject.familyName = familyName + styleName = sourceElement.attrib.get("stylename") + if styleName is not None: + sourceObject.styleName = styleName + sourceObject.location = self.locationFromElement(sourceElement) + layerName = sourceElement.attrib.get('layer') + if layerName is not None: + sourceObject.layerName = layerName + for libElement in sourceElement.findall('.lib'): + if libElement.attrib.get('copy') == '1': + sourceObject.copyLib = True + for groupsElement in sourceElement.findall('.groups'): + if groupsElement.attrib.get('copy') == '1': + sourceObject.copyGroups = True + for infoElement in sourceElement.findall(".info"): + if infoElement.attrib.get('copy') == '1': + sourceObject.copyInfo = True + if infoElement.attrib.get('mute') == '1': + sourceObject.muteInfo = True + for featuresElement in sourceElement.findall(".features"): + if featuresElement.attrib.get('copy') == '1': + sourceObject.copyFeatures = True + for glyphElement in sourceElement.findall(".glyph"): + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + continue + if glyphElement.attrib.get('mute') == '1': + sourceObject.mutedGlyphNames.append(glyphName) + for kerningElement in sourceElement.findall(".kerning"): + if kerningElement.attrib.get('mute') == '1': + sourceObject.muteKerning = True + self.documentObject.sources.append(sourceObject) + + def locationFromElement(self, element): + elementLocation = None + for locationElement in element.findall('.location'): + elementLocation = self.readLocationElement(locationElement) + break + return elementLocation + + def readLocationElement(self, locationElement): + """ Format 0 location reader """ + if not self.documentObject.axes: + raise DesignSpaceDocumentError("No axes defined") + loc = {} + for dimensionElement in locationElement.findall(".dimension"): + dimName = dimensionElement.attrib.get("name") + if self._strictAxisNames and dimName not in self.axisDefaults: + # In case the document contains no axis definitions, + self.log.warning("Location with undefined axis: \"%s\".", dimName) + continue + xValue = yValue = None + try: + xValue = dimensionElement.attrib.get('xvalue') + xValue = float(xValue) + except ValueError: + self.log.warning("KeyError in readLocation xValue %3.3f", xValue) + try: + yValue = dimensionElement.attrib.get('yvalue') + if yValue is not None: + yValue = float(yValue) + except ValueError: + pass + if yValue is not None: + loc[dimName] = (xValue, yValue) + else: + loc[dimName] = xValue + return loc + + def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + instanceElements = self.root.findall('.instances/instance') + for instanceElement in instanceElements: + self._readSingleInstanceElement(instanceElement, makeGlyphs=makeGlyphs, makeKerning=makeKerning, makeInfo=makeInfo) + + def _readSingleInstanceElement(self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True): + filename = instanceElement.attrib.get('filename') + if filename is not None: + instancePath = os.path.join(os.path.dirname(self.documentObject.path), filename) + else: + instancePath = None + instanceObject = self.instanceDescriptorClass() + instanceObject.path = instancePath # absolute path to the instance + instanceObject.filename = filename # path as it is stored in the document + name = instanceElement.attrib.get("name") + if name is not None: + instanceObject.name = name + familyname = instanceElement.attrib.get('familyname') + if familyname is not None: + instanceObject.familyName = familyname + stylename = instanceElement.attrib.get('stylename') + if stylename is not None: + instanceObject.styleName = stylename + postScriptFontName = instanceElement.attrib.get('postscriptfontname') + if postScriptFontName is not None: + instanceObject.postScriptFontName = postScriptFontName + styleMapFamilyName = instanceElement.attrib.get('stylemapfamilyname') + if styleMapFamilyName is not None: + instanceObject.styleMapFamilyName = styleMapFamilyName + styleMapStyleName = instanceElement.attrib.get('stylemapstylename') + if styleMapStyleName is not None: + instanceObject.styleMapStyleName = styleMapStyleName + # read localised names + for styleNameElement in instanceElement.findall('stylename'): + for key, lang in styleNameElement.items(): + if key == XML_LANG: + styleName = styleNameElement.text + instanceObject.setStyleName(styleName, lang) + for familyNameElement in instanceElement.findall('familyname'): + for key, lang in familyNameElement.items(): + if key == XML_LANG: + familyName = familyNameElement.text + instanceObject.setFamilyName(familyName, lang) + for styleMapStyleNameElement in instanceElement.findall('stylemapstylename'): + for key, lang in styleMapStyleNameElement.items(): + if key == XML_LANG: + styleMapStyleName = styleMapStyleNameElement.text + instanceObject.setStyleMapStyleName(styleMapStyleName, lang) + for styleMapFamilyNameElement in instanceElement.findall('stylemapfamilyname'): + for key, lang in styleMapFamilyNameElement.items(): + if key == XML_LANG: + styleMapFamilyName = styleMapFamilyNameElement.text + instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang) + instanceLocation = self.locationFromElement(instanceElement) + if instanceLocation is not None: + instanceObject.location = instanceLocation + for glyphElement in instanceElement.findall('.glyphs/glyph'): + self.readGlyphElement(glyphElement, instanceObject) + for infoElement in instanceElement.findall("info"): + self.readInfoElement(infoElement, instanceObject) + for libElement in instanceElement.findall('lib'): + self.readLibElement(libElement, instanceObject) + self.documentObject.instances.append(instanceObject) + + def readLibElement(self, libElement, instanceObject): + """Read the lib element for the given instance.""" + instanceObject.lib = from_plist(libElement[0]) + + def readInfoElement(self, infoElement, instanceObject): + """ Read the info element.""" + instanceObject.info = True + + def readKerningElement(self, kerningElement, instanceObject): + """ Read the kerning element.""" + kerningLocation = self.locationFromElement(kerningElement) + instanceObject.addKerning(kerningLocation) + + def readGlyphElement(self, glyphElement, instanceObject): + """ + Read the glyph element. + + + + + + + This is an instance from an anisotropic interpolation. + + + """ + glyphData = {} + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + raise DesignSpaceDocumentError("Glyph object without name attribute") + mute = glyphElement.attrib.get("mute") + if mute == "1": + glyphData['mute'] = True + # unicode + unicodes = glyphElement.attrib.get('unicode') + if unicodes is not None: + try: + unicodes = [int(u, 16) for u in unicodes.split(" ")] + glyphData['unicodes'] = unicodes + except ValueError: + raise DesignSpaceDocumentError("unicode values %s are not integers" % unicodes) + + for noteElement in glyphElement.findall('.note'): + glyphData['note'] = noteElement.text + break + instanceLocation = self.locationFromElement(glyphElement) + if instanceLocation is not None: + glyphData['instanceLocation'] = instanceLocation + glyphSources = None + for masterElement in glyphElement.findall('.masters/master'): + fontSourceName = masterElement.attrib.get('source') + sourceLocation = self.locationFromElement(masterElement) + masterGlyphName = masterElement.attrib.get('glyphname') + if masterGlyphName is None: + # if we don't read a glyphname, use the one we have + masterGlyphName = glyphName + d = dict(font=fontSourceName, + location=sourceLocation, + glyphName=masterGlyphName) + if glyphSources is None: + glyphSources = [] + glyphSources.append(d) + if glyphSources is not None: + glyphData['masters'] = glyphSources + instanceObject.glyphs[glyphName] = glyphData + + def readLib(self): + """Read the lib element for the whole document.""" + for libElement in self.root.findall(".lib"): + self.documentObject.lib = from_plist(libElement[0]) + + +class DesignSpaceDocument(LogMixin): + """ Read, write data from the designspace file""" + def __init__(self, readerClass=None, writerClass=None): + self.path = None + self.filename = None + """String, optional. When the document is read from the disk, this is + its original file name, i.e. the last part of its path. + + When the document is produced by a Python script and still only exists + in memory, the producing script can write here an indication of a + possible "good" filename, in case one wants to save the file somewhere. + """ + + self.formatVersion = None + self.sources = [] + self.instances = [] + self.axes = [] + self.rules = [] + self.default = None # name of the default master + self.defaultLoc = None + + self.lib = {} + """Custom data associated with the whole document.""" + + # + if readerClass is not None: + self.readerClass = readerClass + else: + self.readerClass = BaseDocReader + if writerClass is not None: + self.writerClass = writerClass + else: + self.writerClass = BaseDocWriter + + def read(self, path): + self.path = path + self.filename = os.path.basename(path) + reader = self.readerClass(path, self) + reader.read() + if self.sources: + self.findDefault() + + def write(self, path): + self.path = path + self.filename = os.path.basename(path) + self.updatePaths() + writer = self.writerClass(path, self) + writer.write() + + def _posixRelativePath(self, otherPath): + relative = os.path.relpath(otherPath, os.path.dirname(self.path)) + return posix(relative) + + def updatePaths(self): + """ + Right before we save we need to identify and respond to the following situations: + In each descriptor, we have to do the right thing for the filename attribute. + + case 1. + descriptor.filename == None + descriptor.path == None + + -- action: + write as is, descriptors will not have a filename attr. + useless, but no reason to interfere. + + + case 2. + descriptor.filename == "../something" + descriptor.path == None + + -- action: + write as is. The filename attr should not be touched. + + + case 3. + descriptor.filename == None + descriptor.path == "~/absolute/path/there" + + -- action: + calculate the relative path for filename. + We're not overwriting some other value for filename, it should be fine + + + case 4. + descriptor.filename == '../somewhere' + descriptor.path == "~/absolute/path/there" + + -- action: + there is a conflict between the given filename, and the path. + So we know where the file is relative to the document. + Can't guess why they're different, we just choose for path to be correct and update filename. + + + """ + for descriptor in self.sources + self.instances: + # check what the relative path really should be? + expectedFilename = None + if descriptor.path is not None and self.path is not None: + expectedFilename = self._posixRelativePath(descriptor.path) + + # 3 + if descriptor.filename is None and descriptor.path is not None and self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + continue + + # 4 + if descriptor.filename is not None and descriptor.path is not None and self.path is not None: + if descriptor.filename is not expectedFilename: + descriptor.filename = expectedFilename + + def addSource(self, sourceDescriptor): + self.sources.append(sourceDescriptor) + + def addInstance(self, instanceDescriptor): + self.instances.append(instanceDescriptor) + + def addAxis(self, axisDescriptor): + self.axes.append(axisDescriptor) + + def addRule(self, ruleDescriptor): + self.rules.append(ruleDescriptor) + + def newDefaultLocation(self): + # Without OrderedDict, output XML would be non-deterministic. + # https://github.com/LettError/designSpaceDocument/issues/10 + loc = collections.OrderedDict() + for axisDescriptor in self.axes: + loc[axisDescriptor.name] = axisDescriptor.default + return loc + + def updateFilenameFromPath(self, masters=True, instances=True, force=False): + # set a descriptor filename attr from the path and this document path + # if the filename attribute is not None: skip it. + if masters: + for descriptor in self.sources: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + if instances: + for descriptor in self.instances: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + + def newAxisDescriptor(self): + # Ask the writer class to make us a new axisDescriptor + return self.writerClass.getAxisDecriptor() + + def newSourceDescriptor(self): + # Ask the writer class to make us a new sourceDescriptor + return self.writerClass.getSourceDescriptor() + + def newInstanceDescriptor(self): + # Ask the writer class to make us a new instanceDescriptor + return self.writerClass.getInstanceDescriptor() + + def getAxisOrder(self): + names = [] + for axisDescriptor in self.axes: + names.append(axisDescriptor.name) + return names + + def getAxis(self, name): + for axisDescriptor in self.axes: + if axisDescriptor.name == name: + return axisDescriptor + return None + + def findDefault(self): + # new default finder + # take the sourcedescriptor with the location at all the defaults + # if we can't find it, return None, let someone else figure it out + self.default = None + for sourceDescriptor in self.sources: + if sourceDescriptor.location == self.defaultLoc: + # we choose you! + self.default = sourceDescriptor + return sourceDescriptor + return None + + def normalizeLocation(self, location): + # adapted from fontTools.varlib.models.normalizeLocation because: + # - this needs to work with axis names, not tags + # - this needs to accomodate anisotropic locations + # - the axes are stored differently here, it's just math + new = {} + for axis in self.axes: + if axis.name not in location: + # skipping this dimension it seems + continue + v = location.get(axis.name, axis.default) + if type(v) == tuple: + v = v[0] + if v == axis.default: + v = 0.0 + elif v < axis.default: + if axis.default == axis.minimum: + v = 0.0 + else: + v = (max(v, axis.minimum) - axis.default) / (axis.default - axis.minimum) + else: + if axis.default == axis.maximum: + v = 0.0 + else: + v = (min(v, axis.maximum) - axis.default) / (axis.maximum - axis.default) + new[axis.name] = v + return new + + def normalize(self): + # Normalise the geometry of this designspace: + # scale all the locations of all masters and instances to the -1 - 0 - 1 value. + # we need the axis data to do the scaling, so we do those last. + # masters + for item in self.sources: + item.location = self.normalizeLocation(item.location) + # instances + for item in self.instances: + # glyph masters for this instance + for _, glyphData in item.glyphs.items(): + glyphData['instanceLocation'] = self.normalizeLocation(glyphData['instanceLocation']) + for glyphMaster in glyphData['masters']: + glyphMaster['location'] = self.normalizeLocation(glyphMaster['location']) + item.location = self.normalizeLocation(item.location) + # the axes + for axis in self.axes: + # scale the map first + newMap = [] + for inputValue, outputValue in axis.map: + newOutputValue = self.normalizeLocation({axis.name: outputValue}).get(axis.name) + newMap.append((inputValue, newOutputValue)) + if newMap: + axis.map = newMap + # finally the axis values + minimum = self.normalizeLocation({axis.name: axis.minimum}).get(axis.name) + maximum = self.normalizeLocation({axis.name: axis.maximum}).get(axis.name) + default = self.normalizeLocation({axis.name: axis.default}).get(axis.name) + # and set them in the axis.minimum + axis.minimum = minimum + axis.maximum = maximum + axis.default = default + # now the rules + for rule in self.rules: + newConditionSets = [] + for conditions in rule.conditionSets: + newConditions = [] + for cond in conditions: + if cond.get('minimum') is not None: + minimum = self.normalizeLocation({cond['name']: cond['minimum']}).get(cond['name']) + else: + minimum = None + if cond.get('maximum') is not None: + maximum = self.normalizeLocation({cond['name']: cond['maximum']}).get(cond['name']) + else: + maximum = None + newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum)) + newConditionSets.append(newConditions) + rule.conditionSets = newConditionSets diff -Nru fonttools-3.21.2/Snippets/fontTools/feaLib/ast.py fonttools-3.29.0/Snippets/fontTools/feaLib/ast.py --- fonttools-3.21.2/Snippets/fontTools/feaLib/ast.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/feaLib/ast.py 2018-07-26 14:12:55.000000000 +0000 @@ -8,6 +8,76 @@ SHIFT = " " * 4 +__all__ = [ + 'AlternateSubstStatement', + 'Anchor', + 'AnchorDefinition', + 'AnonymousBlock', + 'AttachStatement', + 'BaseAxis', + 'Block', + 'BytesIO', + 'CVParametersNameStatement', + 'ChainContextPosStatement', + 'ChainContextSubstStatement', + 'CharacterStatement', + 'Comment', + 'CursivePosStatement', + 'Element', + 'Expression', + 'FeatureBlock', + 'FeatureFile', + 'FeatureLibError', + 'FeatureNameStatement', + 'FeatureReferenceStatement', + 'FontRevisionStatement', + 'GlyphClass', + 'GlyphClassDefStatement', + 'GlyphClassDefinition', + 'GlyphClassName', + 'GlyphName', + 'HheaField', + 'IgnorePosStatement', + 'IgnoreSubstStatement', + 'IncludeStatement', + 'LanguageStatement', + 'LanguageSystemStatement', + 'LigatureCaretByIndexStatement', + 'LigatureCaretByPosStatement', + 'LigatureSubstStatement', + 'LookupBlock', + 'LookupFlagStatement', + 'LookupReferenceStatement', + 'MarkBasePosStatement', + 'MarkClass', + 'MarkClassDefinition', + 'MarkClassName', + 'MarkLigPosStatement', + 'MarkMarkPosStatement', + 'MultipleSubstStatement', + 'NameRecord', + 'NestedBlock', + 'OS2Field', + 'OrderedDict', + 'PairPosStatement', + 'Py23Error', + 'ReverseChainSingleSubstStatement', + 'ScriptStatement', + 'SimpleNamespace', + 'SinglePosStatement', + 'SingleSubstStatement', + 'SizeParameters', + 'Statement', + 'StringIO', + 'SubtableStatement', + 'TableBlock', + 'Tag', + 'UnicodeIO', + 'ValueRecord', + 'ValueRecordDefinition', + 'VheaField', +] + def deviceToString(device): if device is None: @@ -49,7 +119,7 @@ class Element(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -71,7 +141,7 @@ class Comment(Element): - def __init__(self, location, text): + def __init__(self, text, location=None): super(Comment, self).__init__(location) self.text = text @@ -81,7 +151,7 @@ class GlyphName(Expression): """A single glyph name, such as cedilla.""" - def __init__(self, location, glyph): + def __init__(self, glyph, location=None): Expression.__init__(self, location) self.glyph = glyph @@ -94,7 +164,7 @@ class GlyphClass(Expression): """A glyph class, such as [acute cedilla grave].""" - def __init__(self, location, glyphs=None): + def __init__(self, glyphs=None, location=None): Expression.__init__(self, location) self.glyphs = glyphs if glyphs is not None else [] self.original = [] @@ -142,7 +212,7 @@ class GlyphClassName(Expression): """A glyph class name, such as @FRENCH_MARKS.""" - def __init__(self, location, glyphclass): + def __init__(self, glyphclass, location=None): Expression.__init__(self, location) assert isinstance(glyphclass, GlyphClassDefinition) self.glyphclass = glyphclass @@ -156,7 +226,7 @@ class MarkClassName(Expression): """A mark class name, such as @FRENCH_MARKS defined with markClass.""" - def __init__(self, location, markClass): + def __init__(self, markClass, location=None): Expression.__init__(self, location) assert isinstance(markClass, MarkClass) self.markClass = markClass @@ -169,7 +239,7 @@ class AnonymousBlock(Statement): - def __init__(self, tag, content, location): + def __init__(self, tag, content, location=None): Statement.__init__(self, location) self.tag, self.content = tag, content @@ -181,7 +251,7 @@ class Block(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) self.statements = [] @@ -205,7 +275,7 @@ class FeatureBlock(Block): - def __init__(self, location, name, use_extension): + def __init__(self, name, use_extension=False, location=None): Block.__init__(self, location) self.name, self.use_extension = name, use_extension @@ -229,19 +299,26 @@ return res -class FeatureNamesBlock(Block): - def __init__(self, location): +class NestedBlock(Block): + def __init__(self, tag, block_name, location=None): Block.__init__(self, location) + self.tag = tag + self.block_name = block_name + + def build(self, builder): + Block.build(self, builder) + if self.block_name == "ParamUILabelNameID": + builder.add_to_cv_num_named_params(self.tag) def asFea(self, indent=""): - res = indent + "featureNames {\n" + res = "{}{} {{\n".format(indent, self.block_name) res += Block.asFea(self, indent=indent) - res += indent + "};\n" + res += "{}}};\n".format(indent) return res class LookupBlock(Block): - def __init__(self, location, name, use_extension): + def __init__(self, name, use_extension=False, location=None): Block.__init__(self, location) self.name, self.use_extension = name, use_extension @@ -259,7 +336,7 @@ class TableBlock(Block): - def __init__(self, location, name): + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -272,7 +349,7 @@ class GlyphClassDefinition(Statement): """Example: @UPPERCASE = [A-Z];""" - def __init__(self, location, name, glyphs): + def __init__(self, name, glyphs, location=None): Statement.__init__(self, location) self.name = name self.glyphs = glyphs @@ -286,8 +363,8 @@ class GlyphClassDefStatement(Statement): """Example: GlyphClassDef @UPPERCASE, [B], [C], [D];""" - def __init__(self, location, baseGlyphs, markGlyphs, - ligatureGlyphs, componentGlyphs): + def __init__(self, baseGlyphs, markGlyphs, ligatureGlyphs, + componentGlyphs, location=None): Statement.__init__(self, location) self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs) self.ligatureGlyphs = ligatureGlyphs @@ -328,9 +405,13 @@ for glyph in definition.glyphSet(): if glyph in self.glyphs: otherLoc = self.glyphs[glyph].location + if otherLoc is None: + end = "" + else: + end = " at %s:%d:%d" % ( + otherLoc[0], otherLoc[1], otherLoc[2]) raise FeatureLibError( - "Glyph %s already defined at %s:%d:%d" % ( - glyph, otherLoc[0], otherLoc[1], otherLoc[2]), + "Glyph %s already defined%s" % (glyph, end), definition.location) self.glyphs[glyph] = definition @@ -343,7 +424,7 @@ class MarkClassDefinition(Statement): - def __init__(self, location, markClass, anchor, glyphs): + def __init__(self, markClass, anchor, glyphs, location=None): Statement.__init__(self, location) assert isinstance(markClass, MarkClass) assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression) @@ -359,7 +440,7 @@ class AlternateSubstStatement(Statement): - def __init__(self, location, prefix, glyph, suffix, replacement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): Statement.__init__(self, location) self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) self.replacement = replacement @@ -391,8 +472,8 @@ class Anchor(Expression): - def __init__(self, location, name, x, y, contourpoint, - xDeviceTable, yDeviceTable): + def __init__(self, x, y, name=None, contourpoint=None, + xDeviceTable=None, yDeviceTable=None, location=None): Expression.__init__(self, location) self.name = name self.x, self.y, self.contourpoint = x, y, contourpoint @@ -414,7 +495,7 @@ class AnchorDefinition(Statement): - def __init__(self, location, name, x, y, contourpoint): + def __init__(self, name, x, y, contourpoint=None, location=None): Statement.__init__(self, location) self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint @@ -427,7 +508,7 @@ class AttachStatement(Statement): - def __init__(self, location, glyphs, contourPoints): + def __init__(self, glyphs, contourPoints, location=None): Statement.__init__(self, location) self.glyphs, self.contourPoints = (glyphs, contourPoints) @@ -441,7 +522,7 @@ class ChainContextPosStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, lookups): + def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix self.lookups = lookups @@ -473,7 +554,7 @@ class ChainContextSubstStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, lookups): + def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix self.lookups = lookups @@ -505,7 +586,7 @@ class CursivePosStatement(Statement): - def __init__(self, location, glyphclass, entryAnchor, exitAnchor): + def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None): Statement.__init__(self, location) self.glyphclass = glyphclass self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor @@ -522,7 +603,7 @@ class FeatureReferenceStatement(Statement): """Example: feature salt;""" - def __init__(self, location, featureName): + def __init__(self, featureName, location=None): Statement.__init__(self, location) self.location, self.featureName = (location, featureName) @@ -534,7 +615,7 @@ class IgnorePosStatement(Statement): - def __init__(self, location, chainContexts): + def __init__(self, chainContexts, location=None): Statement.__init__(self, location) self.chainContexts = chainContexts @@ -563,7 +644,7 @@ class IgnoreSubstStatement(Statement): - def __init__(self, location, chainContexts): + def __init__(self, chainContexts, location=None): Statement.__init__(self, location) self.chainContexts = chainContexts @@ -591,8 +672,25 @@ return "ignore sub " + ", ".join(contexts) + ";" +class IncludeStatement(Statement): + def __init__(self, filename, location=None): + super(IncludeStatement, self).__init__(location) + self.filename = filename + + def build(self): + # TODO: consider lazy-loading the including parser/lexer? + raise FeatureLibError( + "Building an include statement is not implemented yet. " + "Instead, use Parser(..., followIncludes=True) for building.", + self.location) + + def asFea(self, indent=""): + return indent + "include(%s);" % self.filename + + class LanguageStatement(Statement): - def __init__(self, location, language, include_default, required): + def __init__(self, language, include_default=True, required=False, + location=None): Statement.__init__(self, location) assert(len(language) == 4) self.language = language @@ -615,7 +713,7 @@ class LanguageSystemStatement(Statement): - def __init__(self, location, script, language): + def __init__(self, script, language, location=None): Statement.__init__(self, location) self.script, self.language = (script, language) @@ -627,7 +725,7 @@ class FontRevisionStatement(Statement): - def __init__(self, location, revision): + def __init__(self, revision, location=None): Statement.__init__(self, location) self.revision = revision @@ -639,7 +737,7 @@ class LigatureCaretByIndexStatement(Statement): - def __init__(self, location, glyphs, carets): + def __init__(self, glyphs, carets, location=None): Statement.__init__(self, location) self.glyphs, self.carets = (glyphs, carets) @@ -653,7 +751,7 @@ class LigatureCaretByPosStatement(Statement): - def __init__(self, location, glyphs, carets): + def __init__(self, glyphs, carets, location=None): Statement.__init__(self, location) self.glyphs, self.carets = (glyphs, carets) @@ -667,8 +765,8 @@ class LigatureSubstStatement(Statement): - def __init__(self, location, prefix, glyphs, suffix, replacement, - forceChain): + def __init__(self, prefix, glyphs, suffix, replacement, + forceChain, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) self.replacement, self.forceChain = replacement, forceChain @@ -698,7 +796,8 @@ class LookupFlagStatement(Statement): - def __init__(self, location, value, markAttachment, markFilteringSet): + def __init__(self, value=0, markAttachment=None, markFilteringSet=None, + location=None): Statement.__init__(self, location) self.value = value self.markAttachment = markAttachment @@ -731,7 +830,7 @@ class LookupReferenceStatement(Statement): - def __init__(self, location, lookup): + def __init__(self, lookup, location=None): Statement.__init__(self, location) self.location, self.lookup = (location, lookup) @@ -743,7 +842,7 @@ class MarkBasePosStatement(Statement): - def __init__(self, location, base, marks): + def __init__(self, base, marks, location=None): Statement.__init__(self, location) self.base, self.marks = base, marks @@ -759,7 +858,7 @@ class MarkLigPosStatement(Statement): - def __init__(self, location, ligatures, marks): + def __init__(self, ligatures, marks, location=None): Statement.__init__(self, location) self.ligatures, self.marks = ligatures, marks @@ -783,7 +882,7 @@ class MarkMarkPosStatement(Statement): - def __init__(self, location, baseMarks, marks): + def __init__(self, baseMarks, marks, location=None): Statement.__init__(self, location) self.baseMarks, self.marks = baseMarks, marks @@ -799,7 +898,7 @@ class MultipleSubstStatement(Statement): - def __init__(self, location, prefix, glyph, suffix, replacement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): Statement.__init__(self, location) self.prefix, self.glyph, self.suffix = prefix, glyph, suffix self.replacement = replacement @@ -827,8 +926,8 @@ class PairPosStatement(Statement): - def __init__(self, location, enumerated, - glyphs1, valuerecord1, glyphs2, valuerecord2): + def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2, + enumerated=False, location=None): Statement.__init__(self, location) self.enumerated = enumerated self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 @@ -868,7 +967,8 @@ class ReverseChainSingleSubstStatement(Statement): - def __init__(self, location, old_prefix, old_suffix, glyphs, replacements): + def __init__(self, old_prefix, old_suffix, glyphs, replacements, + location=None): Statement.__init__(self, location) self.old_prefix, self.old_suffix = old_prefix, old_suffix self.glyphs = glyphs @@ -899,7 +999,8 @@ class SingleSubstStatement(Statement): - def __init__(self, location, glyphs, replace, prefix, suffix, forceChain): + def __init__(self, glyphs, replace, prefix, suffix, forceChain, + location=None): Statement.__init__(self, location) self.prefix, self.suffix = prefix, suffix self.forceChain = forceChain @@ -932,7 +1033,7 @@ class ScriptStatement(Statement): - def __init__(self, location, script): + def __init__(self, script, location=None): Statement.__init__(self, location) self.script = script @@ -944,7 +1045,7 @@ class SinglePosStatement(Statement): - def __init__(self, location, pos, prefix, suffix, forceChain): + def __init__(self, pos, prefix, suffix, forceChain, location=None): Statement.__init__(self, location) self.pos, self.prefix, self.suffix = pos, prefix, suffix self.forceChain = forceChain @@ -973,14 +1074,22 @@ class SubtableStatement(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) + def build(self, builder): + builder.add_subtable_break(self.location) + + def asFea(self, indent=""): + return indent + "subtable;" + class ValueRecord(Expression): - def __init__(self, location, vertical, - xPlacement, yPlacement, xAdvance, yAdvance, - xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice): + def __init__(self, xPlacement=None, yPlacement=None, + xAdvance=None, yAdvance=None, + xPlaDevice=None, yPlaDevice=None, + xAdvDevice=None, yAdvDevice=None, + vertical=False, location=None): Expression.__init__(self, location) self.xPlacement, self.yPlacement = (xPlacement, yPlacement) self.xAdvance, self.yAdvance = (xAdvance, yAdvance) @@ -1033,7 +1142,7 @@ class ValueRecordDefinition(Statement): - def __init__(self, location, name, value): + def __init__(self, name, value, location=None): Statement.__init__(self, location) self.name = name self.value = value @@ -1052,8 +1161,8 @@ class NameRecord(Statement): - def __init__(self, location, nameID, platformID, - platEncID, langID, string): + def __init__(self, nameID, platformID, platEncID, langID, string, + location=None): Statement.__init__(self, location) self.nameID = nameID self.platformID = platformID @@ -1093,7 +1202,7 @@ class FeatureNameStatement(NameRecord): def build(self, builder): NameRecord.build(self, builder) - builder.add_featureName(self.location, self.nameID) + builder.add_featureName(self.nameID) def asFea(self, indent=""): if self.nameID == "size": @@ -1107,8 +1216,8 @@ class SizeParameters(Statement): - def __init__(self, location, DesignSize, SubfamilyID, RangeStart, - RangeEnd): + def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, + location=None): Statement.__init__(self, location) self.DesignSize = DesignSize self.SubfamilyID = SubfamilyID @@ -1126,8 +1235,50 @@ return res + ";" +class CVParametersNameStatement(NameRecord): + def __init__(self, nameID, platformID, platEncID, langID, string, + block_name, location=None): + NameRecord.__init__(self, nameID, platformID, platEncID, langID, + string, location=location) + self.block_name = block_name + + def build(self, builder): + item = "" + if self.block_name == "ParamUILabelNameID": + item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) + builder.add_cv_parameter(self.nameID) + self.nameID = (self.nameID, self.block_name + item) + NameRecord.build(self, builder) + + def asFea(self, indent=""): + plat = simplify_name_attributes(self.platformID, self.platEncID, + self.langID) + if plat != "": + plat += " " + return "name {}\"{}\";".format(plat, self.string) + + +class CharacterStatement(Statement): + """ + Statement used in cvParameters blocks of Character Variant features (cvXX). + The Unicode value may be written with either decimal or hexadecimal + notation. The value must be preceded by '0x' if it is a hexadecimal value. + The largest Unicode value allowed is 0xFFFFFF. + """ + def __init__(self, character, tag, location=None): + Statement.__init__(self, location) + self.character = character + self.tag = tag + + def build(self, builder): + builder.add_cv_character(self.character, self.tag) + + def asFea(self, indent=""): + return "Character {:#x};".format(self.character) + + class BaseAxis(Statement): - def __init__(self, location, bases, scripts, vertical): + def __init__(self, bases, scripts, vertical, location=None): Statement.__init__(self, location) self.bases = bases self.scripts = scripts @@ -1144,7 +1295,7 @@ class OS2Field(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value @@ -1169,7 +1320,7 @@ class HheaField(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value @@ -1184,7 +1335,7 @@ class VheaField(Statement): - def __init__(self, location, key, value): + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key self.value = value diff -Nru fonttools-3.21.2/Snippets/fontTools/feaLib/builder.py fonttools-3.29.0/Snippets/fontTools/feaLib/builder.py --- fonttools-3.21.2/Snippets/fontTools/feaLib/builder.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/feaLib/builder.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,51 +5,80 @@ from fontTools.misc.textTools import binary2num, safeEval from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser +from fontTools.feaLib.ast import FeatureFile from fontTools.otlLib import builder as otl from fontTools.ttLib import newTable, getTableModule from fontTools.ttLib.tables import otBase, otTables +from collections import defaultdict import itertools +import logging -def addOpenTypeFeatures(font, featurefile): +log = logging.getLogger(__name__) + + +def addOpenTypeFeatures(font, featurefile, tables=None): builder = Builder(font, featurefile) - builder.build() + builder.build(tables=tables) -def addOpenTypeFeaturesFromString(font, features, filename=None): +def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): featurefile = UnicodeIO(tounicode(features)) if filename: # the directory containing 'filename' is used as the root of relative # include paths; if None is provided, the current directory is assumed featurefile.name = filename - addOpenTypeFeatures(font, featurefile) + addOpenTypeFeatures(font, featurefile, tables=tables) class Builder(object): + + supportedTables = frozenset(Tag(tag) for tag in [ + "BASE", + "GDEF", + "GPOS", + "GSUB", + "OS/2", + "head", + "hhea", + "name", + "vhea", + ]) + def __init__(self, font, featurefile): self.font = font - self.file = featurefile + # 'featurefile' can be either a path or file object (in which case we + # parse it into an AST), or a pre-parsed AST instance + if isinstance(featurefile, FeatureFile): + self.parseTree, self.file = featurefile, None + else: + self.parseTree, self.file = None, featurefile self.glyphMap = font.getReverseGlyphMap() self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() + self.seen_non_DFLT_script_ = False self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] - self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} # for 'featureNames' - self.featureNames_ = [] + self.featureNames_ = set() self.featureNames_ids_ = {} + # for 'cvParameters' + self.cv_parameters_ = set() + self.cv_parameters_ids_ = {} + self.cv_num_named_params_ = {} + self.cv_characters_ = defaultdict(list) # for feature 'size' self.size_parameters_ = None # for table 'head' @@ -74,16 +103,32 @@ # for table 'vhea' self.vhea_ = {} - def build(self): - self.parseTree = Parser(self.file, self.glyphMap).parse() + def build(self, tables=None): + if self.parseTree is None: + self.parseTree = Parser(self.file, self.glyphMap).parse() self.parseTree.build(self) - self.build_feature_aalt_() - self.build_head() - self.build_hhea() - self.build_vhea() - self.build_name() - self.build_OS_2() + # by default, build all the supported tables + if tables is None: + tables = self.supportedTables + else: + tables = frozenset(tables) + unsupported = tables - self.supportedTables + assert not unsupported, unsupported + if "GSUB" in tables: + self.build_feature_aalt_() + if "head" in tables: + self.build_head() + if "hhea" in tables: + self.build_hhea() + if "vhea" in tables: + self.build_vhea() + if "name" in tables: + self.build_name() + if "OS/2" in tables: + self.build_OS_2() for tag in ('GPOS', 'GSUB'): + if tag not in tables: + continue table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or @@ -92,16 +137,18 @@ fontTable.table = table elif tag in self.font: del self.font[tag] - gdef = self.buildGDEF() - if gdef: - self.font["GDEF"] = gdef - elif "GDEF" in self.font: - del self.font["GDEF"] - base = self.buildBASE() - if base: - self.font["BASE"] = base - elif "BASE" in self.font: - del self.font["BASE"] + if "GDEF" in tables: + gdef = self.buildGDEF() + if gdef: + self.font["GDEF"] = gdef + elif "GDEF" in self.font: + del self.font["GDEF"] + if "BASE" in tables: + base = self.buildBASE() + if base: + self.font["BASE"] = base + elif "BASE" in self.font: + del self.font["BASE"] def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) @@ -243,10 +290,28 @@ else: params.SubfamilyNameID = 0 elif tag in self.featureNames_: - assert tag in self.featureNames_ids_ - params = otTables.FeatureParamsStylisticSet() - params.Version = 0 - params.UINameID = self.featureNames_ids_[tag] + if not self.featureNames_ids_: + # name table wasn't selected among the tables to build; skip + pass + else: + assert tag in self.featureNames_ids_ + params = otTables.FeatureParamsStylisticSet() + params.Version = 0 + params.UINameID = self.featureNames_ids_[tag] + elif tag in self.cv_parameters_: + params = otTables.FeatureParamsCharacterVariants() + params.Format = 0 + params.FeatUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUILabelNameID'), 0) + params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUITooltipTextNameID'), 0) + params.SampleTextNameID = self.cv_parameters_ids_.get( + (tag, 'SampleTextNameID'), 0) + params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0) + params.FirstParamUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'ParamUILabelNameID_0'), 0) + params.CharCount = len(self.cv_characters_[tag]) + params.Character = self.cv_characters_[tag] return params def build_name(self): @@ -258,13 +323,20 @@ table.names = [] for name in self.names_: nameID, platformID, platEncID, langID, string = name + # For featureNames block, nameID is 'feature tag' + # For cvParameters blocks, nameID is ('feature tag', 'block name') if not isinstance(nameID, int): - # A featureNames name and nameID is actually the tag tag = nameID - if tag not in self.featureNames_ids_: - self.featureNames_ids_[tag] = self.get_user_name_id(table) - assert self.featureNames_ids_[tag] is not None - nameID = self.featureNames_ids_[tag] + if tag in self.featureNames_: + if tag not in self.featureNames_ids_: + self.featureNames_ids_[tag] = self.get_user_name_id(table) + assert self.featureNames_ids_[tag] is not None + nameID = self.featureNames_ids_[tag] + elif tag[0] in self.cv_parameters_: + if tag not in self.cv_parameters_ids_: + self.cv_parameters_ids_[tag] = self.get_user_name_id(table) + assert self.cv_parameters_ids_[tag] is not None + nameID = self.cv_parameters_ids_[tag] table.setName(string, nameID, platformID, platEncID, langID) def build_OS_2(self): @@ -496,7 +568,7 @@ frec.Feature = otTables.Feature() frec.Feature.FeatureParams = self.buildFeatureParams( feature_tag) - frec.Feature.LookupListIndex = lookup_indices + frec.Feature.LookupListIndex = list(lookup_indices) frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index @@ -549,6 +621,15 @@ raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' 'the first of the languagesystem statements', location) + if script == "DFLT": + if self.seen_non_DFLT_script_: + raise FeatureLibError( + 'languagesystems using the "DFLT" script tag must ' + "precede all other languagesystems", + location + ) + else: + self.seen_non_DFLT_script_ = True if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % @@ -621,9 +702,6 @@ raise FeatureLibError( "Language statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) - if language != 'dflt' and self.script_ == 'DFLT': - raise FeatureLibError("Need non-DFLT script when using non-dflt " - "language (was: \"%s\")" % language, location) self.cur_lookup_ = None key = (self.script_, language, self.cur_feature_name_) @@ -640,12 +718,7 @@ if key[:2] in self.get_default_language_systems_(): lookups = [l for l in lookups if l not in dflt_lookups] self.features_.setdefault(key, []).extend(lookups) - if self.script_ == 'DFLT': - langsys = set(self.get_default_language_systems_()) - else: - langsys = set() - langsys.add((self.script_, language)) - self.language_systems = frozenset(langsys) + self.language_systems = frozenset([(self.script_, language)]) if required: key = (self.script_, language) @@ -764,8 +837,22 @@ location) self.aalt_features_.append((location, featureName)) - def add_featureName(self, location, tag): - self.featureNames_.append(tag) + def add_featureName(self, tag): + self.featureNames_.add(tag) + + def add_cv_parameter(self, tag): + self.cv_parameters_.add(tag) + + def add_to_cv_num_named_params(self, tag): + """Adds new items to self.cv_num_named_params_ + or increments the count of existing items.""" + if tag in self.cv_num_named_params_: + self.cv_num_named_params_[tag] += 1 + else: + self.cv_num_named_params_[tag] = 1 + + def add_cv_character(self, character, tag): + self.cv_characters_[tag].append(character) def set_base_axis(self, bases, scripts, vertical): if vertical: @@ -916,6 +1003,16 @@ lookup = self.get_lookup_(location, PairPosBuilder) lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2) + def add_subtable_break(self, location): + if type(self.cur_lookup_) is not PairPosBuilder: + raise FeatureLibError( + 'explicit "subtable" statement is intended for use with only ' + "Pair Adjustment Positioning Format 2 (i.e. pair class kerning)", + location + ) + lookup = self.get_lookup_(location, PairPosBuilder) + lookup.add_subtable_break(location) + def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addGlyphPair(location, glyph1, value1, glyph2, value2) @@ -1406,6 +1503,7 @@ st = otl.buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap) self.subtables_.append(st) + self.forceSubtableBreak_ = False class PairPosBuilder(LookupBuilder): @@ -1424,15 +1522,19 @@ key = (glyph1, glyph2) oldValue = self.glyphPairs.get(key, None) if oldValue is not None: + # the Feature File spec explicitly allows specific pairs generated + # by an 'enum' rule to be overridden by preceding single pairs; + # we emit a warning and use the previously defined value otherLoc = self.locations[key] - raise FeatureLibError( - 'Already defined position for pair %s %s at %s:%d:%d' - % (glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]), - location) - val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True) - val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True) - self.glyphPairs[key] = (val1, val2) - self.locations[key] = location + log.warning( + 'Already defined position for pair %s %s at %s:%d:%d; ' + 'choosing the first value', + glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]) + else: + val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True) + val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True) + self.glyphPairs[key] = (val1, val2) + self.locations[key] = location def add_subtable_break(self, location): self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, diff -Nru fonttools-3.21.2/Snippets/fontTools/feaLib/error.py fonttools-3.29.0/Snippets/fontTools/feaLib/error.py --- fonttools-3.21.2/Snippets/fontTools/feaLib/error.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/feaLib/error.py 2018-07-26 14:12:55.000000000 +0000 @@ -14,3 +14,7 @@ return "%s:%d:%d: %s" % (path, line, column, message) else: return message + + +class IncludedFeaNotFound(FeatureLibError): + pass diff -Nru fonttools-3.21.2/Snippets/fontTools/feaLib/lexer.py fonttools-3.29.0/Snippets/fontTools/feaLib/lexer.py --- fonttools-3.21.2/Snippets/fontTools/feaLib/lexer.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/feaLib/lexer.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.misc.py23 import * -from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound import re import os @@ -56,7 +56,7 @@ def location_(self): column = self.pos_ - self.line_start_ + 1 - return (self.filename_, self.line_, column) + return (self.filename_ or "", self.line_, column) def next_(self): self.scan_over_(Lexer.CHAR_WHITESPACE_) @@ -211,32 +211,51 @@ #semi_type, semi_token, semi_location = lexer.next() #if semi_type is not Lexer.SYMBOL or semi_token != ";": # raise FeatureLibError("Expected ';'", semi_location) - curpath = os.path.dirname(self.featurefilepath) - path = os.path.join(curpath, fname_token) + if os.path.isabs(fname_token): + path = fname_token + else: + if self.featurefilepath is not None: + curpath = os.path.dirname(self.featurefilepath) + else: + # if the IncludingLexer was initialized from an in-memory + # file-like stream, it doesn't have a 'name' pointing to + # its filesystem path, therefore we fall back to using the + # current working directory to resolve relative includes + curpath = os.getcwd() + path = os.path.join(curpath, fname_token) if len(self.lexers_) >= 5: raise FeatureLibError("Too many recursive includes", fname_location) - self.lexers_.append(self.make_lexer_(path, fname_location)) - continue + try: + self.lexers_.append(self.make_lexer_(path)) + except IOError as err: + # FileNotFoundError does not exist on Python < 3.3 + import errno + if err.errno == errno.ENOENT: + raise IncludedFeaNotFound(fname_token, fname_location) + raise # pragma: no cover else: return (token_type, token, location) raise StopIteration() @staticmethod - def make_lexer_(file_or_path, location=None): + def make_lexer_(file_or_path): if hasattr(file_or_path, "read"): fileobj, closing = file_or_path, False else: filename, closing = file_or_path, True - try: - fileobj = open(filename, "r", encoding="utf-8") - except IOError as err: - raise FeatureLibError(str(err), location) + fileobj = open(filename, "r", encoding="utf-8") data = fileobj.read() - filename = fileobj.name if hasattr(fileobj, "name") else "" + filename = getattr(fileobj, "name", None) if closing: fileobj.close() return Lexer(data, filename) def scan_anonymous_block(self, tag): return self.lexers_[-1].scan_anonymous_block(tag) + + +class NonIncludingLexer(IncludingLexer): + """Lexer that does not follow `include` statements, emits them as-is.""" + def __next__(self): # Python 3 + return next(self.lexers_[0]) diff -Nru fonttools-3.21.2/Snippets/fontTools/feaLib/parser.py fonttools-3.29.0/Snippets/fontTools/feaLib/parser.py --- fonttools-3.21.2/Snippets/fontTools/feaLib/parser.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/feaLib/parser.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.feaLib.error import FeatureLibError -from fontTools.feaLib.lexer import Lexer, IncludingLexer +from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer from fontTools.misc.encodingTools import getEncoding from fontTools.misc.py23 import * import fontTools.feaLib.ast as ast @@ -16,8 +16,11 @@ class Parser(object): extensions = {} ast = ast + SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)} + CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99+1)} - def __init__(self, featurefile, glyphNames=(), **kwargs): + def __init__(self, featurefile, glyphNames=(), followIncludes=True, + **kwargs): if "glyphMap" in kwargs: from fontTools.misc.loggingTools import deprecateArgument deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") @@ -42,15 +45,20 @@ self.next_token_type_, self.next_token_ = (None, None) self.cur_comments_ = [] self.next_token_location_ = None - self.lexer_ = IncludingLexer(featurefile) + lexerClass = IncludingLexer if followIncludes else NonIncludingLexer + self.lexer_ = lexerClass(featurefile) self.advance_lexer_(comments=True) def parse(self): statements = self.doc_.statements - while self.next_token_type_ is not None: + while self.next_token_type_ is not None or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append( + self.ast.Comment(self.cur_token_, + location=self.cur_token_location_)) + elif self.is_cur_keyword_("include"): + statements.append(self.parse_include_()) elif self.cur_token_type_ is Lexer.GLYPHCLASS: statements.append(self.parse_glyphclass_definition_()) elif self.is_cur_keyword_(("anon", "anonymous")): @@ -99,9 +107,11 @@ 'Unknown anchor "%s"' % name, self.cur_token_location_) self.expect_symbol_(">") - return self.ast.Anchor(location, name, anchordef.x, anchordef.y, - anchordef.contourpoint, - xDeviceTable=None, yDeviceTable=None) + return self.ast.Anchor(anchordef.x, anchordef.y, + name=name, + contourpoint=anchordef.contourpoint, + xDeviceTable=None, yDeviceTable=None, + location=location) x, y = self.expect_number_(), self.expect_number_() @@ -117,8 +127,11 @@ xDeviceTable, yDeviceTable = None, None self.expect_symbol_(">") - return self.ast.Anchor(location, None, x, y, contourpoint, - xDeviceTable, yDeviceTable) + return self.ast.Anchor(x, y, name=None, + contourpoint=contourpoint, + xDeviceTable=xDeviceTable, + yDeviceTable=yDeviceTable, + location=location) def parse_anchor_marks_(self): """Parses a sequence of [ mark @MARKCLASS]*.""" @@ -142,7 +155,9 @@ contourpoint = self.expect_number_() name = self.expect_name_() self.expect_symbol_(";") - anchordef = self.ast.AnchorDefinition(location, name, x, y, contourpoint) + anchordef = self.ast.AnchorDefinition(name, x, y, + contourpoint=contourpoint, + location=location) self.anchors_.define(name, anchordef) return anchordef @@ -155,7 +170,7 @@ end_tag = self.expect_tag_() assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()" self.expect_symbol_(';') - return self.ast.AnonymousBlock(tag, content, location) + return self.ast.AnonymousBlock(tag, content, location=location) def parse_attach_(self): assert self.is_cur_keyword_("Attach") @@ -165,7 +180,8 @@ while self.next_token_ != ";": contourPoints.add(self.expect_number_()) self.expect_symbol_(";") - return self.ast.AttachStatement(location, glyphs, contourPoints) + return self.ast.AttachStatement(glyphs, contourPoints, + location=location) def parse_enumerate_(self, vertical): assert self.cur_token_ in {"enumerate", "enum"} @@ -196,8 +212,9 @@ else: componentGlyphs = None self.expect_symbol_(";") - return self.ast.GlyphClassDefStatement(location, baseGlyphs, markGlyphs, - ligatureGlyphs, componentGlyphs) + return self.ast.GlyphClassDefStatement(baseGlyphs, markGlyphs, + ligatureGlyphs, componentGlyphs, + location=location) def parse_glyphclass_definition_(self): """Parses glyph class definitions such as '@UPPERCASE = [A-Z];'""" @@ -205,7 +222,8 @@ self.expect_symbol_("=") glyphs = self.parse_glyphclass_(accept_glyphname=False) self.expect_symbol_(";") - glyphclass = self.ast.GlyphClassDefinition(location, name, glyphs) + glyphclass = self.ast.GlyphClassDefinition(name, glyphs, + location=location) self.glyphclasses_.define(name, glyphclass) return glyphclass @@ -252,7 +270,7 @@ if (accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID)): glyph = self.expect_glyph_() - return self.ast.GlyphName(self.cur_token_location_, glyph) + return self.ast.GlyphName(glyph, location=self.cur_token_location_) if self.next_token_type_ is Lexer.GLYPHCLASS: self.advance_lexer_() gc = self.glyphclasses_.resolve(self.cur_token_) @@ -261,13 +279,15 @@ "Unknown glyph class @%s" % self.cur_token_, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - return self.ast.MarkClassName(self.cur_token_location_, gc) + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - return self.ast.GlyphClassName(self.cur_token_location_, gc) + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) self.expect_symbol_("[") location = self.cur_token_location_ - glyphs = self.ast.GlyphClass(location) + glyphs = self.ast.GlyphClass(location=location) while self.next_token_ != "]": if self.next_token_type_ is Lexer.NAME: glyph = self.expect_glyph_() @@ -306,9 +326,11 @@ "Unknown glyph class @%s" % self.cur_token_, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - gc = self.ast.MarkClassName(self.cur_token_location_, gc) + gc = self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - gc = self.ast.GlyphClassName(self.cur_token_location_, gc) + gc = self.ast.GlyphClassName( + gc, location=self.cur_token_location_) glyphs.add_class(gc) else: raise FeatureLibError( @@ -326,9 +348,11 @@ "Unknown glyph class @%s" % name, self.cur_token_location_) if isinstance(gc, self.ast.MarkClass): - return self.ast.MarkClassName(self.cur_token_location_, gc) + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) else: - return self.ast.GlyphClassName(self.cur_token_location_, gc) + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) def parse_glyph_pattern_(self, vertical): prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) @@ -408,18 +432,27 @@ raise FeatureLibError( "No lookups can be specified for \"ignore sub\"", location) - return self.ast.IgnoreSubstStatement(location, chainContext) + return self.ast.IgnoreSubstStatement(chainContext, + location=location) if self.cur_token_ in ["position", "pos"]: chainContext, hasLookups = self.parse_chain_context_() if hasLookups: raise FeatureLibError( "No lookups can be specified for \"ignore pos\"", location) - return self.ast.IgnorePosStatement(location, chainContext) + return self.ast.IgnorePosStatement(chainContext, + location=location) raise FeatureLibError( "Expected \"substitute\" or \"position\"", self.cur_token_location_) + def parse_include_(self): + assert self.cur_token_ == "include" + location = self.cur_token_location_ + filename = self.expect_filename_() + # self.expect_symbol_(";") + return ast.IncludeStatement(filename, location=location) + def parse_language_(self): assert self.is_cur_keyword_("language") location = self.cur_token_location_ @@ -431,8 +464,9 @@ self.expect_keyword_("required") required = True self.expect_symbol_(";") - return self.ast.LanguageStatement(location, language, - include_default, required) + return self.ast.LanguageStatement(language, + include_default, required, + location=location) def parse_ligatureCaretByIndex_(self): assert self.is_cur_keyword_("LigatureCaretByIndex") @@ -442,7 +476,8 @@ while self.next_token_ != ";": carets.append(self.expect_number_()) self.expect_symbol_(";") - return self.ast.LigatureCaretByIndexStatement(location, glyphs, carets) + return self.ast.LigatureCaretByIndexStatement(glyphs, carets, + location=location) def parse_ligatureCaretByPos_(self): assert self.is_cur_keyword_("LigatureCaretByPos") @@ -452,7 +487,8 @@ while self.next_token_ != ";": carets.append(self.expect_number_()) self.expect_symbol_(";") - return self.ast.LigatureCaretByPosStatement(location, glyphs, carets) + return self.ast.LigatureCaretByPosStatement(glyphs, carets, + location=location) def parse_lookup_(self, vertical): assert self.is_cur_keyword_("lookup") @@ -464,14 +500,15 @@ raise FeatureLibError("Unknown lookup \"%s\"" % name, self.cur_token_location_) self.expect_symbol_(";") - return self.ast.LookupReferenceStatement(location, lookup) + return self.ast.LookupReferenceStatement(lookup, + location=location) use_extension = False if self.next_token_ == "useExtension": self.expect_keyword_("useExtension") use_extension = True - block = self.ast.LookupBlock(location, name, use_extension) + block = self.ast.LookupBlock(name, use_extension, location=location) self.parse_block_(block, vertical) self.lookups_.define(name, block) return block @@ -484,7 +521,7 @@ if self.next_token_type_ == Lexer.NUMBER: value = self.expect_number_() self.expect_symbol_(";") - return self.ast.LookupFlagStatement(location, value, None, None) + return self.ast.LookupFlagStatement(value, location=location) # format A: "lookupflag RightToLeft MarkAttachmentType @M;" value, markAttachment, markFilteringSet = 0, None, None @@ -512,8 +549,10 @@ '"%s" is not a recognized lookupflag' % self.next_token_, self.next_token_location_) self.expect_symbol_(";") - return self.ast.LookupFlagStatement(location, value, - markAttachment, markFilteringSet) + return self.ast.LookupFlagStatement(value, + markAttachment=markAttachment, + markFilteringSet=markFilteringSet, + location=location) def parse_markClass_(self): assert self.is_cur_keyword_("markClass") @@ -527,7 +566,8 @@ markClass = self.ast.MarkClass(name) self.doc_.markClasses[name] = markClass self.glyphclasses_.define(name, markClass) - mcdef = self.ast.MarkClassDefinition(location, markClass, anchor, glyphs) + mcdef = self.ast.MarkClassDefinition(markClass, anchor, glyphs, + location=location) markClass.addDefinition(mcdef) return mcdef @@ -554,7 +594,7 @@ "If \"lookup\" is present, no values must be specified", location) return self.ast.ChainContextPosStatement( - location, prefix, glyphs, suffix, lookups) + prefix, glyphs, suffix, lookups, location=location) # Pair positioning, format A: "pos V 10 A -10;" # Pair positioning, format B: "pos V A -20;" @@ -562,14 +602,16 @@ if values[0] is None: # Format B: "pos V A -20;" values.reverse() return self.ast.PairPosStatement( - location, enumerated, - glyphs[0], values[0], glyphs[1], values[1]) + glyphs[0], values[0], glyphs[1], values[1], + enumerated=enumerated, + location=location) if enumerated: raise FeatureLibError( '"enumerate" is only allowed with pair positionings', location) - return self.ast.SinglePosStatement(location, list(zip(glyphs, values)), - prefix, suffix, forceChain=hasMarks) + return self.ast.SinglePosStatement(list(zip(glyphs, values)), + prefix, suffix, forceChain=hasMarks, + location=location) def parse_position_cursive_(self, enumerated, vertical): location = self.cur_token_location_ @@ -584,7 +626,7 @@ exitAnchor = self.parse_anchor_() self.expect_symbol_(";") return self.ast.CursivePosStatement( - location, glyphclass, entryAnchor, exitAnchor) + glyphclass, entryAnchor, exitAnchor, location=location) def parse_position_base_(self, enumerated, vertical): location = self.cur_token_location_ @@ -597,7 +639,7 @@ base = self.parse_glyphclass_(accept_glyphname=True) marks = self.parse_anchor_marks_() self.expect_symbol_(";") - return self.ast.MarkBasePosStatement(location, base, marks) + return self.ast.MarkBasePosStatement(base, marks, location=location) def parse_position_ligature_(self, enumerated, vertical): location = self.cur_token_location_ @@ -613,7 +655,7 @@ self.expect_keyword_("ligComponent") marks.append(self.parse_anchor_marks_()) self.expect_symbol_(";") - return self.ast.MarkLigPosStatement(location, ligatures, marks) + return self.ast.MarkLigPosStatement(ligatures, marks, location=location) def parse_position_mark_(self, enumerated, vertical): location = self.cur_token_location_ @@ -626,13 +668,14 @@ baseMarks = self.parse_glyphclass_(accept_glyphname=True) marks = self.parse_anchor_marks_() self.expect_symbol_(";") - return self.ast.MarkMarkPosStatement(location, baseMarks, marks) + return self.ast.MarkMarkPosStatement(baseMarks, marks, + location=location) def parse_script_(self): assert self.is_cur_keyword_("script") location, script = self.cur_token_location_, self.expect_script_tag_() self.expect_symbol_(";") - return self.ast.ScriptStatement(location, script) + return self.ast.ScriptStatement(script, location=location) def parse_substitute_(self): assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"} @@ -676,7 +719,7 @@ 'Expected a single glyphclass after "from"', location) return self.ast.AlternateSubstStatement( - location, old_prefix, old[0], old_suffix, new[0]) + old_prefix, old[0], old_suffix, new[0], location=location) num_lookups = len([l for l in lookups if l is not None]) @@ -696,9 +739,10 @@ 'but found a glyph class with %d elements' % (len(glyphs), len(replacements)), location) return self.ast.SingleSubstStatement( - location, old, new, + old, new, old_prefix, old_suffix, - forceChain=hasMarks + forceChain=hasMarks, + location=location ) # GSUB lookup type 2: Multiple substitution. @@ -708,8 +752,8 @@ len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and num_lookups == 0): return self.ast.MultipleSubstStatement( - location, old_prefix, tuple(old[0].glyphSet())[0], old_suffix, - tuple([list(n.glyphSet())[0] for n in new])) + old_prefix, tuple(old[0].glyphSet())[0], old_suffix, + tuple([list(n.glyphSet())[0] for n in new]), location=location) # GSUB lookup type 4: Ligature substitution. # Format: "substitute f f i by f_f_i;" @@ -718,8 +762,9 @@ len(new[0].glyphSet()) == 1 and num_lookups == 0): return self.ast.LigatureSubstStatement( - location, old_prefix, old, old_suffix, - list(new[0].glyphSet())[0], forceChain=hasMarks) + old_prefix, old, old_suffix, + list(new[0].glyphSet())[0], forceChain=hasMarks, + location=location) # GSUB lookup type 8: Reverse chaining substitution. if reverse: @@ -747,19 +792,25 @@ 'but found a glyph class with %d elements' % (len(glyphs), len(replacements)), location) return self.ast.ReverseChainSingleSubstStatement( - location, old_prefix, old_suffix, old, new) + old_prefix, old_suffix, old, new, location=location) + + if len(old) > 1 and len(new) > 1: + raise FeatureLibError( + 'Direct substitution of multiple glyphs by multiple glyphs ' + 'is not supported', + location) # GSUB lookup type 6: Chaining contextual substitution. assert len(new) == 0, new rule = self.ast.ChainContextSubstStatement( - location, old_prefix, old, old_suffix, lookups) + old_prefix, old, old_suffix, lookups, location=location) return rule def parse_subtable_(self): assert self.is_cur_keyword_("subtable") location = self.cur_token_location_ self.expect_symbol_(";") - return self.ast.SubtableStatement(location) + return self.ast.SubtableStatement(location=location) def parse_size_parameters_(self): assert self.is_cur_keyword_("parameters") @@ -774,20 +825,22 @@ RangeEnd = self.expect_decipoint_() self.expect_symbol_(";") - return self.ast.SizeParameters(location, DesignSize, SubfamilyID, - RangeStart, RangeEnd) + return self.ast.SizeParameters(DesignSize, SubfamilyID, + RangeStart, RangeEnd, + location=location) def parse_size_menuname_(self): assert self.is_cur_keyword_("sizemenuname") location = self.cur_token_location_ platformID, platEncID, langID, string = self.parse_name_() - return self.ast.FeatureNameStatement(location, "size", platformID, - platEncID, langID, string) + return self.ast.FeatureNameStatement("size", platformID, + platEncID, langID, string, + location=location) def parse_table_(self): assert self.is_cur_keyword_("table") location, name = self.cur_token_location_, self.expect_tag_() - table = self.ast.TableBlock(location, name) + table = self.ast.TableBlock(name, location=location) self.expect_symbol_("{") handler = { "GDEF": self.parse_table_GDEF_, @@ -816,7 +869,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("Attach"): statements.append(self.parse_attach_()) elif self.is_cur_keyword_("GlyphClassDef"): @@ -838,7 +892,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("FontRevision"): statements.append(self.parse_FontRevision_()) elif self.cur_token_ == ";": @@ -853,12 +908,14 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: key = self.cur_token_.lower() value = self.expect_number_() statements.append( - self.ast.HheaField(self.cur_token_location_, key, value)) + self.ast.HheaField(key, value, + location=self.cur_token_location_)) if self.next_token_ != ";": raise FeatureLibError("Incomplete statement", self.next_token_location_) elif self.cur_token_ == ";": @@ -874,12 +931,14 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: key = self.cur_token_.lower() value = self.expect_number_() statements.append( - self.ast.VheaField(self.cur_token_location_, key, value)) + self.ast.VheaField(key, value, + location=self.cur_token_location_)) if self.next_token_ != ";": raise FeatureLibError("Incomplete statement", self.next_token_location_) elif self.cur_token_ == ";": @@ -894,7 +953,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("nameid"): statement = self.parse_nameid_() if statement: @@ -949,8 +1009,8 @@ return None platformID, platEncID, langID, string = self.parse_name_() - return self.ast.NameRecord(location, nameID, platformID, platEncID, - langID, string) + return self.ast.NameRecord(nameID, platformID, platEncID, + langID, string, location=location) def unescape_string_(self, string, encoding): if encoding == "utf_16_be": @@ -979,21 +1039,24 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("HorizAxis.BaseTagList"): horiz_bases = self.parse_base_tag_list_() elif self.is_cur_keyword_("HorizAxis.BaseScriptList"): horiz_scripts = self.parse_base_script_list_(len(horiz_bases)) statements.append( - self.ast.BaseAxis(self.cur_token_location_, horiz_bases, - horiz_scripts, False)) + self.ast.BaseAxis(horiz_bases, + horiz_scripts, False, + location=self.cur_token_location_)) elif self.is_cur_keyword_("VertAxis.BaseTagList"): vert_bases = self.parse_base_tag_list_() elif self.is_cur_keyword_("VertAxis.BaseScriptList"): vert_scripts = self.parse_base_script_list_(len(vert_bases)) statements.append( - self.ast.BaseAxis(self.cur_token_location_, vert_bases, - vert_scripts, True)) + self.ast.BaseAxis(vert_bases, + vert_scripts, True, + location=self.cur_token_location_)) elif self.cur_token_ == ";": continue @@ -1006,7 +1069,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.NAME: key = self.cur_token_.lower() value = None @@ -1023,7 +1087,8 @@ elif self.is_cur_keyword_("Vendor"): value = self.expect_string_() statements.append( - self.ast.OS2Field(self.cur_token_location_, key, value)) + self.ast.OS2Field(key, value, + location=self.cur_token_location_)) elif self.cur_token_ == ";": continue @@ -1074,13 +1139,13 @@ if self.next_token_type_ is Lexer.NUMBER: number, location = self.expect_number_(), self.cur_token_location_ if vertical: - val = self.ast.ValueRecord(location, vertical, - None, None, None, number, - None, None, None, None) + val = self.ast.ValueRecord(yAdvance=number, + vertical=vertical, + location=location) else: - val = self.ast.ValueRecord(location, vertical, - None, None, number, None, - None, None, None, None) + val = self.ast.ValueRecord(xAdvance=number, + vertical=vertical, + location=location) return val self.expect_symbol_("<") location = self.cur_token_location_ @@ -1122,8 +1187,9 @@ self.expect_symbol_(">") return self.ast.ValueRecord( - location, vertical, xPlacement, yPlacement, xAdvance, yAdvance, - xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice) + xPlacement, yPlacement, xAdvance, yAdvance, + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice, + vertical=vertical, location=location) def parse_valuerecord_definition_(self, vertical): assert self.is_cur_keyword_("valueRecordDef") @@ -1131,7 +1197,7 @@ value = self.parse_valuerecord_(vertical) name = self.expect_name_() self.expect_symbol_(";") - vrd = self.ast.ValueRecordDefinition(location, name, value) + vrd = self.ast.ValueRecordDefinition(name, value, location=location) self.valuerecords_.define(name, vrd) return vrd @@ -1141,30 +1207,34 @@ script = self.expect_script_tag_() language = self.expect_language_tag_() self.expect_symbol_(";") - if script == "DFLT" and language != "dflt": - raise FeatureLibError( - 'For script "DFLT", the language must be "dflt"', - self.cur_token_location_) - return self.ast.LanguageSystemStatement(location, script, language) + return self.ast.LanguageSystemStatement(script, language, + location=location) def parse_feature_block_(self): assert self.cur_token_ == "feature" location = self.cur_token_location_ tag = self.expect_tag_() vertical = (tag in {"vkrn", "vpal", "vhal", "valt"}) + stylisticset = None - if tag in ["ss%02d" % i for i in range(1, 20+1)]: + cv_feature = None + size_feature = False + if tag in self.SS_FEATURE_TAGS: stylisticset = tag - - size_feature = (tag == "size") + elif tag in self.CV_FEATURE_TAGS: + cv_feature = tag + elif tag == "size": + size_feature = True use_extension = False if self.next_token_ == "useExtension": self.expect_keyword_("useExtension") use_extension = True - block = self.ast.FeatureBlock(location, tag, use_extension) - self.parse_block_(block, vertical, stylisticset, size_feature) + block = self.ast.FeatureBlock(tag, use_extension=use_extension, + location=location) + self.parse_block_(block, vertical, stylisticset, size_feature, + cv_feature) return block def parse_feature_reference_(self): @@ -1172,24 +1242,93 @@ location = self.cur_token_location_ featureName = self.expect_tag_() self.expect_symbol_(";") - return self.ast.FeatureReferenceStatement(location, featureName) + return self.ast.FeatureReferenceStatement(featureName, + location=location) def parse_featureNames_(self, tag): assert self.cur_token_ == "featureNames", self.cur_token_ - block = self.ast.FeatureNamesBlock(self.cur_token_location_) + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("name"): + location = self.cur_token_location_ + platformID, platEncID, langID, string = self.parse_name_() + block.statements.append( + self.ast.FeatureNameStatement(tag, platformID, + platEncID, langID, string, + location=location)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError('Expected "name"', + self.cur_token_location_) + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvParameters_(self, tag): + assert self.cur_token_ == "cvParameters", self.cur_token_ + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) self.expect_symbol_("{") for symtab in self.symbol_tables_: symtab.enter_scope() + + statements = block.statements while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - block.statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_({"FeatUILabelNameID", + "FeatUITooltipTextNameID", + "SampleTextNameID", + "ParamUILabelNameID"}): + statements.append(self.parse_cvNameIDs_(tag, self.cur_token_)) + elif self.is_cur_keyword_("Character"): + statements.append(self.parse_cvCharacter_(tag)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected statement: got {} {}".format( + self.cur_token_type_, self.cur_token_), + self.cur_token_location_) + + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvNameIDs_(self, tag, block_name): + assert self.cur_token_ == block_name, self.cur_token_ + block = self.ast.NestedBlock(tag, block_name, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.is_cur_keyword_("name"): location = self.cur_token_location_ platformID, platEncID, langID, string = self.parse_name_() block.statements.append( - self.ast.FeatureNameStatement(location, tag, platformID, - platEncID, langID, string)) + self.ast.CVParametersNameStatement( + tag, platformID, platEncID, langID, string, + block_name, location=location)) elif self.cur_token_ == ";": continue else: @@ -1201,6 +1340,16 @@ self.expect_symbol_(";") return block + def parse_cvCharacter_(self, tag): + assert self.cur_token_ == "Character", self.cur_token_ + location, character = self.cur_token_location_, self.expect_decimal_or_hexadecimal_() + self.expect_symbol_(";") + if not (0xFFFFFF >= character >= 0): + raise FeatureLibError("Character value must be between " + "{:#x} and {:#x}".format(0, 0xFFFFFF), + location) + return self.ast.CharacterStatement(character, tag, location=location) + def parse_FontRevision_(self): assert self.cur_token_ == "FontRevision", self.cur_token_ location, version = self.cur_token_location_, self.expect_float_() @@ -1208,10 +1357,10 @@ if version <= 0: raise FeatureLibError("Font revision numbers must be positive", location) - return self.ast.FontRevisionStatement(location, version) + return self.ast.FontRevisionStatement(version, location=location) def parse_block_(self, block, vertical, stylisticset=None, - size_feature=False): + size_feature=False, cv_feature=None): self.expect_symbol_("{") for symtab in self.symbol_tables_: symtab.enter_scope() @@ -1220,7 +1369,8 @@ while self.next_token_ != "}" or self.cur_comments_: self.advance_lexer_(comments=True) if self.cur_token_type_ is Lexer.COMMENT: - statements.append(self.ast.Comment(self.cur_token_location_, self.cur_token_)) + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) elif self.cur_token_type_ is Lexer.GLYPHCLASS: statements.append(self.parse_glyphclass_definition_()) elif self.is_cur_keyword_("anchorDef"): @@ -1253,6 +1403,8 @@ statements.append(self.parse_valuerecord_definition_(vertical)) elif stylisticset and self.is_cur_keyword_("featureNames"): statements.append(self.parse_featureNames_(stylisticset)) + elif cv_feature and self.is_cur_keyword_("cvParameters"): + statements.append(self.parse_cvParameters_(cv_feature)) elif size_feature and self.is_cur_keyword_("parameters"): statements.append(self.parse_size_parameters_()) elif size_feature and self.is_cur_keyword_("sizemenuname"): @@ -1294,9 +1446,10 @@ if has_single and has_multiple: for i, s in enumerate(statements): if isinstance(s, self.ast.SingleSubstStatement): - statements[i] = self.ast.MultipleSubstStatement(s.location, + statements[i] = self.ast.MultipleSubstStatement( s.prefix, s.glyphs[0].glyphSet()[0], s.suffix, - [r.glyphSet()[0] for r in s.replacements]) + [r.glyphSet()[0] for r in s.replacements], + location=s.location) def is_cur_keyword_(self, k): if self.cur_token_type_ is Lexer.NAME: @@ -1318,6 +1471,13 @@ return self.cur_token_ raise FeatureLibError("Expected a CID", self.cur_token_location_) + def expect_filename_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.FILENAME: + raise FeatureLibError("Expected file name", + self.cur_token_location_) + return self.cur_token_ + def expect_glyph_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.NAME: @@ -1388,6 +1548,7 @@ return self.cur_token_ raise FeatureLibError("Expected a name", self.cur_token_location_) + # TODO: Don't allow this method to accept hexadecimal values def expect_number_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.NUMBER: @@ -1401,6 +1562,7 @@ raise FeatureLibError("Expected a floating-point number", self.cur_token_location_) + # TODO: Don't allow this method to accept hexadecimal values def expect_decipoint_(self): if self.next_token_type_ == Lexer.FLOAT: return self.expect_float_() @@ -1410,6 +1572,18 @@ raise FeatureLibError("Expected an integer or floating-point number", self.cur_token_location_) + def expect_decimal_or_hexadecimal_(self): + # the lexer returns the same token type 'NUMBER' for either decimal or + # hexadecimal integers, and casts them both to a `int` type, so it's + # impossible to distinguish the two here. This method is implemented + # the same as `expect_number_`, only it gives a more informative + # error message + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NUMBER: + return self.cur_token_ + raise FeatureLibError("Expected a decimal or hexadecimal number", + self.cur_token_location_) + def expect_string_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.STRING: @@ -1424,7 +1598,6 @@ else: self.cur_token_type_, self.cur_token_, self.cur_token_location_ = ( self.next_token_type_, self.next_token_, self.next_token_location_) - self.cur_comments_ = [] while True: try: (self.next_token_type_, self.next_token_, diff -Nru fonttools-3.21.2/Snippets/fontTools/__init__.py fonttools-3.29.0/Snippets/fontTools/__init__.py --- fonttools-3.21.2/Snippets/fontTools/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,6 +5,6 @@ log = logging.getLogger(__name__) -version = __version__ = "3.21.2" +version = __version__ = "3.29.0" __all__ = ["version", "log", "configLogger"] diff -Nru fonttools-3.21.2/Snippets/fontTools/merge.py fonttools-3.29.0/Snippets/fontTools/merge.py --- fonttools-3.21.2/Snippets/fontTools/merge.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/merge.py 2018-07-26 14:12:55.000000000 +0000 @@ -12,6 +12,7 @@ from fontTools.ttLib.tables import otTables, _h_e_a_d from fontTools.ttLib.tables.DefaultTable import DefaultTable from fontTools.misc.loggingTools import Timer +from fontTools.pens.recordingPen import DecomposingRecordingPen from functools import reduce import sys import time @@ -351,6 +352,17 @@ ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable +def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2): + pen1 = DecomposingRecordingPen(glyphSet1) + pen2 = DecomposingRecordingPen(glyphSet2) + g1 = glyphSet1[glyph1] + g2 = glyphSet2[glyph2] + g1.draw(pen1) + g2.draw(pen2) + return (pen1.value == pen2.value and + g1.width == g2.width and + (not hasattr(g1, 'height') or g1.height == g2.height)) + @_add_method(ttLib.getTableClass('cmap')) def merge(self, m, tables): # TODO Handle format=14. @@ -373,19 +385,30 @@ # Build a unicode mapping, then decide which format is needed to store it. cmap = {} + fontIndexForGlyph = {} + glyphSets = [None for f in m.fonts] if hasattr(m, 'fonts') else None for table,fontIdx in cmapTables: # handle duplicates for uni,gid in table.cmap.items(): oldgid = cmap.get(uni, None) if oldgid is None: cmap[uni] = gid + fontIndexForGlyph[gid] = fontIdx elif oldgid != gid: # Char previously mapped to oldgid, now to gid. # Record, to fix up in GSUB 'locl' later. - if m.duplicateGlyphsPerFont[fontIdx].get(oldgid, gid) == gid: + if m.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None: + if glyphSets is not None: + oldFontIdx = fontIndexForGlyph[oldgid] + for idx in (fontIdx, oldFontIdx): + if glyphSets[idx] is None: + glyphSets[idx] = m.fonts[idx].getGlyphSet() + if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid): + continue m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid - else: - # Char previously mapped to oldgid but already remapped to a different gid. + elif m.duplicateGlyphsPerFont[fontIdx][oldgid] != gid: + # Char previously mapped to oldgid but oldgid is already remapped to a different + # gid, because of another Unicode character. # TODO: Try harder to do something about these. log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid) @@ -459,12 +482,22 @@ if len(lst) == 1: return lst[0] - # TODO Support merging LangSysRecords - assert all(not s.LangSysRecord for s in lst) + langSyses = {} + for sr in lst: + for lsr in sr.LangSysRecord: + if lsr.LangSysTag not in langSyses: + langSyses[lsr.LangSysTag] = [] + langSyses[lsr.LangSysTag].append(lsr.LangSys) + lsrecords = [] + for tag, langSys_list in sorted(langSyses.items()): + lsr = otTables.LangSysRecord() + lsr.LangSys = mergeLangSyses(langSys_list) + lsr.LangSysTag = tag + lsrecords.append(lsr) self = otTables.Script() - self.LangSysRecord = [] - self.LangSysCount = 0 + self.LangSysRecord = lsrecords + self.LangSysCount = len(lsrecords) self.DefaultLangSys = mergeLangSyses([s.DefaultLangSys for s in lst if s.DefaultLangSys]) return self @@ -604,6 +637,13 @@ synthLookup.LookupType = 1 synthLookup.SubTableCount = 1 synthLookup.SubTable = [subtable] + if table.table.LookupList is None: + # mtiLib uses None as default value for LookupList, + # while feaLib points to an empty array with count 0 + # TODO: make them do the same + table.table.LookupList = otTables.LookupList() + table.table.LookupList.Lookup = [] + table.table.LookupList.LookupCount = 0 table.table.LookupList.Lookup.append(synthLookup) table.table.LookupList.LookupCount += 1 @@ -824,9 +864,9 @@ return ret -class _AttendanceRecordingIdentityDict(dict): +class _AttendanceRecordingIdentityDict(object): """A dictionary-like object that records indices of items actually accessed - from a list.""" + from a list.""" def __init__(self, lst): self.l = lst @@ -837,7 +877,7 @@ self.s.add(self.d[id(v)]) return v -class _GregariousDict(dict): +class _GregariousIdentityDict(object): """A dictionary-like object that welcomes guests without reservations and adds them to the end of the guest list.""" @@ -851,14 +891,23 @@ self.l.append(v) return v -class _NonhashableDict(dict): - """A dictionary-like object mapping objects to their index within a list.""" +class _NonhashableDict(object): + """A dictionary-like object mapping objects to values.""" - def __init__(self, lst): - self.d = {id(v):i for i,v in enumerate(lst)} + def __init__(self, keys, values=None): + if values is None: + self.d = {id(v):i for i,v in enumerate(keys)} + else: + self.d = {id(k):v for k,v in zip(keys, values)} - def __getitem__(self, v): - return self.d[id(v)] + def __getitem__(self, k): + return self.d[id(k)] + + def __setitem__(self, k, v): + self.d[id(k)] = v + + def __delitem__(self, k): + del self.d[id(k)] class Merger(object): @@ -890,6 +939,7 @@ for font in fonts: self._preMerge(font) + self.fonts = fonts self.duplicateGlyphsPerFont = [{} for f in fonts] allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) @@ -919,6 +969,7 @@ log.info("Dropped '%s'.", tag) del self.duplicateGlyphsPerFont + del self.fonts self._postMerge(mega) @@ -997,7 +1048,7 @@ if t.table.FeatureList and t.table.ScriptList: # Collect unregistered (new) features. - featureMap = _GregariousDict(t.table.FeatureList.FeatureRecord) + featureMap = _GregariousIdentityDict(t.table.FeatureList.FeatureRecord) t.table.ScriptList.mapFeatures(featureMap) # Record used features. @@ -1017,7 +1068,7 @@ if t.table.LookupList: # Collect unregistered (new) lookups. - lookupMap = _GregariousDict(t.table.LookupList.Lookup) + lookupMap = _GregariousIdentityDict(t.table.LookupList.Lookup) t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/bezierTools.py fonttools-3.29.0/Snippets/fontTools/misc/bezierTools.py --- fonttools-3.21.2/Snippets/fontTools/misc/bezierTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/bezierTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -13,10 +13,12 @@ "approximateCubicArcLengthC", "approximateQuadraticArcLength", "approximateQuadraticArcLengthC", + "calcCubicArcLength", + "calcCubicArcLengthC", "calcQuadraticArcLength", "calcQuadraticArcLengthC", - "calcQuadraticBounds", "calcCubicBounds", + "calcQuadraticBounds", "splitLine", "splitQuadratic", "splitCubic", @@ -27,6 +29,32 @@ ] +def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment.""" + return calcCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance) + + +def _split_cubic_into_two(p0, p1, p2, p3): + mid = (p0 + 3 * (p1 + p2) + p3) * .125 + deriv3 = (p3 + p2 - p1 - p0) * .125 + return ((p0, (p0 + p1) * .5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * .5, p3)) + +def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): + arch = abs(p0-p3) + box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3) + if arch * mult >= box: + return (arch + box) * .5 + else: + one,two = _split_cubic_into_two(p0,p1,p2,p3) + return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(mult, *two) + +def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment using complex points.""" + mult = 1. + 1.5 * tolerance # The 1.5 is a empirical hack; no math + return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) + + epsilonDigits = 6 epsilon = 1e-10 @@ -41,7 +69,7 @@ return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2 -def calcQuadraticArcLength(pt1, pt2, pt3, approximate_fallback=False): +def calcQuadraticArcLength(pt1, pt2, pt3): """Return the arc length for a qudratic bezier segment. pt1 and pt3 are the "anchor" points, pt2 is the "handle". @@ -59,18 +87,18 @@ 120.21581243984076 >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50)) 102.53273816445825 - >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0), True) # collinear points, control point outside, exact result should be 66.6666666666667 - 69.41755572720999 - >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0), True) # collinear points, looping back, exact result should be 40 - 34.4265186329548 + >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside + 66.66666666666667 + >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back + 40.0 """ - return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), approximate_fallback) + return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) -def calcQuadraticArcLengthC(pt1, pt2, pt3, approximate_fallback=False): +def calcQuadraticArcLengthC(pt1, pt2, pt3): """Return the arc length for a qudratic bezier segment using complex points. pt1 and pt3 are the "anchor" points, pt2 is the "handle".""" - + # Analytical solution to the length of a quadratic bezier. # I'll explain how I arrived at this later. d0 = pt2 - pt1 @@ -81,12 +109,11 @@ if scale == 0.: return abs(pt3-pt1) origDist = _dot(n,d0) - if origDist == 0.: + if abs(origDist) < epsilon: if _dot(d0,d1) >= 0: return abs(pt3-pt1) - if approximate_fallback: - return approximateQuadraticArcLengthC(pt1, pt2, pt3) - assert 0 # TODO handle cusps + a, b = abs(d0), abs(d1) + return (a*a + b*b) / (a+b) x0 = _dot(d,d0) / origDist x1 = _dot(d,d1) / origDist Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0))) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/filenames.py fonttools-3.29.0/Snippets/fontTools/misc/filenames.py --- fonttools-3.21.2/Snippets/fontTools/misc/filenames.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/filenames.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,224 @@ +""" +User name to file name conversion based on the UFO 3 spec: +http://unifiedfontobject.org/versions/ufo3/conventions/ + +The code was copied from: +https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py + +Author: Tal Leming +Copyright (c) 2005-2016, The RoboFab Developers: + Erik van Blokland + Tal Leming + Just van Rossum +""" +from __future__ import unicode_literals +from fontTools.misc.py23 import basestring, unicode + + +illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") +illegalCharacters += [chr(i) for i in range(1, 32)] +illegalCharacters += [chr(0x7F)] +reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") +reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") +maxFileNameLength = 255 + + +class NameTranslationError(Exception): + pass + + +def userNameToFileName(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True + """ + # the incoming name must be a unicode string + if not isinstance(userName, unicode): + raise ValueError("The value for userName must be a unicode string.") + # establish the prefix and suffix lengths + prefixLength = len(prefix) + suffixLength = len(suffix) + # replace an initial period with an _ + # if no prefix is to be added + if not prefix and userName[0] == ".": + userName = "_" + userName[1:] + # filter the user name + filteredUserName = [] + for character in userName: + # replace illegal characters with _ + if character in illegalCharacters: + character = "_" + # add _ to all non-lower characters + elif character != character.lower(): + character += "_" + filteredUserName.append(character) + userName = "".join(filteredUserName) + # clip to 255 + sliceLength = maxFileNameLength - prefixLength - suffixLength + userName = userName[:sliceLength] + # test for illegal files names + parts = [] + for part in userName.split("."): + if part.lower() in reservedFileNames: + part = "_" + part + parts.append(part) + userName = ".".join(parts) + # test for clash + fullName = prefix + userName + suffix + if fullName.lower() in existing: + fullName = handleClash1(userName, existing, prefix, suffix) + # finished + return fullName + +def handleClash1(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + """ + # if the prefix length + user name length + suffix length + 15 is at + # or past the maximum length, silce 15 characters off of the user name + prefixLength = len(prefix) + suffixLength = len(suffix) + if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: + l = (prefixLength + len(userName) + suffixLength + 15) + sliceLength = maxFileNameLength - l + userName = userName[:sliceLength] + finalName = None + # try to add numbers to create a unique name + counter = 1 + while finalName is None: + name = userName + str(counter).zfill(15) + fullName = prefix + name + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= 999999999999999: + break + # if there is a clash, go to the next fallback + if finalName is None: + finalName = handleClash2(existing, prefix, suffix) + # finished + return finalName + +def handleClash2(existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True + """ + # calculate the longest possible string + maxLength = maxFileNameLength - len(prefix) - len(suffix) + maxValue = int("9" * maxLength) + # try to find a number + finalName = None + counter = 1 + while finalName is None: + fullName = prefix + str(counter) + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= maxValue: + break + # raise an error if nothing has been found + if finalName is None: + raise NameTranslationError("No unique name could be found.") + # finished + return finalName + +if __name__ == "__main__": + import doctest + import sys + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/fixedTools.py fonttools-3.29.0/Snippets/fontTools/misc/fixedTools.py --- fonttools-3.21.2/Snippets/fontTools/misc/fixedTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/fixedTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,11 +3,13 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +import math import logging log = logging.getLogger(__name__) __all__ = [ + "otRound", "fixedToFloat", "floatToFixed", "floatToFixedToFloat", @@ -15,6 +17,18 @@ "versionToFixed", ] + +def otRound(value): + """Round float value to nearest integer towards +Infinity. + For fractional values of 0.5 and higher, take the next higher integer; + for other fractional values, truncate. + + https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview + https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 + """ + return int(math.floor(value + 0.5)) + + def fixedToFloat(value, precisionBits): """Converts a fixed-point number to a float, choosing the float that has the shortest decimal reprentation. Eg. to convert a @@ -50,7 +64,7 @@ """Converts a float to a fixed-point number given the number of precisionBits. Ie. round(value * (1<= hdlr.level: + hdlr.handle(record) + if not c.propagate: + c = None # break out + else: + c = c.parent + if found == 0: + if logging.lastResort: + if record.levelno >= logging.lastResort.level: + logging.lastResort.handle(record) + elif ( + logging.raiseExceptions + and not self.manager.emittedNoHandlerWarning + ): + sys.stderr.write( + "No handlers could be found for logger" + ' "%s"\n' % self.name + ) + self.manager.emittedNoHandlerWarning = True + + +class StderrHandler(logging.StreamHandler): + """ This class is like a StreamHandler using sys.stderr, but always uses + whateve sys.stderr is currently set to rather than the value of + sys.stderr at handler construction time. + """ + + def __init__(self, level=logging.NOTSET): + """ + Initialize the handler. + """ + logging.Handler.__init__(self, level) + + @property + def stream(self): + # the try/execept avoids failures during interpreter shutdown, when + # globals are set to None + try: + return sys.stderr + except AttributeError: + return __import__("sys").stderr + + if __name__ == "__main__": import doctest sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/psCharStrings.py fonttools-3.29.0/Snippets/fontTools/misc/psCharStrings.py --- fonttools-3.21.2/Snippets/fontTools/misc/psCharStrings.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/psCharStrings.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import fixedToFloat +from fontTools.misc.fixedTools import fixedToFloat, otRound from fontTools.pens.boundsPen import BoundsPen import struct import logging @@ -216,8 +216,12 @@ encodeIntT2 = getIntEncoder("t2") def encodeFixed(f, pack=struct.pack): - # For T2 only - return b"\xff" + pack(">l", round(f * 65536)) + """For T2 only""" + value = otRound(f * 65536) # convert the float to fixed point + if value & 0xFFFF == 0: # check if the fractional part is zero + return encodeIntT2(value >> 16) # encode only the integer part + else: + return b"\xff" + pack(">l", value) # encode the entire fixed point value def encodeFloat(f): # For CFF only, used in cffLib @@ -944,7 +948,7 @@ decompilerClass = SimpleT2Decompiler outlineExtractor = T2OutlineExtractor isCFF2 = False - + def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): if program is None: program = [] @@ -979,8 +983,8 @@ extractor.execute(self) self.width = extractor.width - def calcBounds(self): - boundsPen = BoundsPen(None) + def calcBounds(self, glyphSet): + boundsPen = BoundsPen(glyphSet) self.draw(boundsPen) return boundsPen.bounds @@ -1006,8 +1010,7 @@ while i < end: token = program[i] i = i + 1 - tp = type(token) - if issubclass(tp, basestring): + if isinstance(token, basestring): try: bytecode.extend(bytechr(b) for b in opcodes[token]) except KeyError: @@ -1015,12 +1018,12 @@ if token in ('hintmask', 'cntrmask'): bytecode.append(program[i]) # hint mask i = i + 1 - elif tp == int: + elif isinstance(token, int): bytecode.append(encodeInt(token)) - elif tp == float: + elif isinstance(token, float): bytecode.append(encodeFixed(token)) else: - assert 0, "unsupported type: %s" % tp + assert 0, "unsupported type: %s" % type(token) try: bytecode = bytesjoin(bytecode) except TypeError: @@ -1259,12 +1262,12 @@ """ There may be non-blend args at the top of the stack. We first calculate where the blend args start in the stack. These are the last - numMasters*numBlends) +1 args. + numMasters*numBlends) +1 args. The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by numBlends list of values. Each of value in one of these lists is the Variable Font delta for the matching region. - - We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by + + We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by the delta values. We then convert the default values, the first item in each entry, to an absolute value. """ vsindex = self.dict.get('vsindex', 0) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/psLib.py fonttools-3.29.0/Snippets/fontTools/misc/psLib.py --- fonttools-3.21.2/Snippets/fontTools/misc/psLib.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/psLib.py 2018-07-26 14:12:55.000000000 +0000 @@ -43,13 +43,14 @@ class PSTokenizer(object): - def __init__(self, buf=b''): + def __init__(self, buf=b'', encoding="ascii"): # Force self.buf to be a byte string buf = tobytes(buf) self.buf = buf self.len = len(buf) self.pos = 0 self.closed = False + self.encoding = encoding def read(self, n=-1): """Read at most 'n' bytes from the buffer, or less if the read @@ -122,7 +123,7 @@ _, nextpos = m.span() token = buf[pos:nextpos] self.pos = pos + len(token) - token = tostr(token, encoding='ascii') + token = tostr(token, encoding=self.encoding) return tokentype, token def skipwhite(self, whitematch=skipwhiteRE.match): @@ -145,9 +146,10 @@ class PSInterpreter(PSOperators): - def __init__(self): + def __init__(self, encoding="ascii"): systemdict = {} userdict = {} + self.encoding = encoding self.dictstack = [systemdict, userdict] self.stack = [] self.proclevel = 0 @@ -174,7 +176,7 @@ self.suckoperators(systemdict, baseclass) def interpret(self, data, getattr=getattr): - tokenizer = self.tokenizer = PSTokenizer(data) + tokenizer = self.tokenizer = PSTokenizer(data, self.encoding) getnexttoken = tokenizer.getnexttoken do_token = self.do_token handle_object = self.handle_object @@ -345,13 +347,13 @@ newitem = item.value return newitem -def suckfont(data): +def suckfont(data, encoding="ascii"): m = re.search(br"/FontName\s+/([^ \t\n\r]+)\s+def", data) if m: fontName = m.group(1) else: fontName = None - interpreter = PSInterpreter() + interpreter = PSInterpreter(encoding=encoding) interpreter.interpret(b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop") interpreter.interpret(data) fontdir = interpreter.dictstack[0]['FontDirectory'].value diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/py23.py fonttools-3.29.0/Snippets/fontTools/misc/py23.py --- fonttools-3.21.2/Snippets/fontTools/misc/py23.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/py23.py 2018-07-26 14:12:55.000000000 +0000 @@ -6,7 +6,8 @@ __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', - 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error'] + 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error', + 'SimpleNamespace', 'zip'] class Py23Error(NotImplementedError): @@ -147,7 +148,7 @@ @staticmethod def transcode(blob): - if not isinstance(blob, str): + if isinstance(blob, bytes): blob = blob.decode('latin-1') return blob @@ -249,7 +250,7 @@ file, mode, buffering, encoding, errors, newline, closefd) -# always use iterator for 'range' on both py 2 and 3 +# always use iterator for 'range' and 'zip' on both py 2 and 3 try: range = xrange except NameError: @@ -258,6 +259,11 @@ def xrange(*args, **kwargs): raise Py23Error("'xrange' is not defined. Use 'range' instead.") +try: + from itertools import izip as zip +except ImportError: + zip = zip + import math as _math @@ -413,70 +419,6 @@ round = round3 -import logging - - -class _Logger(logging.Logger): - """ Add support for 'lastResort' handler introduced in Python 3.2. """ - - def callHandlers(self, record): - # this is the same as Python 3.5's logging.Logger.callHandlers - c = self - found = 0 - while c: - for hdlr in c.handlers: - found = found + 1 - if record.levelno >= hdlr.level: - hdlr.handle(record) - if not c.propagate: - c = None # break out - else: - c = c.parent - if (found == 0): - if logging.lastResort: - if record.levelno >= logging.lastResort.level: - logging.lastResort.handle(record) - elif logging.raiseExceptions and not self.manager.emittedNoHandlerWarning: - sys.stderr.write("No handlers could be found for logger" - " \"%s\"\n" % self.name) - self.manager.emittedNoHandlerWarning = True - - -class _StderrHandler(logging.StreamHandler): - """ This class is like a StreamHandler using sys.stderr, but always uses - whatever sys.stderr is currently set to rather than the value of - sys.stderr at handler construction time. - """ - def __init__(self, level=logging.NOTSET): - """ - Initialize the handler. - """ - logging.Handler.__init__(self, level) - - @property - def stream(self): - # the try/execept avoids failures during interpreter shutdown, when - # globals are set to None - try: - return sys.stderr - except AttributeError: - return __import__('sys').stderr - - -if not hasattr(logging, 'lastResort'): - # for Python pre-3.2, we need to define the "last resort" handler used when - # clients don't explicitly configure logging (in Python 3.2 and above this is - # already defined). The handler prints the bare message to sys.stderr, only - # for events of severity WARNING or greater. - # To obtain the pre-3.2 behaviour, you can set logging.lastResort to None. - # https://docs.python.org/3.5/howto/logging.html#what-happens-if-no-configuration-is-provided - logging.lastResort = _StderrHandler(logging.WARNING) - # Also, we need to set the Logger class to one which supports the last resort - # handler. All new loggers instantiated after this call will use the custom - # logger class (the already existing ones, like the 'root' logger, will not) - logging.setLoggerClass(_Logger) - - try: from types import SimpleNamespace except ImportError: diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/testTools.py fonttools-3.29.0/Snippets/fontTools/misc/testTools.py --- fonttools-3.21.2/Snippets/fontTools/misc/testTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/testTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,8 +1,13 @@ """Helpers for writing unit tests.""" -from __future__ import print_function, division, absolute_import -from __future__ import unicode_literals +from __future__ import (print_function, division, absolute_import, + unicode_literals) import collections +import os +import shutil +import sys +import tempfile +from unittest import TestCase as _TestCase from fontTools.misc.py23 import * from fontTools.misc.xmlWriter import XMLWriter @@ -37,7 +42,7 @@ class FakeFont: def __init__(self, glyphs): self.glyphOrder_ = glyphs - self.reverseGlyphOrderDict_ = {g:i for i,g in enumerate(glyphs)} + self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)} self.lazy = False self.tables = {} @@ -114,29 +119,65 @@ class MockFont(object): - """A font-like object that automatically adds any looked up glyphname - to its glyphOrder.""" + """A font-like object that automatically adds any looked up glyphname + to its glyphOrder.""" - def __init__(self): - self._glyphOrder = ['.notdef'] - class AllocatingDict(dict): - def __missing__(reverseDict, key): - self._glyphOrder.append(key) - gid = len(reverseDict) - reverseDict[key] = gid - return gid - self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) - self.lazy = False - - def getGlyphID(self, glyph, requireReal=None): - gid = self._reverseGlyphOrder[glyph] - return gid + def __init__(self): + self._glyphOrder = ['.notdef'] + + class AllocatingDict(dict): + def __missing__(reverseDict, key): + self._glyphOrder.append(key) + gid = len(reverseDict) + reverseDict[key] = gid + return gid + self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) + self.lazy = False + + def getGlyphID(self, glyph, requireReal=None): + gid = self._reverseGlyphOrder[glyph] + return gid + + def getReverseGlyphMap(self): + return self._reverseGlyphOrder + + def getGlyphName(self, gid): + return self._glyphOrder[gid] + + def getGlyphOrder(self): + return self._glyphOrder + + +class TestCase(_TestCase): + + def __init__(self, methodName): + _TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + +class DataFilesHandler(TestCase): + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) - def getReverseGlyphMap(self): - return self._reverseGlyphOrder + def getpath(self, testfile): + folder = os.path.dirname(sys.modules[self.__module__].__file__) + return os.path.join(folder, "data", testfile) - def getGlyphName(self, gid): - return self._glyphOrder[gid] + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() - def getGlyphOrder(self): - return self._glyphOrder + def temp_font(self, font_path, file_name): + self.temp_dir() + temppath = os.path.join(self.tempdir, file_name) + shutil.copy2(font_path, temppath) + return temppath diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/textTools.py fonttools-3.29.0/Snippets/fontTools/misc/textTools.py --- fonttools-3.21.2/Snippets/fontTools/misc/textTools.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/textTools.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,18 +3,19 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +import ast import string -def safeEval(data, eval=eval): - """A (kindof) safe replacement for eval.""" - return eval(data, {"__builtins__":{"True":True,"False":False}}) +# alias kept for backward compatibility +safeEval = ast.literal_eval def readHex(content): """Convert a list of hex strings to binary data.""" return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, basestring))) + def deHexStr(hexdata): """Convert a hex string to binary data.""" hexdata = strjoin(hexdata.split()) diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/xmlReader.py fonttools-3.29.0/Snippets/fontTools/misc/xmlReader.py --- fonttools-3.21.2/Snippets/fontTools/misc/xmlReader.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/xmlReader.py 2018-07-26 14:12:55.000000000 +0000 @@ -17,7 +17,7 @@ class XMLReader(object): - def __init__(self, fileOrPath, ttFont, progress=None, quiet=None): + def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): if fileOrPath == '-': fileOrPath = sys.stdin if not hasattr(fileOrPath, "read"): @@ -35,6 +35,7 @@ self.quiet = quiet self.root = None self.contentStack = [] + self.contentOnly = contentOnly self.stackSize = 0 def read(self, rootless=False): @@ -73,8 +74,24 @@ parser.Parse(chunk, 0) def _startElementHandler(self, name, attrs): + if self.stackSize == 1 and self.contentOnly: + # We already know the table we're parsing, skip + # parsing the table tag and continue to + # stack '2' which begins parsing content + self.contentStack.append([]) + self.stackSize = 2 + return stackSize = self.stackSize self.stackSize = stackSize + 1 + subFile = attrs.get("src") + if subFile is not None: + if hasattr(self.file, 'name'): + # if file has a name, get its parent directory + dirname = os.path.dirname(self.file.name) + else: + # else fall back to using the current working directory + dirname = os.getcwd() + subFile = os.path.join(dirname, subFile) if not stackSize: if name != "ttFont": raise TTXParseError("illegal root tag: %s" % name) @@ -85,15 +102,7 @@ self.ttFont.sfntVersion = sfntVersion self.contentStack.append([]) elif stackSize == 1: - subFile = attrs.get("src") if subFile is not None: - if hasattr(self.file, 'name'): - # if file has a name, get its parent directory - dirname = os.path.dirname(self.file.name) - else: - # else fall back to using the current working directory - dirname = os.getcwd() - subFile = os.path.join(dirname, subFile) subReader = XMLReader(subFile, self.ttFont, self.progress) subReader.read() self.contentStack.append([]) @@ -119,6 +128,11 @@ self.currentTable = tableClass(tag) self.ttFont[tag] = self.currentTable self.contentStack.append([]) + elif stackSize == 2 and subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) + subReader.read() + self.contentStack.append([]) + self.root = subReader.root elif stackSize == 2: self.contentStack.append([]) self.root = (name, attrs, self.contentStack[-1]) @@ -134,12 +148,13 @@ def _endElementHandler(self, name): self.stackSize = self.stackSize - 1 del self.contentStack[-1] - if self.stackSize == 1: - self.root = None - elif self.stackSize == 2: - name, attrs, content = self.root - self.currentTable.fromXML(name, attrs, content, self.ttFont) - self.root = None + if not self.contentOnly: + if self.stackSize == 1: + self.root = None + elif self.stackSize == 2: + name, attrs, content = self.root + self.currentTable.fromXML(name, attrs, content, self.ttFont) + self.root = None class ProgressPrinter(object): diff -Nru fonttools-3.21.2/Snippets/fontTools/misc/xmlWriter.py fonttools-3.29.0/Snippets/fontTools/misc/xmlWriter.py --- fonttools-3.21.2/Snippets/fontTools/misc/xmlWriter.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/misc/xmlWriter.py 2018-07-26 14:12:55.000000000 +0000 @@ -18,10 +18,14 @@ if fileOrPath == '-': fileOrPath = sys.stdout if not hasattr(fileOrPath, "write"): + self.filename = fileOrPath self.file = open(fileOrPath, "wb") + self._closeStream = True else: + self.filename = None # assume writable file object self.file = fileOrPath + self._closeStream = False # Figure out if writer expects bytes or unicodes try: @@ -46,8 +50,15 @@ self._writeraw('') self.newline() + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.close() + def close(self): - self.file.close() + if self._closeStream: + self.file.close() def write(self, string, indent=True): """Writes text.""" diff -Nru fonttools-3.21.2/Snippets/fontTools/pens/perimeterPen.py fonttools-3.29.0/Snippets/fontTools/pens/perimeterPen.py --- fonttools-3.21.2/Snippets/fontTools/pens/perimeterPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/pens/perimeterPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.pens.basePen import BasePen -from fontTools.misc.bezierTools import splitQuadraticAtT, splitCubicAtT, approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC +from fontTools.misc.bezierTools import approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC, calcCubicArcLengthC import math @@ -14,18 +14,12 @@ def _distance(p0, p1): return math.hypot(p0[0] - p1[0], p0[1] - p1[1]) -def _split_cubic_into_two(p0, p1, p2, p3): - mid = (p0 + 3 * (p1 + p2) + p3) * .125 - deriv3 = (p3 + p2 - p1 - p0) * .125 - return ((p0, (p0 + p1) * .5, mid - deriv3, mid), - (mid, mid + deriv3, (p2 + p3) * .5, p3)) - class PerimeterPen(BasePen): def __init__(self, glyphset=None, tolerance=0.005): BasePen.__init__(self, glyphset) self.value = 0 - self._mult = 1.+1.5*tolerance # The 1.5 is a empirical hack; no math + self.tolerance = tolerance # Choose which algorithm to use for quadratic and for cubic. # Quadrature is faster but has fixed error characteristic with no strong @@ -55,15 +49,8 @@ p0 = self._getCurrentPoint() self._addQuadratic(complex(*p0), complex(*p1), complex(*p2)) - def _addCubicRecursive(self, p0, p1, p2, p3): - arch = abs(p0-p3) - box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3) - if arch * self._mult >= box: - self.value += (arch + box) * .5 - else: - one,two = _split_cubic_into_two(p0,p1,p2,p3) - self._addCubicRecursive(*one) - self._addCubicRecursive(*two) + def _addCubicRecursive(self, c0, c1, c2, c3): + self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance) def _addCubicQuadrature(self, c0, c1, c2, c3): self.value += approximateCubicArcLengthC(c0, c1, c2, c3) diff -Nru fonttools-3.21.2/Snippets/fontTools/pens/t2CharStringPen.py fonttools-3.29.0/Snippets/fontTools/pens/t2CharStringPen.py --- fonttools-3.21.2/Snippets/fontTools/pens/t2CharStringPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/pens/t2CharStringPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,6 +3,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc.psCharStrings import T2CharString from fontTools.pens.basePen import BasePen from fontTools.cffLib.specializer import specializeCommands, commandsToProgram @@ -15,7 +16,7 @@ def _round(number): if tolerance == 0: return number # no-op - rounded = round(number) + rounded = otRound(number) # return rounded integer if the tolerance >= 0.5, or if the absolute # difference between the original float and the rounded integer is # within the tolerance @@ -82,7 +83,7 @@ program = commandsToProgram(commands) if self._width is not None: assert not self._CFF2, "CFF2 does not allow encoding glyph width in CharString." - program.insert(0, round(self._width)) + program.insert(0, otRound(self._width)) if not self._CFF2: program.append('endchar') charString = T2CharString( diff -Nru fonttools-3.21.2/Snippets/fontTools/pens/ttGlyphPen.py fonttools-3.29.0/Snippets/fontTools/pens/ttGlyphPen.py --- fonttools-3.21.2/Snippets/fontTools/pens/ttGlyphPen.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/pens/ttGlyphPen.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from array import array -from fontTools.pens.basePen import AbstractPen +from fontTools.pens.basePen import LoggingPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib.tables import ttProgram from fontTools.ttLib.tables._g_l_y_f import Glyph @@ -12,11 +12,32 @@ __all__ = ["TTGlyphPen"] -class TTGlyphPen(AbstractPen): - """Pen used for drawing to a TrueType glyph.""" +# the max value that can still fit in an F2Dot14: +# 1.99993896484375 +MAX_F2DOT14 = 0x7FFF / (1 << 14) + + +class TTGlyphPen(LoggingPen): + """Pen used for drawing to a TrueType glyph. + + If `handleOverflowingTransforms` is True, the components' transform values + are checked that they don't overflow the limits of a F2Dot14 number: + -2.0 <= v < +2.0. If any transform value exceeds these, the composite + glyph is decomposed. + An exception to this rule is done for values that are very close to +2.0 + (both for consistency with the -2.0 case, and for the relative frequency + these occur in real fonts). When almost +2.0 values occur (and all other + values are within the range -2.0 <= x <= +2.0), they are clamped to the + maximum positive value that can still be encoded as an F2Dot14: i.e. + 1.99993896484375. + If False, no check is done and all components are translated unmodified + into the glyf table, followed by an inevitable `struct.error` once an + attempt is made to compile them. + """ - def __init__(self, glyphSet): + def __init__(self, glyphSet, handleOverflowingTransforms=True): self.glyphSet = glyphSet + self.handleOverflowingTransforms = handleOverflowingTransforms self.init() def init(self): @@ -79,24 +100,46 @@ def addComponent(self, glyphName, transformation): self.components.append((glyphName, transformation)) - def glyph(self, componentFlags=0x4): - assert self._isClosed(), "Didn't close last contour." - + def _buildComponents(self, componentFlags): + if self.handleOverflowingTransforms: + # we can't encode transform values > 2 or < -2 in F2Dot14, + # so we must decompose the glyph if any transform exceeds these + overflowing = any(s > 2 or s < -2 + for (glyphName, transformation) in self.components + for s in transformation[:4]) components = [] for glyphName, transformation in self.components: - if self.points: - # can't have both, so decompose the glyph + if glyphName not in self.glyphSet: + self.log.warning( + "skipped non-existing component '%s'", glyphName + ) + continue + if (self.points or + (self.handleOverflowingTransforms and overflowing)): + # can't have both coordinates and components, so decompose tpen = TransformPen(self, transformation) self.glyphSet[glyphName].draw(tpen) continue component = GlyphComponent() component.glyphName = glyphName - if transformation[:4] != (1, 0, 0, 1): - component.transform = (transformation[:2], transformation[2:4]) component.x, component.y = transformation[4:] + transformation = transformation[:4] + if transformation != (1, 0, 0, 1): + if (self.handleOverflowingTransforms and + any(MAX_F2DOT14 < s <= 2 for s in transformation)): + # clamp values ~= +2.0 so we can keep the component + transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 + else s for s in transformation) + component.transform = (transformation[:2], transformation[2:]) component.flags = componentFlags components.append(component) + return components + + def glyph(self, componentFlags=0x4): + assert self._isClosed(), "Didn't close last contour." + + components = self._buildComponents(componentFlags) glyph = Glyph() glyph.coordinates = GlyphCoordinates(self.points) diff -Nru fonttools-3.21.2/Snippets/fontTools/subset/__init__.py fonttools-3.29.0/Snippets/fontTools/subset/__init__.py --- fonttools-3.21.2/Snippets/fontTools/subset/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/subset/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,11 +4,13 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools import ttLib from fontTools.ttLib.tables import otTables from fontTools.misc import psCharStrings from fontTools.pens.basePen import NullPen from fontTools.misc.loggingTools import Timer +from fontTools.varLib import varStore import sys import struct import array @@ -100,7 +102,7 @@ $ pyftsubset --glyph-names? Current setting for 'glyph-names' is: False $ ./pyftsubset --name-IDs=? - Current setting for 'name-IDs' is: [1, 2] + Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] $ ./pyftsubset --hinting? --no-hinting --hinting? Current setting for 'hinting' is: True Current setting for 'hinting' is: False @@ -211,8 +213,8 @@ Add to the set of tables that will not be subsetted. By default, the following tables are included in this list, as they do not need subsetting (ignore the fact that 'loca' is listed - here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', - 'name', 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'STAT'. + here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', + 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'. By default, tables that the tool does not know how to subset and are not specified here will be dropped from the font, unless --passthrough-tables option is passed. @@ -242,11 +244,11 @@ codes, see: http://www.microsoft.com/typography/otspec/name.htm --name-IDs[+|-]=[,...] Specify (=), add to (+=) or exclude from (-=) the set of 'name' table - entry nameIDs that will be preserved. By default only nameID 1 (Family) - and nameID 2 (Style) are preserved. Use '*' to keep all entries. + entry nameIDs that will be preserved. By default, only nameIDs between 0 + and 6 are preserved, the rest are dropped. Use '*' to keep all entries. Examples: - --name-IDs+=0,4,6 - * Also keep Copyright, Full name and PostScript name entry. + --name-IDs+=7,8,9 + * Also keep Trademark, Manufacturer and Designer name entries. --name-IDs='' * Drop all 'name' table entries. --name-IDs='*' @@ -310,6 +312,8 @@ Update the 'OS/2 xAvgCharWidth' field after subsetting. --no-recalc-average-width Don't change the 'OS/2 xAvgCharWidth' field. [default] + --font-number= + Select font number for TrueType Collection (.ttc/.otc), starting from 0. Application options: --verbose @@ -333,10 +337,10 @@ log = logging.getLogger("fontTools.subset") def _log_glyphs(self, glyphs, font=None): - self.info("Glyph names: %s", sorted(glyphs)) - if font: - reverseGlyphMap = font.getReverseGlyphMap() - self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) + self.info("Glyph names: %s", sorted(glyphs)) + if font: + reverseGlyphMap = font.getReverseGlyphMap() + self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) # bind "glyphs" function to 'log' object log.glyphs = MethodType(_log_glyphs, log) @@ -347,2203 +351,2349 @@ def _add_method(*clazzes): - """Returns a decorator function that adds a new method to one or - more classes.""" - def wrapper(method): - done = [] - for clazz in clazzes: - if clazz in done: continue # Support multiple names of a clazz - done.append(clazz) - assert clazz.__name__ != 'DefaultTable', \ - 'Oops, table class not found.' - assert not hasattr(clazz, method.__name__), \ - "Oops, class '%s' has method '%s'." % (clazz.__name__, - method.__name__) - setattr(clazz, method.__name__, method) - return None - return wrapper + """Returns a decorator function that adds a new method to one or + more classes.""" + def wrapper(method): + done = [] + for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) + assert clazz.__name__ != 'DefaultTable', \ + 'Oops, table class not found.' + assert not hasattr(clazz, method.__name__), \ + "Oops, class '%s' has method '%s'." % (clazz.__name__, + method.__name__) + setattr(clazz, method.__name__, method) + return None + return wrapper def _uniq_sort(l): - return sorted(set(l)) + return sorted(set(l)) def _set_update(s, *others): - # Jython's set.update only takes one other argument. - # Emulate real set.update... - for other in others: - s.update(other) + # Jython's set.update only takes one other argument. + # Emulate real set.update... + for other in others: + s.update(other) def _dict_subset(d, glyphs): - return {g:d[g] for g in glyphs} + return {g:d[g] for g in glyphs} @_add_method(otTables.Coverage) def intersect(self, glyphs): - """Returns ascending list of matching coverage values.""" - return [i for i,g in enumerate(self.glyphs) if g in glyphs] + """Returns ascending list of matching coverage values.""" + return [i for i,g in enumerate(self.glyphs) if g in glyphs] @_add_method(otTables.Coverage) def intersect_glyphs(self, glyphs): - """Returns set of intersecting glyphs.""" - return set(g for g in self.glyphs if g in glyphs) + """Returns set of intersecting glyphs.""" + return set(g for g in self.glyphs if g in glyphs) @_add_method(otTables.Coverage) def subset(self, glyphs): - """Returns ascending list of remaining coverage values.""" - indices = self.intersect(glyphs) - self.glyphs = [g for g in self.glyphs if g in glyphs] - return indices + """Returns ascending list of remaining coverage values.""" + indices = self.intersect(glyphs) + self.glyphs = [g for g in self.glyphs if g in glyphs] + return indices @_add_method(otTables.Coverage) def remap(self, coverage_map): - """Remaps coverage.""" - self.glyphs = [self.glyphs[i] for i in coverage_map] + """Remaps coverage.""" + self.glyphs = [self.glyphs[i] for i in coverage_map] @_add_method(otTables.ClassDef) def intersect(self, glyphs): - """Returns ascending list of matching class values.""" - return _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - [v for g,v in self.classDefs.items() if g in glyphs]) + """Returns ascending list of matching class values.""" + return _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + [v for g,v in self.classDefs.items() if g in glyphs]) @_add_method(otTables.ClassDef) def intersect_class(self, glyphs, klass): - """Returns set of glyphs matching class.""" - if klass == 0: - return set(g for g in glyphs if g not in self.classDefs) - return set(g for g,v in self.classDefs.items() - if v == klass and g in glyphs) + """Returns set of glyphs matching class.""" + if klass == 0: + return set(g for g in glyphs if g not in self.classDefs) + return set(g for g,v in self.classDefs.items() + if v == klass and g in glyphs) @_add_method(otTables.ClassDef) def subset(self, glyphs, remap=False): - """Returns ascending list of remaining classes.""" - self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs} - # Note: while class 0 has the special meaning of "not matched", - # if no glyph will ever /not match/, we can optimize class 0 out too. - indices = _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - list(self.classDefs.values())) - if remap: - self.remap(indices) - return indices + """Returns ascending list of remaining classes.""" + self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs} + # Note: while class 0 has the special meaning of "not matched", + # if no glyph will ever /not match/, we can optimize class 0 out too. + indices = _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + list(self.classDefs.values())) + if remap: + self.remap(indices) + return indices @_add_method(otTables.ClassDef) def remap(self, class_map): - """Remaps classes.""" - self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()} + """Remaps classes.""" + self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()} @_add_method(otTables.SingleSubst) def closure_glyphs(self, s, cur_glyphs): - s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) + s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) @_add_method(otTables.SingleSubst) def subset_glyphs(self, s): - self.mapping = {g:v for g,v in self.mapping.items() - if g in s.glyphs and v in s.glyphs} - return bool(self.mapping) + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and v in s.glyphs} + return bool(self.mapping) @_add_method(otTables.MultipleSubst) def closure_glyphs(self, s, cur_glyphs): - for glyph, subst in self.mapping.items(): - if glyph in cur_glyphs: - _set_update(s.glyphs, subst) + for glyph, subst in self.mapping.items(): + if glyph in cur_glyphs: + _set_update(s.glyphs, subst) @_add_method(otTables.MultipleSubst) def subset_glyphs(self, s): - self.mapping = {g:v for g,v in self.mapping.items() - if g in s.glyphs and all(sub in s.glyphs for sub in v)} - return bool(self.mapping) + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and all(sub in s.glyphs for sub in v)} + return bool(self.mapping) @_add_method(otTables.AlternateSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() - if g in cur_glyphs)) + _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() + if g in cur_glyphs)) @_add_method(otTables.AlternateSubst) def subset_glyphs(self, s): - self.alternates = {g:vlist - for g,vlist in self.alternates.items() - if g in s.glyphs and - all(v in s.glyphs for v in vlist)} - return bool(self.alternates) + self.alternates = {g:vlist + for g,vlist in self.alternates.items() + if g in s.glyphs and + all(v in s.glyphs for v in vlist)} + return bool(self.alternates) @_add_method(otTables.LigatureSubst) def closure_glyphs(self, s, cur_glyphs): - _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs - if all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items() - if g in cur_glyphs)) + _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs + if all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items() + if g in cur_glyphs)) @_add_method(otTables.LigatureSubst) def subset_glyphs(self, s): - self.ligatures = {g:v for g,v in self.ligatures.items() - if g in s.glyphs} - self.ligatures = {g:[seq for seq in seqs - if seq.LigGlyph in s.glyphs and - all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items()} - self.ligatures = {g:v for g,v in self.ligatures.items() if v} - return bool(self.ligatures) + self.ligatures = {g:v for g,v in self.ligatures.items() + if g in s.glyphs} + self.ligatures = {g:[seq for seq in seqs + if seq.LigGlyph in s.glyphs and + all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items()} + self.ligatures = {g:v for g,v in self.ligatures.items() if v} + return bool(self.ligatures) @_add_method(otTables.ReverseChainSingleSubst) def closure_glyphs(self, s, cur_glyphs): - if self.Format == 1: - indices = self.Coverage.intersect(cur_glyphs) - if(not indices or - not all(c.intersect(s.glyphs) - for c in self.LookAheadCoverage + self.BacktrackCoverage)): - return - s.glyphs.update(self.Substitute[i] for i in indices) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.intersect(cur_glyphs) + if(not indices or + not all(c.intersect(s.glyphs) + for c in self.LookAheadCoverage + self.BacktrackCoverage)): + return + s.glyphs.update(self.Substitute[i] for i in indices) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ReverseChainSingleSubst) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - self.Substitute = [self.Substitute[i] for i in indices] - # Now drop rules generating glyphs we don't want - indices = [i for i,sub in enumerate(self.Substitute) - if sub in s.glyphs] - self.Substitute = [self.Substitute[i] for i in indices] - self.Coverage.remap(indices) - self.GlyphCount = len(self.Substitute) - return bool(self.GlyphCount and - all(c.subset(s.glyphs) - for c in self.LookAheadCoverage+self.BacktrackCoverage)) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + self.Substitute = [self.Substitute[i] for i in indices] + # Now drop rules generating glyphs we don't want + indices = [i for i,sub in enumerate(self.Substitute) + if sub in s.glyphs] + self.Substitute = [self.Substitute[i] for i in indices] + self.Coverage.remap(indices) + self.GlyphCount = len(self.Substitute) + return bool(self.GlyphCount and + all(c.subset(s.glyphs) + for c in self.LookAheadCoverage+self.BacktrackCoverage)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.SinglePos) def subset_glyphs(self, s): - if self.Format == 1: - return len(self.Coverage.subset(s.glyphs)) - elif self.Format == 2: - indices = self.Coverage.subset(s.glyphs) - values = self.Value - count = len(values) - self.Value = [values[i] for i in indices if i < count] - self.ValueCount = len(self.Value) - return bool(self.ValueCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return len(self.Coverage.subset(s.glyphs)) + elif self.Format == 2: + indices = self.Coverage.subset(s.glyphs) + values = self.Value + count = len(values) + self.Value = [values[i] for i in indices if i < count] + self.ValueCount = len(self.Value) + return bool(self.ValueCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.SinglePos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat &= ~0x00F0 - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat &= ~0x00F0 + return True @_add_method(otTables.PairPos) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - pairs = self.PairSet - count = len(pairs) - self.PairSet = [pairs[i] for i in indices if i < count] - for p in self.PairSet: - p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs] - p.PairValueCount = len(p.PairValueRecord) - # Remove empty pairsets - indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] - self.Coverage.remap(indices) - self.PairSet = [self.PairSet[i] for i in indices] - self.PairSetCount = len(self.PairSet) - return bool(self.PairSetCount) - elif self.Format == 2: - class1_map = [c for c in self.ClassDef1.subset(s.glyphs, remap=True) if c < self.Class1Count] - class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True) if c < self.Class2Count] - self.Class1Record = [self.Class1Record[i] for i in class1_map] - for c in self.Class1Record: - c.Class2Record = [c.Class2Record[i] for i in class2_map] - self.Class1Count = len(class1_map) - self.Class2Count = len(class2_map) - return bool(self.Class1Count and - self.Class2Count and - self.Coverage.subset(s.glyphs)) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + pairs = self.PairSet + count = len(pairs) + self.PairSet = [pairs[i] for i in indices if i < count] + for p in self.PairSet: + p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs] + p.PairValueCount = len(p.PairValueRecord) + # Remove empty pairsets + indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] + self.Coverage.remap(indices) + self.PairSet = [self.PairSet[i] for i in indices] + self.PairSetCount = len(self.PairSet) + return bool(self.PairSetCount) + elif self.Format == 2: + class1_map = [c for c in self.ClassDef1.subset(s.glyphs, remap=True) if c < self.Class1Count] + class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True) if c < self.Class2Count] + self.Class1Record = [self.Class1Record[i] for i in class1_map] + for c in self.Class1Record: + c.Class2Record = [c.Class2Record[i] for i in class2_map] + self.Class1Count = len(class1_map) + self.Class2Count = len(class2_map) + return bool(self.Class1Count and + self.Class2Count and + self.Coverage.subset(s.glyphs)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.PairPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat1 &= ~0x00F0 - self.ValueFormat2 &= ~0x00F0 - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat1 &= ~0x00F0 + self.ValueFormat2 &= ~0x00F0 + return True @_add_method(otTables.CursivePos) def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - records = self.EntryExitRecord - count = len(records) - self.EntryExitRecord = [records[i] for i in indices if i < count] - self.EntryExitCount = len(self.EntryExitRecord) - return bool(self.EntryExitCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + records = self.EntryExitRecord + count = len(records) + self.EntryExitRecord = [records[i] for i in indices if i < count] + self.EntryExitCount = len(self.EntryExitRecord) + return bool(self.EntryExitCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.Anchor) def prune_hints(self): - # Drop device tables / contour anchor point - self.ensureDecompiled() - self.Format = 1 + # Drop device tables / contour anchor point + self.ensureDecompiled() + self.Format = 1 @_add_method(otTables.CursivePos) -def prune_post_subset(self, options): - if not options.hinting: - for rec in self.EntryExitRecord: - if rec.EntryAnchor: rec.EntryAnchor.prune_hints() - if rec.ExitAnchor: rec.ExitAnchor.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for rec in self.EntryExitRecord: + if rec.EntryAnchor: rec.EntryAnchor.prune_hints() + if rec.ExitAnchor: rec.ExitAnchor.prune_hints() + return True @_add_method(otTables.MarkBasePos) def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - base_indices = self.BaseCoverage.subset(s.glyphs) - self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] for i in base_indices] - self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.BaseArray.BaseRecord: - b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.BaseArray.BaseCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + base_indices = self.BaseCoverage.subset(s.glyphs) + self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] for i in base_indices] + self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.BaseArray.BaseRecord: + b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.BaseArray.BaseCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkBasePos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.BaseArray.BaseRecord: - for a in b.BaseAnchor: - if a: - a.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.BaseArray.BaseRecord: + for a in b.BaseAnchor: + if a: + a.prune_hints() + return True @_add_method(otTables.MarkLigPos) def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - ligature_indices = self.LigatureCoverage.subset(s.glyphs) - self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] for i in ligature_indices] - self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.LigatureArray.LigatureCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + ligature_indices = self.LigatureCoverage.subset(s.glyphs) + self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] for i in ligature_indices] + self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.LigatureArray.LigatureCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkLigPos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - for a in c.LigatureAnchor: - if a: - a.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + for a in c.LigatureAnchor: + if a: + a.prune_hints() + return True @_add_method(otTables.MarkMarkPos) def subset_glyphs(self, s): - if self.Format == 1: - mark1_indices = self.Mark1Coverage.subset(s.glyphs) - self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] for i in mark1_indices] - self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) - mark2_indices = self.Mark2Coverage.subset(s.glyphs) - self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] for i in mark2_indices] - self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.Mark1Array.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.Mark2Array.Mark2Record: - b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] - return bool(self.ClassCount and - self.Mark1Array.MarkCount and - self.Mark2Array.MarkCount) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + mark1_indices = self.Mark1Coverage.subset(s.glyphs) + self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] for i in mark1_indices] + self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) + mark2_indices = self.Mark2Coverage.subset(s.glyphs) + self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] for i in mark2_indices] + self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.Mark1Array.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.Mark2Array.Mark2Record: + b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] + return bool(self.ClassCount and + self.Mark1Array.MarkCount and + self.Mark2Array.MarkCount) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.MarkMarkPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables or contour anchor point - for m in self.Mark1Array.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.Mark2Array.Mark2Record: - for m in b.Mark2Anchor: - if m: - m.prune_hints() - return True +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables or contour anchor point + for m in self.Mark1Array.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.Mark2Array.Mark2Record: + for m in b.Mark2Anchor: + if m: + m.prune_hints() + return True @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) def subset_lookups(self, lookup_indices): - pass + pass @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) def collect_lookups(self): - return [] + return [] @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def prune_post_subset(self, options): - return True + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def prune_post_subset(self, font, options): + return True @_add_method(otTables.SingleSubst, - otTables.AlternateSubst, - otTables.ReverseChainSingleSubst) + otTables.AlternateSubst, + otTables.ReverseChainSingleSubst) def may_have_non_1to1(self): - return False + return False @_add_method(otTables.MultipleSubst, - otTables.LigatureSubst, - otTables.ContextSubst, - otTables.ChainContextSubst) + otTables.LigatureSubst, + otTables.ContextSubst, + otTables.ChainContextSubst) def may_have_non_1to1(self): - return True + return True @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def __subset_classify_context(self): - class ContextHelper(object): - def __init__(self, klass, Format): - if klass.__name__.endswith('Subst'): - Typ = 'Sub' - Type = 'Subst' - else: - Typ = 'Pos' - Type = 'Pos' - if klass.__name__.startswith('Chain'): - Chain = 'Chain' - InputIdx = 1 - DataLen = 3 - else: - Chain = '' - InputIdx = 0 - DataLen = 1 - ChainTyp = Chain+Typ - - self.Typ = Typ - self.Type = Type - self.Chain = Chain - self.ChainTyp = ChainTyp - self.InputIdx = InputIdx - self.DataLen = DataLen - - self.LookupRecord = Type+'LookupRecord' - - if Format == 1: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(None,) - ChainContextData = lambda r:(None, None, None) - SetContextData = None - SetChainContextData = None - RuleData = lambda r:(r.Input,) - ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) - def SetRuleData(r, d): - (r.Input,) = d - (r.GlyphCount,) = (len(x)+1 for x in d) - def ChainSetRuleData(r, d): - (r.Backtrack, r.Input, r.LookAhead) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) - elif Format == 2: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(r.ClassDef,) - ChainContextData = lambda r:(r.BacktrackClassDef, - r.InputClassDef, - r.LookAheadClassDef) - def SetContextData(r, d): - (r.ClassDef,) = d - def SetChainContextData(r, d): - (r.BacktrackClassDef, - r.InputClassDef, - r.LookAheadClassDef) = d - RuleData = lambda r:(r.Class,) - ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) - def SetRuleData(r, d): - (r.Class,) = d - (r.GlyphCount,) = (len(x)+1 for x in d) - def ChainSetRuleData(r, d): - (r.Backtrack, r.Input, r.LookAhead) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) - elif Format == 3: - Coverage = lambda r: r.Coverage[0] - ChainCoverage = lambda r: r.InputCoverage[0] - ContextData = None - ChainContextData = None - SetContextData = None - SetChainContextData = None - RuleData = lambda r: r.Coverage - ChainRuleData = lambda r:(r.BacktrackCoverage + - r.InputCoverage + - r.LookAheadCoverage) - def SetRuleData(r, d): - (r.Coverage,) = d - (r.GlyphCount,) = (len(x) for x in d) - def ChainSetRuleData(r, d): - (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d - (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) - else: - assert 0, "unknown format: %s" % Format - - if Chain: - self.Coverage = ChainCoverage - self.ContextData = ChainContextData - self.SetContextData = SetChainContextData - self.RuleData = ChainRuleData - self.SetRuleData = ChainSetRuleData - else: - self.Coverage = Coverage - self.ContextData = ContextData - self.SetContextData = SetContextData - self.RuleData = RuleData - self.SetRuleData = SetRuleData - - if Format == 1: - self.Rule = ChainTyp+'Rule' - self.RuleCount = ChainTyp+'RuleCount' - self.RuleSet = ChainTyp+'RuleSet' - self.RuleSetCount = ChainTyp+'RuleSetCount' - self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] - elif Format == 2: - self.Rule = ChainTyp+'ClassRule' - self.RuleCount = ChainTyp+'ClassRuleCount' - self.RuleSet = ChainTyp+'ClassSet' - self.RuleSetCount = ChainTyp+'ClassSetCount' - self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c - else (set(glyphs) if r == 0 else set())) - - self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' - self.ClassDefIndex = 1 if Chain else 0 - self.Input = 'Input' if Chain else 'Class' - - if self.Format not in [1, 2, 3]: - return None # Don't shoot the messenger; let it go - if not hasattr(self.__class__, "__ContextHelpers"): - self.__class__.__ContextHelpers = {} - if self.Format not in self.__class__.__ContextHelpers: - helper = ContextHelper(self.__class__, self.Format) - self.__class__.__ContextHelpers[self.Format] = helper - return self.__class__.__ContextHelpers[self.Format] + class ContextHelper(object): + def __init__(self, klass, Format): + if klass.__name__.endswith('Subst'): + Typ = 'Sub' + Type = 'Subst' + else: + Typ = 'Pos' + Type = 'Pos' + if klass.__name__.startswith('Chain'): + Chain = 'Chain' + InputIdx = 1 + DataLen = 3 + else: + Chain = '' + InputIdx = 0 + DataLen = 1 + ChainTyp = Chain+Typ + + self.Typ = Typ + self.Type = Type + self.Chain = Chain + self.ChainTyp = ChainTyp + self.InputIdx = InputIdx + self.DataLen = DataLen + + self.LookupRecord = Type+'LookupRecord' + + if Format == 1: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(None,) + ChainContextData = lambda r:(None, None, None) + SetContextData = None + SetChainContextData = None + RuleData = lambda r:(r.Input,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Input,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 2: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(r.ClassDef,) + ChainContextData = lambda r:(r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) + def SetContextData(r, d): + (r.ClassDef,) = d + def SetChainContextData(r, d): + (r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) = d + RuleData = lambda r:(r.Class,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Class,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 3: + Coverage = lambda r: r.Coverage[0] + ChainCoverage = lambda r: r.InputCoverage[0] + ContextData = None + ChainContextData = None + SetContextData = None + SetChainContextData = None + RuleData = lambda r: r.Coverage + ChainRuleData = lambda r:(r.BacktrackCoverage + + r.InputCoverage + + r.LookAheadCoverage) + def SetRuleData(r, d): + (r.Coverage,) = d + (r.GlyphCount,) = (len(x) for x in d) + def ChainSetRuleData(r, d): + (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) + else: + assert 0, "unknown format: %s" % Format + + if Chain: + self.Coverage = ChainCoverage + self.ContextData = ChainContextData + self.SetContextData = SetChainContextData + self.RuleData = ChainRuleData + self.SetRuleData = ChainSetRuleData + else: + self.Coverage = Coverage + self.ContextData = ContextData + self.SetContextData = SetContextData + self.RuleData = RuleData + self.SetRuleData = SetRuleData + + if Format == 1: + self.Rule = ChainTyp+'Rule' + self.RuleCount = ChainTyp+'RuleCount' + self.RuleSet = ChainTyp+'RuleSet' + self.RuleSetCount = ChainTyp+'RuleSetCount' + self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] + elif Format == 2: + self.Rule = ChainTyp+'ClassRule' + self.RuleCount = ChainTyp+'ClassRuleCount' + self.RuleSet = ChainTyp+'ClassSet' + self.RuleSetCount = ChainTyp+'ClassSetCount' + self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c + else (set(glyphs) if r == 0 else set())) + + self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' + self.ClassDefIndex = 1 if Chain else 0 + self.Input = 'Input' if Chain else 'Class' + + if self.Format not in [1, 2, 3]: + return None # Don't shoot the messenger; let it go + if not hasattr(self.__class__, "__ContextHelpers"): + self.__class__.__ContextHelpers = {} + if self.Format not in self.__class__.__ContextHelpers: + helper = ContextHelper(self.__class__, self.Format) + self.__class__.__ContextHelpers[self.Format] = helper + return self.__class__.__ContextHelpers[self.Format] @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst) + otTables.ChainContextSubst) def closure_glyphs(self, s, cur_glyphs): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - indices = c.Coverage(self).intersect(cur_glyphs) - if not indices: - return [] - cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) - - if self.Format == 1: - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - continue - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) - else: - pos_glyphs = frozenset([r.Input[seqi - 1]]) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(r.Input)+2)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 2: - ClassDef = getattr(self, c.ClassDef) - indices = ClassDef.intersect(cur_glyphs) - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - continue - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i)) - else: - pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1])) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(getattr(r, c.Input))+2)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 3: - if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): - return [] - r = self - chaos = set() - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if seqi in chaos: - # TODO Can we improve this? - pos_glyphs = None - else: - if seqi == 0: - pos_glyphs = frozenset(cur_glyphs) - else: - pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs)) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos.add(seqi) - if lookup.may_have_non_1to1(): - chaos.update(range(seqi, len(r.InputCoverage)+1)) - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - else: - assert 0, "unknown format: %s" % self.Format + indices = c.Coverage(self).intersect(cur_glyphs) + if not indices: + return [] + cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) + + if self.Format == 1: + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) + else: + pos_glyphs = frozenset([r.Input[seqi - 1]]) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.Input)+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 2: + ClassDef = getattr(self, c.ClassDef) + indices = ClassDef.intersect(cur_glyphs) + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i)) + else: + pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1])) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(getattr(r, c.Input))+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 3: + if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): + return [] + r = self + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(cur_glyphs) + else: + pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs)) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.InputCoverage)+1)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ContextPos, - otTables.ChainContextSubst, - otTables.ChainContextPos) -def subset_glyphs(self, s): - c = self.__subset_classify_context() - - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - rss = [rss[i] for i in indices if i < rssCount] - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(g in s.glyphs for g in glist) - for glist in c.RuleData(r))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - # Prune empty rulesets - indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)] - self.Coverage.remap(indices) - rss = [rss[i] for i in indices] - setattr(self, c.RuleSet, rss) - setattr(self, c.RuleSetCount, len(rss)) - return bool(rss) - elif self.Format == 2: - if not self.Coverage.subset(s.glyphs): - return False - ContextData = c.ContextData(self) - klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData] - - # Keep rulesets for class numbers that survived. - indices = klass_maps[c.ClassDefIndex] - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - rss = [rss[i] for i in indices if i < rssCount] - del rssCount - # Delete, but not renumber, unreachable rulesets. - indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) - rss = [rss if i in indices else None for i,rss in enumerate(rss)] - - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(k in klass_map for k in klist) - for klass_map,klist in zip(klass_maps, c.RuleData(r)))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - - # Remap rule classes - for r in ss: - c.SetRuleData(r, [[klass_map.index(k) for k in klist] - for klass_map,klist in zip(klass_maps, c.RuleData(r))]) - - # Prune empty rulesets - rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] - while rss and rss[-1] is None: - del rss[-1] - setattr(self, c.RuleSet, rss) - setattr(self, c.RuleSetCount, len(rss)) - - # TODO: We can do a second round of remapping class values based - # on classes that are actually used in at least one rule. Right - # now we subset classes to c.glyphs only. Or better, rewrite - # the above to do that. - - return bool(rss) - elif self.Format == 3: - return all(x.subset(s.glyphs) for x in c.RuleData(self)) - else: - assert 0, "unknown format: %s" % self.Format + otTables.ContextPos, + otTables.ChainContextSubst, + otTables.ChainContextPos) +def subset_glyphs(self, s): + c = self.__subset_classify_context() + + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(g in s.glyphs for g in glist) + for glist in c.RuleData(r))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + # Prune empty rulesets + indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)] + self.Coverage.remap(indices) + rss = [rss[i] for i in indices] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + return bool(rss) + elif self.Format == 2: + if not self.Coverage.subset(s.glyphs): + return False + ContextData = c.ContextData(self) + klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData] + + # Keep rulesets for class numbers that survived. + indices = klass_maps[c.ClassDefIndex] + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + del rssCount + # Delete, but not renumber, unreachable rulesets. + indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) + rss = [rss if i in indices else None for i,rss in enumerate(rss)] + + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(k in klass_map for k in klist) + for klass_map,klist in zip(klass_maps, c.RuleData(r)))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + + # Remap rule classes + for r in ss: + c.SetRuleData(r, [[klass_map.index(k) for k in klist] + for klass_map,klist in zip(klass_maps, c.RuleData(r))]) + + # Prune empty rulesets + rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] + while rss and rss[-1] is None: + del rss[-1] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + + # TODO: We can do a second round of remapping class values based + # on classes that are actually used in at least one rule. Right + # now we subset classes to c.glyphs only. Or better, rewrite + # the above to do that. + + return bool(rss) + elif self.Format == 3: + return all(x.subset(s.glyphs) for x in c.RuleData(self)) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def subset_lookups(self, lookup_indices): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - if self.Format in [1, 2]: - for rs in getattr(self, c.RuleSet): - if not rs: continue - for r in getattr(rs, c.Rule): - if not r: continue - setattr(r, c.LookupRecord, - [ll for ll in getattr(r, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(r, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - elif self.Format == 3: - setattr(self, c.LookupRecord, - [ll for ll in getattr(self, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(self, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format in [1, 2]: + for rs in getattr(self, c.RuleSet): + if not rs: continue + for r in getattr(rs, c.Rule): + if not r: continue + setattr(r, c.LookupRecord, + [ll for ll in getattr(r, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(r, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + elif self.Format == 3: + setattr(self, c.LookupRecord, + [ll for ll in getattr(self, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(self, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) def collect_lookups(self): - c = self.__subset_classify_context() + c = self.__subset_classify_context() - if self.Format in [1, 2]: - return [ll.LookupListIndex - for rs in getattr(self, c.RuleSet) if rs - for r in getattr(rs, c.Rule) if r - for ll in getattr(r, c.LookupRecord) if ll] - elif self.Format == 3: - return [ll.LookupListIndex - for ll in getattr(self, c.LookupRecord) if ll] - else: - assert 0, "unknown format: %s" % self.Format + if self.Format in [1, 2]: + return [ll.LookupListIndex + for rs in getattr(self, c.RuleSet) if rs + for r in getattr(rs, c.Rule) if r + for ll in getattr(r, c.LookupRecord) if ll] + elif self.Format == 3: + return [ll.LookupListIndex + for ll in getattr(self, c.LookupRecord) if ll] + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst) def closure_glyphs(self, s, cur_glyphs): - if self.Format == 1: - self.ExtSubTable.closure_glyphs(s, cur_glyphs) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + self.ExtSubTable.closure_glyphs(s, cur_glyphs) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst) def may_have_non_1to1(self): - if self.Format == 1: - return self.ExtSubTable.may_have_non_1to1() - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.may_have_non_1to1() + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def subset_glyphs(self, s): - if self.Format == 1: - return self.ExtSubTable.subset_glyphs(s) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.subset_glyphs(s) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def prune_post_subset(self, options): - if self.Format == 1: - return self.ExtSubTable.prune_post_subset(options) - else: - assert 0, "unknown format: %s" % self.Format + otTables.ExtensionPos) +def prune_post_subset(self, font, options): + if self.Format == 1: + return self.ExtSubTable.prune_post_subset(font, options) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def subset_lookups(self, lookup_indices): - if self.Format == 1: - return self.ExtSubTable.subset_lookups(lookup_indices) - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.subset_lookups(lookup_indices) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) + otTables.ExtensionPos) def collect_lookups(self): - if self.Format == 1: - return self.ExtSubTable.collect_lookups() - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + return self.ExtSubTable.collect_lookups() + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.Lookup) def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: - cur_glyphs = frozenset(s.glyphs) + if cur_glyphs is None: + cur_glyphs = frozenset(s.glyphs) - # Memoize - if (id(self), cur_glyphs) in s._doneLookups: - return - s._doneLookups.add((id(self), cur_glyphs)) - - if self in s._activeLookups: - raise Exception("Circular loop in lookup recursion") - s._activeLookups.append(self) - for st in self.SubTable: - if not st: continue - st.closure_glyphs(s, cur_glyphs) - assert(s._activeLookups[-1] == self) - del s._activeLookups[-1] + # Memoize + key = id(self) + doneLookups = s._doneLookups + count,covered = doneLookups.get(key, (0, None)) + if count != len(s.glyphs): + count,covered = doneLookups[key] = (len(s.glyphs), set()) + if cur_glyphs.issubset(covered): + return + covered.update(cur_glyphs) + + for st in self.SubTable: + if not st: continue + st.closure_glyphs(s, cur_glyphs) @_add_method(otTables.Lookup) def subset_glyphs(self, s): - self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] - self.SubTableCount = len(self.SubTable) - return bool(self.SubTableCount) + self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] + self.SubTableCount = len(self.SubTable) + return bool(self.SubTableCount) @_add_method(otTables.Lookup) -def prune_post_subset(self, options): - ret = False - for st in self.SubTable: - if not st: continue - if st.prune_post_subset(options): ret = True - return ret +def prune_post_subset(self, font, options): + ret = False + for st in self.SubTable: + if not st: continue + if st.prune_post_subset(font, options): ret = True + return ret @_add_method(otTables.Lookup) def subset_lookups(self, lookup_indices): - for s in self.SubTable: - s.subset_lookups(lookup_indices) + for s in self.SubTable: + s.subset_lookups(lookup_indices) @_add_method(otTables.Lookup) def collect_lookups(self): - return sum((st.collect_lookups() for st in self.SubTable if st), []) + return sum((st.collect_lookups() for st in self.SubTable if st), []) @_add_method(otTables.Lookup) def may_have_non_1to1(self): - return any(st.may_have_non_1to1() for st in self.SubTable if st) + return any(st.may_have_non_1to1() for st in self.SubTable if st) @_add_method(otTables.LookupList) def subset_glyphs(self, s): - """Returns the indices of nonempty lookups.""" - return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] + """Returns the indices of nonempty lookups.""" + return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] @_add_method(otTables.LookupList) -def prune_post_subset(self, options): - ret = False - for l in self.Lookup: - if not l: continue - if l.prune_post_subset(options): ret = True - return ret +def prune_post_subset(self, font, options): + ret = False + for l in self.Lookup: + if not l: continue + if l.prune_post_subset(font, options): ret = True + return ret @_add_method(otTables.LookupList) def subset_lookups(self, lookup_indices): - self.ensureDecompiled() - self.Lookup = [self.Lookup[i] for i in lookup_indices - if i < self.LookupCount] - self.LookupCount = len(self.Lookup) - for l in self.Lookup: - l.subset_lookups(lookup_indices) + self.ensureDecompiled() + self.Lookup = [self.Lookup[i] for i in lookup_indices + if i < self.LookupCount] + self.LookupCount = len(self.Lookup) + for l in self.Lookup: + l.subset_lookups(lookup_indices) @_add_method(otTables.LookupList) def neuter_lookups(self, lookup_indices): - """Sets lookups not in lookup_indices to None.""" - self.ensureDecompiled() - self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)] + """Sets lookups not in lookup_indices to None.""" + self.ensureDecompiled() + self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)] @_add_method(otTables.LookupList) def closure_lookups(self, lookup_indices): - """Returns sorted index of all lookups reachable from lookup_indices.""" - lookup_indices = _uniq_sort(lookup_indices) - recurse = lookup_indices - while True: - recurse_lookups = sum((self.Lookup[i].collect_lookups() - for i in recurse if i < self.LookupCount), []) - recurse_lookups = [l for l in recurse_lookups - if l not in lookup_indices and l < self.LookupCount] - if not recurse_lookups: - return _uniq_sort(lookup_indices) - recurse_lookups = _uniq_sort(recurse_lookups) - lookup_indices.extend(recurse_lookups) - recurse = recurse_lookups + """Returns sorted index of all lookups reachable from lookup_indices.""" + lookup_indices = _uniq_sort(lookup_indices) + recurse = lookup_indices + while True: + recurse_lookups = sum((self.Lookup[i].collect_lookups() + for i in recurse if i < self.LookupCount), []) + recurse_lookups = [l for l in recurse_lookups + if l not in lookup_indices and l < self.LookupCount] + if not recurse_lookups: + return _uniq_sort(lookup_indices) + recurse_lookups = _uniq_sort(recurse_lookups) + lookup_indices.extend(recurse_lookups) + recurse = recurse_lookups @_add_method(otTables.Feature) def subset_lookups(self, lookup_indices): - """"Returns True if feature is non-empty afterwards.""" - self.LookupListIndex = [l for l in self.LookupListIndex - if l in lookup_indices] - # Now map them. - self.LookupListIndex = [lookup_indices.index(l) - for l in self.LookupListIndex] - self.LookupCount = len(self.LookupListIndex) - return self.LookupCount or self.FeatureParams + """"Returns True if feature is non-empty afterwards.""" + self.LookupListIndex = [l for l in self.LookupListIndex + if l in lookup_indices] + # Now map them. + self.LookupListIndex = [lookup_indices.index(l) + for l in self.LookupListIndex] + self.LookupCount = len(self.LookupListIndex) + return self.LookupCount or self.FeatureParams @_add_method(otTables.FeatureList) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - # Note: Never ever drop feature 'pref', even if it's empty. - # HarfBuzz chooses shaper for Khmer based on presence of this - # feature. See thread at: - # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html - return [i for i,f in enumerate(self.FeatureRecord) - if (f.Feature.subset_lookups(lookup_indices) or - f.FeatureTag == 'pref')] + """Returns the indices of nonempty features.""" + # Note: Never ever drop feature 'pref', even if it's empty. + # HarfBuzz chooses shaper for Khmer based on presence of this + # feature. See thread at: + # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html + return [i for i,f in enumerate(self.FeatureRecord) + if (f.Feature.subset_lookups(lookup_indices) or + f.FeatureTag == 'pref')] @_add_method(otTables.FeatureList) def collect_lookups(self, feature_indices): - return sum((self.FeatureRecord[i].Feature.LookupListIndex - for i in feature_indices - if i < self.FeatureCount), []) + return sum((self.FeatureRecord[i].Feature.LookupListIndex + for i in feature_indices + if i < self.FeatureCount), []) @_add_method(otTables.FeatureList) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] - self.FeatureCount = len(self.FeatureRecord) - return bool(self.FeatureCount) + self.ensureDecompiled() + self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] + self.FeatureCount = len(self.FeatureRecord) + return bool(self.FeatureCount) @_add_method(otTables.FeatureTableSubstitution) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - return [r.FeatureIndex for r in self.SubstitutionRecord - if r.Feature.subset_lookups(lookup_indices)] + """Returns the indices of nonempty features.""" + return [r.FeatureIndex for r in self.SubstitutionRecord + if r.Feature.subset_lookups(lookup_indices)] @_add_method(otTables.FeatureVariations) def subset_lookups(self, lookup_indices): - """Returns the indices of nonempty features.""" - return sum((f.FeatureTableSubstitution.subset_lookups(lookup_indices) - for f in self.FeatureVariationRecord), []) + """Returns the indices of nonempty features.""" + return sum((f.FeatureTableSubstitution.subset_lookups(lookup_indices) + for f in self.FeatureVariationRecord), []) @_add_method(otTables.FeatureVariations) def collect_lookups(self, feature_indices): - return sum((r.Feature.LookupListIndex - for vr in self.FeatureVariationRecord - for r in vr.FeatureTableSubstitution.SubstitutionRecord - if r.FeatureIndex in feature_indices), []) + return sum((r.Feature.LookupListIndex + for vr in self.FeatureVariationRecord + for r in vr.FeatureTableSubstitution.SubstitutionRecord + if r.FeatureIndex in feature_indices), []) @_add_method(otTables.FeatureTableSubstitution) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.SubstitutionRecord = [r for r in self.SubstitutionRecord - if r.FeatureIndex in feature_indices] - self.SubstitutionCount = len(self.SubstitutionRecord) - return bool(self.SubstitutionCount) + self.ensureDecompiled() + self.SubstitutionRecord = [r for r in self.SubstitutionRecord + if r.FeatureIndex in feature_indices] + self.SubstitutionCount = len(self.SubstitutionRecord) + return bool(self.SubstitutionCount) @_add_method(otTables.FeatureVariations) def subset_features(self, feature_indices): - self.ensureDecompiled() - self.FeaturVariationRecord = [r for r in self.FeatureVariationRecord - if r.FeatureTableSubstitution.subset_features(feature_indices)] - self.FeatureVariationCount = len(self.FeatureVariationRecord) - return bool(self.FeatureVariationCount) + self.ensureDecompiled() + self.FeaturVariationRecord = [r for r in self.FeatureVariationRecord + if r.FeatureTableSubstitution.subset_features(feature_indices)] + self.FeatureVariationCount = len(self.FeatureVariationRecord) + return bool(self.FeatureVariationCount) @_add_method(otTables.DefaultLangSys, - otTables.LangSys) + otTables.LangSys) def subset_features(self, feature_indices): - if self.ReqFeatureIndex in feature_indices: - self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) - else: - self.ReqFeatureIndex = 65535 - self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] - # Now map them. - self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex - if f in feature_indices] - self.FeatureCount = len(self.FeatureIndex) - return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) + if self.ReqFeatureIndex in feature_indices: + self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) + else: + self.ReqFeatureIndex = 65535 + self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] + # Now map them. + self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex + if f in feature_indices] + self.FeatureCount = len(self.FeatureIndex) + return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) @_add_method(otTables.DefaultLangSys, - otTables.LangSys) + otTables.LangSys) def collect_features(self): - feature_indices = self.FeatureIndex[:] - if self.ReqFeatureIndex != 65535: - feature_indices.append(self.ReqFeatureIndex) - return _uniq_sort(feature_indices) + feature_indices = self.FeatureIndex[:] + if self.ReqFeatureIndex != 65535: + feature_indices.append(self.ReqFeatureIndex) + return _uniq_sort(feature_indices) @_add_method(otTables.Script) def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False): - if(self.DefaultLangSys and - not self.DefaultLangSys.subset_features(feature_indices) and - not keepEmptyDefaultLangSys): - self.DefaultLangSys = None - self.LangSysRecord = [l for l in self.LangSysRecord - if l.LangSys.subset_features(feature_indices)] - self.LangSysCount = len(self.LangSysRecord) - return bool(self.LangSysCount or self.DefaultLangSys) + if(self.DefaultLangSys and + not self.DefaultLangSys.subset_features(feature_indices) and + not keepEmptyDefaultLangSys): + self.DefaultLangSys = None + self.LangSysRecord = [l for l in self.LangSysRecord + if l.LangSys.subset_features(feature_indices)] + self.LangSysCount = len(self.LangSysRecord) + return bool(self.LangSysCount or self.DefaultLangSys) @_add_method(otTables.Script) def collect_features(self): - feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] - if self.DefaultLangSys: - feature_indices.append(self.DefaultLangSys.collect_features()) - return _uniq_sort(sum(feature_indices, [])) + feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] + if self.DefaultLangSys: + feature_indices.append(self.DefaultLangSys.collect_features()) + return _uniq_sort(sum(feature_indices, [])) @_add_method(otTables.ScriptList) def subset_features(self, feature_indices, retain_empty): - # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 - self.ScriptRecord = [s for s in self.ScriptRecord - if s.Script.subset_features(feature_indices, s.ScriptTag=='DFLT') or - retain_empty] - self.ScriptCount = len(self.ScriptRecord) - return bool(self.ScriptCount) + # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 + self.ScriptRecord = [s for s in self.ScriptRecord + if s.Script.subset_features(feature_indices, s.ScriptTag=='DFLT') or + retain_empty] + self.ScriptCount = len(self.ScriptRecord) + return bool(self.ScriptCount) @_add_method(otTables.ScriptList) def collect_features(self): - return _uniq_sort(sum((s.Script.collect_features() - for s in self.ScriptRecord), [])) + return _uniq_sort(sum((s.Script.collect_features() + for s in self.ScriptRecord), [])) # CBLC will inherit it @_add_method(ttLib.getTableClass('EBLC')) def subset_glyphs(self, s): - for strike in self.strikes: - for indexSubTable in strike.indexSubTables: - indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] - strike.indexSubTables = [i for i in strike.indexSubTables if i.names] - self.strikes = [s for s in self.strikes if s.indexSubTables] + for strike in self.strikes: + for indexSubTable in strike.indexSubTables: + indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] + strike.indexSubTables = [i for i in strike.indexSubTables if i.names] + self.strikes = [s for s in self.strikes if s.indexSubTables] - return True + return True # CBDC will inherit it @_add_method(ttLib.getTableClass('EBDT')) def subset_glyphs(self, s): self.strikeData = [{g: strike[g] for g in s.glyphs if g in strike} - for strike in self.strikeData] + for strike in self.strikeData] return True @_add_method(ttLib.getTableClass('GSUB')) def closure_glyphs(self, s): - s.table = self.table - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if getattr(self.table, 'FeatureVariations', None): - lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) - lookup_indices = _uniq_sort(lookup_indices) - if self.table.LookupList: - while True: - orig_glyphs = frozenset(s.glyphs) - s._activeLookups = [] - s._doneLookups = set() - for i in lookup_indices: - if i >= self.table.LookupList.LookupCount: continue - if not self.table.LookupList.Lookup[i]: continue - self.table.LookupList.Lookup[i].closure_glyphs(s) - del s._activeLookups, s._doneLookups - if orig_glyphs == s.glyphs: - break - del s.table + s.table = self.table + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + s._doneLookups = {} + while True: + orig_glyphs = frozenset(s.glyphs) + for i in lookup_indices: + if i >= self.table.LookupList.LookupCount: continue + if not self.table.LookupList.Lookup[i]: continue + self.table.LookupList.Lookup[i].closure_glyphs(s) + if orig_glyphs == s.glyphs: + break + del s._doneLookups + del s.table @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_glyphs(self, s): - s.glyphs = s.glyphs_gsubed - if self.table.LookupList: - lookup_indices = self.table.LookupList.subset_glyphs(s) - else: - lookup_indices = [] - self.subset_lookups(lookup_indices) - return True + s.glyphs = s.glyphs_gsubed + if self.table.LookupList: + lookup_indices = self.table.LookupList.subset_glyphs(s) + else: + lookup_indices = [] + self.subset_lookups(lookup_indices) + return True @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def retain_empty_scripts(self): - # https://github.com/behdad/fonttools/issues/518 - # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 - return self.__class__ == ttLib.getTableClass('GSUB') + # https://github.com/behdad/fonttools/issues/518 + # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 + return self.__class__ == ttLib.getTableClass('GSUB') @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_lookups(self, lookup_indices): - """Retains specified lookups, then removes empty features, language - systems, and scripts.""" - if self.table.LookupList: - self.table.LookupList.subset_lookups(lookup_indices) - if self.table.FeatureList: - feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) - else: - feature_indices = [] - if getattr(self.table, 'FeatureVariations', None): - feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) - feature_indices = _uniq_sort(feature_indices) - if self.table.FeatureList: - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + """Retains specified lookups, then removes empty features, language + systems, and scripts.""" + if self.table.LookupList: + self.table.LookupList.subset_lookups(lookup_indices) + if self.table.FeatureList: + feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) + else: + feature_indices = [] + if getattr(self.table, 'FeatureVariations', None): + feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) + feature_indices = _uniq_sort(feature_indices) + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def neuter_lookups(self, lookup_indices): - """Sets lookups not in lookup_indices to None.""" - if self.table.LookupList: - self.table.LookupList.neuter_lookups(lookup_indices) + """Sets lookups not in lookup_indices to None.""" + if self.table.LookupList: + self.table.LookupList.neuter_lookups(lookup_indices) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_lookups(self, remap=True): - """Remove (default) or neuter unreferenced lookups""" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if getattr(self.table, 'FeatureVariations', None): - lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) - lookup_indices = _uniq_sort(lookup_indices) - if self.table.LookupList: - lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) - else: - lookup_indices = [] - if remap: - self.subset_lookups(lookup_indices) - else: - self.neuter_lookups(lookup_indices) + """Remove (default) or neuter unreferenced lookups""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) + else: + lookup_indices = [] + if remap: + self.subset_lookups(lookup_indices) + else: + self.neuter_lookups(lookup_indices) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def subset_feature_tags(self, feature_tags): - if self.table.FeatureList: - feature_indices = \ - [i for i,f in enumerate(self.table.FeatureList.FeatureRecord) - if f.FeatureTag in feature_tags] - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - else: - feature_indices = [] - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + if self.table.FeatureList: + feature_indices = \ + [i for i,f in enumerate(self.table.FeatureList.FeatureRecord) + if f.FeatureTag in feature_tags] + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + else: + feature_indices = [] + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_features(self): - """Remove unreferenced features""" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - self.table.FeatureList.subset_features(feature_indices) - if getattr(self.table, 'FeatureVariations', None): - self.table.FeatureVariations.subset_features(feature_indices) - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + """Remove unreferenced features""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def prune_pre_subset(self, font, options): - # Drop undesired features - if '*' not in options.layout_features: - self.subset_feature_tags(options.layout_features) - # Neuter unreferenced lookups - self.prune_lookups(remap=False) - return True + # Drop undesired features + if '*' not in options.layout_features: + self.subset_feature_tags(options.layout_features) + # Neuter unreferenced lookups + self.prune_lookups(remap=False) + return True @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) + ttLib.getTableClass('GPOS')) def remove_redundant_langsys(self): - table = self.table - if not table.ScriptList or not table.FeatureList: - return - - features = table.FeatureList.FeatureRecord - - for s in table.ScriptList.ScriptRecord: - d = s.Script.DefaultLangSys - if not d: - continue - for lr in s.Script.LangSysRecord[:]: - l = lr.LangSys - # Compare d and l - if len(d.FeatureIndex) != len(l.FeatureIndex): - continue - if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): - continue - - if d.ReqFeatureIndex != 65535: - if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: - continue - - for i in range(len(d.FeatureIndex)): - if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: - break - else: - # LangSys and default are equal; delete LangSys - s.Script.LangSysRecord.remove(lr) + table = self.table + if not table.ScriptList or not table.FeatureList: + return + + features = table.FeatureList.FeatureRecord + + for s in table.ScriptList.ScriptRecord: + d = s.Script.DefaultLangSys + if not d: + continue + for lr in s.Script.LangSysRecord[:]: + l = lr.LangSys + # Compare d and l + if len(d.FeatureIndex) != len(l.FeatureIndex): + continue + if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): + continue + + if d.ReqFeatureIndex != 65535: + if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: + continue + + for i in range(len(d.FeatureIndex)): + if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: + break + else: + # LangSys and default are equal; delete LangSys + s.Script.LangSysRecord.remove(lr) @_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_post_subset(self, options): - table = self.table - - self.prune_lookups() # XXX Is this actually needed?! - - if table.LookupList: - table.LookupList.prune_post_subset(options) - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if not table.LookupList.Lookup: - # table.LookupList = None - - if not table.LookupList: - table.FeatureList = None - - - if table.FeatureList: - self.remove_redundant_langsys() - # Remove unreferenced features - self.prune_features() - - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if table.FeatureList and not table.FeatureList.FeatureRecord: - # table.FeatureList = None - - # Never drop scripts themselves as them just being available - # holds semantic significance. - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsets here. - #if table.ScriptList and not table.ScriptList.ScriptRecord: - # table.ScriptList = None - - if not table.FeatureList and hasattr(table, 'FeatureVariations'): - table.FeatureVariations = None - - if hasattr(table, 'FeatureVariations') and not table.FeatureVariations: - if table.Version == 0x00010001: - table.Version = 0x00010000 + ttLib.getTableClass('GPOS')) +def prune_post_subset(self, font, options): + table = self.table + + self.prune_lookups() # XXX Is this actually needed?! + + if table.LookupList: + table.LookupList.prune_post_subset(font, options) + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if not table.LookupList.Lookup: + # table.LookupList = None + + if not table.LookupList: + table.FeatureList = None + + + if table.FeatureList: + self.remove_redundant_langsys() + # Remove unreferenced features + self.prune_features() + + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.FeatureList and not table.FeatureList.FeatureRecord: + # table.FeatureList = None + + # Never drop scripts themselves as them just being available + # holds semantic significance. + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.ScriptList and not table.ScriptList.ScriptRecord: + # table.ScriptList = None + + if not table.FeatureList and hasattr(table, 'FeatureVariations'): + table.FeatureVariations = None + + if hasattr(table, 'FeatureVariations') and not table.FeatureVariations: + if table.Version == 0x00010001: + table.Version = 0x00010000 - return True + return True @_add_method(ttLib.getTableClass('GDEF')) def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - table = self.table - if table.LigCaretList: - indices = table.LigCaretList.Coverage.subset(glyphs) - table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] for i in indices] - table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) - if table.MarkAttachClassDef: - table.MarkAttachClassDef.classDefs = \ - {g:v for g,v in table.MarkAttachClassDef.classDefs.items() - if g in glyphs} - if table.GlyphClassDef: - table.GlyphClassDef.classDefs = \ - {g:v for g,v in table.GlyphClassDef.classDefs.items() - if g in glyphs} - if table.AttachList: - indices = table.AttachList.Coverage.subset(glyphs) - GlyphCount = table.AttachList.GlyphCount - table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] - for i in indices if i < GlyphCount] - table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) - if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: - for coverage in table.MarkGlyphSetsDef.Coverage: - coverage.subset(glyphs) - # TODO: The following is disabled. If enabling, we need to go fixup all - # lookups that use MarkFilteringSet and map their set. - # indices = table.MarkGlyphSetsDef.Coverage = \ - # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] - return True + glyphs = s.glyphs_gsubed + table = self.table + if table.LigCaretList: + indices = table.LigCaretList.Coverage.subset(glyphs) + table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] for i in indices] + table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) + if table.MarkAttachClassDef: + table.MarkAttachClassDef.classDefs = \ + {g:v for g,v in table.MarkAttachClassDef.classDefs.items() + if g in glyphs} + if table.GlyphClassDef: + table.GlyphClassDef.classDefs = \ + {g:v for g,v in table.GlyphClassDef.classDefs.items() + if g in glyphs} + if table.AttachList: + indices = table.AttachList.Coverage.subset(glyphs) + GlyphCount = table.AttachList.GlyphCount + table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] + for i in indices if i < GlyphCount] + table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) + if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: + for coverage in table.MarkGlyphSetsDef.Coverage: + if coverage: + coverage.subset(glyphs) + + # TODO: The following is disabled. If enabling, we need to go fixup all + # lookups that use MarkFilteringSet and map their set. + # indices = table.MarkGlyphSetsDef.Coverage = \ + # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] + # TODO: The following is disabled, as ots doesn't like it. Phew... + # https://github.com/khaledhosny/ots/issues/172 + # table.MarkGlyphSetsDef.Coverage = [c if c.glyphs else None for c in table.MarkGlyphSetsDef.Coverage] + return True + + +def _pruneGDEF(font): + if 'GDEF' not in font: return + gdef = font['GDEF'] + table = gdef.table + if not hasattr(table, 'VarStore'): return + + store = table.VarStore + + usedVarIdxes = set() + + # Collect. + table.collect_device_varidxes(usedVarIdxes) + if 'GPOS' in font: + font['GPOS'].table.collect_device_varidxes(usedVarIdxes) + + # Subset. + varidx_map = store.subset_varidxes(usedVarIdxes) + + # Map. + table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) @_add_method(ttLib.getTableClass('GDEF')) -def prune_post_subset(self, options): - table = self.table - # XXX check these against OTS - if table.LigCaretList and not table.LigCaretList.LigGlyphCount: - table.LigCaretList = None - if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: - table.MarkAttachClassDef = None - if table.GlyphClassDef and not table.GlyphClassDef.classDefs: - table.GlyphClassDef = None - if table.AttachList and not table.AttachList.GlyphCount: - table.AttachList = None - if (hasattr(table, "MarkGlyphSetsDef") and - table.MarkGlyphSetsDef and - not table.MarkGlyphSetsDef.Coverage): - table.MarkGlyphSetsDef = None - if table.Version == 0x00010002: - table.Version = 0x00010000 - return bool(table.LigCaretList or - table.MarkAttachClassDef or - table.GlyphClassDef or - table.AttachList or - (table.Version >= 0x00010002 and table.MarkGlyphSetsDef)) +def prune_post_subset(self, font, options): + table = self.table + # XXX check these against OTS + if table.LigCaretList and not table.LigCaretList.LigGlyphCount: + table.LigCaretList = None + if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: + table.MarkAttachClassDef = None + if table.GlyphClassDef and not table.GlyphClassDef.classDefs: + table.GlyphClassDef = None + if table.AttachList and not table.AttachList.GlyphCount: + table.AttachList = None + if hasattr(table, "VarStore"): + _pruneGDEF(font) + if table.VarStore.VarDataCount == 0: + if table.Version == 0x00010003: + table.Version = 0x00010002 + if (not hasattr(table, "MarkGlyphSetsDef") or + not table.MarkGlyphSetsDef or + not table.MarkGlyphSetsDef.Coverage): + table.MarkGlyphSetsDef = None + if table.Version == 0x00010002: + table.Version = 0x00010000 + return bool(table.LigCaretList or + table.MarkAttachClassDef or + table.GlyphClassDef or + table.AttachList or + (table.Version >= 0x00010002 and table.MarkGlyphSetsDef) or + (table.Version >= 0x00010003 and table.VarStore)) @_add_method(ttLib.getTableClass('kern')) def prune_pre_subset(self, font, options): - # Prune unknown kern table types - self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] - return bool(self.kernTables) + # Prune unknown kern table types + self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] + return bool(self.kernTables) @_add_method(ttLib.getTableClass('kern')) def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - for t in self.kernTables: - t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items() - if a in glyphs and b in glyphs} - self.kernTables = [t for t in self.kernTables if t.kernTable] - return bool(self.kernTables) + glyphs = s.glyphs_gsubed + for t in self.kernTables: + t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items() + if a in glyphs and b in glyphs} + self.kernTables = [t for t in self.kernTables if t.kernTable] + return bool(self.kernTables) @_add_method(ttLib.getTableClass('vmtx')) def subset_glyphs(self, s): - self.metrics = _dict_subset(self.metrics, s.glyphs) - return bool(self.metrics) + self.metrics = _dict_subset(self.metrics, s.glyphs) + return bool(self.metrics) @_add_method(ttLib.getTableClass('hmtx')) def subset_glyphs(self, s): - self.metrics = _dict_subset(self.metrics, s.glyphs) - return True # Required table + self.metrics = _dict_subset(self.metrics, s.glyphs) + return True # Required table @_add_method(ttLib.getTableClass('hdmx')) def subset_glyphs(self, s): - self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()} - return bool(self.hdmx) + self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()} + return bool(self.hdmx) @_add_method(ttLib.getTableClass('ankr')) def subset_glyphs(self, s): - table = self.table.AnchorPoints - assert table.Format == 0, "unknown 'ankr' format %s" % table.Format - table.Anchors = {glyph: table.Anchors[glyph] for glyph in s.glyphs - if glyph in table.Anchors} - return len(table.Anchors) > 0 + table = self.table.AnchorPoints + assert table.Format == 0, "unknown 'ankr' format %s" % table.Format + table.Anchors = {glyph: table.Anchors[glyph] for glyph in s.glyphs + if glyph in table.Anchors} + return len(table.Anchors) > 0 @_add_method(ttLib.getTableClass('bsln')) def closure_glyphs(self, s): - table = self.table.Baseline - if table.Format in (2, 3): - s.glyphs.add(table.StandardGlyph) + table = self.table.Baseline + if table.Format in (2, 3): + s.glyphs.add(table.StandardGlyph) @_add_method(ttLib.getTableClass('bsln')) def subset_glyphs(self, s): - table = self.table.Baseline - if table.Format in (1, 3): - baselines = {glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) - for glyph in s.glyphs} - if len(baselines) > 0: - mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] - table.DefaultBaseline = mostCommon - baselines = {glyph: b for glyph, b in baselines.items() - if b != mostCommon} - if len(baselines) > 0: - table.BaselineValues = baselines - else: - table.Format = {1: 0, 3: 2}[table.Format] - del table.BaselineValues - return True + table = self.table.Baseline + if table.Format in (1, 3): + baselines = {glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) + for glyph in s.glyphs} + if len(baselines) > 0: + mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] + table.DefaultBaseline = mostCommon + baselines = {glyph: b for glyph, b in baselines.items() + if b != mostCommon} + if len(baselines) > 0: + table.BaselineValues = baselines + else: + table.Format = {1: 0, 3: 2}[table.Format] + del table.BaselineValues + return True @_add_method(ttLib.getTableClass('lcar')) def subset_glyphs(self, s): - table = self.table.LigatureCarets - if table.Format in (0, 1): - table.Carets = {glyph: table.Carets[glyph] for glyph in s.glyphs - if glyph in table.Carets} - return len(table.Carets) > 0 - else: - assert False, "unknown 'lcar' format %s" % table.Format + table = self.table.LigatureCarets + if table.Format in (0, 1): + table.Carets = {glyph: table.Carets[glyph] for glyph in s.glyphs + if glyph in table.Carets} + return len(table.Carets) > 0 + else: + assert False, "unknown 'lcar' format %s" % table.Format @_add_method(ttLib.getTableClass('gvar')) def prune_pre_subset(self, font, options): - if options.notdef_glyph and not options.notdef_outline: - self.variations[font.glyphOrder[0]] = [] - return True + if options.notdef_glyph and not options.notdef_outline: + self.variations[font.glyphOrder[0]] = [] + return True @_add_method(ttLib.getTableClass('gvar')) def subset_glyphs(self, s): - self.variations = _dict_subset(self.variations, s.glyphs) - self.glyphCount = len(self.variations) - return bool(self.variations) + self.variations = _dict_subset(self.variations, s.glyphs) + self.glyphCount = len(self.variations) + return bool(self.variations) + +@_add_method(ttLib.getTableClass('HVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs) + used.update(table.AdvWidthMap.mapping.values()) + else: + assert table.LsbMap is None and table.RsbMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + + if table.LsbMap: + table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs) + used.update(table.LsbMap.mapping.values()) + if table.RsbMap: + table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs) + used.update(table.RsbMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = {k:varidx_map[v] for k,v in table.AdvWidthMap.mapping.items()} + if table.LsbMap: + table.LsbMap.mapping = {k:varidx_map[v] for k,v in table.LsbMap.mapping.items()} + if table.RsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True + +@_add_method(ttLib.getTableClass('VVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs) + used.update(table.AdvHeightMap.mapping.values()) + else: + assert table.TsbMap is None and table.BsbMap is None and table.VOrgMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + if table.TsbMap: + table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs) + used.update(table.TsbMap.mapping.values()) + if table.BsbMap: + table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs) + used.update(table.BsbMap.mapping.values()) + if table.VOrgMap: + table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs) + used.update(table.VOrgMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = {k:varidx_map[v] for k,v in table.AdvHeightMap.mapping.items()} + if table.TsbMap: + table.TsbMap.mapping = {k:varidx_map[v] for k,v in table.TsbMap.mapping.items()} + if table.BsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + if table.VOrgMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True @_add_method(ttLib.getTableClass('VORG')) def subset_glyphs(self, s): - self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() - if g in s.glyphs} - self.numVertOriginYMetrics = len(self.VOriginRecords) - return True # Never drop; has default metrics + self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() + if g in s.glyphs} + self.numVertOriginYMetrics = len(self.VOriginRecords) + return True # Never drop; has default metrics @_add_method(ttLib.getTableClass('opbd')) def subset_glyphs(self, s): - table = self.table.OpticalBounds - if table.Format == 0: - table.OpticalBoundsDeltas = {glyph: table.OpticalBoundsDeltas[glyph] - for glyph in s.glyphs - if glyph in table.OpticalBoundsDeltas} - return len(table.OpticalBoundsDeltas) > 0 - elif table.Format == 1: - table.OpticalBoundsPoints = {glyph: table.OpticalBoundsPoints[glyph] - for glyph in s.glyphs - if glyph in table.OpticalBoundsPoints} - return len(table.OpticalBoundsPoints) > 0 - else: - assert False, "unknown 'opbd' format %s" % table.Format + table = self.table.OpticalBounds + if table.Format == 0: + table.OpticalBoundsDeltas = {glyph: table.OpticalBoundsDeltas[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsDeltas} + return len(table.OpticalBoundsDeltas) > 0 + elif table.Format == 1: + table.OpticalBoundsPoints = {glyph: table.OpticalBoundsPoints[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsPoints} + return len(table.OpticalBoundsPoints) > 0 + else: + assert False, "unknown 'opbd' format %s" % table.Format @_add_method(ttLib.getTableClass('post')) def prune_pre_subset(self, font, options): - if not options.glyph_names: - self.formatType = 3.0 - return True # Required table + if not options.glyph_names: + self.formatType = 3.0 + return True # Required table @_add_method(ttLib.getTableClass('post')) def subset_glyphs(self, s): - self.extraNames = [] # This seems to do it - return True # Required table + self.extraNames = [] # This seems to do it + return True # Required table @_add_method(ttLib.getTableClass('prop')) def subset_glyphs(self, s): - prop = self.table.GlyphProperties - if prop.Format == 0: - return prop.DefaultProperties != 0 - elif prop.Format == 1: - prop.Properties = {g: prop.Properties.get(g, prop.DefaultProperties) - for g in s.glyphs} - mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] - prop.DefaultProperties = mostCommon - prop.Properties = {g: prop for g, prop in prop.Properties.items() - if prop != mostCommon} - if len(prop.Properties) == 0: - del prop.Properties - prop.Format = 0 - return prop.DefaultProperties != 0 - return True - else: - assert False, "unknown 'prop' format %s" % prop.Format + prop = self.table.GlyphProperties + if prop.Format == 0: + return prop.DefaultProperties != 0 + elif prop.Format == 1: + prop.Properties = {g: prop.Properties.get(g, prop.DefaultProperties) + for g in s.glyphs} + mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] + prop.DefaultProperties = mostCommon + prop.Properties = {g: prop for g, prop in prop.Properties.items() + if prop != mostCommon} + if len(prop.Properties) == 0: + del prop.Properties + prop.Format = 0 + return prop.DefaultProperties != 0 + return True + else: + assert False, "unknown 'prop' format %s" % prop.Format @_add_method(ttLib.getTableClass('COLR')) def closure_glyphs(self, s): - decompose = s.glyphs - while decompose: - layers = set() - for g in decompose: - for l in self.ColorLayers.get(g, []): - layers.add(l.name) - layers -= s.glyphs - s.glyphs.update(layers) - decompose = layers + decompose = s.glyphs + while decompose: + layers = set() + for g in decompose: + for l in self.ColorLayers.get(g, []): + layers.add(l.name) + layers -= s.glyphs + s.glyphs.update(layers) + decompose = layers @_add_method(ttLib.getTableClass('COLR')) def subset_glyphs(self, s): - self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers} - return bool(self.ColorLayers) + self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers} + return bool(self.ColorLayers) # TODO: prune unused palettes @_add_method(ttLib.getTableClass('CPAL')) -def prune_post_subset(self, options): - return True +def prune_post_subset(self, font, options): + return True @_add_method(otTables.MathGlyphConstruction) def closure_glyphs(self, glyphs): - variants = set() - for v in self.MathGlyphVariantRecord: - variants.add(v.VariantGlyph) - if self.GlyphAssembly: - for p in self.GlyphAssembly.PartRecords: - variants.add(p.glyph) - return variants + variants = set() + for v in self.MathGlyphVariantRecord: + variants.add(v.VariantGlyph) + if self.GlyphAssembly: + for p in self.GlyphAssembly.PartRecords: + variants.add(p.glyph) + return variants @_add_method(otTables.MathVariants) def closure_glyphs(self, s): - glyphs = frozenset(s.glyphs) - variants = set() + glyphs = frozenset(s.glyphs) + variants = set() - if self.VertGlyphCoverage: - indices = self.VertGlyphCoverage.intersect(glyphs) - for i in indices: - variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) - - if self.HorizGlyphCoverage: - indices = self.HorizGlyphCoverage.intersect(glyphs) - for i in indices: - variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) - s.glyphs.update(variants) + s.glyphs.update(variants) @_add_method(ttLib.getTableClass('MATH')) def closure_glyphs(self, s): - self.table.MathVariants.closure_glyphs(s) + self.table.MathVariants.closure_glyphs(s) @_add_method(otTables.MathItalicsCorrectionInfo) def subset_glyphs(self, s): - indices = self.Coverage.subset(s.glyphs) - self.ItalicsCorrection = [self.ItalicsCorrection[i] for i in indices] - self.ItalicsCorrectionCount = len(self.ItalicsCorrection) - return bool(self.ItalicsCorrectionCount) + indices = self.Coverage.subset(s.glyphs) + self.ItalicsCorrection = [self.ItalicsCorrection[i] for i in indices] + self.ItalicsCorrectionCount = len(self.ItalicsCorrection) + return bool(self.ItalicsCorrectionCount) @_add_method(otTables.MathTopAccentAttachment) def subset_glyphs(self, s): - indices = self.TopAccentCoverage.subset(s.glyphs) - self.TopAccentAttachment = [self.TopAccentAttachment[i] for i in indices] - self.TopAccentAttachmentCount = len(self.TopAccentAttachment) - return bool(self.TopAccentAttachmentCount) + indices = self.TopAccentCoverage.subset(s.glyphs) + self.TopAccentAttachment = [self.TopAccentAttachment[i] for i in indices] + self.TopAccentAttachmentCount = len(self.TopAccentAttachment) + return bool(self.TopAccentAttachmentCount) @_add_method(otTables.MathKernInfo) def subset_glyphs(self, s): - indices = self.MathKernCoverage.subset(s.glyphs) - self.MathKernInfoRecords = [self.MathKernInfoRecords[i] for i in indices] - self.MathKernCount = len(self.MathKernInfoRecords) - return bool(self.MathKernCount) + indices = self.MathKernCoverage.subset(s.glyphs) + self.MathKernInfoRecords = [self.MathKernInfoRecords[i] for i in indices] + self.MathKernCount = len(self.MathKernInfoRecords) + return bool(self.MathKernCount) @_add_method(otTables.MathGlyphInfo) def subset_glyphs(self, s): - if self.MathItalicsCorrectionInfo: - self.MathItalicsCorrectionInfo.subset_glyphs(s) - if self.MathTopAccentAttachment: - self.MathTopAccentAttachment.subset_glyphs(s) - if self.MathKernInfo: - self.MathKernInfo.subset_glyphs(s) - if self.ExtendedShapeCoverage: - self.ExtendedShapeCoverage.subset(s.glyphs) - return True + if self.MathItalicsCorrectionInfo: + self.MathItalicsCorrectionInfo.subset_glyphs(s) + if self.MathTopAccentAttachment: + self.MathTopAccentAttachment.subset_glyphs(s) + if self.MathKernInfo: + self.MathKernInfo.subset_glyphs(s) + if self.ExtendedShapeCoverage: + self.ExtendedShapeCoverage.subset(s.glyphs) + return True @_add_method(otTables.MathVariants) def subset_glyphs(self, s): - if self.VertGlyphCoverage: - indices = self.VertGlyphCoverage.subset(s.glyphs) - self.VertGlyphConstruction = [self.VertGlyphConstruction[i] for i in indices] - self.VertGlyphCount = len(self.VertGlyphConstruction) - - if self.HorizGlyphCoverage: - indices = self.HorizGlyphCoverage.subset(s.glyphs) - self.HorizGlyphConstruction = [self.HorizGlyphConstruction[i] for i in indices] - self.HorizGlyphCount = len(self.HorizGlyphConstruction) + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.subset(s.glyphs) + self.VertGlyphConstruction = [self.VertGlyphConstruction[i] for i in indices] + self.VertGlyphCount = len(self.VertGlyphConstruction) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.subset(s.glyphs) + self.HorizGlyphConstruction = [self.HorizGlyphConstruction[i] for i in indices] + self.HorizGlyphCount = len(self.HorizGlyphConstruction) - return True + return True @_add_method(ttLib.getTableClass('MATH')) def subset_glyphs(self, s): - s.glyphs = s.glyphs_mathed - self.table.MathGlyphInfo.subset_glyphs(s) - self.table.MathVariants.subset_glyphs(s) - return True + s.glyphs = s.glyphs_mathed + self.table.MathGlyphInfo.subset_glyphs(s) + self.table.MathVariants.subset_glyphs(s) + return True @_add_method(ttLib.getTableModule('glyf').Glyph) def remapComponentsFast(self, indices): - if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: - return # Not composite - data = array.array("B", self.data) - i = 10 - more = 1 - while more: - flags =(data[i] << 8) | data[i+1] - glyphID =(data[i+2] << 8) | data[i+3] - # Remap - glyphID = indices.index(glyphID) - data[i+2] = glyphID >> 8 - data[i+3] = glyphID & 0xFF - i += 4 - flags = int(flags) - - if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS - else: i += 2 - if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE - elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE - elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO - more = flags & 0x0020 # MORE_COMPONENTS + if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: + return # Not composite + data = array.array("B", self.data) + i = 10 + more = 1 + while more: + flags =(data[i] << 8) | data[i+1] + glyphID =(data[i+2] << 8) | data[i+3] + # Remap + glyphID = indices.index(glyphID) + data[i+2] = glyphID >> 8 + data[i+3] = glyphID & 0xFF + i += 4 + flags = int(flags) + + if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS + else: i += 2 + if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE + elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE + elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO + more = flags & 0x0020 # MORE_COMPONENTS - self.data = data.tostring() + self.data = data.tostring() @_add_method(ttLib.getTableClass('glyf')) def closure_glyphs(self, s): - decompose = s.glyphs - while decompose: - components = set() - for g in decompose: - if g not in self.glyphs: - continue - gl = self.glyphs[g] - for c in gl.getComponentNames(self): - components.add(c) - components -= s.glyphs - s.glyphs.update(components) - decompose = components + glyphSet = self.glyphs + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + for c in gl.getComponentNames(self): + components.add(c) + components -= s.glyphs + s.glyphs.update(components) + decompose = components @_add_method(ttLib.getTableClass('glyf')) def prune_pre_subset(self, font, options): - if options.notdef_glyph and not options.notdef_outline: - g = self[self.glyphOrder[0]] - # Yay, easy! - g.__dict__.clear() - g.data = "" - return True + if options.notdef_glyph and not options.notdef_outline: + g = self[self.glyphOrder[0]] + # Yay, easy! + g.__dict__.clear() + g.data = "" + return True @_add_method(ttLib.getTableClass('glyf')) def subset_glyphs(self, s): - self.glyphs = _dict_subset(self.glyphs, s.glyphs) - indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] - for v in self.glyphs.values(): - if hasattr(v, "data"): - v.remapComponentsFast(indices) - else: - pass # No need - self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] - # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. - return True + self.glyphs = _dict_subset(self.glyphs, s.glyphs) + indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] + for v in self.glyphs.values(): + if hasattr(v, "data"): + v.remapComponentsFast(indices) + else: + pass # No need + self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] + # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. + return True @_add_method(ttLib.getTableClass('glyf')) -def prune_post_subset(self, options): - remove_hinting = not options.hinting - for v in self.glyphs.values(): - v.trim(remove_hinting=remove_hinting) - return True +def prune_post_subset(self, font, options): + remove_hinting = not options.hinting + for v in self.glyphs.values(): + v.trim(remove_hinting=remove_hinting) + return True + + +class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, components, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + self.components = components + + def op_endchar(self, index): + args = self.popall() + if len(args) >= 4: + from fontTools.encodings.StandardEncoding import StandardEncoding + # endchar can do seac accent bulding; The T2 spec says it's deprecated, + # but recent software that shall remain nameless does output it. + adx, ady, bchar, achar = args[-4:] + baseGlyph = StandardEncoding[bchar] + accentGlyph = StandardEncoding[achar] + self.components.add(baseGlyph) + self.components.add(accentGlyph) + +@_add_method(ttLib.getTableClass('CFF ')) +def closure_glyphs(self, s): + cff = self.cff + assert len(cff) == 1 + font = cff[cff.keys()[0]] + glyphSet = font.CharStrings + + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + + subrs = getattr(gl.private, "Subrs", []) + decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) + decompiler.execute(gl) + components -= s.glyphs + s.glyphs.update(components) + decompose = components @_add_method(ttLib.getTableClass('CFF ')) def prune_pre_subset(self, font, options): - cff = self.cff - # CFF table must have one font only - cff.fontNames = cff.fontNames[:1] - - if options.notdef_glyph and not options.notdef_outline: - for fontname in cff.keys(): - font = cff[fontname] - c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') - if hasattr(font, 'FDArray') and font.FDArray is not None: - private = font.FDArray[fdSelectIndex].Private - else: - private = font.Private - dfltWdX = private.defaultWidthX - nmnlWdX = private.nominalWidthX - pen = NullPen() - c.draw(pen) # this will set the charstring's width - if c.width != dfltWdX: - c.program = [c.width - nmnlWdX, 'endchar'] - else: - c.program = ['endchar'] - - # Clear useless Encoding - for fontname in cff.keys(): - font = cff[fontname] - # https://github.com/behdad/fonttools/issues/620 - font.Encoding = "StandardEncoding" + cff = self.cff + # CFF table must have one font only + cff.fontNames = cff.fontNames[:1] + + if options.notdef_glyph and not options.notdef_outline: + for fontname in cff.keys(): + font = cff[fontname] + c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') + if hasattr(font, 'FDArray') and font.FDArray is not None: + private = font.FDArray[fdSelectIndex].Private + else: + private = font.Private + dfltWdX = private.defaultWidthX + nmnlWdX = private.nominalWidthX + pen = NullPen() + c.draw(pen) # this will set the charstring's width + if c.width != dfltWdX: + c.program = [c.width - nmnlWdX, 'endchar'] + else: + c.program = ['endchar'] + + # Clear useless Encoding + for fontname in cff.keys(): + font = cff[fontname] + # https://github.com/behdad/fonttools/issues/620 + font.Encoding = "StandardEncoding" - return True # bool(cff.fontNames) + return True # bool(cff.fontNames) @_add_method(ttLib.getTableClass('CFF ')) def subset_glyphs(self, s): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Load all glyphs - for g in font.charset: - if g not in s.glyphs: continue - c, _ = cs.getItemAndSelector(g) - - if cs.charStringsAreIndexed: - indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] - csi = cs.charStringsIndex - csi.items = [csi.items[i] for i in indices] - del csi.file, csi.offsets - if hasattr(font, "FDSelect"): - sel = font.FDSelect - # XXX We want to set sel.format to None, such that the - # most compact format is selected. However, OTS was - # broken and couldn't parse a FDSelect format 0 that - # happened before CharStrings. As such, always force - # format 3 until we fix cffLib to always generate - # FDSelect after CharStrings. - # https://github.com/khaledhosny/ots/pull/31 - #sel.format = None - sel.format = 3 - sel.gidArray = [sel.gidArray[i] for i in indices] - cs.charStrings = {g:indices.index(v) - for g,v in cs.charStrings.items() - if g in s.glyphs} - else: - cs.charStrings = {g:v - for g,v in cs.charStrings.items() - if g in s.glyphs} - font.charset = [g for g in font.charset if g in s.glyphs] - font.numGlyphs = len(font.charset) + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Load all glyphs + for g in font.charset: + if g not in s.glyphs: continue + c, _ = cs.getItemAndSelector(g) + + if cs.charStringsAreIndexed: + indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] + csi = cs.charStringsIndex + csi.items = [csi.items[i] for i in indices] + del csi.file, csi.offsets + if hasattr(font, "FDSelect"): + sel = font.FDSelect + # XXX We want to set sel.format to None, such that the + # most compact format is selected. However, OTS was + # broken and couldn't parse a FDSelect format 0 that + # happened before CharStrings. As such, always force + # format 3 until we fix cffLib to always generate + # FDSelect after CharStrings. + # https://github.com/khaledhosny/ots/pull/31 + #sel.format = None + sel.format = 3 + sel.gidArray = [sel.gidArray[i] for i in indices] + cs.charStrings = {g:indices.index(v) + for g,v in cs.charStrings.items() + if g in s.glyphs} + else: + cs.charStrings = {g:v + for g,v in cs.charStrings.items() + if g in s.glyphs} + font.charset = [g for g in font.charset if g in s.glyphs] + font.numGlyphs = len(font.charset) - return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) + return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) @_add_method(psCharStrings.T2CharString) def subset_subroutines(self, subrs, gsubrs): - p = self.program - assert len(p) - for i in range(1, len(p)): - if p[i] == 'callsubr': - assert isinstance(p[i-1], int) - p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias - elif p[i] == 'callgsubr': - assert isinstance(p[i-1], int) - p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias + p = self.program + assert len(p) + for i in range(1, len(p)): + if p[i] == 'callsubr': + assert isinstance(p[i-1], int) + p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias + elif p[i] == 'callgsubr': + assert isinstance(p[i-1], int) + p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias @_add_method(psCharStrings.T2CharString) def drop_hints(self): - hints = self._hints + hints = self._hints - if hints.deletions: - p = self.program - for idx in reversed(hints.deletions): - del p[idx-2:idx] - - if hints.has_hint: - assert not hints.deletions or hints.last_hint <= hints.deletions[0] - self.program = self.program[hints.last_hint:] - if hasattr(self, 'width'): - # Insert width back if needed - if self.width != self.private.defaultWidthX: - self.program.insert(0, self.width - self.private.nominalWidthX) - - if hints.has_hintmask: - i = 0 - p = self.program - while i < len(p): - if p[i] in ['hintmask', 'cntrmask']: - assert i + 1 <= len(p) - del p[i:i+2] - continue - i += 1 + if hints.deletions: + p = self.program + for idx in reversed(hints.deletions): + del p[idx-2:idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + self.program = self.program[hints.last_hint:] + if hasattr(self, 'width'): + # Insert width back if needed + if self.width != self.private.defaultWidthX: + self.program.insert(0, self.width - self.private.nominalWidthX) + + if hints.has_hintmask: + i = 0 + p = self.program + while i < len(p): + if p[i] in ['hintmask', 'cntrmask']: + assert i + 1 <= len(p) + del p[i:i+2] + continue + i += 1 - assert len(self.program) + assert len(self.program) - del self._hints + del self._hints class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - for subrs in [localSubrs, globalSubrs]: - if subrs and not hasattr(subrs, "_used"): - subrs._used = set() - - def op_callsubr(self, index): - self.localSubrs._used.add(self.operandStack[-1]+self.localBias) - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - - def op_callgsubr(self, index): - self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1]+self.localBias) + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): - class Hints(object): - def __init__(self): - # Whether calling this charstring produces any hint stems - # Note that if a charstring starts with hintmask, it will - # have has_hint set to True, because it *might* produce an - # implicit vstem if called under certain conditions. - self.has_hint = False - # Index to start at to drop all hints - self.last_hint = 0 - # Index up to which we know more hints are possible. - # Only relevant if status is 0 or 1. - self.last_checked = 0 - # The status means: - # 0: after dropping hints, this charstring is empty - # 1: after dropping hints, there may be more hints - # continuing after this - # 2: no more hints possible after this charstring - self.status = 0 - # Has hintmask instructions; not recursive - self.has_hintmask = False - # List of indices of calls to empty subroutines to remove. - self.deletions = [] - pass - - def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): - self._css = css - psCharStrings.T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) - - def execute(self, charString): - old_hints = charString._hints if hasattr(charString, '_hints') else None - charString._hints = self.Hints() - - psCharStrings.T2WidthExtractor.execute(self, charString) - - hints = charString._hints - - if hints.has_hint or hints.has_hintmask: - self._css.add(charString) - - if hints.status != 2: - # Check from last_check, make sure we didn't have any operators. - for i in range(hints.last_checked, len(charString.program) - 1): - if isinstance(charString.program[i], str): - hints.status = 2 - break - else: - hints.status = 1 # There's *something* here - hints.last_checked = len(charString.program) - - if old_hints: - assert hints.__dict__ == old_hints.__dict__ - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.T2WidthExtractor.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.T2WidthExtractor.op_callgsubr(self, index) - self.processSubr(index, subr) - - def op_hstem(self, index): - psCharStrings.T2WidthExtractor.op_hstem(self, index) - self.processHint(index) - def op_vstem(self, index): - psCharStrings.T2WidthExtractor.op_vstem(self, index) - self.processHint(index) - def op_hstemhm(self, index): - psCharStrings.T2WidthExtractor.op_hstemhm(self, index) - self.processHint(index) - def op_vstemhm(self, index): - psCharStrings.T2WidthExtractor.op_vstemhm(self, index) - self.processHint(index) - def op_hintmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) - self.processHintmask(index) - return rv - def op_cntrmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) - self.processHintmask(index) - return rv - - def processHintmask(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hintmask = True - if hints.status != 2: - # Check from last_check, see if we may be an implicit vstem - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - else: - # We are an implicit vstem - hints.has_hint = True - hints.last_hint = index + 1 - hints.status = 0 - hints.last_checked = index + 1 - - def processHint(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hint = True - hints.last_hint = index - hints.last_checked = index - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - hints = cs._hints - subr_hints = subr._hints - - # Check from last_check, make sure we didn't have - # any operators. - if hints.status != 2: - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - hints.last_checked = index - - if hints.status != 2: - if subr_hints.has_hint: - hints.has_hint = True - - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - elif subr_hints.status == 0: - hints.deletions.append(index) + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + pass + + def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + self._css = css + psCharStrings.T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, '_hints') else None + charString._hints = self.Hints() + + psCharStrings.T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + psCharStrings.T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + def op_vstem(self, index): + psCharStrings.T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + def op_hstemhm(self, index): + psCharStrings.T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + def op_vstemhm(self, index): + psCharStrings.T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + def op_hintmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + def op_cntrmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + elif subr_hints.status == 0: + hints.deletions.append(index) - hints.status = max(hints.status, subr_hints.status) + hints.status = max(hints.status, subr_hints.status) class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - - def execute(self, charString): - # Note: Currently we recompute _desubroutinized each time. - # This is more robust in some cases, but in other places we assume - # that each subroutine always expands to the same code, so - # maybe it doesn't matter. To speed up we can just not - # recompute _desubroutinized if it's there. For now I just - # double-check that it desubroutinized to the same thing. - old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None - - charString._patches = [] - psCharStrings.SimpleT2Decompiler.execute(self, charString) - desubroutinized = charString.program[:] - for idx,expansion in reversed (charString._patches): - assert idx >= 2 - assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] - assert type(desubroutinized[idx - 2]) == int - if expansion[-1] == 'return': - expansion = expansion[:-1] - desubroutinized[idx-2:idx] = expansion - if 'endchar' in desubroutinized: - # Cut off after first endchar - desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] - else: - if not len(desubroutinized) or desubroutinized[-1] != 'return': - desubroutinized.append('return') - - charString._desubroutinized = desubroutinized - del charString._patches - - if old_desubroutinized: - assert desubroutinized == old_desubroutinized - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - self.processSubr(index, subr) - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - cs._patches.append((index, subr._desubroutinized)) + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + + def execute(self, charString): + # Note: Currently we recompute _desubroutinized each time. + # This is more robust in some cases, but in other places we assume + # that each subroutine always expands to the same code, so + # maybe it doesn't matter. To speed up we can just not + # recompute _desubroutinized if it's there. For now I just + # double-check that it desubroutinized to the same thing. + old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None + + charString._patches = [] + psCharStrings.SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx,expansion in reversed (charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == 'return': + expansion = expansion[:-1] + desubroutinized[idx-2:idx] = expansion + if 'endchar' in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] + else: + if not len(desubroutinized) or desubroutinized[-1] != 'return': + desubroutinized.append('return') + + charString._desubroutinized = desubroutinized + del charString._patches + + if old_desubroutinized: + assert desubroutinized == old_desubroutinized + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + cs._patches.append((index, subr._desubroutinized)) @_add_method(ttLib.getTableClass('CFF ')) -def prune_post_subset(self, options): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Drop unused FontDictionaries - if hasattr(font, "FDSelect"): - sel = font.FDSelect - indices = _uniq_sort(sel.gidArray) - sel.gidArray = [indices.index (ss) for ss in sel.gidArray] - arr = font.FDArray - arr.items = [arr[i] for i in indices] - del arr.file, arr.offsets - - # Desubroutinize if asked for - if options.desubroutinize: - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - c.program = c._desubroutinized - - # Drop hints if not needed - if not options.hinting: - - # This can be tricky, but doesn't have to. What we do is: - # - # - Run all used glyph charstrings and recurse into subroutines, - # - For each charstring (including subroutines), if it has any - # of the hint stem operators, we mark it as such. - # Upon returning, for each charstring we note all the - # subroutine calls it makes that (recursively) contain a stem, - # - Dropping hinting then consists of the following two ops: - # * Drop the piece of the program in each charstring before the - # last call to a stem op or a stem-calling subroutine, - # * Drop all hintmask operations. - # - It's trickier... A hintmask right after hints and a few numbers - # will act as an implicit vstemhm. As such, we track whether - # we have seen any non-hint operators so far and do the right - # thing, recursively... Good luck understanding that :( - css = set() - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, - c.private.nominalWidthX, - c.private.defaultWidthX) - decompiler.execute(c) - c.width = decompiler.width - for charstring in css: - charstring.drop_hints() - del css - - # Drop font-wide hinting values - all_privs = [] - if hasattr(font, 'FDSelect'): - all_privs.extend(fd.Private for fd in font.FDArray) - else: - all_privs.append(font.Private) - for priv in all_privs: - for k in ['BlueValues', 'OtherBlues', - 'FamilyBlues', 'FamilyOtherBlues', - 'BlueScale', 'BlueShift', 'BlueFuzz', - 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: - if hasattr(priv, k): - setattr(priv, k, None) - - # Renumber subroutines to remove unused ones - - # Mark all used subroutines - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - - all_subrs = [font.GlobalSubrs] - if hasattr(font, 'FDSelect'): - all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) - elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: - all_subrs.append(font.Private.Subrs) - - subrs = set(subrs) # Remove duplicates - - # Prepare - for subrs in all_subrs: - if not hasattr(subrs, '_used'): - subrs._used = set() - subrs._used = _uniq_sort(subrs._used) - subrs._old_bias = psCharStrings.calcSubrBias(subrs) - subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) - - # Renumber glyph charstrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - c.subset_subroutines (subrs, font.GlobalSubrs) - - # Renumber subroutines themselves - for subrs in all_subrs: - if subrs == font.GlobalSubrs: - if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): - local_subrs = font.Private.Subrs - else: - local_subrs = [] - else: - local_subrs = subrs - - subrs.items = [subrs.items[i] for i in subrs._used] - if hasattr(subrs, 'file'): - del subrs.file - if hasattr(subrs, 'offsets'): - del subrs.offsets - - for subr in subrs.items: - subr.subset_subroutines (local_subrs, font.GlobalSubrs) - - # Delete local SubrsIndex if empty - if hasattr(font, 'FDSelect'): - for fd in font.FDArray: - _delete_empty_subrs(fd.Private) - else: - _delete_empty_subrs(font.Private) - - # Cleanup - for subrs in all_subrs: - del subrs._used, subrs._old_bias, subrs._new_bias +def prune_post_subset(self, font, options): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Drop unused FontDictionaries + if hasattr(font, "FDSelect"): + sel = font.FDSelect + indices = _uniq_sort(sel.gidArray) + sel.gidArray = [indices.index (ss) for ss in sel.gidArray] + arr = font.FDArray + arr.items = [arr[i] for i in indices] + del arr.file, arr.offsets + + # Desubroutinize if asked for + if options.desubroutinize: + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + c.program = c._desubroutinized + + # Drop hints if not needed + if not options.hinting: + + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + charstring.drop_hints() + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, 'FDSelect'): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in ['BlueValues', 'OtherBlues', + 'FamilyBlues', 'FamilyOtherBlues', + 'BlueScale', 'BlueShift', 'BlueFuzz', + 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: + if hasattr(priv, k): + setattr(priv, k, None) + + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, 'FDSelect'): + all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) + elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, '_used'): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = psCharStrings.calcSubrBias(subrs) + subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + c.subset_subroutines (subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): + local_subrs = font.Private.Subrs + else: + local_subrs = [] + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, 'file'): + del subrs.file + if hasattr(subrs, 'offsets'): + del subrs.offsets + + for subr in subrs.items: + subr.subset_subroutines (local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, 'FDSelect'): + for fd in font.FDArray: + _delete_empty_subrs(fd.Private) + else: + _delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias - return True + return True def _delete_empty_subrs(private_dict): - if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: - if 'Subrs' in private_dict.rawDict: - del private_dict.rawDict['Subrs'] - del private_dict.Subrs + if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: + if 'Subrs' in private_dict.rawDict: + del private_dict.rawDict['Subrs'] + del private_dict.Subrs @_add_method(ttLib.getTableClass('cmap')) def closure_glyphs(self, s): - tables = [t for t in self.tables if t.isUnicode()] + tables = [t for t in self.tables if t.isUnicode()] - # Close glyphs - for table in tables: - if table.format == 14: - for cmap in table.uvsDict.values(): - glyphs = {g for u,g in cmap if u in s.unicodes_requested} - if None in glyphs: - glyphs.remove(None) - s.glyphs.update(glyphs) - else: - cmap = table.cmap - intersection = s.unicodes_requested.intersection(cmap.keys()) - s.glyphs.update(cmap[u] for u in intersection) - - # Calculate unicodes_missing - s.unicodes_missing = s.unicodes_requested.copy() - for table in tables: - s.unicodes_missing.difference_update(table.cmap) + # Close glyphs + for table in tables: + if table.format == 14: + for cmap in table.uvsDict.values(): + glyphs = {g for u,g in cmap if u in s.unicodes_requested} + if None in glyphs: + glyphs.remove(None) + s.glyphs.update(glyphs) + else: + cmap = table.cmap + intersection = s.unicodes_requested.intersection(cmap.keys()) + s.glyphs.update(cmap[u] for u in intersection) + + # Calculate unicodes_missing + s.unicodes_missing = s.unicodes_requested.copy() + for table in tables: + s.unicodes_missing.difference_update(table.cmap) @_add_method(ttLib.getTableClass('cmap')) def prune_pre_subset(self, font, options): - if not options.legacy_cmap: - # Drop non-Unicode / non-Symbol cmaps - self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] - if not options.symbol_cmap: - self.tables = [t for t in self.tables if not t.isSymbol()] - # TODO(behdad) Only keep one subtable? - # For now, drop format=0 which can't be subset_glyphs easily? - self.tables = [t for t in self.tables if t.format != 0] - self.numSubTables = len(self.tables) - return True # Required table + if not options.legacy_cmap: + # Drop non-Unicode / non-Symbol cmaps + self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] + if not options.symbol_cmap: + self.tables = [t for t in self.tables if not t.isSymbol()] + # TODO(behdad) Only keep one subtable? + # For now, drop format=0 which can't be subset_glyphs easily? + self.tables = [t for t in self.tables if t.format != 0] + self.numSubTables = len(self.tables) + return True # Required table @_add_method(ttLib.getTableClass('cmap')) def subset_glyphs(self, s): - s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only - for t in self.tables: - if t.format == 14: - # TODO(behdad) We drop all the default-UVS mappings - # for glyphs_requested. So it's the caller's responsibility to make - # sure those are included. - t.uvsDict = {v:[(u,g) for u,g in l - if g in s.glyphs_requested or u in s.unicodes_requested] - for v,l in t.uvsDict.items()} - t.uvsDict = {v:l for v,l in t.uvsDict.items() if l} - elif t.isUnicode(): - t.cmap = {u:g for u,g in t.cmap.items() - if g in s.glyphs_requested or u in s.unicodes_requested} - else: - t.cmap = {u:g for u,g in t.cmap.items() - if g in s.glyphs_requested} - self.tables = [t for t in self.tables - if (t.cmap if t.format != 14 else t.uvsDict)] - self.numSubTables = len(self.tables) - # TODO(behdad) Convert formats when needed. - # In particular, if we have a format=12 without non-BMP - # characters, either drop format=12 one or convert it - # to format=4 if there's not one. - return True # Required table + s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only + for t in self.tables: + if t.format == 14: + # TODO(behdad) We drop all the default-UVS mappings + # for glyphs_requested. So it's the caller's responsibility to make + # sure those are included. + t.uvsDict = {v:[(u,g) for u,g in l + if g in s.glyphs_requested or u in s.unicodes_requested] + for v,l in t.uvsDict.items()} + t.uvsDict = {v:l for v,l in t.uvsDict.items() if l} + elif t.isUnicode(): + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested or u in s.unicodes_requested} + else: + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested} + self.tables = [t for t in self.tables + if (t.cmap if t.format != 14 else t.uvsDict)] + self.numSubTables = len(self.tables) + # TODO(behdad) Convert formats when needed. + # In particular, if we have a format=12 without non-BMP + # characters, either drop format=12 one or convert it + # to format=4 if there's not one. + return True # Required table @_add_method(ttLib.getTableClass('DSIG')) def prune_pre_subset(self, font, options): - # Drop all signatures since they will be invalid - self.usNumSigs = 0 - self.signatureRecords = [] - return True + # Drop all signatures since they will be invalid + self.usNumSigs = 0 + self.signatureRecords = [] + return True @_add_method(ttLib.getTableClass('maxp')) def prune_pre_subset(self, font, options): - if not options.hinting: - if self.tableVersion == 0x00010000: - self.maxZones = 1 - self.maxTwilightPoints = 0 - self.maxStorage = 0 - self.maxFunctionDefs = 0 - self.maxInstructionDefs = 0 - self.maxStackElements = 0 - self.maxSizeOfInstructions = 0 - return True + if not options.hinting: + if self.tableVersion == 0x00010000: + self.maxZones = 1 + self.maxTwilightPoints = 0 + self.maxStorage = 0 + self.maxFunctionDefs = 0 + self.maxInstructionDefs = 0 + self.maxStackElements = 0 + self.maxSizeOfInstructions = 0 + return True @_add_method(ttLib.getTableClass('name')) def prune_pre_subset(self, font, options): - nameIDs = set(options.name_IDs) - fvar = font.get('fvar') - if fvar: - nameIDs.update([axis.axisNameID for axis in fvar.axes]) - nameIDs.update([inst.subfamilyNameID for inst in fvar.instances]) - nameIDs.update([inst.postscriptNameID for inst in fvar.instances - if inst.postscriptNameID != 0xFFFF]) - if '*' not in options.name_IDs: - self.names = [n for n in self.names if n.nameID in nameIDs] - if not options.name_legacy: - # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman - # entry for Latin and no Unicode names. - self.names = [n for n in self.names if n.isUnicode()] - # TODO(behdad) Option to keep only one platform's - if '*' not in options.name_languages: - # TODO(behdad) This is Windows-platform specific! - self.names = [n for n in self.names - if n.langID in options.name_languages] - if options.obfuscate_names: - namerecs = [] - for n in self.names: - if n.nameID in [1, 4]: - n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f" - elif n.nameID in [2, 6]: - n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f" - elif n.nameID == 3: - n.string = "" - elif n.nameID in [16, 17, 18]: - continue - namerecs.append(n) - self.names = namerecs - return True # Required table + nameIDs = set(options.name_IDs) + fvar = font.get('fvar') + if fvar: + nameIDs.update([axis.axisNameID for axis in fvar.axes]) + nameIDs.update([inst.subfamilyNameID for inst in fvar.instances]) + nameIDs.update([inst.postscriptNameID for inst in fvar.instances + if inst.postscriptNameID != 0xFFFF]) + if '*' not in options.name_IDs: + self.names = [n for n in self.names if n.nameID in nameIDs] + if not options.name_legacy: + # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman + # entry for Latin and no Unicode names. + self.names = [n for n in self.names if n.isUnicode()] + # TODO(behdad) Option to keep only one platform's + if '*' not in options.name_languages: + # TODO(behdad) This is Windows-platform specific! + self.names = [n for n in self.names + if n.langID in options.name_languages] + if options.obfuscate_names: + namerecs = [] + for n in self.names: + if n.nameID in [1, 4]: + n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f" + elif n.nameID in [2, 6]: + n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f" + elif n.nameID == 3: + n.string = "" + elif n.nameID in [16, 17, 18]: + continue + namerecs.append(n) + self.names = namerecs + return True # Required table # TODO(behdad) OS/2 ulCodePageRange? @@ -2561,586 +2711,608 @@ class Options(object): - class OptionError(Exception): pass - class UnknownOptionError(OptionError): pass + class OptionError(Exception): pass + class UnknownOptionError(OptionError): pass - # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser - _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', - 'EBSC', 'SVG', 'PCLT', 'LTSH'] - _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite - _drop_tables_default += ['sbix'] # Color - _no_subset_tables_default = ['avar', 'fvar', - 'gasp', 'head', 'hhea', 'maxp', - 'vhea', 'OS/2', 'loca', 'name', 'cvt', - 'fpgm', 'prep', 'VDMX', 'DSIG', 'CPAL', - 'MVAR', 'STAT'] - _hinting_tables_default = ['cvar', 'cvt', 'fpgm', 'prep', 'hdmx', 'VDMX'] - - # Based on HarfBuzz shapers - _layout_features_groups = { - # Default shaper - 'common': ['rvrn', 'ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], - 'fractions': ['frac', 'numr', 'dnom'], - 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], - 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], - 'ltr': ['ltra', 'ltrm'], - 'rtl': ['rtla', 'rtlm'], - # Complex shapers - 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', - 'cswh', 'mset', 'stch'], - 'hangul': ['ljmo', 'vjmo', 'tjmo'], - 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], - 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', - 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', - 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], - } - _layout_features_default = _uniq_sort(sum( - iter(_layout_features_groups.values()), [])) - - def __init__(self, **kwargs): - - self.drop_tables = self._drop_tables_default[:] - self.no_subset_tables = self._no_subset_tables_default[:] - self.passthrough_tables = False # keep/drop tables we can't subset - self.hinting_tables = self._hinting_tables_default[:] - self.legacy_kern = False # drop 'kern' table if GPOS available - self.layout_features = self._layout_features_default[:] - self.ignore_missing_glyphs = False - self.ignore_missing_unicodes = True - self.hinting = True - self.glyph_names = False - self.legacy_cmap = False - self.symbol_cmap = False - self.name_IDs = [1, 2] # Family and Style - self.name_legacy = False - self.name_languages = [0x0409] # English - self.obfuscate_names = False # to make webfont unusable as a system font - self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF - self.notdef_outline = False # No need for notdef to have an outline really - self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType - self.recalc_bounds = False # Recalculate font bounding boxes - self.recalc_timestamp = False # Recalculate font modified timestamp - self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits - self.recalc_average_width = False # update 'xAvgCharWidth' - self.canonical_order = None # Order tables as recommended - self.flavor = None # May be 'woff' or 'woff2' - self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 - self.desubroutinize = False # Desubroutinize CFF CharStrings - self.verbose = False - self.timing = False - self.xml = False - - self.set(**kwargs) - - def set(self, **kwargs): - for k,v in kwargs.items(): - if not hasattr(self, k): - raise self.UnknownOptionError("Unknown option '%s'" % k) - setattr(self, k, v) - - def parse_opts(self, argv, ignore_unknown=[]): - posargs = [] - passthru_options = [] - for a in argv: - orig_a = a - if not a.startswith('--'): - posargs.append(a) - continue - a = a[2:] - i = a.find('=') - op = '=' - if i == -1: - if a.startswith("no-"): - k = a[3:] - if k == "canonical-order": - # reorderTables=None is faster than False (the latter - # still reorders to "keep" the original table order) - v = None - else: - v = False - else: - k = a - v = True - if k.endswith("?"): - k = k[:-1] - v = '?' - else: - k = a[:i] - if k[-1] in "-+": - op = k[-1]+'=' # Op is '-=' or '+=' now. - k = k[:-1] - v = a[i+1:] - ok = k - k = k.replace('-', '_') - if not hasattr(self, k): - if ignore_unknown is True or ok in ignore_unknown: - passthru_options.append(orig_a) - continue - else: - raise self.UnknownOptionError("Unknown option '%s'" % a) - - ov = getattr(self, k) - if v == '?': - print("Current setting for '%s' is: %s" % (ok, ov)) - continue - if isinstance(ov, bool): - v = bool(v) - elif isinstance(ov, int): - v = int(v) - elif isinstance(ov, str): - v = str(v) # redundant - elif isinstance(ov, list): - if isinstance(v, bool): - raise self.OptionError("Option '%s' requires values to be specified using '='" % a) - vv = v.replace(',', ' ').split() - if vv == ['']: - vv = [] - vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] - if op == '=': - v = vv - elif op == '+=': - v = ov - v.extend(vv) - elif op == '-=': - v = ov - for x in vv: - if x in v: - v.remove(x) - else: - assert False + # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser + _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', + 'EBSC', 'SVG', 'PCLT', 'LTSH'] + _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite + _drop_tables_default += ['sbix'] # Color + _no_subset_tables_default = ['avar', 'fvar', + 'gasp', 'head', 'hhea', 'maxp', + 'vhea', 'OS/2', 'loca', 'name', 'cvt', + 'fpgm', 'prep', 'VDMX', 'DSIG', 'CPAL', + 'MVAR', 'cvar', 'STAT'] + _hinting_tables_default = ['cvt', 'cvar', 'fpgm', 'prep', 'hdmx', 'VDMX'] + + # Based on HarfBuzz shapers + _layout_features_groups = { + # Default shaper + 'common': ['rvrn', 'ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], + 'fractions': ['frac', 'numr', 'dnom'], + 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], + 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], + 'ltr': ['ltra', 'ltrm'], + 'rtl': ['rtla', 'rtlm'], + # Complex shapers + 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', + 'cswh', 'mset', 'stch'], + 'hangul': ['ljmo', 'vjmo', 'tjmo'], + 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], + 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', + 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', + 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], + } + _layout_features_default = _uniq_sort(sum( + iter(_layout_features_groups.values()), [])) + + def __init__(self, **kwargs): + + self.drop_tables = self._drop_tables_default[:] + self.no_subset_tables = self._no_subset_tables_default[:] + self.passthrough_tables = False # keep/drop tables we can't subset + self.hinting_tables = self._hinting_tables_default[:] + self.legacy_kern = False # drop 'kern' table if GPOS available + self.layout_features = self._layout_features_default[:] + self.ignore_missing_glyphs = False + self.ignore_missing_unicodes = True + self.hinting = True + self.glyph_names = False + self.legacy_cmap = False + self.symbol_cmap = False + self.name_IDs = [0, 1, 2, 3, 4, 5, 6] # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225 + self.name_legacy = False + self.name_languages = [0x0409] # English + self.obfuscate_names = False # to make webfont unusable as a system font + self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF + self.notdef_outline = False # No need for notdef to have an outline really + self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType + self.recalc_bounds = False # Recalculate font bounding boxes + self.recalc_timestamp = False # Recalculate font modified timestamp + self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits + self.recalc_average_width = False # update 'xAvgCharWidth' + self.canonical_order = None # Order tables as recommended + self.flavor = None # May be 'woff' or 'woff2' + self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 + self.desubroutinize = False # Desubroutinize CFF CharStrings + self.verbose = False + self.timing = False + self.xml = False + self.font_number = -1 + + self.set(**kwargs) + + def set(self, **kwargs): + for k,v in kwargs.items(): + if not hasattr(self, k): + raise self.UnknownOptionError("Unknown option '%s'" % k) + setattr(self, k, v) + + def parse_opts(self, argv, ignore_unknown=[]): + posargs = [] + passthru_options = [] + for a in argv: + orig_a = a + if not a.startswith('--'): + posargs.append(a) + continue + a = a[2:] + i = a.find('=') + op = '=' + if i == -1: + if a.startswith("no-"): + k = a[3:] + if k == "canonical-order": + # reorderTables=None is faster than False (the latter + # still reorders to "keep" the original table order) + v = None + else: + v = False + else: + k = a + v = True + if k.endswith("?"): + k = k[:-1] + v = '?' + else: + k = a[:i] + if k[-1] in "-+": + op = k[-1]+'=' # Op is '-=' or '+=' now. + k = k[:-1] + v = a[i+1:] + ok = k + k = k.replace('-', '_') + if not hasattr(self, k): + if ignore_unknown is True or ok in ignore_unknown: + passthru_options.append(orig_a) + continue + else: + raise self.UnknownOptionError("Unknown option '%s'" % a) + + ov = getattr(self, k) + if v == '?': + print("Current setting for '%s' is: %s" % (ok, ov)) + continue + if isinstance(ov, bool): + v = bool(v) + elif isinstance(ov, int): + v = int(v) + elif isinstance(ov, str): + v = str(v) # redundant + elif isinstance(ov, list): + if isinstance(v, bool): + raise self.OptionError("Option '%s' requires values to be specified using '='" % a) + vv = v.replace(',', ' ').split() + if vv == ['']: + vv = [] + vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] + if op == '=': + v = vv + elif op == '+=': + v = ov + v.extend(vv) + elif op == '-=': + v = ov + for x in vv: + if x in v: + v.remove(x) + else: + assert False - setattr(self, k, v) + setattr(self, k, v) - return posargs + passthru_options + return posargs + passthru_options class Subsetter(object): - class SubsettingError(Exception): pass - class MissingGlyphsSubsettingError(SubsettingError): pass - class MissingUnicodesSubsettingError(SubsettingError): pass - - def __init__(self, options=None): - - if not options: - options = Options() - - self.options = options - self.unicodes_requested = set() - self.glyph_names_requested = set() - self.glyph_ids_requested = set() - - def populate(self, glyphs=[], gids=[], unicodes=[], text=""): - self.unicodes_requested.update(unicodes) - if isinstance(text, bytes): - text = text.decode("utf_8") - text_utf32 = text.encode("utf-32-be") - nchars = len(text_utf32)//4 - for u in struct.unpack('>%dL' % nchars, text_utf32): - self.unicodes_requested.add(u) - self.glyph_names_requested.update(glyphs) - self.glyph_ids_requested.update(gids) - - def _prune_pre_subset(self, font): - for tag in self._sort_tables(font): - if(tag.strip() in self.options.drop_tables or - (tag.strip() in self.options.hinting_tables and not self.options.hinting) or - (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))): - log.info("%s dropped", tag) - del font[tag] - continue - - clazz = ttLib.getTableClass(tag) - - if hasattr(clazz, 'prune_pre_subset'): - with timer("load '%s'" % tag): - table = font[tag] - with timer("prune '%s'" % tag): - retain = table.prune_pre_subset(font, self.options) - if not retain: - log.info("%s pruned to empty; dropped", tag) - del font[tag] - continue - else: - log.info("%s pruned", tag) - - def _closure_glyphs(self, font): - - realGlyphs = set(font.getGlyphOrder()) - glyph_order = font.getGlyphOrder() - - self.glyphs_requested = set() - self.glyphs_requested.update(self.glyph_names_requested) - self.glyphs_requested.update(glyph_order[i] - for i in self.glyph_ids_requested - if i < len(glyph_order)) - - self.glyphs_missing = set() - self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) - self.glyphs_missing.update(i for i in self.glyph_ids_requested - if i >= len(glyph_order)) - if self.glyphs_missing: - log.info("Missing requested glyphs: %s", self.glyphs_missing) - if not self.options.ignore_missing_glyphs: - raise self.MissingGlyphsSubsettingError(self.glyphs_missing) - - self.glyphs = self.glyphs_requested.copy() - - self.unicodes_missing = set() - if 'cmap' in font: - with timer("close glyph list over 'cmap'"): - font['cmap'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - self.glyphs_cmaped = frozenset(self.glyphs) - if self.unicodes_missing: - missing = ["U+%04X" % u for u in self.unicodes_missing] - log.info("Missing glyphs for requested Unicodes: %s", missing) - if not self.options.ignore_missing_unicodes: - raise self.MissingUnicodesSubsettingError(missing) - del missing - - if self.options.notdef_glyph: - if 'glyf' in font: - self.glyphs.add(font.getGlyphName(0)) - log.info("Added gid0 to subset") - else: - self.glyphs.add('.notdef') - log.info("Added .notdef to subset") - if self.options.recommended_glyphs: - if 'glyf' in font: - for i in range(min(4, len(font.getGlyphOrder()))): - self.glyphs.add(font.getGlyphName(i)) - log.info("Added first four glyphs to subset") - - if 'GSUB' in font: - with timer("close glyph list over 'GSUB'"): - log.info("Closing glyph list over 'GSUB': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['GSUB'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'GSUB': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_gsubed = frozenset(self.glyphs) - - if 'MATH' in font: - with timer("close glyph list over 'MATH'"): - log.info("Closing glyph list over 'MATH': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['MATH'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'MATH': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_mathed = frozenset(self.glyphs) - - for table in ('COLR', 'bsln'): - if table in font: - with timer("close glyph list over '%s'" % table): - log.info("Closing glyph list over '%s': %d glyphs before", - table, len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font[table].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over '%s': %d glyphs after", - table, len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - - if 'glyf' in font: - with timer("close glyph list over 'glyf'"): - log.info("Closing glyph list over 'glyf': %d glyphs before", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - font['glyf'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - log.info("Closed glyph list over 'glyf': %d glyphs after", - len(self.glyphs)) - log.glyphs(self.glyphs, font=font) - self.glyphs_glyfed = frozenset(self.glyphs) - - self.glyphs_all = frozenset(self.glyphs) - - log.info("Retaining %d glyphs", len(self.glyphs_all)) - - del self.glyphs - - def _subset_glyphs(self, font): - for tag in self._sort_tables(font): - clazz = ttLib.getTableClass(tag) - - if tag.strip() in self.options.no_subset_tables: - log.info("%s subsetting not needed", tag) - elif hasattr(clazz, 'subset_glyphs'): - with timer("subset '%s'" % tag): - table = font[tag] - self.glyphs = self.glyphs_all - retain = table.subset_glyphs(self) - del self.glyphs - if not retain: - log.info("%s subsetted to empty; dropped", tag) - del font[tag] - else: - log.info("%s subsetted", tag) - elif self.options.passthrough_tables: - log.info("%s NOT subset; don't know how to subset", tag) - else: - log.info("%s NOT subset; don't know how to subset; dropped", tag) - del font[tag] - - with timer("subset GlyphOrder"): - glyphOrder = font.getGlyphOrder() - glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] - font.setGlyphOrder(glyphOrder) - font._buildReverseGlyphOrderDict() - - def _prune_post_subset(self, font): - for tag in font.keys(): - if tag == 'GlyphOrder': continue - if tag == 'OS/2' and self.options.prune_unicode_ranges: - old_uniranges = font[tag].getUnicodeRanges() - new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) - if old_uniranges != new_uniranges: - log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)) - if self.options.recalc_average_width: - widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0] - avg_width = round(sum(widths) / len(widths)) - if avg_width != font[tag].xAvgCharWidth: - font[tag].xAvgCharWidth = avg_width - log.info("%s xAvgCharWidth updated: %d", tag, avg_width) - clazz = ttLib.getTableClass(tag) - if hasattr(clazz, 'prune_post_subset'): - with timer("prune '%s'" % tag): - table = font[tag] - retain = table.prune_post_subset(self.options) - if not retain: - log.info("%s pruned to empty; dropped", tag) - del font[tag] - else: - log.info("%s pruned", tag) - - def _sort_tables(self, font): - tagOrder = ['fvar', 'avar', 'gvar', 'name', 'glyf'] - tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} - tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) - return [t for t in tags if t != 'GlyphOrder'] - - def subset(self, font): - self._prune_pre_subset(font) - self._closure_glyphs(font) - self._subset_glyphs(font) - self._prune_post_subset(font) + class SubsettingError(Exception): pass + class MissingGlyphsSubsettingError(SubsettingError): pass + class MissingUnicodesSubsettingError(SubsettingError): pass + + def __init__(self, options=None): + + if not options: + options = Options() + + self.options = options + self.unicodes_requested = set() + self.glyph_names_requested = set() + self.glyph_ids_requested = set() + + def populate(self, glyphs=[], gids=[], unicodes=[], text=""): + self.unicodes_requested.update(unicodes) + if isinstance(text, bytes): + text = text.decode("utf_8") + text_utf32 = text.encode("utf-32-be") + nchars = len(text_utf32)//4 + for u in struct.unpack('>%dL' % nchars, text_utf32): + self.unicodes_requested.add(u) + self.glyph_names_requested.update(glyphs) + self.glyph_ids_requested.update(gids) + + def _prune_pre_subset(self, font): + for tag in self._sort_tables(font): + if (tag.strip() in self.options.drop_tables or + (tag.strip() in self.options.hinting_tables and not self.options.hinting) or + (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))): + log.info("%s dropped", tag) + del font[tag] + continue + + clazz = ttLib.getTableClass(tag) + + if hasattr(clazz, 'prune_pre_subset'): + with timer("load '%s'" % tag): + table = font[tag] + with timer("prune '%s'" % tag): + retain = table.prune_pre_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + continue + else: + log.info("%s pruned", tag) + + def _closure_glyphs(self, font): + + realGlyphs = set(font.getGlyphOrder()) + glyph_order = font.getGlyphOrder() + + self.glyphs_requested = set() + self.glyphs_requested.update(self.glyph_names_requested) + self.glyphs_requested.update(glyph_order[i] + for i in self.glyph_ids_requested + if i < len(glyph_order)) + + self.glyphs_missing = set() + self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) + self.glyphs_missing.update(i for i in self.glyph_ids_requested + if i >= len(glyph_order)) + if self.glyphs_missing: + log.info("Missing requested glyphs: %s", self.glyphs_missing) + if not self.options.ignore_missing_glyphs: + raise self.MissingGlyphsSubsettingError(self.glyphs_missing) + + self.glyphs = self.glyphs_requested.copy() + + self.unicodes_missing = set() + if 'cmap' in font: + with timer("close glyph list over 'cmap'"): + font['cmap'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + self.glyphs_cmaped = frozenset(self.glyphs) + if self.unicodes_missing: + missing = ["U+%04X" % u for u in self.unicodes_missing] + log.info("Missing glyphs for requested Unicodes: %s", missing) + if not self.options.ignore_missing_unicodes: + raise self.MissingUnicodesSubsettingError(missing) + del missing + + if self.options.notdef_glyph: + if 'glyf' in font: + self.glyphs.add(font.getGlyphName(0)) + log.info("Added gid0 to subset") + else: + self.glyphs.add('.notdef') + log.info("Added .notdef to subset") + if self.options.recommended_glyphs: + if 'glyf' in font: + for i in range(min(4, len(font.getGlyphOrder()))): + self.glyphs.add(font.getGlyphName(i)) + log.info("Added first four glyphs to subset") + + if 'GSUB' in font: + with timer("close glyph list over 'GSUB'"): + log.info("Closing glyph list over 'GSUB': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['GSUB'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'GSUB': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_gsubed = frozenset(self.glyphs) + + if 'MATH' in font: + with timer("close glyph list over 'MATH'"): + log.info("Closing glyph list over 'MATH': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['MATH'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'MATH': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_mathed = frozenset(self.glyphs) + + for table in ('COLR', 'bsln'): + if table in font: + with timer("close glyph list over '%s'" % table): + log.info("Closing glyph list over '%s': %d glyphs before", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font[table].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over '%s': %d glyphs after", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + + if 'glyf' in font: + with timer("close glyph list over 'glyf'"): + log.info("Closing glyph list over 'glyf': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['glyf'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'glyf': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_glyfed = frozenset(self.glyphs) + + if 'CFF ' in font: + with timer("close glyph list over 'CFF '"): + log.info("Closing glyph list over 'CFF ': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['CFF '].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'CFF ': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_cffed = frozenset(self.glyphs) + + self.glyphs_all = frozenset(self.glyphs) + + order = font.getReverseGlyphMap() + self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_all} + + log.info("Retaining %d glyphs", len(self.glyphs_all)) + + del self.glyphs + + def _subset_glyphs(self, font): + for tag in self._sort_tables(font): + clazz = ttLib.getTableClass(tag) + + if tag.strip() in self.options.no_subset_tables: + log.info("%s subsetting not needed", tag) + elif hasattr(clazz, 'subset_glyphs'): + with timer("subset '%s'" % tag): + table = font[tag] + self.glyphs = self.glyphs_all + retain = table.subset_glyphs(self) + del self.glyphs + if not retain: + log.info("%s subsetted to empty; dropped", tag) + del font[tag] + else: + log.info("%s subsetted", tag) + elif self.options.passthrough_tables: + log.info("%s NOT subset; don't know how to subset", tag) + else: + log.warning("%s NOT subset; don't know how to subset; dropped", tag) + del font[tag] + + with timer("subset GlyphOrder"): + glyphOrder = font.getGlyphOrder() + glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] + font.setGlyphOrder(glyphOrder) + font._buildReverseGlyphOrderDict() + + def _prune_post_subset(self, font): + for tag in font.keys(): + if tag == 'GlyphOrder': continue + if tag == 'OS/2' and self.options.prune_unicode_ranges: + old_uniranges = font[tag].getUnicodeRanges() + new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) + if old_uniranges != new_uniranges: + log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)) + if self.options.recalc_average_width: + widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0] + avg_width = otRound(sum(widths) / len(widths)) + if avg_width != font[tag].xAvgCharWidth: + font[tag].xAvgCharWidth = avg_width + log.info("%s xAvgCharWidth updated: %d", tag, avg_width) + clazz = ttLib.getTableClass(tag) + if hasattr(clazz, 'prune_post_subset'): + with timer("prune '%s'" % tag): + table = font[tag] + retain = table.prune_post_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + else: + log.info("%s pruned", tag) + + def _sort_tables(self, font): + tagOrder = ['fvar', 'avar', 'gvar', 'name', 'glyf'] + tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} + tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) + return [t for t in tags if t != 'GlyphOrder'] + + def subset(self, font): + self._prune_pre_subset(font) + self._closure_glyphs(font) + self._subset_glyphs(font) + self._prune_post_subset(font) @timer("load font") def load_font(fontFile, - options, - allowVID=False, - checkChecksums=False, - dontLoadGlyphNames=False, - lazy=True): - - font = ttLib.TTFont(fontFile, - allowVID=allowVID, - checkChecksums=checkChecksums, - recalcBBoxes=options.recalc_bounds, - recalcTimestamp=options.recalc_timestamp, - lazy=lazy) - - # Hack: - # - # If we don't need glyph names, change 'post' class to not try to - # load them. It avoid lots of headache with broken fonts as well - # as loading time. - # - # Ideally ttLib should provide a way to ask it to skip loading - # glyph names. But it currently doesn't provide such a thing. - # - if dontLoadGlyphNames: - post = ttLib.getTableClass('post') - saved = post.decode_format_2_0 - post.decode_format_2_0 = post.decode_format_3_0 - f = font['post'] - if f.formatType == 2.0: - f.formatType = 3.0 - post.decode_format_2_0 = saved + options, + allowVID=False, + checkChecksums=False, + dontLoadGlyphNames=False, + lazy=True): + + font = ttLib.TTFont(fontFile, + allowVID=allowVID, + checkChecksums=checkChecksums, + recalcBBoxes=options.recalc_bounds, + recalcTimestamp=options.recalc_timestamp, + lazy=lazy, + fontNumber=options.font_number) + + # Hack: + # + # If we don't need glyph names, change 'post' class to not try to + # load them. It avoid lots of headache with broken fonts as well + # as loading time. + # + # Ideally ttLib should provide a way to ask it to skip loading + # glyph names. But it currently doesn't provide such a thing. + # + if dontLoadGlyphNames: + post = ttLib.getTableClass('post') + saved = post.decode_format_2_0 + post.decode_format_2_0 = post.decode_format_3_0 + f = font['post'] + if f.formatType == 2.0: + f.formatType = 3.0 + post.decode_format_2_0 = saved - return font + return font @timer("compile and save font") def save_font(font, outfile, options): - if options.flavor and not hasattr(font, 'flavor'): - raise Exception("fonttools version does not support flavors.") - if options.with_zopfli and options.flavor == "woff": - from fontTools.ttLib import sfnt - sfnt.USE_ZOPFLI = True - font.flavor = options.flavor - font.save(outfile, reorderTables=options.canonical_order) + if options.with_zopfli and options.flavor == "woff": + from fontTools.ttLib import sfnt + sfnt.USE_ZOPFLI = True + font.flavor = options.flavor + font.save(outfile, reorderTables=options.canonical_order) def parse_unicodes(s): - import re - s = re.sub (r"0[xX]", " ", s) - s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s) - l = [] - for item in s.split(): - fields = item.split('-') - if len(fields) == 1: - l.append(int(item, 16)) - else: - start,end = fields - l.extend(range(int(start, 16), int(end, 16)+1)) - return l + import re + s = re.sub (r"0[xX]", " ", s) + s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s) + l = [] + for item in s.split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(item, 16)) + else: + start,end = fields + l.extend(range(int(start, 16), int(end, 16)+1)) + return l def parse_gids(s): - l = [] - for item in s.replace(',', ' ').split(): - fields = item.split('-') - if len(fields) == 1: - l.append(int(fields[0])) - else: - l.extend(range(int(fields[0]), int(fields[1])+1)) - return l + l = [] + for item in s.replace(',', ' ').split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(fields[0])) + else: + l.extend(range(int(fields[0]), int(fields[1])+1)) + return l def parse_glyphs(s): - return s.replace(',', ' ').split() + return s.replace(',', ' ').split() def usage(): - print("usage:", __usage__, file=sys.stderr) - print("Try pyftsubset --help for more information.\n", file=sys.stderr) + print("usage:", __usage__, file=sys.stderr) + print("Try pyftsubset --help for more information.\n", file=sys.stderr) @timer("make one with everything (TOTAL TIME)") def main(args=None): - from os.path import splitext - from fontTools import configLogger + from os.path import splitext + from fontTools import configLogger - if args is None: - args = sys.argv[1:] + if args is None: + args = sys.argv[1:] - if '--help' in args: - print(__doc__) - return 0 - - options = Options() - try: - args = options.parse_opts(args, - ignore_unknown=['gids', 'gids-file', - 'glyphs', 'glyphs-file', - 'text', 'text-file', - 'unicodes', 'unicodes-file', - 'output-file']) - except options.OptionError as e: - usage() - print("ERROR:", e, file=sys.stderr) - return 2 - - if len(args) < 2: - usage() - return 1 - - configLogger(level=logging.INFO if options.verbose else logging.WARNING) - if options.timing: - timer.logger.setLevel(logging.DEBUG) - else: - timer.logger.disabled = True - - fontfile = args[0] - args = args[1:] - - subsetter = Subsetter(options=options) - basename, extension = splitext(fontfile) - outfile = basename + '.subset' + extension - glyphs = [] - gids = [] - unicodes = [] - wildcard_glyphs = False - wildcard_unicodes = False - text = "" - for g in args: - if g == '*': - wildcard_glyphs = True - continue - if g.startswith('--output-file='): - outfile = g[14:] - continue - if g.startswith('--text='): - text += g[7:] - continue - if g.startswith('--text-file='): - text += open(g[12:], encoding='utf-8').read().replace('\n', '') - continue - if g.startswith('--unicodes='): - if g[11:] == '*': - wildcard_unicodes = True - else: - unicodes.extend(parse_unicodes(g[11:])) - continue - if g.startswith('--unicodes-file='): - for line in open(g[16:]).readlines(): - unicodes.extend(parse_unicodes(line.split('#')[0])) - continue - if g.startswith('--gids='): - gids.extend(parse_gids(g[7:])) - continue - if g.startswith('--gids-file='): - for line in open(g[12:]).readlines(): - gids.extend(parse_gids(line.split('#')[0])) - continue - if g.startswith('--glyphs='): - if g[9:] == '*': - wildcard_glyphs = True - else: - glyphs.extend(parse_glyphs(g[9:])) - continue - if g.startswith('--glyphs-file='): - for line in open(g[14:]).readlines(): - glyphs.extend(parse_glyphs(line.split('#')[0])) - continue - glyphs.append(g) - - dontLoadGlyphNames = not options.glyph_names and not glyphs - font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) - - with timer("compile glyph list"): - if wildcard_glyphs: - glyphs.extend(font.getGlyphOrder()) - if wildcard_unicodes: - for t in font['cmap'].tables: - if t.isUnicode(): - unicodes.extend(t.cmap.keys()) - assert '' not in glyphs - - log.info("Text: '%s'" % text) - log.info("Unicodes: %s", unicodes) - log.info("Glyphs: %s", glyphs) - log.info("Gids: %s", gids) - - subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) - subsetter.subset(font) - - save_font(font, outfile, options) - - if options.verbose: - import os - log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) - log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) + if '--help' in args: + print(__doc__) + return 0 + + options = Options() + try: + args = options.parse_opts(args, + ignore_unknown=['gids', 'gids-file', + 'glyphs', 'glyphs-file', + 'text', 'text-file', + 'unicodes', 'unicodes-file', + 'output-file']) + except options.OptionError as e: + usage() + print("ERROR:", e, file=sys.stderr) + return 2 + + if len(args) < 2: + usage() + return 1 + + configLogger(level=logging.INFO if options.verbose else logging.WARNING) + if options.timing: + timer.logger.setLevel(logging.DEBUG) + else: + timer.logger.disabled = True + + fontfile = args[0] + args = args[1:] + + subsetter = Subsetter(options=options) + outfile = None + glyphs = [] + gids = [] + unicodes = [] + wildcard_glyphs = False + wildcard_unicodes = False + text = "" + for g in args: + if g == '*': + wildcard_glyphs = True + continue + if g.startswith('--output-file='): + outfile = g[14:] + continue + if g.startswith('--text='): + text += g[7:] + continue + if g.startswith('--text-file='): + text += open(g[12:], encoding='utf-8').read().replace('\n', '') + continue + if g.startswith('--unicodes='): + if g[11:] == '*': + wildcard_unicodes = True + else: + unicodes.extend(parse_unicodes(g[11:])) + continue + if g.startswith('--unicodes-file='): + for line in open(g[16:]).readlines(): + unicodes.extend(parse_unicodes(line.split('#')[0])) + continue + if g.startswith('--gids='): + gids.extend(parse_gids(g[7:])) + continue + if g.startswith('--gids-file='): + for line in open(g[12:]).readlines(): + gids.extend(parse_gids(line.split('#')[0])) + continue + if g.startswith('--glyphs='): + if g[9:] == '*': + wildcard_glyphs = True + else: + glyphs.extend(parse_glyphs(g[9:])) + continue + if g.startswith('--glyphs-file='): + for line in open(g[14:]).readlines(): + glyphs.extend(parse_glyphs(line.split('#')[0])) + continue + glyphs.append(g) + + dontLoadGlyphNames = not options.glyph_names and not glyphs + font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) + + if outfile is None: + basename, _ = splitext(fontfile) + if options.flavor is not None: + ext = "." + options.flavor.lower() + else: + ext = ".ttf" if font.sfntVersion == "\0\1\0\0" else ".otf" + outfile = basename + ".subset" + ext + + with timer("compile glyph list"): + if wildcard_glyphs: + glyphs.extend(font.getGlyphOrder()) + if wildcard_unicodes: + for t in font['cmap'].tables: + if t.isUnicode(): + unicodes.extend(t.cmap.keys()) + assert '' not in glyphs + + log.info("Text: '%s'" % text) + log.info("Unicodes: %s", unicodes) + log.info("Glyphs: %s", glyphs) + log.info("Gids: %s", gids) + + subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) + subsetter.subset(font) + + save_font(font, outfile, options) + + if options.verbose: + import os + log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) + log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) - if options.xml: - font.saveXML(sys.stdout) + if options.xml: + font.saveXML(sys.stdout) - font.close() + font.close() __all__ = [ - 'Options', - 'Subsetter', - 'load_font', - 'save_font', - 'parse_gids', - 'parse_glyphs', - 'parse_unicodes', - 'main' + 'Options', + 'Subsetter', + 'load_font', + 'save_font', + 'parse_gids', + 'parse_glyphs', + 'parse_unicodes', + 'main' ] if __name__ == '__main__': - sys.exit(main()) + sys.exit(main()) diff -Nru fonttools-3.21.2/Snippets/fontTools/t1Lib/__init__.py fonttools-3.29.0/Snippets/fontTools/t1Lib/__init__.py --- fonttools-3.21.2/Snippets/fontTools/t1Lib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/t1Lib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -49,11 +49,18 @@ Type 1 fonts. """ - def __init__(self, path=None): - if path is not None: - self.data, type = read(path) + def __init__(self, path, encoding="ascii", kind=None): + if kind is None: + self.data, _ = read(path) + elif kind == "LWFN": + self.data = readLWFN(path) + elif kind == "PFB": + self.data = readPFB(path) + elif kind == "OTHER": + self.data = readOther(path) else: - pass # XXX + raise ValueError(kind) + self.encoding = encoding def saveAs(self, path, type, dohex=False): write(path, self.getData(), type, dohex) @@ -82,7 +89,7 @@ def parse(self): from fontTools.misc import psLib from fontTools.misc import psCharStrings - self.font = psLib.suckfont(self.data) + self.font = psLib.suckfont(self.data, self.encoding) charStrings = self.font["CharStrings"] lenIV = self.font["Private"].get("lenIV", 4) assert lenIV >= 0 diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/__init__.py fonttools-3.29.0/Snippets/fontTools/ttLib/__init__.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -43,9 +43,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.loggingTools import deprecateArgument, deprecateFunction -import os -import sys +from fontTools.misc.loggingTools import deprecateFunction import logging @@ -53,971 +51,10 @@ class TTLibError(Exception): pass - -class TTFont(object): - - """The main font object. It manages file input and output, and offers - a convenient way of accessing tables. - Tables will be only decompiled when necessary, ie. when they're actually - accessed. This means that simple operations can be extremely fast. - """ - - def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, - verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, - recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None): - - """The constructor can be called with a few different arguments. - When reading a font from disk, 'file' should be either a pathname - pointing to a file, or a readable file object. - - It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt - resource name or an sfnt resource index number or zero. The latter - case will cause TTLib to autodetect whether the file is a flat file - or a suitcase. (If it's a suitcase, only the first 'sfnt' resource - will be read!) - - The 'checkChecksums' argument is used to specify how sfnt - checksums are treated upon reading a file from disk: - 0: don't check (default) - 1: check, print warnings if a wrong checksum is found - 2: check, raise an exception if a wrong checksum is found. - - The TTFont constructor can also be called without a 'file' - argument: this is the way to create a new empty font. - In this case you can optionally supply the 'sfntVersion' argument, - and a 'flavor' which can be None, 'woff', or 'woff2'. - - If the recalcBBoxes argument is false, a number of things will *not* - be recalculated upon save/compile: - 1) 'glyf' glyph bounding boxes - 2) 'CFF ' font bounding box - 3) 'head' font bounding box - 4) 'hhea' min/max values - 5) 'vhea' min/max values - (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). - Additionally, upon importing an TTX file, this option cause glyphs - to be compiled right away. This should reduce memory consumption - greatly, and therefore should have some impact on the time needed - to parse/compile large fonts. - - If the recalcTimestamp argument is false, the modified timestamp in the - 'head' table will *not* be recalculated upon save/compile. - - If the allowVID argument is set to true, then virtual GID's are - supported. Asking for a glyph ID with a glyph name or GID that is not in - the font will return a virtual GID. This is valid for GSUB and cmap - tables. For SING glyphlets, the cmap table is used to specify Unicode - values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested - and does not exist in the font, or the glyphname has the form glyphN - and does not exist in the font, then N is used as the virtual GID. - Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new - virtual GIDs, the next is one less than the previous. - - If ignoreDecompileErrors is set to True, exceptions raised in - individual tables during decompilation will be ignored, falling - back to the DefaultTable implementation, which simply keeps the - binary data. - - If lazy is set to True, many data structures are loaded lazily, upon - access only. If it is set to False, many data structures are loaded - immediately. The default is lazy=None which is somewhere in between. - """ - - from fontTools.ttLib import sfnt - - for name in ("verbose", "quiet"): - val = locals().get(name) - if val is not None: - deprecateArgument(name, "configure logging instead") - setattr(self, name, val) - - self.lazy = lazy - self.recalcBBoxes = recalcBBoxes - self.recalcTimestamp = recalcTimestamp - self.tables = {} - self.reader = None - - # Permit the user to reference glyphs that are not int the font. - self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. - self.reverseVIDDict = {} - self.VIDDict = {} - self.allowVID = allowVID - self.ignoreDecompileErrors = ignoreDecompileErrors - - if not file: - self.sfntVersion = sfntVersion - self.flavor = flavor - self.flavorData = None - return - if not hasattr(file, "read"): - closeStream = True - # assume file is a string - if res_name_or_index is not None: - # see if it contains 'sfnt' resources in the resource or data fork - from . import macUtils - if res_name_or_index == 0: - if macUtils.getSFNTResIndices(file): - # get the first available sfnt font. - file = macUtils.SFNTResourceReader(file, 1) - else: - file = open(file, "rb") - else: - file = macUtils.SFNTResourceReader(file, res_name_or_index) - else: - file = open(file, "rb") - - else: - # assume "file" is a readable file object - closeStream = False - if not self.lazy: - # read input file in memory and wrap a stream around it to allow overwriting - tmp = BytesIO(file.read()) - if hasattr(file, 'name'): - # save reference to input file name - tmp.name = file.name - if closeStream: - file.close() - file = tmp - self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) - self.sfntVersion = self.reader.sfntVersion - self.flavor = self.reader.flavor - self.flavorData = self.reader.flavorData - - def close(self): - """If we still have a reader object, close it.""" - if self.reader is not None: - self.reader.close() - - def save(self, file, reorderTables=True): - """Save the font to disk. Similarly to the constructor, - the 'file' argument can be either a pathname or a writable - file object. - """ - from fontTools.ttLib import sfnt - if not hasattr(file, "write"): - if self.lazy and self.reader.file.name == file: - raise TTLibError( - "Can't overwrite TTFont when 'lazy' attribute is True") - closeStream = True - file = open(file, "wb") - else: - # assume "file" is a writable file object - closeStream = False - - if self.recalcTimestamp and 'head' in self: - self['head'] # make sure 'head' is loaded so the recalculation is actually done - - tags = list(self.keys()) - if "GlyphOrder" in tags: - tags.remove("GlyphOrder") - numTables = len(tags) - # write to a temporary stream to allow saving to unseekable streams - tmp = BytesIO() - writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData) - - done = [] - for tag in tags: - self._writeTable(tag, writer, done) - - writer.close() - - if (reorderTables is None or writer.reordersTables() or - (reorderTables is False and self.reader is None)): - # don't reorder tables and save as is - file.write(tmp.getvalue()) - tmp.close() - else: - if reorderTables is False: - # sort tables using the original font's order - tableOrder = list(self.reader.keys()) - else: - # use the recommended order from the OpenType specification - tableOrder = None - tmp.flush() - tmp.seek(0) - tmp2 = BytesIO() - reorderFontTables(tmp, tmp2, tableOrder) - file.write(tmp2.getvalue()) - tmp.close() - tmp2.close() - - if closeStream: - file.close() - - def saveXML(self, fileOrPath, progress=None, quiet=None, - tables=None, skipTables=None, splitTables=False, disassembleInstructions=True, - bitmapGlyphDataFormat='raw', newlinestr=None): - """Export the font as TTX (an XML-based text file), or as a series of text - files when splitTables is true. In the latter case, the 'fileOrPath' - argument should be a path to a directory. - The 'tables' argument must either be false (dump all tables) or a - list of tables to dump. The 'skipTables' argument may be a list of tables - to skip, but only when the 'tables' argument is false. - """ - from fontTools import version - from fontTools.misc import xmlWriter - - # only write the MAJOR.MINOR version in the 'ttLibVersion' attribute of - # TTX files' root element (without PATCH or .dev suffixes) - version = ".".join(version.split('.')[:2]) - - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - self.disassembleInstructions = disassembleInstructions - self.bitmapGlyphDataFormat = bitmapGlyphDataFormat - if not tables: - tables = list(self.keys()) - if "GlyphOrder" not in tables: - tables = ["GlyphOrder"] + tables - if skipTables: - for tag in skipTables: - if tag in tables: - tables.remove(tag) - numTables = len(tables) - if progress: - progress.set(0, numTables) - idlefunc = getattr(progress, "idle", None) - else: - idlefunc = None - - writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc, - newlinestr=newlinestr) - writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], - ttLibVersion=version) - writer.newline() - - if not splitTables: - writer.newline() - else: - # 'fileOrPath' must now be a path - path, ext = os.path.splitext(fileOrPath) - fileNameTemplate = path + ".%s" + ext - - for i in range(numTables): - if progress: - progress.set(i) - tag = tables[i] - if splitTables: - tablePath = fileNameTemplate % tagToIdentifier(tag) - tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc, - newlinestr=newlinestr) - tableWriter.begintag("ttFont", ttLibVersion=version) - tableWriter.newline() - tableWriter.newline() - writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) - writer.newline() - else: - tableWriter = writer - self._tableToXML(tableWriter, tag, progress) - if splitTables: - tableWriter.endtag("ttFont") - tableWriter.newline() - tableWriter.close() - if progress: - progress.set((i + 1)) - writer.endtag("ttFont") - writer.newline() - # close if 'fileOrPath' is a path; leave it open if it's a file. - # The special string "-" means standard output so leave that open too - if not hasattr(fileOrPath, "write") and fileOrPath != "-": - writer.close() - - def _tableToXML(self, writer, tag, progress, quiet=None): - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - if tag in self: - table = self[tag] - report = "Dumping '%s' table..." % tag - else: - report = "No '%s' table found." % tag - if progress: - progress.setLabel(report) - log.info(report) - if tag not in self: - return - xmlTag = tagToXML(tag) - attrs = dict() - if hasattr(table, "ERROR"): - attrs['ERROR'] = "decompilation error" - from .tables.DefaultTable import DefaultTable - if table.__class__ == DefaultTable: - attrs['raw'] = True - writer.begintag(xmlTag, **attrs) - writer.newline() - if tag in ("glyf", "CFF "): - table.toXML(writer, self, progress) - else: - table.toXML(writer, self) - writer.endtag(xmlTag) - writer.newline() - writer.newline() - - def importXML(self, fileOrPath, progress=None, quiet=None): - """Import a TTX file (an XML-based text format), so as to recreate - a font object. - """ - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - if "maxp" in self and "post" in self: - # Make sure the glyph order is loaded, as it otherwise gets - # lost if the XML doesn't contain the glyph order, yet does - # contain the table which was originally used to extract the - # glyph names from (ie. 'post', 'cmap' or 'CFF '). - self.getGlyphOrder() - - from fontTools.misc import xmlReader - - reader = xmlReader.XMLReader(fileOrPath, self, progress) - reader.read() - - def isLoaded(self, tag): - """Return true if the table identified by 'tag' has been - decompiled and loaded into memory.""" - return tag in self.tables - - def has_key(self, tag): - if self.isLoaded(tag): - return True - elif self.reader and tag in self.reader: - return True - elif tag == "GlyphOrder": - return True - else: - return False - - __contains__ = has_key - - def keys(self): - keys = list(self.tables.keys()) - if self.reader: - for key in list(self.reader.keys()): - if key not in keys: - keys.append(key) - - if "GlyphOrder" in keys: - keys.remove("GlyphOrder") - keys = sortedTagList(keys) - return ["GlyphOrder"] + keys - - def __len__(self): - return len(list(self.keys())) - - def __getitem__(self, tag): - tag = Tag(tag) - try: - return self.tables[tag] - except KeyError: - if tag == "GlyphOrder": - table = GlyphOrder(tag) - self.tables[tag] = table - return table - if self.reader is not None: - import traceback - log.debug("Reading '%s' table from disk", tag) - data = self.reader[tag] - tableClass = getTableClass(tag) - table = tableClass(tag) - self.tables[tag] = table - log.debug("Decompiling '%s' table", tag) - try: - table.decompile(data, self) - except: - if not self.ignoreDecompileErrors: - raise - # fall back to DefaultTable, retaining the binary table data - log.exception( - "An exception occurred during the decompilation of the '%s' table", tag) - from .tables.DefaultTable import DefaultTable - file = StringIO() - traceback.print_exc(file=file) - table = DefaultTable(tag) - table.ERROR = file.getvalue() - self.tables[tag] = table - table.decompile(data, self) - return table - else: - raise KeyError("'%s' table not found" % tag) - - def __setitem__(self, tag, table): - self.tables[Tag(tag)] = table - - def __delitem__(self, tag): - if tag not in self: - raise KeyError("'%s' table not found" % tag) - if tag in self.tables: - del self.tables[tag] - if self.reader and tag in self.reader: - del self.reader[tag] - - def get(self, tag, default=None): - try: - return self[tag] - except KeyError: - return default - - def setGlyphOrder(self, glyphOrder): - self.glyphOrder = glyphOrder - - def getGlyphOrder(self): - try: - return self.glyphOrder - except AttributeError: - pass - if 'CFF ' in self: - cff = self['CFF '] - self.glyphOrder = cff.getGlyphOrder() - elif 'post' in self: - # TrueType font - glyphOrder = self['post'].getGlyphOrder() - if glyphOrder is None: - # - # No names found in the 'post' table. - # Try to create glyph names from the unicode cmap (if available) - # in combination with the Adobe Glyph List (AGL). - # - self._getGlyphNamesFromCmap() - else: - self.glyphOrder = glyphOrder - else: - self._getGlyphNamesFromCmap() - return self.glyphOrder - - def _getGlyphNamesFromCmap(self): - # - # This is rather convoluted, but then again, it's an interesting problem: - # - we need to use the unicode values found in the cmap table to - # build glyph names (eg. because there is only a minimal post table, - # or none at all). - # - but the cmap parser also needs glyph names to work with... - # So here's what we do: - # - make up glyph names based on glyphID - # - load a temporary cmap table based on those names - # - extract the unicode values, build the "real" glyph names - # - unload the temporary cmap table - # - if self.isLoaded("cmap"): - # Bootstrapping: we're getting called by the cmap parser - # itself. This means self.tables['cmap'] contains a partially - # loaded cmap, making it impossible to get at a unicode - # subtable here. We remove the partially loaded cmap and - # restore it later. - # This only happens if the cmap table is loaded before any - # other table that does f.getGlyphOrder() or f.getGlyphName(). - cmapLoading = self.tables['cmap'] - del self.tables['cmap'] - else: - cmapLoading = None - # Make up glyph names based on glyphID, which will be used by the - # temporary cmap and by the real cmap in case we don't find a unicode - # cmap. - numGlyphs = int(self['maxp'].numGlyphs) - glyphOrder = [None] * numGlyphs - glyphOrder[0] = ".notdef" - for i in range(1, numGlyphs): - glyphOrder[i] = "glyph%.5d" % i - # Set the glyph order, so the cmap parser has something - # to work with (so we don't get called recursively). - self.glyphOrder = glyphOrder - - # Make up glyph names based on the reversed cmap table. Because some - # glyphs (eg. ligatures or alternates) may not be reachable via cmap, - # this naming table will usually not cover all glyphs in the font. - # If the font has no Unicode cmap table, reversecmap will be empty. - reversecmap = self['cmap'].buildReversed() - useCount = {} - for i in range(numGlyphs): - tempName = glyphOrder[i] - if tempName in reversecmap: - # If a font maps both U+0041 LATIN CAPITAL LETTER A and - # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, - # we prefer naming the glyph as "A". - glyphName = self._makeGlyphName(min(reversecmap[tempName])) - numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 - if numUses > 1: - glyphName = "%s.alt%d" % (glyphName, numUses - 1) - glyphOrder[i] = glyphName - - # Delete the temporary cmap table from the cache, so it can - # be parsed again with the right names. - del self.tables['cmap'] - self.glyphOrder = glyphOrder - if cmapLoading: - # restore partially loaded cmap, so it can continue loading - # using the proper names. - self.tables['cmap'] = cmapLoading - - @staticmethod - def _makeGlyphName(codepoint): - from fontTools import agl # Adobe Glyph List - if codepoint in agl.UV2AGL: - return agl.UV2AGL[codepoint] - elif codepoint <= 0xFFFF: - return "uni%04X" % codepoint - else: - return "u%X" % codepoint - - def getGlyphNames(self): - """Get a list of glyph names, sorted alphabetically.""" - glyphNames = sorted(self.getGlyphOrder()) - return glyphNames - - def getGlyphNames2(self): - """Get a list of glyph names, sorted alphabetically, - but not case sensitive. - """ - from fontTools.misc import textTools - return textTools.caselessSort(self.getGlyphOrder()) - - def getGlyphName(self, glyphID, requireReal=False): - try: - return self.getGlyphOrder()[glyphID] - except IndexError: - if requireReal or not self.allowVID: - # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in - # the cmap table than there are glyphs. I don't think it's legal... - return "glyph%.5d" % glyphID - else: - # user intends virtual GID support - try: - glyphName = self.VIDDict[glyphID] - except KeyError: - glyphName ="glyph%.5d" % glyphID - self.last_vid = min(glyphID, self.last_vid ) - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphName - - def getGlyphID(self, glyphName, requireReal=False): - if not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - glyphOrder = self.getGlyphOrder() - d = self._reverseGlyphOrderDict - if glyphName not in d: - if glyphName in glyphOrder: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - else: - if requireReal: - raise KeyError(glyphName) - elif not self.allowVID: - # Handle glyphXXX only - if glyphName[:5] == "glyph": - try: - return int(glyphName[5:]) - except (NameError, ValueError): - raise KeyError(glyphName) - else: - # user intends virtual GID support - try: - glyphID = self.reverseVIDDict[glyphName] - except KeyError: - # if name is in glyphXXX format, use the specified name. - if glyphName[:5] == "glyph": - try: - glyphID = int(glyphName[5:]) - except (NameError, ValueError): - glyphID = None - if glyphID is None: - glyphID = self.last_vid -1 - self.last_vid = glyphID - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphID - - glyphID = d[glyphName] - if glyphName != glyphOrder[glyphID]: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - return glyphID - - def getReverseGlyphMap(self, rebuild=False): - if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - return self._reverseGlyphOrderDict - - def _buildReverseGlyphOrderDict(self): - self._reverseGlyphOrderDict = d = {} - glyphOrder = self.getGlyphOrder() - for glyphID in range(len(glyphOrder)): - d[glyphOrder[glyphID]] = glyphID - - def _writeTable(self, tag, writer, done): - """Internal helper function for self.save(). Keeps track of - inter-table dependencies. - """ - if tag in done: - return - tableClass = getTableClass(tag) - for masterTable in tableClass.dependencies: - if masterTable not in done: - if masterTable in self: - self._writeTable(masterTable, writer, done) - else: - done.append(masterTable) - tabledata = self.getTableData(tag) - log.debug("writing '%s' table to disk", tag) - writer[tag] = tabledata - done.append(tag) - - def getTableData(self, tag): - """Returns raw table data, whether compiled or directly read from disk. - """ - tag = Tag(tag) - if self.isLoaded(tag): - log.debug("compiling '%s' table", tag) - return self.tables[tag].compile(self) - elif self.reader and tag in self.reader: - log.debug("Reading '%s' table from disk", tag) - return self.reader[tag] - else: - raise KeyError(tag) - - def getGlyphSet(self, preferCFF=True): - """Return a generic GlyphSet, which is a dict-like object - mapping glyph names to glyph objects. The returned glyph objects - have a .draw() method that supports the Pen protocol, and will - have an attribute named 'width'. - - If the font is CFF-based, the outlines will be taken from the 'CFF ' or - 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. - If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use - the 'preferCFF' argument to specify which one should be taken. If the - font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. - """ - glyphs = None - if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or - ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): - table_tag = "CFF2" if "CFF2" in self else "CFF " - glyphs = _TTGlyphSet(self, - list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) - - if glyphs is None and "glyf" in self: - glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) - - if glyphs is None: - raise TTLibError("Font contains no outlines") - - return glyphs - - def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): - """Return the 'best' unicode cmap dictionary available in the font, - or None, if no unicode cmap subtable is available. - - By default it will search for the following (platformID, platEncID) - pairs: - (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) - This can be customized via the cmapPreferences argument. - """ - return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) - - -class _TTGlyphSet(object): - - """Generic dict-like GlyphSet class that pulls metrics from hmtx and - glyph shape from TrueType or CFF. - """ - - def __init__(self, ttFont, glyphs, glyphType): - self._glyphs = glyphs - self._hmtx = ttFont['hmtx'] - self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None - self._glyphType = glyphType - - def keys(self): - return list(self._glyphs.keys()) - - def has_key(self, glyphName): - return glyphName in self._glyphs - - __contains__ = has_key - - def __getitem__(self, glyphName): - horizontalMetrics = self._hmtx[glyphName] - verticalMetrics = self._vmtx[glyphName] if self._vmtx else None - return self._glyphType( - self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) - - def get(self, glyphName, default=None): - try: - return self[glyphName] - except KeyError: - return default - -class _TTGlyph(object): - - """Wrapper for a TrueType glyph that supports the Pen protocol, meaning - that it has a .draw() method that takes a pen object as its only - argument. Additionally there are 'width' and 'lsb' attributes, read from - the 'hmtx' table. - - If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' - attributes. - """ - - def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): - self._glyphset = glyphset - self._glyph = glyph - self.width, self.lsb = horizontalMetrics - if verticalMetrics: - self.height, self.tsb = verticalMetrics - else: - self.height, self.tsb = None, None - - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - self._glyph.draw(pen) - -class _TTGlyphCFF(_TTGlyph): - pass - -class _TTGlyphGlyf(_TTGlyph): - - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - glyfTable = self._glyphset._glyphs - glyph = self._glyph - offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 - glyph.draw(pen, glyfTable, offset) - - -class GlyphOrder(object): - - """A pseudo table. The glyph order isn't in the font as a separate - table, but it's nice to present it as such in the TTX format. - """ - - def __init__(self, tag=None): - pass - - def toXML(self, writer, ttFont): - glyphOrder = ttFont.getGlyphOrder() - writer.comment("The 'id' attribute is only for humans; " - "it is ignored when parsed.") - writer.newline() - for i in range(len(glyphOrder)): - glyphName = glyphOrder[i] - writer.simpletag("GlyphID", id=i, name=glyphName) - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "glyphOrder"): - self.glyphOrder = [] - ttFont.setGlyphOrder(self.glyphOrder) - if name == "GlyphID": - self.glyphOrder.append(attrs["name"]) - - -def getTableModule(tag): - """Fetch the packer/unpacker module for a table. - Return None when no module is found. - """ - from . import tables - pyTag = tagToIdentifier(tag) - try: - __import__("fontTools.ttLib.tables." + pyTag) - except ImportError as err: - # If pyTag is found in the ImportError message, - # means table is not implemented. If it's not - # there, then some other module is missing, don't - # suppress the error. - if str(err).find(pyTag) >= 0: - return None - else: - raise err - else: - return getattr(tables, pyTag) - - -def getTableClass(tag): - """Fetch the packer/unpacker class for a table. - Return None when no class is found. - """ - module = getTableModule(tag) - if module is None: - from .tables.DefaultTable import DefaultTable - return DefaultTable - pyTag = tagToIdentifier(tag) - tableClass = getattr(module, "table_" + pyTag) - return tableClass - - -def getClassTag(klass): - """Fetch the table tag for a class object.""" - name = klass.__name__ - assert name[:6] == 'table_' - name = name[6:] # Chop 'table_' - return identifierToTag(name) - - -def newTable(tag): - """Return a new instance of a table.""" - tableClass = getTableClass(tag) - return tableClass(tag) - - -def _escapechar(c): - """Helper function for tagToIdentifier()""" - import re - if re.match("[a-z0-9]", c): - return "_" + c - elif re.match("[A-Z]", c): - return c + "_" - else: - return hex(byteord(c))[2:] - - -def tagToIdentifier(tag): - """Convert a table tag to a valid (but UGLY) python identifier, - as well as a filename that's guaranteed to be unique even on a - caseless file system. Each character is mapped to two characters. - Lowercase letters get an underscore before the letter, uppercase - letters get an underscore after the letter. Trailing spaces are - trimmed. Illegal characters are escaped as two hex bytes. If the - result starts with a number (as the result of a hex escape), an - extra underscore is prepended. Examples: - 'glyf' -> '_g_l_y_f' - 'cvt ' -> '_c_v_t' - 'OS/2' -> 'O_S_2f_2' - """ - import re - tag = Tag(tag) - if tag == "GlyphOrder": - return tag - assert len(tag) == 4, "tag should be 4 characters long" - while len(tag) > 1 and tag[-1] == ' ': - tag = tag[:-1] - ident = "" - for c in tag: - ident = ident + _escapechar(c) - if re.match("[0-9]", ident): - ident = "_" + ident - return ident - - -def identifierToTag(ident): - """the opposite of tagToIdentifier()""" - if ident == "GlyphOrder": - return ident - if len(ident) % 2 and ident[0] == "_": - ident = ident[1:] - assert not (len(ident) % 2) - tag = "" - for i in range(0, len(ident), 2): - if ident[i] == "_": - tag = tag + ident[i+1] - elif ident[i+1] == "_": - tag = tag + ident[i] - else: - # assume hex - tag = tag + chr(int(ident[i:i+2], 16)) - # append trailing spaces - tag = tag + (4 - len(tag)) * ' ' - return Tag(tag) - - -def tagToXML(tag): - """Similarly to tagToIdentifier(), this converts a TT tag - to a valid XML element name. Since XML element names are - case sensitive, this is a fairly simple/readable translation. - """ - import re - tag = Tag(tag) - if tag == "OS/2": - return "OS_2" - elif tag == "GlyphOrder": - return tag - if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): - return tag.strip() - else: - return tagToIdentifier(tag) - - -def xmlToTag(tag): - """The opposite of tagToXML()""" - if tag == "OS_2": - return Tag("OS/2") - if len(tag) == 8: - return identifierToTag(tag) - else: - return Tag(tag + " " * (4 - len(tag))) - - @deprecateFunction("use logging instead", category=DeprecationWarning) def debugmsg(msg): import time print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) - -# Table order as recommended in the OpenType specification 1.4 -TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", - "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", - "kern", "name", "post", "gasp", "PCLT"] - -OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", - "CFF "] - -def sortedTagList(tagList, tableOrder=None): - """Return a sorted copy of tagList, sorted according to the OpenType - specification, or according to a custom tableOrder. If given and not - None, tableOrder needs to be a list of tag names. - """ - tagList = sorted(tagList) - if tableOrder is None: - if "DSIG" in tagList: - # DSIG should be last (XXX spec reference?) - tagList.remove("DSIG") - tagList.append("DSIG") - if "CFF " in tagList: - tableOrder = OTFTableOrder - else: - tableOrder = TTFTableOrder - orderedTables = [] - for tag in tableOrder: - if tag in tagList: - orderedTables.append(tag) - tagList.remove(tag) - orderedTables.extend(tagList) - return orderedTables - - -def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): - """Rewrite a font file, ordering the tables as recommended by the - OpenType specification 1.4. - """ - from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter - reader = SFNTReader(inFile, checkChecksums=checkChecksums) - writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) - tables = list(reader.keys()) - for tag in sortedTagList(tables, tableOrder): - writer[tag] = reader[tag] - writer.close() - - -def maxPowerOfTwo(x): - """Return the highest exponent of two, so that - (2 ** exponent) <= x. Return 0 if x is 0. - """ - exponent = 0 - while x: - x = x >> 1 - exponent = exponent + 1 - return max(exponent - 1, 0) - - -def getSearchRange(n, itemSize=16): - """Calculate searchRange, entrySelector, rangeShift. - """ - # itemSize defaults to 16, for backward compatibility - # with upstream fonttools. - exponent = maxPowerOfTwo(n) - searchRange = (2 ** exponent) * itemSize - entrySelector = exponent - rangeShift = max(0, n * itemSize - searchRange) - return searchRange, entrySelector, rangeShift +from fontTools.ttLib.ttFont import * +from fontTools.ttLib.ttCollection import TTCollection diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/sfnt.py fonttools-3.29.0/Snippets/fontTools/ttLib/sfnt.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/sfnt.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/sfnt.py 2018-07-26 14:12:55.000000000 +0000 @@ -15,7 +15,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct -from fontTools.ttLib import getSearchRange +from fontTools.ttLib import TTLibError import struct from collections import OrderedDict import logging @@ -32,6 +32,7 @@ """ if args and cls is SFNTReader: infile = args[0] + infile.seek(0) sfntVersion = Tag(infile.read(4)) infile.seek(0) if sfntVersion == "wOF2": @@ -48,46 +49,36 @@ self.flavor = None self.flavorData = None self.DirectoryEntry = SFNTDirectoryEntry + self.file.seek(0) self.sfntVersion = self.file.read(4) self.file.seek(0) if self.sfntVersion == b"ttcf": - data = self.file.read(ttcHeaderSize) - if len(data) != ttcHeaderSize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a Font Collection (not enough data)") - sstruct.unpack(ttcHeaderFormat, data, self) - assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version - if not 0 <= fontNumber < self.numFonts: - from fontTools import ttLib - raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)) - offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4)) - if self.Version == 0x00020000: - pass # ignoring version 2.0 signatures - self.file.seek(offsetTable[fontNumber]) + header = readTTCHeader(self.file) + numFonts = header.numFonts + if not 0 <= fontNumber < numFonts: + raise TTLibError("specify a font number between 0 and %d (inclusive)" % (numFonts - 1)) + self.numFonts = numFonts + self.file.seek(header.offsetTable[fontNumber]) data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a Font Collection (not enough data)") + raise TTLibError("Not a Font Collection (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) elif self.sfntVersion == b"wOFF": self.flavor = "woff" self.DirectoryEntry = WOFFDirectoryEntry data = self.file.read(woffDirectorySize) if len(data) != woffDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a WOFF font (not enough data)") + raise TTLibError("Not a WOFF font (not enough data)") sstruct.unpack(woffDirectoryFormat, data, self) else: data = self.file.read(sfntDirectorySize) if len(data) != sfntDirectorySize: - from fontTools import ttLib - raise ttLib.TTLibError("Not a TrueType or OpenType font (not enough data)") + raise TTLibError("Not a TrueType or OpenType font (not enough data)") sstruct.unpack(sfntDirectoryFormat, data, self) self.sfntVersion = Tag(self.sfntVersion) if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): - from fontTools import ttLib - raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") + raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") tables = {} for i in range(self.numTables): entry = self.DirectoryEntry() @@ -215,20 +206,27 @@ self.directorySize = sfntDirectorySize self.DirectoryEntry = SFNTDirectoryEntry + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables, 16) - self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize + self.directoryOffset = self.file.tell() + self.nextTableOffset = self.directoryOffset + self.directorySize + numTables * self.DirectoryEntry.formatSize # clear out directory area self.file.seek(self.nextTableOffset) # make sure we're actually where we want to be. (old cStringIO bug) self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) self.tables = OrderedDict() + def setEntry(self, tag, entry): + if tag in self.tables: + raise TTLibError("cannot rewrite '%s' table" % tag) + + self.tables[tag] = entry + def __setitem__(self, tag, data): """Write raw table data to disk.""" if tag in self.tables: - from fontTools import ttLib - raise ttLib.TTLibError("cannot rewrite '%s' table" % tag) + raise TTLibError("cannot rewrite '%s' table" % tag) entry = self.DirectoryEntry() entry.tag = tag @@ -253,7 +251,10 @@ self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) assert self.nextTableOffset == self.file.tell() - self.tables[tag] = entry + self.setEntry(tag, entry) + + def __getitem__(self, tag): + return self.tables[tag] def close(self): """All tables must have been written to disk. Now write the @@ -261,8 +262,7 @@ """ tables = sorted(self.tables.items()) if len(tables) != self.numTables: - from fontTools import ttLib - raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) + raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) if self.flavor == "woff": self.signature = b"wOFF" @@ -311,7 +311,7 @@ directory = sstruct.pack(self.directoryFormat, self) - self.file.seek(self.directorySize) + self.file.seek(self.directoryOffset + self.directorySize) seenHead = 0 for tag, entry in tables: if tag == "head": @@ -319,7 +319,7 @@ directory = directory + entry.toString() if seenHead: self.writeMasterChecksum(directory) - self.file.seek(0) + self.file.seek(self.directoryOffset) self.file.write(directory) def _calcMasterChecksum(self, directory): @@ -331,6 +331,7 @@ if self.DirectoryEntry != SFNTDirectoryEntry: # Create a SFNT directory for checksum calculation purposes + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) @@ -566,6 +567,31 @@ value = (value + sum(longs)) & 0xffffffff return value +def readTTCHeader(file): + file.seek(0) + data = file.read(ttcHeaderSize) + if len(data) != ttcHeaderSize: + raise TTLibError("Not a Font Collection (not enough data)") + self = SimpleNamespace() + sstruct.unpack(ttcHeaderFormat, data, self) + if self.TTCTag != "ttcf": + raise TTLibError("Not a Font Collection") + assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version + self.offsetTable = struct.unpack(">%dL" % self.numFonts, file.read(self.numFonts * 4)) + if self.Version == 0x00020000: + pass # ignoring version 2.0 signatures + return self + +def writeTTCHeader(file, numFonts): + self = SimpleNamespace() + self.TTCTag = 'ttcf' + self.Version = 0x00010000 + self.numFonts = numFonts + file.seek(0) + file.write(sstruct.pack(ttcHeaderFormat, self)) + offset = file.tell() + file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) + return offset if __name__ == "__main__": import sys diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_a_v_a_r.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_a_v_a_r.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_a_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_a_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -70,7 +70,7 @@ segments[fixedToFloat(fromValue, 14)] = fixedToFloat(toValue, 14) pos = pos + 4 - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] for axis in axisTags: writer.begintag("segment", axis=axis) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/C_F_F_.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/C_F_F_.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/C_F_F_.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/C_F_F_.py 2018-07-26 14:12:55.000000000 +0000 @@ -38,8 +38,8 @@ # XXX #self.cff[self.cff.fontNames[0]].setGlyphOrder(glyphOrder) - def toXML(self, writer, otFont, progress=None): - self.cff.toXML(writer, progress) + def toXML(self, writer, otFont): + self.cff.toXML(writer) def fromXML(self, name, attrs, content, otFont): if not hasattr(self, "cff"): diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_c_v_a_r.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_c_v_a_r.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_c_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_c_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -75,7 +75,7 @@ tupleName, tupleAttrs, tupleContent = tupleElement var.fromXML(tupleName, tupleAttrs, tupleContent) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] writer.simpletag("version", major=self.majorVersion, minor=self.minorVersion) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/DefaultTable.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/DefaultTable.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/DefaultTable.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/DefaultTable.py 2018-07-26 14:12:55.000000000 +0000 @@ -17,7 +17,7 @@ def compile(self, ttFont): return self.data - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): if hasattr(self, "ERROR"): writer.comment("An error occurred during the decompilation of this table") writer.newline() diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_f_v_a_r.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_f_v_a_r.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_f_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_f_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -89,7 +89,7 @@ self.instances.append(instance) pos += instanceSize - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): for axis in self.axes: axis.toXML(writer, ttFont) for instance in self.instances: diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_g_l_y_f.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_g_l_y_f.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_g_l_y_f.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_g_l_y_f.py 2018-07-26 14:12:55.000000000 +0000 @@ -5,10 +5,15 @@ from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools import ttLib +from fontTools import version from fontTools.misc.textTools import safeEval, pad from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds -from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + otRound, +) from numbers import Number from . import DefaultTable from . import ttProgram @@ -16,10 +21,17 @@ import struct import array import logging - +import os +from fontTools.misc import xmlWriter +from fontTools.misc.filenames import userNameToFileName log = logging.getLogger(__name__) +# We compute the version the same as is computed in ttlib/__init__ +# so that we can write 'ttLibVersion' attribute of the glyf TTX files +# when glyf is written to separate files. +version = ".".join(version.split('.')[:2]) + # # The Apple and MS rasterizers behave differently for # scaled composite components: one does scale first and then translate @@ -110,37 +122,64 @@ ttFont['maxp'].numGlyphs = len(self.glyphs) return data - def toXML(self, writer, ttFont, progress=None): - writer.newline() + def toXML(self, writer, ttFont, splitGlyphs=False): + notice = ( + "The xMin, yMin, xMax and yMax values\n" + "will be recalculated by the compiler.") glyphNames = ttFont.getGlyphNames() - writer.comment("The xMin, yMin, xMax and yMax values\nwill be recalculated by the compiler.") - writer.newline() - writer.newline() - counter = 0 - progressStep = 10 + if not splitGlyphs: + writer.newline() + writer.comment(notice) + writer.newline() + writer.newline() numGlyphs = len(glyphNames) + if splitGlyphs: + path, ext = os.path.splitext(writer.file.name) + existingGlyphFiles = set() for glyphName in glyphNames: - if not counter % progressStep and progress is not None: - progress.setLabel("Dumping 'glyf' table... (%s)" % glyphName) - progress.increment(progressStep / numGlyphs) - counter = counter + 1 glyph = self[glyphName] if glyph.numberOfContours: - writer.begintag('TTGlyph', [ - ("name", glyphName), - ("xMin", glyph.xMin), - ("yMin", glyph.yMin), - ("xMax", glyph.xMax), - ("yMax", glyph.yMax), - ]) - writer.newline() - glyph.toXML(writer, ttFont) - writer.endtag('TTGlyph') - writer.newline() + if splitGlyphs: + glyphPath = userNameToFileName( + tounicode(glyphName, 'utf-8'), + existingGlyphFiles, + prefix=path + ".", + suffix=ext) + existingGlyphFiles.add(glyphPath.lower()) + glyphWriter = xmlWriter.XMLWriter( + glyphPath, idlefunc=writer.idlefunc, + newlinestr=writer.newlinestr) + glyphWriter.begintag("ttFont", ttLibVersion=version) + glyphWriter.newline() + glyphWriter.begintag("glyf") + glyphWriter.newline() + glyphWriter.comment(notice) + glyphWriter.newline() + writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) + else: + glyphWriter = writer + glyphWriter.begintag('TTGlyph', [ + ("name", glyphName), + ("xMin", glyph.xMin), + ("yMin", glyph.yMin), + ("xMax", glyph.xMax), + ("yMax", glyph.yMax), + ]) + glyphWriter.newline() + glyph.toXML(glyphWriter, ttFont) + glyphWriter.endtag('TTGlyph') + glyphWriter.newline() + if splitGlyphs: + glyphWriter.endtag("glyf") + glyphWriter.newline() + glyphWriter.endtag("ttFont") + glyphWriter.newline() + glyphWriter.close() else: writer.simpletag('TTGlyph', name=glyphName) writer.comment("contains no outline data") - writer.newline() + if not splitGlyphs: + writer.newline() writer.newline() def fromXML(self, name, attrs, content, ttFont): @@ -1079,8 +1118,8 @@ data = data + struct.pack(">HH", self.firstPt, self.secondPt) flags = flags | ARG_1_AND_2_ARE_WORDS else: - x = round(self.x) - y = round(self.y) + x = otRound(self.x) + y = otRound(self.y) flags = flags | ARGS_ARE_XY_VALUES if (-128 <= x <= 127) and (-128 <= y <= 127): data = data + struct.pack(">bb", x, y) @@ -1185,6 +1224,9 @@ def _checkFloat(self, p): if self.isFloat(): return p + if any(v > 0x7FFF or v < -0x8000 for v in p): + self._ensureFloat() + return p if any(isinstance(v, float) for v in p): p = [int(v) if int(v) == v else v for v in p] if any(isinstance(v, float) for v in p): @@ -1224,7 +1266,6 @@ del self._a[i] del self._a[i] - def __repr__(self): return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' @@ -1242,15 +1283,16 @@ return a = array.array("h") for n in self._a: - a.append(round(n)) + a.append(otRound(n)) self._a = a def relativeToAbsolute(self): a = self._a x,y = 0,0 for i in range(len(a) // 2): - a[2*i ] = x = a[2*i ] + x - a[2*i+1] = y = a[2*i+1] + y + x = a[2*i ] + x + y = a[2*i+1] + y + self[i] = (x, y) def absoluteToRelative(self): a = self._a @@ -1260,8 +1302,7 @@ dy = a[2*i+1] - y x = a[2*i ] y = a[2*i+1] - a[2*i ] = dx - a[2*i+1] = dy + self[i] = (dx, dy) def translate(self, p): """ @@ -1270,8 +1311,7 @@ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): - a[2*i ] += x - a[2*i+1] += y + self[i] = (a[2*i] + x, a[2*i+1] + y) def scale(self, p): """ @@ -1280,8 +1320,7 @@ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): - a[2*i ] *= x - a[2*i+1] *= y + self[i] = (a[2*i] * x, a[2*i+1] * y) def transform(self, t): """ @@ -1397,8 +1436,8 @@ other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] += other[i] + for i in range(len(a) // 2): + self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) return self return NotImplemented @@ -1422,8 +1461,8 @@ other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] -= other[i] + for i in range(len(a) // 2): + self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) return self return NotImplemented diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_g_v_a_r.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_g_v_a_r.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_g_v_a_r.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_g_v_a_r.py 2018-07-26 14:12:55.000000000 +0000 @@ -156,7 +156,7 @@ packed.byteswap() return (packed.tostring(), tableFormat) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): writer.simpletag("version", value=self.version) writer.newline() writer.simpletag("reserved", value=self.reserved) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_h_h_e_a.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_h_h_e_a.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_h_h_e_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_h_h_e_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -64,9 +64,10 @@ boundsWidthDict[name] = g.xMax - g.xMin elif 'CFF ' in ttFont: topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings for name in ttFont.getGlyphOrder(): - cs = topDict.CharStrings[name] - bounds = cs.calcBounds() + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) if bounds is not None: boundsWidthDict[name] = int( math.ceil(bounds[2]) - math.floor(bounds[0])) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_h_m_t_x.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_h_m_t_x.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_h_m_t_x.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_h_m_t_x.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,5 +1,6 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools import ttLib from fontTools.misc.textTools import safeEval from . import DefaultTable @@ -78,14 +79,14 @@ lastIndex = 1 break additionalMetrics = metrics[lastIndex:] - additionalMetrics = [round(sb) for _, sb in additionalMetrics] + additionalMetrics = [otRound(sb) for _, sb in additionalMetrics] metrics = metrics[:lastIndex] numberOfMetrics = len(metrics) setattr(ttFont[self.headerTag], self.numberOfMetricsName, numberOfMetrics) allMetrics = [] for advance, sb in metrics: - allMetrics.extend([round(advance), round(sb)]) + allMetrics.extend([otRound(advance), otRound(sb)]) metricsFmt = ">" + self.longMetricFormat * numberOfMetrics try: data = struct.pack(metricsFmt, *allMetrics) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_m_e_t_a.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_m_e_t_a.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_m_e_t_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_m_e_t_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -74,7 +74,7 @@ dataOffset += len(data) return bytesjoin([header] + dataMaps + dataBlocks) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): for tag in sorted(self.data.keys()): if tag in ["dlng", "slng"]: writer.begintag("text", tag=tag) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otBase.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otBase.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otBase.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otBase.py 2018-07-26 14:12:55.000000000 +0000 @@ -42,7 +42,7 @@ self.table.decompile(reader, font) def compile(self, font): - """ Create a top-level OTFWriter for the GPOS/GSUB table. + """ Create a top-level OTTableWriter for the GPOS/GSUB table. Call the compile method for the the table for each 'converter' record in the table converter list call converter's write method for each item in the value. @@ -87,7 +87,13 @@ from .otTables import fixSubTableOverFlows ok = fixSubTableOverFlows(font, overflowRecord) if not ok: - raise + # Try upgrading lookup to Extension and hope + # that cross-lookup sharing not happening would + # fix overflow... + from .otTables import fixLookupOverFlows + ok = fixLookupOverFlows(font, overflowRecord) + if not ok: + raise def toXML(self, writer, font): self.table.toXML2(writer, font) @@ -283,7 +289,7 @@ def __eq__(self, other): if type(self) != type(other): return NotImplemented - return self.items == other.items + return self.longOffset == other.longOffset and self.items == other.items def _doneWriting(self, internedTables): # Convert CountData references to data string items @@ -331,7 +337,6 @@ iRange.reverse() isExtension = hasattr(self, "Extension") - dontShare = hasattr(self, 'DontShare') selfTables = tables @@ -610,22 +615,27 @@ if conv.name == "SubStruct": conv = conv.getConverter(reader.tableTag, table["MorphType"]) - if conv.repeat: - if isinstance(conv.repeat, int): - countValue = conv.repeat - elif conv.repeat in table: - countValue = table[conv.repeat] + try: + if conv.repeat: + if isinstance(conv.repeat, int): + countValue = conv.repeat + elif conv.repeat in table: + countValue = table[conv.repeat] + else: + # conv.repeat is a propagated count + countValue = reader[conv.repeat] + countValue += conv.aux + table[conv.name] = conv.readArray(reader, font, table, countValue) else: - # conv.repeat is a propagated count - countValue = reader[conv.repeat] - countValue += conv.aux - table[conv.name] = conv.readArray(reader, font, table, countValue) - else: - if conv.aux and not eval(conv.aux, None, table): - continue - table[conv.name] = conv.read(reader, font, table) - if conv.isPropagated: - reader[conv.name] = table[conv.name] + if conv.aux and not eval(conv.aux, None, table): + continue + table[conv.name] = conv.read(reader, font, table) + if conv.isPropagated: + reader[conv.name] = table[conv.name] + except Exception as e: + name = conv.name + e.args = e.args + (name,) + raise if hasattr(self, 'postRead'): self.postRead(table, font) @@ -894,7 +904,8 @@ setattr(self, name, None if isDevice else 0) if src is not None: for key,val in src.__dict__.items(): - assert hasattr(self, key) + if not hasattr(self, key): + continue setattr(self, key, val) elif src is not None: self.__dict__ = src.__dict__.copy() diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otConverters.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otConverters.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otConverters.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otConverters.py 2018-07-26 14:12:55.000000000 +0000 @@ -285,15 +285,16 @@ class NameID(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) - nameTable = font.get("name") if font else None - if nameTable: - name = nameTable.getDebugName(value) - xmlWriter.write(" ") - if name: - xmlWriter.comment(name) - else: - xmlWriter.comment("missing from name table") - log.warning("name id %d missing from name table" % value) + if font and value: + nameTable = font.get("name") + if nameTable: + name = nameTable.getDebugName(value) + xmlWriter.write(" ") + if name: + xmlWriter.comment(name) + else: + xmlWriter.comment("missing from name table") + log.warning("name id %d missing from name table" % value) xmlWriter.newline() @@ -1239,7 +1240,6 @@ actionIndex.setdefault( suffix, suffixIndex) result += a - assert len(result) % self.tableClass.staticSize == 0 return (result, actionIndex) def _compileLigComponents(self, table, font): diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otData.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otData.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otData.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otData.py 2018-07-26 14:12:55.000000000 +0000 @@ -397,16 +397,16 @@ ('LOffset', 'ExtSubTable', None, None, 'Offset to SubTable'), ]), - ('ValueRecord', [ - ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), - ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), - ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), - ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), - ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), - ]), +# ('ValueRecord', [ +# ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), +# ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), +# ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), +# ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), +# ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), +# ]), ('AnchorFormat1', [ ('uint16', 'AnchorFormat', None, None, 'Format identifier-format = 1'), @@ -970,7 +970,7 @@ ('VarData', [ ('uint16', 'ItemCount', None, None, ''), - ('uint16', 'NumShorts', None, None, ''), # Automatically computed + ('uint16', 'NumShorts', None, None, ''), ('uint16', 'VarRegionCount', None, None, ''), ('uint16', 'VarRegionIndex', 'VarRegionCount', 0, ''), ('VarDataValue', 'Item', 'ItemCount', 0, ''), diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otTables.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otTables.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/otTables.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/otTables.py 2018-07-26 14:12:55.000000000 +0000 @@ -8,7 +8,7 @@ from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import * from fontTools.misc.textTools import safeEval -from .otBase import BaseTable, FormatSwitchingBaseTable +from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord import operator import logging import struct @@ -482,7 +482,7 @@ glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID)) else: self.glyphs = [] - log.warning("Unknown Coverage format: %s" % self.Format) + log.warning("Unknown Coverage format: %s", self.Format) def preWrite(self, font): glyphs = getattr(self, "glyphs", None) @@ -546,21 +546,28 @@ def populateDefaults(self, propagator=None): if not hasattr(self, 'mapping'): - self.mapping = [] + self.mapping = {} def postRead(self, rawTable, font): assert (rawTable['EntryFormat'] & 0xFFC0) == 0 - self.mapping = rawTable['mapping'] + glyphOrder = font.getGlyphOrder() + mapList = rawTable['mapping'] + mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) + self.mapping = dict(zip(glyphOrder, mapList)) def preWrite(self, font): mapping = getattr(self, "mapping", None) if mapping is None: - mapping = self.mapping = [] + mapping = self.mapping = {} + + glyphOrder = font.getGlyphOrder() + mapping = [mapping[g] for g in glyphOrder] + while len(mapping) > 1 and mapping[-2] == mapping[-1]: + del mapping[-1] + rawTable = { 'mapping': mapping } rawTable['MappingCount'] = len(mapping) - # TODO Remove this abstraction/optimization and move it varLib.builder? - ored = 0 for idx in mapping: ored |= idx @@ -589,9 +596,9 @@ return rawTable def toXML2(self, xmlWriter, font): - for i, value in enumerate(getattr(self, "mapping", [])): + for glyph, value in sorted(getattr(self, "mapping", {}).items()): attrs = ( - ('index', i), + ('glyph', glyph), ('outer', value >> 16), ('inner', value & 0xFFFF), ) @@ -601,12 +608,16 @@ def fromXML(self, name, attrs, content, font): mapping = getattr(self, "mapping", None) if mapping is None: - mapping = [] + mapping = {} self.mapping = mapping + try: + glyph = attrs['glyph'] + except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 + glyph = font.getGlyphOrder()[attrs['index']] outer = safeEval(attrs['outer']) inner = safeEval(attrs['inner']) assert inner <= 0xFFFF - mapping.append((outer << 16) | inner) + mapping[glyph] = (outer << 16) | inner class SingleSubst(FormatSwitchingBaseTable): @@ -819,7 +830,7 @@ if cls: classDefs[glyphOrder[glyphID]] = cls else: - assert 0, "unknown format: %s" % self.Format + log.warning("Unknown ClassDef format: %s", self.Format) self.classDefs = classDefs def _getClassRanges(self, font): @@ -1270,6 +1281,67 @@ return ok +def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): + # split half of the mark classes to the new subtable + classCount = oldSubTable.ClassCount + if classCount < 2: + # oh well, not much left to split... + return False + + oldClassCount = classCount // 2 + newClassCount = classCount - oldClassCount + + oldMarkCoverage, oldMarkRecords = [], [] + newMarkCoverage, newMarkRecords = [], [] + for glyphName, markRecord in zip( + oldSubTable.MarkCoverage.glyphs, + oldSubTable.MarkArray.MarkRecord + ): + if markRecord.Class < oldClassCount: + oldMarkCoverage.append(glyphName) + oldMarkRecords.append(markRecord) + else: + newMarkCoverage.append(glyphName) + newMarkRecords.append(markRecord) + + oldBaseRecords, newBaseRecords = [], [] + for rec in oldSubTable.BaseArray.BaseRecord: + oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() + oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] + newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] + oldBaseRecords.append(oldBaseRecord) + newBaseRecords.append(newBaseRecord) + + newSubTable.Format = oldSubTable.Format + + oldSubTable.MarkCoverage.glyphs = oldMarkCoverage + newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() + newSubTable.MarkCoverage.Format = oldSubTable.MarkCoverage.Format + newSubTable.MarkCoverage.glyphs = newMarkCoverage + + # share the same BaseCoverage in both halves + newSubTable.BaseCoverage = oldSubTable.BaseCoverage + + oldSubTable.ClassCount = oldClassCount + newSubTable.ClassCount = newClassCount + + oldSubTable.MarkArray.MarkRecord = oldMarkRecords + newSubTable.MarkArray = oldSubTable.MarkArray.__class__() + newSubTable.MarkArray.MarkRecord = newMarkRecords + + oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) + newSubTable.MarkArray.MarkCount = len(newMarkRecords) + + oldSubTable.BaseArray.BaseRecord = oldBaseRecords + newSubTable.BaseArray = oldSubTable.BaseArray.__class__() + newSubTable.BaseArray.BaseRecord = newBaseRecords + + oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) + newSubTable.BaseArray.BaseCount = len(newBaseRecords) + + return True + + splitTable = { 'GSUB': { # 1: splitSingleSubst, # 2: splitMultipleSubst, @@ -1284,7 +1356,7 @@ # 1: splitSinglePos, 2: splitPairPos, # 3: splitCursivePos, -# 4: splitMarkBasePos, + 4: splitMarkBasePos, # 5: splitMarkLigPos, # 6: splitMarkMarkPos, # 7: splitContextPos, @@ -1298,7 +1370,6 @@ """ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. """ - ok = 0 table = ttf[overflowRecord.tableType].table lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] subIndex = overflowRecord.SubTableIndex @@ -1319,7 +1390,7 @@ newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType] newExtSubTable = newExtSubTableClass() newExtSubTable.Format = extSubTable.Format - lookup.SubTable.insert(subIndex + 1, newExtSubTable) + toInsert = newExtSubTable newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] newSubTable = newSubTableClass() @@ -1328,7 +1399,7 @@ subTableType = subtable.__class__.LookupType newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] newSubTable = newSubTableClass() - lookup.SubTable.insert(subIndex + 1, newSubTable) + toInsert = newSubTable if hasattr(lookup, 'SubTableCount'): # may not be defined yet. lookup.SubTableCount = lookup.SubTableCount + 1 @@ -1336,9 +1407,16 @@ try: splitFunc = splitTable[overflowRecord.tableType][subTableType] except KeyError: - return ok + log.error( + "Don't know how to split %s lookup type %s", + overflowRecord.tableType, + subTableType, + ) + return False ok = splitFunc(subtable, newSubTable, overflowRecord) + if ok: + lookup.SubTable.insert(subIndex + 1, toInsert) return ok # End of OverFlow logic diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/sbixGlyph.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/sbixGlyph.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/sbixGlyph.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/sbixGlyph.py 2018-07-26 14:12:55.000000000 +0000 @@ -75,7 +75,7 @@ # (needed if you just want to compile the sbix table on its own) self.gid = struct.pack(">H", ttFont.getGlyphID(self.glyphName)) if self.graphicType is None: - self.rawdata = "" + self.rawdata = b"" else: self.rawdata = sstruct.pack(sbixGlyphHeaderFormat, self) + self.imageData diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_s_b_i_x.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_s_b_i_x.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_s_b_i_x.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_s_b_i_x.py 2018-07-26 14:12:55.000000000 +0000 @@ -69,7 +69,7 @@ del self.numStrikes def compile(self, ttFont): - sbixData = "" + sbixData = b"" self.numStrikes = len(self.strikes) sbixHeader = sstruct.pack(sbixHeaderFormat, self) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/sbixStrike.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/sbixStrike.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/sbixStrike.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/sbixStrike.py 2018-07-26 14:12:55.000000000 +0000 @@ -65,8 +65,8 @@ del self.data def compile(self, ttFont): - self.glyphDataOffsets = "" - self.bitmapData = "" + self.glyphDataOffsets = b"" + self.bitmapData = b"" glyphOrder = ttFont.getGlyphOrder() diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_t_r_a_k.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_t_r_a_k.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_t_r_a_k.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_t_r_a_k.py 2018-07-26 14:12:55.000000000 +0000 @@ -92,7 +92,7 @@ trackData.decompile(data, offset) setattr(self, direction + 'Data', trackData) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): writer.simpletag('version', value=self.version) writer.newline() writer.simpletag('format', value=self.format) @@ -194,7 +194,7 @@ self[entry.track] = entry offset += TRACK_TABLE_ENTRY_FORMAT_SIZE - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): nTracks = len(self) nSizes = len(self.sizes()) writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes)) @@ -254,7 +254,7 @@ self.nameIndex = nameIndex self._map = dict(values) - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont): name = ttFont["name"].getDebugName(self.nameIndex) writer.begintag( "trackEntry", diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/TupleVariation.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/TupleVariation.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/TupleVariation.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/TupleVariation.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,6 +1,6 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed, otRound from fontTools.misc.textTools import safeEval import array import io @@ -369,7 +369,7 @@ assert runLength >= 1 and runLength <= 64 stream.write(bytechr(runLength - 1)) for i in range(offset, pos): - stream.write(struct.pack('b', round(deltas[i]))) + stream.write(struct.pack('b', otRound(deltas[i]))) return pos @staticmethod @@ -403,7 +403,7 @@ assert runLength >= 1 and runLength <= 64 stream.write(bytechr(DELTAS_ARE_WORDS | (runLength - 1))) for i in range(offset, pos): - stream.write(struct.pack('>h', round(deltas[i]))) + stream.write(struct.pack('>h', otRound(deltas[i]))) return pos @staticmethod diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_v_h_e_a.py fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_v_h_e_a.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/tables/_v_h_e_a.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/tables/_v_h_e_a.py 2018-07-26 14:12:55.000000000 +0000 @@ -63,9 +63,10 @@ boundsHeightDict[name] = g.yMax - g.yMin elif 'CFF ' in ttFont: topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings for name in ttFont.getGlyphOrder(): - cs = topDict.CharStrings[name] - bounds = cs.calcBounds() + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) if bounds is not None: boundsHeightDict[name] = int( math.ceil(bounds[3]) - math.floor(bounds[1])) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/ttCollection.py fonttools-3.29.0/Snippets/fontTools/ttLib/ttCollection.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/ttCollection.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/ttCollection.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,107 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib.ttFont import TTFont +from fontTools.ttLib.sfnt import readTTCHeader, writeTTCHeader +import struct +import logging + +log = logging.getLogger(__name__) + + +class TTCollection(object): + + """Object representing a TrueType Collection / OpenType Collection. + The main API is self.fonts being a list of TTFont instances. + + If shareTables is True, then different fonts in the collection + might point to the same table object if the data for the table was + the same in the font file. Note, however, that this might result + in suprises and incorrect behavior if the different fonts involved + have different GlyphOrder. Use only if you know what you are doing. + """ + + def __init__(self, file=None, shareTables=False, **kwargs): + fonts = self.fonts = [] + if file is None: + return + + assert 'fontNumber' not in kwargs, kwargs + + if not hasattr(file, "read"): + file = open(file, "rb") + + tableCache = {} if shareTables else None + + header = readTTCHeader(file) + for i in range(header.numFonts): + font = TTFont(file, fontNumber=i, _tableCache=tableCache, **kwargs) + fonts.append(font) + + def save(self, file, shareTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + final = None + file = open(file, "wb") + else: + # assume "file" is a writable file object + # write to a temporary stream to allow saving to unseekable streams + final = file + file = BytesIO() + + tableCache = {} if shareTables else None + + offsets_offset = writeTTCHeader(file, len(self.fonts)) + offsets = [] + for font in self.fonts: + offsets.append(file.tell()) + font._save(file, tableCache=tableCache) + file.seek(0,2) + + file.seek(offsets_offset) + file.write(struct.pack(">%dL" % len(self.fonts), *offsets)) + + if final: + final.write(file.getvalue()) + file.close() + + def saveXML(self, fileOrPath, newlinestr=None, writeVersion=True, **kwargs): + + from fontTools.misc import xmlWriter + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttCollection", ttLibVersion=version) + else: + writer.begintag("ttCollection") + writer.newline() + writer.newline() + + for font in self.fonts: + font._saveXML(writer, writeVersion=False, **kwargs) + writer.newline() + + writer.endtag("ttCollection") + writer.newline() + + writer.close() + + + def __getitem__(self, item): + return self.fonts[item] + + def __setitem__(self, item, value): + self.fonts[item] = values + + def __delitem__(self, item): + return self.fonts[item] + + def __len__(self): + return len(self.fonts) + + def __iter__(self): + return iter(self.fonts) diff -Nru fonttools-3.21.2/Snippets/fontTools/ttLib/ttFont.py fonttools-3.29.0/Snippets/fontTools/ttLib/ttFont.py --- fonttools-3.21.2/Snippets/fontTools/ttLib/ttFont.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttLib/ttFont.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,1002 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc import xmlWriter +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import deprecateArgument +from fontTools.ttLib import TTLibError +from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter +import os +import logging +import itertools + +log = logging.getLogger(__name__) + +class TTFont(object): + + """The main font object. It manages file input and output, and offers + a convenient way of accessing tables. + Tables will be only decompiled when necessary, ie. when they're actually + accessed. This means that simple operations can be extremely fast. + """ + + def __init__(self, file=None, res_name_or_index=None, + sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, + verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, + recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, + _tableCache=None): + + """The constructor can be called with a few different arguments. + When reading a font from disk, 'file' should be either a pathname + pointing to a file, or a readable file object. + + It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt + resource name or an sfnt resource index number or zero. The latter + case will cause TTLib to autodetect whether the file is a flat file + or a suitcase. (If it's a suitcase, only the first 'sfnt' resource + will be read!) + + The 'checkChecksums' argument is used to specify how sfnt + checksums are treated upon reading a file from disk: + 0: don't check (default) + 1: check, print warnings if a wrong checksum is found + 2: check, raise an exception if a wrong checksum is found. + + The TTFont constructor can also be called without a 'file' + argument: this is the way to create a new empty font. + In this case you can optionally supply the 'sfntVersion' argument, + and a 'flavor' which can be None, 'woff', or 'woff2'. + + If the recalcBBoxes argument is false, a number of things will *not* + be recalculated upon save/compile: + 1) 'glyf' glyph bounding boxes + 2) 'CFF ' font bounding box + 3) 'head' font bounding box + 4) 'hhea' min/max values + 5) 'vhea' min/max values + (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). + Additionally, upon importing an TTX file, this option cause glyphs + to be compiled right away. This should reduce memory consumption + greatly, and therefore should have some impact on the time needed + to parse/compile large fonts. + + If the recalcTimestamp argument is false, the modified timestamp in the + 'head' table will *not* be recalculated upon save/compile. + + If the allowVID argument is set to true, then virtual GID's are + supported. Asking for a glyph ID with a glyph name or GID that is not in + the font will return a virtual GID. This is valid for GSUB and cmap + tables. For SING glyphlets, the cmap table is used to specify Unicode + values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested + and does not exist in the font, or the glyphname has the form glyphN + and does not exist in the font, then N is used as the virtual GID. + Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new + virtual GIDs, the next is one less than the previous. + + If ignoreDecompileErrors is set to True, exceptions raised in + individual tables during decompilation will be ignored, falling + back to the DefaultTable implementation, which simply keeps the + binary data. + + If lazy is set to True, many data structures are loaded lazily, upon + access only. If it is set to False, many data structures are loaded + immediately. The default is lazy=None which is somewhere in between. + """ + + for name in ("verbose", "quiet"): + val = locals().get(name) + if val is not None: + deprecateArgument(name, "configure logging instead") + setattr(self, name, val) + + self.lazy = lazy + self.recalcBBoxes = recalcBBoxes + self.recalcTimestamp = recalcTimestamp + self.tables = {} + self.reader = None + + # Permit the user to reference glyphs that are not int the font. + self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. + self.reverseVIDDict = {} + self.VIDDict = {} + self.allowVID = allowVID + self.ignoreDecompileErrors = ignoreDecompileErrors + + if not file: + self.sfntVersion = sfntVersion + self.flavor = flavor + self.flavorData = None + return + if not hasattr(file, "read"): + closeStream = True + # assume file is a string + if res_name_or_index is not None: + # see if it contains 'sfnt' resources in the resource or data fork + from . import macUtils + if res_name_or_index == 0: + if macUtils.getSFNTResIndices(file): + # get the first available sfnt font. + file = macUtils.SFNTResourceReader(file, 1) + else: + file = open(file, "rb") + else: + file = macUtils.SFNTResourceReader(file, res_name_or_index) + else: + file = open(file, "rb") + else: + # assume "file" is a readable file object + closeStream = False + file.seek(0) + + if not self.lazy: + # read input file in memory and wrap a stream around it to allow overwriting + file.seek(0) + tmp = BytesIO(file.read()) + if hasattr(file, 'name'): + # save reference to input file name + tmp.name = file.name + if closeStream: + file.close() + file = tmp + self._tableCache = _tableCache + self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) + self.sfntVersion = self.reader.sfntVersion + self.flavor = self.reader.flavor + self.flavorData = self.reader.flavorData + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + """If we still have a reader object, close it.""" + if self.reader is not None: + self.reader.close() + + def save(self, file, reorderTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + if self.lazy and self.reader.file.name == file: + raise TTLibError( + "Can't overwrite TTFont when 'lazy' attribute is True") + closeStream = True + file = open(file, "wb") + else: + # assume "file" is a writable file object + closeStream = False + + tmp = BytesIO() + + writer_reordersTables = self._save(tmp) + + if (reorderTables is None or writer_reordersTables or + (reorderTables is False and self.reader is None)): + # don't reorder tables and save as is + file.write(tmp.getvalue()) + tmp.close() + else: + if reorderTables is False: + # sort tables using the original font's order + tableOrder = list(self.reader.keys()) + else: + # use the recommended order from the OpenType specification + tableOrder = None + tmp.flush() + tmp2 = BytesIO() + reorderFontTables(tmp, tmp2, tableOrder) + file.write(tmp2.getvalue()) + tmp.close() + tmp2.close() + + if closeStream: + file.close() + + def _save(self, file, tableCache=None): + """Internal function, to be shared by save() and TTCollection.save()""" + + if self.recalcTimestamp and 'head' in self: + self['head'] # make sure 'head' is loaded so the recalculation is actually done + + tags = list(self.keys()) + if "GlyphOrder" in tags: + tags.remove("GlyphOrder") + numTables = len(tags) + # write to a temporary stream to allow saving to unseekable streams + writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData) + + done = [] + for tag in tags: + self._writeTable(tag, writer, done, tableCache) + + writer.close() + + return writer.reordersTables() + + def saveXML(self, fileOrPath, newlinestr=None, **kwargs): + """Export the font as TTX (an XML-based text file), or as a series of text + files when splitTables is true. In the latter case, the 'fileOrPath' + argument should be a path to a directory. + The 'tables' argument must either be false (dump all tables) or a + list of tables to dump. The 'skipTables' argument may be a list of tables + to skip, but only when the 'tables' argument is false. + """ + + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + self._saveXML(writer, **kwargs) + writer.close() + + def _saveXML(self, writer, + writeVersion=True, + quiet=None, tables=None, skipTables=None, splitTables=False, + splitGlyphs=False, disassembleInstructions=True, + bitmapGlyphDataFormat='raw'): + + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + self.disassembleInstructions = disassembleInstructions + self.bitmapGlyphDataFormat = bitmapGlyphDataFormat + if not tables: + tables = list(self.keys()) + if "GlyphOrder" not in tables: + tables = ["GlyphOrder"] + tables + if skipTables: + for tag in skipTables: + if tag in tables: + tables.remove(tag) + numTables = len(tables) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], + ttLibVersion=version) + else: + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) + writer.newline() + + # always splitTables if splitGlyphs is enabled + splitTables = splitTables or splitGlyphs + + if not splitTables: + writer.newline() + else: + path, ext = os.path.splitext(writer.filename) + fileNameTemplate = path + ".%s" + ext + + for i in range(numTables): + tag = tables[i] + if splitTables: + tablePath = fileNameTemplate % tagToIdentifier(tag) + tableWriter = xmlWriter.XMLWriter(tablePath, + newlinestr=writer.newlinestr) + tableWriter.begintag("ttFont", ttLibVersion=version) + tableWriter.newline() + tableWriter.newline() + writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) + writer.newline() + else: + tableWriter = writer + self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) + if splitTables: + tableWriter.endtag("ttFont") + tableWriter.newline() + tableWriter.close() + writer.endtag("ttFont") + writer.newline() + + def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + if tag in self: + table = self[tag] + report = "Dumping '%s' table..." % tag + else: + report = "No '%s' table found." % tag + log.info(report) + if tag not in self: + return + xmlTag = tagToXML(tag) + attrs = dict() + if hasattr(table, "ERROR"): + attrs['ERROR'] = "decompilation error" + from .tables.DefaultTable import DefaultTable + if table.__class__ == DefaultTable: + attrs['raw'] = True + writer.begintag(xmlTag, **attrs) + writer.newline() + if tag == "glyf": + table.toXML(writer, self, splitGlyphs=splitGlyphs) + else: + table.toXML(writer, self) + writer.endtag(xmlTag) + writer.newline() + writer.newline() + + def importXML(self, fileOrPath, quiet=None): + """Import a TTX file (an XML-based text format), so as to recreate + a font object. + """ + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + if "maxp" in self and "post" in self: + # Make sure the glyph order is loaded, as it otherwise gets + # lost if the XML doesn't contain the glyph order, yet does + # contain the table which was originally used to extract the + # glyph names from (ie. 'post', 'cmap' or 'CFF '). + self.getGlyphOrder() + + from fontTools.misc import xmlReader + + reader = xmlReader.XMLReader(fileOrPath, self) + reader.read() + + def isLoaded(self, tag): + """Return true if the table identified by 'tag' has been + decompiled and loaded into memory.""" + return tag in self.tables + + def has_key(self, tag): + if self.isLoaded(tag): + return True + elif self.reader and tag in self.reader: + return True + elif tag == "GlyphOrder": + return True + else: + return False + + __contains__ = has_key + + def keys(self): + keys = list(self.tables.keys()) + if self.reader: + for key in list(self.reader.keys()): + if key not in keys: + keys.append(key) + + if "GlyphOrder" in keys: + keys.remove("GlyphOrder") + keys = sortedTagList(keys) + return ["GlyphOrder"] + keys + + def __len__(self): + return len(list(self.keys())) + + def __getitem__(self, tag): + tag = Tag(tag) + try: + return self.tables[tag] + except KeyError: + if tag == "GlyphOrder": + table = GlyphOrder(tag) + self.tables[tag] = table + return table + if self.reader is not None: + import traceback + log.debug("Reading '%s' table from disk", tag) + data = self.reader[tag] + if self._tableCache is not None: + table = self._tableCache.get((Tag(tag), data)) + if table is not None: + return table + tableClass = getTableClass(tag) + table = tableClass(tag) + self.tables[tag] = table + log.debug("Decompiling '%s' table", tag) + try: + table.decompile(data, self) + except: + if not self.ignoreDecompileErrors: + raise + # fall back to DefaultTable, retaining the binary table data + log.exception( + "An exception occurred during the decompilation of the '%s' table", tag) + from .tables.DefaultTable import DefaultTable + file = StringIO() + traceback.print_exc(file=file) + table = DefaultTable(tag) + table.ERROR = file.getvalue() + self.tables[tag] = table + table.decompile(data, self) + if self._tableCache is not None: + self._tableCache[(Tag(tag), data)] = table + return table + else: + raise KeyError("'%s' table not found" % tag) + + def __setitem__(self, tag, table): + self.tables[Tag(tag)] = table + + def __delitem__(self, tag): + if tag not in self: + raise KeyError("'%s' table not found" % tag) + if tag in self.tables: + del self.tables[tag] + if self.reader and tag in self.reader: + del self.reader[tag] + + def get(self, tag, default=None): + try: + return self[tag] + except KeyError: + return default + + def setGlyphOrder(self, glyphOrder): + self.glyphOrder = glyphOrder + + def getGlyphOrder(self): + try: + return self.glyphOrder + except AttributeError: + pass + if 'CFF ' in self: + cff = self['CFF '] + self.glyphOrder = cff.getGlyphOrder() + elif 'post' in self: + # TrueType font + glyphOrder = self['post'].getGlyphOrder() + if glyphOrder is None: + # + # No names found in the 'post' table. + # Try to create glyph names from the unicode cmap (if available) + # in combination with the Adobe Glyph List (AGL). + # + self._getGlyphNamesFromCmap() + else: + self.glyphOrder = glyphOrder + else: + self._getGlyphNamesFromCmap() + return self.glyphOrder + + def _getGlyphNamesFromCmap(self): + # + # This is rather convoluted, but then again, it's an interesting problem: + # - we need to use the unicode values found in the cmap table to + # build glyph names (eg. because there is only a minimal post table, + # or none at all). + # - but the cmap parser also needs glyph names to work with... + # So here's what we do: + # - make up glyph names based on glyphID + # - load a temporary cmap table based on those names + # - extract the unicode values, build the "real" glyph names + # - unload the temporary cmap table + # + if self.isLoaded("cmap"): + # Bootstrapping: we're getting called by the cmap parser + # itself. This means self.tables['cmap'] contains a partially + # loaded cmap, making it impossible to get at a unicode + # subtable here. We remove the partially loaded cmap and + # restore it later. + # This only happens if the cmap table is loaded before any + # other table that does f.getGlyphOrder() or f.getGlyphName(). + cmapLoading = self.tables['cmap'] + del self.tables['cmap'] + else: + cmapLoading = None + # Make up glyph names based on glyphID, which will be used by the + # temporary cmap and by the real cmap in case we don't find a unicode + # cmap. + numGlyphs = int(self['maxp'].numGlyphs) + glyphOrder = [None] * numGlyphs + glyphOrder[0] = ".notdef" + for i in range(1, numGlyphs): + glyphOrder[i] = "glyph%.5d" % i + # Set the glyph order, so the cmap parser has something + # to work with (so we don't get called recursively). + self.glyphOrder = glyphOrder + + # Make up glyph names based on the reversed cmap table. Because some + # glyphs (eg. ligatures or alternates) may not be reachable via cmap, + # this naming table will usually not cover all glyphs in the font. + # If the font has no Unicode cmap table, reversecmap will be empty. + if 'cmap' in self: + reversecmap = self['cmap'].buildReversed() + else: + reversecmap = {} + useCount = {} + for i in range(numGlyphs): + tempName = glyphOrder[i] + if tempName in reversecmap: + # If a font maps both U+0041 LATIN CAPITAL LETTER A and + # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, + # we prefer naming the glyph as "A". + glyphName = self._makeGlyphName(min(reversecmap[tempName])) + numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 + if numUses > 1: + glyphName = "%s.alt%d" % (glyphName, numUses - 1) + glyphOrder[i] = glyphName + + if 'cmap' in self: + # Delete the temporary cmap table from the cache, so it can + # be parsed again with the right names. + del self.tables['cmap'] + self.glyphOrder = glyphOrder + if cmapLoading: + # restore partially loaded cmap, so it can continue loading + # using the proper names. + self.tables['cmap'] = cmapLoading + + @staticmethod + def _makeGlyphName(codepoint): + from fontTools import agl # Adobe Glyph List + if codepoint in agl.UV2AGL: + return agl.UV2AGL[codepoint] + elif codepoint <= 0xFFFF: + return "uni%04X" % codepoint + else: + return "u%X" % codepoint + + def getGlyphNames(self): + """Get a list of glyph names, sorted alphabetically.""" + glyphNames = sorted(self.getGlyphOrder()) + return glyphNames + + def getGlyphNames2(self): + """Get a list of glyph names, sorted alphabetically, + but not case sensitive. + """ + from fontTools.misc import textTools + return textTools.caselessSort(self.getGlyphOrder()) + + def getGlyphName(self, glyphID, requireReal=False): + try: + return self.getGlyphOrder()[glyphID] + except IndexError: + if requireReal or not self.allowVID: + # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in + # the cmap table than there are glyphs. I don't think it's legal... + return "glyph%.5d" % glyphID + else: + # user intends virtual GID support + try: + glyphName = self.VIDDict[glyphID] + except KeyError: + glyphName ="glyph%.5d" % glyphID + self.last_vid = min(glyphID, self.last_vid ) + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphName + + def getGlyphID(self, glyphName, requireReal=False): + if not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + glyphOrder = self.getGlyphOrder() + d = self._reverseGlyphOrderDict + if glyphName not in d: + if glyphName in glyphOrder: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + else: + if requireReal: + raise KeyError(glyphName) + elif not self.allowVID: + # Handle glyphXXX only + if glyphName[:5] == "glyph": + try: + return int(glyphName[5:]) + except (NameError, ValueError): + raise KeyError(glyphName) + else: + # user intends virtual GID support + try: + glyphID = self.reverseVIDDict[glyphName] + except KeyError: + # if name is in glyphXXX format, use the specified name. + if glyphName[:5] == "glyph": + try: + glyphID = int(glyphName[5:]) + except (NameError, ValueError): + glyphID = None + if glyphID is None: + glyphID = self.last_vid -1 + self.last_vid = glyphID + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphID + + glyphID = d[glyphName] + if glyphName != glyphOrder[glyphID]: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + return glyphID + + def getReverseGlyphMap(self, rebuild=False): + if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + return self._reverseGlyphOrderDict + + def _buildReverseGlyphOrderDict(self): + self._reverseGlyphOrderDict = d = {} + glyphOrder = self.getGlyphOrder() + for glyphID in range(len(glyphOrder)): + d[glyphOrder[glyphID]] = glyphID + + def _writeTable(self, tag, writer, done, tableCache=None): + """Internal helper function for self.save(). Keeps track of + inter-table dependencies. + """ + if tag in done: + return + tableClass = getTableClass(tag) + for masterTable in tableClass.dependencies: + if masterTable not in done: + if masterTable in self: + self._writeTable(masterTable, writer, done, tableCache) + else: + done.append(masterTable) + done.append(tag) + tabledata = self.getTableData(tag) + if tableCache is not None: + entry = tableCache.get((Tag(tag), tabledata)) + if entry is not None: + log.debug("reusing '%s' table", tag) + writer.setEntry(tag, entry) + return + log.debug("writing '%s' table to disk", tag) + writer[tag] = tabledata + if tableCache is not None: + tableCache[(Tag(tag), tabledata)] = writer[tag] + + def getTableData(self, tag): + """Returns raw table data, whether compiled or directly read from disk. + """ + tag = Tag(tag) + if self.isLoaded(tag): + log.debug("compiling '%s' table", tag) + return self.tables[tag].compile(self) + elif self.reader and tag in self.reader: + log.debug("Reading '%s' table from disk", tag) + return self.reader[tag] + else: + raise KeyError(tag) + + def getGlyphSet(self, preferCFF=True): + """Return a generic GlyphSet, which is a dict-like object + mapping glyph names to glyph objects. The returned glyph objects + have a .draw() method that supports the Pen protocol, and will + have an attribute named 'width'. + + If the font is CFF-based, the outlines will be taken from the 'CFF ' or + 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. + If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use + the 'preferCFF' argument to specify which one should be taken. If the + font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. + """ + glyphs = None + if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or + ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): + table_tag = "CFF2" if "CFF2" in self else "CFF " + glyphs = _TTGlyphSet(self, + list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) + + if glyphs is None and "glyf" in self: + glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) + + if glyphs is None: + raise TTLibError("Font contains no outlines") + + return glyphs + + def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): + """Return the 'best' unicode cmap dictionary available in the font, + or None, if no unicode cmap subtable is available. + + By default it will search for the following (platformID, platEncID) + pairs: + (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) + This can be customized via the cmapPreferences argument. + """ + return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) + + +class _TTGlyphSet(object): + + """Generic dict-like GlyphSet class that pulls metrics from hmtx and + glyph shape from TrueType or CFF. + """ + + def __init__(self, ttFont, glyphs, glyphType): + self._glyphs = glyphs + self._hmtx = ttFont['hmtx'] + self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None + self._glyphType = glyphType + + def keys(self): + return list(self._glyphs.keys()) + + def has_key(self, glyphName): + return glyphName in self._glyphs + + __contains__ = has_key + + def __getitem__(self, glyphName): + horizontalMetrics = self._hmtx[glyphName] + verticalMetrics = self._vmtx[glyphName] if self._vmtx else None + return self._glyphType( + self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) + + def __len__(self): + return len(self._glyphs) + + def get(self, glyphName, default=None): + try: + return self[glyphName] + except KeyError: + return default + +class _TTGlyph(object): + + """Wrapper for a TrueType glyph that supports the Pen protocol, meaning + that it has a .draw() method that takes a pen object as its only + argument. Additionally there are 'width' and 'lsb' attributes, read from + the 'hmtx' table. + + If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' + attributes. + """ + + def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): + self._glyphset = glyphset + self._glyph = glyph + self.width, self.lsb = horizontalMetrics + if verticalMetrics: + self.height, self.tsb = verticalMetrics + else: + self.height, self.tsb = None, None + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + self._glyph.draw(pen) + +class _TTGlyphCFF(_TTGlyph): + pass + +class _TTGlyphGlyf(_TTGlyph): + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + glyfTable = self._glyphset._glyphs + glyph = self._glyph + offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 + glyph.draw(pen, glyfTable, offset) + + +class GlyphOrder(object): + + """A pseudo table. The glyph order isn't in the font as a separate + table, but it's nice to present it as such in the TTX format. + """ + + def __init__(self, tag=None): + pass + + def toXML(self, writer, ttFont): + glyphOrder = ttFont.getGlyphOrder() + writer.comment("The 'id' attribute is only for humans; " + "it is ignored when parsed.") + writer.newline() + for i in range(len(glyphOrder)): + glyphName = glyphOrder[i] + writer.simpletag("GlyphID", id=i, name=glyphName) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "glyphOrder"): + self.glyphOrder = [] + ttFont.setGlyphOrder(self.glyphOrder) + if name == "GlyphID": + self.glyphOrder.append(attrs["name"]) + + +def getTableModule(tag): + """Fetch the packer/unpacker module for a table. + Return None when no module is found. + """ + from . import tables + pyTag = tagToIdentifier(tag) + try: + __import__("fontTools.ttLib.tables." + pyTag) + except ImportError as err: + # If pyTag is found in the ImportError message, + # means table is not implemented. If it's not + # there, then some other module is missing, don't + # suppress the error. + if str(err).find(pyTag) >= 0: + return None + else: + raise err + else: + return getattr(tables, pyTag) + + +def getTableClass(tag): + """Fetch the packer/unpacker class for a table. + Return None when no class is found. + """ + module = getTableModule(tag) + if module is None: + from .tables.DefaultTable import DefaultTable + return DefaultTable + pyTag = tagToIdentifier(tag) + tableClass = getattr(module, "table_" + pyTag) + return tableClass + + +def getClassTag(klass): + """Fetch the table tag for a class object.""" + name = klass.__name__ + assert name[:6] == 'table_' + name = name[6:] # Chop 'table_' + return identifierToTag(name) + + +def newTable(tag): + """Return a new instance of a table.""" + tableClass = getTableClass(tag) + return tableClass(tag) + + +def _escapechar(c): + """Helper function for tagToIdentifier()""" + import re + if re.match("[a-z0-9]", c): + return "_" + c + elif re.match("[A-Z]", c): + return c + "_" + else: + return hex(byteord(c))[2:] + + +def tagToIdentifier(tag): + """Convert a table tag to a valid (but UGLY) python identifier, + as well as a filename that's guaranteed to be unique even on a + caseless file system. Each character is mapped to two characters. + Lowercase letters get an underscore before the letter, uppercase + letters get an underscore after the letter. Trailing spaces are + trimmed. Illegal characters are escaped as two hex bytes. If the + result starts with a number (as the result of a hex escape), an + extra underscore is prepended. Examples: + 'glyf' -> '_g_l_y_f' + 'cvt ' -> '_c_v_t' + 'OS/2' -> 'O_S_2f_2' + """ + import re + tag = Tag(tag) + if tag == "GlyphOrder": + return tag + assert len(tag) == 4, "tag should be 4 characters long" + while len(tag) > 1 and tag[-1] == ' ': + tag = tag[:-1] + ident = "" + for c in tag: + ident = ident + _escapechar(c) + if re.match("[0-9]", ident): + ident = "_" + ident + return ident + + +def identifierToTag(ident): + """the opposite of tagToIdentifier()""" + if ident == "GlyphOrder": + return ident + if len(ident) % 2 and ident[0] == "_": + ident = ident[1:] + assert not (len(ident) % 2) + tag = "" + for i in range(0, len(ident), 2): + if ident[i] == "_": + tag = tag + ident[i+1] + elif ident[i+1] == "_": + tag = tag + ident[i] + else: + # assume hex + tag = tag + chr(int(ident[i:i+2], 16)) + # append trailing spaces + tag = tag + (4 - len(tag)) * ' ' + return Tag(tag) + + +def tagToXML(tag): + """Similarly to tagToIdentifier(), this converts a TT tag + to a valid XML element name. Since XML element names are + case sensitive, this is a fairly simple/readable translation. + """ + import re + tag = Tag(tag) + if tag == "OS/2": + return "OS_2" + elif tag == "GlyphOrder": + return tag + if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): + return tag.strip() + else: + return tagToIdentifier(tag) + + +def xmlToTag(tag): + """The opposite of tagToXML()""" + if tag == "OS_2": + return Tag("OS/2") + if len(tag) == 8: + return identifierToTag(tag) + else: + return Tag(tag + " " * (4 - len(tag))) + + + +# Table order as recommended in the OpenType specification 1.4 +TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", + "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", + "kern", "name", "post", "gasp", "PCLT"] + +OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", + "CFF "] + +def sortedTagList(tagList, tableOrder=None): + """Return a sorted copy of tagList, sorted according to the OpenType + specification, or according to a custom tableOrder. If given and not + None, tableOrder needs to be a list of tag names. + """ + tagList = sorted(tagList) + if tableOrder is None: + if "DSIG" in tagList: + # DSIG should be last (XXX spec reference?) + tagList.remove("DSIG") + tagList.append("DSIG") + if "CFF " in tagList: + tableOrder = OTFTableOrder + else: + tableOrder = TTFTableOrder + orderedTables = [] + for tag in tableOrder: + if tag in tagList: + orderedTables.append(tag) + tagList.remove(tag) + orderedTables.extend(tagList) + return orderedTables + + +def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): + """Rewrite a font file, ordering the tables as recommended by the + OpenType specification 1.4. + """ + inFile.seek(0) + outFile.seek(0) + reader = SFNTReader(inFile, checkChecksums=checkChecksums) + writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) + tables = list(reader.keys()) + for tag in sortedTagList(tables, tableOrder): + writer[tag] = reader[tag] + writer.close() + + +def maxPowerOfTwo(x): + """Return the highest exponent of two, so that + (2 ** exponent) <= x. Return 0 if x is 0. + """ + exponent = 0 + while x: + x = x >> 1 + exponent = exponent + 1 + return max(exponent - 1, 0) + + +def getSearchRange(n, itemSize=16): + """Calculate searchRange, entrySelector, rangeShift. + """ + # itemSize defaults to 16, for backward compatibility + # with upstream fonttools. + exponent = maxPowerOfTwo(n) + searchRange = (2 ** exponent) * itemSize + entrySelector = exponent + rangeShift = max(0, n * itemSize - searchRange) + return searchRange, entrySelector, rangeShift diff -Nru fonttools-3.21.2/Snippets/fontTools/ttx.py fonttools-3.29.0/Snippets/fontTools/ttx.py --- fonttools-3.21.2/Snippets/fontTools/ttx.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/ttx.py 2018-07-26 14:12:55.000000000 +0000 @@ -38,6 +38,10 @@ to the individual table dumps. This file can be used as input to ttx, as long as the table files are in the same directory. + -g Split glyf table: Save the glyf data into separate TTX files + per glyph and write a small TTX for the glyf table which + contains references to the individual TTGlyph elements. + NOTE: specifying -g implies -s (no need for -s together with -g) -i Do NOT disassemble TT instructions: when this option is given, all TrueType programs (glyph programs, the font program and the pre-program) will be written to the TTX file as hex data @@ -110,6 +114,7 @@ verbose = False quiet = False splitTables = False + splitGlyphs = False disassembleInstructions = True mergeFile = None recalcBBoxes = True @@ -160,6 +165,10 @@ self.skipTables.append(value) elif option == "-s": self.splitTables = True + elif option == "-g": + # -g implies (and forces) splitTables + self.splitGlyphs = True + self.splitTables = True elif option == "-i": self.disassembleInstructions = False elif option == "-z": @@ -209,7 +218,6 @@ self.logLevel = logging.INFO if self.mergeFile and self.flavor: raise getopt.GetoptError("-m and --flavor options are mutually exclusive") - sys.exit(2) if self.onlyTables and self.skipTables: raise getopt.GetoptError("-t and -x options are mutually exclusive") if self.mergeFile and numFiles > 1: @@ -223,9 +231,9 @@ reader = ttf.reader tags = sorted(reader.keys()) print('Listing table info for "%s":' % input) - format = " %4s %10s %7s %7s" - print(format % ("tag ", " checksum", " length", " offset")) - print(format % ("----", "----------", "-------", "-------")) + format = " %4s %10s %8s %8s" + print(format % ("tag ", " checksum", " length", " offset")) + print(format % ("----", "----------", "--------", "--------")) for tag in tags: entry = reader.tables[tag] if ttf.flavor == "woff2": @@ -255,6 +263,7 @@ tables=options.onlyTables, skipTables=options.skipTables, splitTables=options.splitTables, + splitGlyphs=options.splitGlyphs, disassembleInstructions=options.disassembleInstructions, bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, newlinestr=options.newlinestr) @@ -318,7 +327,7 @@ def parseOptions(args): - rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sim:z:baey:", + rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", ['unicodedata=', "recalc-timestamp", 'flavor=', 'version', 'with-zopfli', 'newline=']) diff -Nru fonttools-3.21.2/Snippets/fontTools/unicodedata/__init__.py fonttools-3.29.0/Snippets/fontTools/unicodedata/__init__.py --- fonttools-3.21.2/Snippets/fontTools/unicodedata/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/unicodedata/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -13,7 +13,7 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * -from . import Blocks, Scripts, ScriptExtensions +from . import Blocks, Scripts, ScriptExtensions, OTTags __all__ = [tostr(s) for s in ( @@ -38,6 +38,9 @@ "script_extension", "script_name", "script_code", + "script_horizontal_direction", + "ot_tags_from_script", + "ot_tag_to_script", )] @@ -72,7 +75,7 @@ >>> script_extension("a") == {'Latn'} True - >>> script_extension(unichr(0x060C)) == {'Arab', 'Syrc', 'Thaa'} + >>> script_extension(unichr(0x060C)) == {'Arab', 'Rohg', 'Syrc', 'Thaa'} True >>> script_extension(unichr(0x10FFFF)) == {'Zzzz'} True @@ -133,6 +136,75 @@ return default +# The data on script direction is taken from harfbuzz's "hb-common.cc": +# https://goo.gl/X5FDXC +# It matches the CLDR "scriptMetadata.txt as of January 2018: +# http://unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt +RTL_SCRIPTS = { + # Unicode-1.1 additions + 'Arab', # Arabic + 'Hebr', # Hebrew + + # Unicode-3.0 additions + 'Syrc', # Syriac + 'Thaa', # Thaana + + # Unicode-4.0 additions + 'Cprt', # Cypriot + + # Unicode-4.1 additions + 'Khar', # Kharoshthi + + # Unicode-5.0 additions + 'Phnx', # Phoenician + 'Nkoo', # Nko + + # Unicode-5.1 additions + 'Lydi', # Lydian + + # Unicode-5.2 additions + 'Avst', # Avestan + 'Armi', # Imperial Aramaic + 'Phli', # Inscriptional Pahlavi + 'Prti', # Inscriptional Parthian + 'Sarb', # Old South Arabian + 'Orkh', # Old Turkic + 'Samr', # Samaritan + + # Unicode-6.0 additions + 'Mand', # Mandaic + + # Unicode-6.1 additions + 'Merc', # Meroitic Cursive + 'Mero', # Meroitic Hieroglyphs + + # Unicode-7.0 additions + 'Mani', # Manichaean + 'Mend', # Mende Kikakui + 'Nbat', # Nabataean + 'Narb', # Old North Arabian + 'Palm', # Palmyrene + 'Phlp', # Psalter Pahlavi + + # Unicode-8.0 additions + 'Hatr', # Hatran + 'Hung', # Old Hungarian + + # Unicode-9.0 additions + 'Adlm', # Adlam +} + +def script_horizontal_direction(script_code, default=KeyError): + """ Return "RTL" for scripts that contain right-to-left characters + according to the Bidi_Class property. Otherwise return "LTR". + """ + if script_code not in Scripts.NAMES: + if isinstance(default, type) and issubclass(default, KeyError): + raise default(script_code) + return default + return str("RTL") if script_code in RTL_SCRIPTS else str("LTR") + + def block(char): """ Return the block property assigned to the Unicode character 'char' as a string. @@ -147,3 +219,58 @@ code = byteord(char) i = bisect_right(Blocks.RANGES, code) return Blocks.VALUES[i-1] + + +def ot_tags_from_script(script_code): + """ Return a list of OpenType script tags associated with a given + Unicode script code. + Return ['DFLT'] script tag for invalid/unknown script codes. + """ + if script_code not in Scripts.NAMES: + return [OTTags.DEFAULT_SCRIPT] + + script_tags = [ + OTTags.SCRIPT_EXCEPTIONS.get( + script_code, + script_code[0].lower() + script_code[1:] + ) + ] + if script_code in OTTags.NEW_SCRIPT_TAGS: + script_tags.extend(OTTags.NEW_SCRIPT_TAGS[script_code]) + script_tags.reverse() # last in, first out + + return script_tags + + +def ot_tag_to_script(tag): + """ Return the Unicode script code for the given OpenType script tag, or + None for "DFLT" tag or if there is no Unicode script associated with it. + Raises ValueError if the tag is invalid. + """ + tag = tostr(tag).strip() + if not tag or " " in tag or len(tag) > 4: + raise ValueError("invalid OpenType tag: %r" % tag) + + while len(tag) != 4: + tag += str(" ") # pad with spaces + + if tag == OTTags.DEFAULT_SCRIPT: + # it's unclear which Unicode script the "DFLT" OpenType tag maps to, + # so here we return None + return None + + if tag in OTTags.NEW_SCRIPT_TAGS_REVERSED: + return OTTags.NEW_SCRIPT_TAGS_REVERSED[tag] + + # This side of the conversion is fully algorithmic + + # Any spaces at the end of the tag are replaced by repeating the last + # letter. Eg 'nko ' -> 'Nkoo'. + # Change first char to uppercase + script_code = tag[0].upper() + tag[1] + for i in range(2, 4): + script_code += (script_code[i-1] if tag[i] == " " else tag[i]) + + if script_code not in Scripts.NAMES: + return None + return script_code diff -Nru fonttools-3.21.2/Snippets/fontTools/unicodedata/OTTags.py fonttools-3.29.0/Snippets/fontTools/unicodedata/OTTags.py --- fonttools-3.21.2/Snippets/fontTools/unicodedata/OTTags.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/unicodedata/OTTags.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,41 @@ +# Data updated to OpenType 1.8.2 as of January 2018. + +# Complete list of OpenType script tags at: +# https://www.microsoft.com/typography/otspec/scripttags.htm + +# Most of the script tags are the same as the ISO 15924 tag but lowercased, +# so we only have to handle the exceptional cases: +# - KATAKANA and HIRAGANA both map to 'kana'; +# - spaces at the end are preserved, unlike ISO 15924; +# - we map special script codes for Inherited, Common and Unknown to DFLT. + +DEFAULT_SCRIPT = "DFLT" + +SCRIPT_EXCEPTIONS = { + "Hira": "kana", + "Hrkt": "kana", + "Laoo": "lao ", + "Yiii": "yi ", + "Nkoo": "nko ", + "Vaii": "vai ", + "Zinh": DEFAULT_SCRIPT, + "Zyyy": DEFAULT_SCRIPT, + "Zzzz": DEFAULT_SCRIPT, +} + +NEW_SCRIPT_TAGS = { + "Beng": ("bng2",), + "Deva": ("dev2",), + "Gujr": ("gjr2",), + "Guru": ("gur2",), + "Knda": ("knd2",), + "Mlym": ("mlm2",), + "Orya": ("ory2",), + "Taml": ("tml2",), + "Telu": ("tel2",), + "Mymr": ("mym2",), +} + +NEW_SCRIPT_TAGS_REVERSED = { + value: key for key, values in NEW_SCRIPT_TAGS.items() for value in values +} diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/builder.py fonttools-3.29.0/Snippets/fontTools/varLib/builder.py --- fonttools-3.21.2/Snippets/fontTools/varLib/builder.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/builder.py 2018-07-26 14:12:55.000000000 +0000 @@ -28,31 +28,35 @@ return self -def _reorderItem(lst, narrows): +def _reorderItem(lst, narrows, zeroes): out = [] count = len(lst) for i in range(count): if i not in narrows: out.append(lst[i]) for i in range(count): - if i in narrows: + if i in narrows and i not in zeroes: out.append(lst[i]) return out -def varDataCalculateNumShorts(self, optimize=True): +def VarData_CalculateNumShorts(self, optimize=True): count = self.VarRegionCount items = self.Item narrows = set(range(count)) + zeroes = set(range(count)) for item in items: wides = [i for i in narrows if not (-128 <= item[i] <= 127)] narrows.difference_update(wides) - if not narrows: + nonzeroes = [i for i in zeroes if item[i]] + zeroes.difference_update(nonzeroes) + if not narrows and not zeroes: break if optimize: # Reorder columns such that all SHORT columns come before UINT8 - self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows) + self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes) + self.VarRegionCount = len(self.VarRegionIndex) for i in range(self.ItemCount): - items[i] = _reorderItem(items[i], narrows) + items[i] = _reorderItem(items[i], narrows, zeroes) self.NumShorts = count - len(narrows) else: wides = set(range(count)) - narrows @@ -69,7 +73,7 @@ assert len(item) == regionCount records.append(list(item)) self.ItemCount = len(self.Item) - varDataCalculateNumShorts(self, optimize=optimize) + VarData_CalculateNumShorts(self, optimize=optimize) return self @@ -84,10 +88,9 @@ # Variation helpers -def buildVarIdxMap(varIdxes): - # TODO Change VarIdxMap mapping to hold separate outer,inner indices +def buildVarIdxMap(varIdxes, glyphOrder): self = ot.VarIdxMap() - self.mapping = list(varIdxes) + self.mapping = {g:v for g,v in zip(glyphOrder, varIdxes)} return self def buildVarDevTable(varIdx): diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/featureVars.py fonttools-3.29.0/Snippets/fontTools/varLib/featureVars.py --- fonttools-3.21.2/Snippets/fontTools/varLib/featureVars.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/featureVars.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,392 @@ +"""Module to build FeatureVariation tables: +https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table + +NOTE: The API is experimental and subject to change. +""" +from __future__ import print_function, absolute_import, division + +from fontTools.ttLib import newTable +from fontTools.ttLib.tables import otTables as ot +from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable +import itertools + + +def addFeatureVariations(font, conditionalSubstitutions): + """Add conditional substitutions to a Variable Font. + + The `conditionalSubstitutions` argument is a list of (Region, Substitutions) + tuples. + + A Region is a list of Spaces. A Space is a dict mapping axisTags to + (minValue, maxValue) tuples. Irrelevant axes may be omitted. + A Space represents a 'rectangular' subset of an N-dimensional design space. + A Region represents a more complex subset of an N-dimensional design space, + ie. the union of all the Spaces in the Region. + For efficiency, Spaces within a Region should ideally not overlap, but + functionality is not compromised if they do. + + The minimum and maximum values are expressed in normalized coordinates. + + A Substitution is a dict mapping source glyph names to substitute glyph names. + """ + + # Example: + # + # >>> f = TTFont(srcPath) + # >>> condSubst = [ + # ... # A list of (Region, Substitution) tuples. + # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), + # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), + # ... ] + # >>> addFeatureVariations(f, condSubst) + # >>> f.save(dstPath) + + # Since the FeatureVariations table will only ever match one rule at a time, + # we will make new rules for all possible combinations of our input, so we + # can indirectly support overlapping rules. + explodedConditionalSubstitutions = [] + for combination in iterAllCombinations(len(conditionalSubstitutions)): + regions = [] + lookups = [] + for index in combination: + regions.append(conditionalSubstitutions[index][0]) + lookups.append(conditionalSubstitutions[index][1]) + if not regions: + continue + intersection = regions[0] + for region in regions[1:]: + intersection = intersectRegions(intersection, region) + for space in intersection: + # Remove default values, so we don't generate redundant ConditionSets + space = cleanupSpace(space) + if space: + explodedConditionalSubstitutions.append((space, lookups)) + + addFeatureVariationsRaw(font, explodedConditionalSubstitutions) + + +def iterAllCombinations(numRules): + """Given a number of rules, yield all the combinations of indices, sorted + by decreasing length, so we get the most specialized rules first. + + >>> list(iterAllCombinations(0)) + [] + >>> list(iterAllCombinations(1)) + [(0,)] + >>> list(iterAllCombinations(2)) + [(0, 1), (0,), (1,)] + >>> list(iterAllCombinations(3)) + [(0, 1, 2), (0, 1), (0, 2), (1, 2), (0,), (1,), (2,)] + """ + indices = range(numRules) + for length in range(numRules, 0, -1): + for combinations in itertools.combinations(indices, length): + yield combinations + + +# +# Region and Space support +# +# Terminology: +# +# A 'Space' is a dict representing a "rectangular" bit of N-dimensional space. +# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. +# Missing dimensions (keys) are substituted by the default min and max values +# from the corresponding axes. +# +# A 'Region' is a list of Space dicts, representing the union of the Spaces, +# therefore representing a more complex subset of design space. +# + +def intersectRegions(region1, region2): + """Return the region intersecting `region1` and `region2`. + + >>> intersectRegions([], []) + [] + >>> intersectRegions([{'wdth': (0.0, 1.0)}], []) + [] + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-1.0, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0)}], [{'wght': (-1.0, 0.0)}]) + True + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], [{'wght': (-1.0, 0.0)}]) + True + >>> intersectRegions( + ... [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], + ... [{'wdth': (-1.0, 0.0), 'wght': (-1.0, 0.0)}]) + [] + + """ + region = [] + for space1 in region1: + for space2 in region2: + space = intersectSpaces(space1, space2) + if space is not None: + region.append(space) + return region + + +def intersectSpaces(space1, space2): + """Return the space intersected by `space1` and `space2`, or None if there + is no intersection. + + >>> intersectSpaces({}, {}) + {} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {}) + {'wdth': (-0.5, 0.5)} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 0.5)} + >>> expected = {'wdth': (0.0, 0.5), 'wght': (0.25, 0.5)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5), 'wght': (0.0, 0.5)}, {'wdth': (0.0, 1.0), 'wght': (0.25, 0.75)}) + True + >>> expected = {'wdth': (-0.5, 0.5), 'wght': (0.0, 1.0)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5)}, {'wght': (0.0, 1.0)}) + True + >>> intersectSpaces({'wdth': (-0.5, 0)}, {'wdth': (0.1, 0.5)}) + + """ + space = {} + space.update(space1) + space.update(space2) + for axisTag in set(space1) & set(space2): + min1, max1 = space1[axisTag] + min2, max2 = space2[axisTag] + minimum = max(min1, min2) + maximum = min(max1, max2) + if not minimum < maximum: + return None + space[axisTag] = minimum, maximum + return space + + +def cleanupSpace(space): + """Return a sparse copy of `space`, without redundant (default) values. + + >>> cleanupSpace({}) + {} + >>> cleanupSpace({'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 1.0)} + >>> cleanupSpace({'wdth': (-1.0, 1.0)}) + {} + + """ + return {tag: limit for tag, limit in space.items() if limit != (-1.0, 1.0)} + + +# +# Low level implementation +# + +def addFeatureVariationsRaw(font, conditionalSubstitutions): + """Low level implementation of addFeatureVariations that directly + models the possibilities of the FeatureVariations table.""" + + # + # assert there is no 'rvrn' feature + # make dummy 'rvrn' feature with no lookups + # sort features, get 'rvrn' feature index + # add 'rvrn' feature to all scripts + # make lookups + # add feature variations + # + + if "GSUB" not in font: + font["GSUB"] = buildGSUB() + + gsub = font["GSUB"].table + + if gsub.Version < 0x00010001: + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.FeatureVariations = None # delete any existing FeatureVariations + + for feature in gsub.FeatureList.FeatureRecord: + assert feature.FeatureTag != 'rvrn' + + rvrnFeature = buildFeatureRecord('rvrn', []) + gsub.FeatureList.FeatureRecord.append(rvrnFeature) + + sortFeatureList(gsub) + rvrnFeatureIndex = gsub.FeatureList.FeatureRecord.index(rvrnFeature) + + for scriptRecord in gsub.ScriptList.ScriptRecord: + for langSys in [scriptRecord.Script.DefaultLangSys] + scriptRecord.Script.LangSysRecord: + langSys.FeatureIndex.append(rvrnFeatureIndex) + + # setup lookups + + # turn substitution dicts into tuples of tuples, so they are hashable + conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(conditionalSubstitutions) + + lookupMap = buildSubstitutionLookups(gsub, allSubstitutions) + + axisIndices = {axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)} + + featureVariationRecords = [] + for conditionSet, substitutions in conditionalSubstitutions: + conditionTable = [] + for axisTag, (minValue, maxValue) in sorted(conditionSet.items()): + assert minValue < maxValue + ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) + conditionTable.append(ct) + + lookupIndices = [lookupMap[subst] for subst in substitutions] + record = buildFeatureTableSubstitutionRecord(rvrnFeatureIndex, lookupIndices) + featureVariationRecords.append(buildFeatureVariationRecord(conditionTable, [record])) + + gsub.FeatureVariations = buildFeatureVariations(featureVariationRecords) + + +# +# Building GSUB/FeatureVariations internals +# + +def buildGSUB(): + """Build a GSUB table from scratch.""" + fontTable = newTable("GSUB") + gsub = fontTable.table = ot.GSUB() + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.ScriptList = ot.ScriptList() + gsub.ScriptList.ScriptRecord = [] + gsub.FeatureList = ot.FeatureList() + gsub.FeatureList.FeatureRecord = [] + gsub.LookupList = ot.LookupList() + gsub.LookupList.Lookup = [] + + srec = ot.ScriptRecord() + srec.ScriptTag = 'DFLT' + srec.Script = ot.Script() + srec.Script.DefaultLangSys = None + srec.Script.LangSysRecord = [] + + langrec = ot.LangSysRecord() + langrec.LangSys = ot.LangSys() + langrec.LangSys.ReqFeatureIndex = 0xFFFF + langrec.LangSys.FeatureIndex = [0] + srec.Script.DefaultLangSys = langrec.LangSys + + gsub.ScriptList.ScriptRecord.append(srec) + gsub.FeatureVariations = None + + return fontTable + + +def makeSubstitutionsHashable(conditionalSubstitutions): + """Turn all the substitution dictionaries in sorted tuples of tuples so + they are hashable, to detect duplicates so we don't write out redundant + data.""" + allSubstitutions = set() + condSubst = [] + for conditionSet, substitutionMaps in conditionalSubstitutions: + substitutions = [] + for substitutionMap in substitutionMaps: + subst = tuple(sorted(substitutionMap.items())) + substitutions.append(subst) + allSubstitutions.add(subst) + condSubst.append((conditionSet, substitutions)) + return condSubst, sorted(allSubstitutions) + + +def buildSubstitutionLookups(gsub, allSubstitutions): + """Build the lookups for the glyph substitutions, return a dict mapping + the substitution to lookup indices.""" + firstIndex = len(gsub.LookupList.Lookup) + lookupMap = {} + for i, substitutionMap in enumerate(allSubstitutions): + lookupMap[substitutionMap] = i + firstIndex + + for subst in allSubstitutions: + substMap = dict(subst) + lookup = buildLookup([buildSingleSubstSubtable(substMap)]) + gsub.LookupList.Lookup.append(lookup) + assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup + return lookupMap + + +def buildFeatureVariations(featureVariationRecords): + """Build the FeatureVariations subtable.""" + fv = ot.FeatureVariations() + fv.Version = 0x00010000 + fv.FeatureVariationRecord = featureVariationRecords + return fv + + +def buildFeatureRecord(featureTag, lookupListIndices): + """Build a FeatureRecord.""" + fr = ot.FeatureRecord() + fr.FeatureTag = featureTag + fr.Feature = ot.Feature() + fr.Feature.LookupListIndex = lookupListIndices + return fr + + +def buildFeatureVariationRecord(conditionTable, substitutionRecords): + """Build a FeatureVariationRecord.""" + fvr = ot.FeatureVariationRecord() + fvr.ConditionSet = ot.ConditionSet() + fvr.ConditionSet.ConditionTable = conditionTable + fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() + fvr.FeatureTableSubstitution.Version = 0x00010001 + fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords + return fvr + + +def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices): + """Build a FeatureTableSubstitutionRecord.""" + ftsr = ot.FeatureTableSubstitutionRecord() + ftsr.FeatureIndex = featureIndex + ftsr.Feature = ot.Feature() + ftsr.Feature.LookupListIndex = lookupListIndices + return ftsr + + +def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue): + """Build a ConditionTable.""" + ct = ot.ConditionTable() + ct.Format = 1 + ct.AxisIndex = axisIndex + ct.FilterRangeMinValue = filterRangeMinValue + ct.FilterRangeMaxValue = filterRangeMaxValue + return ct + + +def sortFeatureList(table): + """Sort the feature list by feature tag, and remap the feature indices + elsewhere. This is needed after the feature list has been modified. + """ + # decorate, sort, undecorate, because we need to make an index remapping table + tagIndexFea = [(fea.FeatureTag, index, fea) for index, fea in enumerate(table.FeatureList.FeatureRecord)] + tagIndexFea.sort() + table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea] + featureRemap = dict(zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))) + + # Remap the feature indices + remapFeatures(table, featureRemap) + + +def remapFeatures(table, featureRemap): + """Go through the scripts list, and remap feature indices.""" + for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord): + defaultLangSys = script.Script.DefaultLangSys + if defaultLangSys is not None: + _remapLangSys(defaultLangSys, featureRemap) + for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord): + langSys = langSysRec.LangSys + _remapLangSys(langSys, featureRemap) + + if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: + for fvr in table.FeatureVariations.FeatureVariationRecord: + for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: + ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex] + + +def _remapLangSys(langSys, featureRemap): + if langSys.ReqFeatureIndex != 0xffff: + langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex] + langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex] + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/__init__.py fonttools-3.29.0/Snippets/fontTools/varLib/__init__.py --- fonttools-3.21.2/Snippets/fontTools/varLib/__init__.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/__init__.py 2018-07-26 14:12:55.000000000 +0000 @@ -10,17 +10,18 @@ them. Such ttf-interpolatable and designspace files can be generated from a Glyphs source, eg., using noto-source as an example: - $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs + $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs Then you can make a variable-font this way: - $ fonttools varLib master_ufo/NotoSansArabic.designspace + $ fonttools varLib master_ufo/NotoSansArabic.designspace API *will* change in near future. """ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc.arrayTools import Vector from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.tables._n_a_m_e import NameRecord @@ -29,6 +30,7 @@ from fontTools.ttLib.tables.ttProgram import Program from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import OTTableWriter from fontTools.varLib import builder, designspace, models, varStore from fontTools.varLib.merger import VariationMerger, _all_equal from fontTools.varLib.mvar import MVAR_ENTRIES @@ -168,20 +170,25 @@ return avar def _add_stat(font, axes): + # for now we just get the axis tags and nameIDs from the fvar, + # so we can reuse the same nameIDs which were defined in there. + # TODO make use of 'axes' once it adds style attributes info: + # https://github.com/LettError/designSpaceDocument/issues/8 - nameTable = font['name'] + if "STAT" in font: + return + + fvarTable = font['fvar'] - assert "STAT" not in font STAT = font["STAT"] = newTable('STAT') stat = STAT.table = ot.STAT() - stat.Version = 0x00010000 + stat.Version = 0x00010002 axisRecords = [] - for i,a in enumerate(axes.values()): + for i, a in enumerate(fvarTable.axes): axis = ot.AxisRecord() - axis.AxisTag = Tag(a.tag) - # Meh. Reuse fvar nameID! - axis.AxisNameID = nameTable.addName(tounicode(a.labelname['en'])) + axis.AxisTag = Tag(a.axisTag) + axis.AxisNameID = a.axisNameID axis.AxisOrdering = i axisRecords.append(axis) @@ -192,6 +199,10 @@ stat.DesignAxisCount = len(axisRecords) stat.DesignAxisRecord = axisRecordArray + # for the elided fallback name, we default to the base style name. + # TODO make this user-configurable via designspace document + stat.ElidedFallbackNameID = 2 + # TODO Move to glyf or gvar table proper def _GetCoordinates(font, glyphName): """font, glyphName --> glyph coordinates as expected by "gvar" table @@ -258,8 +269,12 @@ glyph.recalcBounds(glyf) - horizontalAdvanceWidth = round(rightSideX - leftSideX) - leftSideBearing = round(glyph.xMin - leftSideX) + horizontalAdvanceWidth = otRound(rightSideX - leftSideX) + if horizontalAdvanceWidth < 0: + # unlikely, but it can happen, see: + # https://github.com/fonttools/fonttools/pull/1198 + horizontalAdvanceWidth = 0 + leftSideBearing = otRound(glyph.xMin - leftSideX) # XXX Handle vertical font["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing @@ -397,7 +412,7 @@ deltas = model.getDeltas(all_cvs) supports = model.supports for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): - delta = [round(d) for d in delta] + delta = [otRound(d) for d in delta] if all(abs(v) <= tolerance for v in delta): continue var = TupleVariation(support, delta) @@ -412,46 +427,58 @@ for glyph in font.getGlyphOrder(): hAdvances = [metrics[glyph][0] for metrics in metricses] # TODO move round somewhere else? - hAdvanceDeltas[glyph] = tuple(round(d) for d in model.getDeltas(hAdvances)[1:]) - - # We only support the direct mapping right now. + hAdvanceDeltas[glyph] = tuple(otRound(d) for d in model.getDeltas(hAdvances)[1:]) + # Direct mapping supports = model.supports[1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) n = len(supports) items = [] - zeroes = [0]*n for glyphName in font.getGlyphOrder(): - items.append(hAdvanceDeltas.get(glyphName, zeroes)) - while items and items[-1] is zeroes: - del items[-1] - - advanceMapping = None - # Add indirect mapping to save on duplicates - uniq = set(items) - # TODO Improve heuristic - if (len(items) - len(uniq)) * len(varTupleIndexes) > len(items): - newItems = sorted(uniq) - mapper = {v:i for i,v in enumerate(newItems)} - mapping = [mapper[item] for item in items] - while len(mapping) > 1 and mapping[-1] == mapping[-2]: - del mapping[-1] - advanceMapping = builder.buildVarIdxMap(mapping) - items = newItems - del mapper, mapping, newItems - del uniq + items.append(hAdvanceDeltas[glyphName]) + + # Build indirect mapping to save on duplicates, compare both sizes + uniq = list(set(items)) + mapper = {v:i for i,v in enumerate(uniq)} + mapping = [mapper[item] for item in items] + advanceMapping = builder.buildVarIdxMap(mapping, font.getGlyphOrder()) + # Direct varData = builder.buildVarData(varTupleIndexes, items) - varstore = builder.buildVarStore(varTupleList, [varData]) + directStore = builder.buildVarStore(varTupleList, [varData]) + + # Indirect + varData = builder.buildVarData(varTupleIndexes, uniq) + indirectStore = builder.buildVarStore(varTupleList, [varData]) + mapping = indirectStore.optimize() + advanceMapping.mapping = {k:mapping[v] for k,v in advanceMapping.mapping.items()} + + # Compile both, see which is more compact + + writer = OTTableWriter() + directStore.compile(writer, font) + directSize = len(writer.getAllData()) + + writer = OTTableWriter() + indirectStore.compile(writer, font) + advanceMapping.compile(writer, font) + indirectSize = len(writer.getAllData()) + + use_direct = directSize < indirectSize + # Done; put it all together. assert "HVAR" not in font HVAR = font["HVAR"] = newTable('HVAR') hvar = HVAR.table = ot.HVAR() hvar.Version = 0x00010000 - hvar.VarStore = varstore - hvar.AdvWidthMap = advanceMapping hvar.LsbMap = hvar.RsbMap = None + if use_direct: + hvar.VarStore = directStore + hvar.AdvWidthMap = None + else: + hvar.VarStore = indirectStore + hvar.AdvWidthMap = advanceMapping def _add_MVAR(font, model, master_ttfs, axisTags): @@ -494,11 +521,17 @@ assert "MVAR" not in font if records: + store = store_builder.finish() + # Optimize + mapping = store.optimize() + for rec in records: + rec.VarIdx = mapping[rec.VarIdx] + MVAR = font["MVAR"] = newTable('MVAR') mvar = MVAR.table = ot.MVAR() mvar.Version = 0x00010000 mvar.Reserved = 0 - mvar.VarStore = store_builder.finish() + mvar.VarStore = store # XXX these should not be hard-coded but computed automatically mvar.ValueRecordSize = 8 mvar.ValueRecordCount = len(records) @@ -511,7 +544,11 @@ merger = VariationMerger(model, axisTags, font) merger.mergeTables(font, master_fonts, ['GPOS']) + # TODO Merge GSUB + # TODO Merge GDEF itself! store = merger.store_builder.finish() + if not store.VarData: + return try: GDEF = font['GDEF'].table assert GDEF.Version <= 0x00010002 @@ -522,6 +559,12 @@ GDEF.Version = 0x00010003 GDEF.VarStore = store + # Optimize + varidx_map = store.optimize() + GDEF.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + # Pretty much all of this file should be redesigned and moved inot submodules... @@ -653,8 +696,8 @@ # Normalize master locations - normalized_master_locs = [o['location'] for o in masters] - log.info("Internal master locations:\n%s", pformat(normalized_master_locs)) + internal_master_locs = [o['location'] for o in masters] + log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} @@ -663,7 +706,7 @@ internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) - normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs] + normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) @@ -679,7 +722,7 @@ return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances -def build(designspace_filename, master_finder=lambda s:s): +def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True): """ Build variation font from a designspace file. @@ -700,8 +743,10 @@ # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(vf, axes, instances) - _add_stat(vf, axes) - _add_avar(vf, axes) + if 'STAT' not in exclude: + _add_stat(vf, axes) + if 'avar' not in exclude: + _add_avar(vf, axes) del instances # Map from axis names to axis tags... @@ -715,32 +760,102 @@ assert 0 == model.mapping[base_idx] log.info("Building variations tables") - _add_MVAR(vf, model, master_fonts, axisTags) - _add_HVAR(vf, model, master_fonts, axisTags) - _merge_OTL(vf, model, master_fonts, axisTags) - if 'glyf' in vf: - _add_gvar(vf, model, master_fonts) + if 'MVAR' not in exclude: + _add_MVAR(vf, model, master_fonts, axisTags) + if 'HVAR' not in exclude: + _add_HVAR(vf, model, master_fonts, axisTags) + if 'GDEF' not in exclude or 'GPOS' not in exclude: + _merge_OTL(vf, model, master_fonts, axisTags) + if 'gvar' not in exclude and 'glyf' in vf: + _add_gvar(vf, model, master_fonts, optimize=optimize) + if 'cvar' not in exclude and 'glyf' in vf: _merge_TTHinting(vf, model, master_fonts) + for tag in exclude: + if tag in vf: + del vf[tag] + return vf, model, master_ttfs +class MasterFinder(object): + + def __init__(self, template): + self.template = template + + def __call__(self, src_path): + fullname = os.path.abspath(src_path) + dirname, basename = os.path.split(fullname) + stem, ext = os.path.splitext(basename) + path = self.template.format( + fullname=fullname, + dirname=dirname, + basename=basename, + stem=stem, + ext=ext, + ) + return os.path.normpath(path) + + def main(args=None): from argparse import ArgumentParser from fontTools import configLogger parser = ArgumentParser(prog='varLib') parser.add_argument('designspace') + parser.add_argument( + '-o', + metavar='OUTPUTFILE', + dest='outfile', + default=None, + help='output file' + ) + parser.add_argument( + '-x', + metavar='TAG', + dest='exclude', + action='append', + default=[], + help='exclude table' + ) + parser.add_argument( + '--disable-iup', + dest='optimize', + action='store_false', + help='do not perform IUP optimization' + ) + parser.add_argument( + '--master-finder', + default='master_ttf_interpolatable/{stem}.ttf', + help=( + 'templated string used for finding binary font ' + 'files given the source file names defined in the ' + 'designspace document. The following special strings ' + 'are defined: {fullname} is the absolute source file ' + 'name; {basename} is the file name without its ' + 'directory; {stem} is the basename without the file ' + 'extension; {ext} is the source file extension; ' + '{dirname} is the directory of the absolute file ' + 'name. The default value is "%(default)s".' + ) + ) options = parser.parse_args(args) # TODO: allow user to configure logging via command-line options configLogger(level="INFO") designspace_filename = options.designspace - finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf') - outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' - - vf, model, master_ttfs = build(designspace_filename, finder) + finder = MasterFinder(options.master_finder) + outfile = options.outfile + if outfile is None: + outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' + + vf, model, master_ttfs = build( + designspace_filename, + finder, + exclude=options.exclude, + optimize=options.optimize + ) log.info("Saving variation font %s", outfile) vf.save(outfile) diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/merger.py fonttools-3.29.0/Snippets/fontTools/varLib/merger.py --- fonttools-3.21.2/Snippets/fontTools/varLib/merger.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/merger.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,6 +3,7 @@ """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.misc import classifyTools from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otBase as otBase @@ -660,8 +661,8 @@ YCoords = [a.YCoordinate for a in lst] model = merger.model scalars = merger.scalars - self.XCoordinate = round(model.interpolateFromMastersAndScalars(XCoords, scalars)) - self.YCoordinate = round(model.interpolateFromMastersAndScalars(YCoords, scalars)) + self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) + self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) @InstancerMerger.merger(otBase.ValueRecord) def merge(merger, self, lst): @@ -677,7 +678,7 @@ if hasattr(self, name): values = [getattr(a, name, 0) for a in lst] - value = round(model.interpolateFromMastersAndScalars(values, scalars)) + value = otRound(model.interpolateFromMastersAndScalars(values, scalars)) setattr(self, name, value) @@ -738,7 +739,7 @@ assert dev.DeltaFormat == 0x8000 varidx = (dev.StartSize << 16) + dev.EndSize - delta = round(instancer[varidx]) + delta = otRound(instancer[varidx]) attr = v+'Coordinate' setattr(self, attr, getattr(self, attr) + delta) @@ -769,7 +770,7 @@ assert dev.DeltaFormat == 0x8000 varidx = (dev.StartSize << 16) + dev.EndSize - delta = round(instancer[varidx]) + delta = otRound(instancer[varidx]) setattr(self, name, getattr(self, name) + delta) diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/models.py fonttools-3.29.0/Snippets/fontTools/varLib/models.py --- fonttools-3.21.2/Snippets/fontTools/varLib/models.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/models.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,6 +4,7 @@ __all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel'] + def normalizeValue(v, triple): """Normalizes value based on a min/default/max triple. >>> normalizeValue(400, (100, 400, 900)) @@ -152,12 +153,12 @@ {0: 1.0}, {0: 1.0}, {0: 1.0, 4: 1.0, 5: 1.0}, - {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, {0: 1.0, 3: 0.75, 4: 0.25, 5: 0.6666666666666667, - 6: 0.16666666666666669, + 6: 0.4444444444444445, 7: 0.6666666666666667}] """ @@ -170,7 +171,7 @@ self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above - self._computeMasterSupports(axisPoints) + self._computeMasterSupports(axisPoints, axisOrder) @staticmethod def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): @@ -183,7 +184,9 @@ value = loc[axis] if axis not in axisPoints: axisPoints[axis] = {0.} - assert value not in axisPoints[axis] + assert value not in axisPoints[axis], ( + 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints) + ) axisPoints[axis].add(value) def getKey(axisPoints, axisOrder): @@ -221,7 +224,7 @@ else: return value - def _computeMasterSupports(self, axisPoints): + def _computeMasterSupports(self, axisPoints, axisOrder): supports = [] deltaWeights = [] locations = self.locations @@ -229,11 +232,15 @@ box = {} # Account for axisPoints first + # TODO Use axis min/max instead? Isn't that always -1/+1? for axis,values in axisPoints.items(): if not axis in loc: continue locV = loc[axis] - box[axis] = (self.lowerBound(locV, values), locV, self.upperBound(locV, values)) + if locV > 0: + box[axis] = (0, locV, max({locV}|values)) + else: + box[axis] = (min({locV}|values), locV, 0) locAxes = set(loc.keys()) # Walk over previous masters now @@ -243,21 +250,42 @@ continue # If it's NOT in the current box, it does not participate relevant = True - for axis, (lower,_,upper) in box.items(): - if axis in m and not (lower < m[axis] < upper): + for axis, (lower,peak,upper) in box.items(): + if axis not in m or not (m[axis] == peak or lower < m[axis] < upper): relevant = False break if not relevant: continue - # Split the box for new master - for axis,val in m.items(): + + # Split the box for new master; split in whatever direction + # that has largest range ratio. See commit for details. + orderedAxes = [axis for axis in axisOrder if axis in m.keys()] + orderedAxes.extend([axis for axis in sorted(m.keys()) if axis not in axisOrder]) + bestAxis = None + bestRatio = -1 + for axis in orderedAxes: + val = m[axis] assert axis in box lower,locV,upper = box[axis] + newLower, newUpper = lower, upper if val < locV: - lower = val + newLower = val + ratio = (val - locV) / (lower - locV) elif locV < val: - upper = val - box[axis] = (lower,locV,upper) + newUpper = val + ratio = (val - locV) / (upper - locV) + else: # val == locV + # Can't split box in this direction. + continue + if ratio > bestRatio: + bestRatio = ratio + bestAxis = axis + bestLower = newLower + bestUpper = newUpper + bestLocV = locV + + if bestAxis: + box[bestAxis] = (bestLower,bestLocV,bestUpper) supports.append(box) deltaWeight = {} @@ -311,6 +339,46 @@ return self.interpolateFromDeltasAndScalars(deltas, scalars) +def main(args): + from fontTools import configLogger + + args = args[1:] + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + if len(args) < 1: + print("usage: fonttools varLib.models source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr) + sys.exit(1) + + from pprint import pprint + + if len(args) == 1 and args[0].endswith('.designspace'): + from fontTools.designspaceLib import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read(args[0]) + locs = [s.location for s in doc.sources] + print("Original locations:") + pprint(locs) + doc.normalize() + print("Normalized locations:") + pprint(locs) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + + model = VariationModel(locs) + print("Sorted locations:") + pprint(model.locations) + print("Supports:") + pprint(model.supports) + if __name__ == "__main__": import doctest, sys + + if len(sys.argv) > 1: + sys.exit(main(sys.argv)) + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/mutator.py fonttools-3.29.0/Snippets/fontTools/varLib/mutator.py --- fonttools-3.21.2/Snippets/fontTools/varLib/mutator.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/mutator.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,11 +1,11 @@ """ Instantiate a variation font. Run, eg: -$ python mutator.py ./NotoSansArabic-VF.ttf wght=140 wdth=85 +$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85 """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import floatToFixedToFloat +from fontTools.misc.fixedTools import floatToFixedToFloat, otRound from fontTools.ttLib import TTFont from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.varLib import _GetCoordinates, _SetCoordinates, _DesignspaceAxis @@ -20,6 +20,13 @@ log = logging.getLogger("fontTools.varlib.mutator") +# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest +OS2_WIDTH_CLASS_VALUES = {} +percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0] +for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1): + half = (prev + curr) / 2 + OS2_WIDTH_CLASS_VALUES[half] = i + def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary @@ -87,7 +94,7 @@ if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): - cvt[i] += round(delta) + cvt[i] += otRound(delta) if 'MVAR' in varfont: log.info("Mutating MVAR table") @@ -99,7 +106,7 @@ if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] - delta = round(varStoreInstancer[rec.VarIdx]) + delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, @@ -112,6 +119,32 @@ log.info("Building interpolated tables") merger.instantiate() + if 'name' in varfont: + log.info("Pruning name table") + exclude = {a.axisNameID for a in fvar.axes} + for i in fvar.instances: + exclude.add(i.subfamilyNameID) + exclude.add(i.postscriptNameID) + varfont['name'].names[:] = [ + n for n in varfont['name'].names + if n.nameID not in exclude + ] + + if "wght" in location and "OS/2" in varfont: + varfont["OS/2"].usWeightClass = otRound( + max(1, min(location["wght"], 1000)) + ) + if "wdth" in location: + wdth = location["wdth"] + for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): + if wdth < percent: + varfont["OS/2"].usWidthClass = widthClass + break + else: + varfont["OS/2"].usWidthClass = 9 + if "slnt" in location and "post" in varfont: + varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) + log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: @@ -122,23 +155,44 @@ def main(args=None): from fontTools import configLogger + import argparse - if args is None: - import sys - args = sys.argv[1:] - - varfilename = args[0] - locargs = args[1:] - outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' - - # TODO Allow to specify logging verbosity as command line option - configLogger(level=logging.INFO) + parser = argparse.ArgumentParser( + "fonttools varLib.mutator", description="Instantiate a variable font") + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input variable TTF file.") + parser.add_argument( + "locargs", metavar="AXIS=LOC", nargs="*", + help="List of space separated locations. A location consist in " + "the name of a variation axis, followed by '=' and a number. E.g.: " + " wght=700 wdth=80. The default is the location of the base master.") + parser.add_argument( + "-o", "--output", metavar="OUTPUT.ttf", default=None, + help="Output instance TTF file (default: INPUT-instance.ttf).") + logging_group = parser.add_mutually_exclusive_group(required=False) + logging_group.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely.") + logging_group.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off.") + options = parser.parse_args(args) + + varfilename = options.input + outfile = ( + os.path.splitext(varfilename)[0] + '-instance.ttf' + if not options.output else options.output) + configLogger(level=( + "DEBUG" if options.verbose else + "ERROR" if options.quiet else + "INFO")) loc = {} - for arg in locargs: - tag,val = arg.split('=') - assert len(tag) <= 4 - loc[tag.ljust(4)] = float(val) + for arg in options.locargs: + try: + tag, val = arg.split('=') + assert len(tag) <= 4 + loc[tag.ljust(4)] = float(val) + except (ValueError, AssertionError): + parser.error("invalid location argument format: %r" % arg) log.info("Location: %s", loc) log.info("Loading variable font") diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/plot.py fonttools-3.29.0/Snippets/fontTools/varLib/plot.py --- fonttools-3.21.2/Snippets/fontTools/varLib/plot.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/plot.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,118 @@ +"""Visualize DesignSpaceDocument and resulting VariationModel.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.varLib.models import VariationModel, supportScalar +from fontTools.designspaceLib import DesignSpaceDocument +from mpl_toolkits.mplot3d import axes3d +from matplotlib import pyplot +from itertools import cycle +import math +import logging +import sys + +log = logging.getLogger(__name__) + + +def stops(support, count=10): + a,b,c = support + + return [a + (b - a) * i / count for i in range(count)] + \ + [b + (c - b) * i / count for i in range(count)] + \ + [c] + +def plotLocations(locations, axes, axis3D, **kwargs): + for loc,color in zip(locations, cycle(pyplot.cm.Set1.colors)): + axis3D.plot([loc.get(axes[0], 0)], + [loc.get(axes[1], 0)], + [1.], + 'o', + color=color, + **kwargs) + +def plotLocationsSurfaces(locations, fig, names=None, **kwargs): + + assert len(locations[0].keys()) == 2 + + if names is None: + names = [''] + + n = len(locations) + cols = math.ceil(n**.5) + rows = math.ceil(n / cols) + + model = VariationModel(locations) + names = [names[model.reverseMapping[i]] for i in range(len(names))] + + ax1, ax2 = sorted(locations[0].keys()) + for i, (support,color, name) in enumerate(zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))): + + axis3D = fig.add_subplot(rows, cols, i + 1, projection='3d') + axis3D.set_title(name) + axis3D.set_xlabel(ax1) + axis3D.set_ylabel(ax2) + pyplot.xlim(-1.,+1.) + pyplot.ylim(-1.,+1.) + + Xs = support.get(ax1, (-1.,0.,+1.)) + Ys = support.get(ax2, (-1.,0.,+1.)) + for x in stops(Xs): + X, Y, Z = [], [], [] + for y in Ys: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + for y in stops(Ys): + X, Y, Z = [], [], [] + for x in Xs: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + + plotLocations(model.locations, [ax1, ax2], axis3D) + + +def plotDocument(doc, fig, **kwargs): + doc.normalize() + locations = [s.location for s in doc.sources] + names = [s.name for s in doc.sources] + plotLocationsSurfaces(locations, fig, names, **kwargs) + + +def main(args=None): + from fontTools import configLogger + + if args is None: + args = sys.argv[1:] + + # configure the library logger (for >= WARNING) + configLogger() + # comment this out to enable debug messages from logger + # log.setLevel(logging.DEBUG) + + if len(args) < 1: + print("usage: fonttools varLib.plot source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.plot location1 location2 ...", file=sys.stderr) + sys.exit(1) + + fig = pyplot.figure() + + if len(args) == 1 and args[0].endswith('.designspace'): + doc = DesignSpaceDocument() + doc.read(args[0]) + plotDocument(doc, fig) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + plotLocationsSurfaces(locs, fig) + + pyplot.show() + +if __name__ == '__main__': + import sys + sys.exit(main()) diff -Nru fonttools-3.21.2/Snippets/fontTools/varLib/varStore.py fonttools-3.29.0/Snippets/fontTools/varLib/varStore.py --- fonttools-3.21.2/Snippets/fontTools/varLib/varStore.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/varLib/varStore.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,10 +1,14 @@ from __future__ import print_function, division, absolute_import -from __future__ import unicode_literals from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.ttLib.tables import otTables as ot from fontTools.varLib.models import supportScalar from fontTools.varLib.builder import (buildVarRegionList, buildVarStore, buildVarRegion, buildVarData, - varDataCalculateNumShorts) + VarData_CalculateNumShorts) +from functools import partial +from collections import defaultdict +from array import array def _getLocationKey(loc): @@ -18,14 +22,27 @@ self._regionMap = {} self._regionList = buildVarRegionList([], axisTags) self._store = buildVarStore(self._regionList, []) + self._data = None + self._model = None + self._cache = {} def setModel(self, model): self._model = model + self._cache = {} # Empty cached items + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.VarDataCount = len(self._store.VarData) + for data in self._store.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data, optimize) + return self._store + + def _add_VarData(self): regionMap = self._regionMap regionList = self._regionList - regions = model.supports[1:] + regions = self._model.supports[1:] regionIndices = [] for region in regions: key = _getLocationKey(region) @@ -40,21 +57,26 @@ self._outer = len(self._store.VarData) self._store.VarData.append(data) - def finish(self, optimize=True): - self._regionList.RegionCount = len(self._regionList.Region) - self._store.VarDataCount = len(self._store.VarData) - for data in self._store.VarData: - data.ItemCount = len(data.Item) - varDataCalculateNumShorts(data, optimize) - return self._store - def storeMasters(self, master_values): - deltas = [round(d) for d in self._model.getDeltas(master_values)] + deltas = [otRound(d) for d in self._model.getDeltas(master_values)] base = deltas.pop(0) + deltas = tuple(deltas) + varIdx = self._cache.get(deltas) + if varIdx is not None: + return base, varIdx + + if not self._data: + self._add_VarData() inner = len(self._data.Item) + if inner == 0xFFFF: + # Full array. Start new one. + self._add_VarData() + return self.storeMasters(master_values) self._data.Item.append(deltas) - # TODO Check for full data array? - return base, (self._outer << 16) + inner + + varIdx = (self._outer << 16) + inner + self._cache[deltas] = varIdx + return base, varIdx def VarRegion_get_support(self, fvar_axes): @@ -98,3 +120,401 @@ delta += d * s return delta + +# +# Optimizations +# + +def VarStore_subset_varidxes(self, varIdxes, optimize=True): + + # Sort out used varIdxes by major/minor. + used = {} + for varIdx in varIdxes: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + d = used.get(major) + if d is None: + d = used[major] = set() + d.add(minor) + del varIdxes + + # + # Subset VarData + # + + varData = self.VarData + newVarData = [] + varDataMap = {} + for major,data in enumerate(varData): + usedMinors = used.get(major) + if usedMinors is None: + continue + newMajor = varDataMap[major] = len(newVarData) + newVarData.append(data) + + items = data.Item + newItems = [] + for minor in sorted(usedMinors): + newMinor = len(newItems) + newItems.append(items[minor]) + varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor + + data.Item = newItems + data.ItemCount = len(data.Item) + + if optimize: + VarData_CalculateNumShorts(data) + + self.VarData = newVarData + self.VarDataCount = len(self.VarData) + + self.prune_regions() + + return varDataMap + +ot.VarStore.subset_varidxes = VarStore_subset_varidxes + +def VarStore_prune_regions(self): + """Remove unused VarRegions.""" + # + # Subset VarRegionList + # + + # Collect. + usedRegions = set() + for data in self.VarData: + usedRegions.update(data.VarRegionIndex) + # Subset. + regionList = self.VarRegionList + regions = regionList.Region + newRegions = [] + regionMap = {} + for i in sorted(usedRegions): + regionMap[i] = len(newRegions) + newRegions.append(regions[i]) + regionList.Region = newRegions + regionList.RegionCount = len(regionList.Region) + # Map. + for data in self.VarData: + data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] + +ot.VarStore.prune_regions = VarStore_prune_regions + + +def _visit(self, objType, func): + """Recurse down from self, if type of an object is objType, + call func() on it. Only works for otData-style classes.""" + + if type(self) == objType: + func(self) + return # We don't recurse down; don't need to. + + if isinstance(self, list): + for that in self: + _visit(that, objType, func) + + if hasattr(self, 'getConverters'): + for conv in self.getConverters(): + that = getattr(self, conv.name, None) + if that is not None: + _visit(that, objType, func) + + if isinstance(self, ot.ValueRecord): + for that in self.__dict__.values(): + _visit(that, objType, func) + +def _Device_recordVarIdx(self, s): + """Add VarIdx in this Device table (if any) to the set s.""" + if self.DeltaFormat == 0x8000: + s.add((self.StartSize<<16)+self.EndSize) + +def Object_collect_device_varidxes(self, varidxes): + adder = partial(_Device_recordVarIdx, s=varidxes) + _visit(self, ot.Device, adder) + +ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes +ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes + +def _Device_mapVarIdx(self, mapping, done): + """Add VarIdx in this Device table (if any) to the set s.""" + if id(self) in done: + return + done.add(id(self)) + if self.DeltaFormat == 0x8000: + varIdx = mapping[(self.StartSize<<16)+self.EndSize] + self.StartSize = varIdx >> 16 + self.EndSize = varIdx & 0xFFFF + +def Object_remap_device_varidxes(self, varidxes_map): + mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set()) + _visit(self, ot.Device, mapper) + +ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes +ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes + + +class _Encoding(object): + + def __init__(self, chars): + self.chars = chars + self.width = self._popcount(chars) + self.overhead = self._characteristic_overhead(chars) + self.items = set() + + def append(self, row): + self.items.add(row) + + def extend(self, lst): + self.items.update(lst) + + def get_room(self): + """Maximum number of bytes that can be added to characteristic + while still being beneficial to merge it into another one.""" + count = len(self.items) + return max(0, (self.overhead - 1) // count - self.width) + room = property(get_room) + + @property + def gain(self): + """Maximum possible byte gain from merging this into another + characteristic.""" + count = len(self.items) + return max(0, self.overhead - count * (self.width + 1)) + + def sort_key(self): + return self.width, self.chars + + def __len__(self): + return len(self.items) + + def can_encode(self, chars): + return not (chars & ~self.chars) + + def __sub__(self, other): + return self._popcount(self.chars & ~other.chars) + + @staticmethod + def _popcount(n): + # Apparently this is the fastest native way to do it... + # https://stackoverflow.com/a/9831671 + return bin(n).count('1') + + @staticmethod + def _characteristic_overhead(chars): + """Returns overhead in bytes of encoding this characteristic + as a VarData.""" + c = 6 + while chars: + if chars & 3: + c += 2 + chars >>= 2 + return c + + + def _find_yourself_best_new_encoding(self, done_by_width): + self.best_new_encoding = None + for new_width in range(self.width+1, self.width+self.room+1): + for new_encoding in done_by_width[new_width]: + if new_encoding.can_encode(self.chars): + break + else: + new_encoding = None + self.best_new_encoding = new_encoding + + +class _EncodingDict(dict): + + def __missing__(self, chars): + r = self[chars] = _Encoding(chars) + return r + + def add_row(self, row): + chars = self._row_characteristics(row) + self[chars].append(row) + + @staticmethod + def _row_characteristics(row): + """Returns encoding characteristics for a row.""" + chars = 0 + i = 1 + for v in row: + if v: + chars += i + if not (-128 <= v <= 127): + chars += i * 2 + i <<= 2 + return chars + + +def VarStore_optimize(self): + """Optimize storage. Returns mapping from old VarIdxes to new ones.""" + + # TODO + # Check that no two VarRegions are the same; if they are, fold them. + + n = len(self.VarRegionList.Region) # Number of columns + zeroes = array('h', [0]*n) + + front_mapping = {} # Map from old VarIdxes to full row tuples + + encodings = _EncodingDict() + + # Collect all items into a set of full rows (with lots of zeroes.) + for major,data in enumerate(self.VarData): + regionIndices = data.VarRegionIndex + + for minor,item in enumerate(data.Item): + + row = array('h', zeroes) + for regionIdx,v in zip(regionIndices, item): + row[regionIdx] += v + row = tuple(row) + + encodings.add_row(row) + front_mapping[(major<<16)+minor] = row + + # Separate encodings that have no gain (are decided) and those having + # possible gain (possibly to be merged into others.) + encodings = sorted(encodings.values(), key=_Encoding.__len__, reverse=True) + done_by_width = defaultdict(list) + todo = [] + for encoding in encodings: + if not encoding.gain: + done_by_width[encoding.width].append(encoding) + else: + todo.append(encoding) + + # For each encoding that is possibly to be merged, find the best match + # in the decided encodings, and record that. + todo.sort(key=_Encoding.get_room) + for encoding in todo: + encoding._find_yourself_best_new_encoding(done_by_width) + + # Walk through todo encodings, for each, see if merging it with + # another todo encoding gains more than each of them merging with + # their best decided encoding. If yes, merge them and add resulting + # encoding back to todo queue. If not, move the enconding to decided + # list. Repeat till done. + while todo: + encoding = todo.pop() + best_idx = None + best_gain = 0 + for i,other_encoding in enumerate(todo): + combined_chars = other_encoding.chars | encoding.chars + combined_width = _Encoding._popcount(combined_chars) + combined_overhead = _Encoding._characteristic_overhead(combined_chars) + combined_gain = ( + + encoding.overhead + + other_encoding.overhead + - combined_overhead + - (combined_width - encoding.width) * len(encoding) + - (combined_width - other_encoding.width) * len(other_encoding) + ) + this_gain = 0 if encoding.best_new_encoding is None else ( + + encoding.overhead + - (encoding.best_new_encoding.width - encoding.width) * len(encoding) + ) + other_gain = 0 if other_encoding.best_new_encoding is None else ( + + other_encoding.overhead + - (other_encoding.best_new_encoding.width - other_encoding.width) * len(other_encoding) + ) + separate_gain = this_gain + other_gain + + if combined_gain > separate_gain: + best_idx = i + best_gain = combined_gain - separate_gain + + if best_idx is None: + # Encoding is decided as is + done_by_width[encoding.width].append(encoding) + else: + other_encoding = todo[best_idx] + combined_chars = other_encoding.chars | encoding.chars + combined_encoding = _Encoding(combined_chars) + combined_encoding.extend(encoding.items) + combined_encoding.extend(other_encoding.items) + combined_encoding._find_yourself_best_new_encoding(done_by_width) + del todo[best_idx] + todo.append(combined_encoding) + + # Assemble final store. + back_mapping = {} # Mapping from full rows to new VarIdxes + encodings = sum(done_by_width.values(), []) + encodings.sort(key=_Encoding.sort_key) + self.VarData = [] + for major,encoding in enumerate(encodings): + data = ot.VarData() + self.VarData.append(data) + data.VarRegionIndex = range(n) + data.VarRegionCount = len(data.VarRegionIndex) + data.Item = sorted(encoding.items) + for minor,item in enumerate(data.Item): + back_mapping[item] = (major<<16)+minor + + # Compile final mapping. + varidx_map = {} + for k,v in front_mapping.items(): + varidx_map[k] = back_mapping[v] + + # Remove unused regions. + self.prune_regions() + + # Recalculate things and go home. + self.VarRegionList.RegionCount = len(self.VarRegionList.Region) + self.VarDataCount = len(self.VarData) + for data in self.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data) + + return varidx_map + +ot.VarStore.optimize = VarStore_optimize + + +def main(args=None): + from argparse import ArgumentParser + from fontTools import configLogger + from fontTools.ttLib import TTFont + from fontTools.ttLib.tables.otBase import OTTableWriter + + parser = ArgumentParser(prog='varLib.varStore') + parser.add_argument('fontfile') + parser.add_argument('outfile', nargs='?') + options = parser.parse_args(args) + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + fontfile = options.fontfile + outfile = options.outfile + + font = TTFont(fontfile) + gdef = font['GDEF'] + store = gdef.table.VarStore + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("Before: %7d bytes" % size) + + varidx_map = store.optimize() + + gdef.table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("After: %7d bytes" % size) + + if outfile is not None: + font.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff -Nru fonttools-3.21.2/Snippets/fontTools/voltLib/ast.py fonttools-3.29.0/Snippets/fontTools/voltLib/ast.py --- fonttools-3.21.2/Snippets/fontTools/voltLib/ast.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/voltLib/ast.py 2018-07-26 14:12:55.000000000 +0000 @@ -4,7 +4,7 @@ class Statement(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -12,7 +12,7 @@ class Expression(object): - def __init__(self, location): + def __init__(self, location=None): self.location = location def build(self, builder): @@ -20,7 +20,7 @@ class Block(Statement): - def __init__(self, location): + def __init__(self, location=None): Statement.__init__(self, location) self.statements = [] @@ -35,7 +35,7 @@ class LookupBlock(Block): - def __init__(self, location, name): + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -46,7 +46,7 @@ class GlyphDefinition(Statement): - def __init__(self, location, name, gid, gunicode, gtype, components): + def __init__(self, name, gid, gunicode, gtype, components, location=None): Statement.__init__(self, location) self.name = name self.id = gid @@ -56,7 +56,7 @@ class GroupDefinition(Statement): - def __init__(self, location, name, enum): + def __init__(self, name, enum, location=None): Statement.__init__(self, location) self.name = name self.enum = enum @@ -78,7 +78,7 @@ class GlyphName(Expression): """A single glyph name, such as cedilla.""" - def __init__(self, location, glyph): + def __init__(self, glyph, location=None): Expression.__init__(self, location) self.glyph = glyph @@ -88,7 +88,7 @@ class Enum(Expression): """An enum""" - def __init__(self, location, enum): + def __init__(self, enum, location=None): Expression.__init__(self, location) self.enum = enum @@ -108,7 +108,7 @@ class GroupName(Expression): """A glyph group""" - def __init__(self, location, group, parser): + def __init__(self, group, parser, location=None): Expression.__init__(self, location) self.group = group self.parser_ = parser @@ -126,7 +126,7 @@ class Range(Expression): """A glyph range""" - def __init__(self, location, start, end, parser): + def __init__(self, start, end, parser, location=None): Expression.__init__(self, location) self.start = start self.end = end @@ -138,7 +138,7 @@ class ScriptDefinition(Statement): - def __init__(self, location, name, tag, langs): + def __init__(self, name, tag, langs, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -146,7 +146,7 @@ class LangSysDefinition(Statement): - def __init__(self, location, name, tag, features): + def __init__(self, name, tag, features, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -154,7 +154,7 @@ class FeatureDefinition(Statement): - def __init__(self, location, name, tag, lookups): + def __init__(self, name, tag, lookups, location=None): Statement.__init__(self, location) self.name = name self.tag = tag @@ -162,8 +162,8 @@ class LookupDefinition(Statement): - def __init__(self, location, name, process_base, process_marks, direction, - reversal, comments, context, sub, pos): + def __init__(self, name, process_base, process_marks, direction, + reversal, comments, context, sub, pos, location=None): Statement.__init__(self, location) self.name = name self.process_base = process_base @@ -177,47 +177,43 @@ class SubstitutionDefinition(Statement): - def __init__(self, location, mapping): + def __init__(self, mapping, location=None): Statement.__init__(self, location) self.mapping = mapping class SubstitutionSingleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionMultipleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionLigatureDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): - def __init__(self, location, mapping): - SubstitutionDefinition.__init__(self, location, mapping) + pass class PositionAttachDefinition(Statement): - def __init__(self, location, coverage, coverage_to): + def __init__(self, coverage, coverage_to, location=None): Statement.__init__(self, location) self.coverage = coverage self.coverage_to = coverage_to class PositionAttachCursiveDefinition(Statement): - def __init__(self, location, coverages_exit, coverages_enter): + def __init__(self, coverages_exit, coverages_enter, location=None): Statement.__init__(self, location) self.coverages_exit = coverages_exit self.coverages_enter = coverages_enter class PositionAdjustPairDefinition(Statement): - def __init__(self, location, coverages_1, coverages_2, adjust_pair): + def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): Statement.__init__(self, location) self.coverages_1 = coverages_1 self.coverages_2 = coverages_2 @@ -225,22 +221,22 @@ class PositionAdjustSingleDefinition(Statement): - def __init__(self, location, adjust_single): + def __init__(self, adjust_single, location=None): Statement.__init__(self, location) self.adjust_single = adjust_single class ContextDefinition(Statement): - def __init__(self, location, ex_or_in, left=[], right=[]): + def __init__(self, ex_or_in, left=None, right=None, location=None): Statement.__init__(self, location) self.ex_or_in = ex_or_in - self.left = left - self.right = right + self.left = left if left is not None else [] + self.right = right if right is not None else [] class AnchorDefinition(Statement): - def __init__(self, location, name, gid, glyph_name, component, locked, - pos): + def __init__(self, name, gid, glyph_name, component, locked, + pos, location=None): Statement.__init__(self, location) self.name = name self.gid = gid @@ -251,7 +247,7 @@ class SettingDefinition(Statement): - def __init__(self, location, name, value): + def __init__(self, name, value, location=None): Statement.__init__(self, location) self.name = name self.value = value diff -Nru fonttools-3.21.2/Snippets/fontTools/voltLib/parser.py fonttools-3.29.0/Snippets/fontTools/voltLib/parser.py --- fonttools-3.21.2/Snippets/fontTools/voltLib/parser.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/fontTools/voltLib/parser.py 2018-07-26 14:12:55.000000000 +0000 @@ -87,8 +87,9 @@ 'Glyph "%s" (gid %i) already defined' % (name, gid), location ) - def_glyph = ast.GlyphDefinition(location, name, gid, - gunicode, gtype, components) + def_glyph = ast.GlyphDefinition(name, gid, + gunicode, gtype, components, + location=location) self.glyphs_.define(name, def_glyph) return def_glyph @@ -107,7 +108,8 @@ 'group names are case insensitive' % name, location ) - def_group = ast.GroupDefinition(location, name, enum) + def_group = ast.GroupDefinition(name, enum, + location=location) self.groups_.define(name, def_group) return def_group @@ -142,7 +144,7 @@ langs.append(lang) self.expect_keyword_("END_SCRIPT") self.langs_.exit_scope() - def_script = ast.ScriptDefinition(location, name, tag, langs) + def_script = ast.ScriptDefinition(name, tag, langs, location=location) self.scripts_.define(tag, def_script) return def_script @@ -161,7 +163,8 @@ feature = self.parse_feature_() self.expect_keyword_("END_FEATURE") features.append(feature) - def_langsys = ast.LangSysDefinition(location, name, tag, features) + def_langsys = ast.LangSysDefinition(name, tag, features, + location=location) return def_langsys def parse_feature_(self): @@ -177,7 +180,8 @@ self.expect_keyword_("LOOKUP") lookup = self.expect_string_() lookups.append(lookup) - feature = ast.FeatureDefinition(location, name, tag, lookups) + feature = ast.FeatureDefinition(name, tag, lookups, + location=location) return feature def parse_def_lookup_(self): @@ -248,8 +252,8 @@ "Got %s" % (as_pos_or_sub), location) def_lookup = ast.LookupDefinition( - location, name, process_base, process_marks, direction, reversal, - comments, context, sub, pos) + name, process_base, process_marks, direction, reversal, + comments, context, sub, pos, location=location) self.lookups_.define(name, def_lookup) return def_lookup @@ -272,8 +276,8 @@ else: right.append(coverage) self.expect_keyword_("END_CONTEXT") - context = ast.ContextDefinition(location, ex_or_in, left, - right) + context = ast.ContextDefinition(ex_or_in, left, + right, location=location) contexts.append(context) else: self.expect_keyword_("END_CONTEXT") @@ -305,13 +309,16 @@ if max_src == 1 and max_dest == 1: if reversal: sub = ast.SubstitutionReverseChainingSingleDefinition( - location, mapping) + mapping, location=location) else: - sub = ast.SubstitutionSingleDefinition(location, mapping) + sub = ast.SubstitutionSingleDefinition(mapping, + location=location) elif max_src == 1 and max_dest > 1: - sub = ast.SubstitutionMultipleDefinition(location, mapping) + sub = ast.SubstitutionMultipleDefinition(mapping, + location=location) elif max_src > 1 and max_dest == 1: - sub = ast.SubstitutionLigatureDefinition(location, mapping) + sub = ast.SubstitutionLigatureDefinition(mapping, + location=location) return sub def parse_position_(self): @@ -348,7 +355,7 @@ coverage_to.append((cov, anchor_name)) self.expect_keyword_("END_ATTACH") position = ast.PositionAttachDefinition( - location, coverage, coverage_to) + coverage, coverage_to, location=location) return position def parse_attach_cursive_(self): @@ -364,7 +371,7 @@ coverages_enter.append(self.parse_coverage_()) self.expect_keyword_("END_ATTACH") position = ast.PositionAttachCursiveDefinition( - location, coverages_exit, coverages_enter) + coverages_exit, coverages_enter, location=location) return position def parse_adjust_pair_(self): @@ -390,7 +397,7 @@ adjust_pair[(id_1, id_2)] = (pos_1, pos_2) self.expect_keyword_("END_ADJUST") position = ast.PositionAdjustPairDefinition( - location, coverages_1, coverages_2, adjust_pair) + coverages_1, coverages_2, adjust_pair, location=location) return position def parse_adjust_single_(self): @@ -404,7 +411,7 @@ adjust_single.append((coverages, pos)) self.expect_keyword_("END_ADJUST") position = ast.PositionAdjustSingleDefinition( - location, adjust_single) + adjust_single, location=location) return position def parse_def_anchor_(self): @@ -433,8 +440,9 @@ self.expect_keyword_("AT") pos = self.parse_pos_() self.expect_keyword_("END_ANCHOR") - anchor = ast.AnchorDefinition(location, name, gid, glyph_name, - component, locked, pos) + anchor = ast.AnchorDefinition(name, gid, glyph_name, + component, locked, pos, + location=location) if glyph_name not in self.anchors_: self.anchors_[glyph_name] = SymbolTable() self.anchors_[glyph_name].define(name, anchor) @@ -493,7 +501,6 @@ def parse_enum_(self): assert self.is_cur_keyword_("ENUM") - location = self.cur_token_location_ enum = self.parse_coverage_() self.expect_keyword_("END_ENUM") return enum @@ -544,14 +551,14 @@ location = self.cur_token_location_ ppem_name = self.cur_token_ value = self.expect_number_() - setting = ast.SettingDefinition(location, ppem_name, value) + setting = ast.SettingDefinition(ppem_name, value, location=location) return setting def parse_compiler_flag_(self): location = self.cur_token_location_ flag_name = self.cur_token_ value = True - setting = ast.SettingDefinition(location, flag_name, value) + setting = ast.SettingDefinition(flag_name, value, location=location) return setting def parse_cmap_format(self): @@ -559,7 +566,7 @@ name = self.cur_token_ value = (self.expect_number_(), self.expect_number_(), self.expect_number_()) - setting = ast.SettingDefinition(location, name, value) + setting = ast.SettingDefinition(name, value, location=location) return setting def is_cur_keyword_(self, k): diff -Nru fonttools-3.21.2/Snippets/layout-features.py fonttools-3.29.0/Snippets/layout-features.py --- fonttools-3.21.2/Snippets/layout-features.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Snippets/layout-features.py 2018-07-26 14:12:55.000000000 +0000 @@ -10,7 +10,11 @@ print("usage: layout-features.py fontfile.ttf") sys.exit(1) fontfile = sys.argv[1] -font = TTFont(fontfile) +if fontfile.rsplit(".", 1)[-1] == "ttx": + font = TTFont() + font.importXML(fontfile) +else: + font = TTFont(fontfile) for tag in ('GSUB', 'GPOS'): if not tag in font: continue diff -Nru fonttools-3.21.2/Snippets/rename-fonts.py fonttools-3.29.0/Snippets/rename-fonts.py --- fonttools-3.21.2/Snippets/rename-fonts.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Snippets/rename-fonts.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,170 @@ +#!/usr/bin/env python +"""Script to add a suffix to all family names in the input font's `name` table, +and to optionally rename the output files with the given suffix. + +The current family name substring is searched in the nameIDs 1, 3, 4, 6, 16, +and 21, and if found the suffix is inserted after it; or else the suffix is +appended at the end. +""" +from __future__ import print_function, absolute_import, unicode_literals +import os +import argparse +import logging +from fontTools.ttLib import TTFont +from fontTools.misc.cliTools import makeOutputFileName + + +logger = logging.getLogger() + +WINDOWS_ENGLISH_IDS = 3, 1, 0x409 +MAC_ROMAN_IDS = 1, 0, 0 + +FAMILY_RELATED_IDS = dict( + LEGACY_FAMILY=1, + TRUETYPE_UNIQUE_ID=3, + FULL_NAME=4, + POSTSCRIPT_NAME=6, + PREFERRED_FAMILY=16, + WWS_FAMILY=21, +) + + +def get_current_family_name(table): + family_name_rec = None + for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS): + for name_id in ( + FAMILY_RELATED_IDS["PREFERRED_FAMILY"], + FAMILY_RELATED_IDS["LEGACY_FAMILY"], + ): + family_name_rec = table.getName( + nameID=name_id, + platformID=plat_id, + platEncID=enc_id, + langID=lang_id, + ) + if family_name_rec is not None: + break + if family_name_rec is not None: + break + if not family_name_rec: + raise ValueError("family name not found; can't add suffix") + return family_name_rec.toUnicode() + + +def insert_suffix(string, family_name, suffix): + # check whether family_name is a substring + start = string.find(family_name) + if start != -1: + # insert suffix after the family_name substring + end = start + len(family_name) + new_string = string[:end] + suffix + string[end:] + else: + # it's not, we just append the suffix at the end + new_string = string + suffix + return new_string + + +def rename_record(name_record, family_name, suffix): + string = name_record.toUnicode() + new_string = insert_suffix(string, family_name, suffix) + name_record.string = new_string + return string, new_string + + +def rename_file(filename, family_name, suffix): + filename, ext = os.path.splitext(filename) + ps_name = family_name.replace(" ", "") + if ps_name in filename: + ps_suffix = suffix.replace(" ", "") + return insert_suffix(filename, ps_name, ps_suffix) + ext + else: + return insert_suffix(filename, family_name, suffix) + ext + + +def add_family_suffix(font, suffix): + table = font["name"] + + family_name = get_current_family_name(table) + logger.info(" Current family name: '%s'", family_name) + + # postcript name can't contain spaces + ps_family_name = family_name.replace(" ", "") + ps_suffix = suffix.replace(" ", "") + for rec in table.names: + name_id = rec.nameID + if name_id not in FAMILY_RELATED_IDS.values(): + continue + if name_id == FAMILY_RELATED_IDS["POSTSCRIPT_NAME"]: + old, new = rename_record(rec, ps_family_name, ps_suffix) + elif name_id == FAMILY_RELATED_IDS["TRUETYPE_UNIQUE_ID"]: + # The Truetype Unique ID rec may contain either the PostScript + # Name or the Full Name string, so we try both + if ps_family_name in rec.toUnicode(): + old, new = rename_record(rec, ps_family_name, ps_suffix) + else: + old, new = rename_record(rec, family_name, suffix) + else: + old, new = rename_record(rec, family_name, suffix) + logger.info(" %r: '%s' -> '%s'", rec, old, new) + + return family_name + + +def main(args=None): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("-s", "--suffix", required=True) + parser.add_argument("input_fonts", metavar="FONTFILE", nargs="+") + output_group = parser.add_mutually_exclusive_group() + output_group.add_argument("-i", "--inplace", action="store_true") + output_group.add_argument("-d", "--output-dir") + output_group.add_argument("-o", "--output-file") + parser.add_argument("-R", "--rename-files", action="store_true") + parser.add_argument("-v", "--verbose", action="count", default=0) + options = parser.parse_args(args) + + if not options.verbose: + level = "WARNING" + elif options.verbose == 1: + level = "INFO" + else: + level = "DEBUG" + logging.basicConfig(level=level, format="%(message)s") + + if options.output_file and len(options.input_fonts) > 1: + parser.error( + "argument -o/--output-file can't be used with multiple inputs" + ) + if options.rename_files and (options.inplace or options.output_file): + parser.error("argument -R not allowed with arguments -i or -o") + + for input_name in options.input_fonts: + logger.info("Renaming font: '%s'", input_name) + + font = TTFont(input_name) + family_name = add_family_suffix(font, options.suffix) + + if options.inplace: + output_name = input_name + elif options.output_file: + output_name = options.output_file + else: + if options.rename_files: + input_name = rename_file( + input_name, family_name, options.suffix + ) + output_name = makeOutputFileName(input_name, options.output_dir) + + font.save(output_name) + logger.info("Saved font: '%s'", output_name) + + font.close() + del font + + logger.info("Done!") + + +if __name__ == "__main__": + main() diff -Nru fonttools-3.21.2/Tests/cffLib/cffLib_test.py fonttools-3.29.0/Tests/cffLib/cffLib_test.py --- fonttools-3.21.2/Tests/cffLib/cffLib_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/cffLib/cffLib_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,12 +1,14 @@ from __future__ import print_function, division, absolute_import from fontTools.cffLib import TopDict, PrivateDict, CharStrings -from fontTools.misc.testTools import parseXML +from fontTools.misc.testTools import parseXML, DataFilesHandler +from fontTools.ttLib import TTFont +import sys import unittest -class TopDictTest(unittest.TestCase): +class CffLibTest(DataFilesHandler): - def test_recalcFontBBox(self): + def test_topDict_recalcFontBBox(self): topDict = TopDict() topDict.CharStrings = CharStrings(None, None, None, PrivateDict(), None, None) topDict.CharStrings.fromXML(None, None, parseXML(""" @@ -27,7 +29,7 @@ topDict.recalcFontBBox() self.assertEqual(topDict.FontBBox, [-56, -100, 300, 200]) - def test_recalcFontBBox_empty(self): + def test_topDict_recalcFontBBox_empty(self): topDict = TopDict() topDict.CharStrings = CharStrings(None, None, None, PrivateDict(), None, None) topDict.CharStrings.fromXML(None, None, parseXML(""" @@ -42,7 +44,21 @@ topDict.recalcFontBBox() self.assertEqual(topDict.FontBBox, [0, 0, 0, 0]) + def test_topDict_set_Encoding(self): + file_name = 'TestOTF.otf' + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + save_path = temp_path[:-4] + '2.otf' + font = TTFont(temp_path) + topDict = font["CFF "].cff.topDictIndex[0] + encoding = [".notdef"] * 256 + encoding[0x20] = "space" + topDict.Encoding = encoding + font.save(save_path) + font2 = TTFont(save_path) + topDict2 = font2["CFF "].cff.topDictIndex[0] + self.assertEqual(topDict2.Encoding[32], "space") + if __name__ == "__main__": - import sys sys.exit(unittest.main()) Binary files /tmp/tmpk2bzJ5/XItIwvLh0S/fonttools-3.21.2/Tests/cffLib/data/TestOTF.otf and /tmp/tmpk2bzJ5/KfFrs7FxYa/fonttools-3.29.0/Tests/cffLib/data/TestOTF.otf differ diff -Nru fonttools-3.21.2/Tests/cffLib/specializer_test.py fonttools-3.29.0/Tests/cffLib/specializer_test.py --- fonttools-3.21.2/Tests/cffLib/specializer_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/cffLib/specializer_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -904,12 +904,12 @@ xpct_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 rlinecurve' self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) -# maxstack CFF=48 +# maxstack CFF=48, specializer uses up to 47 def test_maxstack(self): operands = '1 2 3 4 5 6 ' operator = 'rrcurveto ' test_charstr = (operands + operator)*9 - xpct_charstr = (operands + operator + operands*8 + operator).rstrip() + xpct_charstr = (operands*2 + operator + operands*7 + operator).rstrip() self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) diff -Nru fonttools-3.21.2/Tests/designspaceLib/data/test.designspace fonttools-3.29.0/Tests/designspaceLib/data/test.designspace --- fonttools-3.21.2/Tests/designspaceLib/data/test.designspace 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/designspaceLib/data/test.designspace 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,107 @@ + + + + + Wéíght + قطر + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.coolDesignspaceApp.specimenText + Hamburgerwhatever + + + + + + + + + + + + + + + A note about this glyph + + + + + + + + + + + + + + + + + + + + + + + + com.coolDesignspaceApp.previewSize + 30 + + + diff -Nru fonttools-3.21.2/Tests/designspaceLib/designspace_test.py fonttools-3.29.0/Tests/designspaceLib/designspace_test.py --- fonttools-3.21.2/Tests/designspaceLib/designspace_test.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/designspaceLib/designspace_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,791 @@ +# coding=utf-8 + +from __future__ import (print_function, division, absolute_import, + unicode_literals) + +import os +import pytest +import warnings + +from fontTools.misc.py23 import open +from fontTools.designspaceLib import ( + DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor, + InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError) + +def _axesAsDict(axes): + """ + Make the axis data we have available in + """ + axesDict = {} + for axisDescriptor in axes: + d = { + 'name': axisDescriptor.name, + 'tag': axisDescriptor.tag, + 'minimum': axisDescriptor.minimum, + 'maximum': axisDescriptor.maximum, + 'default': axisDescriptor.default, + 'map': axisDescriptor.map, + } + axesDict[axisDescriptor.name] = d + return axesDict + + +def assert_equals_test_file(path, test_filename): + with open(path) as fp: + actual = fp.read() + + test_path = os.path.join(os.path.dirname(__file__), test_filename) + with open(test_path) as fp: + expected = fp.read() + + assert actual == expected + + +def test_fill_document(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "test.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + # note: just to test the element language, not an actual label name recommendations. + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + doc.addAxis(a1) + a2 = AxisDescriptor() + a2.minimum = 0 + a2.maximum = 1000 + a2.default = 20 + a2.name = "width" + a2.tag = "wdth" + a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + a2.hidden = True + a2.labelNames[u'fr'] = u"Chasse" + doc.addAxis(a2) + + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + assert s1.font is None + s1.name = "master.ufo1" + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(weight=0) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + s1.mutedGlyphNames.append("A") + s1.mutedGlyphNames.append("Z") + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.copyLib = False + s2.copyInfo = False + s2.copyFeatures = False + s2.muteKerning = True + s2.location = dict(weight=1000) + s2.familyName = "MasterFamilyName" + s2.styleName = "MasterStyleNameTwo" + doc.addSource(s2) + # add master 3 from a different layer + s3 = SourceDescriptor() + s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s3.name = "master.ufo2" + s3.copyLib = False + s3.copyInfo = False + s3.copyFeatures = False + s3.muteKerning = False + s3.layerName = "supports" + s3.location = dict(weight=1000) + s3.familyName = "MasterFamilyName" + s3.styleName = "Supports" + doc.addSource(s3) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "InstanceFamilyName" + i1.styleName = "InstanceStyleName" + i1.name = "instance.ufo1" + i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. + i1.postScriptFontName = "InstancePostscriptName" + i1.styleMapFamilyName = "InstanceStyleMapFamilyName" + i1.styleMapStyleName = "InstanceStyleMapStyleName" + glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125]) + i1.glyphs['arrow'] = glyphData + i1.lib['com.coolDesignspaceApp.specimenText'] = "Hamburgerwhatever" + doc.addInstance(i1) + # add instance 2 + i2 = InstanceDescriptor() + i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath)) + i2.familyName = "InstanceFamilyName" + i2.styleName = "InstanceStyleName" + i2.name = "instance.ufo2" + # anisotropic location + i2.location = dict(weight=500, width=(400,300)) + i2.postScriptFontName = "InstancePostscriptName" + i2.styleMapFamilyName = "InstanceStyleMapFamilyName" + i2.styleMapStyleName = "InstanceStyleMapStyleName" + glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] + glyphData = dict(name="arrow", unicodes=[101, 201, 301]) + glyphData['masters'] = glyphMasters + glyphData['note'] = "A note about this glyph" + glyphData['instanceLocation'] = dict(width=100, weight=120) + i2.glyphs['arrow'] = glyphData + i2.glyphs['arrow2'] = dict(mute=False) + doc.addInstance(i2) + + doc.filename = "suggestedFileName.designspace" + doc.lib['com.coolDesignspaceApp.previewSize'] = 30 + + # write some rules + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1), + dict(name='axisName_b', minimum=2, maximum=3) + ]) + r1.subs.append(("a", "a.alt")) + doc.addRule(r1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + assert_equals_test_file(testDocPath, 'data/test.designspace') + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + + assert new.default.location == {'width': 20.0, 'weight': 0.0} + assert new.filename == 'test.designspace' + assert new.lib == doc.lib + assert new.instances[0].lib == doc.instances[0].lib + + # test roundtrip for the axis attributes and data + axes = {} + for axis in doc.axes: + if axis.tag not in axes: + axes[axis.tag] = [] + axes[axis.tag].append(axis.serialize()) + for axis in new.axes: + if axis.tag[0] == "_": + continue + if axis.tag not in axes: + axes[axis.tag] = [] + axes[axis.tag].append(axis.serialize()) + for v in axes.values(): + a, b = v + assert a == b + + +def test_unicodes(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testUnicodes.designspace") + testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyInfo = True + s1.location = dict(weight=0) + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.location = dict(weight=1000) + doc.addSource(s2) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.name = "instance.ufo1" + i1.location = dict(weight=500) + glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300]) + i1.glyphs['arrow'] = glyphData + doc.addInstance(i1) + # now we have sources and instances, but no axes yet. + doc.axes = [] # clear the axes + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + doc.addAxis(a1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + new.write(testDocPath2) + # compare the file contents + f1 = open(testDocPath, 'r', encoding='utf-8') + t1 = f1.read() + f1.close() + f2 = open(testDocPath2, 'r', encoding='utf-8') + t2 = f2.read() + f2.close() + assert t1 == t2 + # check the unicode values read from the document + assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300] + + +def test_localisedNames(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace") + testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyInfo = True + s1.location = dict(weight=0) + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.location = dict(weight=1000) + doc.addSource(s2) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "Montserrat" + i1.styleName = "SemiBold" + i1.styleMapFamilyName = "Montserrat SemiBold" + i1.styleMapStyleName = "Regular" + i1.setFamilyName("Montserrat", "fr") + i1.setFamilyName(u"モンセラート", "ja") + i1.setStyleName("Demigras", "fr") + i1.setStyleName(u"半ば", "ja") + i1.setStyleMapStyleName(u"Standard", "de") + i1.setStyleMapFamilyName("Montserrat Halbfett", "de") + i1.setStyleMapFamilyName(u"モンセラート SemiBold", "ja") + i1.name = "instance.ufo1" + i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. + i1.postScriptFontName = "InstancePostscriptName" + glyphData = dict(name="arrow", mute=True, unicodes=[0x123]) + i1.glyphs['arrow'] = glyphData + doc.addInstance(i1) + # now we have sources and instances, but no axes yet. + doc.axes = [] # clear the axes + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + # note: just to test the element language, not an actual label name recommendations. + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + doc.addAxis(a1) + a2 = AxisDescriptor() + a2.minimum = 0 + a2.maximum = 1000 + a2.default = 0 + a2.name = "width" + a2.tag = "wdth" + a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + a2.labelNames[u'fr'] = u"Poids" + doc.addAxis(a2) + # add an axis that is not part of any location to see if that works + a3 = AxisDescriptor() + a3.minimum = 333 + a3.maximum = 666 + a3.default = 444 + a3.name = "spooky" + a3.tag = "spok" + a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + #doc.addAxis(a3) # uncomment this line to test the effects of default axes values + # write some rules + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='weight', minimum=200, maximum=500), + dict(name='width', minimum=0, maximum=150) + ]) + r1.subs.append(("a", "a.alt")) + doc.addRule(r1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + new.write(testDocPath2) + f1 = open(testDocPath, 'r', encoding='utf-8') + t1 = f1.read() + f1.close() + f2 = open(testDocPath2, 'r', encoding='utf-8') + t2 = f2.read() + f2.close() + assert t1 == t2 + + +def test_handleNoAxes(tmpdir): + tmpdir = str(tmpdir) + # test what happens if the designspacedocument has no axes element. + testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace") + testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + + # Case 1: No axes element in the document, but there are sources and instances + doc = DesignSpaceDocument() + + for name, value in [('One', 1),('Two', 2),('Three', 3)]: + a = AxisDescriptor() + a.minimum = 0 + a.maximum = 1000 + a.default = 0 + a.name = "axisName%s" % (name) + a.tag = "ax_%d" % (value) + doc.addAxis(a) + + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + doc.addSource(s1) + + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo1" + s2.copyLib = False + s2.copyInfo = False + s2.copyFeatures = False + s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0) + s2.familyName = "MasterFamilyName" + s2.styleName = "MasterStyleNameTwo" + doc.addSource(s2) + + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "InstanceFamilyName" + i1.styleName = "InstanceStyleName" + i1.name = "instance.ufo1" + i1.location = dict(axisNameOne=(-1000,500), axisNameTwo=100) + i1.postScriptFontName = "InstancePostscriptName" + i1.styleMapFamilyName = "InstanceStyleMapFamilyName" + i1.styleMapStyleName = "InstanceStyleMapStyleName" + doc.addInstance(i1) + + doc.write(testDocPath) + verify = DesignSpaceDocument() + verify.read(testDocPath) + verify.write(testDocPath2) + +def test_pathNameResolve(tmpdir): + tmpdir = str(tmpdir) + # test how descriptor.path and descriptor.filename are resolved + testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace") + testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace") + testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace") + testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace") + testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace") + testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + + a1 = AxisDescriptor() + a1.tag = "TAGA" + a1.name = "axisName_a" + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + + # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = None + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath1) + verify = DesignSpaceDocument() + verify.read(testDocPath1) + assert verify.sources[0].filename == None + assert verify.sources[0].path == None + + # Case 2: filename is empty, path points somewhere: calculate a new filename. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath2) + verify = DesignSpaceDocument() + verify.read(testDocPath2) + assert verify.sources[0].filename == "masters/masterTest1.ufo" + assert verify.sources[0].path == posix(masterPath1) + + # Case 3: the filename is set, the path is None. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = None + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath3) + verify = DesignSpaceDocument() + verify.read(testDocPath3) + assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo" + # make the absolute path for filename so we can see if it matches the path + p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename)) + assert verify.sources[0].path == posix(p) + + # Case 4: the filename points to one file, the path points to another. The path takes precedence. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath4) + verify = DesignSpaceDocument() + verify.read(testDocPath4) + assert verify.sources[0].filename == "masters/masterTest1.ufo" + + # Case 5: the filename is None, path has a value, update the filename + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath5) # so that the document has a path + doc.updateFilenameFromPath() + assert doc.sources[0].filename == "masters/masterTest1.ufo" + + # Case 6: the filename has a value, path has a value, update the filenames with force + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.write(testDocPath5) # so that the document has a path + doc.addSource(s) + assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo" + doc.updateFilenameFromPath(force=True) + assert doc.sources[0].filename == "masters/masterTest1.ufo" + + +def test_normalise1(): + # normalisation of anisotropic locations, clipping + doc = DesignSpaceDocument() + # write some axes + a1 = AxisDescriptor() + a1.minimum = -1000 + a1.maximum = 1000 + a1.default = 0 + a1.name = "axisName_a" + a1.tag = "TAGA" + doc.addAxis(a1) + assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0} + assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0} + # clipping beyond max values: + assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0} + assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5} + assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0} + assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0} + # anisotropic coordinates normalise to isotropic + assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('axisName_a', -1.0, 0.0, 1.0)] + +def test_normalise2(): + # normalisation with minimum > 0 + doc = DesignSpaceDocument() + # write some axes + a2 = AxisDescriptor() + a2.minimum = 100 + a2.maximum = 1000 + a2.default = 100 + a2.name = "axisName_b" + doc.addAxis(a2) + assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0} + assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0} + # clipping beyond max values: + assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} + assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444} + assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0} + assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0} + # anisotropic coordinates normalise to isotropic + assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0} + assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('axisName_b', 0.0, 0.0, 1.0)] + +def test_normalise3(): + # normalisation of negative values, with default == maximum + doc = DesignSpaceDocument() + # write some axes + a3 = AxisDescriptor() + a3.minimum = -1000 + a3.maximum = 0 + a3.default = 0 + a3.name = "ccc" + doc.addAxis(a3) + assert doc.normalizeLocation(dict(ccc=0)) == {'ccc': 0.0} + assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0} + assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0} + assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('ccc', -1.0, 0.0, 0.0)] + +def test_normalise4(): + # normalisation with a map + doc = DesignSpaceDocument() + # write some axes + a4 = AxisDescriptor() + a4.minimum = 0 + a4.maximum = 1000 + a4.default = 0 + a4.name = "ddd" + a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] + doc.addAxis(a4) + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.map)) + r.sort() + assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])] + +def test_axisMapping(): + # note: because designspance lib does not do any actual + # processing of the mapping data, we can only check if there data is there. + doc = DesignSpaceDocument() + # write some axes + a4 = AxisDescriptor() + a4.minimum = 0 + a4.maximum = 1000 + a4.default = 0 + a4.name = "ddd" + a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] + doc.addAxis(a4) + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.map)) + r.sort() + assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])] + +def test_rulesConditions(tmpdir): + # tests of rules, conditionsets and conditions + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1000), + dict(name='axisName_b', minimum=0, maximum=3000) + ]) + r1.subs.append(("a", "a.alt")) + + assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = -100)) == False + assert evaluateRule(r1, dict(axisName_a = 1000.0001, axisName_b = 0)) == False + assert evaluateRule(r1, dict(axisName_a = -0.0001, axisName_b = 0)) == False + assert evaluateRule(r1, dict(axisName_a = -100, axisName_b = 0)) == False + assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a", "b", "c"]) == ['a.alt', 'b', 'c'] + assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c'] + assert processRules([r1], dict(axisName_a = 2000, axisName_b = 0), ["a", "b", "c"]) == ['a', 'b', 'c'] + + # rule with only a maximum + r2 = RuleDescriptor() + r2.name = "named.rule.2" + r2.conditionSets.append([dict(name='axisName_a', maximum=500)]) + r2.subs.append(("b", "b.alt")) + + assert evaluateRule(r2, dict(axisName_a = 0)) == True + assert evaluateRule(r2, dict(axisName_a = -500)) == True + assert evaluateRule(r2, dict(axisName_a = 1000)) == False + + # rule with only a minimum + r3 = RuleDescriptor() + r3.name = "named.rule.3" + r3.conditionSets.append([dict(name='axisName_a', minimum=500)]) + r3.subs.append(("c", "c.alt")) + + assert evaluateRule(r3, dict(axisName_a = 0)) == False + assert evaluateRule(r3, dict(axisName_a = 1000)) == True + assert evaluateRule(r3, dict(axisName_a = 1000)) == True + + # rule with only a minimum, maximum in separate conditions + r4 = RuleDescriptor() + r4.name = "named.rule.4" + r4.conditionSets.append([ + dict(name='axisName_a', minimum=500), + dict(name='axisName_b', maximum=500) + ]) + r4.subs.append(("c", "c.alt")) + + assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 0)) == True + assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False + assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False + +def test_rulesDocument(tmpdir): + # tests of rules in a document, roundtripping. + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testRules.designspace") + testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace") + doc = DesignSpaceDocument() + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "axisName_a" + a1.tag = "TAGA" + b1 = AxisDescriptor() + b1.minimum = 2000 + b1.maximum = 3000 + b1.default = 2000 + b1.name = "axisName_b" + b1.tag = "TAGB" + doc.addAxis(a1) + doc.addAxis(b1) + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1000), + dict(name='axisName_b', minimum=0, maximum=3000) + ]) + r1.subs.append(("a", "a.alt")) + # rule with minium and maximum + doc.addRule(r1) + assert len(doc.rules) == 1 + assert len(doc.rules[0].conditionSets) == 1 + assert len(doc.rules[0].conditionSets[0]) == 2 + assert _axesAsDict(doc.axes) == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}} + assert doc.rules[0].conditionSets == [[ + {'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'}, + {'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]] + assert doc.rules[0].subs == [('a', 'a.alt')] + doc.normalize() + assert doc.rules[0].name == 'named.rule.1' + assert doc.rules[0].conditionSets == [[ + {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'}, + {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]] + # still one conditionset + assert len(doc.rules[0].conditionSets) == 1 + doc.write(testDocPath) + # add a stray conditionset + _addUnwrappedCondition(testDocPath) + doc2 = DesignSpaceDocument() + doc2.read(testDocPath) + assert len(doc2.axes) == 2 + assert len(doc2.rules) == 1 + assert len(doc2.rules[0].conditionSets) == 2 + doc2.write(testDocPath2) + # verify these results + # make sure the stray condition is now neatly wrapped in a conditionset. + doc3 = DesignSpaceDocument() + doc3.read(testDocPath2) + assert len(doc3.rules) == 1 + assert len(doc3.rules[0].conditionSets) == 2 + +def _addUnwrappedCondition(path): + # only for testing, so we can make an invalid designspace file + # older designspace files may have conditions that are not wrapped in a conditionset + # These can be read into a new conditionset. + f = open(path, 'r', encoding='utf-8') + d = f.read() + print(d) + f.close() + d = d.replace('', '\n\t') + f = open(path, 'w', encoding='utf-8') + f.write(d) + f.close() + +def test_documentLib(tmpdir): + # roundtrip test of the document lib with some nested data + tmpdir = str(tmpdir) + testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace") + doc = DesignSpaceDocument() + a1 = AxisDescriptor() + a1.tag = "TAGA" + a1.name = "axisName_a" + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + doc.addAxis(a1) + dummyData = dict(a=123, b=u"äbc", c=[1,2,3], d={'a':123}) + dummyKey = "org.fontTools.designspaceLib" + doc.lib = {dummyKey: dummyData} + doc.write(testDocPath1) + new = DesignSpaceDocument() + new.read(testDocPath1) + assert dummyKey in new.lib + assert new.lib[dummyKey] == dummyData + diff -Nru fonttools-3.21.2/Tests/feaLib/builder_test.py fonttools-3.29.0/Tests/feaLib/builder_test.py --- fonttools-3.21.2/Tests/feaLib/builder_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/builder_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \ addOpenTypeFeaturesFromString from fontTools.feaLib.error import FeatureLibError @@ -13,6 +14,7 @@ import shutil import sys import tempfile +import logging import unittest @@ -59,7 +61,7 @@ spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 spec5h1 spec6b_ii spec6d2 spec6e spec6f - spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c + spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g spec10 bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 @@ -68,6 +70,7 @@ ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical + PairPosSubtable """.split() def __init__(self, methodName): @@ -121,9 +124,9 @@ sys.stderr.write(line) self.fail("TTX output is different from expected") - def build(self, featureFile): + def build(self, featureFile, tables=None): font = makeTTFont() - addOpenTypeFeaturesFromString(font, featureFile) + addOpenTypeFeaturesFromString(font, featureFile, tables=tables) return font def check_feature_file(self, name): @@ -196,16 +199,29 @@ " sub f_f_i by f f i;" "} test;") - def test_pairPos_redefinition(self): - self.assertRaisesRegex( - FeatureLibError, - r"Already defined position for pair A B " - "at .*:2:[0-9]+", # :2: = line 2 - self.build, - "feature test {\n" - " pos A B 123;\n" # line 2 - " pos A B 456;\n" - "} test;\n") + def test_pairPos_redefinition_warning(self): + # https://github.com/fonttools/fonttools/issues/1147 + logger = logging.getLogger("fontTools.feaLib.builder") + with CapturingLogHandler(logger, "WARNING") as captor: + # the pair "yacute semicolon" is redefined in the enum pos + font = self.build( + "@Y_LC = [y yacute ydieresis];" + "@SMALL_PUNC = [comma semicolon period];" + "feature kern {" + " pos yacute semicolon -70;" + " enum pos @Y_LC semicolon -80;" + " pos @Y_LC @SMALL_PUNC -100;" + "} kern;") + + captor.assertRegex("Already defined position for pair yacute semicolon") + + # the first definition prevails: yacute semicolon -70 + st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.Coverage.glyphs[2], "yacute") + self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, + "semicolon") + self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1), + {"XAdvance": -70}) def test_singleSubst_multipleSubstitutionsForSameGlyph(self): self.assertRaisesRegex( @@ -269,6 +285,17 @@ "it must be the first of the languagesystem statements", self.build, "languagesystem latn TRK; languagesystem DFLT dflt;") + def test_languagesystem_DFLT_not_preceding(self): + self.assertRaisesRegex( + FeatureLibError, + "languagesystems using the \"DFLT\" script tag must " + "precede all other languagesystems", + self.build, + "languagesystem DFLT dflt; " + "languagesystem latn dflt; " + "languagesystem DFLT fooo; " + ) + def test_script(self): builder = Builder(makeTTFont(), (None, None)) builder.start_feature(location=None, name='test') @@ -300,11 +327,7 @@ self.assertEqual(builder.language_systems, {('cyrl', 'BGR ')}) builder.start_feature(location=None, name='test2') - self.assertRaisesRegex( - FeatureLibError, - "Need non-DFLT script when using non-dflt language " - "\(was: \"FRA \"\)", - builder.set_language, None, 'FRA ', True, False) + self.assertEqual(builder.language_systems, {('latn', 'FRA ')}) def test_language_in_aalt_feature(self): self.assertRaisesRegex( @@ -423,7 +446,8 @@ m = self.expect_markClass_reference_() marks.append(m) self.expect_symbol_(";") - return self.ast.MarkBasePosStatement(location, base, marks) + return self.ast.MarkBasePosStatement(base, marks, + location=location) def parseBaseClass(self): if not hasattr(self.doc_, 'baseClasses'): @@ -438,7 +462,8 @@ baseClass = ast_BaseClass(name) self.doc_.baseClasses[name] = baseClass self.glyphclasses_.define(name, baseClass) - bcdef = ast_BaseClassDefinition(location, baseClass, anchor, glyphs) + bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs, + location=location) baseClass.addDefinition(bcdef) return bcdef @@ -469,6 +494,49 @@ " pos base [a] mark @cedilla;" "} mark;") + def test_build_specific_tables(self): + features = "feature liga {sub f i by f_i;} liga;" + font = self.build(features) + assert "GSUB" in font + + font2 = self.build(features, tables=set()) + assert "GSUB" not in font2 + + def test_build_unsupported_tables(self): + self.assertRaises(AssertionError, self.build, "", tables={"FOO"}) + + def test_build_pre_parsed_ast_featurefile(self): + f = UnicodeIO("feature liga {sub f i by f_i;} liga;") + tree = Parser(f).parse() + font = makeTTFont() + addOpenTypeFeatures(font, tree) + assert "GSUB" in font + + def test_unsupported_subtable_break(self): + self.assertRaisesRegex( + FeatureLibError, + 'explicit "subtable" statement is intended for .* class kerning', + self.build, + "feature liga {" + " sub f f by f_f;" + " subtable;" + " sub f i by f_i;" + "} liga;" + ) + + def test_skip_featureNames_if_no_name_table(self): + features = ( + "feature ss01 {" + " featureNames {" + ' name "ignored as we request to skip name table";' + " };" + " sub A by A.alt1;" + "} ss01;" + ) + font = self.build(features, tables=["GSUB"]) + self.assertIn("GSUB", font) + self.assertNotIn("name", font) + def generate_feature_file_test(name): return lambda self: self.check_feature_file(name) diff -Nru fonttools-3.21.2/Tests/feaLib/data/PairPosSubtable.fea fonttools-3.29.0/Tests/feaLib/data/PairPosSubtable.fea --- fonttools-3.21.2/Tests/feaLib/data/PairPosSubtable.fea 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/data/PairPosSubtable.fea 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,25 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; + +@group1 = [b o]; +@group2 = [c d]; +@group3 = [v w]; + +lookup kernlookup { + pos A V -34; + subtable; + pos @group1 @group2 -12; + subtable; + pos @group1 @group3 -10; + pos @group3 @group2 -20; +} kernlookup; + +feature kern { + script DFLT; + language dflt; + lookup kernlookup; + + script latn; + language dflt; + lookup kernlookup; +} kern; diff -Nru fonttools-3.21.2/Tests/feaLib/data/PairPosSubtable.ttx fonttools-3.29.0/Tests/feaLib/data/PairPosSubtable.ttx --- fonttools-3.21.2/Tests/feaLib/data/PairPosSubtable.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/data/PairPosSubtable.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-3.21.2/Tests/feaLib/data/spec8d.fea fonttools-3.29.0/Tests/feaLib/data/spec8d.fea --- fonttools-3.21.2/Tests/feaLib/data/spec8d.fea 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/data/spec8d.fea 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,57 @@ +# The cvParameters block must precede any of the rules in the feature. +# The ParamUILabelNameID entry may be omitted or repeated as often as needed. +# The other NameID types may be omitted, or defined only once. +# The NameID entries must be specified in the order listed below. + +# Following the set of NameID entries, a series of 24-bit Unicode values may be specified. +# These provide Unicode values for the base glyphs referenced by the feature. +# The developer may specify none, some, or all of the Unicode values for the base glyphs. +# The Unicode value may be written with either decimal or hexadecimal notation. +# The value must be preceded by '0x' if it is a hexadecimal value. + +# NOTE: The ParamUILabelNameID entries are used when one base glyph is mapped to more than +# one variant; the font designer may then specify one ParamUILabelNameID for each variant, in +# order to uniquely describe that variant. If any ParamUILabelNameID entries are specified, +# the number of ParamUILabelNameID entries must match the number of variants for each base +# glyph. If the Character Variant feature specifies more than one base glyph, then the set +# of NameID entries in the parameter block will be used for each base glyph and its variants. +feature cv01 { + cvParameters { + FeatUILabelNameID { +#test-fea2fea: name "uilabel simple a"; + name 3 1 0x0409 "uilabel simple a"; # English US +#test-fea2fea: name 1 "uilabel simple a"; + name 1 0 0 "uilabel simple a"; # Mac English + }; + FeatUITooltipTextNameID { +#test-fea2fea: name "tool tip simple a"; + name 3 1 0x0409 "tool tip simple a"; # English US +#test-fea2fea: name 1 "tool tip simple a"; + name 1 0 0 "tool tip simple a"; # Mac English + }; + SampleTextNameID { +#test-fea2fea: name "sample text simple a"; + name 3 1 0x0409 "sample text simple a"; # English US +#test-fea2fea: name 1 "sample text simple a"; + name 1 0 0 "sample text simple a"; # Mac English + }; + ParamUILabelNameID { +#test-fea2fea: name "param1 text simple a"; + name 3 1 0x0409 "param1 text simple a"; # English US +#test-fea2fea: name 1 "param1 text simple a"; + name 1 0 0 "param1 text simple a"; # Mac English + }; + ParamUILabelNameID { +#test-fea2fea: name "param2 text simple a"; + name 3 1 0x0409 "param2 text simple a"; # English US +#test-fea2fea: name 1 "param2 text simple a"; + name 1 0 0 "param2 text simple a"; # Mac English + }; +#test-fea2fea: Character 0xa; + Character 10; +#test-fea2fea: Character 0x5dde; + Character 0x5DDE; + }; +# --- rules for this feature --- + sub A by B; +} cv01; diff -Nru fonttools-3.21.2/Tests/feaLib/data/spec8d.ttx fonttools-3.29.0/Tests/feaLib/data/spec8d.ttx --- fonttools-3.21.2/Tests/feaLib/data/spec8d.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/data/spec8d.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,87 @@ + + + + + + uilabel simple a + + + uilabel simple a + + + tool tip simple a + + + tool tip simple a + + + sample text simple a + + + sample text simple a + + + param1 text simple a + + + param1 text simple a + + + param2 text simple a + + + param2 text simple a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-3.21.2/Tests/feaLib/lexer_test.py fonttools-3.29.0/Tests/feaLib/lexer_test.py --- fonttools-3.21.2/Tests/feaLib/lexer_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/lexer_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,14 +1,18 @@ from __future__ import print_function, division, absolute_import from __future__ import unicode_literals -from fontTools.feaLib.error import FeatureLibError +from fontTools.misc.py23 import * +from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound from fontTools.feaLib.lexer import IncludingLexer, Lexer import os +import shutil +import tempfile import unittest def lex(s): return [(typ, tok) for (typ, tok, _) in Lexer(s, "test.fea")] + class LexerTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) @@ -173,7 +177,59 @@ def test_include_missing_file(self): lexer = IncludingLexer(self.getpath("include/includemissingfile.fea")) - self.assertRaises(FeatureLibError, lambda: list(lexer)) + self.assertRaisesRegex(IncludedFeaNotFound, + "includemissingfile.fea:1:8: missingfile.fea", + lambda: list(lexer)) + + def test_featurefilepath_None(self): + lexer = IncludingLexer(UnicodeIO("# foobar")) + self.assertIsNone(lexer.featurefilepath) + files = set(loc[0] for _, _, loc in lexer) + self.assertIn("", files) + + def test_include_absolute_path(self): + with tempfile.NamedTemporaryFile(delete=False) as included: + included.write(tobytes(""" + feature kern { + pos A B -40; + } kern; + """, encoding="utf-8")) + including = UnicodeIO("include(%s);" % included.name) + try: + lexer = IncludingLexer(including) + files = set(loc[0] for _, _, loc in lexer) + self.assertIn(included.name, files) + finally: + os.remove(included.name) + + def test_include_relative_to_cwd(self): + # save current working directory, to be restored later + cwd = os.getcwd() + tmpdir = tempfile.mkdtemp() + try: + # create new feature file in a temporary directory + with open(os.path.join(tmpdir, "included.fea"), "w", + encoding="utf-8") as included: + included.write(""" + feature kern { + pos A B -40; + } kern; + """) + # change current folder to the temporary dir + os.chdir(tmpdir) + # instantiate a new lexer that includes the above file + # using a relative path; the IncludingLexer does not + # itself have a path, because it was initialized from + # an in-memory stream, so it will use the current working + # directory to resolve relative include statements + lexer = IncludingLexer(UnicodeIO("include(included.fea);")) + files = set(loc[0] for _, _, loc in lexer) + expected = os.path.realpath(included.name) + self.assertIn(expected, files) + finally: + # remove temporary folder and restore previous working directory + os.chdir(cwd) + shutil.rmtree(tmpdir) if __name__ == "__main__": diff -Nru fonttools-3.21.2/Tests/feaLib/parser_test.py fonttools-3.29.0/Tests/feaLib/parser_test.py --- fonttools-3.21.2/Tests/feaLib/parser_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/feaLib/parser_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -86,6 +86,15 @@ self.assertEqual(c2.text, "# simple") self.assertEqual(doc.statements[1].name, "test") + def test_only_comments(self): + doc = self.parse("""\ + # Initial + """) + c1 = doc.statements[0] + self.assertEqual(type(c1), ast.Comment) + self.assertEqual(c1.text, "# Initial") + self.assertEqual(str(c1), "# Initial") + def test_anchor_format_a(self): doc = self.parse( "feature test {" @@ -228,7 +237,7 @@ [feature] = self.parse( "feature ss01 { featureNames { # Comment\n }; } ss01;").statements [featureNames] = feature.statements - self.assertIsInstance(featureNames, ast.FeatureNamesBlock) + self.assertIsInstance(featureNames, ast.NestedBlock) [comment] = featureNames.statements self.assertIsInstance(comment, ast.Comment) self.assertEqual(comment.text, "# Comment") @@ -237,7 +246,7 @@ [feature] = self.parse( "feature ss01 { featureNames { ;;; }; } ss01;").statements [featureNames] = feature.statements - self.assertIsInstance(featureNames, ast.FeatureNamesBlock) + self.assertIsInstance(featureNames, ast.NestedBlock) self.assertEqual(featureNames.statements, []) def test_FontRevision(self): @@ -486,6 +495,32 @@ "lookup L { sub [A A.sc] by a; } L;" "feature test { ignore sub f' i', A' lookup L; } test;") + def test_include_statement(self): + doc = self.parse("""\ + include(../family.fea); + include # Comment + (foo) + ; + """, followIncludes=False) + s1, s2, s3 = doc.statements + self.assertEqual(type(s1), ast.IncludeStatement) + self.assertEqual(s1.filename, "../family.fea") + self.assertEqual(s1.asFea(), "include(../family.fea);") + self.assertEqual(type(s2), ast.IncludeStatement) + self.assertEqual(s2.filename, "foo") + self.assertEqual(s2.asFea(), "include(foo);") + self.assertEqual(type(s3), ast.Comment) + self.assertEqual(s3.text, "# Comment") + + def test_include_statement_no_semicolon(self): + doc = self.parse("""\ + include(../family.fea) + """, followIncludes=False) + s1 = doc.statements[0] + self.assertEqual(type(s1), ast.IncludeStatement) + self.assertEqual(s1.filename, "../family.fea") + self.assertEqual(s1.asFea(), "include(../family.fea);") + def test_language(self): doc = self.parse("feature test {language DEU;} test;") s = doc.statements[0].statements[0] @@ -1220,6 +1255,14 @@ self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(sub.replacement, ("f", "f", "i")) + def test_substitute_multiple_by_mutliple(self): + self.assertRaisesRegex( + FeatureLibError, + "Direct substitution of multiple glyphs by multiple glyphs " + "is not supported", + self.parse, + "lookup MxM {sub a b c by d e f;} MxM;") + def test_split_marked_glyphs_runs(self): self.assertRaisesRegex( FeatureLibError, @@ -1517,10 +1560,9 @@ [langsys] = self.parse("languagesystem latn DEU;").statements self.assertEqual(langsys.script, "latn") self.assertEqual(langsys.language, "DEU ") - self.assertRaisesRegex( - FeatureLibError, - 'For script "DFLT", the language must be "dflt"', - self.parse, "languagesystem DFLT DEU;") + [langsys] = self.parse("languagesystem DFLT DEU;").statements + self.assertEqual(langsys.script, "DFLT") + self.assertEqual(langsys.language, "DEU ") self.assertRaisesRegex( FeatureLibError, '"dflt" is not a valid script tag; use "DFLT" instead', @@ -1548,9 +1590,9 @@ doc = self.parse("table %s { ;;; } %s;" % (table, table)) self.assertEqual(doc.statements[0].statements, []) - def parse(self, text, glyphNames=GLYPHNAMES): + def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True): featurefile = UnicodeIO(text) - p = Parser(featurefile, glyphNames) + p = Parser(featurefile, glyphNames, followIncludes=followIncludes) return p.parse() @staticmethod diff -Nru fonttools-3.21.2/Tests/misc/filenames_test.py fonttools-3.29.0/Tests/misc/filenames_test.py --- fonttools-3.21.2/Tests/misc/filenames_test.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-3.29.0/Tests/misc/filenames_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -0,0 +1,137 @@ +from __future__ import unicode_literals +import unittest +from fontTools.misc.filenames import ( + userNameToFileName, handleClash1, handleClash2) + + +class UserNameToFilenameTest(unittest.TestCase): + + def test_names(self): + self.assertEqual(userNameToFileName("a"),"a") + self.assertEqual(userNameToFileName("A"), "A_") + self.assertEqual(userNameToFileName("AE"), "A_E_") + self.assertEqual(userNameToFileName("Ae"), "A_e") + self.assertEqual(userNameToFileName("ae"), "ae") + self.assertEqual(userNameToFileName("aE"), "aE_") + self.assertEqual(userNameToFileName("a.alt"), "a.alt") + self.assertEqual(userNameToFileName("A.alt"), "A_.alt") + self.assertEqual(userNameToFileName("A.Alt"), "A_.A_lt") + self.assertEqual(userNameToFileName("A.aLt"), "A_.aL_t") + self.assertEqual(userNameToFileName(u"A.alT"), "A_.alT_") + self.assertEqual(userNameToFileName("T_H"), "T__H_") + self.assertEqual(userNameToFileName("T_h"), "T__h") + self.assertEqual(userNameToFileName("t_h"), "t_h") + self.assertEqual(userNameToFileName("F_F_I"), "F__F__I_") + self.assertEqual(userNameToFileName("f_f_i"), "f_f_i") + self.assertEqual( + userNameToFileName("Aacute_V.swash"), + "A_acute_V_.swash") + self.assertEqual(userNameToFileName(".notdef"), "_notdef") + self.assertEqual(userNameToFileName("con"), "_con") + self.assertEqual(userNameToFileName("CON"), "C_O_N_") + self.assertEqual(userNameToFileName("con.alt"), "_con.alt") + self.assertEqual(userNameToFileName("alt.con"), "alt._con") + + def test_prefix_suffix(self): + prefix = "TEST_PREFIX" + suffix = "TEST_SUFFIX" + name = "NAME" + name_file = "N_A_M_E_" + self.assertEqual( + userNameToFileName(name, prefix=prefix, suffix=suffix), + prefix + name_file + suffix) + + def test_collide(self): + prefix = "TEST_PREFIX" + suffix = "TEST_SUFFIX" + name = "NAME" + name_file = "N_A_M_E_" + collision_avoidance1 = "000000000000001" + collision_avoidance2 = "000000000000002" + exist = set() + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + exist.add(generated.lower()) + self.assertEqual(generated, prefix + name_file + suffix) + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + exist.add(generated.lower()) + self.assertEqual( + generated, + prefix + name_file + collision_avoidance1 + suffix) + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + self.assertEqual( + generated, + prefix + name_file + collision_avoidance2+ suffix) + + def test_ValueError(self): + with self.assertRaises(ValueError): + userNameToFileName(b"a") + with self.assertRaises(ValueError): + userNameToFileName({"a"}) + with self.assertRaises(ValueError): + userNameToFileName(("a",)) + with self.assertRaises(ValueError): + userNameToFileName(["a"]) + with self.assertRaises(ValueError): + userNameToFileName(["a"]) + with self.assertRaises(ValueError): + userNameToFileName(b"\xd8\x00") + + def test_handleClash1(self): + prefix = ("0" * 5) + "." + suffix = "." + ("0" * 10) + existing = ["a" * 5] + + e = list(existing) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000001.0000000000' + ) + + e = list(existing) + e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000002.0000000000' + ) + + e = list(existing) + e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000001.0000000000' + ) + + def test_handleClash2(self): + prefix = ("0" * 5) + "." + suffix = "." + ("0" * 10) + existing = [prefix + str(i) + suffix for i in range(100)] + + e = list(existing) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.100.0000000000' + ) + + e = list(existing) + e.remove(prefix + "1" + suffix) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.1.0000000000' + ) + + e = list(existing) + e.remove(prefix + "2" + suffix) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.2.0000000000' + ) + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff -Nru fonttools-3.21.2/Tests/misc/loggingTools_test.py fonttools-3.29.0/Tests/misc/loggingTools_test.py --- fonttools-3.21.2/Tests/misc/loggingTools_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/misc/loggingTools_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,11 +1,20 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc.loggingTools import ( - LevelFormatter, Timer, configLogger, ChannelsFilter, LogMixin) + LevelFormatter, + Timer, + configLogger, + ChannelsFilter, + LogMixin, + StderrHandler, + LastResortLogger, + _resetExistingLoggers, +) import logging import textwrap import time import re +import sys import pytest @@ -170,3 +179,32 @@ assert isinstance(b.log, logging.Logger) assert a.log.name == "loggingTools_test.A" assert b.log.name == "loggingTools_test.B" + + +@pytest.mark.skipif(sys.version_info[:2] > (2, 7), reason="only for python2.7") +@pytest.mark.parametrize( + "reset", [True, False], ids=["reset", "no-reset"] +) +def test_LastResortLogger(reset, capsys, caplog): + current = logging.getLoggerClass() + msg = "The quick brown fox jumps over the lazy dog" + try: + if reset: + _resetExistingLoggers() + else: + caplog.set_level(logging.ERROR, logger="myCustomLogger") + logging.lastResort = StderrHandler(logging.WARNING) + logging.setLoggerClass(LastResortLogger) + logger = logging.getLogger("myCustomLogger") + logger.error(msg) + finally: + del logging.lastResort + logging.setLoggerClass(current) + + captured = capsys.readouterr() + if reset: + assert msg in captured.err + msg not in caplog.text + else: + msg in caplog.text + msg not in captured.err diff -Nru fonttools-3.21.2/Tests/misc/psCharStrings_test.py fonttools-3.29.0/Tests/misc/psCharStrings_test.py --- fonttools-3.21.2/Tests/misc/psCharStrings_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/misc/psCharStrings_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -13,19 +13,40 @@ def test_calcBounds_empty(self): cs = self.stringToT2CharString("endchar") - bounds = cs.calcBounds() + bounds = cs.calcBounds(None) self.assertEqual(bounds, None) def test_calcBounds_line(self): cs = self.stringToT2CharString("100 100 rmoveto 40 10 rlineto -20 50 rlineto endchar") - bounds = cs.calcBounds() + bounds = cs.calcBounds(None) self.assertEqual(bounds, (100, 100, 140, 160)) def test_calcBounds_curve(self): cs = self.stringToT2CharString("100 100 rmoveto -50 -150 200 0 -50 150 rrcurveto endchar") - bounds = cs.calcBounds() + bounds = cs.calcBounds(None) self.assertEqual(bounds, (91.90524980688875, -12.5, 208.09475019311125, 100)) + def test_charstring_bytecode_optimization(self): + cs = self.stringToT2CharString( + "100.0 100 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto endchar") + cs.isCFF2 = False + cs.private._isCFF2 = False + cs.compile() + cs.decompile() + self.assertEqual( + cs.program, [100, 100, 'rmoveto', -50, -150, 200.5, 0, -50, 150, + 'rrcurveto', 'endchar']) + + cs2 = self.stringToT2CharString( + "100.0 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto") + cs2.isCFF2 = True + cs2.private._isCFF2 = True + cs2.compile(isCFF2=True) + cs2.decompile() + self.assertEqual( + cs2.program, [100, 'rmoveto', -50, -150, 200.5, 0, -50, 150, + 'rrcurveto']) + if __name__ == "__main__": import sys diff -Nru fonttools-3.21.2/Tests/misc/xmlReader_test.py fonttools-3.29.0/Tests/misc/xmlReader_test.py --- fonttools-3.21.2/Tests/misc/xmlReader_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/misc/xmlReader_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -142,7 +142,48 @@ self.assertTrue(reader.file.closed) os.remove(tmp.name) + def test_read_sub_file(self): + # Verifies that sub-file content is able to be read to a table. + expectedContent = 'testContent' + expectedNameID = '1' + expectedPlatform = '3' + expectedLangId = '0x409' + with tempfile.NamedTemporaryFile(delete=False) as tmp: + subFileData = ( + '' + '' + '' + '%s' + '' + '' + '' + ) % (expectedNameID, expectedPlatform, expectedLangId, expectedContent) + tmp.write(subFileData.encode("utf-8")) + + with tempfile.NamedTemporaryFile(delete=False) as tmp2: + fileData = ( + '' + '' + '' + '' + '' + ) % tmp.name + tmp2.write(fileData.encode('utf-8')) + + ttf = TTFont() + with open(tmp2.name, "rb") as f: + reader = XMLReader(f, ttf) + reader.read() + reader.close() + nameTable = ttf['name'] + self.assertTrue(int(expectedNameID) == nameTable.names[0].nameID) + self.assertTrue(int(expectedLangId, 16) == nameTable.names[0].langID) + self.assertTrue(int(expectedPlatform) == nameTable.names[0].platformID) + self.assertEqual(expectedContent, nameTable.names[0].string.decode(nameTable.names[0].getEncoding())) + + os.remove(tmp.name) + os.remove(tmp2.name) if __name__ == '__main__': import sys diff -Nru fonttools-3.21.2/Tests/pens/t2CharStringPen_test.py fonttools-3.29.0/Tests/pens/t2CharStringPen_test.py --- fonttools-3.21.2/Tests/pens/t2CharStringPen_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/pens/t2CharStringPen_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -142,14 +142,14 @@ pen = T2CharStringPen(100.1, {}, roundTolerance=0.5) pen.moveTo((0, 0)) pen.curveTo((10.1, 0.1), (19.9, 9.9), (20.49, 20.49)) - pen.curveTo((20.49, 30.49), (9.9, 39.9), (0.1, 40.1)) + pen.curveTo((20.49, 30.5), (9.9, 39.9), (0.1, 40.1)) pen.closePath() charstring = pen.getCharString(None, None) self.assertEqual( [100, 0, 'hmoveto', - 10, 10, 10, 10, 10, -10, 10, -10, 'hvcurveto', + 10, 10, 10, 10, 11, -10, 9, -10, 'hvcurveto', 'endchar'], charstring.program) diff -Nru fonttools-3.21.2/Tests/pens/ttGlyphPen_test.py fonttools-3.29.0/Tests/pens/ttGlyphPen_test.py --- fonttools-3.21.2/Tests/pens/ttGlyphPen_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/pens/ttGlyphPen_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -3,12 +3,14 @@ import os import unittest +import struct from fontTools import ttLib -from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.misc.testTools import TestCase +from fontTools.pens.ttGlyphPen import TTGlyphPen, MAX_F2DOT14 -class TTGlyphPenTest(unittest.TestCase): +class TTGlyphPenTest(TestCase): def runEndToEnd(self, filename): font = ttLib.TTFont() @@ -92,6 +94,7 @@ pen.lineTo((1, 0)) pen.closePath() pen.addComponent(componentName, (1, 0, 0, 1, 2, 0)) + pen.addComponent("missing", (1, 0, 0, 1, 0, 0)) # skipped compositeGlyph = pen.glyph() pen.moveTo((0, 0)) @@ -135,6 +138,108 @@ self.assertEqual(len(pen.points), 5) self.assertEqual(pen.points[0], (0, 0)) + def test_within_range_component_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0)) + compositeGlyph = pen.glyph() + + pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0)) + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_clamp_to_almost_2_component_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (1.99999, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 2, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 2, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, 2, 0, 0)) + pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0)) + compositeGlyph = pen.glyph() + + almost2 = MAX_F2DOT14 # 0b1.11111111111111 + pen.addComponent(componentName, (almost2, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, almost2, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, almost2, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, almost2, 0, 0)) + pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0)) + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_out_of_range_transform_decomposed(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (3, 0, 0, 2, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, 1, -1, 2)) + pen.addComponent(componentName, (2, 0, 0, -3, 0, 0)) + compositeGlyph = pen.glyph() + + pen.moveTo((0, 0)) + pen.lineTo((0, 2)) + pen.lineTo((3, 0)) + pen.closePath() + pen.moveTo((-1, 2)) + pen.lineTo((-1, 3)) + pen.lineTo((0, 2)) + pen.closePath() + pen.moveTo((0, 0)) + pen.lineTo((0, -3)) + pen.lineTo((2, 0)) + pen.closePath() + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_no_handle_overflowing_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet, handleOverflowingTransforms=False) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + baseGlyph = pen.glyph() + glyphSet[componentName] = _TestGlyph(baseGlyph) + + pen.addComponent(componentName, (3, 0, 0, 1, 0, 0)) + compositeGlyph = pen.glyph() + + self.assertEqual(compositeGlyph.components[0].transform, + ((3, 0), (0, 1))) + + with self.assertRaises(struct.error): + compositeGlyph.compile({'a': baseGlyph}) + class _TestGlyph(object): def __init__(self, glyph): diff -Nru fonttools-3.21.2/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx fonttools-3.29.0/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx --- fonttools-3.21.2/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -178,6 +178,18 @@ Regular + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR-Regular + + + Version 1.000 + + + TestGVAR-Regular + Weight diff -Nru fonttools-3.21.2/Tests/subset/data/expect_keep_gvar.ttx fonttools-3.29.0/Tests/subset/data/expect_keep_gvar.ttx --- fonttools-3.21.2/Tests/subset/data/expect_keep_gvar.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/subset/data/expect_keep_gvar.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -131,6 +131,18 @@ Regular + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR-Regular + + + Version 1.000 + + + TestGVAR-Regular + Weight diff -Nru fonttools-3.21.2/Tests/subset/subset_test.py fonttools-3.29.0/Tests/subset/subset_test.py --- fonttools-3.21.2/Tests/subset/subset_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/subset/subset_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -333,10 +333,8 @@ subsetter.populate(text='ABC') font = TTFont(fontpath) with CapturingLogHandler('fontTools.subset.timer', logging.DEBUG) as captor: - captor.logger.propagate = False subsetter.subset(font) - logs = captor.records - captor.logger.propagate = True + logs = captor.records self.assertTrue(len(logs) > 5) self.assertEqual(len(logs), len([l for l in logs if 'msg' in l.args and 'time' in l.args])) diff -Nru fonttools-3.21.2/Tests/t1Lib/t1Lib_test.py fonttools-3.29.0/Tests/t1Lib/t1Lib_test.py --- fonttools-3.21.2/Tests/t1Lib/t1Lib_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/t1Lib/t1Lib_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -67,8 +67,7 @@ def test_parse_lwfn(self): # the extended attrs are lost on git so we can't auto-detect 'LWFN' - font = t1Lib.T1Font() - font.data = t1Lib.readLWFN(LWFN) + font = t1Lib.T1Font(LWFN, kind="LWFN") font.parse() self.assertEqual(font['FontName'], 'TestT1-Regular') self.assertTrue('Subrs' in font['Private']) diff -Nru fonttools-3.21.2/Tests/ttLib/tables/_g_l_y_f_test.py fonttools-3.29.0/Tests/ttLib/tables/_g_l_y_f_test.py --- fonttools-3.21.2/Tests/ttLib/tables/_g_l_y_f_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttLib/tables/_g_l_y_f_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,7 +1,9 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates import sys +import array import pytest @@ -55,7 +57,7 @@ def test__round__(self): g = GlyphCoordinates([(-1.5,2)]) g2 = round(g) - assert g2 == GlyphCoordinates([(-2,2)]) + assert g2 == GlyphCoordinates([(-1,2)]) def test__add__(self): g1 = GlyphCoordinates([(1,2)]) @@ -149,4 +151,10 @@ # this would return 242 if the internal array.array typecode is 'f', # since the Python float is truncated to a C float. # when using typecode 'd' it should return the correct value 243 - assert g[0][0] == round(afloat) + assert g[0][0] == otRound(afloat) + + def test__checkFloat_overflow(self): + g = GlyphCoordinates([(1, 1)], typecode="h") + g.append((0x8000, 0)) + assert g.array.typecode == "d" + assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0]) diff -Nru fonttools-3.21.2/Tests/ttLib/tables/_h_m_t_x_test.py fonttools-3.29.0/Tests/ttLib/tables/_h_m_t_x_test.py --- fonttools-3.21.2/Tests/ttLib/tables/_h_m_t_x_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttLib/tables/_h_m_t_x_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -164,14 +164,14 @@ font = self.makeFont(numGlyphs=3, numberOfMetrics=2) mtxTable = font[self.tag] = newTable(self.tag) mtxTable.metrics = { - 'A': (0.5, 0.5), # round -> (0, 0) + 'A': (0.5, 0.5), # round -> (1, 1) 'B': (0.1, 0.9), # round -> (0, 1) 'C': (0.1, 0.1), # round -> (0, 0) } data = mtxTable.compile(font) - self.assertEqual(data, deHexStr("0000 0000 0000 0001 0000")) + self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000")) def test_toXML(self): font = self.makeFont(numGlyphs=2, numberOfMetrics=2) diff -Nru fonttools-3.21.2/Tests/ttLib/tables/_n_a_m_e_test.py fonttools-3.29.0/Tests/ttLib/tables/_n_a_m_e_test.py --- fonttools-3.21.2/Tests/ttLib/tables/_n_a_m_e_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttLib/tables/_n_a_m_e_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -182,7 +182,7 @@ "offset": 8765 # out of range } data = bytesjoin([ - struct.pack(">HHH", 1, 1, 6 + nameRecordSize), + struct.pack(tostr(">HHH"), 1, 1, 6 + nameRecordSize), sstruct.pack(nameRecordFormat, badRecord)]) table.decompile(data, ttFont=None) self.assertEqual(table.names, []) diff -Nru fonttools-3.21.2/Tests/ttLib/tables/otConverters_test.py fonttools-3.29.0/Tests/ttLib/tables/otConverters_test.py --- fonttools-3.21.2/Tests/ttLib/tables/otConverters_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttLib/tables/otConverters_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -114,6 +114,7 @@ def makeFont(self): nameTable = newTable('name') nameTable.setName(u"Demibold Condensed", 0x123, 3, 0, 0x409) + nameTable.setName(u"Copyright 2018", 0, 3, 0, 0x409) return {"name": nameTable} def test_read(self): @@ -148,6 +149,14 @@ ' ') + def test_xmlWrite_NULL(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.makeFont(), 0, + "FooNameID", [("attr", "val")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual( + xml, '') + class UInt8Test(unittest.TestCase): font = FakeFont([]) diff -Nru fonttools-3.21.2/Tests/ttLib/tables/otTables_test.py fonttools-3.29.0/Tests/ttLib/tables/otTables_test.py --- fonttools-3.21.2/Tests/ttLib/tables/otTables_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttLib/tables/otTables_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -485,6 +485,50 @@ self.assertEqual(hexStr(writer.getAllData()), "1234fc4300090007") +def test_splitMarkBasePos(): + from fontTools.otlLib.builder import buildAnchor, buildMarkBasePosSubtable + + marks = { + "acutecomb": (0, buildAnchor(0, 600)), + "gravecomb": (0, buildAnchor(0, 590)), + "cedillacomb": (1, buildAnchor(0, 0)), + } + bases = { + "a": { + 0: buildAnchor(350, 500), + 1: None, + }, + "c": { + 0: buildAnchor(300, 700), + 1: buildAnchor(300, 0), + }, + } + glyphOrder = ["a", "c", "acutecomb", "gravecomb", "cedillacomb"] + glyphMap = {g: i for i, g in enumerate(glyphOrder)} + + oldSubTable = buildMarkBasePosSubtable(marks, bases, glyphMap) + oldSubTable.MarkCoverage.Format = oldSubTable.BaseCoverage.Format = 1 + newSubTable = otTables.MarkBasePos() + + ok = otTables.splitMarkBasePos(oldSubTable, newSubTable, overflowRecord=None) + + assert ok + assert oldSubTable.Format == newSubTable.Format + assert oldSubTable.MarkCoverage.glyphs == [ + "acutecomb", "gravecomb" + ] + assert newSubTable.MarkCoverage.glyphs == ["cedillacomb"] + assert newSubTable.MarkCoverage.Format == 1 + assert oldSubTable.BaseCoverage.glyphs == newSubTable.BaseCoverage.glyphs + assert newSubTable.BaseCoverage.Format == 1 + assert oldSubTable.ClassCount == newSubTable.ClassCount == 1 + assert oldSubTable.MarkArray.MarkCount == 2 + assert newSubTable.MarkArray.MarkCount == 1 + assert oldSubTable.BaseArray.BaseCount == newSubTable.BaseArray.BaseCount + assert newSubTable.BaseArray.BaseRecord[0].BaseAnchor[0] is None + assert newSubTable.BaseArray.BaseRecord[1].BaseAnchor[0] == buildAnchor(300, 0) + + if __name__ == "__main__": import sys sys.exit(unittest.main()) diff -Nru fonttools-3.21.2/Tests/ttx/ttx_test.py fonttools-3.29.0/Tests/ttx/ttx_test.py --- fonttools-3.21.2/Tests/ttx/ttx_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/ttx/ttx_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -1,15 +1,31 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.timeTools import timestampSinceEpoch +from fontTools.ttLib import TTFont, TTLibError from fontTools import ttx import getopt +import logging import os import shutil import sys import tempfile import unittest +import pytest + +try: + import zopfli +except ImportError: + zopfli = None +try: + import brotli +except ImportError: + brotli = None + class TTXTest(unittest.TestCase): + def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, @@ -40,148 +56,959 @@ shutil.copy2(font_path, temppath) return temppath -# ----- -# Tests -# ----- + @staticmethod + def read_file(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return f.readlines() + + # ----- + # Tests + # ----- def test_parseOptions_no_args(self): with self.assertRaises(getopt.GetoptError) as cm: ttx.parseOptions([]) - self.assertTrue('Must specify at least one input file' in str(cm.exception)) + self.assertTrue( + "Must specify at least one input file" in str(cm.exception) + ) def test_parseOptions_invalid_path(self): - file_path = 'invalid_font_path' + file_path = "invalid_font_path" with self.assertRaises(getopt.GetoptError) as cm: ttx.parseOptions([file_path]) self.assertTrue('File not found: "%s"' % file_path in str(cm.exception)) def test_parseOptions_font2ttx_1st_time(self): - file_name = 'TestOTF.otf' + file_name = "TestOTF.otf" font_path = self.getpath(file_name) temp_path = self.temp_font(font_path, file_name) jobs, _ = ttx.parseOptions([temp_path]) - self.assertEqual(jobs[0][0].__name__, 'ttDump') - self.assertEqual(jobs[0][1:], - (os.path.join(self.tempdir, file_name), - os.path.join(self.tempdir, file_name.split('.')[0] + '.ttx'))) + self.assertEqual(jobs[0][0].__name__, "ttDump") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + ".ttx"), + ), + ) def test_parseOptions_font2ttx_2nd_time(self): - file_name = 'TestTTF.ttf' + file_name = "TestTTF.ttf" font_path = self.getpath(file_name) temp_path = self.temp_font(font_path, file_name) - _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake + _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake jobs, _ = ttx.parseOptions([temp_path]) - self.assertEqual(jobs[0][0].__name__, 'ttDump') - self.assertEqual(jobs[0][1:], - (os.path.join(self.tempdir, file_name), - os.path.join(self.tempdir, file_name.split('.')[0] + '#1.ttx'))) + self.assertEqual(jobs[0][0].__name__, "ttDump") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + "#1.ttx"), + ), + ) def test_parseOptions_ttx2font_1st_time(self): - file_name = 'TestTTF.ttx' + file_name = "TestTTF.ttx" font_path = self.getpath(file_name) temp_path = self.temp_font(font_path, file_name) jobs, _ = ttx.parseOptions([temp_path]) - self.assertEqual(jobs[0][0].__name__, 'ttCompile') - self.assertEqual(jobs[0][1:], - (os.path.join(self.tempdir, file_name), - os.path.join(self.tempdir, file_name.split('.')[0] + '.ttf'))) + self.assertEqual(jobs[0][0].__name__, "ttCompile") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + ".ttf"), + ), + ) def test_parseOptions_ttx2font_2nd_time(self): - file_name = 'TestOTF.ttx' + file_name = "TestOTF.ttx" font_path = self.getpath(file_name) temp_path = self.temp_font(font_path, file_name) - _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake + _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake jobs, _ = ttx.parseOptions([temp_path]) - self.assertEqual(jobs[0][0].__name__, 'ttCompile') - self.assertEqual(jobs[0][1:], - (os.path.join(self.tempdir, file_name), - os.path.join(self.tempdir, file_name.split('.')[0] + '#1.otf'))) + self.assertEqual(jobs[0][0].__name__, "ttCompile") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + "#1.otf"), + ), + ) def test_parseOptions_multiple_fonts(self): - file_names = ['TestOTF.otf', 'TestTTF.ttf'] + file_names = ["TestOTF.otf", "TestTTF.ttf"] font_paths = [self.getpath(file_name) for file_name in file_names] - temp_paths = [self.temp_font(font_path, file_name) \ - for font_path, file_name in zip(font_paths, file_names)] + temp_paths = [ + self.temp_font(font_path, file_name) + for font_path, file_name in zip(font_paths, file_names) + ] jobs, _ = ttx.parseOptions(temp_paths) for i in range(len(jobs)): - self.assertEqual(jobs[i][0].__name__, 'ttDump') - self.assertEqual(jobs[i][1:], - (os.path.join(self.tempdir, file_names[i]), - os.path.join(self.tempdir, file_names[i].split('.')[0] + '.ttx'))) + self.assertEqual(jobs[i][0].__name__, "ttDump") + self.assertEqual( + jobs[i][1:], + ( + os.path.join(self.tempdir, file_names[i]), + os.path.join( + self.tempdir, file_names[i].split(".")[0] + ".ttx" + ), + ), + ) def test_parseOptions_mixed_files(self): - operations = ['ttDump', 'ttCompile'] - extensions = ['.ttx', '.ttf'] - file_names = ['TestOTF.otf', 'TestTTF.ttx'] + operations = ["ttDump", "ttCompile"] + extensions = [".ttx", ".ttf"] + file_names = ["TestOTF.otf", "TestTTF.ttx"] font_paths = [self.getpath(file_name) for file_name in file_names] - temp_paths = [self.temp_font(font_path, file_name) \ - for font_path, file_name in zip(font_paths, file_names)] + temp_paths = [ + self.temp_font(font_path, file_name) + for font_path, file_name in zip(font_paths, file_names) + ] jobs, _ = ttx.parseOptions(temp_paths) for i in range(len(jobs)): self.assertEqual(jobs[i][0].__name__, operations[i]) - self.assertEqual(jobs[i][1:], - (os.path.join(self.tempdir, file_names[i]), - os.path.join(self.tempdir, file_names[i].split('.')[0] + extensions[i]))) + self.assertEqual( + jobs[i][1:], + ( + os.path.join(self.tempdir, file_names[i]), + os.path.join( + self.tempdir, + file_names[i].split(".")[0] + extensions[i], + ), + ), + ) + + def test_parseOptions_splitTables(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + args = ["-s", temp_path] + + jobs, options = ttx.parseOptions(args) + + ttx_file_path = jobs[0][2] + temp_folder = os.path.dirname(ttx_file_path) + self.assertTrue(options.splitTables) + self.assertTrue(os.path.exists(ttx_file_path)) + + ttx.process(jobs, options) + + # Read the TTX file but strip the first two and the last lines: + # + # + # ... + # + parsed_xml = parseXML(self.read_file(ttx_file_path)[2:-1]) + for item in parsed_xml: + if not isinstance(item, tuple): + continue + # the tuple looks like this: + # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) + table_file_name = item[1].get("src") + table_file_path = os.path.join(temp_folder, table_file_name) + self.assertTrue(os.path.exists(table_file_path)) + + def test_parseOptions_splitGlyphs(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + args = ["-g", temp_path] + + jobs, options = ttx.parseOptions(args) + + ttx_file_path = jobs[0][2] + temp_folder = os.path.dirname(ttx_file_path) + self.assertTrue(options.splitGlyphs) + # splitGlyphs also forces splitTables + self.assertTrue(options.splitTables) + self.assertTrue(os.path.exists(ttx_file_path)) + + ttx.process(jobs, options) + + # Read the TTX file but strip the first two and the last lines: + # + # + # ... + # + for item in parseXML(self.read_file(ttx_file_path)[2:-1]): + if not isinstance(item, tuple): + continue + # the tuple looks like this: + # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) + table_tag = item[0] + table_file_name = item[1].get("src") + table_file_path = os.path.join(temp_folder, table_file_name) + self.assertTrue(os.path.exists(table_file_path)) + if table_tag != "glyf": + continue + # also strip the enclosing 'glyf' element + for item in parseXML(self.read_file(table_file_path)[4:-3]): + if not isinstance(item, tuple): + continue + # glyphs without outline data only have 'name' attribute + glyph_file_name = item[1].get("src") + if glyph_file_name is not None: + glyph_file_path = os.path.join(temp_folder, glyph_file_name) + self.assertTrue(os.path.exists(glyph_file_path)) def test_guessFileType_ttf(self): - file_name = 'TestTTF.ttf' + file_name = "TestTTF.ttf" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTF') + self.assertEqual(ttx.guessFileType(font_path), "TTF") def test_guessFileType_otf(self): - file_name = 'TestOTF.otf' + file_name = "TestOTF.otf" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'OTF') + self.assertEqual(ttx.guessFileType(font_path), "OTF") def test_guessFileType_woff(self): - file_name = 'TestWOFF.woff' + file_name = "TestWOFF.woff" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'WOFF') + self.assertEqual(ttx.guessFileType(font_path), "WOFF") def test_guessFileType_woff2(self): - file_name = 'TestWOFF2.woff2' + file_name = "TestWOFF2.woff2" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'WOFF2') + self.assertEqual(ttx.guessFileType(font_path), "WOFF2") def test_guessFileType_ttc(self): - file_name = 'TestTTC.ttc' + file_name = "TestTTC.ttc" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTC') + self.assertEqual(ttx.guessFileType(font_path), "TTC") def test_guessFileType_dfont(self): - file_name = 'TestDFONT.dfont' + file_name = "TestDFONT.dfont" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTF') + self.assertEqual(ttx.guessFileType(font_path), "TTF") def test_guessFileType_ttx_ttf(self): - file_name = 'TestTTF.ttx' + file_name = "TestTTF.ttx" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTX') + self.assertEqual(ttx.guessFileType(font_path), "TTX") def test_guessFileType_ttx_otf(self): - file_name = 'TestOTF.ttx' + file_name = "TestOTF.ttx" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'OTX') + self.assertEqual(ttx.guessFileType(font_path), "OTX") def test_guessFileType_ttx_bom(self): - file_name = 'TestBOM.ttx' + file_name = "TestBOM.ttx" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTX') + self.assertEqual(ttx.guessFileType(font_path), "TTX") def test_guessFileType_ttx_no_sfntVersion(self): - file_name = 'TestNoSFNT.ttx' + file_name = "TestNoSFNT.ttx" font_path = self.getpath(file_name) - self.assertEqual(ttx.guessFileType(font_path), 'TTX') + self.assertEqual(ttx.guessFileType(font_path), "TTX") def test_guessFileType_ttx_no_xml(self): - file_name = 'TestNoXML.ttx' + file_name = "TestNoXML.ttx" font_path = self.getpath(file_name) self.assertIsNone(ttx.guessFileType(font_path)) def test_guessFileType_invalid_path(self): - font_path = 'invalid_font_path' + font_path = "invalid_font_path" self.assertIsNone(ttx.guessFileType(font_path)) -if __name__ == "__main__": - sys.exit(unittest.main()) +# ----------------------- +# ttx.Options class tests +# ----------------------- + + +def test_options_flag_h(capsys): + with pytest.raises(SystemExit): + ttx.Options([("-h", None)], 1) + + out, err = capsys.readouterr() + assert "TTX -- From OpenType To XML And Back" in out + + +def test_options_flag_version(capsys): + with pytest.raises(SystemExit): + ttx.Options([("--version", None)], 1) + + out, err = capsys.readouterr() + version_list = out.split(".") + assert len(version_list) >= 3 + assert version_list[0].isdigit() + assert version_list[1].isdigit() + assert version_list[2].strip().isdigit() + + +def test_options_d_goodpath(tmpdir): + temp_dir_path = str(tmpdir) + tto = ttx.Options([("-d", temp_dir_path)], 1) + assert tto.outputDir == temp_dir_path + + +def test_options_d_badpath(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-d", "bogusdir")], 1) + + +def test_options_o(): + tto = ttx.Options([("-o", "testfile.ttx")], 1) + assert tto.outputFile == "testfile.ttx" + + +def test_options_f(): + tto = ttx.Options([("-f", "")], 1) + assert tto.overWrite is True + + +def test_options_v(): + tto = ttx.Options([("-v", "")], 1) + assert tto.verbose is True + assert tto.logLevel == logging.DEBUG + + +def test_options_q(): + tto = ttx.Options([("-q", "")], 1) + assert tto.quiet is True + assert tto.logLevel == logging.WARNING + + +def test_options_l(): + tto = ttx.Options([("-l", "")], 1) + assert tto.listTables is True + + +def test_options_t_nopadding(): + tto = ttx.Options([("-t", "CFF2")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF2" + + +def test_options_t_withpadding(): + tto = ttx.Options([("-t", "CFF")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF " + + +def test_options_s(): + tto = ttx.Options([("-s", "")], 1) + assert tto.splitTables is True + assert tto.splitGlyphs is False + + +def test_options_g(): + tto = ttx.Options([("-g", "")], 1) + assert tto.splitGlyphs is True + assert tto.splitTables is True + + +def test_options_i(): + tto = ttx.Options([("-i", "")], 1) + assert tto.disassembleInstructions is False + + +def test_options_z_validoptions(): + valid_options = ("raw", "row", "bitwise", "extfile") + for option in valid_options: + tto = ttx.Options([("-z", option)], 1) + assert tto.bitmapGlyphDataFormat == option + + +def test_options_z_invalidoption(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-z", "bogus")], 1) + + +def test_options_y_validvalue(): + tto = ttx.Options([("-y", "1")], 1) + assert tto.fontNumber == 1 + + +def test_options_y_invalidvalue(): + with pytest.raises(ValueError): + ttx.Options([("-y", "A")], 1) + + +def test_options_m(): + tto = ttx.Options([("-m", "testfont.ttf")], 1) + assert tto.mergeFile == "testfont.ttf" + + +def test_options_b(): + tto = ttx.Options([("-b", "")], 1) + assert tto.recalcBBoxes is False + + +def test_options_a(): + tto = ttx.Options([("-a", "")], 1) + assert tto.allowVID is True + + +def test_options_e(): + tto = ttx.Options([("-e", "")], 1) + assert tto.ignoreDecompileErrors is False + + +def test_options_unicodedata(): + tto = ttx.Options([("--unicodedata", "UnicodeData.txt")], 1) + assert tto.unicodedata == "UnicodeData.txt" + + +def test_options_newline_lf(): + tto = ttx.Options([("--newline", "LF")], 1) + assert tto.newlinestr == "\n" + + +def test_options_newline_cr(): + tto = ttx.Options([("--newline", "CR")], 1) + assert tto.newlinestr == "\r" + + +def test_options_newline_crlf(): + tto = ttx.Options([("--newline", "CRLF")], 1) + assert tto.newlinestr == "\r\n" + + +def test_options_newline_invalid(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--newline", "BOGUS")], 1) + + +def test_options_recalc_timestamp(): + tto = ttx.Options([("--recalc-timestamp", "")], 1) + assert tto.recalcTimestamp is True + + +def test_options_flavor(): + tto = ttx.Options([("--flavor", "woff")], 1) + assert tto.flavor == "woff" + + +def test_options_with_zopfli(): + tto = ttx.Options([("--with-zopfli", ""), ("--flavor", "woff")], 1) + assert tto.useZopfli is True + + +def test_options_with_zopfli_fails_without_woff_flavor(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", "")], 1) + + +def test_options_quiet_and_verbose_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-q", ""), ("-v", "")], 1) + + +def test_options_mergefile_and_flavor_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf"), ("--flavor", "woff")], 1) + + +def test_options_onlytables_and_skiptables_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-t", "CFF"), ("-x", "CFF2")], 1) + + +def test_options_mergefile_and_multiplefiles_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf")], 2) + + +def test_options_woff2_and_zopfli_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", ""), ("--flavor", "woff2")], 1) + + +# ---------------------------- +# ttx.ttCompile function tests +# ---------------------------- + + +def test_ttcompile_otf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + # outotf = os.path.join(str(tmpdir), "TestOTF.otf") + outotf = tmpdir.join("TestOTF.ttx") + default_options = ttx.Options([], 1) + ttx.ttCompile(inttx, str(outotf), default_options) + # confirm that font was built + assert outotf.check(file=True) + # confirm that it is valid OTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outotf)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_otf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = tmpdir.join("TestOTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") +def test_ttcompile_otf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = tmpdir.join("TestOTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttcompile_otf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff2 = tmpdir.join("TestTTF.woff2") + options = ttx.Options([], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, str(outwoff2), options) + # confirm that font was built + assert outwoff2.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff2)) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_ttf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outttf = tmpdir.join("TestTTF.ttf") + default_options = ttx.Options([], 1) + ttx.ttCompile(inttx, str(outttf), default_options) + # confirm that font was built + assert outttf.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outttf)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_ttf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = tmpdir.join("TestTTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") +def test_ttcompile_ttf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = tmpdir.join("TestTTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttcompile_ttf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff2 = tmpdir.join("TestTTF.woff2") + options = ttx.Options([], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, str(outwoff2), options) + # confirm that font was built + assert outwoff2.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff2)) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.parametrize( + "inpath, outpath1, outpath2", + [ + ("TestTTF.ttx", "TestTTF1.ttf", "TestTTF2.ttf"), + ("TestOTF.ttx", "TestOTF1.otf", "TestOTF2.otf"), + ], +) +def test_ttcompile_timestamp_calcs(inpath, outpath1, outpath2, tmpdir): + inttx = os.path.join("Tests", "ttx", "data", inpath) + outttf1 = tmpdir.join(outpath1) + outttf2 = tmpdir.join(outpath2) + options = ttx.Options([], 1) + # build with default options = do not recalculate timestamp + ttx.ttCompile(inttx, str(outttf1), options) + # confirm that font was built + assert outttf1.check(file=True) + # confirm that timestamp is same as modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(str(outttf1)) + assert ttf["head"].modified == epochtime + + # reset options to recalculate the timestamp and compile new font + options.recalcTimestamp = True + ttx.ttCompile(inttx, str(outttf2), options) + # confirm that font was built + assert outttf2.check(file=True) + # confirm that timestamp is more recent than modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(str(outttf2)) + assert ttf["head"].modified > epochtime + + +# ------------------------- +# ttx.ttList function tests +# ------------------------- + + +def test_ttlist_ttf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + fakeoutpath = tmpdir.join("TestTTF.ttx") + options = ttx.Options([], 1) + options.listTables = True + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 376" in out + + +def test_ttlist_otf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestOTF.otf") + fakeoutpath = tmpdir.join("TestOTF.ttx") + options = ttx.Options([], 1) + options.listTables = True + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 272" in out + + +def test_ttlist_woff(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF.woff") + fakeoutpath = tmpdir.join("TestWOFF.ttx") + options = ttx.Options([], 1) + options.listTables = True + options.flavor = "woff" + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 84 340" in out + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttlist_woff2(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF2.woff2") + fakeoutpath = tmpdir.join("TestWOFF2.ttx") + options = ttx.Options([], 1) + options.listTables = True + options.flavor = "woff2" + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 0" in out + + +# ------------------- +# main function tests +# ------------------- + + +def test_main_default_ttf_dump_to_ttx(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + outpath = tmpdir.join("TestTTF.ttx") + args = ["-o", str(outpath), inpath] + ttx.main(args) + assert outpath.check(file=True) + + +def test_main_default_ttx_compile_to_ttf(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + ttx.main(args) + assert outpath.check(file=True) + + +def test_main_getopterror_missing_directory(): + with pytest.raises(SystemExit): + with pytest.raises(getopt.GetoptError): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + args = ["-d", "bogusdir", inpath] + ttx.main(args) + + +def test_main_keyboard_interrupt(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, "process", (lambda x, y: raise_exception(KeyboardInterrupt)) + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "(Cancelled.)" in err + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="waitForKeyPress function causes test to hang on Windows platform", +) +def test_main_system_exit(tmpdir, monkeypatch): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, "process", (lambda x, y: raise_exception(SystemExit)) + ) + ttx.main(args) + + +def test_main_ttlib_error(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, + "process", + (lambda x, y: raise_exception(TTLibError("Test error"))), + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Test error" in err + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="waitForKeyPress function causes test to hang on Windows platform", +) +def test_main_base_exception(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, + "process", + (lambda x, y: raise_exception(Exception("Test error"))), + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Unhandled exception has occurred" in err + + +# --------------------------- +# support functions for tests +# --------------------------- + + +def raise_exception(exception): + raise exception diff -Nru fonttools-3.21.2/Tests/unicodedata_test.py fonttools-3.29.0/Tests/unicodedata_test.py --- fonttools-3.21.2/Tests/unicodedata_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/unicodedata_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -165,8 +165,9 @@ assert unicodedata.script_extension("\u0660") == {'Arab', 'Thaa'} assert unicodedata.script_extension("\u0964") == { - 'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', - 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'} + 'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda', + 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', + 'Telu', 'Tirh'} def test_script_name(): @@ -200,7 +201,54 @@ assert unicodedata.block("\x00") == "Basic Latin" assert unicodedata.block("\x7F") == "Basic Latin" assert unicodedata.block("\x80") == "Latin-1 Supplement" - assert unicodedata.block("\u1c90") == "No_Block" + assert unicodedata.block("\u1c90") == "Georgian Extended" + assert unicodedata.block("\u0870") == "No_Block" + + +def test_ot_tags_from_script(): + # simple + assert unicodedata.ot_tags_from_script("Latn") == ["latn"] + # script mapped to multiple new and old script tags + assert unicodedata.ot_tags_from_script("Deva") == ["dev2", "deva"] + # exceptions + assert unicodedata.ot_tags_from_script("Hira") == ["kana"] + # special script codes map to DFLT + assert unicodedata.ot_tags_from_script("Zinh") == ["DFLT"] + assert unicodedata.ot_tags_from_script("Zyyy") == ["DFLT"] + assert unicodedata.ot_tags_from_script("Zzzz") == ["DFLT"] + # this is invalid or unknown + assert unicodedata.ot_tags_from_script("Aaaa") == ["DFLT"] + + +def test_ot_tag_to_script(): + assert unicodedata.ot_tag_to_script("latn") == "Latn" + assert unicodedata.ot_tag_to_script("kana") == "Kana" + assert unicodedata.ot_tag_to_script("DFLT") == None + assert unicodedata.ot_tag_to_script("aaaa") == None + assert unicodedata.ot_tag_to_script("beng") == "Beng" + assert unicodedata.ot_tag_to_script("bng2") == "Beng" + assert unicodedata.ot_tag_to_script("dev2") == "Deva" + assert unicodedata.ot_tag_to_script("gjr2") == "Gujr" + assert unicodedata.ot_tag_to_script("yi ") == "Yiii" + assert unicodedata.ot_tag_to_script("nko ") == "Nkoo" + assert unicodedata.ot_tag_to_script("vai ") == "Vaii" + assert unicodedata.ot_tag_to_script("lao ") == "Laoo" + assert unicodedata.ot_tag_to_script("yi") == "Yiii" + + for invalid_value in ("", " ", "z zz", "zzzzz"): + with pytest.raises(ValueError, match="invalid OpenType tag"): + unicodedata.ot_tag_to_script(invalid_value) + + +def test_script_horizontal_direction(): + assert unicodedata.script_horizontal_direction("Latn") == "LTR" + assert unicodedata.script_horizontal_direction("Arab") == "RTL" + assert unicodedata.script_horizontal_direction("Thaa") == "RTL" + + with pytest.raises(KeyError): + unicodedata.script_horizontal_direction("Azzz") + assert unicodedata.script_horizontal_direction("Azzz", + default="LTR") == "LTR" if __name__ == "__main__": diff -Nru fonttools-3.21.2/Tests/varLib/builder_test.py fonttools-3.29.0/Tests/varLib/builder_test.py --- fonttools-3.21.2/Tests/varLib/builder_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/builder_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -40,12 +40,15 @@ [0, 2, 1], [[128, -129, 1], [3, 5, 4], [6, 8, 7]]), ([0, 1, 2], [[0, 1, 128], [3, -129, 5], [256, 7, 8]], 3, [0, 1, 2], [[0, 1, 128], [3, -129, 5], [256, 7, 8]]), + ([0, 1, 2], [[0, 128, 2], [0, 4, 5], [0, 7, 8]], 1, + [1, 2], [[128, 2], [4, 5], [7, 8]]), ], ids=[ "0/3_shorts_no_reorder", "1/3_shorts_reorder", "2/3_shorts_reorder", "2/3_shorts_same_row_reorder", "3/3_shorts_no_reorder", + "1/3_shorts_1/3_zeroes", ]) def test_buildVarData_optimize( region_indices, items, expected_num_shorts, expected_regions, @@ -54,7 +57,7 @@ assert data.ItemCount == len(items) assert data.NumShorts == expected_num_shorts - assert data.VarRegionCount == len(region_indices) + assert data.VarRegionCount == len(expected_regions) assert data.VarRegionIndex == expected_regions assert data.Item == expected_items diff -Nru fonttools-3.21.2/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx fonttools-3.29.0/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx --- fonttools-3.21.2/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -405,11 +405,13 @@ - - - - - + + + + + + + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/Build3.ttx fonttools-3.29.0/Tests/varLib/data/test_results/Build3.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/Build3.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/Build3.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -1,31 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -93,17 +68,17 @@ - - + + - + - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/BuildMain.ttx fonttools-3.29.0/Tests/varLib/data/test_results/BuildMain.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/BuildMain.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/BuildMain.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -494,12 +494,6 @@ TestFamily-BlackHighContrast - - Weight - - - Contrast - Test Family @@ -578,12 +572,6 @@ TestFamily-BlackHighContrast - - Weight - - - Contrast - @@ -616,88 +604,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -770,18 +676,15 @@ - + - - - - - - - - - + + + + + + @@ -861,14 +764,11 @@ - + - - - - - + + @@ -882,22 +782,23 @@ - + - + - + + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/Build.ttx fonttools-3.29.0/Tests/varLib/data/test_results/Build.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/Build.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/Build.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -1,88 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -155,18 +73,15 @@ - + - - - - - - - - - + + + + + + @@ -246,14 +161,11 @@ - + - - - - - + + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -40,8 +40,8 @@ - - + + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -38,7 +38,7 @@ - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -41,11 +41,11 @@ - + - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -55,7 +55,7 @@ - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -56,7 +56,7 @@ - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -75,7 +75,7 @@ - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -25,7 +25,7 @@ - + diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx fonttools-3.29.0/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -57,7 +57,7 @@ - + @@ -237,15 +237,6 @@ VarFont-Regular - - Width - - - Ascender - - - Regular - VarFont @@ -261,15 +252,6 @@ VarFont-Regular - - Width - - - Ascender - - - Regular - diff -Nru fonttools-3.21.2/Tests/varLib/data/test_results/Mutator.ttx fonttools-3.29.0/Tests/varLib/data/test_results/Mutator.ttx --- fonttools-3.21.2/Tests/varLib/data/test_results/Mutator.ttx 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/data/test_results/Mutator.ttx 2018-07-26 14:12:55.000000000 +0000 @@ -55,7 +55,7 @@ will be recalculated by the compiler --> - + @@ -440,66 +440,6 @@ - - Weight - - - Contrast - - - ExtraLight - - - TestFamily-ExtraLight - - - Light - - - TestFamily-Light - - - Regular - - - TestFamily-Regular - - - Semibold - - - TestFamily-Semibold - - - Bold - - - TestFamily-Bold - - - Black - - - TestFamily-Black - - - Black Medium Contrast - - - TestFamily-BlackMediumContrast - - - Black High Contrast - - - TestFamily-BlackHighContrast - - - Weight - - - Contrast - Test Family @@ -524,66 +464,6 @@ Master 1 - - Weight - - - Contrast - - - ExtraLight - - - TestFamily-ExtraLight - - - Light - - - TestFamily-Light - - - Regular - - - TestFamily-Regular - - - Semibold - - - TestFamily-Semibold - - - Bold - - - TestFamily-Bold - - - Black - - - TestFamily-Black - - - Black Medium Contrast - - - TestFamily-BlackMediumContrast - - - Black High Contrast - - - TestFamily-BlackHighContrast - - - Weight - - - Contrast - diff -Nru fonttools-3.21.2/Tests/varLib/models_test.py fonttools-3.29.0/Tests/varLib/models_test.py --- fonttools-3.21.2/Tests/varLib/models_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/models_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -68,10 +68,32 @@ {0: 1.0}, {0: 1.0}, {0: 1.0, 4: 1.0, 5: 1.0}, - {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, {0: 1.0, 3: 0.75, 4: 0.25, 5: 0.6666666666666667, - 6: 0.16666666666666669, + 6: 0.4444444444444445, 7: 0.6666666666666667}] + +def test_VariationModel(): + locations = [ + {}, + {'bar': 0.5}, + {'bar': 1.0}, + {'foo': 1.0}, + {'bar': 0.5, 'foo': 1.0}, + {'bar': 1.0, 'foo': 1.0}, + ] + model = VariationModel(locations) + + assert model.locations == locations + + assert model.supports == [ + {}, + {'bar': (0, 0.5, 1.0)}, + {'bar': (0.5, 1.0, 1.0)}, + {'foo': (0, 1.0, 1.0)}, + {'bar': (0, 0.5, 1.0), 'foo': (0, 1.0, 1.0)}, + {'bar': (0.5, 1.0, 1.0), 'foo': (0, 1.0, 1.0)}, + ] diff -Nru fonttools-3.21.2/Tests/varLib/varLib_test.py fonttools-3.29.0/Tests/varLib/varLib_test.py --- fonttools-3.21.2/Tests/varLib/varLib_test.py 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/Tests/varLib/varLib_test.py 2018-07-26 14:12:55.000000000 +0000 @@ -190,7 +190,6 @@ """ suffix = '.ttf' ds_path = self.get_test_input('Build.designspace') - ufo_dir = self.get_test_input('master_ufo') ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') self.temp_dir() @@ -202,9 +201,31 @@ ds_copy = os.path.join(self.tempdir, 'BuildMain.designspace') shutil.copy2(ds_path, ds_copy) - varLib_main([ds_copy]) + + # by default, varLib.main finds master TTFs inside a + # 'master_ttf_interpolatable' subfolder in current working dir + cwd = os.getcwd() + os.chdir(self.tempdir) + try: + varLib_main([ds_copy]) + finally: + os.chdir(cwd) varfont_path = os.path.splitext(ds_copy)[0] + '-VF' + suffix + self.assertTrue(os.path.exists(varfont_path)) + + # try again passing an explicit --master-finder + os.remove(varfont_path) + finder = "%s/master_ttf_interpolatable/{stem}.ttf" % self.tempdir + varLib_main([ds_copy, "--master-finder", finder]) + self.assertTrue(os.path.exists(varfont_path)) + + # and also with explicit -o output option + os.remove(varfont_path) + varfont_path = os.path.splitext(varfont_path)[0] + "-o" + suffix + varLib_main([ds_copy, "-o", varfont_path, "--master-finder", finder]) + self.assertTrue(os.path.exists(varfont_path)) + varfont = TTFont(varfont_path) tables = [table_tag for table_tag in varfont.keys() if table_tag != 'head'] expected_ttx_path = self.get_test_output('BuildMain.ttx') diff -Nru fonttools-3.21.2/tox.ini fonttools-3.29.0/tox.ini --- fonttools-3.21.2/tox.ini 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/tox.ini 2018-07-26 14:12:55.000000000 +0000 @@ -1,13 +1,8 @@ [tox] -envlist = py{27,36}-cov, htmlcov +minversion = 3.0 +envlist = py{27,36,37}-cov, htmlcov [testenv] -basepython = - py27: {env:TOXPYTHON:python2.7} - pypy: {env:TOXPYTHON:pypy} - py34: {env:TOXPYTHON:python3.4} - py35: {env:TOXPYTHON:python3.5} - py36: {env:TOXPYTHON:python3.6} deps = cov: coverage>=4.3 pytest @@ -23,7 +18,6 @@ nocov: pytest {posargs} [testenv:htmlcov] -basepython = {env:TOXPYTHON:python3.6} deps = coverage>=4.3 skip_install = true @@ -33,7 +27,6 @@ [testenv:codecov] passenv = * -basepython = {env:TOXPYTHON:python} deps = coverage>=4.3 codecov @@ -44,7 +37,6 @@ codecov --env TOXENV [testenv:bdist] -basepython = {env:TOXPYTHON:python3.6} deps = pygments docutils diff -Nru fonttools-3.21.2/.travis.yml fonttools-3.29.0/.travis.yml --- fonttools-3.21.2/.travis.yml 2018-01-08 12:40:40.000000000 +0000 +++ fonttools-3.29.0/.travis.yml 2018-07-26 14:12:55.000000000 +0000 @@ -1,20 +1,32 @@ sudo: false language: python +python: 3.5 + +# empty "env:" is needed for 'allow_failures' +# https://docs.travis-ci.com/user/customizing-the-build/#Rows-that-are-Allowed-to-Fail +env: matrix: fast_finish: true + exclude: + # Exclude the default Python 3.5 build + - python: 3.5 include: - python: 2.7 env: TOXENV=py27-cov - - python: 3.4 - env: TOXENV=py34-cov - python: 3.5 env: TOXENV=py35-cov - python: 3.6 env: - TOXENV=py36-cov - BUILD_DIST=true + - python: 3.7 + env: TOXENV=py37-cov + # required to run python3.7 on Travis CI + # https://github.com/travis-ci/travis-ci/issues/9815 + dist: xenial + sudo: true - python: pypy2.7-5.8.0 # disable coverage.py on pypy because of performance problems env: TOXENV=pypy-nocov @@ -32,6 +44,17 @@ - PYENV_VERSION_STRING='Python 2.7.6' - PYENV_ROOT=$HOME/.travis-pyenv - TRAVIS_PYENV_VERSION='0.4.0' + allow_failures: + # We use fast_finish + allow_failures because OSX builds take forever + # https://blog.travis-ci.com/2013-11-27-fast-finishing-builds + - language: generic + os: osx + env: TOXENV=py27-cov + - language: generic + os: osx + env: + - TOXENV=py36-cov + - HOMEBREW_NO_AUTO_UPDATE=1 cache: - pip