Merge branch 'main' into avar2

This commit is contained in:
Behdad Esfahbod 2023-03-07 11:21:20 -07:00
commit fd822a2602
434 changed files with 78131 additions and 62625 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# First blackening of code
d584daa8fdc71030f92ee665472d6c7cddd49283

View File

@ -9,6 +9,10 @@ on:
permissions: permissions:
contents: read contents: read
env:
# turns off tox's output redirection so we can debug package installation
TOX_OPTIONS: -vv
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -23,20 +27,20 @@ jobs:
- name: Install packages - name: Install packages
run: pip install tox run: pip install tox
- name: Run Tox - name: Run Tox
run: tox -e mypy,package_readme run: tox $TOX_OPTIONS -e lint,package_readme
test: test:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
strategy: strategy:
matrix: matrix:
python-version: ["3.7", "3.10"] python-version: ["3.8", "3.10"]
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
exclude: # Only test on the latest supported stable Python on macOS and Windows. exclude: # Only test on the latest supported stable Python on macOS and Windows.
- platform: macos-latest - platform: macos-latest
python-version: 3.7 python-version: 3.8
- platform: windows-latest - platform: windows-latest
python-version: 3.7 python-version: 3.8
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -46,9 +50,9 @@ jobs:
- name: Install packages - name: Install packages
run: pip install tox coverage run: pip install tox coverage
- name: Run Tox - name: Run Tox
run: tox -e py-cov run: tox $TOX_OPTIONS -e py-cov
- name: Run Tox without lxml - name: Run Tox without lxml
run: tox -e py-cov-nolxml run: tox $TOX_OPTIONS -e py-cov-nolxml
- name: Produce coverage files - name: Produce coverage files
run: | run: |
coverage combine coverage combine
@ -71,11 +75,11 @@ jobs:
- name: Set up Python 3.x - name: Set up Python 3.x
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.11"
- name: Install packages - name: Install packages
run: pip install tox run: pip install tox
- name: Run Tox - name: Run Tox
run: tox -e py-cy-nolxml run: tox $TOX_OPTIONS -e py-cy-nolxml
test-pypy3: test-pypy3:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -85,8 +89,8 @@ jobs:
- name: Set up Python pypy3 - name: Set up Python pypy3
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "pypy-3.7" python-version: "pypy-3.8"
- name: Install packages - name: Install packages
run: pip install tox run: pip install tox
- name: Run Tox - name: Run Tox
run: tox -e pypy3-nolxml run: tox $TOX_OPTIONS -e pypy3-nolxml

View File

@ -1,4 +1,4 @@
sphinx==5.3.0 sphinx==5.3.0
sphinx_rtd_theme==1.0.0 sphinx_rtd_theme==1.1.1
reportlab==3.6.11 reportlab==3.6.12
freetype-py==2.3.0 freetype-py==2.3.0

View File

@ -30,14 +30,17 @@ needs_sphinx = "1.3"
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx.ext.coverage", "sphinx.ext.autosectionlabel"] extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx.ext.coverage",
"sphinx.ext.autosectionlabel",
]
autodoc_mock_imports = ["gtk", "reportlab"] autodoc_mock_imports = ["gtk", "reportlab"]
autodoc_default_options = { autodoc_default_options = {"members": True, "inherited-members": True}
'members': True,
'inherited-members': True
}
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@ -52,9 +55,11 @@ source_suffix = ".rst"
master_doc = "index" master_doc = "index"
# General information about the project. # General information about the project.
project = u"fontTools" project = "fontTools"
copyright = u"2020, Just van Rossum, Behdad Esfahbod, and the fontTools Authors. CC BY-SA 4.0" copyright = (
author = u"Just van Rossum, Behdad Esfahbod, and the fontTools Authors" "2020, Just van Rossum, Behdad Esfahbod, and the fontTools Authors. CC BY-SA 4.0"
)
author = "Just van Rossum, Behdad Esfahbod, and the fontTools Authors"
# HTML page title # HTML page title
html_title = "fontTools Documentation" html_title = "fontTools Documentation"
@ -64,9 +69,9 @@ html_title = "fontTools Documentation"
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = u"4.0" version = "4.0"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = u"4.0" release = "4.0"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -142,8 +147,8 @@ latex_documents = [
( (
master_doc, master_doc,
"fontTools.tex", "fontTools.tex",
u"fontTools Documentation", "fontTools Documentation",
u"Just van Rossum, Behdad Esfahbod et al.", "Just van Rossum, Behdad Esfahbod et al.",
"manual", "manual",
) )
] ]
@ -153,7 +158,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "fonttools", u"fontTools Documentation", [author], 1)] man_pages = [(master_doc, "fonttools", "fontTools Documentation", [author], 1)]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -165,7 +170,7 @@ texinfo_documents = [
( (
master_doc, master_doc,
"fontTools", "fontTools",
u"fontTools Documentation", "fontTools Documentation",
author, author,
"fontTools", "fontTools",
"A library for manipulating fonts, written in Python.", "A library for manipulating fonts, written in Python.",

View File

@ -187,10 +187,10 @@ for more information.
.. automodule:: fontTools.designspaceLib.split .. automodule:: fontTools.designspaceLib.split
fontTools.designspaceLib.stat fontTools.varLib.stat
============================= =============================
.. automodule:: fontTools.designspaceLib.stat .. automodule:: fontTools.varLib.stat
fontTools.designspaceLib.statNames fontTools.designspaceLib.statNames

View File

@ -542,13 +542,13 @@ element with an ``xml:lang`` attribute:
Defines the coordinates of this source in the design space. Defines the coordinates of this source in the design space.
.. seealso:: `Full documentation of the <location> element <location>`__ .. seealso:: :ref:`Full documentation of the \<location\> element <location>`
``<dimension>`` element (source) ``<dimension>`` element (source)
................................ ................................
.. seealso:: `Full documentation of the <dimension> element <dimension>`__ .. seealso:: :ref:`Full documentation of the \<dimension\> element <dimension>`
``<lib>`` element (source) ``<lib>`` element (source)
@ -836,13 +836,13 @@ The ``<instances>`` element contains one or more ``<instance>`` elements.
Defines the coordinates of this instance in the design space. Defines the coordinates of this instance in the design space.
.. seealso:: `Full documentation of the <location> element <location>`__ .. seealso:: :ref:`Full documentation of the \<location\> element <location>`
``<dimension>`` element (instance) ``<dimension>`` element (instance)
.................................. ..................................
.. seealso:: `Full documentation of the <dimension> element <dimension>`__ .. seealso:: :ref:`Full documentation of the \<dimension\> element <dimension>`
``<lib>`` element (instance) ``<lib>`` element (instance)

View File

@ -101,13 +101,13 @@ Paul Wise.
License License
------- -------
`MIT license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. See the full text of the license for details. `MIT license <https://github.com/fonttools/fonttools/blob/main/LICENSE>`_. See the full text of the license for details.
.. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg .. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg
:target: https://travis-ci.org/fonttools/fonttools :target: https://travis-ci.org/fonttools/fonttools
.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true .. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true
:target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master
.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg .. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/main/graph/badge.svg
:target: https://codecov.io/gh/fonttools/fonttools :target: https://codecov.io/gh/fonttools/fonttools
.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg .. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg
:target: https://pypi.org/project/FontTools :target: https://pypi.org/project/FontTools

View File

@ -13,7 +13,7 @@ About
fontTools is a family of libraries and utilities for manipulating fonts in Python. fontTools is a family of libraries and utilities for manipulating fonts in Python.
The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. Among other things this means you can use it free of charge. The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/main/LICENSE>`_. Among other things this means you can use it free of charge.
Installation Installation
------------ ------------
@ -88,7 +88,7 @@ libraries in the fontTools suite:
- :py:mod:`fontTools.varLib`: Module for dealing with 'gvar'-style font variations - :py:mod:`fontTools.varLib`: Module for dealing with 'gvar'-style font variations
- :py:mod:`fontTools.voltLib`: Module for dealing with Visual OpenType Layout Tool (VOLT) files - :py:mod:`fontTools.voltLib`: Module for dealing with Visual OpenType Layout Tool (VOLT) files
A selection of sample Python programs using these libaries can be found in the `Snippets directory <https://github.com/fonttools/fonttools/blob/master/Snippets/>`_ of the fontTools repository. A selection of sample Python programs using these libaries can be found in the `Snippets directory <https://github.com/fonttools/fonttools/blob/main/Snippets/>`_ of the fontTools repository.
Optional Dependencies Optional Dependencies
--------------------- ---------------------
@ -107,7 +107,7 @@ Information for developers can be found :doc:`here <./developer>`.
License License
------- -------
`MIT license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. See the full text of the license for details. `MIT license <https://github.com/fonttools/fonttools/blob/main/LICENSE>`_. See the full text of the license for details.
Table of Contents Table of Contents
@ -148,7 +148,7 @@ Table of Contents
:target: https://travis-ci.org/fonttools/fonttools :target: https://travis-ci.org/fonttools/fonttools
.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true .. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true
:target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master
.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg .. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/main/graph/badge.svg
:target: https://codecov.io/gh/fonttools/fonttools :target: https://codecov.io/gh/fonttools/fonttools
.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg .. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg
:target: https://pypi.org/project/FontTools :target: https://pypi.org/project/FontTools

View File

@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
version = __version__ = "4.38.1.dev0" version = __version__ = "4.39.1.dev0"
__all__ = ["version", "log", "configLogger"] __all__ = ["version", "log", "configLogger"]

View File

@ -22,13 +22,14 @@ def main(args=None):
sys.argv.append("help") sys.argv.append("help")
if sys.argv[1] == "-h" or sys.argv[1] == "--help": if sys.argv[1] == "-h" or sys.argv[1] == "--help":
sys.argv[1] = "help" sys.argv[1] = "help"
mod = 'fontTools.'+sys.argv[1] mod = "fontTools." + sys.argv[1]
sys.argv[1] = sys.argv[0] + ' ' + sys.argv[1] sys.argv[1] = sys.argv[0] + " " + sys.argv[1]
del sys.argv[0] del sys.argv[0]
import runpy import runpy
runpy.run_module(mod, run_name='__main__')
runpy.run_module(mod, run_name="__main__")
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -82,10 +82,7 @@ kernRE = re.compile(
# regular expressions to parse composite info lines of the form: # regular expressions to parse composite info lines of the form:
# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
compositeRE = re.compile( compositeRE = re.compile(
r"([.A-Za-z0-9_]+)" # char name r"([.A-Za-z0-9_]+)" r"\s+" r"(\d+)" r"\s*;\s*" # char name # number of parts
r"\s+"
r"(\d+)" # number of parts
r"\s*;\s*"
) )
componentRE = re.compile( componentRE = re.compile(
r"PCC\s+" # PPC r"PCC\s+" # PPC
@ -125,16 +122,17 @@ class AFM(object):
_attrs = None _attrs = None
_keywords = ['StartFontMetrics', _keywords = [
'EndFontMetrics', "StartFontMetrics",
'StartCharMetrics', "EndFontMetrics",
'EndCharMetrics', "StartCharMetrics",
'StartKernData', "EndCharMetrics",
'StartKernPairs', "StartKernData",
'EndKernPairs', "StartKernPairs",
'EndKernData', "EndKernPairs",
'StartComposites', "EndKernData",
'EndComposites', "StartComposites",
"EndComposites",
] ]
def __init__(self, path=None): def __init__(self, path=None):
@ -235,13 +233,15 @@ class AFM(object):
assert len(components) == ncomponents assert len(components) == ncomponents
self._composites[charname] = components self._composites[charname] = components
def write(self, path, sep='\r'): def write(self, path, sep="\r"):
"""Writes out an AFM font to the given path.""" """Writes out an AFM font to the given path."""
import time import time
lines = [ "StartFontMetrics 2.0",
"Comment Generated by afmLib; at %s" % ( lines = [
time.strftime("%m/%d/%Y %H:%M:%S", "StartFontMetrics 2.0",
time.localtime(time.time())))] "Comment Generated by afmLib; at %s"
% (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
]
# write comments, assuming (possibly wrongly!) they should # write comments, assuming (possibly wrongly!) they should
# all appear at the top # all appear at the top
@ -267,19 +267,25 @@ class AFM(object):
# write char metrics # write char metrics
lines.append("StartCharMetrics " + repr(len(self._chars))) lines.append("StartCharMetrics " + repr(len(self._chars)))
items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] items = [
(charnum, (charname, width, box))
for charname, (charnum, width, box) in self._chars.items()
]
def myKey(a): def myKey(a):
"""Custom key function to make sure unencoded chars (-1) """Custom key function to make sure unencoded chars (-1)
end up at the end of the list after sorting.""" end up at the end of the list after sorting."""
if a[0] == -1: if a[0] == -1:
a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number
return a return a
items.sort(key=myKey) items.sort(key=myKey)
for charnum, (charname, width, (l, b, r, t)) in items: for charnum, (charname, width, (l, b, r, t)) in items:
lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % lines.append(
(charnum, width, charname, l, b, r, t)) "C %d ; WX %d ; N %s ; B %d %d %d %d ;"
% (charnum, width, charname, l, b, r, t)
)
lines.append("EndCharMetrics") lines.append("EndCharMetrics")
# write kerning info # write kerning info
@ -394,9 +400,9 @@ class AFM(object):
def __repr__(self): def __repr__(self):
if hasattr(self, "FullName"): if hasattr(self, "FullName"):
return '<AFM object for %s>' % self.FullName return "<AFM object for %s>" % self.FullName
else: else:
return '<AFM object at %x>' % id(self) return "<AFM object at %x>" % id(self)
def readlines(path): def readlines(path):
@ -404,20 +410,22 @@ def readlines(path):
data = f.read() data = f.read()
return data.splitlines() return data.splitlines()
def writelines(path, lines, sep='\r'):
def writelines(path, lines, sep="\r"):
with open(path, "w", encoding="ascii", newline=sep) as f: with open(path, "w", encoding="ascii", newline=sep) as f:
f.write("\n".join(lines) + "\n") f.write("\n".join(lines) + "\n")
if __name__ == "__main__": if __name__ == "__main__":
import EasyDialogs import EasyDialogs
path = EasyDialogs.AskFileForOpen() path = EasyDialogs.AskFileForOpen()
if path: if path:
afm = AFM(path) afm = AFM(path)
char = 'A' char = "A"
if afm.has_char(char): if afm.has_char(char):
print(afm[char]) # print charnum, width and boundingbox print(afm[char]) # print charnum, width and boundingbox
pair = ('A', 'V') pair = ("A", "V")
if afm.has_kernpair(pair): if afm.has_kernpair(pair):
print(afm[pair]) # print kerning value for pair print(afm[pair]) # print kerning value for pair
print(afm.Version) # various other afm entries have become attributes print(afm.Version) # various other afm entries have become attributes

View File

@ -5061,10 +5061,12 @@ _aglfnText = """\
class AGLError(Exception): class AGLError(Exception):
pass pass
LEGACY_AGL2UV = {} LEGACY_AGL2UV = {}
AGL2UV = {} AGL2UV = {}
UV2AGL = {} UV2AGL = {}
def _builddicts(): def _builddicts():
import re import re
@ -5073,7 +5075,7 @@ def _builddicts():
parseAGL_RE = re.compile("([A-Za-z0-9]+);((?:[0-9A-F]{4})(?: (?:[0-9A-F]{4}))*)$") parseAGL_RE = re.compile("([A-Za-z0-9]+);((?:[0-9A-F]{4})(?: (?:[0-9A-F]{4}))*)$")
for line in lines: for line in lines:
if not line or line[:1] == '#': if not line or line[:1] == "#":
continue continue
m = parseAGL_RE.match(line) m = parseAGL_RE.match(line)
if not m: if not m:
@ -5089,7 +5091,7 @@ def _builddicts():
parseAGLFN_RE = re.compile("([0-9A-F]{4});([A-Za-z0-9]+);.*?$") parseAGLFN_RE = re.compile("([0-9A-F]{4});([A-Za-z0-9]+);.*?$")
for line in lines: for line in lines:
if not line or line[:1] == '#': if not line or line[:1] == "#":
continue continue
m = parseAGLFN_RE.match(line) m = parseAGLFN_RE.match(line)
if not m: if not m:
@ -5101,6 +5103,7 @@ def _builddicts():
AGL2UV[glyphName] = unicode AGL2UV[glyphName] = unicode
UV2AGL[unicode] = glyphName UV2AGL[unicode] = glyphName
_builddicts() _builddicts()
@ -5123,8 +5126,7 @@ def toUnicode(glyph, isZapfDingbats=False):
# 3. Map each component to a character string according to the # 3. Map each component to a character string according to the
# procedure below, and concatenate those strings; the result # procedure below, and concatenate those strings; the result
# is the character string to which the glyph name is mapped. # is the character string to which the glyph name is mapped.
result = [_glyphComponentToUnicode(c, isZapfDingbats) result = [_glyphComponentToUnicode(c, isZapfDingbats) for c in components]
for c in components]
return "".join(result) return "".join(result)
@ -5169,7 +5171,7 @@ def _glyphComponentToUnicode(component, isZapfDingbats):
return uni return uni
# Otherwise, map the component to an empty string. # Otherwise, map the component to an empty string.
return '' return ""
# https://github.com/adobe-type-tools/agl-aglfn/blob/master/zapfdingbats.txt # https://github.com/adobe-type-tools/agl-aglfn/blob/master/zapfdingbats.txt
@ -5177,12 +5179,13 @@ _AGL_ZAPF_DINGBATS = (
" ✁✂✄☎✆✝✞✟✠✡☛☞✌✍✎✏✑✒✓✔✕✖✗✘✙✚✛✜✢✣✤✥✦✧★✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀" " ✁✂✄☎✆✝✞✟✠✡☛☞✌✍✎✏✑✒✓✔✕✖✗✘✙✚✛✜✢✣✤✥✦✧★✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀"
"❁❂❃❄❅❆❇❈❉❊❋●❍■❏❑▲▼◆❖ ◗❘❙❚❯❱❲❳❨❩❬❭❪❫❴❵❛❜❝❞❡❢❣❤✐❥❦❧♠♥♦♣ ✉✈✇" "❁❂❃❄❅❆❇❈❉❊❋●❍■❏❑▲▼◆❖ ◗❘❙❚❯❱❲❳❨❩❬❭❪❫❴❵❛❜❝❞❡❢❣❤✐❥❦❧♠♥♦♣ ✉✈✇"
"①②③④⑤⑥⑦⑧⑨⑩❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓➔→➣↔" "①②③④⑤⑥⑦⑧⑨⑩❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓➔→➣↔"
"↕➙➛➜➝➞➟➠➡➢➤➥➦➧➨➩➫➭➯➲➳➵➸➺➻➼➽➾➚➪➶➹➘➴➷➬➮➱✃❐❒❮❰") "↕➙➛➜➝➞➟➠➡➢➤➥➦➧➨➩➫➭➯➲➳➵➸➺➻➼➽➾➚➪➶➹➘➴➷➬➮➱✃❐❒❮❰"
)
def _zapfDingbatsToUnicode(glyph): def _zapfDingbatsToUnicode(glyph):
"""Helper for toUnicode().""" """Helper for toUnicode()."""
if len(glyph) < 2 or glyph[0] != 'a': if len(glyph) < 2 or glyph[0] != "a":
return None return None
try: try:
gid = int(glyph[1:]) gid = int(glyph[1:])
@ -5191,7 +5194,7 @@ def _zapfDingbatsToUnicode(glyph):
if gid < 0 or gid >= len(_AGL_ZAPF_DINGBATS): if gid < 0 or gid >= len(_AGL_ZAPF_DINGBATS):
return None return None
uchar = _AGL_ZAPF_DINGBATS[gid] uchar = _AGL_ZAPF_DINGBATS[gid]
return uchar if uchar != ' ' else None return uchar if uchar != " " else None
_re_uni = re.compile("^uni([0-9A-F]+)$") _re_uni = re.compile("^uni([0-9A-F]+)$")
@ -5205,12 +5208,11 @@ def _uniToUnicode(component):
digits = match.group(1) digits = match.group(1)
if len(digits) % 4 != 0: if len(digits) % 4 != 0:
return None return None
chars = [int(digits[i : i + 4], 16) chars = [int(digits[i : i + 4], 16) for i in range(0, len(digits), 4)]
for i in range(0, len(digits), 4)]
if any(c >= 0xD800 and c <= 0xDFFF for c in chars): if any(c >= 0xD800 and c <= 0xDFFF for c in chars):
# The AGL specification explicitly excluded surrogate pairs. # The AGL specification explicitly excluded surrogate pairs.
return None return None
return ''.join([chr(c) for c in chars]) return "".join([chr(c) for c in chars])
_re_u = re.compile("^u([0-9A-F]{4,6})$") _re_u = re.compile("^u([0-9A-F]{4,6})$")
@ -5226,7 +5228,6 @@ def _uToUnicode(component):
value = int(digits, 16) value = int(digits, 16)
except ValueError: except ValueError:
return None return None
if ((value >= 0x0000 and value <= 0xD7FF) or if (value >= 0x0000 and value <= 0xD7FF) or (value >= 0xE000 and value <= 0x10FFFF):
(value >= 0xE000 and value <= 0x10FFFF)):
return chr(value) return chr(value)
return None return None

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@ def stringToProgram(string):
def programToString(program): def programToString(program):
return ' '.join(str(x) for x in program) return " ".join(str(x) for x in program)
def programToCommands(program, getNumRegions=None): def programToCommands(program, getNumRegions=None):
@ -73,7 +73,7 @@ def programToCommands(program, getNumRegions=None):
stack.append(token) stack.append(token)
continue continue
if token == 'blend': if token == "blend":
assert getNumRegions is not None assert getNumRegions is not None
numSourceFonts = 1 + getNumRegions(vsIndex) numSourceFonts = 1 + getNumRegions(vsIndex)
# replace the blend op args on the stack with a single list # replace the blend op args on the stack with a single list
@ -87,16 +87,24 @@ def programToCommands(program, getNumRegions=None):
# if a blend op exists, this is or will be a CFF2 charstring. # if a blend op exists, this is or will be a CFF2 charstring.
continue continue
elif token == 'vsindex': elif token == "vsindex":
vsIndex = stack[-1] vsIndex = stack[-1]
assert type(vsIndex) is int assert type(vsIndex) is int
elif (not seenWidthOp) and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm', elif (not seenWidthOp) and token in {
'cntrmask', 'hintmask', "hstem",
'hmoveto', 'vmoveto', 'rmoveto', "hstemhm",
'endchar'}: "vstem",
"vstemhm",
"cntrmask",
"hintmask",
"hmoveto",
"vmoveto",
"rmoveto",
"endchar",
}:
seenWidthOp = True seenWidthOp = True
parity = token in {'hmoveto', 'vmoveto'} parity = token in {"hmoveto", "vmoveto"}
if lenBlendStack: if lenBlendStack:
# lenBlendStack has the number of args represented by the last blend # lenBlendStack has the number of args represented by the last blend
# arg and all the preceding args. We need to now add the number of # arg and all the preceding args. We need to now add the number of
@ -106,18 +114,18 @@ def programToCommands(program, getNumRegions=None):
numArgs = len(stack) numArgs = len(stack)
if numArgs and (numArgs % 2) ^ parity: if numArgs and (numArgs % 2) ^ parity:
width = stack.pop(0) width = stack.pop(0)
commands.append(('', [width])) commands.append(("", [width]))
if token in {'hintmask', 'cntrmask'}: if token in {"hintmask", "cntrmask"}:
if stack: if stack:
commands.append(('', stack)) commands.append(("", stack))
commands.append((token, [])) commands.append((token, []))
commands.append(('', [next(it)])) commands.append(("", [next(it)]))
else: else:
commands.append((token, stack)) commands.append((token, stack))
stack = [] stack = []
if stack: if stack:
commands.append(('', stack)) commands.append(("", stack))
return commands return commands
@ -126,11 +134,12 @@ def _flattenBlendArgs(args):
for arg in args: for arg in args:
if isinstance(arg, list): if isinstance(arg, list):
token_list.extend(arg) token_list.extend(arg)
token_list.append('blend') token_list.append("blend")
else: else:
token_list.append(arg) token_list.append(arg)
return token_list return token_list
def commandsToProgram(commands): def commandsToProgram(commands):
"""Takes a commands list as returned by programToCommands() and converts """Takes a commands list as returned by programToCommands() and converts
it back to a T2CharString program list.""" it back to a T2CharString program list."""
@ -146,75 +155,93 @@ def commandsToProgram(commands):
def _everyN(el, n): def _everyN(el, n):
"""Group the list el into groups of size n""" """Group the list el into groups of size n"""
if len(el) % n != 0: raise ValueError(el) if len(el) % n != 0:
raise ValueError(el)
for i in range(0, len(el), n): for i in range(0, len(el), n):
yield el[i : i + n] yield el[i : i + n]
class _GeneralizerDecombinerCommandsMap(object): class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def rmoveto(args): def rmoveto(args):
if len(args) != 2: raise ValueError(args) if len(args) != 2:
yield ('rmoveto', args) raise ValueError(args)
yield ("rmoveto", args)
@staticmethod @staticmethod
def hmoveto(args): def hmoveto(args):
if len(args) != 1: raise ValueError(args) if len(args) != 1:
yield ('rmoveto', [args[0], 0]) raise ValueError(args)
yield ("rmoveto", [args[0], 0])
@staticmethod @staticmethod
def vmoveto(args): def vmoveto(args):
if len(args) != 1: raise ValueError(args) if len(args) != 1:
yield ('rmoveto', [0, args[0]]) raise ValueError(args)
yield ("rmoveto", [0, args[0]])
@staticmethod @staticmethod
def rlineto(args): def rlineto(args):
if not args: raise ValueError(args) if not args:
raise ValueError(args)
for args in _everyN(args, 2): for args in _everyN(args, 2):
yield ('rlineto', args) yield ("rlineto", args)
@staticmethod @staticmethod
def hlineto(args): def hlineto(args):
if not args: raise ValueError(args) if not args:
raise ValueError(args)
it = iter(args) it = iter(args)
try: try:
while True: while True:
yield ('rlineto', [next(it), 0]) yield ("rlineto", [next(it), 0])
yield ('rlineto', [0, next(it)]) yield ("rlineto", [0, next(it)])
except StopIteration: except StopIteration:
pass pass
@staticmethod @staticmethod
def vlineto(args): def vlineto(args):
if not args: raise ValueError(args) if not args:
raise ValueError(args)
it = iter(args) it = iter(args)
try: try:
while True: while True:
yield ('rlineto', [0, next(it)]) yield ("rlineto", [0, next(it)])
yield ('rlineto', [next(it), 0]) yield ("rlineto", [next(it), 0])
except StopIteration: except StopIteration:
pass pass
@staticmethod @staticmethod
def rrcurveto(args): def rrcurveto(args):
if not args: raise ValueError(args) if not args:
raise ValueError(args)
for args in _everyN(args, 6): for args in _everyN(args, 6):
yield ('rrcurveto', args) yield ("rrcurveto", args)
@staticmethod @staticmethod
def hhcurveto(args): def hhcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args) if len(args) < 4 or len(args) % 4 > 1:
raise ValueError(args)
if len(args) % 2 == 1: if len(args) % 2 == 1:
yield ('rrcurveto', [args[1], args[0], args[2], args[3], args[4], 0]) yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0])
args = args[5:] args = args[5:]
for args in _everyN(args, 4): for args in _everyN(args, 4):
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[3], 0]) yield ("rrcurveto", [args[0], 0, args[1], args[2], args[3], 0])
@staticmethod @staticmethod
def vvcurveto(args): def vvcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args) if len(args) < 4 or len(args) % 4 > 1:
raise ValueError(args)
if len(args) % 2 == 1: if len(args) % 2 == 1:
yield ('rrcurveto', [args[0], args[1], args[2], args[3], 0, args[4]]) yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]])
args = args[5:] args = args[5:]
for args in _everyN(args, 4): for args in _everyN(args, 4):
yield ('rrcurveto', [0, args[0], args[1], args[2], 0, args[3]]) yield ("rrcurveto", [0, args[0], args[1], args[2], 0, args[3]])
@staticmethod @staticmethod
def hvcurveto(args): def hvcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args) if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}:
raise ValueError(args)
last_args = None last_args = None
if len(args) % 2 == 1: if len(args) % 2 == 1:
lastStraight = len(args) % 8 == 5 lastStraight = len(args) % 8 == 5
@ -223,20 +250,22 @@ class _GeneralizerDecombinerCommandsMap(object):
try: try:
while True: while True:
args = next(it) args = next(it)
yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]]) yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
args = next(it) args = next(it)
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0]) yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
except StopIteration: except StopIteration:
pass pass
if last_args: if last_args:
args = last_args args = last_args
if lastStraight: if lastStraight:
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]]) yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
else: else:
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]]) yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
@staticmethod @staticmethod
def vhcurveto(args): def vhcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args) if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}:
raise ValueError(args)
last_args = None last_args = None
if len(args) % 2 == 1: if len(args) % 2 == 1:
lastStraight = len(args) % 8 == 5 lastStraight = len(args) % 8 == 5
@ -245,32 +274,36 @@ class _GeneralizerDecombinerCommandsMap(object):
try: try:
while True: while True:
args = next(it) args = next(it)
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0]) yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
args = next(it) args = next(it)
yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]]) yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
except StopIteration: except StopIteration:
pass pass
if last_args: if last_args:
args = last_args args = last_args
if lastStraight: if lastStraight:
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]]) yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
else: else:
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]]) yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
@staticmethod @staticmethod
def rcurveline(args): def rcurveline(args):
if len(args) < 8 or len(args) % 6 != 2: raise ValueError(args) if len(args) < 8 or len(args) % 6 != 2:
raise ValueError(args)
args, last_args = args[:-2], args[-2:] args, last_args = args[:-2], args[-2:]
for args in _everyN(args, 6): for args in _everyN(args, 6):
yield ('rrcurveto', args) yield ("rrcurveto", args)
yield ('rlineto', last_args) yield ("rlineto", last_args)
@staticmethod @staticmethod
def rlinecurve(args): def rlinecurve(args):
if len(args) < 8 or len(args) % 2 != 0: raise ValueError(args) if len(args) < 8 or len(args) % 2 != 0:
raise ValueError(args)
args, last_args = args[:-6], args[-6:] args, last_args = args[:-6], args[-6:]
for args in _everyN(args, 2): for args in _everyN(args, 2):
yield ('rlineto', args) yield ("rlineto", args)
yield ('rrcurveto', last_args) yield ("rrcurveto", last_args)
def _convertBlendOpToArgs(blendList): def _convertBlendOpToArgs(blendList):
# args is list of blend op args. Since we are supporting # args is list of blend op args. Since we are supporting
@ -278,8 +311,11 @@ def _convertBlendOpToArgs(blendList):
# be a list of blend op args, and need to be converted before # be a list of blend op args, and need to be converted before
# we convert the current list. # we convert the current list.
if any([isinstance(arg, list) for arg in blendList]): if any([isinstance(arg, list) for arg in blendList]):
args = [i for e in blendList for i in args = [
(_convertBlendOpToArgs(e) if isinstance(e,list) else [e]) ] i
for e in blendList
for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e])
]
else: else:
args = blendList args = blendList
@ -303,10 +339,13 @@ def _convertBlendOpToArgs(blendList):
defaultArgs = [[arg] for arg in args[:numBlends]] defaultArgs = [[arg] for arg in args[:numBlends]]
deltaArgs = args[numBlends:] deltaArgs = args[numBlends:]
numDeltaValues = len(deltaArgs) numDeltaValues = len(deltaArgs)
deltaList = [ deltaArgs[i:i + numRegions] for i in range(0, numDeltaValues, numRegions) ] deltaList = [
deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions)
]
blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)] blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)]
return blend_args return blend_args
def generalizeCommands(commands, ignoreErrors=False): def generalizeCommands(commands, ignoreErrors=False):
result = [] result = []
mapping = _GeneralizerDecombinerCommandsMap mapping = _GeneralizerDecombinerCommandsMap
@ -314,13 +353,19 @@ def generalizeCommands(commands, ignoreErrors=False):
# First, generalize any blend args in the arg list. # First, generalize any blend args in the arg list.
if any([isinstance(arg, list) for arg in args]): if any([isinstance(arg, list) for arg in args]):
try: try:
args = [n for arg in args for n in (_convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg])] args = [
n
for arg in args
for n in (
_convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg]
)
]
except ValueError: except ValueError:
if ignoreErrors: if ignoreErrors:
# Store op as data, such that consumers of commands do not have to # Store op as data, such that consumers of commands do not have to
# deal with incorrect number of arguments. # deal with incorrect number of arguments.
result.append(('', args)) result.append(("", args))
result.append(('', [op])) result.append(("", [op]))
else: else:
raise raise
@ -335,14 +380,17 @@ def generalizeCommands(commands, ignoreErrors=False):
if ignoreErrors: if ignoreErrors:
# Store op as data, such that consumers of commands do not have to # Store op as data, such that consumers of commands do not have to
# deal with incorrect number of arguments. # deal with incorrect number of arguments.
result.append(('', args)) result.append(("", args))
result.append(('', [op])) result.append(("", [op]))
else: else:
raise raise
return result return result
def generalizeProgram(program, getNumRegions=None, **kwargs): def generalizeProgram(program, getNumRegions=None, **kwargs):
return commandsToProgram(generalizeCommands(programToCommands(program, getNumRegions), **kwargs)) return commandsToProgram(
generalizeCommands(programToCommands(program, getNumRegions), **kwargs)
)
def _categorizeVector(v): def _categorizeVector(v):
@ -362,27 +410,35 @@ def _categorizeVector(v):
""" """
if not v[0]: if not v[0]:
if not v[1]: if not v[1]:
return '0', v[:1] return "0", v[:1]
else: else:
return 'v', v[1:] return "v", v[1:]
else: else:
if not v[1]: if not v[1]:
return 'h', v[:1] return "h", v[:1]
else: else:
return 'r', v return "r", v
def _mergeCategories(a, b): def _mergeCategories(a, b):
if a == '0': return b if a == "0":
if b == '0': return a return b
if a == b: return a if b == "0":
return a
if a == b:
return a
return None return None
def _negateCategory(a): def _negateCategory(a):
if a == 'h': return 'v' if a == "h":
if a == 'v': return 'h' return "v"
assert a in '0r' if a == "v":
return "h"
assert a in "0r"
return a return a
def _convertToBlendCmds(args): def _convertToBlendCmds(args):
# return a list of blend commands, and # return a list of blend commands, and
# the remaining non-blended args, if any. # the remaining non-blended args, if any.
@ -435,6 +491,7 @@ def _convertToBlendCmds(args):
return new_args return new_args
def _addArgs(a, b): def _addArgs(a, b):
if isinstance(b, list): if isinstance(b, list):
if isinstance(a, list): if isinstance(a, list):
@ -449,11 +506,13 @@ def _addArgs(a, b):
return a + b return a + b
def specializeCommands(commands, def specializeCommands(
commands,
ignoreErrors=False, ignoreErrors=False,
generalizeFirst=True, generalizeFirst=True,
preserveTopology=False, preserveTopology=False,
maxstack=48): maxstack=48,
):
# We perform several rounds of optimizations. They are carefully ordered and are: # We perform several rounds of optimizations. They are carefully ordered and are:
# #
@ -487,7 +546,6 @@ def specializeCommands(commands,
# #
# 7. For any args which are blend lists, convert them to a blend command. # 7. For any args which are blend lists, convert them to a blend command.
# 0. Generalize commands. # 0. Generalize commands.
if generalizeFirst: if generalizeFirst:
commands = generalizeCommands(commands, ignoreErrors=ignoreErrors) commands = generalizeCommands(commands, ignoreErrors=ignoreErrors)
@ -496,9 +554,9 @@ def specializeCommands(commands,
# 1. Combine successive rmoveto operations. # 1. Combine successive rmoveto operations.
for i in range(len(commands) - 1, 0, -1): for i in range(len(commands) - 1, 0, -1):
if 'rmoveto' == commands[i][0] == commands[i-1][0]: if "rmoveto" == commands[i][0] == commands[i - 1][0]:
v1, v2 = commands[i - 1][1], commands[i][1] v1, v2 = commands[i - 1][1], commands[i][1]
commands[i-1] = ('rmoveto', [v1[0]+v2[0], v1[1]+v2[1]]) commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
del commands[i] del commands[i]
# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
@ -550,15 +608,15 @@ def specializeCommands(commands,
for i in range(len(commands)): for i in range(len(commands)):
op, args = commands[i] op, args = commands[i]
if op in {'rmoveto', 'rlineto'}: if op in {"rmoveto", "rlineto"}:
c, args = _categorizeVector(args) c, args = _categorizeVector(args)
commands[i] = c + op[1:], args commands[i] = c + op[1:], args
continue continue
if op == 'rrcurveto': if op == "rrcurveto":
c1, args1 = _categorizeVector(args[:2]) c1, args1 = _categorizeVector(args[:2])
c2, args2 = _categorizeVector(args[-2:]) c2, args2 = _categorizeVector(args[-2:])
commands[i] = c1+c2+'curveto', args1+args[2:4]+args2 commands[i] = c1 + c2 + "curveto", args1 + args[2:4] + args2
continue continue
# 3. Merge or delete redundant operations, to the extent requested. # 3. Merge or delete redundant operations, to the extent requested.
@ -581,22 +639,21 @@ def specializeCommands(commands,
# For Type2 CharStrings the sequence is: # For Type2 CharStrings the sequence is:
# w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar" # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar"
# Some other redundancies change topology (point numbers). # Some other redundancies change topology (point numbers).
if not preserveTopology: if not preserveTopology:
for i in range(len(commands) - 1, -1, -1): for i in range(len(commands) - 1, -1, -1):
op, args = commands[i] op, args = commands[i]
# A 00curveto is demoted to a (specialized) lineto. # A 00curveto is demoted to a (specialized) lineto.
if op == '00curveto': if op == "00curveto":
assert len(args) == 4 assert len(args) == 4
c, args = _categorizeVector(args[1:3]) c, args = _categorizeVector(args[1:3])
op = c+'lineto' op = c + "lineto"
commands[i] = op, args commands[i] = op, args
# and then... # and then...
# A 0lineto can be deleted. # A 0lineto can be deleted.
if op == '0lineto': if op == "0lineto":
del commands[i] del commands[i]
continue continue
@ -604,8 +661,7 @@ def specializeCommands(commands,
# In CFF2 charstrings from variable fonts, each # In CFF2 charstrings from variable fonts, each
# arg item may be a list of blendable values, one from # arg item may be a list of blendable values, one from
# each source font. # each source font.
if (i and op in {'hlineto', 'vlineto'} and if i and op in {"hlineto", "vlineto"} and (op == commands[i - 1][0]):
(op == commands[i-1][0])):
_, other_args = commands[i - 1] _, other_args = commands[i - 1]
assert len(args) == 1 and len(other_args) == 1 assert len(args) == 1 and len(other_args) == 1
try: try:
@ -622,25 +678,25 @@ def specializeCommands(commands,
op, args = commands[i] op, args = commands[i]
prv, nxt = commands[i - 1][0], commands[i + 1][0] prv, nxt = commands[i - 1][0], commands[i + 1][0]
if op in {'0lineto', 'hlineto', 'vlineto'} and prv == nxt == 'rlineto': if op in {"0lineto", "hlineto", "vlineto"} and prv == nxt == "rlineto":
assert len(args) == 1 assert len(args) == 1
args = [0, args[0]] if op[0] == 'v' else [args[0], 0] args = [0, args[0]] if op[0] == "v" else [args[0], 0]
commands[i] = ('rlineto', args) commands[i] = ("rlineto", args)
continue continue
if op[2:] == 'curveto' and len(args) == 5 and prv == nxt == 'rrcurveto': if op[2:] == "curveto" and len(args) == 5 and prv == nxt == "rrcurveto":
assert (op[0] == 'r') ^ (op[1] == 'r') assert (op[0] == "r") ^ (op[1] == "r")
if op[0] == 'v': if op[0] == "v":
pos = 0 pos = 0
elif op[0] != 'r': elif op[0] != "r":
pos = 1 pos = 1
elif op[1] == 'v': elif op[1] == "v":
pos = 4 pos = 4
else: else:
pos = 5 pos = 5
# Insert, while maintaining the type of args (can be tuple or list). # Insert, while maintaining the type of args (can be tuple or list).
args = args[:pos] + type(args)((0,)) + args[pos:] args = args[:pos] + type(args)((0,)) + args[pos:]
commands[i] = ('rrcurveto', args) commands[i] = ("rrcurveto", args)
continue continue
# 5. Combine adjacent operators when possible, minding not to go over max stack size. # 5. Combine adjacent operators when possible, minding not to go over max stack size.
@ -650,42 +706,46 @@ def specializeCommands(commands,
new_op = None new_op = None
# Merge logic... # Merge logic...
if {op1, op2} <= {'rlineto', 'rrcurveto'}: if {op1, op2} <= {"rlineto", "rrcurveto"}:
if op1 == op2: if op1 == op2:
new_op = op1 new_op = op1
else: else:
if op2 == 'rrcurveto' and len(args2) == 6: if op2 == "rrcurveto" and len(args2) == 6:
new_op = 'rlinecurve' new_op = "rlinecurve"
elif len(args2) == 2: elif len(args2) == 2:
new_op = 'rcurveline' new_op = "rcurveline"
elif (op1, op2) in {('rlineto', 'rlinecurve'), ('rrcurveto', 'rcurveline')}: elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}:
new_op = op2 new_op = op2
elif {op1, op2} == {'vlineto', 'hlineto'}: elif {op1, op2} == {"vlineto", "hlineto"}:
new_op = op1 new_op = op1
elif 'curveto' == op1[2:] == op2[2:]: elif "curveto" == op1[2:] == op2[2:]:
d0, d1 = op1[:2] d0, d1 = op1[:2]
d2, d3 = op2[:2] d2, d3 = op2[:2]
if d1 == 'r' or d2 == 'r' or d0 == d3 == 'r': if d1 == "r" or d2 == "r" or d0 == d3 == "r":
continue continue
d = _mergeCategories(d1, d2) d = _mergeCategories(d1, d2)
if d is None: continue if d is None:
if d0 == 'r': continue
if d0 == "r":
d = _mergeCategories(d, d3) d = _mergeCategories(d, d3)
if d is None: continue if d is None:
new_op = 'r'+d+'curveto' continue
elif d3 == 'r': new_op = "r" + d + "curveto"
elif d3 == "r":
d0 = _mergeCategories(d0, _negateCategory(d)) d0 = _mergeCategories(d0, _negateCategory(d))
if d0 is None: continue if d0 is None:
new_op = d0+'r'+'curveto' continue
new_op = d0 + "r" + "curveto"
else: else:
d0 = _mergeCategories(d0, d3) d0 = _mergeCategories(d0, d3)
if d0 is None: continue if d0 is None:
new_op = d0+d+'curveto' continue
new_op = d0 + d + "curveto"
# Make sure the stack depth does not exceed (maxstack - 1), so # Make sure the stack depth does not exceed (maxstack - 1), so
# that subroutinizer can insert subroutine calls at any point. # that subroutinizer can insert subroutine calls at any point.
@ -697,31 +757,35 @@ def specializeCommands(commands,
for i in range(len(commands)): for i in range(len(commands)):
op, args = commands[i] op, args = commands[i]
if op in {'0moveto', '0lineto'}: if op in {"0moveto", "0lineto"}:
commands[i] = 'h'+op[1:], args commands[i] = "h" + op[1:], args
continue continue
if op[2:] == 'curveto' and op[:2] not in {'rr', 'hh', 'vv', 'vh', 'hv'}: if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}:
op0, op1 = op[:2] op0, op1 = op[:2]
if (op0 == 'r') ^ (op1 == 'r'): if (op0 == "r") ^ (op1 == "r"):
assert len(args) % 2 == 1 assert len(args) % 2 == 1
if op0 == '0': op0 = 'h' if op0 == "0":
if op1 == '0': op1 = 'h' op0 = "h"
if op0 == 'r': op0 = op1 if op1 == "0":
if op1 == 'r': op1 = _negateCategory(op0) op1 = "h"
assert {op0,op1} <= {'h','v'}, (op0, op1) if op0 == "r":
op0 = op1
if op1 == "r":
op1 = _negateCategory(op0)
assert {op0, op1} <= {"h", "v"}, (op0, op1)
if len(args) % 2: if len(args) % 2:
if op0 != op1: # vhcurveto / hvcurveto if op0 != op1: # vhcurveto / hvcurveto
if (op0 == 'h') ^ (len(args) % 8 == 1): if (op0 == "h") ^ (len(args) % 8 == 1):
# Swap last two args order # Swap last two args order
args = args[:-2] + args[-1:] + args[-2:-1] args = args[:-2] + args[-1:] + args[-2:-1]
else: # hhcurveto / vvcurveto else: # hhcurveto / vvcurveto
if op0 == 'h': # hhcurveto if op0 == "h": # hhcurveto
# Swap first two args order # Swap first two args order
args = args[1:2] + args[:1] + args[2:] args = args[1:2] + args[:1] + args[2:]
commands[i] = op0+op1+'curveto', args commands[i] = op0 + op1 + "curveto", args
continue continue
# 7. For any series of args which are blend lists, convert the series to a single blend arg. # 7. For any series of args which are blend lists, convert the series to a single blend arg.
@ -732,36 +796,55 @@ def specializeCommands(commands,
return commands return commands
def specializeProgram(program, getNumRegions=None, **kwargs): def specializeProgram(program, getNumRegions=None, **kwargs):
return commandsToProgram(specializeCommands(programToCommands(program, getNumRegions), **kwargs)) return commandsToProgram(
specializeCommands(programToCommands(program, getNumRegions), **kwargs)
)
if __name__ == '__main__': if __name__ == "__main__":
import sys import sys
if len(sys.argv) == 1: if len(sys.argv) == 1:
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)
import argparse import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
"fonttools cffLib.specialer", description="CFF CharString generalizer/specializer") "fonttools cffLib.specialer",
description="CFF CharString generalizer/specializer",
)
parser.add_argument("program", metavar="command", nargs="*", help="Commands.")
parser.add_argument( parser.add_argument(
"program", metavar="command", nargs="*", help="Commands.") "--num-regions",
parser.add_argument( metavar="NumRegions",
"--num-regions", metavar="NumRegions", nargs="*", default=None, nargs="*",
help="Number of variable-font regions for blend opertaions.") default=None,
help="Number of variable-font regions for blend opertaions.",
)
options = parser.parse_args(sys.argv[1:]) options = parser.parse_args(sys.argv[1:])
getNumRegions = None if options.num_regions is None else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) getNumRegions = (
None
if options.num_regions is None
else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex])
)
program = stringToProgram(options.program) program = stringToProgram(options.program)
print("Program:"); print(programToString(program)) print("Program:")
print(programToString(program))
commands = programToCommands(program, getNumRegions) commands = programToCommands(program, getNumRegions)
print("Commands:"); print(commands) print("Commands:")
print(commands)
program2 = commandsToProgram(commands) program2 = commandsToProgram(commands)
print("Program from commands:"); print(programToString(program2)) print("Program from commands:")
print(programToString(program2))
assert program == program2 assert program == program2
print("Generalized program:"); print(programToString(generalizeProgram(program, getNumRegions))) print("Generalized program:")
print("Specialized program:"); print(programToString(specializeProgram(program, getNumRegions))) print(programToString(generalizeProgram(program, getNumRegions)))
print("Specialized program:")
print(programToString(specializeProgram(program, getNumRegions)))

View File

@ -16,9 +16,11 @@ from functools import reduce
class missingdict(dict): class missingdict(dict):
def __init__(self, missing_func): def __init__(self, missing_func):
self.missing_func = missing_func self.missing_func = missing_func
def __missing__(self, v): def __missing__(self, v):
return self.missing_func(v) return self.missing_func(v)
def cumSum(f, op=add, start=0, decreasing=False): def cumSum(f, op=add, start=0, decreasing=False):
keys = sorted(f.keys()) keys = sorted(f.keys())
@ -42,9 +44,10 @@ def cumSum(f, op=add, start=0, decreasing=False):
return out return out
def byteCost(widths, default, nominal): def byteCost(widths, default, nominal):
if not hasattr(widths, 'items'): if not hasattr(widths, "items"):
d = defaultdict(int) d = defaultdict(int)
for w in widths: for w in widths:
d[w] += 1 d[w] += 1
@ -52,7 +55,8 @@ def byteCost(widths, default, nominal):
cost = 0 cost = 0
for w, freq in widths.items(): for w, freq in widths.items():
if w == default: continue if w == default:
continue
diff = abs(w - nominal) diff = abs(w - nominal)
if diff <= 107: if diff <= 107:
cost += freq cost += freq
@ -98,7 +102,7 @@ def optimizeWidths(widths):
This algorithm is linear in UPEM+numGlyphs.""" This algorithm is linear in UPEM+numGlyphs."""
if not hasattr(widths, 'items'): if not hasattr(widths, "items"):
d = defaultdict(int) d = defaultdict(int)
for w in widths: for w in widths:
d[w] += 1 d[w] += 1
@ -115,13 +119,21 @@ def optimizeWidths(widths):
cumMaxD = cumSum(widths, op=max, decreasing=True) cumMaxD = cumSum(widths, op=max, decreasing=True)
# Cost per nominal choice, without default consideration. # Cost per nominal choice, without default consideration.
nomnCostU = missingdict(lambda x: cumFrqU[x] + cumFrqU[x-108] + cumFrqU[x-1132]*3) nomnCostU = missingdict(
nomnCostD = missingdict(lambda x: cumFrqD[x] + cumFrqD[x+108] + cumFrqD[x+1132]*3) 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]) nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x])
# Cost-saving per nominal choice, by best default choice. # Cost-saving per nominal choice, by best default choice.
dfltCostU = missingdict(lambda x: max(cumMaxU[x], cumMaxU[x-108]*2, cumMaxU[x-1132]*5)) dfltCostU = missingdict(
dfltCostD = missingdict(lambda x: max(cumMaxD[x], cumMaxD[x+108]*2, cumMaxD[x+1132]*5)) 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])) dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x]))
# Combined cost per nominal choice. # Combined cost per nominal choice.
@ -150,34 +162,48 @@ def optimizeWidths(widths):
return default, nominal return default, nominal
def main(args=None): def main(args=None):
"""Calculate optimum defaultWidthX/nominalWidthX values""" """Calculate optimum defaultWidthX/nominalWidthX values"""
import argparse import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
"fonttools cffLib.width", "fonttools cffLib.width",
description=main.__doc__, description=main.__doc__,
) )
parser.add_argument('inputs', metavar='FILE', type=str, nargs='+', parser.add_argument(
help="Input TTF files") "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files"
parser.add_argument('-b', '--brute-force', dest="brute", action="store_true", )
help="Use brute-force approach (VERY slow)") parser.add_argument(
"-b",
"--brute-force",
dest="brute",
action="store_true",
help="Use brute-force approach (VERY slow)",
)
args = parser.parse_args(args) args = parser.parse_args(args)
for fontfile in args.inputs: for fontfile in args.inputs:
font = TTFont(fontfile) font = TTFont(fontfile)
hmtx = font['hmtx'] hmtx = font["hmtx"]
widths = [m[0] for m in hmtx.metrics.values()] widths = [m[0] for m in hmtx.metrics.values()]
if args.brute: if args.brute:
default, nominal = optimizeWidthsBruteforce(widths) default, nominal = optimizeWidthsBruteforce(widths)
else: else:
default, nominal = optimizeWidths(widths) default, nominal = optimizeWidths(widths)
print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) print(
"glyphs=%d default=%d nominal=%d byteCost=%d"
% (len(widths), default, nominal, byteCost(widths, default, nominal))
)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
if len(sys.argv) == 1: if len(sys.argv) == 1:
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)
main() main()

View File

@ -1,3 +1,2 @@
class ColorLibError(Exception): class ColorLibError(Exception):
pass pass

View File

@ -67,9 +67,7 @@ def _split_format(cls, source):
assert isinstance( assert isinstance(
fmt, collections.abc.Hashable fmt, collections.abc.Hashable
), f"{cls} Format is not hashable: {fmt!r}" ), f"{cls} Format is not hashable: {fmt!r}"
assert ( assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}"
fmt in cls.convertersByName
), f"{cls} invalid Format: {fmt!r}"
return fmt, remainder return fmt, remainder

View File

@ -4,46 +4,52 @@ from .cu2qu import *
import random import random
import timeit import timeit
MAX_ERR = 5 MAX_ERR = 0.05
def generate_curve(): def generate_curve():
return [ return [
tuple(float(random.randint(0, 2048)) for coord in range(2)) tuple(float(random.randint(0, 2048)) for coord in range(2))
for point in range(4)] for point in range(4)
]
def setup_curve_to_quadratic(): def setup_curve_to_quadratic():
return generate_curve(), MAX_ERR return generate_curve(), MAX_ERR
def setup_curves_to_quadratic(): def setup_curves_to_quadratic():
num_curves = 3 num_curves = 3
return ( return ([generate_curve() for curve in range(num_curves)], [MAX_ERR] * num_curves)
[generate_curve() for curve in range(num_curves)],
[MAX_ERR] * num_curves)
def run_benchmark(
benchmark_module, module, function, setup_suffix='', repeat=5, number=1000): def run_benchmark(module, function, setup_suffix="", repeat=5, number=1000):
setup_func = 'setup_' + function setup_func = "setup_" + function
if setup_suffix: if setup_suffix:
print('%s with %s:' % (function, setup_suffix), end='') print("%s with %s:" % (function, setup_suffix), end="")
setup_func += '_' + setup_suffix setup_func += "_" + setup_suffix
else: else:
print('%s:' % function, end='') print("%s:" % function, end="")
def wrapper(function, setup_func): def wrapper(function, setup_func):
function = globals()[function] function = globals()[function]
setup_func = globals()[setup_func] setup_func = globals()[setup_func]
def wrapped(): def wrapped():
return function(*setup_func()) return function(*setup_func())
return wrapped return wrapped
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number) results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
print('\t%5.1fus' % (min(results) * 1000000. / number)) print("\t%5.1fus" % (min(results) * 1000000.0 / number))
def main(): def main():
"""Benchmark the cu2qu algorithm performance.""" """Benchmark the cu2qu algorithm performance."""
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic') run_benchmark("cu2qu", "curve_to_quadratic")
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic') run_benchmark("cu2qu", "curves_to_quadratic")
if __name__ == '__main__': if __name__ == "__main__":
random.seed(1) random.seed(1)
main() main()

View File

@ -37,7 +37,7 @@ def open_ufo(path):
def _font_to_quadratic(input_path, output_path=None, **kwargs): def _font_to_quadratic(input_path, output_path=None, **kwargs):
ufo = open_ufo(input_path) ufo = open_ufo(input_path)
logger.info('Converting curves for %s', input_path) logger.info("Converting curves for %s", input_path)
if font_to_quadratic(ufo, **kwargs): if font_to_quadratic(ufo, **kwargs):
logger.info("Saving %s", output_path) logger.info("Saving %s", output_path)
if output_path: if output_path:
@ -67,13 +67,13 @@ def _copytree(input_path, output_path):
def main(args=None): def main(args=None):
"""Convert a UFO font from cubic to quadratic curves""" """Convert a UFO font from cubic to quadratic curves"""
parser = argparse.ArgumentParser(prog="cu2qu") parser = argparse.ArgumentParser(prog="cu2qu")
parser.add_argument( parser.add_argument("--version", action="version", version=fontTools.__version__)
"--version", action="version", version=fontTools.__version__)
parser.add_argument( parser.add_argument(
"infiles", "infiles",
nargs="+", nargs="+",
metavar="INPUT", metavar="INPUT",
help="one or more input UFO source file(s).") help="one or more input UFO source file(s).",
)
parser.add_argument("-v", "--verbose", action="count", default=0) parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument( parser.add_argument(
"-e", "-e",
@ -81,19 +81,28 @@ def main(args=None):
type=float, type=float,
metavar="ERROR", metavar="ERROR",
default=None, default=None,
help="maxiumum approximation error measured in EM (default: 0.001)") help="maxiumum approximation error measured in EM (default: 0.001)",
)
parser.add_argument(
"-m",
"--mixed",
default=False,
action="store_true",
help="whether to used mixed quadratic and cubic curves",
)
parser.add_argument( parser.add_argument(
"--keep-direction", "--keep-direction",
dest="reverse_direction", dest="reverse_direction",
action="store_false", action="store_false",
help="do not reverse the contour direction") help="do not reverse the contour direction",
)
mode_parser = parser.add_mutually_exclusive_group() mode_parser = parser.add_mutually_exclusive_group()
mode_parser.add_argument( mode_parser.add_argument(
"-i", "-i",
"--interpolatable", "--interpolatable",
action="store_true", action="store_true",
help="whether curve conversion should keep interpolation compatibility" help="whether curve conversion should keep interpolation compatibility",
) )
mode_parser.add_argument( mode_parser.add_argument(
"-j", "-j",
@ -103,7 +112,8 @@ def main(args=None):
default=1, default=1,
const=_cpu_count(), const=_cpu_count(),
metavar="N", metavar="N",
help="Convert using N multiple processes (default: %(default)s)") help="Convert using N multiple processes (default: %(default)s)",
)
output_parser = parser.add_mutually_exclusive_group() output_parser = parser.add_mutually_exclusive_group()
output_parser.add_argument( output_parser.add_argument(
@ -111,14 +121,18 @@ def main(args=None):
"--output-file", "--output-file",
default=None, default=None,
metavar="OUTPUT", metavar="OUTPUT",
help=("output filename for the converted UFO. By default fonts are " help=(
"modified in place. This only works with a single input.")) "output filename for the converted UFO. By default fonts are "
"modified in place. This only works with a single input."
),
)
output_parser.add_argument( output_parser.add_argument(
"-d", "-d",
"--output-dir", "--output-dir",
default=None, default=None,
metavar="DIRECTORY", metavar="DIRECTORY",
help="output directory where to save converted UFOs") help="output directory where to save converted UFOs",
)
options = parser.parse_args(args) options = parser.parse_args(args)
@ -143,8 +157,7 @@ def main(args=None):
elif not os.path.isdir(output_dir): elif not os.path.isdir(output_dir):
parser.error("'%s' is not a directory" % output_dir) parser.error("'%s' is not a directory" % output_dir)
output_paths = [ output_paths = [
os.path.join(output_dir, os.path.basename(p)) os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
for p in options.infiles
] ]
elif options.output_file: elif options.output_file:
output_paths = [options.output_file] output_paths = [options.output_file]
@ -152,12 +165,15 @@ def main(args=None):
# save in-place # save in-place
output_paths = [None] * len(options.infiles) output_paths = [None] * len(options.infiles)
kwargs = dict(dump_stats=options.verbose > 0, kwargs = dict(
dump_stats=options.verbose > 0,
max_err_em=options.conversion_error, max_err_em=options.conversion_error,
reverse_direction=options.reverse_direction) reverse_direction=options.reverse_direction,
all_quadratic=False if options.mixed else True,
)
if options.interpolatable: if options.interpolatable:
logger.info('Converting curves compatibly') logger.info("Converting curves compatibly")
ufos = [open_ufo(infile) for infile in options.infiles] ufos = [open_ufo(infile) for infile in options.infiles]
if fonts_to_quadratic(ufos, **kwargs): if fonts_to_quadratic(ufos, **kwargs):
for ufo, output_path in zip(ufos, output_paths): for ufo, output_path in zip(ufos, output_paths):
@ -171,11 +187,10 @@ def main(args=None):
if output_path: if output_path:
_copytree(input_path, output_path) _copytree(input_path, output_path)
else: else:
jobs = min(len(options.infiles), jobs = min(len(options.infiles), options.jobs) if options.jobs > 1 else 1
options.jobs) if options.jobs > 1 else 1
if jobs > 1: if jobs > 1:
func = partial(_font_to_quadratic, **kwargs) func = partial(_font_to_quadratic, **kwargs)
logger.info('Running %d parallel processes', jobs) logger.info("Running %d parallel processes", jobs)
with closing(mp.Pool(jobs)) as pool: with closing(mp.Pool(jobs)) as pool:
pool.starmap(func, zip(options.infiles, output_paths)) pool.starmap(func, zip(options.infiles, output_paths))
else: else:

View File

@ -17,30 +17,26 @@
try: try:
import cython import cython
except ImportError:
COMPILED = cython.compiled
except (AttributeError, ImportError):
# if cython not installed, use mock module with no-op decorators and types # if cython not installed, use mock module with no-op decorators and types
from fontTools.misc import cython from fontTools.misc import cython
COMPILED = False
import math import math
from .errors import Error as Cu2QuError, ApproxNotFoundError from .errors import Error as Cu2QuError, ApproxNotFoundError
__all__ = ['curve_to_quadratic', 'curves_to_quadratic'] __all__ = ["curve_to_quadratic", "curves_to_quadratic"]
MAX_N = 100 MAX_N = 100
NAN = float("NaN") NAN = float("NaN")
if cython.compiled:
# Yep, I'm compiled.
COMPILED = True
else:
# Just a lowly interpreted script.
COMPILED = False
@cython.cfunc @cython.cfunc
@cython.inline @cython.inline
@cython.returns(cython.double) @cython.returns(cython.double)
@ -61,7 +57,9 @@ def dot(v1, v2):
@cython.cfunc @cython.cfunc
@cython.inline @cython.inline
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex) @cython.locals(
_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex
)
def calc_cubic_points(a, b, c, d): def calc_cubic_points(a, b, c, d):
_1 = d _1 = d
_2 = (c / 3.0) + d _2 = (c / 3.0) + d
@ -72,7 +70,9 @@ def calc_cubic_points(a, b, c, d):
@cython.cfunc @cython.cfunc
@cython.inline @cython.inline
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(
p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
)
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
def calc_cubic_parameters(p0, p1, p2, p3): def calc_cubic_parameters(p0, p1, p2, p3):
c = (p1 - p0) * 3.0 c = (p1 - p0) * 3.0
@ -83,7 +83,9 @@ def calc_cubic_parameters(p0, p1, p2, p3):
@cython.cfunc @cython.cfunc
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(
p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
)
def split_cubic_into_n_iter(p0, p1, p2, p3, n): def split_cubic_into_n_iter(p0, p1, p2, p3, n):
"""Split a cubic Bezier into n equal parts. """Split a cubic Bezier into n equal parts.
@ -115,10 +117,20 @@ def split_cubic_into_n_iter(p0, p1, p2, p3, n):
return _split_cubic_into_n_gen(p0, p1, p2, p3, n) return _split_cubic_into_n_gen(p0, p1, p2, p3, n)
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int) @cython.locals(
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
n=cython.int,
)
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int) @cython.locals(
@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex) dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int
)
@cython.locals(
a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex
)
def _split_cubic_into_n_gen(p0, p1, p2, p3, n): def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3)
dt = 1 / n dt = 1 / n
@ -135,7 +147,9 @@ def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
yield calc_cubic_points(a1, b1, c1, d1) yield calc_cubic_points(a1, b1, c1, d1)
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(
p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
)
@cython.locals(mid=cython.complex, deriv3=cython.complex) @cython.locals(mid=cython.complex, deriv3=cython.complex)
def split_cubic_into_two(p0, p1, p2, p3): def split_cubic_into_two(p0, p1, p2, p3):
"""Split a cubic Bezier into two equal parts. """Split a cubic Bezier into two equal parts.
@ -152,14 +166,27 @@ def split_cubic_into_two(p0, p1, p2, p3):
tuple: Two cubic Beziers (each expressed as a tuple of four complex tuple: Two cubic Beziers (each expressed as a tuple of four complex
values). values).
""" """
mid = (p0 + 3 * (p1 + p2) + p3) * .125 mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
deriv3 = (p3 + p2 - p1 - p0) * .125 deriv3 = (p3 + p2 - p1 - p0) * 0.125
return ((p0, (p0 + p1) * .5, mid - deriv3, mid), return (
(mid, mid + deriv3, (p2 + p3) * .5, p3)) (p0, (p0 + p1) * 0.5, mid - deriv3, mid),
(mid, mid + deriv3, (p2 + p3) * 0.5, p3),
)
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double) @cython.locals(
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex) p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
_27=cython.double,
)
@cython.locals(
mid1=cython.complex,
deriv1=cython.complex,
mid2=cython.complex,
deriv2=cython.complex,
)
def split_cubic_into_three(p0, p1, p2, p3, _27=1 / 27): def split_cubic_into_three(p0, p1, p2, p3, _27=1 / 27):
"""Split a cubic Bezier into three equal parts. """Split a cubic Bezier into three equal parts.
@ -181,13 +208,21 @@ def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
deriv1 = (p3 + 3 * p2 - 4 * p0) * _27 deriv1 = (p3 + 3 * p2 - 4 * p0) * _27
mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * _27 mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * _27
deriv2 = (4 * p3 - 3 * p1 - p0) * _27 deriv2 = (4 * p3 - 3 * p1 - p0) * _27
return ((p0, (2*p0 + p1) / 3.0, mid1 - deriv1, mid1), return (
(p0, (2 * p0 + p1) / 3.0, mid1 - deriv1, mid1),
(mid1, mid1 + deriv1, mid2 - deriv2, mid2), (mid1, mid1 + deriv1, mid2 - deriv2, mid2),
(mid2, mid2 + deriv2, (p2 + 2*p3) / 3.0, p3)) (mid2, mid2 + deriv2, (p2 + 2 * p3) / 3.0, p3),
)
@cython.returns(cython.complex) @cython.returns(cython.complex)
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(
t=cython.double,
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
)
@cython.locals(_p1=cython.complex, _p2=cython.complex) @cython.locals(_p1=cython.complex, _p2=cython.complex)
def cubic_approx_control(t, p0, p1, p2, p3): def cubic_approx_control(t, p0, p1, p2, p3):
"""Approximate a cubic Bezier using a quadratic one. """Approximate a cubic Bezier using a quadratic one.
@ -235,7 +270,13 @@ def calc_intersect(a, b, c, d):
@cython.cfunc @cython.cfunc
@cython.returns(cython.int) @cython.returns(cython.int)
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(
tolerance=cython.double,
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
)
@cython.locals(mid=cython.complex, deriv3=cython.complex) @cython.locals(mid=cython.complex, deriv3=cython.complex)
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
"""Check if a cubic Bezier lies within a given distance of the origin. """Check if a cubic Bezier lies within a given distance of the origin.
@ -260,17 +301,24 @@ def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
return True return True
# Split. # Split.
mid = (p0 + 3 * (p1 + p2) + p3) * .125 mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
if abs(mid) > tolerance: if abs(mid) > tolerance:
return False return False
deriv3 = (p3 + p2 - p1 - p0) * .125 deriv3 = (p3 + p2 - p1 - p0) * 0.125
return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and return cubic_farthest_fit_inside(
cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance)) p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance
) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance)
@cython.cfunc @cython.cfunc
@cython.locals(tolerance=cython.double, _2_3=cython.double) @cython.locals(tolerance=cython.double, _2_3=cython.double)
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) @cython.locals(
q1=cython.complex,
c0=cython.complex,
c1=cython.complex,
c2=cython.complex,
c3=cython.complex,
)
def cubic_approx_quadratic(cubic, tolerance, _2_3=2 / 3): def cubic_approx_quadratic(cubic, tolerance, _2_3=2 / 3):
"""Approximate a cubic Bezier with a single quadratic within a given tolerance. """Approximate a cubic Bezier with a single quadratic within a given tolerance.
@ -294,10 +342,7 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
c3 = cubic[3] c3 = cubic[3]
c1 = c0 + (q1 - c0) * _2_3 c1 = c0 + (q1 - c0) * _2_3
c2 = c3 + (q1 - c3) * _2_3 c2 = c3 + (q1 - c3) * _2_3
if not cubic_farthest_fit_inside(0, if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance):
c1 - cubic[1],
c2 - cubic[2],
0, tolerance):
return None return None
return c0, q1, c3 return c0, q1, c3
@ -305,9 +350,17 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
@cython.cfunc @cython.cfunc
@cython.locals(n=cython.int, tolerance=cython.double, _2_3=cython.double) @cython.locals(n=cython.int, tolerance=cython.double, _2_3=cython.double)
@cython.locals(i=cython.int) @cython.locals(i=cython.int)
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) @cython.locals(
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex) c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex
def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): )
@cython.locals(
q0=cython.complex,
q1=cython.complex,
next_q1=cython.complex,
q2=cython.complex,
d1=cython.complex,
)
def cubic_approx_spline(cubic, n, tolerance, all_quadratic, _2_3=2 / 3):
"""Approximate a cubic Bezier curve with a spline of n quadratics. """Approximate a cubic Bezier curve with a spline of n quadratics.
Args: Args:
@ -326,6 +379,8 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
if n == 1: if n == 1:
return cubic_approx_quadratic(cubic, tolerance) return cubic_approx_quadratic(cubic, tolerance)
if n == 2 and all_quadratic == False:
return cubic
cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n)
@ -347,7 +402,7 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
next_cubic = next(cubics) next_cubic = next(cubics)
next_q1 = cubic_approx_control(i / (n - 1), *next_cubic) next_q1 = cubic_approx_control(i / (n - 1), *next_cubic)
spline.append(next_q1) spline.append(next_q1)
q2 = (q1 + next_q1) * .5 q2 = (q1 + next_q1) * 0.5
else: else:
q2 = c3 q2 = c3
@ -355,12 +410,9 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
d0 = d1 d0 = d1
d1 = q2 - c3 d1 = q2 - c3
if (abs(d1) > tolerance or if abs(d1) > tolerance or not cubic_farthest_fit_inside(
not cubic_farthest_fit_inside(d0, d0, q0 + (q1 - q0) * _2_3 - c1, q2 + (q1 - q2) * _2_3 - c2, d1, tolerance
q0 + (q1 - q0) * _2_3 - c1, ):
q2 + (q1 - q2) * _2_3 - c2,
d1,
tolerance)):
return None return None
spline.append(cubic[3]) spline.append(cubic[3])
@ -369,24 +421,31 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
@cython.locals(max_err=cython.double) @cython.locals(max_err=cython.double)
@cython.locals(n=cython.int) @cython.locals(n=cython.int)
def curve_to_quadratic(curve, max_err): def curve_to_quadratic(curve, max_err, all_quadratic=True):
"""Approximate a cubic Bezier curve with a spline of n quadratics. """Approximate a cubic Bezier curve with a spline of n quadratics.
Args: Args:
cubic (sequence): Four 2D tuples representing control points of cubic (sequence): Four 2D tuples representing control points of
the cubic Bezier curve. the cubic Bezier curve.
max_err (double): Permitted deviation from the original curve. max_err (double): Permitted deviation from the original curve.
all_quadratic (bool): If True (default) returned value is a
quadratic spline. If False, it's either a single quadratic
curve or a single cubic curve.
Returns: Returns:
A list of 2D tuples, representing control points of the quadratic If all_quadratic is True: A list of 2D tuples, representing
spline if it fits within the given tolerance, or ``None`` if no control points of the quadratic spline if it fits within the
suitable spline could be calculated. given tolerance, or ``None`` if no suitable spline could be
calculated.
If all_quadratic is False: Either a quadratic curve (if length
of output is 3), or a cubic curve (if length of output is 4).
""" """
curve = [complex(*p) for p in curve] curve = [complex(*p) for p in curve]
for n in range(1, MAX_N + 1): for n in range(1, MAX_N + 1):
spline = cubic_approx_spline(curve, n, max_err) spline = cubic_approx_spline(curve, n, max_err, all_quadratic)
if spline is not None: if spline is not None:
# done. go home # done. go home
return [(s.real, s.imag) for s in spline] return [(s.real, s.imag) for s in spline]
@ -394,9 +453,8 @@ def curve_to_quadratic(curve, max_err):
raise ApproxNotFoundError(curve) raise ApproxNotFoundError(curve)
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int) @cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
def curves_to_quadratic(curves, max_errors): def curves_to_quadratic(curves, max_errors, all_quadratic=True):
"""Return quadratic Bezier splines approximating the input cubic Beziers. """Return quadratic Bezier splines approximating the input cubic Beziers.
Args: Args:
@ -404,6 +462,9 @@ def curves_to_quadratic(curves, max_errors):
2D tuples. 2D tuples.
max_errors: A sequence of *n* floats representing the maximum permissible max_errors: A sequence of *n* floats representing the maximum permissible
deviation from each of the cubic Bezier curves. deviation from each of the cubic Bezier curves.
all_quadratic (bool): If True (default) returned values are a
quadratic spline. If False, they are either a single quadratic
curve or a single cubic curve.
Example:: Example::
@ -419,7 +480,11 @@ def curves_to_quadratic(curves, max_errors):
( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...). ( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).
Returns: Returns:
A list of splines, each spline being a list of 2D tuples. If all_quadratic is True, a list of splines, each spline being a list
of 2D tuples.
If all_quadratic is False, a list of curves, each curve being a quadratic
(length 3), or cubic (length 4).
Raises: Raises:
fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation
@ -434,7 +499,7 @@ def curves_to_quadratic(curves, max_errors):
last_i = i = 0 last_i = i = 0
n = 1 n = 1
while True: while True:
spline = cubic_approx_spline(curves[i], n, max_errors[i]) spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic)
if spline is None: if spline is None:
if n == MAX_N: if n == MAX_N:
break break
@ -448,5 +513,3 @@ def curves_to_quadratic(curves, max_errors):
return [[(s.real, s.imag) for s in spline] for spline in splines] return [[(s.real, s.imag) for s in spline] for spline in splines]
raise ApproxNotFoundError(curves) raise ApproxNotFoundError(curves)

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
class Error(Exception): class Error(Exception):
"""Base Cu2Qu exception class for all other errors.""" """Base Cu2Qu exception class for all other errors."""

View File

@ -30,12 +30,15 @@ from fontTools.pens.reverseContourPen import ReverseContourPen
from . import curves_to_quadratic from . import curves_to_quadratic
from .errors import ( from .errors import (
UnequalZipLengthsError, IncompatibleSegmentNumberError, UnequalZipLengthsError,
IncompatibleSegmentTypesError, IncompatibleGlyphsError, IncompatibleSegmentNumberError,
IncompatibleFontsError) IncompatibleSegmentTypesError,
IncompatibleGlyphsError,
IncompatibleFontsError,
)
__all__ = ['fonts_to_quadratic', 'font_to_quadratic'] __all__ = ["fonts_to_quadratic", "font_to_quadratic"]
# The default approximation error below is a relative value (1/1000 of the EM square). # The default approximation error below is a relative value (1/1000 of the EM square).
# Later on, we convert it to absolute font units by multiplying it by a font's UPEM # Later on, we convert it to absolute font units by multiplying it by a font's UPEM
@ -47,6 +50,8 @@ logger = logging.getLogger(__name__)
_zip = zip _zip = zip
def zip(*args): def zip(*args):
"""Ensure each argument to zip has the same length. Also make sure a list is """Ensure each argument to zip has the same length. Also make sure a list is
returned for python 2/3 compatibility. returned for python 2/3 compatibility.
@ -69,27 +74,27 @@ class GetSegmentsPen(AbstractPen):
self.segments = [] self.segments = []
def _add_segment(self, tag, *args): def _add_segment(self, tag, *args):
if tag in ['move', 'line', 'qcurve', 'curve']: if tag in ["move", "line", "qcurve", "curve"]:
self._last_pt = args[-1] self._last_pt = args[-1]
self.segments.append((tag, args)) self.segments.append((tag, args))
def moveTo(self, pt): def moveTo(self, pt):
self._add_segment('move', pt) self._add_segment("move", pt)
def lineTo(self, pt): def lineTo(self, pt):
self._add_segment('line', pt) self._add_segment("line", pt)
def qCurveTo(self, *points): def qCurveTo(self, *points):
self._add_segment('qcurve', self._last_pt, *points) self._add_segment("qcurve", self._last_pt, *points)
def curveTo(self, *points): def curveTo(self, *points):
self._add_segment('curve', self._last_pt, *points) self._add_segment("curve", self._last_pt, *points)
def closePath(self): def closePath(self):
self._add_segment('close') self._add_segment("close")
def endPath(self): def endPath(self):
self._add_segment('end') self._add_segment("end")
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
pass pass
@ -122,38 +127,41 @@ def _set_segments(glyph, segments, reverse_direction):
if reverse_direction: if reverse_direction:
pen = ReverseContourPen(pen) pen = ReverseContourPen(pen)
for tag, args in segments: for tag, args in segments:
if tag == 'move': if tag == "move":
pen.moveTo(*args) pen.moveTo(*args)
elif tag == 'line': elif tag == "line":
pen.lineTo(*args) pen.lineTo(*args)
elif tag == 'curve': elif tag == "curve":
pen.curveTo(*args[1:]) pen.curveTo(*args[1:])
elif tag == 'qcurve': elif tag == "qcurve":
pen.qCurveTo(*args[1:]) pen.qCurveTo(*args[1:])
elif tag == 'close': elif tag == "close":
pen.closePath() pen.closePath()
elif tag == 'end': elif tag == "end":
pen.endPath() pen.endPath()
else: else:
raise AssertionError('Unhandled segment type "%s"' % tag) raise AssertionError('Unhandled segment type "%s"' % tag)
def _segments_to_quadratic(segments, max_err, stats): def _segments_to_quadratic(segments, max_err, stats, all_quadratic=True):
"""Return quadratic approximations of cubic segments.""" """Return quadratic approximations of cubic segments."""
assert all(s[0] == 'curve' for s in segments), 'Non-cubic given to convert' assert all(s[0] == "curve" for s in segments), "Non-cubic given to convert"
new_points = curves_to_quadratic([s[1] for s in segments], max_err) new_points = curves_to_quadratic([s[1] for s in segments], max_err, all_quadratic)
n = len(new_points[0]) n = len(new_points[0])
assert all(len(s) == n for s in new_points[1:]), 'Converted incompatibly' assert all(len(s) == n for s in new_points[1:]), "Converted incompatibly"
spline_length = str(n - 2) spline_length = str(n - 2)
stats[spline_length] = stats.get(spline_length, 0) + 1 stats[spline_length] = stats.get(spline_length, 0) + 1
return [('qcurve', p) for p in new_points] if all_quadratic or n == 3:
return [("qcurve", p) for p in new_points]
else:
return [("curve", p) for p in new_points]
def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats): def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats, all_quadratic=True):
"""Do the actual conversion of a set of compatible glyphs, after arguments """Do the actual conversion of a set of compatible glyphs, after arguments
have been set up. have been set up.
@ -176,9 +184,13 @@ def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats):
tag = segments[0][0] tag = segments[0][0]
if not all(s[0] == tag for s in segments[1:]): if not all(s[0] == tag for s in segments[1:]):
incompatible[i] = [s[0] for s in segments] incompatible[i] = [s[0] for s in segments]
elif tag == 'curve': elif tag == "curve":
segments = _segments_to_quadratic(segments, max_err, stats) new_segments = _segments_to_quadratic(
segments, max_err, stats, all_quadratic
)
if all_quadratic or new_segments != segments:
glyphs_modified = True glyphs_modified = True
segments = new_segments
new_segments_by_location.append(segments) new_segments_by_location.append(segments)
if glyphs_modified: if glyphs_modified:
@ -192,7 +204,8 @@ def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats):
def glyphs_to_quadratic( def glyphs_to_quadratic(
glyphs, max_err=None, reverse_direction=False, stats=None): glyphs, max_err=None, reverse_direction=False, stats=None, all_quadratic=True
):
"""Convert the curves of a set of compatible of glyphs to quadratic. """Convert the curves of a set of compatible of glyphs to quadratic.
All curves will be converted to quadratic at once, ensuring interpolation All curves will be converted to quadratic at once, ensuring interpolation
@ -216,12 +229,21 @@ def glyphs_to_quadratic(
max_errors = [max_err] * len(glyphs) max_errors = [max_err] * len(glyphs)
assert len(max_errors) == len(glyphs) assert len(max_errors) == len(glyphs)
return _glyphs_to_quadratic(glyphs, max_errors, reverse_direction, stats) return _glyphs_to_quadratic(
glyphs, max_errors, reverse_direction, stats, all_quadratic
)
def fonts_to_quadratic( def fonts_to_quadratic(
fonts, max_err_em=None, max_err=None, reverse_direction=False, fonts,
stats=None, dump_stats=False, remember_curve_type=True): max_err_em=None,
max_err=None,
reverse_direction=False,
stats=None,
dump_stats=False,
remember_curve_type=True,
all_quadratic=True,
):
"""Convert the curves of a collection of fonts to quadratic. """Convert the curves of a collection of fonts to quadratic.
All curves will be converted to quadratic at once, ensuring interpolation All curves will be converted to quadratic at once, ensuring interpolation
@ -243,7 +265,7 @@ def fonts_to_quadratic(
curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts} curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts}
if len(curve_types) == 1: if len(curve_types) == 1:
curve_type = next(iter(curve_types)) curve_type = next(iter(curve_types))
if curve_type == "quadratic": if curve_type in ("quadratic", "mixed"):
logger.info("Curves already converted to quadratic") logger.info("Curves already converted to quadratic")
return False return False
elif curve_type == "cubic": elif curve_type == "cubic":
@ -258,7 +280,7 @@ def fonts_to_quadratic(
stats = {} stats = {}
if max_err_em and max_err: if max_err_em and max_err:
raise TypeError('Only one of max_err and max_err_em can be specified.') raise TypeError("Only one of max_err and max_err_em can be specified.")
if not (max_err_em or max_err): if not (max_err_em or max_err):
max_err_em = DEFAULT_MAX_ERR max_err_em = DEFAULT_MAX_ERR
@ -270,8 +292,7 @@ def fonts_to_quadratic(
if isinstance(max_err_em, (list, tuple)): if isinstance(max_err_em, (list, tuple)):
assert len(fonts) == len(max_err_em) assert len(fonts) == len(max_err_em)
max_errors = [f.info.unitsPerEm * e max_errors = [f.info.unitsPerEm * e for f, e in zip(fonts, max_err_em)]
for f, e in zip(fonts, max_err_em)]
elif max_err_em: elif max_err_em:
max_errors = [f.info.unitsPerEm * max_err_em for f in fonts] max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
@ -286,7 +307,8 @@ def fonts_to_quadratic(
cur_max_errors.append(error) cur_max_errors.append(error)
try: try:
modified |= _glyphs_to_quadratic( modified |= _glyphs_to_quadratic(
glyphs, cur_max_errors, reverse_direction, stats) glyphs, cur_max_errors, reverse_direction, stats, all_quadratic
)
except IncompatibleGlyphsError as exc: except IncompatibleGlyphsError as exc:
logger.error(exc) logger.error(exc)
glyph_errors[name] = exc glyph_errors[name] = exc
@ -296,14 +318,17 @@ def fonts_to_quadratic(
if modified and dump_stats: if modified and dump_stats:
spline_lengths = sorted(stats.keys()) spline_lengths = sorted(stats.keys())
logger.info('New spline lengths: %s' % (', '.join( logger.info(
'%s: %d' % (l, stats[l]) for l in spline_lengths))) "New spline lengths: %s"
% (", ".join("%s: %d" % (l, stats[l]) for l in spline_lengths))
)
if remember_curve_type: if remember_curve_type:
for font in fonts: for font in fonts:
curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic") curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic")
if curve_type != "quadratic": new_curve_type = "quadratic" if all_quadratic else "mixed"
font.lib[CURVE_TYPE_LIB_KEY] = "quadratic" if curve_type != new_curve_type:
font.lib[CURVE_TYPE_LIB_KEY] = new_curve_type
modified = True modified = True
return modified return modified

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,258 @@
MacRoman = [ MacRoman = [
'NUL', 'Eth', 'eth', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Yacute', "NUL",
'yacute', 'HT', 'LF', 'Thorn', 'thorn', 'CR', 'Zcaron', 'zcaron', 'DLE', 'DC1', "Eth",
'DC2', 'DC3', 'DC4', 'onehalf', 'onequarter', 'onesuperior', 'threequarters', "eth",
'threesuperior', 'twosuperior', 'brokenbar', 'minus', 'multiply', 'RS', 'US', "Lslash",
'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', "lslash",
'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', "Scaron",
'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', "scaron",
'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', "Yacute",
'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', "yacute",
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', "HT",
'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', "LF",
'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', "Thorn",
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', "thorn",
'braceright', 'asciitilde', 'DEL', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', "CR",
'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', "Zcaron",
'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', "zcaron",
'edieresis', 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', "DLE",
'oacute', 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', "DC1",
'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', "DC2",
'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', "DC3",
'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', "DC4",
'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', "onehalf",
'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', "onequarter",
'oslash', 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', "onesuperior",
'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', "threequarters",
'nbspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', "threesuperior",
'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', "twosuperior",
'ydieresis', 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', "brokenbar",
'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', "minus",
'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', "multiply",
'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', "RS",
'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', "US",
'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', "space",
'hungarumlaut', 'ogonek', 'caron' "exclam",
"quotedbl",
"numbersign",
"dollar",
"percent",
"ampersand",
"quotesingle",
"parenleft",
"parenright",
"asterisk",
"plus",
"comma",
"hyphen",
"period",
"slash",
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"colon",
"semicolon",
"less",
"equal",
"greater",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"bracketleft",
"backslash",
"bracketright",
"asciicircum",
"underscore",
"grave",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"braceleft",
"bar",
"braceright",
"asciitilde",
"DEL",
"Adieresis",
"Aring",
"Ccedilla",
"Eacute",
"Ntilde",
"Odieresis",
"Udieresis",
"aacute",
"agrave",
"acircumflex",
"adieresis",
"atilde",
"aring",
"ccedilla",
"eacute",
"egrave",
"ecircumflex",
"edieresis",
"iacute",
"igrave",
"icircumflex",
"idieresis",
"ntilde",
"oacute",
"ograve",
"ocircumflex",
"odieresis",
"otilde",
"uacute",
"ugrave",
"ucircumflex",
"udieresis",
"dagger",
"degree",
"cent",
"sterling",
"section",
"bullet",
"paragraph",
"germandbls",
"registered",
"copyright",
"trademark",
"acute",
"dieresis",
"notequal",
"AE",
"Oslash",
"infinity",
"plusminus",
"lessequal",
"greaterequal",
"yen",
"mu",
"partialdiff",
"summation",
"product",
"pi",
"integral",
"ordfeminine",
"ordmasculine",
"Omega",
"ae",
"oslash",
"questiondown",
"exclamdown",
"logicalnot",
"radical",
"florin",
"approxequal",
"Delta",
"guillemotleft",
"guillemotright",
"ellipsis",
"nbspace",
"Agrave",
"Atilde",
"Otilde",
"OE",
"oe",
"endash",
"emdash",
"quotedblleft",
"quotedblright",
"quoteleft",
"quoteright",
"divide",
"lozenge",
"ydieresis",
"Ydieresis",
"fraction",
"currency",
"guilsinglleft",
"guilsinglright",
"fi",
"fl",
"daggerdbl",
"periodcentered",
"quotesinglbase",
"quotedblbase",
"perthousand",
"Acircumflex",
"Ecircumflex",
"Aacute",
"Edieresis",
"Egrave",
"Iacute",
"Icircumflex",
"Idieresis",
"Igrave",
"Oacute",
"Ocircumflex",
"apple",
"Ograve",
"Uacute",
"Ucircumflex",
"Ugrave",
"dotlessi",
"circumflex",
"tilde",
"macron",
"breve",
"dotaccent",
"ring",
"cedilla",
"hungarumlaut",
"ogonek",
"caron",
] ]

View File

@ -1,48 +1,258 @@
StandardEncoding = [ StandardEncoding = [
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', 'space', 'exclam', 'quotedbl', ".notdef",
'numbersign', 'dollar', 'percent', 'ampersand', ".notdef",
'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', ".notdef",
'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', ".notdef",
'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', ".notdef",
'colon', 'semicolon', 'less', 'equal', 'greater', ".notdef",
'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', ".notdef",
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', ".notdef",
'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', ".notdef",
'bracketright', 'asciicircum', 'underscore', 'quoteleft', ".notdef",
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', ".notdef",
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', ".notdef",
'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', ".notdef",
'.notdef', '.notdef', '.notdef', '.notdef', 'exclamdown', ".notdef",
'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', ".notdef",
'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', ".notdef",
'guilsinglleft', 'guilsinglright', 'fi', 'fl', '.notdef', ".notdef",
'endash', 'dagger', 'daggerdbl', 'periodcentered', ".notdef",
'.notdef', 'paragraph', 'bullet', 'quotesinglbase', ".notdef",
'quotedblbase', 'quotedblright', 'guillemotright', ".notdef",
'ellipsis', 'perthousand', '.notdef', 'questiondown', "space",
'.notdef', 'grave', 'acute', 'circumflex', 'tilde', "exclam",
'macron', 'breve', 'dotaccent', 'dieresis', '.notdef', "quotedbl",
'ring', 'cedilla', '.notdef', 'hungarumlaut', 'ogonek', "numbersign",
'caron', 'emdash', '.notdef', '.notdef', '.notdef', "dollar",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', "percent",
'.notdef', '.notdef', '.notdef', '.notdef', '.notdef', "ampersand",
'.notdef', '.notdef', '.notdef', 'AE', '.notdef', "quoteright",
'ordfeminine', '.notdef', '.notdef', '.notdef', '.notdef', "parenleft",
'Lslash', 'Oslash', 'OE', 'ordmasculine', '.notdef', "parenright",
'.notdef', '.notdef', '.notdef', '.notdef', 'ae', '.notdef', "asterisk",
'.notdef', '.notdef', 'dotlessi', '.notdef', '.notdef', "plus",
'lslash', 'oslash', 'oe', 'germandbls', '.notdef', "comma",
'.notdef', '.notdef', '.notdef' "hyphen",
"period",
"slash",
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"colon",
"semicolon",
"less",
"equal",
"greater",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"bracketleft",
"backslash",
"bracketright",
"asciicircum",
"underscore",
"quoteleft",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"braceleft",
"bar",
"braceright",
"asciitilde",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
"exclamdown",
"cent",
"sterling",
"fraction",
"yen",
"florin",
"section",
"currency",
"quotesingle",
"quotedblleft",
"guillemotleft",
"guilsinglleft",
"guilsinglright",
"fi",
"fl",
".notdef",
"endash",
"dagger",
"daggerdbl",
"periodcentered",
".notdef",
"paragraph",
"bullet",
"quotesinglbase",
"quotedblbase",
"quotedblright",
"guillemotright",
"ellipsis",
"perthousand",
".notdef",
"questiondown",
".notdef",
"grave",
"acute",
"circumflex",
"tilde",
"macron",
"breve",
"dotaccent",
"dieresis",
".notdef",
"ring",
"cedilla",
".notdef",
"hungarumlaut",
"ogonek",
"caron",
"emdash",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
"AE",
".notdef",
"ordfeminine",
".notdef",
".notdef",
".notdef",
".notdef",
"Lslash",
"Oslash",
"OE",
"ordmasculine",
".notdef",
".notdef",
".notdef",
".notdef",
".notdef",
"ae",
".notdef",
".notdef",
".notdef",
"dotlessi",
".notdef",
".notdef",
"lslash",
"oslash",
"oe",
"germandbls",
".notdef",
".notdef",
".notdef",
".notdef",
] ]

View File

@ -4,15 +4,17 @@ but missing from Python. See https://github.com/fonttools/fonttools/issues/236
import codecs import codecs
import encodings import encodings
class ExtendCodec(codecs.Codec):
class ExtendCodec(codecs.Codec):
def __init__(self, name, base_encoding, mapping): def __init__(self, name, base_encoding, mapping):
self.name = name self.name = name
self.base_encoding = base_encoding self.base_encoding = base_encoding
self.mapping = mapping self.mapping = mapping
self.reverse = {v: k for k, v in mapping.items()} self.reverse = {v: k for k, v in mapping.items()}
self.max_len = max(len(v) for v in mapping.values()) self.max_len = max(len(v) for v in mapping.values())
self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode) self.info = codecs.CodecInfo(
name=self.name, encode=self.encode, decode=self.decode
)
codecs.register_error(name, self.error) codecs.register_error(name, self.error)
def _map(self, mapper, output_type, exc_type, input, errors): def _map(self, mapper, output_type, exc_type, input, errors):
@ -33,10 +35,10 @@ class ExtendCodec(codecs.Codec):
input = input[pos:] input = input[pos:]
return out, length return out, length
def encode(self, input, errors='strict'): def encode(self, input, errors="strict"):
return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors) return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
def decode(self, input, errors='strict'): def decode(self, input, errors="strict"):
return self._map(codecs.decode, str, UnicodeDecodeError, input, errors) return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
def error(self, e): def error(self, e):
@ -55,7 +57,9 @@ class ExtendCodec(codecs.Codec):
_extended_encodings = { _extended_encodings = {
"x_mac_japanese_ttx": ("shift_jis", { "x_mac_japanese_ttx": (
"shift_jis",
{
b"\xFC": chr(0x007C), b"\xFC": chr(0x007C),
b"\x7E": chr(0x007E), b"\x7E": chr(0x007E),
b"\x80": chr(0x005C), b"\x80": chr(0x005C),
@ -63,39 +67,50 @@ _extended_encodings = {
b"\xFD": chr(0x00A9), b"\xFD": chr(0x00A9),
b"\xFE": chr(0x2122), b"\xFE": chr(0x2122),
b"\xFF": chr(0x2026), b"\xFF": chr(0x2026),
}), },
"x_mac_trad_chinese_ttx": ("big5", { ),
"x_mac_trad_chinese_ttx": (
"big5",
{
b"\x80": chr(0x005C), b"\x80": chr(0x005C),
b"\xA0": chr(0x00A0), b"\xA0": chr(0x00A0),
b"\xFD": chr(0x00A9), b"\xFD": chr(0x00A9),
b"\xFE": chr(0x2122), b"\xFE": chr(0x2122),
b"\xFF": chr(0x2026), b"\xFF": chr(0x2026),
}), },
"x_mac_korean_ttx": ("euc_kr", { ),
"x_mac_korean_ttx": (
"euc_kr",
{
b"\x80": chr(0x00A0), b"\x80": chr(0x00A0),
b"\x81": chr(0x20A9), b"\x81": chr(0x20A9),
b"\x82": chr(0x2014), b"\x82": chr(0x2014),
b"\x83": chr(0x00A9), b"\x83": chr(0x00A9),
b"\xFE": chr(0x2122), b"\xFE": chr(0x2122),
b"\xFF": chr(0x2026), b"\xFF": chr(0x2026),
}), },
"x_mac_simp_chinese_ttx": ("gb2312", { ),
"x_mac_simp_chinese_ttx": (
"gb2312",
{
b"\x80": chr(0x00FC), b"\x80": chr(0x00FC),
b"\xA0": chr(0x00A0), b"\xA0": chr(0x00A0),
b"\xFD": chr(0x00A9), b"\xFD": chr(0x00A9),
b"\xFE": chr(0x2122), b"\xFE": chr(0x2122),
b"\xFF": chr(0x2026), b"\xFF": chr(0x2026),
}), },
),
} }
_cache = {} _cache = {}
def search_function(name): def search_function(name):
name = encodings.normalize_encoding(name) # Rather undocumented... name = encodings.normalize_encoding(name) # Rather undocumented...
if name in _extended_encodings: if name in _extended_encodings:
if name not in _cache: if name not in _cache:
base_encoding, mapping = _extended_encodings[name] base_encoding, mapping = _extended_encodings[name]
assert(name[-4:] == "_ttx") assert name[-4:] == "_ttx"
# Python 2 didn't have any of the encodings that we are implementing # Python 2 didn't have any of the encodings that we are implementing
# in this file. Python 3 added aliases for the East Asian ones, mapping # in this file. Python 3 added aliases for the East Asian ones, mapping
# them "temporarily" to the same base encoding as us, with a comment # them "temporarily" to the same base encoding as us, with a comment
@ -116,4 +131,5 @@ def search_function(name):
return None return None
codecs.register(search_function) codecs.register(search_function)

View File

@ -912,14 +912,11 @@ class IgnoreSubstStatement(Statement):
contexts = [] contexts = []
for prefix, glyphs, suffix in self.chainContexts: for prefix, glyphs, suffix in self.chainContexts:
res = "" res = ""
if len(prefix) or len(suffix):
if len(prefix): if len(prefix):
res += " ".join(map(asFea, prefix)) + " " res += " ".join(map(asFea, prefix)) + " "
res += " ".join(g.asFea() + "'" for g in glyphs) res += " ".join(g.asFea() + "'" for g in glyphs)
if len(suffix): if len(suffix):
res += " " + " ".join(map(asFea, suffix)) res += " " + " ".join(map(asFea, suffix))
else:
res += " ".join(map(asFea, glyphs))
contexts.append(res) contexts.append(res)
return "ignore sub " + ", ".join(contexts) + ";" return "ignore sub " + ", ".join(contexts) + ";"

View File

@ -446,6 +446,7 @@ class Builder(object):
assert self.cv_parameters_ids_[tag] is not None assert self.cv_parameters_ids_[tag] is not None
nameID = self.cv_parameters_ids_[tag] nameID = self.cv_parameters_ids_[tag]
table.setName(string, nameID, platformID, platEncID, langID) table.setName(string, nameID, platformID, platEncID, langID)
table.names.sort()
def build_OS_2(self): def build_OS_2(self):
if not self.os2_: if not self.os2_:
@ -768,8 +769,8 @@ class Builder(object):
varidx_map = store.optimize() varidx_map = store.optimize()
gdef.remap_device_varidxes(varidx_map) gdef.remap_device_varidxes(varidx_map)
if 'GPOS' in self.font: if "GPOS" in self.font:
self.font['GPOS'].table.remap_device_varidxes(varidx_map) self.font["GPOS"].table.remap_device_varidxes(varidx_map)
VariableScalar.clear_cache() VariableScalar.clear_cache()
if any( if any(
( (
@ -1339,7 +1340,9 @@ class Builder(object):
# GSUB 5/6 # GSUB 5/6
def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
if not all(glyphs) or not all(prefix) or not all(suffix): if not all(glyphs) or not all(prefix) or not all(suffix):
raise FeatureLibError("Empty glyph class in contextual substitution", location) raise FeatureLibError(
"Empty glyph class in contextual substitution", location
)
lookup = self.get_lookup_(location, ChainContextSubstBuilder) lookup = self.get_lookup_(location, ChainContextSubstBuilder)
lookup.rules.append( lookup.rules.append(
ChainContextualRule( ChainContextualRule(
@ -1349,10 +1352,13 @@ class Builder(object):
def add_single_subst_chained_(self, location, prefix, suffix, mapping): def add_single_subst_chained_(self, location, prefix, suffix, mapping):
if not mapping or not all(prefix) or not all(suffix): if not mapping or not all(prefix) or not all(suffix):
raise FeatureLibError("Empty glyph class in contextual substitution", location) raise FeatureLibError(
"Empty glyph class in contextual substitution", location
)
# https://github.com/fonttools/fonttools/issues/512 # https://github.com/fonttools/fonttools/issues/512
# https://github.com/fonttools/fonttools/issues/2150
chain = self.get_lookup_(location, ChainContextSubstBuilder) chain = self.get_lookup_(location, ChainContextSubstBuilder)
sub = chain.find_chainable_single_subst(set(mapping.keys())) sub = chain.find_chainable_single_subst(mapping)
if sub is None: if sub is None:
sub = self.get_chained_lookup_(location, SingleSubstBuilder) sub = self.get_chained_lookup_(location, SingleSubstBuilder)
sub.mapping.update(mapping) sub.mapping.update(mapping)
@ -1377,8 +1383,12 @@ class Builder(object):
lookup = self.get_lookup_(location, SinglePosBuilder) lookup = self.get_lookup_(location, SinglePosBuilder)
for glyphs, value in pos: for glyphs, value in pos:
if not glyphs: if not glyphs:
raise FeatureLibError("Empty glyph class in positioning rule", location) raise FeatureLibError(
otValueRecord = self.makeOpenTypeValueRecord(location, value, pairPosContext=False) "Empty glyph class in positioning rule", location
)
otValueRecord = self.makeOpenTypeValueRecord(
location, value, pairPosContext=False
)
for glyph in glyphs: for glyph in glyphs:
try: try:
lookup.add_pos(location, glyph, otValueRecord) lookup.add_pos(location, glyph, otValueRecord)
@ -1388,9 +1398,7 @@ class Builder(object):
# GPOS 2 # GPOS 2
def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2): def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
if not glyphclass1 or not glyphclass2: if not glyphclass1 or not glyphclass2:
raise FeatureLibError( raise FeatureLibError("Empty glyph class in positioning rule", location)
"Empty glyph class in positioning rule", location
)
lookup = self.get_lookup_(location, PairPosBuilder) lookup = self.get_lookup_(location, PairPosBuilder)
v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True) v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True) v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
@ -1458,7 +1466,9 @@ class Builder(object):
# GPOS 7/8 # GPOS 7/8
def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
if not all(glyphs) or not all(prefix) or not all(suffix): if not all(glyphs) or not all(prefix) or not all(suffix):
raise FeatureLibError("Empty glyph class in contextual positioning rule", location) raise FeatureLibError(
"Empty glyph class in contextual positioning rule", location
)
lookup = self.get_lookup_(location, ChainContextPosBuilder) lookup = self.get_lookup_(location, ChainContextPosBuilder)
lookup.rules.append( lookup.rules.append(
ChainContextualRule( ChainContextualRule(
@ -1468,7 +1478,9 @@ class Builder(object):
def add_single_pos_chained_(self, location, prefix, suffix, pos): def add_single_pos_chained_(self, location, prefix, suffix, pos):
if not pos or not all(prefix) or not all(suffix): if not pos or not all(prefix) or not all(suffix):
raise FeatureLibError("Empty glyph class in contextual positioning rule", location) raise FeatureLibError(
"Empty glyph class in contextual positioning rule", location
)
# https://github.com/fonttools/fonttools/issues/514 # https://github.com/fonttools/fonttools/issues/514
chain = self.get_lookup_(location, ChainContextPosBuilder) chain = self.get_lookup_(location, ChainContextPosBuilder)
targets = [] targets = []
@ -1479,7 +1491,9 @@ class Builder(object):
if value is None: if value is None:
subs.append(None) subs.append(None)
continue continue
otValue = self.makeOpenTypeValueRecord(location, value, pairPosContext=False) otValue = self.makeOpenTypeValueRecord(
location, value, pairPosContext=False
)
sub = chain.find_chainable_single_pos(targets, glyphs, otValue) sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
if sub is None: if sub is None:
sub = self.get_chained_lookup_(location, SinglePosBuilder) sub = self.get_chained_lookup_(location, SinglePosBuilder)
@ -1498,7 +1512,9 @@ class Builder(object):
for markClassDef in markClass.definitions: for markClassDef in markClass.definitions:
for mark in markClassDef.glyphs.glyphSet(): for mark in markClassDef.glyphs.glyphSet():
if mark not in lookupBuilder.marks: if mark not in lookupBuilder.marks:
otMarkAnchor = self.makeOpenTypeAnchor(location, markClassDef.anchor) otMarkAnchor = self.makeOpenTypeAnchor(
location, markClassDef.anchor
)
lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor) lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
else: else:
existingMarkClass = lookupBuilder.marks[mark][0] existingMarkClass = lookupBuilder.marks[mark][0]
@ -1592,9 +1608,13 @@ class Builder(object):
if not isinstance(getattr(anchor, dim), VariableScalar): if not isinstance(getattr(anchor, dim), VariableScalar):
continue continue
if getattr(anchor, dim + "DeviceTable") is not None: if getattr(anchor, dim + "DeviceTable") is not None:
raise FeatureLibError("Can't define a device coordinate and variable scalar", location) raise FeatureLibError(
"Can't define a device coordinate and variable scalar", location
)
if not self.varstorebuilder: if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location) raise FeatureLibError(
"Can't define a variable scalar in a non-variable font", location
)
varscalar = getattr(anchor, dim) varscalar = getattr(anchor, dim)
varscalar.axes = self.axes varscalar.axes = self.axes
default, index = varscalar.add_to_variation_store(self.varstorebuilder) default, index = varscalar.add_to_variation_store(self.varstorebuilder)
@ -1606,7 +1626,9 @@ class Builder(object):
deviceY = buildVarDevTable(index) deviceY = buildVarDevTable(index)
variable = True variable = True
otlanchor = otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY) otlanchor = otl.buildAnchor(
anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY
)
if variable: if variable:
otlanchor.Format = 3 otlanchor.Format = 3
return otlanchor return otlanchor
@ -1617,7 +1639,6 @@ class Builder(object):
if not name.startswith("Reserved") if not name.startswith("Reserved")
} }
def makeOpenTypeValueRecord(self, location, v, pairPosContext): def makeOpenTypeValueRecord(self, location, v, pairPosContext):
"""ast.ValueRecord --> otBase.ValueRecord""" """ast.ValueRecord --> otBase.ValueRecord"""
if not v: if not v:
@ -1635,9 +1656,14 @@ class Builder(object):
otDeviceName = otName[0:4] + "Device" otDeviceName = otName[0:4] + "Device"
feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:] feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
if getattr(v, feaDeviceName): if getattr(v, feaDeviceName):
raise FeatureLibError("Can't define a device coordinate and variable scalar", location) raise FeatureLibError(
"Can't define a device coordinate and variable scalar", location
)
if not self.varstorebuilder: if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location) raise FeatureLibError(
"Can't define a variable scalar in a non-variable font",
location,
)
val.axes = self.axes val.axes = self.axes
default, index = val.add_to_variation_store(self.varstorebuilder) default, index = val.add_to_variation_store(self.varstorebuilder)
vr[otName] = default vr[otName] = default

View File

@ -3,6 +3,7 @@ from typing import NamedTuple
LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib" LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib"
LOOKUP_DEBUG_ENV_VAR = "FONTTOOLS_LOOKUP_DEBUGGING" LOOKUP_DEBUG_ENV_VAR = "FONTTOOLS_LOOKUP_DEBUGGING"
class LookupDebugInfo(NamedTuple): class LookupDebugInfo(NamedTuple):
"""Information about where a lookup came from, to be embedded in a font""" """Information about where a lookup came from, to be embedded in a font"""

View File

@ -134,7 +134,8 @@ class Parser(object):
] ]
raise FeatureLibError( raise FeatureLibError(
"The following glyph names are referenced but are missing from the " "The following glyph names are referenced but are missing from the "
"glyph set:\n" + ("\n".join(error)), None "glyph set:\n" + ("\n".join(error)),
None,
) )
return self.doc_ return self.doc_
@ -396,7 +397,8 @@ class Parser(object):
self.expect_symbol_("-") self.expect_symbol_("-")
range_end = self.expect_cid_() range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set( self.check_glyph_name_in_glyph_set(
f"cid{range_start:05d}", f"cid{range_end:05d}", f"cid{range_start:05d}",
f"cid{range_end:05d}",
) )
glyphs.add_cid_range( glyphs.add_cid_range(
range_start, range_start,
@ -522,27 +524,33 @@ class Parser(object):
) )
return (prefix, glyphs, lookups, values, suffix, hasMarks) return (prefix, glyphs, lookups, values, suffix, hasMarks)
def parse_chain_context_(self): def parse_ignore_glyph_pattern_(self, sub):
location = self.cur_token_location_ location = self.cur_token_location_
prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_( prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
vertical=False vertical=False
) )
chainContext = [(prefix, glyphs, suffix)] if any(lookups):
hasLookups = any(lookups) raise FeatureLibError(
f'No lookups can be specified for "ignore {sub}"', location
)
if not hasMarks:
error = FeatureLibError(
f'Ambiguous "ignore {sub}", there should be least one marked glyph',
location,
)
log.warning(str(error))
suffix, glyphs = glyphs[1:], glyphs[0:1]
chainContext = (prefix, glyphs, suffix)
return chainContext
def parse_ignore_context_(self, sub):
location = self.cur_token_location_
chainContext = [self.parse_ignore_glyph_pattern_(sub)]
while self.next_token_ == ",": while self.next_token_ == ",":
self.expect_symbol_(",") self.expect_symbol_(",")
( chainContext.append(self.parse_ignore_glyph_pattern_(sub))
prefix,
glyphs,
lookups,
values,
suffix,
hasMarks,
) = self.parse_glyph_pattern_(vertical=False)
chainContext.append((prefix, glyphs, suffix))
hasLookups = hasLookups or any(lookups)
self.expect_symbol_(";") self.expect_symbol_(";")
return chainContext, hasLookups return chainContext
def parse_ignore_(self): def parse_ignore_(self):
# Parses an ignore sub/pos rule. # Parses an ignore sub/pos rule.
@ -550,18 +558,10 @@ class Parser(object):
location = self.cur_token_location_ location = self.cur_token_location_
self.advance_lexer_() self.advance_lexer_()
if self.cur_token_ in ["substitute", "sub"]: if self.cur_token_ in ["substitute", "sub"]:
chainContext, hasLookups = self.parse_chain_context_() chainContext = self.parse_ignore_context_("sub")
if hasLookups:
raise FeatureLibError(
'No lookups can be specified for "ignore sub"', location
)
return self.ast.IgnoreSubstStatement(chainContext, location=location) return self.ast.IgnoreSubstStatement(chainContext, location=location)
if self.cur_token_ in ["position", "pos"]: if self.cur_token_ in ["position", "pos"]:
chainContext, hasLookups = self.parse_chain_context_() chainContext = self.parse_ignore_context_("pos")
if hasLookups:
raise FeatureLibError(
'No lookups can be specified for "ignore pos"', location
)
return self.ast.IgnorePosStatement(chainContext, location=location) return self.ast.IgnorePosStatement(chainContext, location=location)
raise FeatureLibError( raise FeatureLibError(
'Expected "substitute" or "position"', self.cur_token_location_ 'Expected "substitute" or "position"', self.cur_token_location_
@ -696,7 +696,9 @@ class Parser(object):
location = self.cur_token_location_ location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True) glyphs = self.parse_glyphclass_(accept_glyphname=True)
if not glyphs.glyphSet(): if not glyphs.glyphSet():
raise FeatureLibError("Empty glyph class in mark class definition", location) raise FeatureLibError(
"Empty glyph class in mark class definition", location
)
anchor = self.parse_anchor_() anchor = self.parse_anchor_()
name = self.expect_class_name_() name = self.expect_class_name_()
self.expect_symbol_(";") self.expect_symbol_(";")

View File

@ -800,7 +800,7 @@ class FontBuilder(object):
) )
self._initTableWithValues("DSIG", {}, values) self._initTableWithValues("DSIG", {}, values)
def addOpenTypeFeatures(self, features, filename=None, tables=None): def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False):
"""Add OpenType features to the font from a string containing """Add OpenType features to the font from a string containing
Feature File syntax. Feature File syntax.
@ -810,11 +810,14 @@ class FontBuilder(object):
The optional `tables` argument can be a list of OTL tables tags to The optional `tables` argument can be a list of OTL tables tags to
build, allowing the caller to only build selected OTL tables. See build, allowing the caller to only build selected OTL tables. See
`fontTools.feaLib` for details. `fontTools.feaLib` for details.
The optional `debug` argument controls whether to add source debugging
information to the font in the `Debg` table.
""" """
from .feaLib.builder import addOpenTypeFeaturesFromString from .feaLib.builder import addOpenTypeFeaturesFromString
addOpenTypeFeaturesFromString( addOpenTypeFeaturesFromString(
self.font, features, filename=filename, tables=tables self.font, features, filename=filename, tables=tables, debug=debug
) )
def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"): def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):

View File

@ -4,7 +4,11 @@
from fontTools import ttLib from fontTools import ttLib
import fontTools.merge.base import fontTools.merge.base
from fontTools.merge.cmap import computeMegaGlyphOrder, computeMegaCmap, renameCFFCharStrings from fontTools.merge.cmap import (
computeMegaGlyphOrder,
computeMegaCmap,
renameCFFCharStrings,
)
from fontTools.merge.layout import layoutPreMerge, layoutPostMerge from fontTools.merge.layout import layoutPreMerge, layoutPostMerge
from fontTools.merge.options import Options from fontTools.merge.options import Options
import fontTools.merge.tables import fontTools.merge.tables
@ -57,7 +61,7 @@ class Merger(object):
fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
for font, fontfile in zip(fonts, fontfiles): for font, fontfile in zip(fonts, fontfiles):
font._merger__fontfile = fontfile font._merger__fontfile = fontfile
font._merger__name = font['name'].getDebugName(4) font._merger__name = font["name"].getDebugName(4)
return fonts return fonts
def merge(self, fontfiles): def merge(self, fontfiles):
@ -84,10 +88,10 @@ class Merger(object):
fonts = self._openFonts(fontfiles) fonts = self._openFonts(fontfiles)
for font, glyphOrder in zip(fonts, glyphOrders): for font, glyphOrder in zip(fonts, glyphOrders):
font.setGlyphOrder(glyphOrder) font.setGlyphOrder(glyphOrder)
if 'CFF ' in font: if "CFF " in font:
renameCFFCharStrings(self, glyphOrder, font['CFF ']) renameCFFCharStrings(self, glyphOrder, font["CFF "])
cmaps = [font['cmap'] for font in fonts] cmaps = [font["cmap"] for font in fonts]
self.duplicateGlyphsPerFont = [{} for _ in fonts] self.duplicateGlyphsPerFont = [{} for _ in fonts]
computeMegaCmap(self, cmaps) computeMegaCmap(self, cmaps)
@ -100,9 +104,9 @@ class Merger(object):
self.fonts = fonts self.fonts = fonts
allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
allTags.remove('GlyphOrder') allTags.remove("GlyphOrder")
for tag in allTags: for tag in sorted(allTags):
if tag in self.options.drop_tables: if tag in self.options.drop_tables:
continue continue
@ -131,16 +135,21 @@ class Merger(object):
# Right now we don't use self at all. Will use in the future # Right now we don't use self at all. Will use in the future
# for options and logging. # for options and logging.
allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented)) allKeys = set.union(
set(),
*(vars(table).keys() for table in tables if table is not NotImplemented),
)
for key in allKeys: for key in allKeys:
try: try:
mergeLogic = logic[key] mergeLogic = logic[key]
except KeyError: except KeyError:
try: try:
mergeLogic = logic['*'] mergeLogic = logic["*"]
except KeyError: except KeyError:
raise Exception("Don't know how to merge key %s of class %s" % raise Exception(
(key, returnTable.__class__.__name__)) "Don't know how to merge key %s of class %s"
% (key, returnTable.__class__.__name__)
)
if mergeLogic is NotImplemented: if mergeLogic is NotImplemented:
continue continue
value = mergeLogic(getattr(table, key, NotImplemented) for table in tables) value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
@ -161,11 +170,8 @@ class Merger(object):
font["OS/2"].recalcAvgCharWidth(font) font["OS/2"].recalcAvgCharWidth(font)
__all__ = [ __all__ = ["Options", "Merger", "main"]
'Options',
'Merger',
'main'
]
@timer("make one with everything (TOTAL TIME)") @timer("make one with everything (TOTAL TIME)")
def main(args=None): def main(args=None):
@ -176,11 +182,11 @@ def main(args=None):
args = sys.argv[1:] args = sys.argv[1:]
options = Options() options = Options()
args = options.parse_opts(args, ignore_unknown=['output-file']) args = options.parse_opts(args, ignore_unknown=["output-file"])
outfile = 'merged.ttf' outfile = "merged.ttf"
fontfiles = [] fontfiles = []
for g in args: for g in args:
if g.startswith('--output-file='): if g.startswith("--output-file="):
outfile = g[14:] outfile = g[14:]
continue continue
fontfiles.append(g) fontfiles.append(g)

View File

@ -2,5 +2,5 @@ import sys
from fontTools.merge import main from fontTools.merge import main
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -12,19 +12,24 @@ log = logging.getLogger("fontTools.merge")
def add_method(*clazzes, **kwargs): def add_method(*clazzes, **kwargs):
"""Returns a decorator function that adds a new method to one or """Returns a decorator function that adds a new method to one or
more classes.""" more classes."""
allowDefault = kwargs.get('allowDefaultTable', False) allowDefault = kwargs.get("allowDefaultTable", False)
def wrapper(method): def wrapper(method):
done = [] done = []
for clazz in clazzes: for clazz in clazzes:
if clazz in done: continue # Support multiple names of a clazz if clazz in done:
continue # Support multiple names of a clazz
done.append(clazz) done.append(clazz)
assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.' assert allowDefault or clazz != DefaultTable, "Oops, table class not found."
assert method.__name__ not in clazz.__dict__, \ assert (
"Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__) method.__name__ not in clazz.__dict__
), "Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__)
setattr(clazz, method.__name__, method) setattr(clazz, method.__name__, method)
return None return None
return wrapper return wrapper
def mergeObjects(lst): def mergeObjects(lst):
lst = [item for item in lst if item is not NotImplemented] lst = [item for item in lst if item is not NotImplemented]
if not lst: if not lst:
@ -46,10 +51,11 @@ def mergeObjects(lst):
mergeLogic = logic[key] mergeLogic = logic[key]
except KeyError: except KeyError:
try: try:
mergeLogic = logic['*'] mergeLogic = logic["*"]
except KeyError: except KeyError:
raise Exception("Don't know how to merge key %s of class %s" % raise Exception(
(key, clazz.__name__)) "Don't know how to merge key %s of class %s" % (key, clazz.__name__)
)
if mergeLogic is NotImplemented: if mergeLogic is NotImplemented:
continue continue
value = mergeLogic(getattr(table, key, NotImplemented) for table in lst) value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
@ -60,9 +66,10 @@ def mergeObjects(lst):
return returnTable return returnTable
@add_method(DefaultTable, allowDefaultTable=True) @add_method(DefaultTable, allowDefaultTable=True)
def merge(self, m, tables): def merge(self, m, tables):
if not hasattr(self, 'mergeMap'): if not hasattr(self, "mergeMap"):
log.info("Don't know how to merge '%s'.", self.tableTag) log.info("Don't know how to merge '%s'.", self.tableTag)
return NotImplemented return NotImplemented
@ -72,5 +79,3 @@ def merge(self, m, tables):
return m.mergeObjects(self, self.mergeMap, tables) return m.mergeObjects(self, self.mergeMap, tables)
else: else:
return logic(tables) return logic(tables)

View File

@ -27,9 +27,14 @@ def computeMegaGlyphOrder(merger, glyphOrders):
merger.glyphOrder = megaOrder = list(megaOrder.keys()) merger.glyphOrder = megaOrder = list(megaOrder.keys())
def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2, def _glyphsAreSame(
advanceTolerance=.05, glyphSet1,
advanceToleranceEmpty=.20): glyphSet2,
glyph1,
glyph2,
advanceTolerance=0.05,
advanceToleranceEmpty=0.20,
):
pen1 = DecomposingRecordingPen(glyphSet1) pen1 = DecomposingRecordingPen(glyphSet1)
pen2 = DecomposingRecordingPen(glyphSet2) pen2 = DecomposingRecordingPen(glyphSet2)
g1 = glyphSet1[glyph1] g1 = glyphSet1[glyph1]
@ -43,11 +48,12 @@ def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2,
# TODO Warn if advances not the same but within tolerance. # TODO Warn if advances not the same but within tolerance.
if abs(g1.width - g2.width) > g1.width * tolerance: if abs(g1.width - g2.width) > g1.width * tolerance:
return False return False
if hasattr(g1, 'height') and g1.height is not None: if hasattr(g1, "height") and g1.height is not None:
if abs(g1.height - g2.height) > g1.height * tolerance: if abs(g1.height - g2.height) > g1.height * tolerance:
return False return False
return True return True
# Valid (format, platformID, platEncID) triplets for cmap subtables containing # Valid (format, platformID, platEncID) triplets for cmap subtables containing
# Unicode BMP-only and Unicode Full Repertoire semantics. # Unicode BMP-only and Unicode Full Repertoire semantics.
# Cf. OpenType spec for "Platform specific encodings": # Cf. OpenType spec for "Platform specific encodings":
@ -56,6 +62,7 @@ class _CmapUnicodePlatEncodings:
BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)} BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)} FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
def computeMegaCmap(merger, cmapTables): def computeMegaCmap(merger, cmapTables):
"""Sets merger.cmap and merger.glyphOrder.""" """Sets merger.cmap and merger.glyphOrder."""
@ -76,7 +83,10 @@ def computeMegaCmap(merger, cmapTables):
log.warning( log.warning(
"Dropped cmap subtable from font '%s':\t" "Dropped cmap subtable from font '%s':\t"
"format %2s, platformID %2s, platEncID %2s", "format %2s, platformID %2s, platEncID %2s",
fontIdx, subtable.format, subtable.platformID, subtable.platEncID fontIdx,
subtable.format,
subtable.platformID,
subtable.platEncID,
) )
if format12 is not None: if format12 is not None:
chosenCmapTables.append((format12, fontIdx)) chosenCmapTables.append((format12, fontIdx))
@ -86,7 +96,7 @@ def computeMegaCmap(merger, cmapTables):
# Build the unicode mapping # Build the unicode mapping
merger.cmap = cmap = {} merger.cmap = cmap = {}
fontIndexForGlyph = {} fontIndexForGlyph = {}
glyphSets = [None for f in merger.fonts] if hasattr(merger, 'fonts') else None glyphSets = [None for f in merger.fonts] if hasattr(merger, "fonts") else None
for table, fontIdx in chosenCmapTables: for table, fontIdx in chosenCmapTables:
# handle duplicates # handle duplicates
@ -113,7 +123,9 @@ def computeMegaCmap(merger, cmapTables):
# Char previously mapped to oldgid but oldgid is already remapped to a different # Char previously mapped to oldgid but oldgid is already remapped to a different
# gid, because of another Unicode character. # gid, because of another Unicode character.
# TODO: Try harder to do something about these. # TODO: Try harder to do something about these.
log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid) log.warning(
"Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid
)
def renameCFFCharStrings(merger, glyphOrder, cffTable): def renameCFFCharStrings(merger, glyphOrder, cffTable):

View File

@ -17,14 +17,18 @@ def mergeLookupLists(lst):
# TODO Do smarter merge. # TODO Do smarter merge.
return sumLists(lst) return sumLists(lst)
def mergeFeatures(lst): def mergeFeatures(lst):
assert lst assert lst
self = otTables.Feature() self = otTables.Feature()
self.FeatureParams = None self.FeatureParams = None
self.LookupListIndex = mergeLookupLists([l.LookupListIndex for l in lst if l.LookupListIndex]) self.LookupListIndex = mergeLookupLists(
[l.LookupListIndex for l in lst if l.LookupListIndex]
)
self.LookupCount = len(self.LookupListIndex) self.LookupCount = len(self.LookupListIndex)
return self return self
def mergeFeatureLists(lst): def mergeFeatureLists(lst):
d = {} d = {}
for l in lst: for l in lst:
@ -41,6 +45,7 @@ def mergeFeatureLists(lst):
ret.append(rec) ret.append(rec)
return ret return ret
def mergeLangSyses(lst): def mergeLangSyses(lst):
assert lst assert lst
@ -50,10 +55,13 @@ def mergeLangSyses(lst):
self = otTables.LangSys() self = otTables.LangSys()
self.LookupOrder = None self.LookupOrder = None
self.ReqFeatureIndex = 0xFFFF self.ReqFeatureIndex = 0xFFFF
self.FeatureIndex = mergeFeatureLists([l.FeatureIndex for l in lst if l.FeatureIndex]) self.FeatureIndex = mergeFeatureLists(
[l.FeatureIndex for l in lst if l.FeatureIndex]
)
self.FeatureCount = len(self.FeatureIndex) self.FeatureCount = len(self.FeatureIndex)
return self return self
def mergeScripts(lst): def mergeScripts(lst):
assert lst assert lst
@ -82,6 +90,7 @@ def mergeScripts(lst):
self.DefaultLangSys = None self.DefaultLangSys = None
return self return self
def mergeScriptRecords(lst): def mergeScriptRecords(lst):
d = {} d = {}
for l in lst: for l in lst:
@ -98,111 +107,124 @@ def mergeScriptRecords(lst):
ret.append(rec) ret.append(rec)
return ret return ret
otTables.ScriptList.mergeMap = { otTables.ScriptList.mergeMap = {
'ScriptCount': lambda lst: None, # TODO "ScriptCount": lambda lst: None, # TODO
'ScriptRecord': mergeScriptRecords, "ScriptRecord": mergeScriptRecords,
} }
otTables.BaseScriptList.mergeMap = { otTables.BaseScriptList.mergeMap = {
'BaseScriptCount': lambda lst: None, # TODO "BaseScriptCount": lambda lst: None, # TODO
# TODO: Merge duplicate entries # TODO: Merge duplicate entries
'BaseScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.BaseScriptTag), "BaseScriptRecord": lambda lst: sorted(
sumLists(lst), key=lambda s: s.BaseScriptTag
),
} }
otTables.FeatureList.mergeMap = { otTables.FeatureList.mergeMap = {
'FeatureCount': sum, "FeatureCount": sum,
'FeatureRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag), "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag),
} }
otTables.LookupList.mergeMap = { otTables.LookupList.mergeMap = {
'LookupCount': sum, "LookupCount": sum,
'Lookup': sumLists, "Lookup": sumLists,
} }
otTables.Coverage.mergeMap = { otTables.Coverage.mergeMap = {
'Format': min, "Format": min,
'glyphs': sumLists, "glyphs": sumLists,
} }
otTables.ClassDef.mergeMap = { otTables.ClassDef.mergeMap = {
'Format': min, "Format": min,
'classDefs': sumDicts, "classDefs": sumDicts,
} }
otTables.LigCaretList.mergeMap = { otTables.LigCaretList.mergeMap = {
'Coverage': mergeObjects, "Coverage": mergeObjects,
'LigGlyphCount': sum, "LigGlyphCount": sum,
'LigGlyph': sumLists, "LigGlyph": sumLists,
} }
otTables.AttachList.mergeMap = { otTables.AttachList.mergeMap = {
'Coverage': mergeObjects, "Coverage": mergeObjects,
'GlyphCount': sum, "GlyphCount": sum,
'AttachPoint': sumLists, "AttachPoint": sumLists,
} }
# XXX Renumber MarkFilterSets of lookups # XXX Renumber MarkFilterSets of lookups
otTables.MarkGlyphSetsDef.mergeMap = { otTables.MarkGlyphSetsDef.mergeMap = {
'MarkSetTableFormat': equal, "MarkSetTableFormat": equal,
'MarkSetCount': sum, "MarkSetCount": sum,
'Coverage': sumLists, "Coverage": sumLists,
} }
otTables.Axis.mergeMap = { otTables.Axis.mergeMap = {
'*': mergeObjects, "*": mergeObjects,
} }
# XXX Fix BASE table merging # XXX Fix BASE table merging
otTables.BaseTagList.mergeMap = { otTables.BaseTagList.mergeMap = {
'BaseTagCount': sum, "BaseTagCount": sum,
'BaselineTag': sumLists, "BaselineTag": sumLists,
} }
otTables.GDEF.mergeMap = \ otTables.GDEF.mergeMap = (
otTables.GSUB.mergeMap = \ otTables.GSUB.mergeMap
otTables.GPOS.mergeMap = \ ) = (
otTables.BASE.mergeMap = \ otTables.GPOS.mergeMap
otTables.JSTF.mergeMap = \ ) = otTables.BASE.mergeMap = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = {
otTables.MATH.mergeMap = \ "*": mergeObjects,
{ "Version": max,
'*': mergeObjects,
'Version': max,
} }
ttLib.getTableClass('GDEF').mergeMap = \ ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass(
ttLib.getTableClass('GSUB').mergeMap = \ "GSUB"
ttLib.getTableClass('GPOS').mergeMap = \ ).mergeMap = ttLib.getTableClass("GPOS").mergeMap = ttLib.getTableClass(
ttLib.getTableClass('BASE').mergeMap = \ "BASE"
ttLib.getTableClass('JSTF').mergeMap = \ ).mergeMap = ttLib.getTableClass(
ttLib.getTableClass('MATH').mergeMap = \ "JSTF"
{ ).mergeMap = ttLib.getTableClass(
'tableTag': onlyExisting(equal), # XXX clean me up "MATH"
'table': mergeObjects, ).mergeMap = {
"tableTag": onlyExisting(equal), # XXX clean me up
"table": mergeObjects,
} }
@add_method(ttLib.getTableClass('GSUB'))
@add_method(ttLib.getTableClass("GSUB"))
def merge(self, m, tables): def merge(self, m, tables):
assert len(tables) == len(m.duplicateGlyphsPerFont) assert len(tables) == len(m.duplicateGlyphsPerFont)
for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)): for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
if not dups: continue if not dups:
continue
if table is None or table is NotImplemented: if table is None or table is NotImplemented:
log.warning("Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s", m.fonts[i]._merger__name, dups) log.warning(
"Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s",
m.fonts[i]._merger__name,
dups,
)
continue continue
synthFeature = None synthFeature = None
synthLookup = None synthLookup = None
for script in table.table.ScriptList.ScriptRecord: for script in table.table.ScriptList.ScriptRecord:
if script.ScriptTag == 'DFLT': continue # XXX if script.ScriptTag == "DFLT":
for langsys in [script.Script.DefaultLangSys] + [l.LangSys for l in script.Script.LangSysRecord]: continue # XXX
if langsys is None: continue # XXX Create! for langsys in [script.Script.DefaultLangSys] + [
feature = [v for v in langsys.FeatureIndex if v.FeatureTag == 'locl'] l.LangSys for l in script.Script.LangSysRecord
]:
if langsys is None:
continue # XXX Create!
feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"]
assert len(feature) <= 1 assert len(feature) <= 1
if feature: if feature:
feature = feature[0] feature = feature[0]
else: else:
if not synthFeature: if not synthFeature:
synthFeature = otTables.FeatureRecord() synthFeature = otTables.FeatureRecord()
synthFeature.FeatureTag = 'locl' synthFeature.FeatureTag = "locl"
f = synthFeature.Feature = otTables.Feature() f = synthFeature.Feature = otTables.Feature()
f.FeatureParams = None f.FeatureParams = None
f.LookupCount = 0 f.LookupCount = 0
@ -238,7 +260,9 @@ def merge(self, m, tables):
DefaultTable.merge(self, m, tables) DefaultTable.merge(self, m, tables)
return self return self
@add_method(otTables.SingleSubst,
@add_method(
otTables.SingleSubst,
otTables.MultipleSubst, otTables.MultipleSubst,
otTables.AlternateSubst, otTables.AlternateSubst,
otTables.LigatureSubst, otTables.LigatureSubst,
@ -248,29 +272,32 @@ def merge(self, m, tables):
otTables.CursivePos, otTables.CursivePos,
otTables.MarkBasePos, otTables.MarkBasePos,
otTables.MarkLigPos, otTables.MarkLigPos,
otTables.MarkMarkPos) otTables.MarkMarkPos,
)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
pass pass
# Copied and trimmed down from subset.py # Copied and trimmed down from subset.py
@add_method(otTables.ContextSubst, @add_method(
otTables.ContextSubst,
otTables.ChainContextSubst, otTables.ChainContextSubst,
otTables.ContextPos, otTables.ContextPos,
otTables.ChainContextPos) otTables.ChainContextPos,
)
def __merge_classify_context(self): def __merge_classify_context(self):
class ContextHelper(object): class ContextHelper(object):
def __init__(self, klass, Format): def __init__(self, klass, Format):
if klass.__name__.endswith('Subst'): if klass.__name__.endswith("Subst"):
Typ = 'Sub' Typ = "Sub"
Type = 'Subst' Type = "Subst"
else: else:
Typ = 'Pos' Typ = "Pos"
Type = 'Pos' Type = "Pos"
if klass.__name__.startswith('Chain'): if klass.__name__.startswith("Chain"):
Chain = 'Chain' Chain = "Chain"
else: else:
Chain = '' Chain = ""
ChainTyp = Chain + Typ ChainTyp = Chain + Typ
self.Typ = Typ self.Typ = Typ
@ -278,14 +305,14 @@ def __merge_classify_context(self):
self.Chain = Chain self.Chain = Chain
self.ChainTyp = ChainTyp self.ChainTyp = ChainTyp
self.LookupRecord = Type+'LookupRecord' self.LookupRecord = Type + "LookupRecord"
if Format == 1: if Format == 1:
self.Rule = ChainTyp+'Rule' self.Rule = ChainTyp + "Rule"
self.RuleSet = ChainTyp+'RuleSet' self.RuleSet = ChainTyp + "RuleSet"
elif Format == 2: elif Format == 2:
self.Rule = ChainTyp+'ClassRule' self.Rule = ChainTyp + "ClassRule"
self.RuleSet = ChainTyp+'ClassSet' self.RuleSet = ChainTyp + "ClassSet"
if self.Format not in [1, 2, 3]: if self.Format not in [1, 2, 3]:
return None # Don't shoot the messenger; let it go return None # Don't shoot the messenger; let it go
@ -297,99 +324,121 @@ def __merge_classify_context(self):
return self.__class__._merge__ContextHelpers[self.Format] return self.__class__._merge__ContextHelpers[self.Format]
@add_method(otTables.ContextSubst, @add_method(
otTables.ContextSubst,
otTables.ChainContextSubst, otTables.ChainContextSubst,
otTables.ContextPos, otTables.ContextPos,
otTables.ChainContextPos) otTables.ChainContextPos,
)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
c = self.__merge_classify_context() c = self.__merge_classify_context()
if self.Format in [1, 2]: if self.Format in [1, 2]:
for rs in getattr(self, c.RuleSet): for rs in getattr(self, c.RuleSet):
if not rs: continue if not rs:
continue
for r in getattr(rs, c.Rule): for r in getattr(rs, c.Rule):
if not r: continue if not r:
continue
for ll in getattr(r, c.LookupRecord): for ll in getattr(r, c.LookupRecord):
if not ll: continue if not ll:
continue
ll.LookupListIndex = lookupMap[ll.LookupListIndex] ll.LookupListIndex = lookupMap[ll.LookupListIndex]
elif self.Format == 3: elif self.Format == 3:
for ll in getattr(self, c.LookupRecord): for ll in getattr(self, c.LookupRecord):
if not ll: continue if not ll:
continue
ll.LookupListIndex = lookupMap[ll.LookupListIndex] ll.LookupListIndex = lookupMap[ll.LookupListIndex]
else: else:
assert 0, "unknown format: %s" % self.Format assert 0, "unknown format: %s" % self.Format
@add_method(otTables.ExtensionSubst,
otTables.ExtensionPos) @add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
if self.Format == 1: if self.Format == 1:
self.ExtSubTable.mapLookups(lookupMap) self.ExtSubTable.mapLookups(lookupMap)
else: else:
assert 0, "unknown format: %s" % self.Format assert 0, "unknown format: %s" % self.Format
@add_method(otTables.Lookup) @add_method(otTables.Lookup)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
for st in self.SubTable: for st in self.SubTable:
if not st: continue if not st:
continue
st.mapLookups(lookupMap) st.mapLookups(lookupMap)
@add_method(otTables.LookupList) @add_method(otTables.LookupList)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
for l in self.Lookup: for l in self.Lookup:
if not l: continue if not l:
continue
l.mapLookups(lookupMap) l.mapLookups(lookupMap)
@add_method(otTables.Lookup) @add_method(otTables.Lookup)
def mapMarkFilteringSets(self, markFilteringSetMap): def mapMarkFilteringSets(self, markFilteringSetMap):
if self.LookupFlag & 0x0010: if self.LookupFlag & 0x0010:
self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet] self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet]
@add_method(otTables.LookupList) @add_method(otTables.LookupList)
def mapMarkFilteringSets(self, markFilteringSetMap): def mapMarkFilteringSets(self, markFilteringSetMap):
for l in self.Lookup: for l in self.Lookup:
if not l: continue if not l:
continue
l.mapMarkFilteringSets(markFilteringSetMap) l.mapMarkFilteringSets(markFilteringSetMap)
@add_method(otTables.Feature) @add_method(otTables.Feature)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
@add_method(otTables.FeatureList) @add_method(otTables.FeatureList)
def mapLookups(self, lookupMap): def mapLookups(self, lookupMap):
for f in self.FeatureRecord: for f in self.FeatureRecord:
if not f or not f.Feature: continue if not f or not f.Feature:
continue
f.Feature.mapLookups(lookupMap) f.Feature.mapLookups(lookupMap)
@add_method(otTables.DefaultLangSys,
otTables.LangSys) @add_method(otTables.DefaultLangSys, otTables.LangSys)
def mapFeatures(self, featureMap): def mapFeatures(self, featureMap):
self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
if self.ReqFeatureIndex != 65535: if self.ReqFeatureIndex != 65535:
self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
@add_method(otTables.Script) @add_method(otTables.Script)
def mapFeatures(self, featureMap): def mapFeatures(self, featureMap):
if self.DefaultLangSys: if self.DefaultLangSys:
self.DefaultLangSys.mapFeatures(featureMap) self.DefaultLangSys.mapFeatures(featureMap)
for l in self.LangSysRecord: for l in self.LangSysRecord:
if not l or not l.LangSys: continue if not l or not l.LangSys:
continue
l.LangSys.mapFeatures(featureMap) l.LangSys.mapFeatures(featureMap)
@add_method(otTables.ScriptList) @add_method(otTables.ScriptList)
def mapFeatures(self, featureMap): def mapFeatures(self, featureMap):
for s in self.ScriptRecord: for s in self.ScriptRecord:
if not s or not s.Script: continue if not s or not s.Script:
continue
s.Script.mapFeatures(featureMap) s.Script.mapFeatures(featureMap)
def layoutPreMerge(font): def layoutPreMerge(font):
# Map indices to references # Map indices to references
GDEF = font.get('GDEF') GDEF = font.get("GDEF")
GSUB = font.get('GSUB') GSUB = font.get("GSUB")
GPOS = font.get('GPOS') GPOS = font.get("GPOS")
for t in [GSUB, GPOS]: for t in [GSUB, GPOS]:
if not t: continue if not t:
continue
if t.table.LookupList: if t.table.LookupList:
lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)} lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)}
@ -397,7 +446,9 @@ def layoutPreMerge(font):
t.table.FeatureList.mapLookups(lookupMap) t.table.FeatureList.mapLookups(lookupMap)
if GDEF and GDEF.table.Version >= 0x00010002: if GDEF and GDEF.table.Version >= 0x00010002:
markFilteringSetMap = {i:v for i,v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)} markFilteringSetMap = {
i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)
}
t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
if t.table.FeatureList and t.table.ScriptList: if t.table.FeatureList and t.table.ScriptList:
@ -406,15 +457,17 @@ def layoutPreMerge(font):
# TODO FeatureParams nameIDs # TODO FeatureParams nameIDs
def layoutPostMerge(font): def layoutPostMerge(font):
# Map references back to indices # Map references back to indices
GDEF = font.get('GDEF') GDEF = font.get("GDEF")
GSUB = font.get('GSUB') GSUB = font.get("GSUB")
GPOS = font.get('GPOS') GPOS = font.get("GPOS")
for t in [GSUB, GPOS]: for t in [GSUB, GPOS]:
if not t: continue if not t:
continue
if t.table.FeatureList and t.table.ScriptList: if t.table.FeatureList and t.table.ScriptList:
@ -423,12 +476,18 @@ def layoutPostMerge(font):
t.table.ScriptList.mapFeatures(featureMap) t.table.ScriptList.mapFeatures(featureMap)
# Record used features. # Record used features.
featureMap = AttendanceRecordingIdentityDict(t.table.FeatureList.FeatureRecord) featureMap = AttendanceRecordingIdentityDict(
t.table.FeatureList.FeatureRecord
)
t.table.ScriptList.mapFeatures(featureMap) t.table.ScriptList.mapFeatures(featureMap)
usedIndices = featureMap.s usedIndices = featureMap.s
# Remove unused features # Remove unused features
t.table.FeatureList.FeatureRecord = [f for i,f in enumerate(t.table.FeatureList.FeatureRecord) if i in usedIndices] t.table.FeatureList.FeatureRecord = [
f
for i, f in enumerate(t.table.FeatureList.FeatureRecord)
if i in usedIndices
]
# Map back to indices. # Map back to indices.
featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord) featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord)
@ -450,7 +509,9 @@ def layoutPostMerge(font):
usedIndices = lookupMap.s usedIndices = lookupMap.s
# Remove unused lookups # Remove unused lookups
t.table.LookupList.Lookup = [l for i,l in enumerate(t.table.LookupList.Lookup) if i in usedIndices] t.table.LookupList.Lookup = [
l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices
]
# Map back to indices. # Map back to indices.
lookupMap = NonhashableDict(t.table.LookupList.Lookup) lookupMap = NonhashableDict(t.table.LookupList.Lookup)
@ -460,7 +521,9 @@ def layoutPostMerge(font):
t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup) t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup)
if GDEF and GDEF.table.Version >= 0x00010002: if GDEF and GDEF.table.Version >= 0x00010002:
markFilteringSetMap = NonhashableDict(GDEF.table.MarkGlyphSetsDef.Coverage) markFilteringSetMap = NonhashableDict(
GDEF.table.MarkGlyphSetsDef.Coverage
)
t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
# TODO FeatureParams nameIDs # TODO FeatureParams nameIDs

View File

@ -4,7 +4,6 @@
class Options(object): class Options(object):
class UnknownOptionError(Exception): class UnknownOptionError(Exception):
pass pass
@ -27,12 +26,12 @@ class Options(object):
opts = {} opts = {}
for a in argv: for a in argv:
orig_a = a orig_a = a
if not a.startswith('--'): if not a.startswith("--"):
ret.append(a) ret.append(a)
continue continue
a = a[2:] a = a[2:]
i = a.find('=') i = a.find("=")
op = '=' op = "="
if i == -1: if i == -1:
if a.startswith("no-"): if a.startswith("no-"):
k = a[3:] k = a[3:]
@ -43,11 +42,11 @@ class Options(object):
else: else:
k = a[:i] k = a[:i]
if k[-1] in "-+": if k[-1] in "-+":
op = k[-1]+'=' # Ops is '-=' or '+=' now. op = k[-1] + "=" # Ops is '-=' or '+=' now.
k = k[:-1] k = k[:-1]
v = a[i + 1 :] v = a[i + 1 :]
ok = k ok = k
k = k.replace('-', '_') k = k.replace("-", "_")
if not hasattr(self, k): if not hasattr(self, k):
if ignore_unknown is True or ok in ignore_unknown: if ignore_unknown is True or ok in ignore_unknown:
ret.append(orig_a) ret.append(orig_a)
@ -61,16 +60,16 @@ class Options(object):
elif isinstance(ov, int): elif isinstance(ov, int):
v = int(v) v = int(v)
elif isinstance(ov, list): elif isinstance(ov, list):
vv = v.split(',') vv = v.split(",")
if vv == ['']: if vv == [""]:
vv = [] vv = []
vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
if op == '=': if op == "=":
v = vv v = vv
elif op == '+=': elif op == "+=":
v = ov v = ov
v.extend(vv) v.extend(vv)
elif op == '-=': elif op == "-=":
v = ov v = ov
for x in vv: for x in vv:
if x in v: if x in v:
@ -82,4 +81,3 @@ class Options(object):
self.set(**opts) self.set(**opts)
return ret return ret

View File

@ -13,21 +13,21 @@ import logging
log = logging.getLogger("fontTools.merge") log = logging.getLogger("fontTools.merge")
ttLib.getTableClass('maxp').mergeMap = { ttLib.getTableClass("maxp").mergeMap = {
'*': max, "*": max,
'tableTag': equal, "tableTag": equal,
'tableVersion': equal, "tableVersion": equal,
'numGlyphs': sum, "numGlyphs": sum,
'maxStorage': first, "maxStorage": first,
'maxFunctionDefs': first, "maxFunctionDefs": first,
'maxInstructionDefs': first, "maxInstructionDefs": first,
# TODO When we correctly merge hinting data, update these values: # TODO When we correctly merge hinting data, update these values:
# maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
} }
headFlagsMergeBitMap = { headFlagsMergeBitMap = {
'size': 16, "size": 16,
'*': bitwise_or, "*": bitwise_or,
1: bitwise_and, # Baseline at y = 0 1: bitwise_and, # Baseline at y = 0
2: bitwise_and, # lsb at x = 0 2: bitwise_and, # lsb at x = 0
3: bitwise_and, # Force ppem to integer values. FIXME? 3: bitwise_and, # Force ppem to integer values. FIXME?
@ -39,64 +39,64 @@ headFlagsMergeBitMap = {
15: lambda bit: 0, # Always set to zero 15: lambda bit: 0, # Always set to zero
} }
ttLib.getTableClass('head').mergeMap = { ttLib.getTableClass("head").mergeMap = {
'tableTag': equal, "tableTag": equal,
'tableVersion': max, "tableVersion": max,
'fontRevision': max, "fontRevision": max,
'checkSumAdjustment': lambda lst: 0, # We need *something* here "checkSumAdjustment": lambda lst: 0, # We need *something* here
'magicNumber': equal, "magicNumber": equal,
'flags': mergeBits(headFlagsMergeBitMap), "flags": mergeBits(headFlagsMergeBitMap),
'unitsPerEm': equal, "unitsPerEm": equal,
'created': current_time, "created": current_time,
'modified': current_time, "modified": current_time,
'xMin': min, "xMin": min,
'yMin': min, "yMin": min,
'xMax': max, "xMax": max,
'yMax': max, "yMax": max,
'macStyle': first, "macStyle": first,
'lowestRecPPEM': max, "lowestRecPPEM": max,
'fontDirectionHint': lambda lst: 2, "fontDirectionHint": lambda lst: 2,
'indexToLocFormat': first, "indexToLocFormat": first,
'glyphDataFormat': equal, "glyphDataFormat": equal,
} }
ttLib.getTableClass('hhea').mergeMap = { ttLib.getTableClass("hhea").mergeMap = {
'*': equal, "*": equal,
'tableTag': equal, "tableTag": equal,
'tableVersion': max, "tableVersion": max,
'ascent': max, "ascent": max,
'descent': min, "descent": min,
'lineGap': max, "lineGap": max,
'advanceWidthMax': max, "advanceWidthMax": max,
'minLeftSideBearing': min, "minLeftSideBearing": min,
'minRightSideBearing': min, "minRightSideBearing": min,
'xMaxExtent': max, "xMaxExtent": max,
'caretSlopeRise': first, "caretSlopeRise": first,
'caretSlopeRun': first, "caretSlopeRun": first,
'caretOffset': first, "caretOffset": first,
'numberOfHMetrics': recalculate, "numberOfHMetrics": recalculate,
} }
ttLib.getTableClass('vhea').mergeMap = { ttLib.getTableClass("vhea").mergeMap = {
'*': equal, "*": equal,
'tableTag': equal, "tableTag": equal,
'tableVersion': max, "tableVersion": max,
'ascent': max, "ascent": max,
'descent': min, "descent": min,
'lineGap': max, "lineGap": max,
'advanceHeightMax': max, "advanceHeightMax": max,
'minTopSideBearing': min, "minTopSideBearing": min,
'minBottomSideBearing': min, "minBottomSideBearing": min,
'yMaxExtent': max, "yMaxExtent": max,
'caretSlopeRise': first, "caretSlopeRise": first,
'caretSlopeRun': first, "caretSlopeRun": first,
'caretOffset': first, "caretOffset": first,
'numberOfVMetrics': recalculate, "numberOfVMetrics": recalculate,
} }
os2FsTypeMergeBitMap = { os2FsTypeMergeBitMap = {
'size': 16, "size": 16,
'*': lambda bit: 0, "*": lambda bit: 0,
1: bitwise_or, # no embedding permitted 1: bitwise_or, # no embedding permitted
2: bitwise_and, # allow previewing and printing documents 2: bitwise_and, # allow previewing and printing documents
3: bitwise_and, # allow editing documents 3: bitwise_and, # allow editing documents
@ -104,6 +104,7 @@ os2FsTypeMergeBitMap = {
9: bitwise_or, # no embedding of outlines permitted 9: bitwise_or, # no embedding of outlines permitted
} }
def mergeOs2FsType(lst): def mergeOs2FsType(lst):
lst = list(lst) lst = list(lst)
if all(item == 0 for item in lst): if all(item == 0 for item in lst):
@ -128,39 +129,40 @@ def mergeOs2FsType(lst):
return fsType return fsType
ttLib.getTableClass('OS/2').mergeMap = { ttLib.getTableClass("OS/2").mergeMap = {
'*': first, "*": first,
'tableTag': equal, "tableTag": equal,
'version': max, "version": max,
'xAvgCharWidth': first, # Will be recalculated at the end on the merged font "xAvgCharWidth": first, # Will be recalculated at the end on the merged font
'fsType': mergeOs2FsType, # Will be overwritten "fsType": mergeOs2FsType, # Will be overwritten
'panose': first, # FIXME: should really be the first Latin font "panose": first, # FIXME: should really be the first Latin font
'ulUnicodeRange1': bitwise_or, "ulUnicodeRange1": bitwise_or,
'ulUnicodeRange2': bitwise_or, "ulUnicodeRange2": bitwise_or,
'ulUnicodeRange3': bitwise_or, "ulUnicodeRange3": bitwise_or,
'ulUnicodeRange4': bitwise_or, "ulUnicodeRange4": bitwise_or,
'fsFirstCharIndex': min, "fsFirstCharIndex": min,
'fsLastCharIndex': max, "fsLastCharIndex": max,
'sTypoAscender': max, "sTypoAscender": max,
'sTypoDescender': min, "sTypoDescender": min,
'sTypoLineGap': max, "sTypoLineGap": max,
'usWinAscent': max, "usWinAscent": max,
'usWinDescent': max, "usWinDescent": max,
# Version 1 # Version 1
'ulCodePageRange1': onlyExisting(bitwise_or), "ulCodePageRange1": onlyExisting(bitwise_or),
'ulCodePageRange2': onlyExisting(bitwise_or), "ulCodePageRange2": onlyExisting(bitwise_or),
# Version 2, 3, 4 # Version 2, 3, 4
'sxHeight': onlyExisting(max), "sxHeight": onlyExisting(max),
'sCapHeight': onlyExisting(max), "sCapHeight": onlyExisting(max),
'usDefaultChar': onlyExisting(first), "usDefaultChar": onlyExisting(first),
'usBreakChar': onlyExisting(first), "usBreakChar": onlyExisting(first),
'usMaxContext': onlyExisting(max), "usMaxContext": onlyExisting(max),
# version 5 # version 5
'usLowerOpticalPointSize': onlyExisting(min), "usLowerOpticalPointSize": onlyExisting(min),
'usUpperOpticalPointSize': onlyExisting(max), "usUpperOpticalPointSize": onlyExisting(max),
} }
@add_method(ttLib.getTableClass('OS/2'))
@add_method(ttLib.getTableClass("OS/2"))
def merge(self, m, tables): def merge(self, m, tables):
DefaultTable.merge(self, m, tables) DefaultTable.merge(self, m, tables)
if self.version < 2: if self.version < 2:
@ -174,41 +176,44 @@ def merge(self, m, tables):
self.fsType &= ~0x0004 self.fsType &= ~0x0004
return self return self
ttLib.getTableClass('post').mergeMap = {
'*': first, ttLib.getTableClass("post").mergeMap = {
'tableTag': equal, "*": first,
'formatType': max, "tableTag": equal,
'isFixedPitch': min, "formatType": max,
'minMemType42': max, "isFixedPitch": min,
'maxMemType42': lambda lst: 0, "minMemType42": max,
'minMemType1': max, "maxMemType42": lambda lst: 0,
'maxMemType1': lambda lst: 0, "minMemType1": max,
'mapping': onlyExisting(sumDicts), "maxMemType1": lambda lst: 0,
'extraNames': lambda lst: [], "mapping": onlyExisting(sumDicts),
"extraNames": lambda lst: [],
} }
ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = { ttLib.getTableClass("vmtx").mergeMap = ttLib.getTableClass("hmtx").mergeMap = {
'tableTag': equal, "tableTag": equal,
'metrics': sumDicts, "metrics": sumDicts,
} }
ttLib.getTableClass('name').mergeMap = { ttLib.getTableClass("name").mergeMap = {
'tableTag': equal, "tableTag": equal,
'names': first, # FIXME? Does mixing name records make sense? "names": first, # FIXME? Does mixing name records make sense?
} }
ttLib.getTableClass('loca').mergeMap = { ttLib.getTableClass("loca").mergeMap = {
'*': recalculate, "*": recalculate,
'tableTag': equal, "tableTag": equal,
} }
ttLib.getTableClass('glyf').mergeMap = { ttLib.getTableClass("glyf").mergeMap = {
'tableTag': equal, "tableTag": equal,
'glyphs': sumDicts, "glyphs": sumDicts,
'glyphOrder': sumLists, "glyphOrder": sumLists,
"axisTags": equal,
} }
@add_method(ttLib.getTableClass('glyf'))
@add_method(ttLib.getTableClass("glyf"))
def merge(self, m, tables): def merge(self, m, tables):
for i, table in enumerate(tables): for i, table in enumerate(tables):
for g in table.glyphs.values(): for g in table.glyphs.values():
@ -218,22 +223,24 @@ def merge(self, m, tables):
g.removeHinting() g.removeHinting()
# Expand composite glyphs to load their # Expand composite glyphs to load their
# composite glyph names. # composite glyph names.
if g.isComposite(): if g.isComposite() or g.isVarComposite():
g.expand(table) g.expand(table)
return DefaultTable.merge(self, m, tables) return DefaultTable.merge(self, m, tables)
ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable
@add_method(ttLib.getTableClass('CFF ')) ttLib.getTableClass("prep").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("fpgm").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("cvt ").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("gasp").mergeMap = lambda self, lst: first(
lst
) # FIXME? Appears irreconcilable
@add_method(ttLib.getTableClass("CFF "))
def merge(self, m, tables): def merge(self, m, tables):
if any(hasattr(table, "FDSelect") for table in tables): if any(hasattr(table, "FDSelect") for table in tables):
raise NotImplementedError( raise NotImplementedError("Merging CID-keyed CFF tables is not supported yet")
"Merging CID-keyed CFF tables is not supported yet"
)
for table in tables: for table in tables:
table.cff.desubroutinize() table.cff.desubroutinize()
@ -279,17 +286,18 @@ def merge(self, m, tables):
return newcff return newcff
@add_method(ttLib.getTableClass('cmap'))
@add_method(ttLib.getTableClass("cmap"))
def merge(self, m, tables): def merge(self, m, tables):
# TODO Handle format=14. # TODO Handle format=14.
if not hasattr(m, 'cmap'): if not hasattr(m, "cmap"):
computeMegaCmap(m, tables) computeMegaCmap(m, tables)
cmap = m.cmap cmap = m.cmap
cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF} cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF}
self.tables = [] self.tables = []
module = ttLib.getTableModule('cmap') module = ttLib.getTableModule("cmap")
if len(cmapBmpOnly) != len(cmap): if len(cmapBmpOnly) != len(cmap):
# format-12 required. # format-12 required.
cmapTable = module.cmap_classes[12](12) cmapTable = module.cmap_classes[12](12)

View File

@ -1,5 +1,6 @@
# Copyright 2021 Behdad Esfahbod. All Rights Reserved. # Copyright 2021 Behdad Esfahbod. All Rights Reserved.
def is_Default_Ignorable(u): def is_Default_Ignorable(u):
# http://www.unicode.org/reports/tr44/#Default_Ignorable_Code_Point # http://www.unicode.org/reports/tr44/#Default_Ignorable_Code_Point
# #
@ -35,31 +36,43 @@ def is_Default_Ignorable(u):
# E0100..E01EF # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256 # E0100..E01EF # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
# E01F0..E0FFF # Cn [3600] <reserved-E01F0>..<reserved-E0FFF> # E01F0..E0FFF # Cn [3600] <reserved-E01F0>..<reserved-E0FFF>
return ( return (
u == 0x00AD or # Cf SOFT HYPHEN u == 0x00AD
u == 0x034F or # Mn COMBINING GRAPHEME JOINER or u == 0x034F # Cf SOFT HYPHEN
u == 0x061C or # Cf ARABIC LETTER MARK or u == 0x061C # Mn COMBINING GRAPHEME JOINER
0x115F <= u <= 0x1160 or # Lo [2] HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER or 0x115F <= u <= 0x1160 # Cf ARABIC LETTER MARK
0x17B4 <= u <= 0x17B5 or # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA or 0x17B4 # Lo [2] HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER
0x180B <= u <= 0x180D or # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE <= u
u == 0x180E or # Cf MONGOLIAN VOWEL SEPARATOR <= 0x17B5
u == 0x180F or # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR or 0x180B # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA
0x200B <= u <= 0x200F or # Cf [5] ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK <= u
0x202A <= u <= 0x202E or # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE <= 0x180D
0x2060 <= u <= 0x2064 or # Cf [5] WORD JOINER..INVISIBLE PLUS or u # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE
u == 0x2065 or # Cn <reserved-2065> == 0x180E
0x2066 <= u <= 0x206F or # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES or u == 0x180F # Cf MONGOLIAN VOWEL SEPARATOR
u == 0x3164 or # Lo HANGUL FILLER or 0x200B <= u <= 0x200F # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR
0xFE00 <= u <= 0xFE0F or # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16 or 0x202A <= u <= 0x202E # Cf [5] ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK
u == 0xFEFF or # Cf ZERO WIDTH NO-BREAK SPACE or 0x2060 # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
u == 0xFFA0 or # Lo HALFWIDTH HANGUL FILLER <= u
0xFFF0 <= u <= 0xFFF8 or # Cn [9] <reserved-FFF0>..<reserved-FFF8> <= 0x2064
0x1BCA0 <= u <= 0x1BCA3 or # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP or u == 0x2065 # Cf [5] WORD JOINER..INVISIBLE PLUS
0x1D173 <= u <= 0x1D17A or # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE or 0x2066 <= u <= 0x206F # Cn <reserved-2065>
u == 0xE0000 or # Cn <reserved-E0000> or u == 0x3164 # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES
u == 0xE0001 or # Cf LANGUAGE TAG or 0xFE00 <= u <= 0xFE0F # Lo HANGUL FILLER
0xE0002 <= u <= 0xE001F or # Cn [30] <reserved-E0002>..<reserved-E001F> or u == 0xFEFF # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16
0xE0020 <= u <= 0xE007F or # Cf [96] TAG SPACE..CANCEL TAG or u == 0xFFA0 # Cf ZERO WIDTH NO-BREAK SPACE
0xE0080 <= u <= 0xE00FF or # Cn [128] <reserved-E0080>..<reserved-E00FF> or 0xFFF0 <= u <= 0xFFF8 # Lo HALFWIDTH HANGUL FILLER
0xE0100 <= u <= 0xE01EF or # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256 or 0x1BCA0 <= u <= 0x1BCA3 # Cn [9] <reserved-FFF0>..<reserved-FFF8>
0xE01F0 <= u <= 0xE0FFF or # Cn [3600] <reserved-E01F0>..<reserved-E0FFF> or 0x1D173 # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP
False) <= u
<= 0x1D17A
or u == 0xE0000 # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE
or u == 0xE0001 # Cn <reserved-E0000>
or 0xE0002 <= u <= 0xE001F # Cf LANGUAGE TAG
or 0xE0020 <= u <= 0xE007F # Cn [30] <reserved-E0002>..<reserved-E001F>
or 0xE0080 <= u <= 0xE00FF # Cf [96] TAG SPACE..CANCEL TAG
or 0xE0100 <= u <= 0xE01EF # Cn [128] <reserved-E0080>..<reserved-E00FF>
or 0xE01F0 # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
<= u
<= 0xE0FFF
or False # Cn [3600] <reserved-E01F0>..<reserved-E0FFF>
)

View File

@ -14,6 +14,7 @@ log = logging.getLogger("fontTools.merge")
# General utility functions for merging values from different fonts # General utility functions for merging values from different fonts
def equal(lst): def equal(lst):
lst = list(lst) lst = list(lst)
t = iter(lst) t = iter(lst)
@ -21,25 +22,32 @@ def equal(lst):
assert all(item == first for item in t), "Expected all items to be equal: %s" % lst assert all(item == first for item in t), "Expected all items to be equal: %s" % lst
return first return first
def first(lst): def first(lst):
return next(iter(lst)) return next(iter(lst))
def recalculate(lst): def recalculate(lst):
return NotImplemented return NotImplemented
def current_time(lst): def current_time(lst):
return timestampNow() return timestampNow()
def bitwise_and(lst): def bitwise_and(lst):
return reduce(operator.and_, lst) return reduce(operator.and_, lst)
def bitwise_or(lst): def bitwise_or(lst):
return reduce(operator.or_, lst) return reduce(operator.or_, lst)
def avg_int(lst): def avg_int(lst):
lst = list(lst) lst = list(lst)
return sum(lst) // len(lst) return sum(lst) // len(lst)
def onlyExisting(func): def onlyExisting(func):
"""Returns a filter func that when called with a list, """Returns a filter func that when called with a list,
only calls func on the non-NotImplemented items of the list, only calls func on the non-NotImplemented items of the list,
@ -52,29 +60,31 @@ def onlyExisting(func):
return wrapper return wrapper
def sumLists(lst): def sumLists(lst):
l = [] l = []
for item in lst: for item in lst:
l.extend(item) l.extend(item)
return l return l
def sumDicts(lst): def sumDicts(lst):
d = {} d = {}
for item in lst: for item in lst:
d.update(item) d.update(item)
return d return d
def mergeBits(bitmap):
def mergeBits(bitmap):
def wrapper(lst): def wrapper(lst):
lst = list(lst) lst = list(lst)
returnValue = 0 returnValue = 0
for bitNumber in range(bitmap['size']): for bitNumber in range(bitmap["size"]):
try: try:
mergeLogic = bitmap[bitNumber] mergeLogic = bitmap[bitNumber]
except KeyError: except KeyError:
try: try:
mergeLogic = bitmap['*'] mergeLogic = bitmap["*"]
except KeyError: except KeyError:
raise Exception("Don't know how to merge bit %s" % bitNumber) raise Exception("Don't know how to merge bit %s" % bitNumber)
shiftedBit = 1 << bitNumber shiftedBit = 1 << bitNumber
@ -98,6 +108,7 @@ class AttendanceRecordingIdentityDict(object):
self.s.add(self.d[id(v)]) self.s.add(self.d[id(v)])
return v return v
class GregariousIdentityDict(object): class GregariousIdentityDict(object):
"""A dictionary-like object that welcomes guests without reservations and """A dictionary-like object that welcomes guests without reservations and
adds them to the end of the guest list.""" adds them to the end of the guest list."""
@ -112,6 +123,7 @@ class GregariousIdentityDict(object):
self.l.append(v) self.l.append(v)
return v return v
class NonhashableDict(object): class NonhashableDict(object):
"""A dictionary-like object mapping objects to values.""" """A dictionary-like object mapping objects to values."""

View File

@ -23,6 +23,7 @@ def calcBounds(array):
ys = [y for x, y in array] ys = [y for x, y in array]
return min(xs), min(ys), max(xs), max(ys) return min(xs), min(ys), max(xs), max(ys)
def calcIntBounds(array, round=otRound): def calcIntBounds(array, round=otRound):
"""Calculate the integer bounding rectangle of a 2D points array. """Calculate the integer bounding rectangle of a 2D points array.
@ -57,6 +58,7 @@ def updateBounds(bounds, p, min=min, max=max):
xMin, yMin, xMax, yMax = bounds xMin, yMin, xMax, yMax = bounds
return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y) return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y)
def pointInRect(p, rect): def pointInRect(p, rect):
"""Test if a point is inside a bounding rectangle. """Test if a point is inside a bounding rectangle.
@ -72,6 +74,7 @@ def pointInRect(p, rect):
xMin, yMin, xMax, yMax = rect xMin, yMin, xMax, yMax = rect
return (xMin <= x <= xMax) and (yMin <= y <= yMax) return (xMin <= x <= xMax) and (yMin <= y <= yMax)
def pointsInRect(array, rect): def pointsInRect(array, rect):
"""Determine which points are inside a bounding rectangle. """Determine which points are inside a bounding rectangle.
@ -88,6 +91,7 @@ def pointsInRect(array, rect):
xMin, yMin, xMax, yMax = rect xMin, yMin, xMax, yMax = rect
return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array] return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array]
def vectorLength(vector): def vectorLength(vector):
"""Calculate the length of the given vector. """Calculate the length of the given vector.
@ -100,6 +104,7 @@ def vectorLength(vector):
x, y = vector x, y = vector
return math.sqrt(x**2 + y**2) return math.sqrt(x**2 + y**2)
def asInt16(array): def asInt16(array):
"""Round a list of floats to 16-bit signed integers. """Round a list of floats to 16-bit signed integers.
@ -130,6 +135,7 @@ def normRect(rect):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return min(xMin, xMax), min(yMin, yMax), max(xMin, xMax), max(yMin, yMax) return min(xMin, xMax), min(yMin, yMax), max(xMin, xMax), max(yMin, yMax)
def scaleRect(rect, x, y): def scaleRect(rect, x, y):
"""Scale a bounding box rectangle. """Scale a bounding box rectangle.
@ -145,6 +151,7 @@ def scaleRect(rect, x, y):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return xMin * x, yMin * y, xMax * x, yMax * y return xMin * x, yMin * y, xMax * x, yMax * y
def offsetRect(rect, dx, dy): def offsetRect(rect, dx, dy):
"""Offset a bounding box rectangle. """Offset a bounding box rectangle.
@ -160,6 +167,7 @@ def offsetRect(rect, dx, dy):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return xMin + dx, yMin + dy, xMax + dx, yMax + dy return xMin + dx, yMin + dy, xMax + dx, yMax + dy
def insetRect(rect, dx, dy): def insetRect(rect, dx, dy):
"""Inset a bounding box rectangle on all sides. """Inset a bounding box rectangle on all sides.
@ -175,6 +183,7 @@ def insetRect(rect, dx, dy):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return xMin + dx, yMin + dy, xMax - dx, yMax - dy return xMin + dx, yMin + dy, xMax - dx, yMax - dy
def sectRect(rect1, rect2): def sectRect(rect1, rect2):
"""Test for rectangle-rectangle intersection. """Test for rectangle-rectangle intersection.
@ -191,12 +200,17 @@ def sectRect(rect1, rect2):
""" """
(xMin1, yMin1, xMax1, yMax1) = rect1 (xMin1, yMin1, xMax1, yMax1) = rect1
(xMin2, yMin2, xMax2, yMax2) = rect2 (xMin2, yMin2, xMax2, yMax2) = rect2
xMin, yMin, xMax, yMax = (max(xMin1, xMin2), max(yMin1, yMin2), xMin, yMin, xMax, yMax = (
min(xMax1, xMax2), min(yMax1, yMax2)) max(xMin1, xMin2),
max(yMin1, yMin2),
min(xMax1, xMax2),
min(yMax1, yMax2),
)
if xMin >= xMax or yMin >= yMax: if xMin >= xMax or yMin >= yMax:
return False, (0, 0, 0, 0) return False, (0, 0, 0, 0)
return True, (xMin, yMin, xMax, yMax) return True, (xMin, yMin, xMax, yMax)
def unionRect(rect1, rect2): def unionRect(rect1, rect2):
"""Determine union of bounding rectangles. """Determine union of bounding rectangles.
@ -211,10 +225,15 @@ def unionRect(rect1, rect2):
""" """
(xMin1, yMin1, xMax1, yMax1) = rect1 (xMin1, yMin1, xMax1, yMax1) = rect1
(xMin2, yMin2, xMax2, yMax2) = rect2 (xMin2, yMin2, xMax2, yMax2) = rect2
xMin, yMin, xMax, yMax = (min(xMin1, xMin2), min(yMin1, yMin2), xMin, yMin, xMax, yMax = (
max(xMax1, xMax2), max(yMax1, yMax2)) min(xMin1, xMin2),
min(yMin1, yMin2),
max(xMax1, xMax2),
max(yMax1, yMax2),
)
return (xMin, yMin, xMax, yMax) return (xMin, yMin, xMax, yMax)
def rectCenter(rect): def rectCenter(rect):
"""Determine rectangle center. """Determine rectangle center.
@ -228,6 +247,7 @@ def rectCenter(rect):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return (xMin + xMax) / 2, (yMin + yMax) / 2 return (xMin + xMax) / 2, (yMin + yMax) / 2
def rectArea(rect): def rectArea(rect):
"""Determine rectangle area. """Determine rectangle area.
@ -241,6 +261,7 @@ def rectArea(rect):
(xMin, yMin, xMax, yMax) = rect (xMin, yMin, xMax, yMax) = rect
return (yMax - yMin) * (xMax - xMin) return (yMax - yMin) * (xMax - xMin)
def intRect(rect): def intRect(rect):
"""Round a rectangle to integer values. """Round a rectangle to integer values.
@ -262,7 +283,6 @@ def intRect(rect):
class Vector(_Vector): class Vector(_Vector):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
warnings.warn( warnings.warn(
"fontTools.misc.arrayTools.Vector has been deprecated, please use " "fontTools.misc.arrayTools.Vector has been deprecated, please use "
@ -373,7 +393,9 @@ def _test():
(0, 2, 4, 5) (0, 2, 4, 5)
""" """
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -7,6 +7,17 @@ from fontTools.misc.transform import Identity
import math import math
from collections import namedtuple from collections import namedtuple
try:
import cython
COMPILED = cython.compiled
except (AttributeError, ImportError):
# if cython not installed, use mock module with no-op decorators and types
from fontTools.misc import cython
COMPILED = False
Intersection = namedtuple("Intersection", ["pt", "t1", "t2"]) Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
@ -26,10 +37,13 @@ __all__ = [
"splitCubic", "splitCubic",
"splitQuadraticAtT", "splitQuadraticAtT",
"splitCubicAtT", "splitCubicAtT",
"splitCubicAtTC",
"splitCubicIntoTwoAtTC",
"solveQuadratic", "solveQuadratic",
"solveCubic", "solveCubic",
"quadraticPointAtT", "quadraticPointAtT",
"cubicPointAtT", "cubicPointAtT",
"cubicPointAtTC",
"linePointAtT", "linePointAtT",
"segmentPointAtT", "segmentPointAtT",
"lineLineIntersections", "lineLineIntersections",
@ -67,6 +81,14 @@ def _split_cubic_into_two(p0, p1, p2, p3):
) )
@cython.returns(cython.double)
@cython.locals(
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
)
@cython.locals(mult=cython.double, arch=cython.double, box=cython.double)
def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
arch = abs(p0 - p3) arch = abs(p0 - p3)
box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3) box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3)
@ -79,6 +101,17 @@ def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
) )
@cython.returns(cython.double)
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
)
@cython.locals(
tolerance=cython.double,
mult=cython.double,
)
def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005):
"""Calculates the arc length for a cubic Bezier segment. """Calculates the arc length for a cubic Bezier segment.
@ -97,10 +130,18 @@ epsilonDigits = 6
epsilon = 1e-10 epsilon = 1e-10
@cython.cfunc
@cython.inline
@cython.returns(cython.double)
@cython.locals(v1=cython.complex, v2=cython.complex)
def _dot(v1, v2): def _dot(v1, v2):
return (v1 * v2.conjugate()).real return (v1 * v2.conjugate()).real
@cython.cfunc
@cython.inline
@cython.returns(cython.double)
@cython.locals(x=cython.complex)
def _intSecAtan(x): def _intSecAtan(x):
# In : sympy.integrate(sp.sec(sp.atan(x))) # In : sympy.integrate(sp.sec(sp.atan(x)))
# Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2 # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
@ -142,6 +183,25 @@ def calcQuadraticArcLength(pt1, pt2, pt3):
return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
@cython.returns(cython.double)
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
d0=cython.complex,
d1=cython.complex,
d=cython.complex,
n=cython.complex,
)
@cython.locals(
scale=cython.double,
origDist=cython.double,
a=cython.double,
b=cython.double,
x0=cython.double,
x1=cython.double,
Len=cython.double,
)
def calcQuadraticArcLengthC(pt1, pt2, pt3): def calcQuadraticArcLengthC(pt1, pt2, pt3):
"""Calculates the arc length for a quadratic Bezier segment. """Calculates the arc length for a quadratic Bezier segment.
@ -191,6 +251,17 @@ def approximateQuadraticArcLength(pt1, pt2, pt3):
return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
@cython.returns(cython.double)
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
)
@cython.locals(
v0=cython.double,
v1=cython.double,
v2=cython.double,
)
def approximateQuadraticArcLengthC(pt1, pt2, pt3): def approximateQuadraticArcLengthC(pt1, pt2, pt3):
"""Calculates the arc length for a quadratic Bezier segment. """Calculates the arc length for a quadratic Bezier segment.
@ -288,6 +359,20 @@ def approximateCubicArcLength(pt1, pt2, pt3, pt4):
) )
@cython.returns(cython.double)
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
)
@cython.locals(
v0=cython.double,
v1=cython.double,
v2=cython.double,
v3=cython.double,
v4=cython.double,
)
def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
"""Approximates the arc length for a cubic Bezier segment. """Approximates the arc length for a cubic Bezier segment.
@ -549,6 +634,70 @@ def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
return _splitCubicAtT(a, b, c, d, *ts) return _splitCubicAtT(a, b, c, d, *ts)
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
a=cython.complex,
b=cython.complex,
c=cython.complex,
d=cython.complex,
)
def splitCubicAtTC(pt1, pt2, pt3, pt4, *ts):
"""Split a cubic Bezier curve at one or more values of t.
Args:
pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers..
*ts: Positions at which to split the curve.
Yields:
Curve segments (each curve segment being four complex numbers).
"""
a, b, c, d = calcCubicParametersC(pt1, pt2, pt3, pt4)
yield from _splitCubicAtTC(a, b, c, d, *ts)
@cython.returns(cython.complex)
@cython.locals(
t=cython.double,
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
pointAtT=cython.complex,
off1=cython.complex,
off2=cython.complex,
)
@cython.locals(
t2=cython.double, _1_t=cython.double, _1_t_2=cython.double, _2_t_1_t=cython.double
)
def splitCubicIntoTwoAtTC(pt1, pt2, pt3, pt4, t):
"""Split a cubic Bezier curve at t.
Args:
pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.
t: Position at which to split the curve.
Returns:
A tuple of two curve segments (each curve segment being four complex numbers).
"""
t2 = t * t
_1_t = 1 - t
_1_t_2 = _1_t * _1_t
_2_t_1_t = 2 * t * _1_t
pointAtT = (
_1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
)
off1 = _1_t_2 * pt1 + _2_t_1_t * pt2 + t2 * pt3
off2 = _1_t_2 * pt2 + _2_t_1_t * pt3 + t2 * pt4
pt2 = pt1 + (pt2 - pt1) * t
pt3 = pt4 + (pt3 - pt4) * _1_t
return ((pt1, pt2, off1, pointAtT), (pointAtT, off2, pt3, pt4))
def _splitQuadraticAtT(a, b, c, *ts): def _splitQuadraticAtT(a, b, c, *ts):
ts = list(ts) ts = list(ts)
segments = [] segments = []
@ -611,6 +760,44 @@ def _splitCubicAtT(a, b, c, d, *ts):
return segments return segments
@cython.locals(
a=cython.complex,
b=cython.complex,
c=cython.complex,
d=cython.complex,
t1=cython.double,
t2=cython.double,
delta=cython.double,
delta_2=cython.double,
delta_3=cython.double,
a1=cython.complex,
b1=cython.complex,
c1=cython.complex,
d1=cython.complex,
)
def _splitCubicAtTC(a, b, c, d, *ts):
ts = list(ts)
ts.insert(0, 0.0)
ts.append(1.0)
for i in range(len(ts) - 1):
t1 = ts[i]
t2 = ts[i + 1]
delta = t2 - t1
delta_2 = delta * delta
delta_3 = delta * delta_2
t1_2 = t1 * t1
t1_3 = t1 * t1_2
# calc new a, b, c and d
a1 = a * delta_3
b1 = (3 * a * t1 + b) * delta_2
c1 = (2 * b * t1 + c + 3 * a * t1_2) * delta
d1 = a * t1_3 + b * t1_2 + c * t1 + d
pt1, pt2, pt3, pt4 = calcCubicPointsC(a1, b1, c1, d1)
yield (pt1, pt2, pt3, pt4)
# #
# Equation solvers. # Equation solvers.
# #
@ -773,6 +960,23 @@ def calcCubicParameters(pt1, pt2, pt3, pt4):
return (ax, ay), (bx, by), (cx, cy), (dx, dy) return (ax, ay), (bx, by), (cx, cy), (dx, dy)
@cython.cfunc
@cython.locals(
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
a=cython.complex,
b=cython.complex,
c=cython.complex,
)
def calcCubicParametersC(pt1, pt2, pt3, pt4):
c = (pt2 - pt1) * 3.0
b = (pt3 - pt2) * 3.0 - c
a = pt4 - pt1 - c - b
return (a, b, c, pt1)
def calcQuadraticPoints(a, b, c): def calcQuadraticPoints(a, b, c):
ax, ay = a ax, ay = a
bx, by = b bx, by = b
@ -802,6 +1006,24 @@ def calcCubicPoints(a, b, c, d):
return (x1, y1), (x2, y2), (x3, y3), (x4, y4) return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
@cython.cfunc
@cython.locals(
a=cython.complex,
b=cython.complex,
c=cython.complex,
d=cython.complex,
p2=cython.complex,
p3=cython.complex,
p4=cython.complex,
_1_3=cython.double,
)
def calcCubicPointsC(a, b, c, d, _1_3=1.0 / 3):
p2 = (c * _1_3) + d
p3 = (b + c) * _1_3 + p2
p4 = a + b + c + d
return (d, p2, p3, p4)
# #
# Point at time # Point at time
# #
@ -845,21 +1067,47 @@ def cubicPointAtT(pt1, pt2, pt3, pt4, t):
Returns: Returns:
A 2D tuple with the coordinates of the point. A 2D tuple with the coordinates of the point.
""" """
t2 = t * t
_1_t = 1 - t
_1_t_2 = _1_t * _1_t
x = ( x = (
(1 - t) * (1 - t) * (1 - t) * pt1[0] _1_t_2 * _1_t * pt1[0]
+ 3 * (1 - t) * (1 - t) * t * pt2[0] + 3 * (_1_t_2 * t * pt2[0] + _1_t * t2 * pt3[0])
+ 3 * (1 - t) * t * t * pt3[0] + t2 * t * pt4[0]
+ t * t * t * pt4[0]
) )
y = ( y = (
(1 - t) * (1 - t) * (1 - t) * pt1[1] _1_t_2 * _1_t * pt1[1]
+ 3 * (1 - t) * (1 - t) * t * pt2[1] + 3 * (_1_t_2 * t * pt2[1] + _1_t * t2 * pt3[1])
+ 3 * (1 - t) * t * t * pt3[1] + t2 * t * pt4[1]
+ t * t * t * pt4[1]
) )
return (x, y) return (x, y)
@cython.returns(cython.complex)
@cython.locals(
t=cython.double,
pt1=cython.complex,
pt2=cython.complex,
pt3=cython.complex,
pt4=cython.complex,
)
@cython.locals(t2=cython.double, _1_t=cython.double, _1_t_2=cython.double)
def cubicPointAtTC(pt1, pt2, pt3, pt4, t):
"""Finds the point at time `t` on a cubic curve.
Args:
pt1, pt2, pt3, pt4: Coordinates of the curve as complex numbers.
t: The time along the curve.
Returns:
A complex number with the coordinates of the point.
"""
t2 = t * t
_1_t = 1 - t
_1_t_2 = _1_t * _1_t
return _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
def segmentPointAtT(seg, t): def segmentPointAtT(seg, t):
if len(seg) == 2: if len(seg) == 2:
return linePointAtT(*seg, t) return linePointAtT(*seg, t)

View File

@ -168,4 +168,5 @@ def classify(list_of_sets, sort=True):
if __name__ == "__main__": if __name__ == "__main__":
import sys, doctest import sys, doctest
sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)

View File

@ -6,7 +6,9 @@ import re
numberAddedRE = re.compile(r"#\d+$") numberAddedRE = re.compile(r"#\d+$")
def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False, suffix=""): def makeOutputFileName(
input, outputDir=None, extension=None, overWrite=False, suffix=""
):
"""Generates a suitable file name for writing output. """Generates a suitable file name for writing output.
Often tools will want to take a file, do some kind of transformation to it, Often tools will want to take a file, do some kind of transformation to it,
@ -44,6 +46,7 @@ def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False, s
if not overWrite: if not overWrite:
while os.path.exists(output): while os.path.exists(output):
output = os.path.join( output = os.path.join(
dirName, fileName + suffix + "#" + repr(n) + extension) dirName, fileName + suffix + "#" + repr(n) + extension
)
n += 1 n += 1
return output return output

View File

@ -10,9 +10,11 @@ We only define the symbols that we use. E.g. see fontTools.cu2qu
from types import SimpleNamespace from types import SimpleNamespace
def _empty_decorator(x): def _empty_decorator(x):
return x return x
compiled = False compiled = False
for name in ("double", "complex", "int"): for name in ("double", "complex", "int"):

View File

@ -1,7 +1,7 @@
"""Misc dict tools.""" """Misc dict tools."""
__all__ = ['hashdict'] __all__ = ["hashdict"]
# https://stackoverflow.com/questions/1151658/python-hashable-dicts # https://stackoverflow.com/questions/1151658/python-hashable-dicts
class hashdict(dict): class hashdict(dict):
@ -26,36 +26,54 @@ class hashdict(dict):
http://stackoverflow.com/questions/1151658/python-hashable-dicts http://stackoverflow.com/questions/1151658/python-hashable-dicts
""" """
def __key(self): def __key(self):
return tuple(sorted(self.items())) return tuple(sorted(self.items()))
def __repr__(self): def __repr__(self):
return "{0}({1})".format(self.__class__.__name__, return "{0}({1})".format(
", ".join("{0}={1}".format( self.__class__.__name__,
str(i[0]),repr(i[1])) for i in self.__key())) ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()),
)
def __hash__(self): def __hash__(self):
return hash(self.__key()) return hash(self.__key())
def __setitem__(self, key, value): def __setitem__(self, key, value):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def __delitem__(self, key): def __delitem__(self, key):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def clear(self): def clear(self):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def pop(self, *args, **kwargs): def pop(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def popitem(self, *args, **kwargs): def popitem(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def setdefault(self, *args, **kwargs): def setdefault(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment" raise TypeError(
.format(self.__class__.__name__)) "{0} does not support item assignment".format(self.__class__.__name__)
)
# update is not ok because it mutates the object # update is not ok because it mutates the object
# __add__ is ok because it creates a new object # __add__ is ok because it creates a new object
# while the new object is under construction, it's ok to mutate it # while the new object is under construction, it's ok to mutate it
@ -63,4 +81,3 @@ class hashdict(dict):
result = hashdict(self) result = hashdict(self)
dict.update(result, right) dict.update(result, right)
return result return result

View File

@ -21,6 +21,7 @@ def _decryptChar(cipher, R):
R = ((cipher + R) * 52845 + 22719) & 0xFFFF R = ((cipher + R) * 52845 + 22719) & 0xFFFF
return bytechr(plain), R return bytechr(plain), R
def _encryptChar(plain, R): def _encryptChar(plain, R):
plain = byteord(plain) plain = byteord(plain)
cipher = ((plain ^ (R >> 8))) & 0xFF cipher = ((plain ^ (R >> 8))) & 0xFF
@ -56,6 +57,7 @@ def decrypt(cipherstring, R):
plainstring = bytesjoin(plainList) plainstring = bytesjoin(plainList)
return plainstring, int(R) return plainstring, int(R)
def encrypt(plainstring, R): def encrypt(plainstring, R):
r""" r"""
Encrypts a string using the Type 1 encryption algorithm. Encrypts a string using the Type 1 encryption algorithm.
@ -99,10 +101,13 @@ def encrypt(plainstring, R):
def hexString(s): def hexString(s):
import binascii import binascii
return binascii.hexlify(s) return binascii.hexlify(s)
def deHexString(h): def deHexString(h):
import binascii import binascii
h = bytesjoin(h.split()) h = bytesjoin(h.split())
return binascii.unhexlify(h) return binascii.unhexlify(h)
@ -110,4 +115,5 @@ def deHexString(h):
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -6,13 +6,13 @@ import fontTools.encodings.codecs
# Map keyed by platformID, then platEncID, then possibly langID # Map keyed by platformID, then platEncID, then possibly langID
_encodingMap = { _encodingMap = {
0: { # Unicode 0: { # Unicode
0: 'utf_16_be', 0: "utf_16_be",
1: 'utf_16_be', 1: "utf_16_be",
2: 'utf_16_be', 2: "utf_16_be",
3: 'utf_16_be', 3: "utf_16_be",
4: 'utf_16_be', 4: "utf_16_be",
5: 'utf_16_be', 5: "utf_16_be",
6: 'utf_16_be', 6: "utf_16_be",
}, },
1: { # Macintosh 1: { # Macintosh
# See # See
@ -31,35 +31,36 @@ _encodingMap = {
38: "mac_latin2", 38: "mac_latin2",
39: "mac_latin2", 39: "mac_latin2",
40: "mac_latin2", 40: "mac_latin2",
Ellipsis: 'mac_roman', # Other Ellipsis: "mac_roman", # Other
}, },
1: 'x_mac_japanese_ttx', 1: "x_mac_japanese_ttx",
2: 'x_mac_trad_chinese_ttx', 2: "x_mac_trad_chinese_ttx",
3: 'x_mac_korean_ttx', 3: "x_mac_korean_ttx",
6: 'mac_greek', 6: "mac_greek",
7: 'mac_cyrillic', 7: "mac_cyrillic",
25: 'x_mac_simp_chinese_ttx', 25: "x_mac_simp_chinese_ttx",
29: 'mac_latin2', 29: "mac_latin2",
35: 'mac_turkish', 35: "mac_turkish",
37: 'mac_iceland', 37: "mac_iceland",
}, },
2: { # ISO 2: { # ISO
0: 'ascii', 0: "ascii",
1: 'utf_16_be', 1: "utf_16_be",
2: 'latin1', 2: "latin1",
}, },
3: { # Microsoft 3: { # Microsoft
0: 'utf_16_be', 0: "utf_16_be",
1: 'utf_16_be', 1: "utf_16_be",
2: 'shift_jis', 2: "shift_jis",
3: 'gb2312', 3: "gb2312",
4: 'big5', 4: "big5",
5: 'euc_kr', 5: "euc_kr",
6: 'johab', 6: "johab",
10: 'utf_16_be', 10: "utf_16_be",
}, },
} }
def getEncoding(platformID, platEncID, langID, default=None): def getEncoding(platformID, platEncID, langID, default=None):
"""Returns the Python encoding name for OpenType platformID/encodingID/langID """Returns the Python encoding name for OpenType platformID/encodingID/langID
triplet. If encoding for these values is not known, by default None is triplet. If encoding for these values is not known, by default None is

View File

@ -244,7 +244,8 @@ except ImportError:
except UnicodeDecodeError: except UnicodeDecodeError:
raise ValueError( raise ValueError(
"Bytes strings can only contain ASCII characters. " "Bytes strings can only contain ASCII characters. "
"Use unicode strings for non-ASCII characters.") "Use unicode strings for non-ASCII characters."
)
except AttributeError: except AttributeError:
_raise_serialization_error(s) _raise_serialization_error(s)
if s and _invalid_xml_string.search(s): if s and _invalid_xml_string.search(s):
@ -425,9 +426,7 @@ except ImportError:
write(_escape_cdata(elem.tail)) write(_escape_cdata(elem.tail))
def _raise_serialization_error(text): def _raise_serialization_error(text):
raise TypeError( raise TypeError("cannot serialize %r (type %s)" % (text, type(text).__name__))
"cannot serialize %r (type %s)" % (text, type(text).__name__)
)
def _escape_cdata(text): def _escape_cdata(text):
# escape character data # escape character data

View File

@ -133,6 +133,7 @@ def userNameToFileName(userName, existing=[], prefix="", suffix=""):
# finished # finished
return fullName return fullName
def handleClash1(userName, existing=[], prefix="", suffix=""): def handleClash1(userName, existing=[], prefix="", suffix=""):
""" """
existing should be a case-insensitive list existing should be a case-insensitive list
@ -167,7 +168,7 @@ def handleClash1(userName, existing=[], prefix="", suffix=""):
prefixLength = len(prefix) prefixLength = len(prefix)
suffixLength = len(suffix) suffixLength = len(suffix)
if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
l = (prefixLength + len(userName) + suffixLength + 15) l = prefixLength + len(userName) + suffixLength + 15
sliceLength = maxFileNameLength - l sliceLength = maxFileNameLength - l
userName = userName[:sliceLength] userName = userName[:sliceLength]
finalName = None finalName = None
@ -189,6 +190,7 @@ def handleClash1(userName, existing=[], prefix="", suffix=""):
# finished # finished
return finalName return finalName
def handleClash2(existing=[], prefix="", suffix=""): def handleClash2(existing=[], prefix="", suffix=""):
""" """
existing should be a case-insensitive list existing should be a case-insensitive list
@ -236,7 +238,9 @@ def handleClash2(existing=[], prefix="", suffix=""):
# finished # finished
return finalName return finalName
if __name__ == "__main__": if __name__ == "__main__":
import doctest import doctest
import sys import sys
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -231,8 +231,10 @@ def ensureVersionIsLong(value):
if value < 0x10000: if value < 0x10000:
newValue = floatToFixed(value, 16) newValue = floatToFixed(value, 16)
log.warning( log.warning(
"Table version value is a float: %.4f; " "Table version value is a float: %.4f; " "fix to use hex instead: 0x%08x",
"fix to use hex instead: 0x%08x", value, newValue) value,
newValue,
)
value = newValue value = newValue
return value return value

View File

@ -54,9 +54,10 @@ class LevelFormatter(logging.Formatter):
""" """
def __init__(self, fmt=None, datefmt=None, style="%"): def __init__(self, fmt=None, datefmt=None, style="%"):
if style != '%': if style != "%":
raise ValueError( raise ValueError(
"only '%' percent style is supported in both python 2 and 3") "only '%' percent style is supported in both python 2 and 3"
)
if fmt is None: if fmt is None:
fmt = DEFAULT_FORMATS fmt = DEFAULT_FORMATS
if isinstance(fmt, str): if isinstance(fmt, str):
@ -66,7 +67,7 @@ class LevelFormatter(logging.Formatter):
custom_formats = dict(fmt) custom_formats = dict(fmt)
default_format = custom_formats.pop("*", None) default_format = custom_formats.pop("*", None)
else: else:
raise TypeError('fmt must be a str or a dict of str: %r' % fmt) raise TypeError("fmt must be a str or a dict of str: %r" % fmt)
super(LevelFormatter, self).__init__(default_format, datefmt) super(LevelFormatter, self).__init__(default_format, datefmt)
self.default_format = self._fmt self.default_format = self._fmt
self.custom_formats = {} self.custom_formats = {}
@ -133,15 +134,18 @@ def configLogger(**kwargs):
handlers = kwargs.pop("handlers", None) handlers = kwargs.pop("handlers", None)
if handlers is None: if handlers is None:
if "stream" in kwargs and "filename" in kwargs: if "stream" in kwargs and "filename" in kwargs:
raise ValueError("'stream' and 'filename' should not be " raise ValueError(
"specified together") "'stream' and 'filename' should not be " "specified together"
)
else: else:
if "stream" in kwargs or "filename" in kwargs: if "stream" in kwargs or "filename" in kwargs:
raise ValueError("'stream' or 'filename' should not be " raise ValueError(
"specified together with 'handlers'") "'stream' or 'filename' should not be "
"specified together with 'handlers'"
)
if handlers is None: if handlers is None:
filename = kwargs.pop("filename", None) filename = kwargs.pop("filename", None)
mode = kwargs.pop("filemode", 'a') mode = kwargs.pop("filemode", "a")
if filename: if filename:
h = logging.FileHandler(filename, mode) h = logging.FileHandler(filename, mode)
else: else:
@ -159,7 +163,7 @@ def configLogger(**kwargs):
fs = kwargs.pop("format", None) fs = kwargs.pop("format", None)
dfs = kwargs.pop("datefmt", None) dfs = kwargs.pop("datefmt", None)
# XXX: '%' is the only format style supported on both py2 and 3 # XXX: '%' is the only format style supported on both py2 and 3
style = kwargs.pop("style", '%') style = kwargs.pop("style", "%")
fmt = LevelFormatter(fs, dfs, style) fmt = LevelFormatter(fs, dfs, style)
filters = kwargs.pop("filters", []) filters = kwargs.pop("filters", [])
for h in handlers: for h in handlers:
@ -177,8 +181,8 @@ def configLogger(**kwargs):
if level is not None: if level is not None:
logger.setLevel(level) logger.setLevel(level)
if kwargs: if kwargs:
keys = ', '.join(kwargs.keys()) keys = ", ".join(kwargs.keys())
raise ValueError('Unrecognised argument(s): %s' % keys) raise ValueError("Unrecognised argument(s): %s" % keys)
def _resetExistingLoggers(parent="root"): def _resetExistingLoggers(parent="root"):
@ -287,10 +291,9 @@ class Timer(object):
def __init__(self, logger=None, msg=None, level=None, start=None): def __init__(self, logger=None, msg=None, level=None, start=None):
self.reset(start) self.reset(start)
if logger is None: if logger is None:
for arg in ('msg', 'level'): for arg in ("msg", "level"):
if locals().get(arg) is not None: if locals().get(arg) is not None:
raise ValueError( raise ValueError("'%s' can't be specified without a 'logger'" % arg)
"'%s' can't be specified without a 'logger'" % arg)
self.logger = logger self.logger = logger
self.level = level if level is not None else TIME_LEVEL self.level = level if level is not None else TIME_LEVEL
self.msg = msg self.msg = msg
@ -350,7 +353,7 @@ class Timer(object):
message = self.formatTime(self.msg, time) message = self.formatTime(self.msg, time)
# Allow log handlers to see the individual parts to facilitate things # Allow log handlers to see the individual parts to facilitate things
# like a server accumulating aggregate stats. # like a server accumulating aggregate stats.
msg_parts = { 'msg': self.msg, 'time': time } msg_parts = {"msg": self.msg, "time": time}
self.logger.log(self.level, message, msg_parts) self.logger.log(self.level, message, msg_parts)
def __call__(self, func_or_msg=None, **kwargs): def __call__(self, func_or_msg=None, **kwargs):
@ -370,6 +373,7 @@ class Timer(object):
def wrapper(*args, **kwds): def wrapper(*args, **kwds):
with self: with self:
return func(*args, **kwds) return func(*args, **kwds)
return wrapper return wrapper
else: else:
msg = func_or_msg or kwargs.get("msg") msg = func_or_msg or kwargs.get("msg")
@ -425,8 +429,7 @@ class ChannelsFilter(logging.Filter):
nlen = self.lengths[name] nlen = self.lengths[name]
if name == record.name: if name == record.name:
return True return True
elif (record.name.find(name, 0, nlen) == 0 elif record.name.find(name, 0, nlen) == 0 and record.name[nlen] == ".":
and record.name[nlen] == "."):
return True return True
return False return False
@ -465,6 +468,7 @@ class CapturingLogHandler(logging.Handler):
def assertRegex(self, regexp, msg=None): def assertRegex(self, regexp, msg=None):
import re import re
pattern = re.compile(regexp) pattern = re.compile(regexp)
for r in self.records: for r in self.records:
if pattern.search(r.getMessage()): if pattern.search(r.getMessage()):
@ -505,32 +509,35 @@ class LogMixin(object):
@property @property
def log(self): def log(self):
if not hasattr(self, "_log"): if not hasattr(self, "_log"):
name = ".".join( name = ".".join((self.__class__.__module__, self.__class__.__name__))
(self.__class__.__module__, self.__class__.__name__)
)
self._log = logging.getLogger(name) self._log = logging.getLogger(name)
return self._log return self._log
def deprecateArgument(name, msg, category=UserWarning): def deprecateArgument(name, msg, category=UserWarning):
"""Raise a warning about deprecated function argument 'name'.""" """Raise a warning about deprecated function argument 'name'."""
warnings.warn( warnings.warn("%r is deprecated; %s" % (name, msg), category=category, stacklevel=3)
"%r is deprecated; %s" % (name, msg), category=category, stacklevel=3)
def deprecateFunction(msg, category=UserWarning): def deprecateFunction(msg, category=UserWarning):
"""Decorator to raise a warning when a deprecated function is called.""" """Decorator to raise a warning when a deprecated function is called."""
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
warnings.warn( warnings.warn(
"%r is deprecated; %s" % (func.__name__, msg), "%r is deprecated; %s" % (func.__name__, msg),
category=category, stacklevel=2) category=category,
stacklevel=2,
)
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
return decorator return decorator
if __name__ == "__main__": if __name__ == "__main__":
import doctest import doctest
sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)

View File

@ -1,4 +1,5 @@
from fontTools.misc.textTools import Tag, bytesjoin, strjoin from fontTools.misc.textTools import Tag, bytesjoin, strjoin
try: try:
import xattr import xattr
except ImportError: except ImportError:
@ -24,7 +25,7 @@ def getMacCreatorAndType(path):
""" """
if xattr is not None: if xattr is not None:
try: try:
finderInfo = xattr.getxattr(path, 'com.apple.FinderInfo') finderInfo = xattr.getxattr(path, "com.apple.FinderInfo")
except (KeyError, IOError): except (KeyError, IOError):
pass pass
else: else:
@ -48,7 +49,8 @@ def setMacCreatorAndType(path, fileCreator, fileType):
""" """
if xattr is not None: if xattr is not None:
from fontTools.misc.textTools import pad from fontTools.misc.textTools import pad
if not all(len(s) == 4 for s in (fileCreator, fileType)): if not all(len(s) == 4 for s in (fileCreator, fileType)):
raise TypeError('arg must be string of 4 chars') raise TypeError("arg must be string of 4 chars")
finderInfo = pad(bytesjoin([fileType, fileCreator]), 32) finderInfo = pad(bytesjoin([fileType, fileCreator]), 32)
xattr.setxattr(path, 'com.apple.FinderInfo', finderInfo) xattr.setxattr(path, "com.apple.FinderInfo", finderInfo)

View File

@ -23,6 +23,7 @@ class ResourceReader(MutableMapping):
representing all the resources of a certain type. representing all the resources of a certain type.
""" """
def __init__(self, fileOrPath): def __init__(self, fileOrPath):
"""Open a file """Open a file
@ -31,7 +32,7 @@ class ResourceReader(MutableMapping):
``os.PathLike`` object, or a string. ``os.PathLike`` object, or a string.
""" """
self._resources = OrderedDict() self._resources = OrderedDict()
if hasattr(fileOrPath, 'read'): if hasattr(fileOrPath, "read"):
self.file = fileOrPath self.file = fileOrPath
else: else:
try: try:
@ -48,7 +49,7 @@ class ResourceReader(MutableMapping):
def openResourceFork(path): def openResourceFork(path):
if hasattr(path, "__fspath__"): # support os.PathLike objects if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__() path = path.__fspath__()
with open(path + '/..namedfork/rsrc', 'rb') as resfork: with open(path + "/..namedfork/rsrc", "rb") as resfork:
data = resfork.read() data = resfork.read()
infile = BytesIO(data) infile = BytesIO(data)
infile.name = path infile.name = path
@ -56,7 +57,7 @@ class ResourceReader(MutableMapping):
@staticmethod @staticmethod
def openDataFork(path): def openDataFork(path):
with open(path, 'rb') as datafork: with open(path, "rb") as datafork:
data = datafork.read() data = datafork.read()
infile = BytesIO(data) infile = BytesIO(data)
infile.name = path infile.name = path
@ -73,13 +74,13 @@ class ResourceReader(MutableMapping):
except OverflowError: except OverflowError:
raise ResourceError("Failed to seek offset ('offset' is too large)") raise ResourceError("Failed to seek offset ('offset' is too large)")
if self.file.tell() != offset: if self.file.tell() != offset:
raise ResourceError('Failed to seek offset (reached EOF)') raise ResourceError("Failed to seek offset (reached EOF)")
try: try:
data = self.file.read(numBytes) data = self.file.read(numBytes)
except OverflowError: except OverflowError:
raise ResourceError("Cannot read resource ('numBytes' is too large)") raise ResourceError("Cannot read resource ('numBytes' is too large)")
if len(data) != numBytes: if len(data) != numBytes:
raise ResourceError('Cannot read resource (not enough data)') raise ResourceError("Cannot read resource (not enough data)")
return data return data
def _readHeaderAndMap(self): def _readHeaderAndMap(self):
@ -96,15 +97,15 @@ class ResourceReader(MutableMapping):
def _readTypeList(self): def _readTypeList(self):
absTypeListOffset = self.absTypeListOffset absTypeListOffset = self.absTypeListOffset
numTypesData = self._read(2, absTypeListOffset) numTypesData = self._read(2, absTypeListOffset)
self.numTypes, = struct.unpack('>H', numTypesData) (self.numTypes,) = struct.unpack(">H", numTypesData)
absTypeListOffset2 = absTypeListOffset + 2 absTypeListOffset2 = absTypeListOffset + 2
for i in range(self.numTypes + 1): for i in range(self.numTypes + 1):
resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i
resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset)
item = sstruct.unpack(ResourceTypeItem, resTypeItemData) item = sstruct.unpack(ResourceTypeItem, resTypeItemData)
resType = tostr(item['type'], encoding='mac-roman') resType = tostr(item["type"], encoding="mac-roman")
refListOffset = absTypeListOffset + item['refListOffset'] refListOffset = absTypeListOffset + item["refListOffset"]
numRes = item['numRes'] + 1 numRes = item["numRes"] + 1
resources = self._readReferenceList(resType, refListOffset, numRes) resources = self._readReferenceList(resType, refListOffset, numRes)
self._resources[resType] = resources self._resources[resType] = resources
@ -174,7 +175,7 @@ class ResourceReader(MutableMapping):
def getNamedResource(self, resType, name): def getNamedResource(self, resType, name):
"""Return the named resource of given type, else return None.""" """Return the named resource of given type, else return None."""
name = tostr(name, encoding='mac-roman') name = tostr(name, encoding="mac-roman")
for res in self.get(resType, []): for res in self.get(resType, []):
if res.name == name: if res.name == name:
return res return res
@ -196,8 +197,9 @@ class Resource(object):
attr: attributes. attr: attributes.
""" """
def __init__(self, resType=None, resData=None, resID=None, resName=None, def __init__(
resAttr=None): self, resType=None, resData=None, resID=None, resName=None, resAttr=None
):
self.type = resType self.type = resType
self.data = resData self.data = resData
self.id = resID self.id = resID
@ -207,16 +209,16 @@ class Resource(object):
def decompile(self, refData, reader): def decompile(self, refData, reader):
sstruct.unpack(ResourceRefItem, refData, self) sstruct.unpack(ResourceRefItem, refData, self)
# interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct
self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset])) (self.dataOffset,) = struct.unpack(">L", bytesjoin([b"\0", self.dataOffset]))
absDataOffset = reader.dataOffset + self.dataOffset absDataOffset = reader.dataOffset + self.dataOffset
dataLength, = struct.unpack(">L", reader._read(4, absDataOffset)) (dataLength,) = struct.unpack(">L", reader._read(4, absDataOffset))
self.data = reader._read(dataLength) self.data = reader._read(dataLength)
if self.nameOffset == -1: if self.nameOffset == -1:
return return
absNameOffset = reader.absNameListOffset + self.nameOffset absNameOffset = reader.absNameListOffset + self.nameOffset
nameLength, = struct.unpack('B', reader._read(1, absNameOffset)) (nameLength,) = struct.unpack("B", reader._read(1, absNameOffset))
name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength)) (name,) = struct.unpack(">%ss" % nameLength, reader._read(nameLength))
self.name = tostr(name, encoding='mac-roman') self.name = tostr(name, encoding="mac-roman")
ResourceForkHeader = """ ResourceForkHeader = """

View File

@ -176,7 +176,7 @@ class PlistTarget:
True True
Links: Links:
https://github.com/python/cpython/blob/master/Lib/plistlib.py https://github.com/python/cpython/blob/main/Lib/plistlib.py
http://lxml.de/parsing.html#the-target-parser-interface http://lxml.de/parsing.html#the-target-parser-interface
""" """
@ -353,7 +353,9 @@ def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element:
return el return el
def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element: def _dict_element(
d: Mapping[str, PlistEncodable], ctx: SimpleNamespace
) -> etree.Element:
el = etree.Element("dict") el = etree.Element("dict")
items = d.items() items = d.items()
if ctx.sort_keys: if ctx.sort_keys:
@ -371,7 +373,9 @@ def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etre
return el return el
def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element: def _array_element(
array: Sequence[PlistEncodable], ctx: SimpleNamespace
) -> etree.Element:
el = etree.Element("array") el = etree.Element("array")
if len(array) == 0: if len(array) == 0:
return el return el

View File

@ -3,7 +3,10 @@ CFF dictionary data and Type1/Type2 CharStrings.
""" """
from fontTools.misc.fixedTools import ( from fontTools.misc.fixedTools import (
fixedToFloat, floatToFixed, floatToFixedToStr, strToFixedToFloat, fixedToFloat,
floatToFixed,
floatToFixedToStr,
strToFixedToFloat,
) )
from fontTools.misc.textTools import bytechr, byteord, bytesjoin, strjoin from fontTools.misc.textTools import bytechr, byteord, bytesjoin, strjoin
from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.boundsPen import BoundsPen
@ -27,44 +30,52 @@ def read_operator(self, b0, data, index):
value = self.handle_operator(operator) value = self.handle_operator(operator)
return value, index return value, index
def read_byte(self, b0, data, index): def read_byte(self, b0, data, index):
return b0 - 139, index return b0 - 139, index
def read_smallInt1(self, b0, data, index): def read_smallInt1(self, b0, data, index):
b1 = byteord(data[index]) b1 = byteord(data[index])
return (b0 - 247) * 256 + b1 + 108, index + 1 return (b0 - 247) * 256 + b1 + 108, index + 1
def read_smallInt2(self, b0, data, index): def read_smallInt2(self, b0, data, index):
b1 = byteord(data[index]) b1 = byteord(data[index])
return -(b0 - 251) * 256 - b1 - 108, index + 1 return -(b0 - 251) * 256 - b1 - 108, index + 1
def read_shortInt(self, b0, data, index): def read_shortInt(self, b0, data, index):
value, = struct.unpack(">h", data[index:index+2]) (value,) = struct.unpack(">h", data[index : index + 2])
return value, index + 2 return value, index + 2
def read_longInt(self, b0, data, index): def read_longInt(self, b0, data, index):
value, = struct.unpack(">l", data[index:index+4]) (value,) = struct.unpack(">l", data[index : index + 4])
return value, index + 4 return value, index + 4
def read_fixed1616(self, b0, data, index): def read_fixed1616(self, b0, data, index):
value, = struct.unpack(">l", data[index:index+4]) (value,) = struct.unpack(">l", data[index : index + 4])
return fixedToFloat(value, precisionBits=16), index + 4 return fixedToFloat(value, precisionBits=16), index + 4
def read_reserved(self, b0, data, index): def read_reserved(self, b0, data, index):
assert NotImplementedError assert NotImplementedError
return NotImplemented, index return NotImplemented, index
def read_realNumber(self, b0, data, index): def read_realNumber(self, b0, data, index):
number = '' number = ""
while True: while True:
b = byteord(data[index]) b = byteord(data[index])
index = index + 1 index = index + 1
nibble0 = (b & 0xf0) >> 4 nibble0 = (b & 0xF0) >> 4
nibble1 = b & 0x0f nibble1 = b & 0x0F
if nibble0 == 0xf: if nibble0 == 0xF:
break break
number = number + realNibbles[nibble0] number = number + realNibbles[nibble0]
if nibble1 == 0xf: if nibble1 == 0xF:
break break
number = number + realNibbles[nibble1] number = number + realNibbles[nibble1]
return float(number), index return float(number), index
@ -88,8 +99,23 @@ cffDictOperandEncoding[30] = read_realNumber
cffDictOperandEncoding[255] = read_reserved cffDictOperandEncoding[255] = read_reserved
realNibbles = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', realNibbles = [
'.', 'E', 'E-', None, '-'] "0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
".",
"E",
"E-",
None,
"-",
]
realNibblesDict = {v: i for i, v in enumerate(realNibbles)} realNibblesDict = {v: i for i, v in enumerate(realNibbles)}
maxOpStack = 193 maxOpStack = 193
@ -112,62 +138,63 @@ def buildOperatorDict(operatorList):
t2Operators = [ t2Operators = [
# opcode name # opcode name
(1, 'hstem'), (1, "hstem"),
(3, 'vstem'), (3, "vstem"),
(4, 'vmoveto'), (4, "vmoveto"),
(5, 'rlineto'), (5, "rlineto"),
(6, 'hlineto'), (6, "hlineto"),
(7, 'vlineto'), (7, "vlineto"),
(8, 'rrcurveto'), (8, "rrcurveto"),
(10, 'callsubr'), (10, "callsubr"),
(11, 'return'), (11, "return"),
(14, 'endchar'), (14, "endchar"),
(15, 'vsindex'), (15, "vsindex"),
(16, 'blend'), (16, "blend"),
(18, 'hstemhm'), (18, "hstemhm"),
(19, 'hintmask'), (19, "hintmask"),
(20, 'cntrmask'), (20, "cntrmask"),
(21, 'rmoveto'), (21, "rmoveto"),
(22, 'hmoveto'), (22, "hmoveto"),
(23, 'vstemhm'), (23, "vstemhm"),
(24, 'rcurveline'), (24, "rcurveline"),
(25, 'rlinecurve'), (25, "rlinecurve"),
(26, 'vvcurveto'), (26, "vvcurveto"),
(27, 'hhcurveto'), (27, "hhcurveto"),
# (28, 'shortint'), # not really an operator # (28, 'shortint'), # not really an operator
(29, 'callgsubr'), (29, "callgsubr"),
(30, 'vhcurveto'), (30, "vhcurveto"),
(31, 'hvcurveto'), (31, "hvcurveto"),
((12, 0), 'ignore'), # dotsection. Yes, there a few very early OTF/CFF ((12, 0), "ignore"), # dotsection. Yes, there a few very early OTF/CFF
# fonts with this deprecated operator. Just ignore it. # fonts with this deprecated operator. Just ignore it.
((12, 3), 'and'), ((12, 3), "and"),
((12, 4), 'or'), ((12, 4), "or"),
((12, 5), 'not'), ((12, 5), "not"),
((12, 8), 'store'), ((12, 8), "store"),
((12, 9), 'abs'), ((12, 9), "abs"),
((12, 10), 'add'), ((12, 10), "add"),
((12, 11), 'sub'), ((12, 11), "sub"),
((12, 12), 'div'), ((12, 12), "div"),
((12, 13), 'load'), ((12, 13), "load"),
((12, 14), 'neg'), ((12, 14), "neg"),
((12, 15), 'eq'), ((12, 15), "eq"),
((12, 18), 'drop'), ((12, 18), "drop"),
((12, 20), 'put'), ((12, 20), "put"),
((12, 21), 'get'), ((12, 21), "get"),
((12, 22), 'ifelse'), ((12, 22), "ifelse"),
((12, 23), 'random'), ((12, 23), "random"),
((12, 24), 'mul'), ((12, 24), "mul"),
((12, 26), 'sqrt'), ((12, 26), "sqrt"),
((12, 27), 'dup'), ((12, 27), "dup"),
((12, 28), 'exch'), ((12, 28), "exch"),
((12, 29), 'index'), ((12, 29), "index"),
((12, 30), 'roll'), ((12, 30), "roll"),
((12, 34), 'hflex'), ((12, 34), "hflex"),
((12, 35), 'flex'), ((12, 35), "flex"),
((12, 36), 'hflex1'), ((12, 36), "hflex1"),
((12, 37), 'flex1'), ((12, 37), "flex1"),
] ]
def getIntEncoder(format): def getIntEncoder(format):
if format == "cff": if format == "cff":
fourByteOp = bytechr(29) fourByteOp = bytechr(29)
@ -177,8 +204,13 @@ def getIntEncoder(format):
assert format == "t2" assert format == "t2"
fourByteOp = None fourByteOp = None
def encodeInt(value, fourByteOp=fourByteOp, bytechr=bytechr, def encodeInt(
pack=struct.pack, unpack=struct.unpack): value,
fourByteOp=fourByteOp,
bytechr=bytechr,
pack=struct.pack,
unpack=struct.unpack,
):
if -107 <= value <= 107: if -107 <= value <= 107:
code = bytechr(value + 139) code = bytechr(value + 139)
elif 108 <= value <= 1131: elif 108 <= value <= 1131:
@ -200,9 +232,11 @@ def getIntEncoder(format):
# distinguish anymore between small ints that were supposed to # distinguish anymore between small ints that were supposed to
# be small fixed numbers and small ints that were just small # be small fixed numbers and small ints that were just small
# ints. Hence the warning. # ints. Hence the warning.
log.warning("4-byte T2 number got passed to the " log.warning(
"4-byte T2 number got passed to the "
"IntType handler. This should happen only when reading in " "IntType handler. This should happen only when reading in "
"old XML files.\n") "old XML files.\n"
)
code = bytechr(255) + pack(">l", value) code = bytechr(255) + pack(">l", value)
else: else:
code = fourByteOp + pack(">l", value) code = fourByteOp + pack(">l", value)
@ -215,6 +249,7 @@ encodeIntCFF = getIntEncoder("cff")
encodeIntT1 = getIntEncoder("t1") encodeIntT1 = getIntEncoder("t1")
encodeIntT2 = getIntEncoder("t2") encodeIntT2 = getIntEncoder("t2")
def encodeFixed(f, pack=struct.pack): def encodeFixed(f, pack=struct.pack):
"""For T2 only""" """For T2 only"""
value = floatToFixed(f, precisionBits=16) value = floatToFixed(f, precisionBits=16)
@ -224,7 +259,8 @@ def encodeFixed(f, pack=struct.pack):
return b"\xff" + pack(">l", value) # encode the entire fixed point value return b"\xff" + pack(">l", value) # encode the entire fixed point value
realZeroBytes = bytechr(30) + bytechr(0xf) realZeroBytes = bytechr(30) + bytechr(0xF)
def encodeFloat(f): def encodeFloat(f):
# For CFF only, used in cffLib # For CFF only, used in cffLib
@ -249,20 +285,20 @@ def encodeFloat(f):
elif c2 == "+": elif c2 == "+":
s = s[1:] s = s[1:]
nibbles.append(realNibblesDict[c]) nibbles.append(realNibblesDict[c])
nibbles.append(0xf) nibbles.append(0xF)
if len(nibbles) % 2: if len(nibbles) % 2:
nibbles.append(0xf) nibbles.append(0xF)
d = bytechr(30) d = bytechr(30)
for i in range(0, len(nibbles), 2): for i in range(0, len(nibbles), 2):
d = d + bytechr(nibbles[i] << 4 | nibbles[i + 1]) d = d + bytechr(nibbles[i] << 4 | nibbles[i + 1])
return d return d
class CharStringCompileError(Exception): pass class CharStringCompileError(Exception):
pass
class SimpleT2Decompiler(object): class SimpleT2Decompiler(object):
def __init__(self, localSubrs, globalSubrs, private=None, blender=None): def __init__(self, localSubrs, globalSubrs, private=None, blender=None):
self.localSubrs = localSubrs self.localSubrs = localSubrs
self.localBias = calcSubrBias(localSubrs) self.localBias = calcSubrBias(localSubrs)
@ -346,10 +382,13 @@ class SimpleT2Decompiler(object):
def op_hstem(self, index): def op_hstem(self, index):
self.countHints() self.countHints()
def op_vstem(self, index): def op_vstem(self, index):
self.countHints() self.countHints()
def op_hstemhm(self, index): def op_hstemhm(self, index):
self.countHints() self.countHints()
def op_vstemhm(self, index): def op_vstemhm(self, index):
self.countHints() self.countHints()
@ -369,46 +408,67 @@ class SimpleT2Decompiler(object):
# misc # misc
def op_and(self, index): def op_and(self, index):
raise NotImplementedError raise NotImplementedError
def op_or(self, index): def op_or(self, index):
raise NotImplementedError raise NotImplementedError
def op_not(self, index): def op_not(self, index):
raise NotImplementedError raise NotImplementedError
def op_store(self, index): def op_store(self, index):
raise NotImplementedError raise NotImplementedError
def op_abs(self, index): def op_abs(self, index):
raise NotImplementedError raise NotImplementedError
def op_add(self, index): def op_add(self, index):
raise NotImplementedError raise NotImplementedError
def op_sub(self, index): def op_sub(self, index):
raise NotImplementedError raise NotImplementedError
def op_div(self, index): def op_div(self, index):
raise NotImplementedError raise NotImplementedError
def op_load(self, index): def op_load(self, index):
raise NotImplementedError raise NotImplementedError
def op_neg(self, index): def op_neg(self, index):
raise NotImplementedError raise NotImplementedError
def op_eq(self, index): def op_eq(self, index):
raise NotImplementedError raise NotImplementedError
def op_drop(self, index): def op_drop(self, index):
raise NotImplementedError raise NotImplementedError
def op_put(self, index): def op_put(self, index):
raise NotImplementedError raise NotImplementedError
def op_get(self, index): def op_get(self, index):
raise NotImplementedError raise NotImplementedError
def op_ifelse(self, index): def op_ifelse(self, index):
raise NotImplementedError raise NotImplementedError
def op_random(self, index): def op_random(self, index):
raise NotImplementedError raise NotImplementedError
def op_mul(self, index): def op_mul(self, index):
raise NotImplementedError raise NotImplementedError
def op_sqrt(self, index): def op_sqrt(self, index):
raise NotImplementedError raise NotImplementedError
def op_dup(self, index): def op_dup(self, index):
raise NotImplementedError raise NotImplementedError
def op_exch(self, index): def op_exch(self, index):
raise NotImplementedError raise NotImplementedError
def op_index(self, index): def op_index(self, index):
raise NotImplementedError raise NotImplementedError
def op_roll(self, index): def op_roll(self, index):
raise NotImplementedError raise NotImplementedError
@ -418,7 +478,9 @@ class SimpleT2Decompiler(object):
numBlends = self.pop() numBlends = self.pop()
numOps = numBlends * (self.numRegions + 1) numOps = numBlends * (self.numRegions + 1)
if self.blender is None: if self.blender is None:
del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack. del self.operandStack[
-(numOps - numBlends) :
] # Leave the default operands on the stack.
else: else:
argi = len(self.operandStack) - numOps argi = len(self.operandStack) - numOps
end_args = tuplei = argi + numBlends end_args = tuplei = argi + numBlends
@ -439,37 +501,44 @@ class SimpleT2Decompiler(object):
t1Operators = [ t1Operators = [
# opcode name # opcode name
(1, 'hstem'), (1, "hstem"),
(3, 'vstem'), (3, "vstem"),
(4, 'vmoveto'), (4, "vmoveto"),
(5, 'rlineto'), (5, "rlineto"),
(6, 'hlineto'), (6, "hlineto"),
(7, 'vlineto'), (7, "vlineto"),
(8, 'rrcurveto'), (8, "rrcurveto"),
(9, 'closepath'), (9, "closepath"),
(10, 'callsubr'), (10, "callsubr"),
(11, 'return'), (11, "return"),
(13, 'hsbw'), (13, "hsbw"),
(14, 'endchar'), (14, "endchar"),
(21, 'rmoveto'), (21, "rmoveto"),
(22, 'hmoveto'), (22, "hmoveto"),
(30, 'vhcurveto'), (30, "vhcurveto"),
(31, 'hvcurveto'), (31, "hvcurveto"),
((12, 0), 'dotsection'), ((12, 0), "dotsection"),
((12, 1), 'vstem3'), ((12, 1), "vstem3"),
((12, 2), 'hstem3'), ((12, 2), "hstem3"),
((12, 6), 'seac'), ((12, 6), "seac"),
((12, 7), 'sbw'), ((12, 7), "sbw"),
((12, 12), 'div'), ((12, 12), "div"),
((12, 16), 'callothersubr'), ((12, 16), "callothersubr"),
((12, 17), 'pop'), ((12, 17), "pop"),
((12, 33), 'setcurrentpoint'), ((12, 33), "setcurrentpoint"),
] ]
class T2WidthExtractor(SimpleT2Decompiler): class T2WidthExtractor(SimpleT2Decompiler):
def __init__(
def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None, blender=None): self,
localSubrs,
globalSubrs,
nominalWidthX,
defaultWidthX,
private=None,
blender=None,
):
SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private, blender) SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private, blender)
self.nominalWidthX = nominalWidthX self.nominalWidthX = nominalWidthX
self.defaultWidthX = defaultWidthX self.defaultWidthX = defaultWidthX
@ -484,7 +553,9 @@ class T2WidthExtractor(SimpleT2Decompiler):
if not self.gotWidth: if not self.gotWidth:
if evenOdd ^ (len(args) % 2): if evenOdd ^ (len(args) % 2):
# For CFF2 charstrings, this should never happen # For CFF2 charstrings, this should never happen
assert self.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value" assert (
self.defaultWidthX is not None
), "CFF2 CharStrings must not have an initial width value"
self.width = self.nominalWidthX + args[0] self.width = self.nominalWidthX + args[0]
args = args[1:] args = args[1:]
else: else:
@ -510,10 +581,25 @@ class T2WidthExtractor(SimpleT2Decompiler):
class T2OutlineExtractor(T2WidthExtractor): class T2OutlineExtractor(T2WidthExtractor):
def __init__(
def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None, blender=None): self,
pen,
localSubrs,
globalSubrs,
nominalWidthX,
defaultWidthX,
private=None,
blender=None,
):
T2WidthExtractor.__init__( T2WidthExtractor.__init__(
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private, blender) self,
localSubrs,
globalSubrs,
nominalWidthX,
defaultWidthX,
private,
blender,
)
self.pen = pen self.pen = pen
self.subrLevel = 0 self.subrLevel = 0
@ -586,17 +672,21 @@ class T2OutlineExtractor(T2WidthExtractor):
def op_rmoveto(self, index): def op_rmoveto(self, index):
self.endPath() self.endPath()
self.rMoveTo(self.popallWidth()) self.rMoveTo(self.popallWidth())
def op_hmoveto(self, index): def op_hmoveto(self, index):
self.endPath() self.endPath()
self.rMoveTo((self.popallWidth(1)[0], 0)) self.rMoveTo((self.popallWidth(1)[0], 0))
def op_vmoveto(self, index): def op_vmoveto(self, index):
self.endPath() self.endPath()
self.rMoveTo((0, self.popallWidth(1)[0])) self.rMoveTo((0, self.popallWidth(1)[0]))
def op_endchar(self, index): def op_endchar(self, index):
self.endPath() self.endPath()
args = self.popallWidth() args = self.popallWidth()
if args: if args:
from fontTools.encodings.StandardEncoding import StandardEncoding from fontTools.encodings.StandardEncoding import StandardEncoding
# endchar can do seac accent bulding; The T2 spec says it's deprecated, # endchar can do seac accent bulding; The T2 spec says it's deprecated,
# but recent software that shall remain nameless does output it. # but recent software that shall remain nameless does output it.
adx, ady, bchar, achar = args adx, ady, bchar, achar = args
@ -616,6 +706,7 @@ class T2OutlineExtractor(T2WidthExtractor):
def op_hlineto(self, index): def op_hlineto(self, index):
self.alternatingLineto(1) self.alternatingLineto(1)
def op_vlineto(self, index): def op_vlineto(self, index):
self.alternatingLineto(0) self.alternatingLineto(0)
@ -626,7 +717,14 @@ class T2OutlineExtractor(T2WidthExtractor):
"""{dxa dya dxb dyb dxc dyc}+ rrcurveto""" """{dxa dya dxb dyb dxc dyc}+ rrcurveto"""
args = self.popall() args = self.popall()
for i in range(0, len(args), 6): for i in range(0, len(args), 6):
dxa, dya, dxb, dyb, dxc, dyc, = args[i:i+6] (
dxa,
dya,
dxb,
dyb,
dxc,
dyc,
) = args[i : i + 6]
self.rCurveTo((dxa, dya), (dxb, dyb), (dxc, dyc)) self.rCurveTo((dxa, dya), (dxb, dyb), (dxc, dyc))
def op_rcurveline(self, index): def op_rcurveline(self, index):
@ -701,10 +799,12 @@ class T2OutlineExtractor(T2WidthExtractor):
dy5 = -dy2 dy5 = -dy2
self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3))
self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6))
def op_flex(self, index): def op_flex(self, index):
dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6, fd = self.popall() dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6, fd = self.popall()
self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3))
self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6))
def op_hflex1(self, index): def op_hflex1(self, index):
dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = self.popall() dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = self.popall()
dy3 = dy4 = 0 dy3 = dy4 = 0
@ -712,6 +812,7 @@ class T2OutlineExtractor(T2WidthExtractor):
self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3))
self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6))
def op_flex1(self, index): def op_flex1(self, index):
dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 = self.popall() dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 = self.popall()
dx = dx1 + dx2 + dx3 + dx4 + dx5 dx = dx1 + dx2 + dx3 + dx4 + dx5
@ -728,18 +829,25 @@ class T2OutlineExtractor(T2WidthExtractor):
# misc # misc
def op_and(self, index): def op_and(self, index):
raise NotImplementedError raise NotImplementedError
def op_or(self, index): def op_or(self, index):
raise NotImplementedError raise NotImplementedError
def op_not(self, index): def op_not(self, index):
raise NotImplementedError raise NotImplementedError
def op_store(self, index): def op_store(self, index):
raise NotImplementedError raise NotImplementedError
def op_abs(self, index): def op_abs(self, index):
raise NotImplementedError raise NotImplementedError
def op_add(self, index): def op_add(self, index):
raise NotImplementedError raise NotImplementedError
def op_sub(self, index): def op_sub(self, index):
raise NotImplementedError raise NotImplementedError
def op_div(self, index): def op_div(self, index):
num2 = self.pop() num2 = self.pop()
num1 = self.pop() num1 = self.pop()
@ -749,32 +857,46 @@ class T2OutlineExtractor(T2WidthExtractor):
self.push(d1) self.push(d1)
else: else:
self.push(d2) self.push(d2)
def op_load(self, index): def op_load(self, index):
raise NotImplementedError raise NotImplementedError
def op_neg(self, index): def op_neg(self, index):
raise NotImplementedError raise NotImplementedError
def op_eq(self, index): def op_eq(self, index):
raise NotImplementedError raise NotImplementedError
def op_drop(self, index): def op_drop(self, index):
raise NotImplementedError raise NotImplementedError
def op_put(self, index): def op_put(self, index):
raise NotImplementedError raise NotImplementedError
def op_get(self, index): def op_get(self, index):
raise NotImplementedError raise NotImplementedError
def op_ifelse(self, index): def op_ifelse(self, index):
raise NotImplementedError raise NotImplementedError
def op_random(self, index): def op_random(self, index):
raise NotImplementedError raise NotImplementedError
def op_mul(self, index): def op_mul(self, index):
raise NotImplementedError raise NotImplementedError
def op_sqrt(self, index): def op_sqrt(self, index):
raise NotImplementedError raise NotImplementedError
def op_dup(self, index): def op_dup(self, index):
raise NotImplementedError raise NotImplementedError
def op_exch(self, index): def op_exch(self, index):
raise NotImplementedError raise NotImplementedError
def op_index(self, index): def op_index(self, index):
raise NotImplementedError raise NotImplementedError
def op_roll(self, index): def op_roll(self, index):
raise NotImplementedError raise NotImplementedError
@ -813,8 +935,8 @@ class T2OutlineExtractor(T2WidthExtractor):
self.rCurveTo((dxa, 0), (dxb, dyb), (dxc, dyc)) self.rCurveTo((dxa, 0), (dxb, dyb), (dxc, dyc))
return args return args
class T1OutlineExtractor(T2OutlineExtractor):
class T1OutlineExtractor(T2OutlineExtractor):
def __init__(self, pen, subrs): def __init__(self, pen, subrs):
self.pen = pen self.pen = pen
self.subrs = subrs self.subrs = subrs
@ -846,6 +968,7 @@ class T1OutlineExtractor(T2OutlineExtractor):
return return
self.endPath() self.endPath()
self.rMoveTo(self.popall()) self.rMoveTo(self.popall())
def op_hmoveto(self, index): def op_hmoveto(self, index):
if self.flexing: if self.flexing:
# We must add a parameter to the stack if we are flexing # We must add a parameter to the stack if we are flexing
@ -853,6 +976,7 @@ class T1OutlineExtractor(T2OutlineExtractor):
return return
self.endPath() self.endPath()
self.rMoveTo((self.popall()[0], 0)) self.rMoveTo((self.popall()[0], 0))
def op_vmoveto(self, index): def op_vmoveto(self, index):
if self.flexing: if self.flexing:
# We must add a parameter to the stack if we are flexing # We must add a parameter to the stack if we are flexing
@ -861,8 +985,10 @@ class T1OutlineExtractor(T2OutlineExtractor):
return return
self.endPath() self.endPath()
self.rMoveTo((0, self.popall()[0])) self.rMoveTo((0, self.popall()[0]))
def op_closepath(self, index): def op_closepath(self, index):
self.closePath() self.closePath()
def op_setcurrentpoint(self, index): def op_setcurrentpoint(self, index):
args = self.popall() args = self.popall()
x, y = args x, y = args
@ -876,6 +1002,7 @@ class T1OutlineExtractor(T2OutlineExtractor):
self.width = wx self.width = wx
self.sbx = sbx self.sbx = sbx
self.currentPoint = sbx, self.currentPoint[1] self.currentPoint = sbx, self.currentPoint[1]
def op_sbw(self, index): def op_sbw(self, index):
self.popall() # XXX self.popall() # XXX
@ -884,6 +1011,7 @@ class T1OutlineExtractor(T2OutlineExtractor):
subrIndex = self.pop() subrIndex = self.pop()
subr = self.subrs[subrIndex] subr = self.subrs[subrIndex]
self.execute(subr) self.execute(subr)
def op_callothersubr(self, index): def op_callothersubr(self, index):
subrIndex = self.pop() subrIndex = self.pop()
nArgs = self.pop() nArgs = self.pop()
@ -894,6 +1022,7 @@ class T1OutlineExtractor(T2OutlineExtractor):
elif subrIndex == 1 and nArgs == 0: elif subrIndex == 1 and nArgs == 0:
self.flexing = 1 self.flexing = 1
# ignore... # ignore...
def op_pop(self, index): def op_pop(self, index):
pass # ignore... pass # ignore...
@ -941,20 +1070,25 @@ class T1OutlineExtractor(T2OutlineExtractor):
def op_dotsection(self, index): def op_dotsection(self, index):
self.popall() # XXX self.popall() # XXX
def op_hstem3(self, index): def op_hstem3(self, index):
self.popall() # XXX self.popall() # XXX
def op_seac(self, index): def op_seac(self, index):
"asb adx ady bchar achar seac" "asb adx ady bchar achar seac"
from fontTools.encodings.StandardEncoding import StandardEncoding from fontTools.encodings.StandardEncoding import StandardEncoding
asb, adx, ady, bchar, achar = self.popall() asb, adx, ady, bchar, achar = self.popall()
baseGlyph = StandardEncoding[bchar] baseGlyph = StandardEncoding[bchar]
self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0))
accentGlyph = StandardEncoding[achar] accentGlyph = StandardEncoding[achar]
adx = adx + self.sbx - asb # seac weirdness adx = adx + self.sbx - asb # seac weirdness
self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady))
def op_vstem3(self, index): def op_vstem3(self, index):
self.popall() # XXX self.popall() # XXX
class T2CharString(object): class T2CharString(object):
operandEncoding = t2OperandEncoding operandEncoding = t2OperandEncoding
@ -973,11 +1107,11 @@ class T2CharString(object):
def getNumRegions(self, vsindex=None): def getNumRegions(self, vsindex=None):
pd = self.private pd = self.private
assert(pd is not None) assert pd is not None
if vsindex is not None: if vsindex is not None:
self._cur_vsindex = vsindex self._cur_vsindex = vsindex
elif self._cur_vsindex is None: elif self._cur_vsindex is None:
self._cur_vsindex = pd.vsindex if hasattr(pd, 'vsindex') else 0 self._cur_vsindex = pd.vsindex if hasattr(pd, "vsindex") else 0
return pd.getNumRegions(self._cur_vsindex) return pd.getNumRegions(self._cur_vsindex)
def __repr__(self): def __repr__(self):
@ -1001,9 +1135,15 @@ class T2CharString(object):
def draw(self, pen, blender=None): def draw(self, pen, blender=None):
subrs = getattr(self.private, "Subrs", []) subrs = getattr(self.private, "Subrs", [])
extractor = self.outlineExtractor(pen, subrs, self.globalSubrs, extractor = self.outlineExtractor(
self.private.nominalWidthX, self.private.defaultWidthX, pen,
self.private, blender) subrs,
self.globalSubrs,
self.private.nominalWidthX,
self.private.defaultWidthX,
self.private,
blender,
)
extractor.execute(self) extractor.execute(self)
self.width = extractor.width self.width = extractor.width
@ -1040,7 +1180,7 @@ class T2CharString(object):
bytecode.extend(bytechr(b) for b in opcodes[token]) bytecode.extend(bytechr(b) for b in opcodes[token])
except KeyError: except KeyError:
raise CharStringCompileError("illegal operator: %s" % token) raise CharStringCompileError("illegal operator: %s" % token)
if token in ('hintmask', 'cntrmask'): if token in ("hintmask", "cntrmask"):
bytecode.append(program[i]) # hint mask bytecode.append(program[i]) # hint mask
i = i + 1 i = i + 1
elif isinstance(token, int): elif isinstance(token, int):
@ -1067,8 +1207,7 @@ class T2CharString(object):
self.bytecode = bytecode self.bytecode = bytecode
self.program = None self.program = None
def getToken(self, index, def getToken(self, index, len=len, byteord=byteord, isinstance=isinstance):
len=len, byteord=byteord, isinstance=isinstance):
if self.bytecode is not None: if self.bytecode is not None:
if index >= len(self.bytecode): if index >= len(self.bytecode):
return None, 0, 0 return None, 0, 0
@ -1100,6 +1239,7 @@ class T2CharString(object):
def toXML(self, xmlWriter, ttFont=None): def toXML(self, xmlWriter, ttFont=None):
from fontTools.misc.textTools import num2binary from fontTools.misc.textTools import num2binary
if self.bytecode is not None: if self.bytecode is not None:
xmlWriter.dumphex(self.bytecode) xmlWriter.dumphex(self.bytecode)
else: else:
@ -1110,15 +1250,15 @@ class T2CharString(object):
if token is None: if token is None:
break break
if isOperator: if isOperator:
if token in ('hintmask', 'cntrmask'): if token in ("hintmask", "cntrmask"):
hintMask, isOperator, index = self.getToken(index) hintMask, isOperator, index = self.getToken(index)
bits = [] bits = []
for byte in hintMask: for byte in hintMask:
bits.append(num2binary(byteord(byte), 8)) bits.append(num2binary(byteord(byte), 8))
hintMask = strjoin(bits) hintMask = strjoin(bits)
line = ' '.join(args + [token, hintMask]) line = " ".join(args + [token, hintMask])
else: else:
line = ' '.join(args + [token]) line = " ".join(args + [token])
xmlWriter.write(line) xmlWriter.write(line)
xmlWriter.newline() xmlWriter.newline()
args = [] args = []
@ -1132,11 +1272,12 @@ class T2CharString(object):
# NOTE: only CFF2 charstrings/subrs can have numeric arguments on # NOTE: only CFF2 charstrings/subrs can have numeric arguments on
# the stack after the last operator. Compiling this would fail if # the stack after the last operator. Compiling this would fail if
# this is part of CFF 1.0 table. # this is part of CFF 1.0 table.
line = ' '.join(args) line = " ".join(args)
xmlWriter.write(line) xmlWriter.write(line)
def fromXML(self, name, attrs, content): def fromXML(self, name, attrs, content):
from fontTools.misc.textTools import binary2num, readHex from fontTools.misc.textTools import binary2num, readHex
if attrs.get("raw"): if attrs.get("raw"):
self.setBytecode(readHex(content)) self.setBytecode(readHex(content))
return return
@ -1155,7 +1296,7 @@ class T2CharString(object):
token = strToFixedToFloat(token, precisionBits=16) token = strToFixedToFloat(token, precisionBits=16)
except ValueError: except ValueError:
program.append(token) program.append(token)
if token in ('hintmask', 'cntrmask'): if token in ("hintmask", "cntrmask"):
mask = content[i] mask = content[i]
maskBytes = b"" maskBytes = b""
for j in range(0, len(mask), 8): for j in range(0, len(mask), 8):
@ -1168,6 +1309,7 @@ class T2CharString(object):
program.append(token) program.append(token)
self.setProgram(program) self.setProgram(program)
class T1CharString(T2CharString): class T1CharString(T2CharString):
operandEncoding = t1OperandEncoding operandEncoding = t1OperandEncoding
@ -1201,6 +1343,7 @@ class T1CharString(T2CharString):
extractor.execute(self) extractor.execute(self)
self.width = extractor.width self.width = extractor.width
class DictDecompiler(object): class DictDecompiler(object):
operandEncoding = cffDictOperandEncoding operandEncoding = cffDictOperandEncoding
@ -1226,6 +1369,7 @@ class DictDecompiler(object):
value, index = handler(self, b0, data, index) value, index = handler(self, b0, data, index)
if value is not None: if value is not None:
push(value) push(value)
def pop(self): def pop(self):
value = self.stack[-1] value = self.stack[-1]
del self.stack[-1] del self.stack[-1]
@ -1270,8 +1414,10 @@ class DictDecompiler(object):
def arg_SID(self, name): def arg_SID(self, name):
return self.strings[self.pop()] return self.strings[self.pop()]
def arg_array(self, name): def arg_array(self, name):
return self.popall() return self.popall()
def arg_blendList(self, name): def arg_blendList(self, name):
""" """
There may be non-blend args at the top of the stack. We first calculate There may be non-blend args at the top of the stack. We first calculate
@ -1284,13 +1430,15 @@ class DictDecompiler(object):
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. 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) vsindex = self.dict.get("vsindex", 0)
numMasters = self.parent.getNumRegions(vsindex) + 1 # only a PrivateDict has blended ops. numMasters = (
self.parent.getNumRegions(vsindex) + 1
) # only a PrivateDict has blended ops.
numBlends = self.pop() numBlends = self.pop()
args = self.popall() args = self.popall()
numArgs = len(args) numArgs = len(args)
# The spec says that there should be no non-blended Blue Values,. # The spec says that there should be no non-blended Blue Values,.
assert(numArgs == numMasters * numBlends) assert numArgs == numMasters * numBlends
value = [None] * numBlends value = [None] * numBlends
numDeltas = numMasters - 1 numDeltas = numMasters - 1
i = 0 i = 0

View File

@ -24,7 +24,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
ps_special = b'()<>[]{}%' # / is one too, but we take care of that one differently ps_special = b"()<>[]{}%" # / is one too, but we take care of that one differently
skipwhiteRE = re.compile(bytesjoin([b"[", whitespace, b"]*"])) skipwhiteRE = re.compile(bytesjoin([b"[", whitespace, b"]*"]))
endofthingPat = bytesjoin([b"[^][(){}<>/%", whitespace, b"]*"]) endofthingPat = bytesjoin([b"[^][(){}<>/%", whitespace, b"]*"])
@ -32,7 +32,7 @@ endofthingRE = re.compile(endofthingPat)
commentRE = re.compile(b"%[^\n\r]*") commentRE = re.compile(b"%[^\n\r]*")
# XXX This not entirely correct as it doesn't allow *nested* embedded parens: # XXX This not entirely correct as it doesn't allow *nested* embedded parens:
stringPat = br""" stringPat = rb"""
\( \(
( (
( (
@ -51,13 +51,17 @@ stringRE = re.compile(stringPat)
hexstringRE = re.compile(bytesjoin([b"<[", whitespace, b"0-9A-Fa-f]*>"])) hexstringRE = re.compile(bytesjoin([b"<[", whitespace, b"0-9A-Fa-f]*>"]))
class PSTokenError(Exception): pass
class PSError(Exception): pass class PSTokenError(Exception):
pass
class PSError(Exception):
pass
class PSTokenizer(object): class PSTokenizer(object):
def __init__(self, buf=b"", encoding="ascii"):
def __init__(self, buf=b'', encoding="ascii"):
# Force self.buf to be a byte string # Force self.buf to be a byte string
buf = tobytes(buf) buf = tobytes(buf)
self.buf = buf self.buf = buf
@ -86,14 +90,16 @@ class PSTokenizer(object):
self.closed = True self.closed = True
del self.buf, self.pos del self.buf, self.pos
def getnexttoken(self, def getnexttoken(
self,
# localize some stuff, for performance # localize some stuff, for performance
len=len, len=len,
ps_special=ps_special, ps_special=ps_special,
stringmatch=stringRE.match, stringmatch=stringRE.match,
hexstringmatch=hexstringRE.match, hexstringmatch=hexstringRE.match,
commentmatch=commentRE.match, commentmatch=commentRE.match,
endmatch=endofthingRE.match): endmatch=endofthingRE.match,
):
self.skipwhite() self.skipwhite()
if self.pos >= self.len: if self.pos >= self.len:
@ -102,38 +108,38 @@ class PSTokenizer(object):
buf = self.buf buf = self.buf
char = bytechr(byteord(buf[pos])) char = bytechr(byteord(buf[pos]))
if char in ps_special: if char in ps_special:
if char in b'{}[]': if char in b"{}[]":
tokentype = 'do_special' tokentype = "do_special"
token = char token = char
elif char == b'%': elif char == b"%":
tokentype = 'do_comment' tokentype = "do_comment"
_, nextpos = commentmatch(buf, pos).span() _, nextpos = commentmatch(buf, pos).span()
token = buf[pos:nextpos] token = buf[pos:nextpos]
elif char == b'(': elif char == b"(":
tokentype = 'do_string' tokentype = "do_string"
m = stringmatch(buf, pos) m = stringmatch(buf, pos)
if m is None: if m is None:
raise PSTokenError('bad string at character %d' % pos) raise PSTokenError("bad string at character %d" % pos)
_, nextpos = m.span() _, nextpos = m.span()
token = buf[pos:nextpos] token = buf[pos:nextpos]
elif char == b'<': elif char == b"<":
tokentype = 'do_hexstring' tokentype = "do_hexstring"
m = hexstringmatch(buf, pos) m = hexstringmatch(buf, pos)
if m is None: if m is None:
raise PSTokenError('bad hexstring at character %d' % pos) raise PSTokenError("bad hexstring at character %d" % pos)
_, nextpos = m.span() _, nextpos = m.span()
token = buf[pos:nextpos] token = buf[pos:nextpos]
else: else:
raise PSTokenError('bad token at character %d' % pos) raise PSTokenError("bad token at character %d" % pos)
else: else:
if char == b'/': if char == b"/":
tokentype = 'do_literal' tokentype = "do_literal"
m = endmatch(buf, pos + 1) m = endmatch(buf, pos + 1)
else: else:
tokentype = '' tokentype = ""
m = endmatch(buf, pos) m = endmatch(buf, pos)
if m is None: if m is None:
raise PSTokenError('bad token at character %d' % pos) raise PSTokenError("bad token at character %d" % pos)
_, nextpos = m.span() _, nextpos = m.span()
token = buf[pos:nextpos] token = buf[pos:nextpos]
self.pos = pos + len(token) self.pos = pos + len(token)
@ -152,14 +158,13 @@ class PSTokenizer(object):
self.pos = 4 self.pos = 4
def stopeexec(self): def stopeexec(self):
if not hasattr(self, 'dirtybuf'): if not hasattr(self, "dirtybuf"):
return return
self.buf = self.dirtybuf self.buf = self.dirtybuf
del self.dirtybuf del self.dirtybuf
class PSInterpreter(PSOperators): class PSInterpreter(PSOperators):
def __init__(self, encoding="ascii"): def __init__(self, encoding="ascii"):
systemdict = {} systemdict = {}
userdict = {} userdict = {}
@ -172,18 +177,18 @@ class PSInterpreter(PSOperators):
def fillsystemdict(self): def fillsystemdict(self):
systemdict = self.dictstack[0] systemdict = self.dictstack[0]
systemdict['['] = systemdict['mark'] = self.mark = ps_mark() systemdict["["] = systemdict["mark"] = self.mark = ps_mark()
systemdict[']'] = ps_operator(']', self.do_makearray) systemdict["]"] = ps_operator("]", self.do_makearray)
systemdict['true'] = ps_boolean(1) systemdict["true"] = ps_boolean(1)
systemdict['false'] = ps_boolean(0) systemdict["false"] = ps_boolean(0)
systemdict['StandardEncoding'] = ps_array(ps_StandardEncoding) systemdict["StandardEncoding"] = ps_array(ps_StandardEncoding)
systemdict['FontDirectory'] = ps_dict({}) systemdict["FontDirectory"] = ps_dict({})
self.suckoperators(systemdict, self.__class__) self.suckoperators(systemdict, self.__class__)
def suckoperators(self, systemdict, klass): def suckoperators(self, systemdict, klass):
for name in dir(klass): for name in dir(klass):
attr = getattr(self, name) attr = getattr(self, name)
if isinstance(attr, Callable) and name[:3] == 'ps_': if isinstance(attr, Callable) and name[:3] == "ps_":
name = name[3:] name = name[3:]
systemdict[name] = ps_operator(name, attr) systemdict[name] = ps_operator(name, attr)
for baseclass in klass.__bases__: for baseclass in klass.__bases__:
@ -211,24 +216,25 @@ class PSInterpreter(PSOperators):
except: except:
if self.tokenizer is not None: if self.tokenizer is not None:
log.debug( log.debug(
'ps error:\n' "ps error:\n"
'- - - - - - -\n' "- - - - - - -\n"
'%s\n' "%s\n"
'>>>\n' ">>>\n"
'%s\n' "%s\n"
'- - - - - - -', "- - - - - - -",
self.tokenizer.buf[self.tokenizer.pos - 50 : self.tokenizer.pos], self.tokenizer.buf[self.tokenizer.pos - 50 : self.tokenizer.pos],
self.tokenizer.buf[self.tokenizer.pos:self.tokenizer.pos+50]) self.tokenizer.buf[self.tokenizer.pos : self.tokenizer.pos + 50],
)
raise raise
def handle_object(self, object): def handle_object(self, object):
if not (self.proclevel or object.literal or object.type == 'proceduretype'): if not (self.proclevel or object.literal or object.type == "proceduretype"):
if object.type != 'operatortype': if object.type != "operatortype":
object = self.resolve_name(object.value) object = self.resolve_name(object.value)
if object.literal: if object.literal:
self.push(object) self.push(object)
else: else:
if object.type == 'proceduretype': if object.type == "proceduretype":
self.call_procedure(object) self.call_procedure(object)
else: else:
object.function() object.function()
@ -245,22 +251,25 @@ class PSInterpreter(PSOperators):
for i in range(len(dictstack) - 1, -1, -1): for i in range(len(dictstack) - 1, -1, -1):
if name in dictstack[i]: if name in dictstack[i]:
return dictstack[i][name] return dictstack[i][name]
raise PSError('name error: ' + str(name)) raise PSError("name error: " + str(name))
def do_token(self, token, def do_token(
self,
token,
int=int, int=int,
float=float, float=float,
ps_name=ps_name, ps_name=ps_name,
ps_integer=ps_integer, ps_integer=ps_integer,
ps_real=ps_real): ps_real=ps_real,
):
try: try:
num = int(token) num = int(token)
except (ValueError, OverflowError): except (ValueError, OverflowError):
try: try:
num = float(token) num = float(token)
except (ValueError, OverflowError): except (ValueError, OverflowError):
if '#' in token: if "#" in token:
hashpos = token.find('#') hashpos = token.find("#")
try: try:
base = int(token[:hashpos]) base = int(token[:hashpos])
num = int(token[hashpos + 1 :], base) num = int(token[hashpos + 1 :], base)
@ -287,7 +296,7 @@ class PSInterpreter(PSOperators):
def do_hexstring(self, token): def do_hexstring(self, token):
hexStr = "".join(token[1:-1].split()) hexStr = "".join(token[1:-1].split())
if len(hexStr) % 2: if len(hexStr) % 2:
hexStr = hexStr + '0' hexStr = hexStr + "0"
cleanstr = [] cleanstr = []
for i in range(0, len(hexStr), 2): for i in range(0, len(hexStr), 2):
cleanstr.append(chr(int(hexStr[i : i + 2], 16))) cleanstr.append(chr(int(hexStr[i : i + 2], 16)))
@ -295,10 +304,10 @@ class PSInterpreter(PSOperators):
return ps_string(cleanstr) return ps_string(cleanstr)
def do_special(self, token): def do_special(self, token):
if token == '{': if token == "{":
self.proclevel = self.proclevel + 1 self.proclevel = self.proclevel + 1
return self.procmark return self.procmark
elif token == '}': elif token == "}":
proc = [] proc = []
while 1: while 1:
topobject = self.pop() topobject = self.pop()
@ -308,12 +317,12 @@ class PSInterpreter(PSOperators):
self.proclevel = self.proclevel - 1 self.proclevel = self.proclevel - 1
proc.reverse() proc.reverse()
return ps_procedure(proc) return ps_procedure(proc)
elif token == '[': elif token == "[":
return self.mark return self.mark
elif token == ']': elif token == "]":
return ps_name(']') return ps_name("]")
else: else:
raise PSTokenError('huh?') raise PSTokenError("huh?")
def push(self, object): def push(self, object):
self.stack.append(object) self.stack.append(object)
@ -321,11 +330,13 @@ class PSInterpreter(PSOperators):
def pop(self, *types): def pop(self, *types):
stack = self.stack stack = self.stack
if not stack: if not stack:
raise PSError('stack underflow') raise PSError("stack underflow")
object = stack[-1] object = stack[-1]
if types: if types:
if object.type not in types: if object.type not in types:
raise PSError('typecheck, expected %s, found %s' % (repr(types), object.type)) raise PSError(
"typecheck, expected %s, found %s" % (repr(types), object.type)
)
del stack[-1] del stack[-1]
return object return object
@ -355,23 +366,26 @@ def unpack_item(item):
newitem = [None] * len(item.value) newitem = [None] * len(item.value)
for i in range(len(item.value)): for i in range(len(item.value)):
newitem[i] = unpack_item(item.value[i]) newitem[i] = unpack_item(item.value[i])
if item.type == 'proceduretype': if item.type == "proceduretype":
newitem = tuple(newitem) newitem = tuple(newitem)
else: else:
newitem = item.value newitem = item.value
return newitem return newitem
def suckfont(data, encoding="ascii"): def suckfont(data, encoding="ascii"):
m = re.search(br"/FontName\s+/([^ \t\n\r]+)\s+def", data) m = re.search(rb"/FontName\s+/([^ \t\n\r]+)\s+def", data)
if m: if m:
fontName = m.group(1) fontName = m.group(1)
fontName = fontName.decode() fontName = fontName.decode()
else: else:
fontName = None fontName = None
interpreter = PSInterpreter(encoding=encoding) interpreter = PSInterpreter(encoding=encoding)
interpreter.interpret(b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop") interpreter.interpret(
b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop"
)
interpreter.interpret(data) interpreter.interpret(data)
fontdir = interpreter.dictstack[0]['FontDirectory'].value fontdir = interpreter.dictstack[0]["FontDirectory"].value
if fontName in fontdir: if fontName in fontdir:
rawfont = fontdir[fontName] rawfont = fontdir[fontName]
else: else:

View File

@ -23,50 +23,60 @@ class ps_operator(ps_object):
self.name = name self.name = name
self.function = function self.function = function
self.type = self.__class__.__name__[3:] + "type" self.type = self.__class__.__name__[3:] + "type"
def __repr__(self): def __repr__(self):
return "<operator %s>" % self.name return "<operator %s>" % self.name
class ps_procedure(ps_object): class ps_procedure(ps_object):
literal = 0 literal = 0
def __repr__(self): def __repr__(self):
return "<procedure>" return "<procedure>"
def __str__(self): def __str__(self):
psstring = '{' psstring = "{"
for i in range(len(self.value)): for i in range(len(self.value)):
if i: if i:
psstring = psstring + ' ' + str(self.value[i]) psstring = psstring + " " + str(self.value[i])
else: else:
psstring = psstring + str(self.value[i]) psstring = psstring + str(self.value[i])
return psstring + '}' return psstring + "}"
class ps_name(ps_object): class ps_name(ps_object):
literal = 0 literal = 0
def __str__(self): def __str__(self):
if self.literal: if self.literal:
return '/' + self.value return "/" + self.value
else: else:
return self.value return self.value
class ps_literal(ps_object): class ps_literal(ps_object):
def __str__(self): def __str__(self):
return '/' + self.value return "/" + self.value
class ps_array(ps_object): class ps_array(ps_object):
def __str__(self): def __str__(self):
psstring = '[' psstring = "["
for i in range(len(self.value)): for i in range(len(self.value)):
item = self.value[i] item = self.value[i]
access = _accessstrings[item.access] access = _accessstrings[item.access]
if access: if access:
access = ' ' + access access = " " + access
if i: if i:
psstring = psstring + ' ' + str(item) + access psstring = psstring + " " + str(item) + access
else: else:
psstring = psstring + str(item) + access psstring = psstring + str(item) + access
return psstring + ']' return psstring + "]"
def __repr__(self): def __repr__(self):
return "<array>" return "<array>"
_type1_pre_eexec_order = [ _type1_pre_eexec_order = [
"FontInfo", "FontInfo",
"FontName", "FontName",
@ -77,7 +87,7 @@ _type1_pre_eexec_order = [
"FontBBox", "FontBBox",
"UniqueID", "UniqueID",
"Metrics", "Metrics",
"StrokeWidth" "StrokeWidth",
] ]
_type1_fontinfo_order = [ _type1_fontinfo_order = [
@ -89,40 +99,43 @@ _type1_fontinfo_order = [
"ItalicAngle", "ItalicAngle",
"isFixedPitch", "isFixedPitch",
"UnderlinePosition", "UnderlinePosition",
"UnderlineThickness" "UnderlineThickness",
] ]
_type1_post_eexec_order = [ _type1_post_eexec_order = ["Private", "CharStrings", "FID"]
"Private",
"CharStrings",
"FID"
]
def _type1_item_repr(key, value): def _type1_item_repr(key, value):
psstring = "" psstring = ""
access = _accessstrings[value.access] access = _accessstrings[value.access]
if access: if access:
access = access + ' ' access = access + " "
if key == 'CharStrings': if key == "CharStrings":
psstring = psstring + "/%s %s def\n" % (key, _type1_CharString_repr(value.value)) psstring = psstring + "/%s %s def\n" % (
elif key == 'Encoding': key,
_type1_CharString_repr(value.value),
)
elif key == "Encoding":
psstring = psstring + _type1_Encoding_repr(value, access) psstring = psstring + _type1_Encoding_repr(value, access)
else: else:
psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access)
return psstring return psstring
def _type1_Encoding_repr(encoding, access): def _type1_Encoding_repr(encoding, access):
encoding = encoding.value encoding = encoding.value
psstring = "/Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n" psstring = "/Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n"
for i in range(256): for i in range(256):
name = encoding[i].value name = encoding[i].value
if name != '.notdef': if name != ".notdef":
psstring = psstring + "dup %d /%s put\n" % (i, name) psstring = psstring + "dup %d /%s put\n" % (i, name)
return psstring + access + "def\n" return psstring + access + "def\n"
def _type1_CharString_repr(charstrings): def _type1_CharString_repr(charstrings):
items = sorted(charstrings.items()) items = sorted(charstrings.items())
return 'xxx' return "xxx"
class ps_font(ps_object): class ps_font(ps_object):
def __str__(self): def __str__(self):
@ -146,14 +159,22 @@ class ps_font(ps_object):
pass pass
else: else:
psstring = psstring + _type1_item_repr(key, value) psstring = psstring + _type1_item_repr(key, value)
return psstring + 'dup/FontName get exch definefont pop\nmark currentfile closefile\n' + \ return (
8 * (64 * '0' + '\n') + 'cleartomark' + '\n' psstring
+ "dup/FontName get exch definefont pop\nmark currentfile closefile\n"
+ 8 * (64 * "0" + "\n")
+ "cleartomark"
+ "\n"
)
def __repr__(self): def __repr__(self):
return '<font>' return "<font>"
class ps_file(ps_object): class ps_file(ps_object):
pass pass
class ps_dict(ps_object): class ps_dict(ps_object):
def __str__(self): def __str__(self):
psstring = "%d dict dup begin\n" % len(self.value) psstring = "%d dict dup begin\n" % len(self.value)
@ -161,62 +182,69 @@ class ps_dict(ps_object):
for key, value in items: for key, value in items:
access = _accessstrings[value.access] access = _accessstrings[value.access]
if access: if access:
access = access + ' ' access = access + " "
psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access)
return psstring + 'end ' return psstring + "end "
def __repr__(self): def __repr__(self):
return "<dict>" return "<dict>"
class ps_mark(ps_object): class ps_mark(ps_object):
def __init__(self): def __init__(self):
self.value = 'mark' self.value = "mark"
self.type = self.__class__.__name__[3:] + "type" self.type = self.__class__.__name__[3:] + "type"
class ps_procmark(ps_object): class ps_procmark(ps_object):
def __init__(self): def __init__(self):
self.value = 'procmark' self.value = "procmark"
self.type = self.__class__.__name__[3:] + "type" self.type = self.__class__.__name__[3:] + "type"
class ps_null(ps_object): class ps_null(ps_object):
def __init__(self): def __init__(self):
self.type = self.__class__.__name__[3:] + "type" self.type = self.__class__.__name__[3:] + "type"
class ps_boolean(ps_object): class ps_boolean(ps_object):
def __str__(self): def __str__(self):
if self.value: if self.value:
return 'true' return "true"
else: else:
return 'false' return "false"
class ps_string(ps_object): class ps_string(ps_object):
def __str__(self): def __str__(self):
return "(%s)" % repr(self.value)[1:-1] return "(%s)" % repr(self.value)[1:-1]
class ps_integer(ps_object): class ps_integer(ps_object):
def __str__(self): def __str__(self):
return repr(self.value) return repr(self.value)
class ps_real(ps_object): class ps_real(ps_object):
def __str__(self): def __str__(self):
return repr(self.value) return repr(self.value)
class PSOperators(object): class PSOperators(object):
def ps_def(self): def ps_def(self):
obj = self.pop() obj = self.pop()
name = self.pop() name = self.pop()
self.dictstack[-1][name.value] = obj self.dictstack[-1][name.value] = obj
def ps_bind(self): def ps_bind(self):
proc = self.pop('proceduretype') proc = self.pop("proceduretype")
self.proc_bind(proc) self.proc_bind(proc)
self.push(proc) self.push(proc)
def proc_bind(self, proc): def proc_bind(self, proc):
for i in range(len(proc.value)): for i in range(len(proc.value)):
item = proc.value[i] item = proc.value[i]
if item.type == 'proceduretype': if item.type == "proceduretype":
self.proc_bind(item) self.proc_bind(item)
else: else:
if not item.literal: if not item.literal:
@ -225,12 +253,12 @@ class PSOperators(object):
except: except:
pass pass
else: else:
if obj.type == 'operatortype': if obj.type == "operatortype":
proc.value[i] = obj proc.value[i] = obj
def ps_exch(self): def ps_exch(self):
if len(self.stack) < 2: if len(self.stack) < 2:
raise RuntimeError('stack underflow') raise RuntimeError("stack underflow")
obj1 = self.pop() obj1 = self.pop()
obj2 = self.pop() obj2 = self.pop()
self.push(obj1) self.push(obj1)
@ -238,12 +266,12 @@ class PSOperators(object):
def ps_dup(self): def ps_dup(self):
if not self.stack: if not self.stack:
raise RuntimeError('stack underflow') raise RuntimeError("stack underflow")
self.push(self.stack[-1]) self.push(self.stack[-1])
def ps_exec(self): def ps_exec(self):
obj = self.pop() obj = self.pop()
if obj.type == 'proceduretype': if obj.type == "proceduretype":
self.call_procedure(obj) self.call_procedure(obj)
else: else:
self.handle_object(obj) self.handle_object(obj)
@ -267,12 +295,19 @@ class PSOperators(object):
self.push(obj) self.push(obj)
def ps_matrix(self): def ps_matrix(self):
matrix = [ps_real(1.0), ps_integer(0), ps_integer(0), ps_real(1.0), ps_integer(0), ps_integer(0)] matrix = [
ps_real(1.0),
ps_integer(0),
ps_integer(0),
ps_real(1.0),
ps_integer(0),
ps_integer(0),
]
self.push(ps_array(matrix)) self.push(ps_array(matrix))
def ps_string(self): def ps_string(self):
num = self.pop('integertype').value num = self.pop("integertype").value
self.push(ps_string('\0' * num)) self.push(ps_string("\0" * num))
def ps_type(self): def ps_type(self):
obj = self.pop() obj = self.pop()
@ -306,11 +341,11 @@ class PSOperators(object):
self.push(ps_file(self.tokenizer)) self.push(ps_file(self.tokenizer))
def ps_eexec(self): def ps_eexec(self):
f = self.pop('filetype').value f = self.pop("filetype").value
f.starteexec() f.starteexec()
def ps_closefile(self): def ps_closefile(self):
f = self.pop('filetype').value f = self.pop("filetype").value
f.skipwhite() f.skipwhite()
f.stopeexec() f.stopeexec()
@ -319,12 +354,10 @@ class PSOperators(object):
while obj != self.mark: while obj != self.mark:
obj = self.pop() obj = self.pop()
def ps_readstring(self, def ps_readstring(self, ps_boolean=ps_boolean, len=len):
ps_boolean=ps_boolean, s = self.pop("stringtype")
len=len):
s = self.pop('stringtype')
oldstr = s.value oldstr = s.value
f = self.pop('filetype') f = self.pop("filetype")
# pad = file.value.read(1) # pad = file.value.read(1)
# for StringIO, this is faster # for StringIO, this is faster
f.value.pos = f.value.pos + 1 f.value.pos = f.value.pos + 1
@ -335,18 +368,18 @@ class PSOperators(object):
def ps_known(self): def ps_known(self):
key = self.pop() key = self.pop()
d = self.pop('dicttype', 'fonttype') d = self.pop("dicttype", "fonttype")
self.push(ps_boolean(key.value in d.value)) self.push(ps_boolean(key.value in d.value))
def ps_if(self): def ps_if(self):
proc = self.pop('proceduretype') proc = self.pop("proceduretype")
if self.pop('booleantype').value: if self.pop("booleantype").value:
self.call_procedure(proc) self.call_procedure(proc)
def ps_ifelse(self): def ps_ifelse(self):
proc2 = self.pop('proceduretype') proc2 = self.pop("proceduretype")
proc1 = self.pop('proceduretype') proc1 = self.pop("proceduretype")
if self.pop('booleantype').value: if self.pop("booleantype").value:
self.call_procedure(proc1) self.call_procedure(proc1)
else: else:
self.call_procedure(proc2) self.call_procedure(proc2)
@ -370,19 +403,19 @@ class PSOperators(object):
self.push(obj) self.push(obj)
def ps_not(self): def ps_not(self):
obj = self.pop('booleantype', 'integertype') obj = self.pop("booleantype", "integertype")
if obj.type == 'booleantype': if obj.type == "booleantype":
self.push(ps_boolean(not obj.value)) self.push(ps_boolean(not obj.value))
else: else:
self.push(ps_integer(~obj.value)) self.push(ps_integer(~obj.value))
def ps_print(self): def ps_print(self):
str = self.pop('stringtype') str = self.pop("stringtype")
print('PS output --->', str.value) print("PS output --->", str.value)
def ps_anchorsearch(self): def ps_anchorsearch(self):
seek = self.pop('stringtype') seek = self.pop("stringtype")
s = self.pop('stringtype') s = self.pop("stringtype")
seeklen = len(seek.value) seeklen = len(seek.value)
if s.value[:seeklen] == seek.value: if s.value[:seeklen] == seek.value:
self.push(ps_string(s.value[seeklen:])) self.push(ps_string(s.value[seeklen:]))
@ -393,12 +426,12 @@ class PSOperators(object):
self.push(ps_boolean(0)) self.push(ps_boolean(0))
def ps_array(self): def ps_array(self):
num = self.pop('integertype') num = self.pop("integertype")
array = ps_array([None] * num.value) array = ps_array([None] * num.value)
self.push(array) self.push(array)
def ps_astore(self): def ps_astore(self):
array = self.pop('arraytype') array = self.pop("arraytype")
for i in range(len(array.value) - 1, -1, -1): for i in range(len(array.value) - 1, -1, -1):
array.value[i] = self.pop() array.value[i] = self.pop()
self.push(array) self.push(array)
@ -410,13 +443,13 @@ class PSOperators(object):
def ps_put(self): def ps_put(self):
obj1 = self.pop() obj1 = self.pop()
obj2 = self.pop() obj2 = self.pop()
obj3 = self.pop('arraytype', 'dicttype', 'stringtype', 'proceduretype') obj3 = self.pop("arraytype", "dicttype", "stringtype", "proceduretype")
tp = obj3.type tp = obj3.type
if tp == 'arraytype' or tp == 'proceduretype': if tp == "arraytype" or tp == "proceduretype":
obj3.value[obj2.value] = obj1 obj3.value[obj2.value] = obj1
elif tp == 'dicttype': elif tp == "dicttype":
obj3.value[obj2.value] = obj1 obj3.value[obj2.value] = obj1
elif tp == 'stringtype': elif tp == "stringtype":
index = obj2.value index = obj2.value
obj3.value = obj3.value[:index] + chr(obj1.value) + obj3.value[index + 1 :] obj3.value = obj3.value[:index] + chr(obj1.value) + obj3.value[index + 1 :]
@ -424,54 +457,56 @@ class PSOperators(object):
obj1 = self.pop() obj1 = self.pop()
if obj1.value == "Encoding": if obj1.value == "Encoding":
pass pass
obj2 = self.pop('arraytype', 'dicttype', 'stringtype', 'proceduretype', 'fonttype') obj2 = self.pop(
"arraytype", "dicttype", "stringtype", "proceduretype", "fonttype"
)
tp = obj2.type tp = obj2.type
if tp in ('arraytype', 'proceduretype'): if tp in ("arraytype", "proceduretype"):
self.push(obj2.value[obj1.value]) self.push(obj2.value[obj1.value])
elif tp in ('dicttype', 'fonttype'): elif tp in ("dicttype", "fonttype"):
self.push(obj2.value[obj1.value]) self.push(obj2.value[obj1.value])
elif tp == 'stringtype': elif tp == "stringtype":
self.push(ps_integer(ord(obj2.value[obj1.value]))) self.push(ps_integer(ord(obj2.value[obj1.value])))
else: else:
assert False, "shouldn't get here" assert False, "shouldn't get here"
def ps_getinterval(self): def ps_getinterval(self):
obj1 = self.pop('integertype') obj1 = self.pop("integertype")
obj2 = self.pop('integertype') obj2 = self.pop("integertype")
obj3 = self.pop('arraytype', 'stringtype') obj3 = self.pop("arraytype", "stringtype")
tp = obj3.type tp = obj3.type
if tp == 'arraytype': if tp == "arraytype":
self.push(ps_array(obj3.value[obj2.value : obj2.value + obj1.value])) self.push(ps_array(obj3.value[obj2.value : obj2.value + obj1.value]))
elif tp == 'stringtype': elif tp == "stringtype":
self.push(ps_string(obj3.value[obj2.value : obj2.value + obj1.value])) self.push(ps_string(obj3.value[obj2.value : obj2.value + obj1.value]))
def ps_putinterval(self): def ps_putinterval(self):
obj1 = self.pop('arraytype', 'stringtype') obj1 = self.pop("arraytype", "stringtype")
obj2 = self.pop('integertype') obj2 = self.pop("integertype")
obj3 = self.pop('arraytype', 'stringtype') obj3 = self.pop("arraytype", "stringtype")
tp = obj3.type tp = obj3.type
if tp == 'arraytype': if tp == "arraytype":
obj3.value[obj2.value : obj2.value + len(obj1.value)] = obj1.value obj3.value[obj2.value : obj2.value + len(obj1.value)] = obj1.value
elif tp == 'stringtype': elif tp == "stringtype":
newstr = obj3.value[: obj2.value] newstr = obj3.value[: obj2.value]
newstr = newstr + obj1.value newstr = newstr + obj1.value
newstr = newstr + obj3.value[obj2.value + len(obj1.value) :] newstr = newstr + obj3.value[obj2.value + len(obj1.value) :]
obj3.value = newstr obj3.value = newstr
def ps_cvn(self): def ps_cvn(self):
self.push(ps_name(self.pop('stringtype').value)) self.push(ps_name(self.pop("stringtype").value))
def ps_index(self): def ps_index(self):
n = self.pop('integertype').value n = self.pop("integertype").value
if n < 0: if n < 0:
raise RuntimeError('index may not be negative') raise RuntimeError("index may not be negative")
self.push(self.stack[-1 - n]) self.push(self.stack[-1 - n])
def ps_for(self): def ps_for(self):
proc = self.pop('proceduretype') proc = self.pop("proceduretype")
limit = self.pop('integertype', 'realtype').value limit = self.pop("integertype", "realtype").value
increment = self.pop('integertype', 'realtype').value increment = self.pop("integertype", "realtype").value
i = self.pop('integertype', 'realtype').value i = self.pop("integertype", "realtype").value
while 1: while 1:
if increment > 0: if increment > 0:
if i > limit: if i > limit:
@ -487,51 +522,53 @@ class PSOperators(object):
i = i + increment i = i + increment
def ps_forall(self): def ps_forall(self):
proc = self.pop('proceduretype') proc = self.pop("proceduretype")
obj = self.pop('arraytype', 'stringtype', 'dicttype') obj = self.pop("arraytype", "stringtype", "dicttype")
tp = obj.type tp = obj.type
if tp == 'arraytype': if tp == "arraytype":
for item in obj.value: for item in obj.value:
self.push(item) self.push(item)
self.call_procedure(proc) self.call_procedure(proc)
elif tp == 'stringtype': elif tp == "stringtype":
for item in obj.value: for item in obj.value:
self.push(ps_integer(ord(item))) self.push(ps_integer(ord(item)))
self.call_procedure(proc) self.call_procedure(proc)
elif tp == 'dicttype': elif tp == "dicttype":
for key, value in obj.value.items(): for key, value in obj.value.items():
self.push(ps_name(key)) self.push(ps_name(key))
self.push(value) self.push(value)
self.call_procedure(proc) self.call_procedure(proc)
def ps_definefont(self): def ps_definefont(self):
font = self.pop('dicttype') font = self.pop("dicttype")
name = self.pop() name = self.pop()
font = ps_font(font.value) font = ps_font(font.value)
self.dictstack[0]['FontDirectory'].value[name.value] = font self.dictstack[0]["FontDirectory"].value[name.value] = font
self.push(font) self.push(font)
def ps_findfont(self): def ps_findfont(self):
name = self.pop() name = self.pop()
font = self.dictstack[0]['FontDirectory'].value[name.value] font = self.dictstack[0]["FontDirectory"].value[name.value]
self.push(font) self.push(font)
def ps_pop(self): def ps_pop(self):
self.pop() self.pop()
def ps_dict(self): def ps_dict(self):
self.pop('integertype') self.pop("integertype")
self.push(ps_dict({})) self.push(ps_dict({}))
def ps_begin(self): def ps_begin(self):
self.dictstack.append(self.pop('dicttype').value) self.dictstack.append(self.pop("dicttype").value)
def ps_end(self): def ps_end(self):
if len(self.dictstack) > 2: if len(self.dictstack) > 2:
del self.dictstack[-1] del self.dictstack[-1]
else: else:
raise RuntimeError('dictstack underflow') raise RuntimeError("dictstack underflow")
notdef = '.notdef'
notdef = ".notdef"
from fontTools.encodings.StandardEncoding import StandardEncoding from fontTools.encodings.StandardEncoding import StandardEncoding
ps_StandardEncoding = list(map(ps_name, StandardEncoding)) ps_StandardEncoding = list(map(ps_name, StandardEncoding))

View File

@ -15,9 +15,11 @@ __all__ = [
"roundFunc", "roundFunc",
] ]
def noRound(value): def noRound(value):
return value return value
def otRound(value): def otRound(value):
"""Round float value to nearest integer towards ``+Infinity``. """Round float value to nearest integer towards ``+Infinity``.
@ -41,10 +43,12 @@ def otRound(value):
# https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 # https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166
return int(math.floor(value + 0.5)) return int(math.floor(value + 0.5))
def maybeRound(v, tolerance, round=otRound): def maybeRound(v, tolerance, round=otRound):
rounded = round(v) rounded = round(v)
return rounded if abs(rounded - v) <= tolerance else v return rounded if abs(rounded - v) <= tolerance else v
def roundFunc(tolerance, round=otRound): def roundFunc(tolerance, round=otRound):
if tolerance < 0: if tolerance < 0:
raise ValueError("Rounding tolerance must be positive") raise ValueError("Rounding tolerance must be positive")
@ -52,7 +56,7 @@ def roundFunc(tolerance, round=otRound):
if tolerance == 0: if tolerance == 0:
return noRound return noRound
if tolerance >= .5: if tolerance >= 0.5:
return round return round
return functools.partial(maybeRound, tolerance=tolerance, round=round) return functools.partial(maybeRound, tolerance=tolerance, round=round)
@ -85,7 +89,7 @@ def nearestMultipleShortestRepr(value: float, factor: float) -> str:
return "0.0" return "0.0"
value = otRound(value / factor) * factor value = otRound(value / factor) * factor
eps = .5 * factor eps = 0.5 * factor
lo = value - eps lo = value - eps
hi = value + eps hi = value + eps
# If the range of valid choices spans an integer, return the integer. # If the range of valid choices spans an integer, return the integer.
@ -99,7 +103,7 @@ def nearestMultipleShortestRepr(value: float, factor: float) -> str:
for i in range(len(lo)): for i in range(len(lo)):
if lo[i] != hi[i]: if lo[i] != hi[i]:
break break
period = lo.find('.') period = lo.find(".")
assert period < i assert period < i
fmt = "%%.%df" % (i - period) fmt = "%%.%df" % (i - period)
return fmt % value return fmt % value

View File

@ -58,6 +58,7 @@ __copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>"
class Error(Exception): class Error(Exception):
pass pass
def pack(fmt, obj): def pack(fmt, obj):
formatstring, names, fixes = getformat(fmt, keep_pad_byte=True) formatstring, names, fixes = getformat(fmt, keep_pad_byte=True)
elements = [] elements = []
@ -74,6 +75,7 @@ def pack(fmt, obj):
data = struct.pack(*(formatstring,) + tuple(elements)) data = struct.pack(*(formatstring,) + tuple(elements))
return data return data
def unpack(fmt, data, obj=None): def unpack(fmt, data, obj=None):
if obj is None: if obj is None:
obj = {} obj = {}
@ -98,10 +100,12 @@ def unpack(fmt, data, obj=None):
d[name] = value d[name] = value
return obj return obj
def unpack2(fmt, data, obj=None): def unpack2(fmt, data, obj=None):
length = calcsize(fmt) length = calcsize(fmt)
return unpack(fmt, data[:length], obj), data[length:] return unpack(fmt, data[:length], obj), data[length:]
def calcsize(fmt): def calcsize(fmt):
formatstring, names, fixes = getformat(fmt) formatstring, names, fixes = getformat(fmt)
return struct.calcsize(formatstring) return struct.calcsize(formatstring)
@ -125,13 +129,11 @@ _extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$")
# matches an "empty" string, possibly containing whitespace and/or a comment # matches an "empty" string, possibly containing whitespace and/or a comment
_emptyRE = re.compile(r"\s*(#.*)?$") _emptyRE = re.compile(r"\s*(#.*)?$")
_fixedpointmappings = { _fixedpointmappings = {8: "b", 16: "h", 32: "l"}
8: "b",
16: "h",
32: "l"}
_formatcache = {} _formatcache = {}
def getformat(fmt, keep_pad_byte=False): def getformat(fmt, keep_pad_byte=False):
fmt = tostr(fmt, encoding="ascii") fmt = tostr(fmt, encoding="ascii")
try: try:
@ -147,7 +149,7 @@ def getformat(fmt, keep_pad_byte=False):
m = _extraRE.match(line) m = _extraRE.match(line)
if m: if m:
formatchar = m.group(1) formatchar = m.group(1)
if formatchar != 'x' and formatstring: if formatchar != "x" and formatstring:
raise Error("a special fmt char must be first") raise Error("a special fmt char must be first")
else: else:
m = _elementRE.match(line) m = _elementRE.match(line)
@ -171,6 +173,7 @@ def getformat(fmt, keep_pad_byte=False):
_formatcache[fmt] = formatstring, names, fixes _formatcache[fmt] = formatstring, names, fixes
return formatstring, names, fixes return formatstring, names, fixes
def _test(): def _test():
fmt = """ fmt = """
# comments are allowed # comments are allowed
@ -188,16 +191,16 @@ def _test():
apad: x apad: x
""" """
print('size:', calcsize(fmt)) print("size:", calcsize(fmt))
class foo(object): class foo(object):
pass pass
i = foo() i = foo()
i.ashort = 0x7fff i.ashort = 0x7FFF
i.along = 0x7fffffff i.along = 0x7FFFFFFF
i.abyte = 0x7f i.abyte = 0x7F
i.achar = "a" i.achar = "a"
i.astr = "12345" i.astr = "12345"
i.afloat = 0.5 i.afloat = 0.5
@ -206,11 +209,12 @@ def _test():
i.abool = True i.abool = True
data = pack(fmt, i) data = pack(fmt, i)
print('data:', repr(data)) print("data:", repr(data))
print(unpack(fmt, data)) print(unpack(fmt, data))
i2 = foo() i2 = foo()
unpack(fmt, data, i2) unpack(fmt, data, i2)
print(vars(i2)) print(vars(i2))
if __name__ == "__main__": if __name__ == "__main__":
_test() _test()

View File

@ -6,13 +6,13 @@ import sys
n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic
t, x, y = sp.symbols('t x y', real=True) t, x, y = sp.symbols("t x y", real=True)
c = sp.symbols('c', real=False) # Complex representation instead of x/y c = sp.symbols("c", real=False) # Complex representation instead of x/y
X = tuple(sp.symbols('x:%d'%(n+1), real=True)) X = tuple(sp.symbols("x:%d" % (n + 1), real=True))
Y = tuple(sp.symbols('y:%d'%(n+1), real=True)) Y = tuple(sp.symbols("y:%d" % (n + 1), real=True))
P = tuple(zip(*(sp.symbols('p:%d[%s]'%(n+1,w), real=True) for w in '01'))) P = tuple(zip(*(sp.symbols("p:%d[%s]" % (n + 1, w), real=True) for w in "01")))
C = tuple(sp.symbols('c:%d'%(n+1), real=False)) C = tuple(sp.symbols("c:%d" % (n + 1), real=False))
# Cubic Bernstein basis functions # Cubic Bernstein basis functions
BinomialCoefficient = [(1, 0)] BinomialCoefficient = [(1, 0)]
@ -25,15 +25,20 @@ del last, this
BernsteinPolynomial = tuple( BernsteinPolynomial = tuple(
tuple(c * t**i * (1 - t) ** (n - i) for i, c in enumerate(coeffs)) tuple(c * t**i * (1 - t) ** (n - i) for i, c in enumerate(coeffs))
for n,coeffs in enumerate(BinomialCoefficient)) for n, coeffs in enumerate(BinomialCoefficient)
)
BezierCurve = tuple( BezierCurve = tuple(
tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins)) tuple(
for j in range(2)) sum(P[i][j] * bernstein for i, bernstein in enumerate(bernsteins))
for n,bernsteins in enumerate(BernsteinPolynomial)) for j in range(2)
)
for n, bernsteins in enumerate(BernsteinPolynomial)
)
BezierCurveC = tuple( BezierCurveC = tuple(
sum(C[i] * bernstein for i, bernstein in enumerate(bernsteins)) sum(C[i] * bernstein for i, bernstein in enumerate(bernsteins))
for n,bernsteins in enumerate(BernsteinPolynomial)) for n, bernsteins in enumerate(BernsteinPolynomial)
)
def green(f, curveXY): def green(f, curveXY):
@ -44,17 +49,17 @@ def green(f, curveXY):
class _BezierFuncsLazy(dict): class _BezierFuncsLazy(dict):
def __init__(self, symfunc): def __init__(self, symfunc):
self._symfunc = symfunc self._symfunc = symfunc
self._bezfuncs = {} self._bezfuncs = {}
def __missing__(self, i): def __missing__(self, i):
args = ['p%d'%d for d in range(i+1)] args = ["p%d" % d for d in range(i + 1)]
f = green(self._symfunc, BezierCurve[i]) f = green(self._symfunc, BezierCurve[i])
f = sp.gcd_terms(f.collect(sum(P, ()))) # Optimize f = sp.gcd_terms(f.collect(sum(P, ()))) # Optimize
return sp.lambdify(args, f) return sp.lambdify(args, f)
class GreenPen(BasePen): class GreenPen(BasePen):
_BezierFuncs = {} _BezierFuncs = {}
@ -97,6 +102,7 @@ class GreenPen(BasePen):
p0 = self._getCurrentPoint() p0 = self._getCurrentPoint()
self.value += self._funcs[3](p0, p1, p2, p3) self.value += self._funcs[3](p0, p1, p2, p3)
# Sample pens. # Sample pens.
# Do not use this in real code. # Do not use this in real code.
# Use fontTools.pens.momentsPen.MomentsPen instead. # Use fontTools.pens.momentsPen.MomentsPen instead.
@ -114,18 +120,15 @@ def printGreenPen(penName, funcs, file=sys.stdout, docstring=None):
print('"""%s"""' % docstring) print('"""%s"""' % docstring)
print( print(
'''from fontTools.pens.basePen import BasePen, OpenContourError """from fontTools.pens.basePen import BasePen, OpenContourError
try: try:
import cython import cython
except ImportError:
COMPILED = cython.compiled
except (AttributeError, ImportError):
# if cython not installed, use mock module with no-op decorators and types # if cython not installed, use mock module with no-op decorators and types
from fontTools.misc import cython from fontTools.misc import cython
if cython.compiled:
# Yep, I'm compiled.
COMPILED = True
else:
# Just a lowly interpreted script.
COMPILED = False COMPILED = False
@ -135,10 +138,14 @@ class %s(BasePen):
def __init__(self, glyphset=None): def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset) BasePen.__init__(self, glyphset)
'''% (penName, penName), file=file) """
% (penName, penName),
file=file,
)
for name, f in funcs: for name, f in funcs:
print(' self.%s = 0' % name, file=file) print(" self.%s = 0" % name, file=file)
print(''' print(
"""
def _moveTo(self, p0): def _moveTo(self, p0):
self.__startPoint = p0 self.__startPoint = p0
@ -154,32 +161,40 @@ class %s(BasePen):
raise OpenContourError( raise OpenContourError(
"Green theorem is not defined on open contours." "Green theorem is not defined on open contours."
) )
''', end='', file=file) """,
end="",
file=file,
)
for n in (1, 2, 3): for n in (1, 2, 3):
subs = {P[i][j]: [X, Y][j][i] for i in range(n + 1) for j in range(2)} subs = {P[i][j]: [X, Y][j][i] for i in range(n + 1) for j in range(2)}
greens = [green(f, BezierCurve[n]) for name, f in funcs] greens = [green(f, BezierCurve[n]) for name, f in funcs]
greens = [sp.gcd_terms(f.collect(sum(P, ()))) for f in greens] # Optimize greens = [sp.gcd_terms(f.collect(sum(P, ()))) for f in greens] # Optimize
greens = [f.subs(subs) for f in greens] # Convert to p to x/y greens = [f.subs(subs) for f in greens] # Convert to p to x/y
defs, exprs = sp.cse(greens, defs, exprs = sp.cse(
optimizations='basic', greens,
symbols=(sp.Symbol('r%d'%i) for i in count())) optimizations="basic",
symbols=(sp.Symbol("r%d" % i) for i in count()),
)
print() print()
for name, value in defs: for name, value in defs:
print(' @cython.locals(%s=cython.double)' % name, file=file) print(" @cython.locals(%s=cython.double)" % name, file=file)
if n == 1: if n == 1:
print('''\ print(
"""\
@cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x0=cython.double, y0=cython.double)
@cython.locals(x1=cython.double, y1=cython.double) @cython.locals(x1=cython.double, y1=cython.double)
def _lineTo(self, p1): def _lineTo(self, p1):
x0,y0 = self._getCurrentPoint() x0,y0 = self._getCurrentPoint()
x1,y1 = p1 x1,y1 = p1
''', file=file) """,
file=file,
)
elif n == 2: elif n == 2:
print('''\ print(
"""\
@cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x0=cython.double, y0=cython.double)
@cython.locals(x1=cython.double, y1=cython.double) @cython.locals(x1=cython.double, y1=cython.double)
@cython.locals(x2=cython.double, y2=cython.double) @cython.locals(x2=cython.double, y2=cython.double)
@ -187,9 +202,12 @@ class %s(BasePen):
x0,y0 = self._getCurrentPoint() x0,y0 = self._getCurrentPoint()
x1,y1 = p1 x1,y1 = p1
x2,y2 = p2 x2,y2 = p2
''', file=file) """,
file=file,
)
elif n == 3: elif n == 3:
print('''\ print(
"""\
@cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x0=cython.double, y0=cython.double)
@cython.locals(x1=cython.double, y1=cython.double) @cython.locals(x1=cython.double, y1=cython.double)
@cython.locals(x2=cython.double, y2=cython.double) @cython.locals(x2=cython.double, y2=cython.double)
@ -199,24 +217,30 @@ class %s(BasePen):
x1,y1 = p1 x1,y1 = p1
x2,y2 = p2 x2,y2 = p2
x3,y3 = p3 x3,y3 = p3
''', file=file) """,
file=file,
)
for name, value in defs: for name, value in defs:
print(' %s = %s' % (name, value), file=file) print(" %s = %s" % (name, value), file=file)
print(file=file) print(file=file)
for name, value in zip([f[0] for f in funcs], exprs): for name, value in zip([f[0] for f in funcs], exprs):
print(' self.%s += %s' % (name, value), file=file) print(" self.%s += %s" % (name, value), file=file)
print(''' print(
"""
if __name__ == '__main__': if __name__ == '__main__':
from fontTools.misc.symfont import x, y, printGreenPen from fontTools.misc.symfont import x, y, printGreenPen
printGreenPen('%s', ['''%penName, file=file) printGreenPen('%s', ["""
% penName,
file=file,
)
for name, f in funcs: for name, f in funcs:
print(" ('%s', %s)," % (name, str(f)), file=file) print(" ('%s', %s)," % (name, str(f)), file=file)
print(' ])', file=file) print(" ])", file=file)
if __name__ == '__main__': if __name__ == "__main__":
pen = AreaPen() pen = AreaPen()
pen.moveTo((100, 100)) pen.moveTo((100, 100))
pen.lineTo((100, 200)) pen.lineTo((100, 200))

View File

@ -29,12 +29,14 @@ def parseXML(xmlSnippet):
if isinstance(xmlSnippet, bytes): if isinstance(xmlSnippet, bytes):
xml += xmlSnippet xml += xmlSnippet
elif isinstance(xmlSnippet, str): elif isinstance(xmlSnippet, str):
xml += tobytes(xmlSnippet, 'utf-8') xml += tobytes(xmlSnippet, "utf-8")
elif isinstance(xmlSnippet, Iterable): elif isinstance(xmlSnippet, Iterable):
xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet) xml += b"".join(tobytes(s, "utf-8") for s in xmlSnippet)
else: else:
raise TypeError("expected string or sequence of strings; found %r" raise TypeError(
% type(xmlSnippet).__name__) "expected string or sequence of strings; found %r"
% type(xmlSnippet).__name__
)
xml += b"</root>" xml += b"</root>"
reader.parser.Parse(xml, 0) reader.parser.Parse(xml, 0)
return reader.root[2] return reader.root[2]
@ -76,6 +78,7 @@ class FakeFont:
return self.glyphOrder_[glyphID] return self.glyphOrder_[glyphID]
else: else:
return "glyph%.5d" % glyphID return "glyph%.5d" % glyphID
def getGlyphNameMany(self, lst): def getGlyphNameMany(self, lst):
return [self.getGlyphName(gid) for gid in lst] return [self.getGlyphName(gid) for gid in lst]
@ -92,6 +95,7 @@ class FakeFont:
class TestXMLReader_(object): class TestXMLReader_(object):
def __init__(self): def __init__(self):
from xml.parsers.expat import ParserCreate from xml.parsers.expat import ParserCreate
self.parser = ParserCreate() self.parser = ParserCreate()
self.parser.StartElementHandler = self.startElement_ self.parser.StartElementHandler = self.startElement_
self.parser.EndElementHandler = self.endElement_ self.parser.EndElementHandler = self.endElement_
@ -114,7 +118,7 @@ class TestXMLReader_(object):
self.stack[-1][2].append(data) self.stack[-1][2].append(data)
def makeXMLWriter(newlinestr='\n'): def makeXMLWriter(newlinestr="\n"):
# don't write OS-specific new lines # don't write OS-specific new lines
writer = XMLWriter(BytesIO(), newlinestr=newlinestr) writer = XMLWriter(BytesIO(), newlinestr=newlinestr)
# erase XML declaration # erase XML declaration
@ -166,7 +170,7 @@ class MockFont(object):
to its glyphOrder.""" to its glyphOrder."""
def __init__(self): def __init__(self):
self._glyphOrder = ['.notdef'] self._glyphOrder = [".notdef"]
class AllocatingDict(dict): class AllocatingDict(dict):
def __missing__(reverseDict, key): def __missing__(reverseDict, key):
@ -174,7 +178,8 @@ class MockFont(object):
gid = len(reverseDict) gid = len(reverseDict)
reverseDict[key] = gid reverseDict[key] = gid
return gid return gid
self._reverseGlyphOrder = AllocatingDict({'.notdef': 0})
self._reverseGlyphOrder = AllocatingDict({".notdef": 0})
self.lazy = False self.lazy = False
def getGlyphID(self, glyph): def getGlyphID(self, glyph):
@ -192,7 +197,6 @@ class MockFont(object):
class TestCase(_TestCase): class TestCase(_TestCase):
def __init__(self, methodName): def __init__(self, methodName):
_TestCase.__init__(self, methodName) _TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex, # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
@ -202,7 +206,6 @@ class TestCase(_TestCase):
class DataFilesHandler(TestCase): class DataFilesHandler(TestCase):
def setUp(self): def setUp(self):
self.tempdir = None self.tempdir = None
self.num_tempfiles = 0 self.num_tempfiles = 0

View File

@ -51,7 +51,7 @@ def deHexStr(hexdata):
def hexStr(data): def hexStr(data):
"""Convert binary data to a hex string.""" """Convert binary data to a hex string."""
h = string.hexdigits h = string.hexdigits
r = '' r = ""
for c in data: for c in data:
i = byteord(c) i = byteord(c)
r = r + h[(i >> 4) & 0xF] + h[i & 0xF] r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
@ -74,7 +74,7 @@ def num2binary(l, bits=32):
items.append(binary) items.append(binary)
items.reverse() items.reverse()
assert l in (0, -1), "number doesn't fit in number of bits" assert l in (0, -1), "number doesn't fit in number of bits"
return ' '.join(items) return " ".join(items)
def binary2num(bin): def binary2num(bin):
@ -151,4 +151,5 @@ def bytesjoin(iterable, joiner=b""):
if __name__ == "__main__": if __name__ == "__main__":
import doctest, sys import doctest, sys
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -10,8 +10,21 @@ import calendar
epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0)) epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0))
DAYNAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] DAYNAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", MONTHNAMES = [
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] None,
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
def asctime(t=None): def asctime(t=None):
@ -35,22 +48,27 @@ def asctime(t=None):
if t is None: if t is None:
t = time.localtime() t = time.localtime()
s = "%s %s %2s %s" % ( s = "%s %s %2s %s" % (
DAYNAMES[t.tm_wday], MONTHNAMES[t.tm_mon], t.tm_mday, DAYNAMES[t.tm_wday],
time.strftime("%H:%M:%S %Y", t)) MONTHNAMES[t.tm_mon],
t.tm_mday,
time.strftime("%H:%M:%S %Y", t),
)
return s return s
def timestampToString(value): def timestampToString(value):
return asctime(time.gmtime(max(0, value + epoch_diff))) return asctime(time.gmtime(max(0, value + epoch_diff)))
def timestampFromString(value): def timestampFromString(value):
wkday, mnth = value[:7].split() wkday, mnth = value[:7].split()
t = datetime.strptime(value[7:], ' %d %H:%M:%S %Y') t = datetime.strptime(value[7:], " %d %H:%M:%S %Y")
t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc) t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc)
wkday_idx = DAYNAMES.index(wkday) wkday_idx = DAYNAMES.index(wkday)
assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday' assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday'
return int(t.timestamp()) - epoch_diff return int(t.timestamp()) - epoch_diff
def timestampNow(): def timestampNow():
# https://reproducible-builds.org/specs/source-date-epoch/ # https://reproducible-builds.org/specs/source-date-epoch/
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
@ -58,6 +76,7 @@ def timestampNow():
return int(source_date_epoch) - epoch_diff return int(source_date_epoch) - epoch_diff
return int(time.time() - epoch_diff) return int(time.time() - epoch_diff)
def timestampSinceEpoch(value): def timestampSinceEpoch(value):
return int(value - epoch_diff) return int(value - epoch_diff)
@ -65,4 +84,5 @@ def timestampSinceEpoch(value):
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -19,6 +19,9 @@ Offset
Scale Scale
Convenience function that returns a scaling transformation Convenience function that returns a scaling transformation
The DecomposedTransform class implements a transformation with separate
translate, rotation, scale, skew, and transformation-center components.
:Example: :Example:
>>> t = Transform(2, 0, 0, 3, 0, 0) >>> t = Transform(2, 0, 0, 3, 0, 0)
@ -49,10 +52,12 @@ Scale
>>> >>>
""" """
import math
from typing import NamedTuple from typing import NamedTuple
from dataclasses import dataclass
__all__ = ["Transform", "Identity", "Offset", "Scale"] __all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
_EPSILON = 1e-15 _EPSILON = 1e-15
@ -248,6 +253,7 @@ class Transform(NamedTuple):
>>> >>>
""" """
import math import math
c = _normSinCos(math.cos(angle)) c = _normSinCos(math.cos(angle))
s = _normSinCos(math.sin(angle)) s = _normSinCos(math.sin(angle))
return self.transform((c, s, -s, c, 0, 0)) return self.transform((c, s, -s, c, 0, 0))
@ -263,6 +269,7 @@ class Transform(NamedTuple):
>>> >>>
""" """
import math import math
return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
def transform(self, other): def transform(self, other):
@ -283,7 +290,8 @@ class Transform(NamedTuple):
yx1 * xx2 + yy1 * yx2, yx1 * xx2 + yy1 * yx2,
yx1 * xy2 + yy1 * yy2, yx1 * xy2 + yy1 * yy2,
xx2 * dx1 + yx2 * dy1 + dx2, xx2 * dx1 + yx2 * dy1 + dx2,
xy2*dx1 + yy2*dy1 + dy2) xy2 * dx1 + yy2 * dy1 + dy2,
)
def reverseTransform(self, other): def reverseTransform(self, other):
"""Return a new transformation, which is the other transformation """Return a new transformation, which is the other transformation
@ -306,7 +314,8 @@ class Transform(NamedTuple):
yx1 * xx2 + yy1 * yx2, yx1 * xx2 + yy1 * yx2,
yx1 * xy2 + yy1 * yy2, yx1 * xy2 + yy1 * yy2,
xx2 * dx1 + yx2 * dy1 + dx2, xx2 * dx1 + yx2 * dy1 + dx2,
xy2*dx1 + yy2*dy1 + dy2) xy2 * dx1 + yy2 * dy1 + dy2,
)
def inverse(self): def inverse(self):
"""Return the inverse transformation. """Return the inverse transformation.
@ -340,6 +349,10 @@ class Transform(NamedTuple):
""" """
return "[%s %s %s %s %s %s]" % self return "[%s %s %s %s %s %s]" % self
def toDecomposed(self) -> "DecomposedTransform":
"""Decompose into a DecomposedTransform."""
return DecomposedTransform.fromTransform(self)
def __bool__(self): def __bool__(self):
"""Returns True if transform is not identity, False otherwise. """Returns True if transform is not identity, False otherwise.
@ -368,6 +381,7 @@ class Transform(NamedTuple):
Identity = Transform() Identity = Transform()
def Offset(x=0, y=0): def Offset(x=0, y=0):
"""Return the identity transformation offset by x, y. """Return the identity transformation offset by x, y.
@ -378,6 +392,7 @@ def Offset(x=0, y=0):
""" """
return Transform(1, 0, 0, 1, x, y) return Transform(1, 0, 0, 1, x, y)
def Scale(x, y=None): def Scale(x, y=None):
"""Return the identity transformation scaled by x, y. The 'y' argument """Return the identity transformation scaled by x, y. The 'y' argument
may be None, which implies to use the x value for y as well. may be None, which implies to use the x value for y as well.
@ -392,7 +407,89 @@ def Scale(x, y=None):
return Transform(x, 0, 0, y, 0, 0) return Transform(x, 0, 0, y, 0, 0)
@dataclass
class DecomposedTransform:
"""The DecomposedTransform class implements a transformation with separate
translate, rotation, scale, skew, and transformation-center components.
"""
translateX: float = 0
translateY: float = 0
rotation: float = 0 # in degrees, counter-clockwise
scaleX: float = 1
scaleY: float = 1
skewX: float = 0 # in degrees, clockwise
skewY: float = 0 # in degrees, counter-clockwise
tCenterX: float = 0
tCenterY: float = 0
@classmethod
def fromTransform(self, transform):
# Adapted from an answer on
# https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
a, b, c, d, x, y = transform
sx = math.copysign(1, a)
if sx < 0:
a *= sx
b *= sx
delta = a * d - b * c
rotation = 0
scaleX = scaleY = 0
skewX = skewY = 0
# Apply the QR-like decomposition.
if a != 0 or b != 0:
r = math.sqrt(a * a + b * b)
rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
scaleX, scaleY = (r, delta / r)
skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0)
elif c != 0 or d != 0:
s = math.sqrt(c * c + d * d)
rotation = math.pi / 2 - (
math.acos(-c / s) if d >= 0 else -math.acos(c / s)
)
scaleX, scaleY = (delta / s, s)
skewX, skewY = (0, math.atan((a * c + b * d) / (s * s)))
else:
# a = b = c = d = 0
pass
return DecomposedTransform(
x,
y,
math.degrees(rotation),
scaleX * sx,
scaleY,
math.degrees(skewX) * sx,
math.degrees(skewY),
0,
0,
)
def toTransform(self):
"""Return the Transform() equivalent of this transformation.
:Example:
>>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
<Transform [2 0 0 2 0 0]>
>>>
"""
t = Transform()
t = t.translate(
self.translateX + self.tCenterX, self.translateY + self.tCenterY
)
t = t.rotate(math.radians(self.rotation))
t = t.scale(self.scaleX, self.scaleY)
t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
t = t.translate(-self.tCenterX, -self.tCenterY)
return t
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)

View File

@ -8,15 +8,19 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class TTXParseError(Exception): pass
class TTXParseError(Exception):
pass
BUFSIZE = 0x4000 BUFSIZE = 0x4000
class XMLReader(object): class XMLReader(object):
def __init__(
def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False
if fileOrPath == '-': ):
if fileOrPath == "-":
fileOrPath = sys.stdin fileOrPath = sys.stdin
if not hasattr(fileOrPath, "read"): if not hasattr(fileOrPath, "read"):
self.file = open(fileOrPath, "rb") self.file = open(fileOrPath, "rb")
@ -29,6 +33,7 @@ class XMLReader(object):
self.progress = progress self.progress = progress
if quiet is not None: if quiet is not None:
from fontTools.misc.loggingTools import deprecateArgument from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("quiet", "configure logging instead") deprecateArgument("quiet", "configure logging instead")
self.quiet = quiet self.quiet = quiet
self.root = None self.root = None
@ -55,6 +60,7 @@ class XMLReader(object):
def _parseFile(self, file): def _parseFile(self, file):
from xml.parsers.expat import ParserCreate from xml.parsers.expat import ParserCreate
parser = ParserCreate() parser = ParserCreate()
parser.StartElementHandler = self._startElementHandler parser.StartElementHandler = self._startElementHandler
parser.EndElementHandler = self._endElementHandler parser.EndElementHandler = self._endElementHandler
@ -83,7 +89,7 @@ class XMLReader(object):
self.stackSize = stackSize + 1 self.stackSize = stackSize + 1
subFile = attrs.get("src") subFile = attrs.get("src")
if subFile is not None: if subFile is not None:
if hasattr(self.file, 'name'): if hasattr(self.file, "name"):
# if file has a name, get its parent directory # if file has a name, get its parent directory
dirname = os.path.dirname(self.file.name) dirname = os.path.dirname(self.file.name)
else: else:
@ -113,13 +119,13 @@ class XMLReader(object):
log.info(msg) log.info(msg)
if tag == "GlyphOrder": if tag == "GlyphOrder":
tableClass = ttLib.GlyphOrder tableClass = ttLib.GlyphOrder
elif "ERROR" in attrs or ('raw' in attrs and safeEval(attrs['raw'])): elif "ERROR" in attrs or ("raw" in attrs and safeEval(attrs["raw"])):
tableClass = DefaultTable tableClass = DefaultTable
else: else:
tableClass = ttLib.getTableClass(tag) tableClass = ttLib.getTableClass(tag)
if tableClass is None: if tableClass is None:
tableClass = DefaultTable tableClass = DefaultTable
if tag == 'loca' and tag in self.ttFont: if tag == "loca" and tag in self.ttFont:
# Special-case the 'loca' table as we need the # Special-case the 'loca' table as we need the
# original if the 'glyf' table isn't recompiled. # original if the 'glyf' table isn't recompiled.
self.currentTable = self.ttFont[tag] self.currentTable = self.ttFont[tag]
@ -157,7 +163,6 @@ class XMLReader(object):
class ProgressPrinter(object): class ProgressPrinter(object):
def __init__(self, title, maxval=100): def __init__(self, title, maxval=100):
print(title) print(title)

View File

@ -9,12 +9,17 @@ INDENT = " "
class XMLWriter(object): class XMLWriter(object):
def __init__(
def __init__(self, fileOrPath, indentwhite=INDENT, idlefunc=None, encoding="utf_8", self,
newlinestr="\n"): fileOrPath,
if encoding.lower().replace('-','').replace('_','') != 'utf8': indentwhite=INDENT,
raise Exception('Only UTF-8 encoding is supported.') idlefunc=None,
if fileOrPath == '-': encoding="utf_8",
newlinestr="\n",
):
if encoding.lower().replace("-", "").replace("_", "") != "utf8":
raise Exception("Only UTF-8 encoding is supported.")
if fileOrPath == "-":
fileOrPath = sys.stdout fileOrPath = sys.stdout
if not hasattr(fileOrPath, "write"): if not hasattr(fileOrPath, "write"):
self.filename = fileOrPath self.filename = fileOrPath
@ -30,11 +35,11 @@ class XMLWriter(object):
try: try:
# The bytes check should be first. See: # The bytes check should be first. See:
# https://github.com/fonttools/fonttools/pull/233 # https://github.com/fonttools/fonttools/pull/233
self.file.write(b'') self.file.write(b"")
self.totype = tobytes self.totype = tobytes
except TypeError: except TypeError:
# This better not fail. # This better not fail.
self.file.write('') self.file.write("")
self.totype = tostr self.totype = tostr
self.indentwhite = self.totype(indentwhite) self.indentwhite = self.totype(indentwhite)
if newlinestr is None: if newlinestr is None:
@ -84,7 +89,7 @@ class XMLWriter(object):
self.file.write(self.indentlevel * self.indentwhite) self.file.write(self.indentlevel * self.indentwhite)
self.needindent = 0 self.needindent = 0
s = self.totype(data, encoding="utf_8") s = self.totype(data, encoding="utf_8")
if (strip): if strip:
s = s.strip() s = s.strip()
self.file.write(s) self.file.write(s)
@ -163,31 +168,36 @@ class XMLWriter(object):
def escape(data): def escape(data):
data = tostr(data, 'utf_8') data = tostr(data, "utf_8")
data = data.replace("&", "&amp;") data = data.replace("&", "&amp;")
data = data.replace("<", "&lt;") data = data.replace("<", "&lt;")
data = data.replace(">", "&gt;") data = data.replace(">", "&gt;")
data = data.replace("\r", "&#13;") data = data.replace("\r", "&#13;")
return data return data
def escapeattr(data): def escapeattr(data):
data = escape(data) data = escape(data)
data = data.replace('"', "&quot;") data = data.replace('"', "&quot;")
return data return data
def escape8bit(data): def escape8bit(data):
"""Input is Unicode string.""" """Input is Unicode string."""
def escapechar(c): def escapechar(c):
n = ord(c) n = ord(c)
if 32 <= n <= 127 and c not in "<&>": if 32 <= n <= 127 and c not in "<&>":
return c return c
else: else:
return "&#" + repr(n) + ";" return "&#" + repr(n) + ";"
return strjoin(map(escapechar, data.decode('latin-1')))
return strjoin(map(escapechar, data.decode("latin-1")))
def hexStr(s): def hexStr(s):
h = string.hexdigits h = string.hexdigits
r = '' r = ""
for c in s: for c in s:
i = byteord(c) i = byteord(c)
r = r + h[(i >> 4) & 0xF] + h[i & 0xF] r = r + h[(i >> 4) & 0xF] + h[i & 0xF]

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import sys import sys
from fontTools.mtiLib import main from fontTools.mtiLib import main
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -764,7 +764,7 @@ class ChainContextSubstBuilder(ChainContextualBuilder):
result.setdefault(glyph, set()).update(replacements) result.setdefault(glyph, set()).update(replacements)
return result return result
def find_chainable_single_subst(self, glyphs): def find_chainable_single_subst(self, mapping):
"""Helper for add_single_subst_chained_()""" """Helper for add_single_subst_chained_()"""
res = None res = None
for rule in self.rules[::-1]: for rule in self.rules[::-1]:
@ -772,7 +772,7 @@ class ChainContextSubstBuilder(ChainContextualBuilder):
return res return res
for sub in rule.lookups: for sub in rule.lookups:
if isinstance(sub, SingleSubstBuilder) and not any( if isinstance(sub, SingleSubstBuilder) and not any(
g in glyphs for g in sub.mapping.keys() g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping
): ):
res = sub res = sub
return res return res

View File

@ -2,5 +2,5 @@ import sys
from fontTools.otlLib.optimize import main from fontTools.otlLib.optimize import main
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -7,7 +7,6 @@ __all__ = ["AreaPen"]
class AreaPen(BasePen): class AreaPen(BasePen):
def __init__(self, glyphset=None): def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset) BasePen.__init__(self, glyphset)
self.value = 0 self.value = 0
@ -18,7 +17,7 @@ class AreaPen(BasePen):
def _lineTo(self, p1): def _lineTo(self, p1):
x0, y0 = self._p0 x0, y0 = self._p0
x1, y1 = p1 x1, y1 = p1
self.value -= (x1 - x0) * (y1 + y0) * .5 self.value -= (x1 - x0) * (y1 + y0) * 0.5
self._p0 = p1 self._p0 = p1
def _qCurveToOne(self, p1, p2): def _qCurveToOne(self, p1, p2):
@ -38,11 +37,7 @@ class AreaPen(BasePen):
x1, y1 = p1[0] - x0, p1[1] - y0 x1, y1 = p1[0] - x0, p1[1] - y0
x2, y2 = p2[0] - x0, p2[1] - y0 x2, y2 = p2[0] - x0, p2[1] - y0
x3, y3 = p3[0] - x0, p3[1] - y0 x3, y3 = p3[0] - x0, p3[1] - y0
self.value -= ( self.value -= (x1 * (-y2 - y3) + x2 * (y1 - 2 * y3) + x3 * (y1 + 2 * y2)) * 0.15
x1 * ( - y2 - y3) +
x2 * (y1 - 2*y3) +
x3 * (y1 + 2*y2 )
) * 0.15
self._lineTo(p3) self._lineTo(p3)
self._p0 = p3 self._p0 = p3

View File

@ -36,23 +36,30 @@ Coordinates are usually expressed as (x, y) tuples, but generally any
sequence of length 2 will do. sequence of length 2 will do.
""" """
from typing import Tuple from typing import Tuple, Dict
from fontTools.misc.loggingTools import LogMixin from fontTools.misc.loggingTools import LogMixin
from fontTools.misc.transform import DecomposedTransform
__all__ = ["AbstractPen", "NullPen", "BasePen", "PenError", __all__ = [
"decomposeSuperBezierSegment", "decomposeQuadraticSegment"] "AbstractPen",
"NullPen",
"BasePen",
"PenError",
"decomposeSuperBezierSegment",
"decomposeQuadraticSegment",
]
class PenError(Exception): class PenError(Exception):
"""Represents an error during penning.""" """Represents an error during penning."""
class OpenContourError(PenError): class OpenContourError(PenError):
pass pass
class AbstractPen: class AbstractPen:
def moveTo(self, pt: Tuple[float, float]) -> None: def moveTo(self, pt: Tuple[float, float]) -> None:
"""Begin a new sub path, set the current point to 'pt'. You must """Begin a new sub path, set the current point to 'pt'. You must
end each sub path with a call to pen.closePath() or pen.endPath(). end each sub path with a call to pen.closePath() or pen.endPath().
@ -116,7 +123,7 @@ class AbstractPen:
def addComponent( def addComponent(
self, self,
glyphName: str, glyphName: str,
transformation: Tuple[float, float, float, float, float, float] transformation: Tuple[float, float, float, float, float, float],
) -> None: ) -> None:
"""Add a sub glyph. The 'transformation' argument must be a 6-tuple """Add a sub glyph. The 'transformation' argument must be a 6-tuple
containing an affine transformation, or a Transform object from the containing an affine transformation, or a Transform object from the
@ -125,11 +132,24 @@ class AbstractPen:
""" """
raise NotImplementedError raise NotImplementedError
def addVarComponent(
self,
glyphName: str,
transformation: DecomposedTransform,
location: Dict[str, float],
) -> None:
"""Add a VarComponent sub glyph. The 'transformation' argument
must be a DecomposedTransform from the fontTools.misc.transform module,
and the 'location' argument must be a dictionary mapping axis tags
to their locations.
"""
# GlyphSet decomposes for us
raise AttributeError
class NullPen(AbstractPen): class NullPen(AbstractPen):
"""A pen that does nothing. """A pen that does nothing."""
"""
def moveTo(self, pt): def moveTo(self, pt):
pass pass
@ -152,10 +172,13 @@ class NullPen(AbstractPen):
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
pass pass
def addVarComponent(self, glyphName, transformation, location):
pass
class LoggingPen(LogMixin, AbstractPen): class LoggingPen(LogMixin, AbstractPen):
"""A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin) """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
"""
pass pass
@ -187,27 +210,30 @@ class DecomposingPen(LoggingPen):
self.glyphSet = glyphSet self.glyphSet = glyphSet
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
""" Transform the points of the base glyph and draw it onto self. """Transform the points of the base glyph and draw it onto self."""
"""
from fontTools.pens.transformPen import TransformPen from fontTools.pens.transformPen import TransformPen
try: try:
glyph = self.glyphSet[glyphName] glyph = self.glyphSet[glyphName]
except KeyError: except KeyError:
if not self.skipMissingComponents: if not self.skipMissingComponents:
raise MissingComponentError(glyphName) raise MissingComponentError(glyphName)
self.log.warning( self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
"glyph '%s' is missing from glyphSet; skipped" % glyphName)
else: else:
tPen = TransformPen(self, transformation) tPen = TransformPen(self, transformation)
glyph.draw(tPen) glyph.draw(tPen)
def addVarComponent(self, glyphName, transformation, location):
# GlyphSet decomposes for us
raise AttributeError
class BasePen(DecomposingPen): class BasePen(DecomposingPen):
"""Base class for drawing pens. You must override _moveTo, _lineTo and """Base class for drawing pens. You must override _moveTo, _lineTo and
_curveToOne. You may additionally override _closePath, _endPath, _curveToOne. You may additionally override _closePath, _endPath,
addComponent and/or _qCurveToOne. You should not override any other addComponent, addVarComponent, and/or _qCurveToOne. You should not
methods. override any other methods.
""" """
def __init__(self, glyphSet=None): def __init__(self, glyphSet=None):
@ -350,13 +376,14 @@ def decomposeSuperBezierSegment(points):
factor = j / nDivisions factor = j / nDivisions
temp1 = points[i - 1] temp1 = points[i - 1]
temp2 = points[i - 2] temp2 = points[i - 2]
temp = (temp2[0] + factor * (temp1[0] - temp2[0]), temp = (
temp2[1] + factor * (temp1[1] - temp2[1])) temp2[0] + factor * (temp1[0] - temp2[0]),
temp2[1] + factor * (temp1[1] - temp2[1]),
)
if pt2 is None: if pt2 is None:
pt2 = temp pt2 = temp
else: else:
pt3 = (0.5 * (pt2[0] + temp[0]), pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
0.5 * (pt2[1] + temp[1]))
bezierSegments.append((pt1, pt2, pt3)) bezierSegments.append((pt1, pt2, pt3))
pt1, pt2, pt3 = temp, None, None pt1, pt2, pt3 = temp, None, None
bezierSegments.append((pt1, points[-2], points[-1])) bezierSegments.append((pt1, points[-2], points[-1]))
@ -387,13 +414,19 @@ def decomposeQuadraticSegment(points):
class _TestPen(BasePen): class _TestPen(BasePen):
"""Test class that prints PostScript to stdout.""" """Test class that prints PostScript to stdout."""
def _moveTo(self, pt): def _moveTo(self, pt):
print("%s %s moveto" % (pt[0], pt[1])) print("%s %s moveto" % (pt[0], pt[1]))
def _lineTo(self, pt): def _lineTo(self, pt):
print("%s %s lineto" % (pt[0], pt[1])) print("%s %s lineto" % (pt[0], pt[1]))
def _curveToOne(self, bcp1, bcp2, pt): def _curveToOne(self, bcp1, bcp2, pt):
print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1], print(
bcp2[0], bcp2[1], pt[0], pt[1])) "%s %s %s %s %s %s curveto"
% (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
)
def _closePath(self): def _closePath(self):
print("closepath") print("closepath")

View File

@ -84,8 +84,9 @@ class BoundsPen(ControlBoundsPen):
bounds = self.bounds bounds = self.bounds
bounds = updateBounds(bounds, pt) bounds = updateBounds(bounds, pt)
if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds): if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
bounds = unionRect(bounds, calcCubicBounds( bounds = unionRect(
self._getCurrentPoint(), bcp1, bcp2, pt)) bounds, calcCubicBounds(self._getCurrentPoint(), bcp1, bcp2, pt)
)
self.bounds = bounds self.bounds = bounds
def _qCurveToOne(self, bcp, pt): def _qCurveToOne(self, bcp, pt):
@ -93,6 +94,7 @@ class BoundsPen(ControlBoundsPen):
bounds = self.bounds bounds = self.bounds
bounds = updateBounds(bounds, pt) bounds = updateBounds(bounds, pt)
if not pointInRect(bcp, bounds): if not pointInRect(bcp, bounds):
bounds = unionRect(bounds, calcQuadraticBounds( bounds = unionRect(
self._getCurrentPoint(), bcp, pt)) bounds, calcQuadraticBounds(self._getCurrentPoint(), bcp, pt)
)
self.bounds = bounds self.bounds = bounds

View File

@ -5,11 +5,11 @@ __all__ = ["CocoaPen"]
class CocoaPen(BasePen): class CocoaPen(BasePen):
def __init__(self, glyphSet, path=None): def __init__(self, glyphSet, path=None):
BasePen.__init__(self, glyphSet) BasePen.__init__(self, glyphSet)
if path is None: if path is None:
from AppKit import NSBezierPath from AppKit import NSBezierPath
path = NSBezierPath.bezierPath() path = NSBezierPath.bezierPath()
self.path = path self.path = path

View File

@ -12,14 +12,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from fontTools.cu2qu import curve_to_quadratic import operator
from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
from fontTools.pens.basePen import decomposeSuperBezierSegment
from fontTools.pens.filterPen import FilterPen
from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.pens.reverseContourPen import ReverseContourPen
from fontTools.pens.pointPen import BasePointToSegmentPen from fontTools.pens.pointPen import BasePointToSegmentPen
from fontTools.pens.pointPen import ReverseContourPointPen from fontTools.pens.pointPen import ReverseContourPointPen
class Cu2QuPen(AbstractPen): class Cu2QuPen(FilterPen):
"""A filter pen to convert cubic bezier curves to quadratic b-splines """A filter pen to convert cubic bezier curves to quadratic b-splines
using the FontTools SegmentPen protocol. using the FontTools SegmentPen protocol.
@ -31,114 +33,56 @@ class Cu2QuPen(AbstractPen):
value equal, or close to UPEM / 1000. value equal, or close to UPEM / 1000.
reverse_direction: flip the contours' direction but keep starting point. reverse_direction: flip the contours' direction but keep starting point.
stats: a dictionary counting the point numbers of quadratic segments. stats: a dictionary counting the point numbers of quadratic segments.
ignore_single_points: don't emit contours containing only a single point all_quadratic: if True (default), only quadratic b-splines are generated.
if False, quadratic curves or cubic curves are generated depending
NOTE: The "ignore_single_points" argument is deprecated since v1.3.0, on which one is more economical.
which dropped Robofab subpport. It's no longer needed to special-case
UFO2-style anchors (aka "named points") when using ufoLib >= 2.0,
as these are no longer drawn onto pens as single-point contours,
but are handled separately as anchors.
""" """
def __init__(self, other_pen, max_err, reverse_direction=False, def __init__(
stats=None, ignore_single_points=False): self,
other_pen,
max_err,
reverse_direction=False,
stats=None,
all_quadratic=True,
):
if reverse_direction: if reverse_direction:
self.pen = ReverseContourPen(other_pen) other_pen = ReverseContourPen(other_pen)
else: super().__init__(other_pen)
self.pen = other_pen
self.max_err = max_err self.max_err = max_err
self.stats = stats self.stats = stats
if ignore_single_points: self.all_quadratic = all_quadratic
import warnings
warnings.warn("ignore_single_points is deprecated and "
"will be removed in future versions",
UserWarning, stacklevel=2)
self.ignore_single_points = ignore_single_points
self.start_pt = None
self.current_pt = None
def _check_contour_is_open(self): def _convert_curve(self, pt1, pt2, pt3):
if self.current_pt is None:
raise AssertionError("moveTo is required")
def _check_contour_is_closed(self):
if self.current_pt is not None:
raise AssertionError("closePath or endPath is required")
def _add_moveTo(self):
if self.start_pt is not None:
self.pen.moveTo(self.start_pt)
self.start_pt = None
def moveTo(self, pt):
self._check_contour_is_closed()
self.start_pt = self.current_pt = pt
if not self.ignore_single_points:
self._add_moveTo()
def lineTo(self, pt):
self._check_contour_is_open()
self._add_moveTo()
self.pen.lineTo(pt)
self.current_pt = pt
def qCurveTo(self, *points):
self._check_contour_is_open()
n = len(points)
if n == 1:
self.lineTo(points[0])
elif n > 1:
self._add_moveTo()
self.pen.qCurveTo(*points)
self.current_pt = points[-1]
else:
raise AssertionError("illegal qcurve segment point count: %d" % n)
def _curve_to_quadratic(self, pt1, pt2, pt3):
curve = (self.current_pt, pt1, pt2, pt3) curve = (self.current_pt, pt1, pt2, pt3)
quadratic = curve_to_quadratic(curve, self.max_err) result = curve_to_quadratic(curve, self.max_err, self.all_quadratic)
if self.stats is not None: if self.stats is not None:
n = str(len(quadratic) - 2) n = str(len(result) - 2)
self.stats[n] = self.stats.get(n, 0) + 1 self.stats[n] = self.stats.get(n, 0) + 1
self.qCurveTo(*quadratic[1:]) if self.all_quadratic:
self.qCurveTo(*result[1:])
else:
if len(result) == 3:
self.qCurveTo(*result[1:])
else:
assert len(result) == 4
super().curveTo(*result[1:])
def curveTo(self, *points): def curveTo(self, *points):
self._check_contour_is_open()
n = len(points) n = len(points)
if n == 3: if n == 3:
# this is the most common case, so we special-case it # this is the most common case, so we special-case it
self._curve_to_quadratic(*points) self._convert_curve(*points)
elif n > 3: elif n > 3:
for segment in decomposeSuperBezierSegment(points): for segment in decomposeSuperBezierSegment(points):
self._curve_to_quadratic(*segment) self._convert_curve(*segment)
elif n == 2:
self.qCurveTo(*points)
elif n == 1:
self.lineTo(points[0])
else: else:
raise AssertionError("illegal curve segment point count: %d" % n) self.qCurveTo(*points)
def closePath(self):
self._check_contour_is_open()
if self.start_pt is None:
# if 'start_pt' is _not_ None, we are ignoring single-point paths
self.pen.closePath()
self.current_pt = self.start_pt = None
def endPath(self):
self._check_contour_is_open()
if self.start_pt is None:
self.pen.endPath()
self.current_pt = self.start_pt = None
def addComponent(self, glyphName, transformation):
self._check_contour_is_closed()
self.pen.addComponent(glyphName, transformation)
class Cu2QuPointPen(BasePointToSegmentPen): class Cu2QuPointPen(BasePointToSegmentPen):
"""A filter pen to convert cubic bezier curves to quadratic b-splines """A filter pen to convert cubic bezier curves to quadratic b-splines
using the RoboFab PointPen protocol. using the FontTools PointPen protocol.
Args: Args:
other_point_pen: another PointPen used to draw the transformed outline. other_point_pen: another PointPen used to draw the transformed outline.
@ -147,10 +91,26 @@ class Cu2QuPointPen(BasePointToSegmentPen):
value equal, or close to UPEM / 1000. value equal, or close to UPEM / 1000.
reverse_direction: reverse the winding direction of all contours. reverse_direction: reverse the winding direction of all contours.
stats: a dictionary counting the point numbers of quadratic segments. stats: a dictionary counting the point numbers of quadratic segments.
all_quadratic: if True (default), only quadratic b-splines are generated.
if False, quadratic curves or cubic curves are generated depending
on which one is more economical.
""" """
def __init__(self, other_point_pen, max_err, reverse_direction=False, __points_required = {
stats=None): "move": (1, operator.eq),
"line": (1, operator.eq),
"qcurve": (2, operator.ge),
"curve": (3, operator.eq),
}
def __init__(
self,
other_point_pen,
max_err,
reverse_direction=False,
stats=None,
all_quadratic=True,
):
BasePointToSegmentPen.__init__(self) BasePointToSegmentPen.__init__(self)
if reverse_direction: if reverse_direction:
self.pen = ReverseContourPointPen(other_point_pen) self.pen = ReverseContourPointPen(other_point_pen)
@ -158,6 +118,7 @@ class Cu2QuPointPen(BasePointToSegmentPen):
self.pen = other_point_pen self.pen = other_point_pen
self.max_err = max_err self.max_err = max_err
self.stats = stats self.stats = stats
self.all_quadratic = all_quadratic
def _flushContour(self, segments): def _flushContour(self, segments):
assert len(segments) >= 1 assert len(segments) >= 1
@ -166,18 +127,21 @@ class Cu2QuPointPen(BasePointToSegmentPen):
prev_points = segments[-1][1] prev_points = segments[-1][1]
prev_on_curve = prev_points[-1][0] prev_on_curve = prev_points[-1][0]
for segment_type, points in segments: for segment_type, points in segments:
if segment_type == 'curve': if segment_type == "curve":
for sub_points in self._split_super_bezier_segments(points): for sub_points in self._split_super_bezier_segments(points):
on_curve, smooth, name, kwargs = sub_points[-1] on_curve, smooth, name, kwargs = sub_points[-1]
bcp1, bcp2 = sub_points[0][0], sub_points[1][0] bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
cubic = [prev_on_curve, bcp1, bcp2, on_curve] cubic = [prev_on_curve, bcp1, bcp2, on_curve]
quad = curve_to_quadratic(cubic, self.max_err) quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic)
if self.stats is not None: if self.stats is not None:
n = str(len(quad) - 2) n = str(len(quad) - 2)
self.stats[n] = self.stats.get(n, 0) + 1 self.stats[n] = self.stats.get(n, 0) + 1
new_points = [(pt, False, None, {}) for pt in quad[1:-1]] new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
new_points.append((on_curve, smooth, name, kwargs)) new_points.append((on_curve, smooth, name, kwargs))
if self.all_quadratic or len(new_points) == 2:
new_segments.append(["qcurve", new_points]) new_segments.append(["qcurve", new_points])
else:
new_segments.append(["curve", new_points])
prev_on_curve = sub_points[-1][0] prev_on_curve = sub_points[-1][0]
else: else:
new_segments.append([segment_type, points]) new_segments.append([segment_type, points])
@ -200,8 +164,9 @@ class Cu2QuPointPen(BasePointToSegmentPen):
# a "super" bezier; decompose it # a "super" bezier; decompose it
on_curve, smooth, name, kwargs = points[-1] on_curve, smooth, name, kwargs = points[-1]
num_sub_segments = n - 1 num_sub_segments = n - 1
for i, sub_points in enumerate(decomposeSuperBezierSegment([ for i, sub_points in enumerate(
pt for pt, _, _, _ in points])): decomposeSuperBezierSegment([pt for pt, _, _, _ in points])
):
new_segment = [] new_segment = []
for point in sub_points[:-1]: for point in sub_points[:-1]:
new_segment.append((point, False, None, {})) new_segment.append((point, False, None, {}))
@ -213,25 +178,22 @@ class Cu2QuPointPen(BasePointToSegmentPen):
new_segment.append((sub_points[-1], True, None, {})) new_segment.append((sub_points[-1], True, None, {}))
sub_segments.append(new_segment) sub_segments.append(new_segment)
else: else:
raise AssertionError( raise AssertionError("expected 2 control points, found: %d" % n)
"expected 2 control points, found: %d" % n)
return sub_segments return sub_segments
def _drawPoints(self, segments): def _drawPoints(self, segments):
pen = self.pen pen = self.pen
pen.beginPath() pen.beginPath()
last_offcurves = [] last_offcurves = []
points_required = self.__points_required
for i, (segment_type, points) in enumerate(segments): for i, (segment_type, points) in enumerate(segments):
if segment_type in ("move", "line"): if segment_type in points_required:
assert len(points) == 1, ( n, op = points_required[segment_type]
"illegal line segment point count: %d" % len(points)) assert op(len(points), n), (
pt, smooth, name, kwargs = points[0] f"illegal {segment_type!r} segment point count: "
pen.addPoint(pt, segment_type, smooth, name, **kwargs) f"expected {n}, got {len(points)}"
elif segment_type == "qcurve": )
assert len(points) >= 2, (
"illegal qcurve segment point count: %d" % len(points))
offcurves = points[:-1] offcurves = points[:-1]
if offcurves:
if i == 0: if i == 0:
# any off-curve points preceding the first on-curve # any off-curve points preceding the first on-curve
# will be appended at the end of the contour # will be appended at the end of the contour
@ -241,6 +203,7 @@ class Cu2QuPointPen(BasePointToSegmentPen):
pen.addPoint(pt, None, smooth, name, **kwargs) pen.addPoint(pt, None, smooth, name, **kwargs)
pt, smooth, name, kwargs = points[-1] pt, smooth, name, kwargs = points[-1]
if pt is None: if pt is None:
assert segment_type == "qcurve"
# special quadratic contour with no on-curve points: # special quadratic contour with no on-curve points:
# we need to skip the "None" point. See also the Pen # we need to skip the "None" point. See also the Pen
# protocol's qCurveTo() method and fontTools.pens.basePen # protocol's qCurveTo() method and fontTools.pens.basePen
@ -248,9 +211,7 @@ class Cu2QuPointPen(BasePointToSegmentPen):
else: else:
pen.addPoint(pt, segment_type, smooth, name, **kwargs) pen.addPoint(pt, segment_type, smooth, name, **kwargs)
else: else:
# 'curve' segments must have been converted to 'qcurve' by now raise AssertionError("unexpected segment type: %r" % segment_type)
raise AssertionError(
"unexpected segment type: %r" % segment_type)
for (pt, smooth, name, kwargs) in last_offcurves: for (pt, smooth, name, kwargs) in last_offcurves:
pen.addPoint(pt, None, smooth, name, **kwargs) pen.addPoint(pt, None, smooth, name, **kwargs)
pen.endPath() pen.endPath()
@ -258,3 +219,107 @@ class Cu2QuPointPen(BasePointToSegmentPen):
def addComponent(self, baseGlyphName, transformation): def addComponent(self, baseGlyphName, transformation):
assert self.currentPath is None assert self.currentPath is None
self.pen.addComponent(baseGlyphName, transformation) self.pen.addComponent(baseGlyphName, transformation)
class Cu2QuMultiPen:
"""A filter multi-pen to convert cubic bezier curves to quadratic b-splines
in a interpolation-compatible manner, using the FontTools SegmentPen protocol.
Args:
other_pens: list of SegmentPens used to draw the transformed outlines.
max_err: maximum approximation error in font units. For optimal results,
if you know the UPEM of the font, we recommend setting this to a
value equal, or close to UPEM / 1000.
reverse_direction: flip the contours' direction but keep starting point.
This pen does not follow the normal SegmentPen protocol. Instead, its
moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are
arguments that would normally be passed to a SegmentPen, one item for
each of the pens in other_pens.
"""
# TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
# Remove start_pts and _add_moveTO
def __init__(self, other_pens, max_err, reverse_direction=False):
if reverse_direction:
other_pens = [
ReverseContourPen(pen, outputImpliedClosingLine=True)
for pen in other_pens
]
self.pens = other_pens
self.max_err = max_err
self.start_pts = None
self.current_pts = None
def _check_contour_is_open(self):
if self.current_pts is None:
raise AssertionError("moveTo is required")
def _check_contour_is_closed(self):
if self.current_pts is not None:
raise AssertionError("closePath or endPath is required")
def _add_moveTo(self):
if self.start_pts is not None:
for pt, pen in zip(self.start_pts, self.pens):
pen.moveTo(*pt)
self.start_pts = None
def moveTo(self, pts):
self._check_contour_is_closed()
self.start_pts = self.current_pts = pts
self._add_moveTo()
def lineTo(self, pts):
self._check_contour_is_open()
self._add_moveTo()
for pt, pen in zip(pts, self.pens):
pen.lineTo(*pt)
self.current_pts = pts
def qCurveTo(self, pointsList):
self._check_contour_is_open()
if len(pointsList[0]) == 1:
self.lineTo([(points[0],) for points in pointsList])
return
self._add_moveTo()
current_pts = []
for points, pen in zip(pointsList, self.pens):
pen.qCurveTo(*points)
current_pts.append((points[-1],))
self.current_pts = current_pts
def _curves_to_quadratic(self, pointsList):
curves = []
for current_pt, points in zip(self.current_pts, pointsList):
curves.append(current_pt + points)
quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves))
pointsList = []
for quadratic in quadratics:
pointsList.append(quadratic[1:])
self.qCurveTo(pointsList)
def curveTo(self, pointsList):
self._check_contour_is_open()
self._curves_to_quadratic(pointsList)
def closePath(self):
self._check_contour_is_open()
if self.start_pts is None:
for pen in self.pens:
pen.closePath()
self.current_pts = self.start_pts = None
def endPath(self):
self._check_contour_is_open()
if self.start_pts is None:
for pen in self.pens:
pen.endPath()
self.current_pts = self.start_pts = None
def addComponent(self, glyphName, transformations):
self._check_contour_is_closed()
for trans, pen in zip(transformations, self.pens):
pen.addComponent(glyphName, trans)

View File

@ -4,7 +4,6 @@ from fontTools.pens.recordingPen import RecordingPen
class _PassThruComponentsMixin(object): class _PassThruComponentsMixin(object):
def addComponent(self, glyphName, transformation, **kwargs): def addComponent(self, glyphName, transformation, **kwargs):
self._outPen.addComponent(glyphName, transformation, **kwargs) self._outPen.addComponent(glyphName, transformation, **kwargs)
@ -57,24 +56,31 @@ class FilterPen(_PassThruComponentsMixin, AbstractPen):
def __init__(self, outPen): def __init__(self, outPen):
self._outPen = outPen self._outPen = outPen
self.current_pt = None
def moveTo(self, pt): def moveTo(self, pt):
self._outPen.moveTo(pt) self._outPen.moveTo(pt)
self.current_pt = pt
def lineTo(self, pt): def lineTo(self, pt):
self._outPen.lineTo(pt) self._outPen.lineTo(pt)
self.current_pt = pt
def curveTo(self, *points): def curveTo(self, *points):
self._outPen.curveTo(*points) self._outPen.curveTo(*points)
self.current_pt = points[-1]
def qCurveTo(self, *points): def qCurveTo(self, *points):
self._outPen.qCurveTo(*points) self._outPen.qCurveTo(*points)
self.current_pt = points[-1]
def closePath(self): def closePath(self):
self._outPen.closePath() self._outPen.closePath()
self.current_pt = None
def endPath(self): def endPath(self):
self._outPen.endPath() self._outPen.endPath()
self.current_pt = None
class ContourFilterPen(_PassThruComponentsMixin, RecordingPen): class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):

View File

@ -65,9 +65,7 @@ class HashPointPen(AbstractPointPen):
pt_type = segmentType[0] pt_type = segmentType[0]
self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}") self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
def addComponent( def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
self, baseGlyphName, transformation, identifier=None, **kwargs
):
tr = "".join([f"{t:+}" for t in transformation]) tr = "".join([f"{t:+}" for t in transformation])
self.data.append("[") self.data.append("[")
try: try:

View File

@ -1,22 +1,20 @@
from fontTools.pens.basePen import BasePen, OpenContourError from fontTools.pens.basePen import BasePen, OpenContourError
try: try:
import cython import cython
except ImportError:
COMPILED = cython.compiled
except (AttributeError, ImportError):
# if cython not installed, use mock module with no-op decorators and types # if cython not installed, use mock module with no-op decorators and types
from fontTools.misc import cython from fontTools.misc import cython
if cython.compiled:
# Yep, I'm compiled.
COMPILED = True
else:
# Just a lowly interpreted script.
COMPILED = False COMPILED = False
__all__ = ["MomentsPen"] __all__ = ["MomentsPen"]
class MomentsPen(BasePen):
class MomentsPen(BasePen):
def __init__(self, glyphset=None): def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset) BasePen.__init__(self, glyphset)
@ -39,9 +37,7 @@ class MomentsPen(BasePen):
p0 = self._getCurrentPoint() p0 = self._getCurrentPoint()
if p0 != self.__startPoint: if p0 != self.__startPoint:
# Green theorem is not defined on open contours. # Green theorem is not defined on open contours.
raise OpenContourError( raise OpenContourError("Green theorem is not defined on open contours.")
"Green theorem is not defined on open contours."
)
@cython.locals(r0=cython.double) @cython.locals(r0=cython.double)
@cython.locals(r1=cython.double) @cython.locals(r1=cython.double)
@ -78,10 +74,30 @@ class MomentsPen(BasePen):
self.area += -r0 / 2 - r1 / 2 + x0 * (y0 + y1) / 2 self.area += -r0 / 2 - r1 / 2 + x0 * (y0 + y1) / 2
self.momentX += -r2 * y0 / 6 - r3 / 3 - r5 * x1 / 6 + r6 * (r7 + y1) / 6 self.momentX += -r2 * y0 / 6 - r3 / 3 - r5 * x1 / 6 + r6 * (r7 + y1) / 6
self.momentY += -r0*y1/6 - r8*x1/6 - r9*x1/6 + x0*(r8 + r9 + y0*y1)/6 self.momentY += (
self.momentXX += -r10*y0/12 - r10*y1/4 - r2*r5/12 - r4*r6*x1/12 + x0**3*(3*y0 + y1)/12 -r0 * y1 / 6 - r8 * x1 / 6 - r9 * x1 / 6 + x0 * (r8 + r9 + y0 * y1) / 6
self.momentXY += -r2*r8/24 - r2*r9/8 - r3*r7/24 + r6*(r7*y1 + 3*r8 + r9)/24 - x0*x1*(r8 - r9)/12 )
self.momentYY += -r0*r9/12 - r1*r8/12 - r11*x1/12 - r12*x1/12 + x0*(r11 + r12 + r8*y1 + r9*y0)/12 self.momentXX += (
-r10 * y0 / 12
- r10 * y1 / 4
- r2 * r5 / 12
- r4 * r6 * x1 / 12
+ x0**3 * (3 * y0 + y1) / 12
)
self.momentXY += (
-r2 * r8 / 24
- r2 * r9 / 8
- r3 * r7 / 24
+ r6 * (r7 * y1 + 3 * r8 + r9) / 24
- x0 * x1 * (r8 - r9) / 12
)
self.momentYY += (
-r0 * r9 / 12
- r1 * r8 / 12
- r11 * x1 / 12
- r12 * x1 / 12
+ x0 * (r11 + r12 + r8 * y1 + r9 * y0) / 12
)
@cython.locals(r0=cython.double) @cython.locals(r0=cython.double)
@cython.locals(r1=cython.double) @cython.locals(r1=cython.double)
@ -200,12 +216,99 @@ class MomentsPen(BasePen):
r52 = 10 * y1 r52 = 10 * y1
r53 = 12 * y1 r53 = 12 * y1
self.area += -r1/6 - r3/6 + x0*(r0 + r5 + y2)/6 + x1*y2/3 - y0*(r4 + x2)/6 self.area += (
self.momentX += -r11*(-r10 + y1)/30 + r12*(r13 + r8 + y2)/30 + r6*y2/15 - r7*r8/30 - r7*r9/30 + x0*(r14 - r15 - r16*y0 + r17)/30 - y0*(r11 + 2*r6 + r7)/30 -r1 / 6
self.momentY += -r18/30 - r20*x2/30 - r23/30 - r24*(r16 + x2)/30 + x0*(r0*y2 + r20 + r21 + r25 + r26 + r8*y0)/30 + x1*y2*(r10 + y1)/15 - y0*(r1 + r17)/30 - r3 / 6
self.momentXX += r12*(r1 - 5*r15 - r34*y0 + r36 + r9*x1)/420 + 2*r27*y2/105 - r28*r29/420 - r28*y2/4 - r31*(r0 - 3*y2)/420 - r6*x2*(r0 - r32)/105 + x0**3*(r30 + 21*y0 + y2)/84 - x0*(r0*r7 + r15*r37 - r2*r37 - r33*y2 + r38*y0 - r39 - r40 + r5*r7)/420 - y0*(8*r27 + 5*r28 + r31 + r33*x2)/420 + x0 * (r0 + r5 + y2) / 6
self.momentXY += r12*(r13*y2 + 3*r21 + 105*r24 + r41*y0 + r42 + r46*y1)/840 - r16*x2*(r43 - r44)/840 - r21*r7/8 - r24*(r38 + r45*x1 + 3*r7)/840 - r41*r7*y2/840 - r42*r7/840 + r6*y2*(r32 + r8)/210 + x0*(-r15*r8 + r16*r25 + r18 + r21*r47 - r24*r34 - r26*x2 + r35*r46 + r48)/420 - y0*(r16*r2 + r30*r7 + r35*r45 + r39 + r40)/420 + x1 * y2 / 3
self.momentYY += -r2*r42/420 - r22*r29/420 - r24*(r14 + r36 + r52*x2)/420 - r49*x2/420 - r50*x2/12 - r51*(r47 + x2)/84 + x0*(r19*r46 + r21*r5 + r21*r52 + r24*r29 + r25*r53 + r26*y2 + r42*y0 + r49 + 5*r50 + 35*r51)/420 + x1*y2*(r43 + r44 + r9*y1)/210 - y0*(r19*r45 + r2*r53 - r21*r4 + r48)/420 - y0 * (r4 + x2) / 6
)
self.momentX += (
-r11 * (-r10 + y1) / 30
+ r12 * (r13 + r8 + y2) / 30
+ r6 * y2 / 15
- r7 * r8 / 30
- r7 * r9 / 30
+ x0 * (r14 - r15 - r16 * y0 + r17) / 30
- y0 * (r11 + 2 * r6 + r7) / 30
)
self.momentY += (
-r18 / 30
- r20 * x2 / 30
- r23 / 30
- r24 * (r16 + x2) / 30
+ x0 * (r0 * y2 + r20 + r21 + r25 + r26 + r8 * y0) / 30
+ x1 * y2 * (r10 + y1) / 15
- y0 * (r1 + r17) / 30
)
self.momentXX += (
r12 * (r1 - 5 * r15 - r34 * y0 + r36 + r9 * x1) / 420
+ 2 * r27 * y2 / 105
- r28 * r29 / 420
- r28 * y2 / 4
- r31 * (r0 - 3 * y2) / 420
- r6 * x2 * (r0 - r32) / 105
+ x0**3 * (r30 + 21 * y0 + y2) / 84
- x0
* (
r0 * r7
+ r15 * r37
- r2 * r37
- r33 * y2
+ r38 * y0
- r39
- r40
+ r5 * r7
)
/ 420
- y0 * (8 * r27 + 5 * r28 + r31 + r33 * x2) / 420
)
self.momentXY += (
r12 * (r13 * y2 + 3 * r21 + 105 * r24 + r41 * y0 + r42 + r46 * y1) / 840
- r16 * x2 * (r43 - r44) / 840
- r21 * r7 / 8
- r24 * (r38 + r45 * x1 + 3 * r7) / 840
- r41 * r7 * y2 / 840
- r42 * r7 / 840
+ r6 * y2 * (r32 + r8) / 210
+ x0
* (
-r15 * r8
+ r16 * r25
+ r18
+ r21 * r47
- r24 * r34
- r26 * x2
+ r35 * r46
+ r48
)
/ 420
- y0 * (r16 * r2 + r30 * r7 + r35 * r45 + r39 + r40) / 420
)
self.momentYY += (
-r2 * r42 / 420
- r22 * r29 / 420
- r24 * (r14 + r36 + r52 * x2) / 420
- r49 * x2 / 420
- r50 * x2 / 12
- r51 * (r47 + x2) / 84
+ x0
* (
r19 * r46
+ r21 * r5
+ r21 * r52
+ r24 * r29
+ r25 * r53
+ r26 * y2
+ r42 * y0
+ r49
+ 5 * r50
+ 35 * r51
)
/ 420
+ x1 * y2 * (r43 + r44 + r9 * y1) / 210
- y0 * (r19 * r45 + r2 * r53 - r21 * r4 + r48) / 420
)
@cython.locals(r0=cython.double) @cython.locals(r0=cython.double)
@cython.locals(r1=cython.double) @cython.locals(r1=cython.double)
@ -484,20 +587,296 @@ class MomentsPen(BasePen):
r131 = 189 * r53 r131 = 189 * r53
r132 = 90 * y2 r132 = 90 * y2
self.area += -r1/20 - r3/20 - r4*(x2 + x3)/20 + x0*(r7 + r8 + 10*y0 + y3)/20 + 3*x1*(y2 + y3)/20 + 3*x2*y3/10 - y0*(r5 + r6 + x3)/20 self.area += (
self.momentX += r11/840 - r13/8 - r14/3 - r17*(-r15 + r8)/840 + r19*(r8 + 2*y3)/840 + r20*(r0 + r21 + 56*y0 + y3)/168 + r29*(-r23 + r25 + r28)/840 - r4*(10*r12 + r17 + r22)/840 + x0*(12*r27 + r30*y2 + r34 - r35*x1 - r37 - r38*y0 + r39*x1 - r4*x3 + r45)/840 - y0*(r17 + r30*x2 + r31*x1 + r32 + r33 + 18*r9)/840 -r1 / 20
self.momentY += -r4*(r25 + r58)/840 - r47/8 - r50/840 - r52/6 - r54*(r6 + 2*x3)/840 - r55*(r56 + r57 + x3)/168 + x0*(r35*y1 + r40*y0 + r44*y2 + 18*r48 + 140*r55 + r59 + r63 + 12*r64 + r65 + r66)/840 + x1*(r24*y1 + 10*r51 + r59 + r60 + r7*y3)/280 + x2*y3*(r15 + r8)/56 - y0*(r16*y1 + r31*y2 + r44*x2 + r45 + r61 - r62*x1)/840 - r3 / 20
self.momentXX += -r12*r72*(-r40 + r8)/9240 + 3*r18*(r28 + r34 - r38*y1 + r75)/3080 + r20*(r24*x3 - r72*y0 - r76*y0 - r77*y0 + r78 + r79*y3 + r80*y1 + 210*r81 + r84)/9240 - r29*(r12*r21 + 14*r13 + r44*r9 - r73*y3 + 54*r86 - 84*r87 - r89 - r90)/9240 - r4*(70*r12*x2 + 27*r67 + 42*r68 + r74)/9240 + 3*r67*y3/220 - r68*r69/9240 - r68*y3/4 - r70*r9*(-r62 + y2)/9240 + 3*r71*(r24 + r40)/3080 + x0**3*(r24 + r44 + 165*y0 + y3)/660 + x0*(r100*r27 + 162*r101 + r102 + r11 + 63*r18*y3 + r27*r91 - r33*y0 - r37*x3 + r43*x3 - r73*y0 - r88*y1 + r92*y2 - r93*y0 - 9*r94 - r95*y0 - r96*y0 - r97*y1 - 18*r98 + r99*x1*y3)/9240 - y0*(r12*r56 + r12*r80 + r32*x3 + 45*r67 + 14*r68 + 126*r71 + r74 + r85*r91 + 135*r9*x1 + r92*x2)/9240 - r4 * (x2 + x3) / 20
self.momentXY += -r103*r12/18480 - r12*r51/8 - 3*r14*y2/44 + 3*r18*(r105 + r2*y1 + 18*r46 + 15*r48 + 7*r51)/6160 + r20*(1260*r106 + r107*y1 + r108 + 28*r109 + r110 + r111 + r112 + 30*r46 + 2310*r55 + r66)/18480 - r54*(7*r12 + 18*r85 + 15*r9)/18480 - r55*(r33 + r73 + r93 + r95 + r96 + r97)/18480 - r7*(42*r13 + r82*x3 + 28*r87 + r89 + r90)/18480 - 3*r85*(r48 - r66)/220 + 3*r9*y3*(r62 + 2*y2)/440 + x0*(-r1*y0 - 84*r106*x2 + r109*r56 + 54*r114 + r117*y1 + 15*r118 + 21*r119 + 81*r120 + r121*r46 + 54*r122 + 60*r123 + r124 - r21*x3*y0 + r23*y3 - r54*x3 - r55*r72 - r55*r76 - r55*r77 + r57*y0*y3 + r60*x3 + 84*r81*y0 + 189*r81*y1)/9240 + x1*(r104*r27 - r105*x3 - r113*r53 + 63*r114 + r115 - r16*r53 + 28*r47 + r51*r80)/3080 - y0*(54*r101 + r102 + r116*r5 + r117*x3 + 21*r13 - r19*y3 + r22*y3 + r78*x3 + 189*r83*x2 + 60*r86 + 81*r9*y1 + 15*r94 + 54*r98)/9240 + x0 * (r7 + r8 + 10 * y0 + y3) / 20
self.momentYY += -r103*r116/9240 - r125*r70/9240 - r126*x3/12 - 3*r127*(r26 + r38)/3080 - r128*(r26 + r30 + x3)/660 - r4*(r112*x3 + r115 - 14*r119 + 84*r47)/9240 - r52*r69/9240 - r54*(r58 + r61 + r75)/9240 - r55*(r100*y1 + r121*y2 + r26*y3 + r79*y2 + r84 + 210*x2*y1)/9240 + x0*(r108*y1 + r110*y0 + r111*y0 + r112*y0 + 45*r125 + 14*r126 + 126*r127 + 770*r128 + 42*r129 + r130 + r131*y2 + r132*r64 + 135*r48*y1 + 630*r55*y1 + 126*r55*y2 + 14*r55*y3 + r63*y3 + r65*y3 + r66*y0)/9240 + x1*(27*r125 + 42*r126 + 70*r129 + r130 + r39*r53 + r44*r48 + 27*r53*y2 + 54*r64*y2)/3080 + 3*x2*y3*(r48 + r66 + r8*y3)/220 - y0*(r100*r46 + 18*r114 - 9*r118 - 27*r120 - 18*r122 - 30*r123 + r124 + r131*x2 + r132*x3*y1 + 162*r42*y1 + r50 + 63*r53*x3 + r64*r99)/9240 + 3 * x1 * (y2 + y3) / 20
+ 3 * x2 * y3 / 10
- y0 * (r5 + r6 + x3) / 20
)
self.momentX += (
r11 / 840
- r13 / 8
- r14 / 3
- r17 * (-r15 + r8) / 840
+ r19 * (r8 + 2 * y3) / 840
+ r20 * (r0 + r21 + 56 * y0 + y3) / 168
+ r29 * (-r23 + r25 + r28) / 840
- r4 * (10 * r12 + r17 + r22) / 840
+ x0
* (
12 * r27
+ r30 * y2
+ r34
- r35 * x1
- r37
- r38 * y0
+ r39 * x1
- r4 * x3
+ r45
)
/ 840
- y0 * (r17 + r30 * x2 + r31 * x1 + r32 + r33 + 18 * r9) / 840
)
self.momentY += (
-r4 * (r25 + r58) / 840
- r47 / 8
- r50 / 840
- r52 / 6
- r54 * (r6 + 2 * x3) / 840
- r55 * (r56 + r57 + x3) / 168
+ x0
* (
r35 * y1
+ r40 * y0
+ r44 * y2
+ 18 * r48
+ 140 * r55
+ r59
+ r63
+ 12 * r64
+ r65
+ r66
)
/ 840
+ x1 * (r24 * y1 + 10 * r51 + r59 + r60 + r7 * y3) / 280
+ x2 * y3 * (r15 + r8) / 56
- y0 * (r16 * y1 + r31 * y2 + r44 * x2 + r45 + r61 - r62 * x1) / 840
)
self.momentXX += (
-r12 * r72 * (-r40 + r8) / 9240
+ 3 * r18 * (r28 + r34 - r38 * y1 + r75) / 3080
+ r20
* (
r24 * x3
- r72 * y0
- r76 * y0
- r77 * y0
+ r78
+ r79 * y3
+ r80 * y1
+ 210 * r81
+ r84
)
/ 9240
- r29
* (
r12 * r21
+ 14 * r13
+ r44 * r9
- r73 * y3
+ 54 * r86
- 84 * r87
- r89
- r90
)
/ 9240
- r4 * (70 * r12 * x2 + 27 * r67 + 42 * r68 + r74) / 9240
+ 3 * r67 * y3 / 220
- r68 * r69 / 9240
- r68 * y3 / 4
- r70 * r9 * (-r62 + y2) / 9240
+ 3 * r71 * (r24 + r40) / 3080
+ x0**3 * (r24 + r44 + 165 * y0 + y3) / 660
+ x0
* (
r100 * r27
+ 162 * r101
+ r102
+ r11
+ 63 * r18 * y3
+ r27 * r91
- r33 * y0
- r37 * x3
+ r43 * x3
- r73 * y0
- r88 * y1
+ r92 * y2
- r93 * y0
- 9 * r94
- r95 * y0
- r96 * y0
- r97 * y1
- 18 * r98
+ r99 * x1 * y3
)
/ 9240
- y0
* (
r12 * r56
+ r12 * r80
+ r32 * x3
+ 45 * r67
+ 14 * r68
+ 126 * r71
+ r74
+ r85 * r91
+ 135 * r9 * x1
+ r92 * x2
)
/ 9240
)
self.momentXY += (
-r103 * r12 / 18480
- r12 * r51 / 8
- 3 * r14 * y2 / 44
+ 3 * r18 * (r105 + r2 * y1 + 18 * r46 + 15 * r48 + 7 * r51) / 6160
+ r20
* (
1260 * r106
+ r107 * y1
+ r108
+ 28 * r109
+ r110
+ r111
+ r112
+ 30 * r46
+ 2310 * r55
+ r66
)
/ 18480
- r54 * (7 * r12 + 18 * r85 + 15 * r9) / 18480
- r55 * (r33 + r73 + r93 + r95 + r96 + r97) / 18480
- r7 * (42 * r13 + r82 * x3 + 28 * r87 + r89 + r90) / 18480
- 3 * r85 * (r48 - r66) / 220
+ 3 * r9 * y3 * (r62 + 2 * y2) / 440
+ x0
* (
-r1 * y0
- 84 * r106 * x2
+ r109 * r56
+ 54 * r114
+ r117 * y1
+ 15 * r118
+ 21 * r119
+ 81 * r120
+ r121 * r46
+ 54 * r122
+ 60 * r123
+ r124
- r21 * x3 * y0
+ r23 * y3
- r54 * x3
- r55 * r72
- r55 * r76
- r55 * r77
+ r57 * y0 * y3
+ r60 * x3
+ 84 * r81 * y0
+ 189 * r81 * y1
)
/ 9240
+ x1
* (
r104 * r27
- r105 * x3
- r113 * r53
+ 63 * r114
+ r115
- r16 * r53
+ 28 * r47
+ r51 * r80
)
/ 3080
- y0
* (
54 * r101
+ r102
+ r116 * r5
+ r117 * x3
+ 21 * r13
- r19 * y3
+ r22 * y3
+ r78 * x3
+ 189 * r83 * x2
+ 60 * r86
+ 81 * r9 * y1
+ 15 * r94
+ 54 * r98
)
/ 9240
)
self.momentYY += (
-r103 * r116 / 9240
- r125 * r70 / 9240
- r126 * x3 / 12
- 3 * r127 * (r26 + r38) / 3080
- r128 * (r26 + r30 + x3) / 660
- r4 * (r112 * x3 + r115 - 14 * r119 + 84 * r47) / 9240
- r52 * r69 / 9240
- r54 * (r58 + r61 + r75) / 9240
- r55
* (r100 * y1 + r121 * y2 + r26 * y3 + r79 * y2 + r84 + 210 * x2 * y1)
/ 9240
+ x0
* (
r108 * y1
+ r110 * y0
+ r111 * y0
+ r112 * y0
+ 45 * r125
+ 14 * r126
+ 126 * r127
+ 770 * r128
+ 42 * r129
+ r130
+ r131 * y2
+ r132 * r64
+ 135 * r48 * y1
+ 630 * r55 * y1
+ 126 * r55 * y2
+ 14 * r55 * y3
+ r63 * y3
+ r65 * y3
+ r66 * y0
)
/ 9240
+ x1
* (
27 * r125
+ 42 * r126
+ 70 * r129
+ r130
+ r39 * r53
+ r44 * r48
+ 27 * r53 * y2
+ 54 * r64 * y2
)
/ 3080
+ 3 * x2 * y3 * (r48 + r66 + r8 * y3) / 220
- y0
* (
r100 * r46
+ 18 * r114
- 9 * r118
- 27 * r120
- 18 * r122
- 30 * r123
+ r124
+ r131 * x2
+ r132 * x3 * y1
+ 162 * r42 * y1
+ r50
+ 63 * r53 * x3
+ r64 * r99
)
/ 9240
)
if __name__ == '__main__':
if __name__ == "__main__":
from fontTools.misc.symfont import x, y, printGreenPen from fontTools.misc.symfont import x, y, printGreenPen
printGreenPen('MomentsPen', [
('area', 1), printGreenPen(
('momentX', x), "MomentsPen",
('momentY', y), [
('momentXX', x**2), ("area", 1),
('momentXY', x*y), ("momentX", x),
('momentYY', y**2), ("momentY", y),
]) ("momentXX", x**2),
("momentXY", x * y),
("momentYY", y**2),
],
)

View File

@ -2,7 +2,12 @@
"""Calculate the perimeter of a glyph.""" """Calculate the perimeter of a glyph."""
from fontTools.pens.basePen import BasePen from fontTools.pens.basePen import BasePen
from fontTools.misc.bezierTools import approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC, calcCubicArcLengthC from fontTools.misc.bezierTools import (
approximateQuadraticArcLengthC,
calcQuadraticArcLengthC,
approximateCubicArcLengthC,
calcCubicArcLengthC,
)
import math import math
@ -12,8 +17,8 @@ __all__ = ["PerimeterPen"]
def _distance(p0, p1): def _distance(p0, p1):
return math.hypot(p0[0] - p1[0], p0[1] - p1[1]) return math.hypot(p0[0] - p1[0], p0[1] - p1[1])
class PerimeterPen(BasePen):
class PerimeterPen(BasePen):
def __init__(self, glyphset=None, tolerance=0.005): def __init__(self, glyphset=None, tolerance=0.005):
BasePen.__init__(self, glyphset) BasePen.__init__(self, glyphset)
self.value = 0 self.value = 0
@ -22,8 +27,14 @@ class PerimeterPen(BasePen):
# Choose which algorithm to use for quadratic and for cubic. # Choose which algorithm to use for quadratic and for cubic.
# Quadrature is faster but has fixed error characteristic with no strong # Quadrature is faster but has fixed error characteristic with no strong
# error bound. The cutoff points are derived empirically. # error bound. The cutoff points are derived empirically.
self._addCubic = self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive self._addCubic = (
self._addQuadratic = self._addQuadraticQuadrature if tolerance >= 0.00075 else self._addQuadraticExact self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive
)
self._addQuadratic = (
self._addQuadraticQuadrature
if tolerance >= 0.00075
else self._addQuadraticExact
)
def _moveTo(self, p0): def _moveTo(self, p0):
self.__startPoint = p0 self.__startPoint = p0

View File

@ -119,7 +119,7 @@ class PointInsidePen(BasePen):
by = (y3 - y2) * 3.0 - cy by = (y3 - y2) * 3.0 - cy
ay = y4 - dy - cy - by ay = y4 - dy - cy - by
solutions = sorted(solveCubic(ay, by, cy, dy - y)) solutions = sorted(solveCubic(ay, by, cy, dy - y))
solutions = [t for t in solutions if -0. <= t <= 1.] solutions = [t for t in solutions if -0.0 <= t <= 1.0]
if not solutions: if not solutions:
return return
@ -175,7 +175,9 @@ class PointInsidePen(BasePen):
b = (y2 - c) * 2.0 b = (y2 - c) * 2.0
a = y3 - c - b a = y3 - c - b
solutions = sorted(solveQuadratic(a, b, c - y)) solutions = sorted(solveQuadratic(a, b, c - y))
solutions = [t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON] solutions = [
t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON
]
if not solutions: if not solutions:
return return
# XXX # XXX

View File

@ -13,9 +13,10 @@ For instance, whether or not a point is smooth, and its name.
""" """
import math import math
from typing import Any, Optional, Tuple from typing import Any, Optional, Tuple, Dict
from fontTools.pens.basePen import AbstractPen, PenError from fontTools.pens.basePen import AbstractPen, PenError
from fontTools.misc.transform import DecomposedTransform
__all__ = [ __all__ = [
"AbstractPointPen", "AbstractPointPen",
@ -45,7 +46,7 @@ class AbstractPointPen:
smooth: bool = False, smooth: bool = False,
name: Optional[str] = None, name: Optional[str] = None,
identifier: Optional[str] = None, identifier: Optional[str] = None,
**kwargs: Any **kwargs: Any,
) -> None: ) -> None:
"""Add a point to the current sub path.""" """Add a point to the current sub path."""
raise NotImplementedError raise NotImplementedError
@ -55,11 +56,27 @@ class AbstractPointPen:
baseGlyphName: str, baseGlyphName: str,
transformation: Tuple[float, float, float, float, float, float], transformation: Tuple[float, float, float, float, float, float],
identifier: Optional[str] = None, identifier: Optional[str] = None,
**kwargs: Any **kwargs: Any,
) -> None: ) -> None:
"""Add a sub glyph.""" """Add a sub glyph."""
raise NotImplementedError raise NotImplementedError
def addVarComponent(
self,
glyphName: str,
transformation: DecomposedTransform,
location: Dict[str, float],
identifier: Optional[str] = None,
**kwargs: Any,
) -> None:
"""Add a VarComponent sub glyph. The 'transformation' argument
must be a DecomposedTransform from the fontTools.misc.transform module,
and the 'location' argument must be a dictionary mapping axis tags
to their locations.
"""
# ttGlyphSet decomposes for us
raise AttributeError
class BasePointToSegmentPen(AbstractPointPen): class BasePointToSegmentPen(AbstractPointPen):
""" """
@ -154,8 +171,9 @@ class BasePointToSegmentPen(AbstractPointPen):
self._flushContour(segments) self._flushContour(segments)
def addPoint(self, pt, segmentType=None, smooth=False, name=None, def addPoint(
identifier=None, **kwargs): self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
):
if self.currentPath is None: if self.currentPath is None:
raise PenError("Path not begun") raise PenError("Path not begun")
self.currentPath.append((pt, segmentType, smooth, name, kwargs)) self.currentPath.append((pt, segmentType, smooth, name, kwargs))
@ -388,8 +406,9 @@ class GuessSmoothPointPen(AbstractPointPen):
self._outPen.endPath() self._outPen.endPath()
self._points = None self._points = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, def addPoint(
identifier=None, **kwargs): self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
):
if self._points is None: if self._points is None:
raise PenError("Path not begun") raise PenError("Path not begun")
if identifier is not None: if identifier is not None:
@ -403,6 +422,15 @@ class GuessSmoothPointPen(AbstractPointPen):
kwargs["identifier"] = identifier kwargs["identifier"] = identifier
self._outPen.addComponent(glyphName, transformation, **kwargs) self._outPen.addComponent(glyphName, transformation, **kwargs)
def addVarComponent(
self, glyphName, transformation, location, identifier=None, **kwargs
):
if self._points is not None:
raise PenError("VarComponents must be added before or after contours")
if identifier is not None:
kwargs["identifier"] = identifier
self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
class ReverseContourPointPen(AbstractPointPen): class ReverseContourPointPen(AbstractPointPen):
""" """
@ -464,7 +492,9 @@ class ReverseContourPointPen(AbstractPointPen):
lastSegmentType = nextSegmentType lastSegmentType = nextSegmentType
else: else:
segmentType = None segmentType = None
pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs) pen.addPoint(
pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
)
pen.endPath() pen.endPath()
def beginPath(self, identifier=None, **kwargs): def beginPath(self, identifier=None, **kwargs):
@ -480,7 +510,9 @@ class ReverseContourPointPen(AbstractPointPen):
self._flushContour() self._flushContour()
self.currentContour = None self.currentContour = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): def addPoint(
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
):
if self.currentContour is None: if self.currentContour is None:
raise PenError("Path not begun") raise PenError("Path not begun")
if identifier is not None: if identifier is not None:

View File

@ -5,11 +5,11 @@ __all__ = ["QtPen"]
class QtPen(BasePen): class QtPen(BasePen):
def __init__(self, glyphSet, path=None): def __init__(self, glyphSet, path=None):
BasePen.__init__(self, glyphSet) BasePen.__init__(self, glyphSet)
if path is None: if path is None:
from PyQt5.QtGui import QPainterPath from PyQt5.QtGui import QPainterPath
path = QPainterPath() path = QPainterPath()
self.path = path self.path = path

View File

@ -0,0 +1,105 @@
# Copyright 2016 Google Inc. All Rights Reserved.
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from fontTools.qu2cu import quadratic_to_curves
from fontTools.pens.filterPen import ContourFilterPen
from fontTools.pens.reverseContourPen import ReverseContourPen
import math
class Qu2CuPen(ContourFilterPen):
"""A filter pen to convert quadratic bezier splines to cubic curves
using the FontTools SegmentPen protocol.
Args:
other_pen: another SegmentPen used to draw the transformed outline.
max_err: maximum approximation error in font units. For optimal results,
if you know the UPEM of the font, we recommend setting this to a
value equal, or close to UPEM / 1000.
reverse_direction: flip the contours' direction but keep starting point.
stats: a dictionary counting the point numbers of cubic segments.
"""
def __init__(
self,
other_pen,
max_err,
all_cubic=False,
reverse_direction=False,
stats=None,
):
if reverse_direction:
other_pen = ReverseContourPen(other_pen)
super().__init__(other_pen)
self.all_cubic = all_cubic
self.max_err = max_err
self.stats = stats
def _quadratics_to_curve(self, q):
curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
if self.stats is not None:
for curve in curves:
n = str(len(curve) - 2)
self.stats[n] = self.stats.get(n, 0) + 1
for curve in curves:
if len(curve) == 4:
yield ("curveTo", curve[1:])
else:
yield ("qCurveTo", curve[1:])
def filterContour(self, contour):
quadratics = []
currentPt = None
newContour = []
for op, args in contour:
if op == "qCurveTo" and (
self.all_cubic or (len(args) > 2 and args[-1] is not None)
):
if args[-1] is None:
raise NotImplementedError(
"oncurve-less contours with all_cubic not implemented"
)
quadratics.append((currentPt,) + args)
else:
if quadratics:
newContour.extend(self._quadratics_to_curve(quadratics))
quadratics = []
newContour.append((op, args))
currentPt = args[-1] if args else None
if quadratics:
newContour.extend(self._quadratics_to_curve(quadratics))
if not self.all_cubic:
# Add back implicit oncurve points
contour = newContour
newContour = []
for op, args in contour:
if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
pt0 = newContour[-1][1][-2]
pt1 = newContour[-1][1][-1]
pt2 = args[0]
if (
pt1 is not None
and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
):
newArgs = newContour[-1][1][:-1] + args
newContour[-1] = (op, newArgs)
continue
newContour.append((op, args))
return newContour

View File

@ -42,4 +42,3 @@ class QuartzPen(BasePen):
def _closePath(self): def _closePath(self):
CGPathCloseSubpath(self.path) CGPathCloseSubpath(self.path)

View File

@ -48,20 +48,31 @@ class RecordingPen(AbstractPen):
def __init__(self): def __init__(self):
self.value = [] self.value = []
def moveTo(self, p0): def moveTo(self, p0):
self.value.append(('moveTo', (p0,))) self.value.append(("moveTo", (p0,)))
def lineTo(self, p1): def lineTo(self, p1):
self.value.append(('lineTo', (p1,))) self.value.append(("lineTo", (p1,)))
def qCurveTo(self, *points): def qCurveTo(self, *points):
self.value.append(('qCurveTo', points)) self.value.append(("qCurveTo", points))
def curveTo(self, *points): def curveTo(self, *points):
self.value.append(('curveTo', points)) self.value.append(("curveTo", points))
def closePath(self): def closePath(self):
self.value.append(('closePath', ())) self.value.append(("closePath", ()))
def endPath(self): def endPath(self):
self.value.append(('endPath', ())) self.value.append(("endPath", ()))
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
self.value.append(('addComponent', (glyphName, transformation))) self.value.append(("addComponent", (glyphName, transformation)))
def addVarComponent(self, glyphName, transformation, location):
self.value.append(("addVarComponent", (glyphName, transformation, location)))
def replay(self, pen): def replay(self, pen):
replayRecording(self.value, pen) replayRecording(self.value, pen)
@ -90,6 +101,7 @@ class DecomposingRecordingPen(DecomposingPen, RecordingPen):
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
""" """
# raises KeyError if base glyph is not found in glyphSet # raises KeyError if base glyph is not found in glyphSet
skipMissingComponents = False skipMissingComponents = False
@ -130,7 +142,9 @@ class RecordingPointPen(AbstractPointPen):
def endPath(self): def endPath(self):
self.value.append(("endPath", (), {})) self.value.append(("endPath", (), {}))
def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): def addPoint(
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
):
if identifier is not None: if identifier is not None:
kwargs["identifier"] = identifier kwargs["identifier"] = identifier
self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs)) self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
@ -140,6 +154,15 @@ class RecordingPointPen(AbstractPointPen):
kwargs["identifier"] = identifier kwargs["identifier"] = identifier
self.value.append(("addComponent", (baseGlyphName, transformation), kwargs)) self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
def addVarComponent(
self, baseGlyphName, transformation, location, identifier=None, **kwargs
):
if identifier is not None:
kwargs["identifier"] = identifier
self.value.append(
("addVarComponent", (baseGlyphName, transformation, location), kwargs)
)
def replay(self, pointPen): def replay(self, pointPen):
for operator, args, kwargs in self.value: for operator, args, kwargs in self.value:
getattr(pointPen, operator)(*args, **kwargs) getattr(pointPen, operator)(*args, **kwargs)
@ -152,4 +175,5 @@ if __name__ == "__main__":
pen.curveTo((50, 75), (60, 50), (50, 25)) pen.curveTo((50, 75), (60, 50), (50, 25))
pen.closePath() pen.closePath()
from pprint import pprint from pprint import pprint
pprint(pen.value) pprint(pen.value)

View File

@ -35,11 +35,18 @@ class ReportLabPen(BasePen):
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("Usage: reportLabPen.py <OTF/TTF font> <glyphname> [<image file to create>]") print(
print(" If no image file name is created, by default <glyphname>.png is created.") "Usage: reportLabPen.py <OTF/TTF font> <glyphname> [<image file to create>]"
)
print(
" If no image file name is created, by default <glyphname>.png is created."
)
print(" example: reportLabPen.py Arial.TTF R test.png") print(" example: reportLabPen.py Arial.TTF R test.png")
print(" (The file format will be PNG, regardless of the image file name supplied)") print(
" (The file format will be PNG, regardless of the image file name supplied)"
)
sys.exit(0) sys.exit(0)
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
@ -47,7 +54,7 @@ if __name__=="__main__":
path = sys.argv[1] path = sys.argv[1]
glyphName = sys.argv[2] glyphName = sys.argv[2]
if (len(sys.argv) > 3): if len(sys.argv) > 3:
imageFile = sys.argv[3] imageFile = sys.argv[3]
else: else:
imageFile = "%s.png" % glyphName imageFile = "%s.png" % glyphName

View File

@ -14,11 +14,15 @@ class ReverseContourPen(ContourFilterPen):
the first point. the first point.
""" """
def __init__(self, outPen, outputImpliedClosingLine=False):
super().__init__(outPen)
self.outputImpliedClosingLine = outputImpliedClosingLine
def filterContour(self, contour): def filterContour(self, contour):
return reversedContour(contour) return reversedContour(contour, self.outputImpliedClosingLine)
def reversedContour(contour): def reversedContour(contour, outputImpliedClosingLine=False):
"""Generator that takes a list of pen's (operator, operands) tuples, """Generator that takes a list of pen's (operator, operands) tuples,
and yields them with the winding direction reversed. and yields them with the winding direction reversed.
""" """
@ -36,16 +40,14 @@ def reversedContour(contour):
firstType, firstPts = contour.pop(0) firstType, firstPts = contour.pop(0)
assert firstType in ("moveTo", "qCurveTo"), ( assert firstType in ("moveTo", "qCurveTo"), (
"invalid initial segment type: %r" % firstType) "invalid initial segment type: %r" % firstType
)
firstOnCurve = firstPts[-1] firstOnCurve = firstPts[-1]
if firstType == "qCurveTo": if firstType == "qCurveTo":
# special case for TrueType paths contaning only off-curve points # special case for TrueType paths contaning only off-curve points
assert firstOnCurve is None, ( assert firstOnCurve is None, "off-curve only paths must end with 'None'"
"off-curve only paths must end with 'None'") assert not contour, "only one qCurveTo allowed per off-curve path"
assert not contour, ( firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,)
"only one qCurveTo allowed per off-curve path")
firstPts = ((firstPts[0],) + tuple(reversed(firstPts[1:-1])) +
(None,))
if not contour: if not contour:
# contour contains only one segment, nothing to reverse # contour contains only one segment, nothing to reverse
@ -63,14 +65,15 @@ def reversedContour(contour):
if firstOnCurve != lastOnCurve: if firstOnCurve != lastOnCurve:
# emit an implied line between the last and first points # emit an implied line between the last and first points
yield "lineTo", (lastOnCurve,) yield "lineTo", (lastOnCurve,)
contour[-1] = (lastType, contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
tuple(lastPts[:-1]) + (firstOnCurve,))
if len(contour) > 1: if len(contour) > 1:
secondType, secondPts = contour[0] secondType, secondPts = contour[0]
else: else:
# contour has only two points, the second and last are the same # contour has only two points, the second and last are the same
secondType, secondPts = lastType, lastPts secondType, secondPts = lastType, lastPts
if not outputImpliedClosingLine:
# if a lineTo follows the initial moveTo, after reversing it # if a lineTo follows the initial moveTo, after reversing it
# will be implied by the closePath, so we don't emit one; # will be implied by the closePath, so we don't emit one;
# unless the lineTo and moveTo overlap, in which case we keep the # unless the lineTo and moveTo overlap, in which case we keep the
@ -78,8 +81,7 @@ def reversedContour(contour):
if secondType == "lineTo" and firstPts != secondPts: if secondType == "lineTo" and firstPts != secondPts:
del contour[0] del contour[0]
if contour: if contour:
contour[-1] = (lastType, contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts)
tuple(lastPts[:-1]) + secondPts)
else: else:
# for open paths, the last point will become the first # for open paths, the last point will become the first
yield firstType, (lastOnCurve,) yield firstType, (lastOnCurve,)
@ -88,8 +90,7 @@ def reversedContour(contour):
# we iterate over all segment pairs in reverse order, and yield # we iterate over all segment pairs in reverse order, and yield
# each one with the off-curve points reversed (if any), and # each one with the off-curve points reversed (if any), and
# with the on-curve point of the following segment # with the on-curve point of the following segment
for (curType, curPts), (_, nextPts) in pairwise( for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True):
contour, reverse=True):
yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],) yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],)
yield "closePath" if closed else "endPath", () yield "closePath" if closed else "endPath", ()

View File

@ -53,8 +53,8 @@ class StatisticsPen(MomentsPen):
self.varianceX = varianceX = self.momentXX / area - meanX**2 self.varianceX = varianceX = self.momentXX / area - meanX**2
self.varianceY = varianceY = self.momentYY / area - meanY**2 self.varianceY = varianceY = self.momentYY / area - meanY**2
self.stddevX = stddevX = math.copysign(abs(varianceX)**.5, varianceX) self.stddevX = stddevX = math.copysign(abs(varianceX) ** 0.5, varianceX)
self.stddevY = stddevY = math.copysign(abs(varianceY)**.5, varianceY) self.stddevY = stddevY = math.copysign(abs(varianceY) ** 0.5, varianceY)
# Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] ) # Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] )
self.covariance = covariance = self.momentXY / area - meanX * meanY self.covariance = covariance = self.momentXY / area - meanX * meanY
@ -75,28 +75,48 @@ def _test(glyphset, upem, glyphs):
from fontTools.pens.transformPen import TransformPen from fontTools.pens.transformPen import TransformPen
from fontTools.misc.transform import Scale from fontTools.misc.transform import Scale
print('upem', upem) print("upem", upem)
for glyph_name in glyphs: for glyph_name in glyphs:
print() print()
print("glyph:", glyph_name) print("glyph:", glyph_name)
glyph = glyphset[glyph_name] glyph = glyphset[glyph_name]
pen = StatisticsPen(glyphset=glyphset) pen = StatisticsPen(glyphset=glyphset)
transformer = TransformPen(pen, Scale(1./upem)) transformer = TransformPen(pen, Scale(1.0 / upem))
glyph.draw(transformer) glyph.draw(transformer)
for item in ['area', 'momentX', 'momentY', 'momentXX', 'momentYY', 'momentXY', 'meanX', 'meanY', 'varianceX', 'varianceY', 'stddevX', 'stddevY', 'covariance', 'correlation', 'slant']: for item in [
"area",
"momentX",
"momentY",
"momentXX",
"momentYY",
"momentXY",
"meanX",
"meanY",
"varianceX",
"varianceY",
"stddevX",
"stddevY",
"covariance",
"correlation",
"slant",
]:
print("%s: %g" % (item, getattr(pen, item))) print("%s: %g" % (item, getattr(pen, item)))
def main(args): def main(args):
if not args: if not args:
return return
filename, glyphs = args[0], args[1:] filename, glyphs = args[0], args[1:]
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
font = TTFont(filename) font = TTFont(filename)
if not glyphs: if not glyphs:
glyphs = font.getGlyphOrder() glyphs = font.getGlyphOrder()
_test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs) _test(font.getGlyphSet(), font["head"].unitsPerEm, glyphs)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
main(sys.argv[1:]) main(sys.argv[1:])

View File

@ -36,6 +36,7 @@ class SVGPathPen(BasePen):
glyphset[glyphname].draw(pen) glyphset[glyphname].draw(pen)
print(tpen.getCommands()) print(tpen.getCommands())
""" """
def __init__(self, glyphSet, ntos: Callable[[float], str] = str): def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
BasePen.__init__(self, glyphSet) BasePen.__init__(self, glyphSet)
self._commands = [] self._commands = []
@ -209,22 +210,25 @@ def main(args=None):
if args is None: if args is None:
import sys import sys
args = sys.argv[1:] args = sys.argv[1:]
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
import argparse import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
"fonttools pens.svgPathPen", description="Generate SVG from text") "fonttools pens.svgPathPen", description="Generate SVG from text"
)
parser.add_argument("font", metavar="font.ttf", help="Font file.")
parser.add_argument("text", metavar="text", help="Text string.")
parser.add_argument( parser.add_argument(
"font", metavar="font.ttf", help="Font file.") "--variations",
parser.add_argument( metavar="AXIS=LOC",
"text", metavar="text", help="Text string.") default="",
parser.add_argument(
"--variations", metavar="AXIS=LOC", default='',
help="List of space separated locations. A location consist in " help="List of space separated locations. A location consist in "
"the name of a variation axis, followed by '=' and a number. E.g.: " "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.") "wght=700 wdth=80. The default is the location of the base master.",
)
options = parser.parse_args(args) options = parser.parse_args(args)
@ -233,18 +237,18 @@ def main(args=None):
location = {} location = {}
for tag_v in options.variations.split(): for tag_v in options.variations.split():
fields = tag_v.split('=') fields = tag_v.split("=")
tag = fields[0].strip() tag = fields[0].strip()
v = int(fields[1]) v = int(fields[1])
location[tag] = v location[tag] = v
hhea = font['hhea'] hhea = font["hhea"]
ascent, descent = hhea.ascent, hhea.descent ascent, descent = hhea.ascent, hhea.descent
glyphset = font.getGlyphSet(location=location) glyphset = font.getGlyphSet(location=location)
cmap = font['cmap'].getBestCmap() cmap = font["cmap"].getBestCmap()
s = '' s = ""
width = 0 width = 0
for u in text: for u in text:
g = cmap[ord(u)] g = cmap[ord(u)]
@ -254,20 +258,29 @@ def main(args=None):
glyph.draw(pen) glyph.draw(pen)
commands = pen.getCommands() commands = pen.getCommands()
s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (width, ascent, commands) s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (
width,
ascent,
commands,
)
width += glyph.width width += glyph.width
print('<?xml version="1.0" encoding="UTF-8"?>') print('<?xml version="1.0" encoding="UTF-8"?>')
print('<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">' % (width, ascent-descent)) print(
print(s, end='') '<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
print('</svg>') % (width, ascent - descent)
)
print(s, end="")
print("</svg>")
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
if len(sys.argv) == 1: if len(sys.argv) == 1:
import doctest import doctest
sys.exit(doctest.testmod().failed) sys.exit(doctest.testmod().failed)
sys.exit(main()) sys.exit(main())

View File

@ -32,14 +32,14 @@ class T2CharStringPen(BasePen):
return [pt[0] - p0[0], pt[1] - p0[1]] return [pt[0] - p0[0], pt[1] - p0[1]]
def _moveTo(self, pt): def _moveTo(self, pt):
self._commands.append(('rmoveto', self._p(pt))) self._commands.append(("rmoveto", self._p(pt)))
def _lineTo(self, pt): def _lineTo(self, pt):
self._commands.append(('rlineto', self._p(pt))) self._commands.append(("rlineto", self._p(pt)))
def _curveToOne(self, pt1, pt2, pt3): def _curveToOne(self, pt1, pt2, pt3):
_p = self._p _p = self._p
self._commands.append(('rrcurveto', _p(pt1)+_p(pt2)+_p(pt3))) self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
def _closePath(self): def _closePath(self):
pass pass
@ -51,15 +51,18 @@ class T2CharStringPen(BasePen):
commands = self._commands commands = self._commands
if optimize: if optimize:
maxstack = 48 if not self._CFF2 else 513 maxstack = 48 if not self._CFF2 else 513
commands = specializeCommands(commands, commands = specializeCommands(
generalizeFirst=False, commands, generalizeFirst=False, maxstack=maxstack
maxstack=maxstack) )
program = commandsToProgram(commands) program = commandsToProgram(commands)
if self._width is not None: if self._width is not None:
assert not self._CFF2, "CFF2 does not allow encoding glyph width in CharString." assert (
not self._CFF2
), "CFF2 does not allow encoding glyph width in CharString."
program.insert(0, otRound(self._width)) program.insert(0, otRound(self._width))
if not self._CFF2: if not self._CFF2:
program.append('endchar') program.append("endchar")
charString = T2CharString( charString = T2CharString(
program=program, private=private, globalSubrs=globalSubrs) program=program, private=private, globalSubrs=globalSubrs
)
return charString return charString

View File

@ -14,24 +14,31 @@ class TeePen(AbstractPen):
if len(pens) == 1: if len(pens) == 1:
pens = pens[0] pens = pens[0]
self.pens = pens self.pens = pens
def moveTo(self, p0): def moveTo(self, p0):
for pen in self.pens: for pen in self.pens:
pen.moveTo(p0) pen.moveTo(p0)
def lineTo(self, p1): def lineTo(self, p1):
for pen in self.pens: for pen in self.pens:
pen.lineTo(p1) pen.lineTo(p1)
def qCurveTo(self, *points): def qCurveTo(self, *points):
for pen in self.pens: for pen in self.pens:
pen.qCurveTo(*points) pen.qCurveTo(*points)
def curveTo(self, *points): def curveTo(self, *points):
for pen in self.pens: for pen in self.pens:
pen.curveTo(*points) pen.curveTo(*points)
def closePath(self): def closePath(self):
for pen in self.pens: for pen in self.pens:
pen.closePath() pen.closePath()
def endPath(self): def endPath(self):
for pen in self.pens: for pen in self.pens:
pen.endPath() pen.endPath()
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
for pen in self.pens: for pen in self.pens:
pen.addComponent(glyphName, transformation) pen.addComponent(glyphName, transformation)
@ -39,6 +46,7 @@ class TeePen(AbstractPen):
if __name__ == "__main__": if __name__ == "__main__":
from fontTools.pens.basePen import _TestPen from fontTools.pens.basePen import _TestPen
pen = TeePen(_TestPen(), _TestPen()) pen = TeePen(_TestPen(), _TestPen())
pen.moveTo((0, 0)) pen.moveTo((0, 0))
pen.lineTo((0, 100)) pen.lineTo((0, 100))

View File

@ -18,6 +18,7 @@ class TransformPen(FilterPen):
super(TransformPen, self).__init__(outPen) super(TransformPen, self).__init__(outPen)
if not hasattr(transformation, "transformPoint"): if not hasattr(transformation, "transformPoint"):
from fontTools.misc.transform import Transform from fontTools.misc.transform import Transform
transformation = Transform(*transformation) transformation = Transform(*transformation)
self._transformation = transformation self._transformation = transformation
self._transformPoint = transformation.transformPoint self._transformPoint = transformation.transformPoint
@ -85,6 +86,7 @@ class TransformPointPen(FilterPointPen):
super().__init__(outPointPen) super().__init__(outPointPen)
if not hasattr(transformation, "transformPoint"): if not hasattr(transformation, "transformPoint"):
from fontTools.misc.transform import Transform from fontTools.misc.transform import Transform
transformation = Transform(*transformation) transformation = Transform(*transformation)
self._transformation = transformation self._transformation = transformation
self._transformPoint = transformation.transformPoint self._transformPoint = transformation.transformPoint
@ -101,6 +103,7 @@ class TransformPointPen(FilterPointPen):
if __name__ == "__main__": if __name__ == "__main__":
from fontTools.pens.basePen import _TestPen from fontTools.pens.basePen import _TestPen
pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0)) pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0))
pen.moveTo((0, 0)) pen.moveTo((0, 0))
pen.lineTo((0, 100)) pen.lineTo((0, 100))

View File

@ -7,14 +7,60 @@ from fontTools.misc.roundTools import otRound
from fontTools.pens.basePen import LoggingPen, PenError from fontTools.pens.basePen import LoggingPen, PenError
from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.ttLib.tables import ttProgram from fontTools.ttLib.tables import ttProgram
from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic
from fontTools.ttLib.tables._g_l_y_f import Glyph from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
import math
__all__ = ["TTGlyphPen", "TTGlyphPointPen"] __all__ = ["TTGlyphPen", "TTGlyphPointPen"]
def drop_implied_oncurves(glyph):
drop = set()
start = 0
flags = glyph.flags
coords = glyph.coordinates
for last in glyph.endPtsOfContours:
for i in range(start, last + 1):
if not (flags[i] & flagOnCurve):
continue
prv = i - 1 if i > start else last
nxt = i + 1 if i < last else start
if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
continue
p0 = coords[prv]
p1 = coords[i]
p2 = coords[nxt]
if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose(
p1[1] - p0[1], p2[1] - p1[1]
):
continue
drop.add(i)
if drop:
# Do the actual dropping
glyph.coordinates = GlyphCoordinates(
coords[i] for i in range(len(coords)) if i not in drop
)
glyph.flags = array("B", (flags[i] for i in range(len(flags)) if i not in drop))
endPts = glyph.endPtsOfContours
newEndPts = []
i = 0
delta = 0
for d in sorted(drop):
while d > endPts[i]:
newEndPts.append(endPts[i] - delta)
i += 1
delta += 1
while i < len(endPts):
newEndPts.append(endPts[i] - delta)
i += 1
glyph.endPtsOfContours = newEndPts
class _TTGlyphBasePen: class _TTGlyphBasePen:
def __init__( def __init__(
self, self,
@ -124,9 +170,14 @@ class _TTGlyphBasePen:
components.append(component) components.append(component)
return components return components
def glyph(self, componentFlags: int = 0x4) -> Glyph: def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph:
""" """
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
Args:
componentFlags: Flags to use for component glyphs. (default: 0x04)
dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False)
""" """
if not self._isClosed(): if not self._isClosed():
raise PenError("Didn't close last contour.") raise PenError("Didn't close last contour.")
@ -134,9 +185,13 @@ class _TTGlyphBasePen:
glyph = Glyph() glyph = Glyph()
glyph.coordinates = GlyphCoordinates(self.points) glyph.coordinates = GlyphCoordinates(self.points)
glyph.coordinates.toInt()
glyph.endPtsOfContours = self.endPts glyph.endPtsOfContours = self.endPts
glyph.flags = array("B", self.types) glyph.flags = array("B", self.types)
glyph.coordinates.toInt()
if dropImpliedOnCurves:
drop_implied_oncurves(glyph)
self.init() self.init()
if components: if components:
@ -164,9 +219,18 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
drawMethod = "draw" drawMethod = "draw"
transformPen = TransformPen transformPen = TransformPen
def _addPoint(self, pt: Tuple[float, float], onCurve: int) -> None: def __init__(
self,
glyphSet: Optional[Dict[str, Any]] = None,
handleOverflowingTransforms: bool = True,
outputImpliedClosingLine: bool = False,
) -> None:
super().__init__(glyphSet, handleOverflowingTransforms)
self.outputImpliedClosingLine = outputImpliedClosingLine
def _addPoint(self, pt: Tuple[float, float], tp: int) -> None:
self.points.append(pt) self.points.append(pt)
self.types.append(onCurve) self.types.append(tp)
def _popPoint(self) -> None: def _popPoint(self) -> None:
self.points.pop() self.points.pop()
@ -178,15 +242,21 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
) )
def lineTo(self, pt: Tuple[float, float]) -> None: def lineTo(self, pt: Tuple[float, float]) -> None:
self._addPoint(pt, 1) self._addPoint(pt, flagOnCurve)
def moveTo(self, pt: Tuple[float, float]) -> None: def moveTo(self, pt: Tuple[float, float]) -> None:
if not self._isClosed(): if not self._isClosed():
raise PenError('"move"-type point must begin a new contour.') raise PenError('"move"-type point must begin a new contour.')
self._addPoint(pt, 1) self._addPoint(pt, flagOnCurve)
def curveTo(self, *points) -> None: def curveTo(self, *points) -> None:
raise NotImplementedError assert len(points) % 2 == 1
for pt in points[:-1]:
self._addPoint(pt, flagCubic)
# last point is None if there are no on-curve points
if points[-1] is not None:
self._addPoint(points[-1], 1)
def qCurveTo(self, *points) -> None: def qCurveTo(self, *points) -> None:
assert len(points) >= 1 assert len(points) >= 1
@ -205,6 +275,7 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
self._popPoint() self._popPoint()
return return
if not self.outputImpliedClosingLine:
# if first and last point on this path are the same, remove last # if first and last point on this path are the same, remove last
startPt = 0 startPt = 0
if self.endPts: if self.endPts:
@ -256,9 +327,23 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
raise PenError("Contour is already closed.") raise PenError("Contour is already closed.")
if self._currentContourStartIndex == len(self.points): if self._currentContourStartIndex == len(self.points):
raise PenError("Tried to end an empty contour.") raise PenError("Tried to end an empty contour.")
contourStart = self.endPts[-1] + 1 if self.endPts else 0
self.endPts.append(len(self.points) - 1) self.endPts.append(len(self.points) - 1)
self._currentContourStartIndex = None self._currentContourStartIndex = None
# Resolve types for any cubic segments
flags = self.types
for i in range(contourStart, len(flags)):
if flags[i] == "curve":
j = i - 1
if j < contourStart:
j = len(flags) - 1
while flags[j] == 0:
flags[j] = flagCubic
j -= 1
flags[i] = flagOnCurve
def addPoint( def addPoint(
self, self,
pt: Tuple[float, float], pt: Tuple[float, float],
@ -274,11 +359,13 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
if self._isClosed(): if self._isClosed():
raise PenError("Can't add a point to a closed contour.") raise PenError("Can't add a point to a closed contour.")
if segmentType is None: if segmentType is None:
self.types.append(0) # offcurve self.types.append(0)
elif segmentType in ("qcurve", "line", "move"): elif segmentType in ("line", "move"):
self.types.append(1) # oncurve self.types.append(flagOnCurve)
elif segmentType == "qcurve":
self.types.append(flagOnCurve)
elif segmentType == "curve": elif segmentType == "curve":
raise NotImplementedError("cubic curves are not supported") self.types.append("curve")
else: else:
raise AssertionError(segmentType) raise AssertionError(segmentType)

View File

@ -5,11 +5,11 @@ __all__ = ["WxPen"]
class WxPen(BasePen): class WxPen(BasePen):
def __init__(self, glyphSet, path=None): def __init__(self, glyphSet, path=None):
BasePen.__init__(self, glyphSet) BasePen.__init__(self, glyphSet)
if path is None: if path is None:
import wx import wx
path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath() path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath()
self.path = path self.path = path

View File

@ -0,0 +1,15 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .qu2cu import *

View File

@ -0,0 +1,7 @@
import sys
from .cli import main
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,57 @@
"""Benchmark the qu2cu algorithm performance."""
from .qu2cu import *
from fontTools.cu2qu import curve_to_quadratic
import random
import timeit
MAX_ERR = 0.5
NUM_CURVES = 5
def generate_curves(n):
points = [
tuple(float(random.randint(0, 2048)) for coord in range(2))
for point in range(1 + 3 * n)
]
curves = []
for i in range(n):
curves.append(tuple(points[i * 3 : i * 3 + 4]))
return curves
def setup_quadratic_to_curves():
curves = generate_curves(NUM_CURVES)
quadratics = [curve_to_quadratic(curve, MAX_ERR) for curve in curves]
return quadratics, MAX_ERR
def run_benchmark(module, function, setup_suffix="", repeat=25, number=1):
setup_func = "setup_" + function
if setup_suffix:
print("%s with %s:" % (function, setup_suffix), end="")
setup_func += "_" + setup_suffix
else:
print("%s:" % function, end="")
def wrapper(function, setup_func):
function = globals()[function]
setup_func = globals()[setup_func]
def wrapped():
return function(*setup_func())
return wrapped
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
print("\t%5.1fus" % (min(results) * 1000000.0 / number))
def main():
"""Benchmark the qu2cu algorithm performance."""
run_benchmark("qu2cu", "quadratic_to_curves")
if __name__ == "__main__":
random.seed(1)
main()

124
Lib/fontTools/qu2cu/cli.py Normal file
View File

@ -0,0 +1,124 @@
import os
import argparse
import logging
from fontTools.misc.cliTools import makeOutputFileName
from fontTools.ttLib import TTFont
from fontTools.pens.qu2cuPen import Qu2CuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
import fontTools
logger = logging.getLogger("fontTools.qu2cu")
def _font_to_cubic(input_path, output_path=None, **kwargs):
font = TTFont(input_path)
logger.info("Converting curves for %s", input_path)
stats = {} if kwargs["dump_stats"] else None
qu2cu_kwargs = {
"stats": stats,
"max_err": kwargs["max_err_em"] * font["head"].unitsPerEm,
"all_cubic": kwargs["all_cubic"],
}
assert "gvar" not in font, "Cannot convert variable font"
glyphSet = font.getGlyphSet()
glyphOrder = font.getGlyphOrder()
glyf = font["glyf"]
for glyphName in glyphOrder:
glyph = glyphSet[glyphName]
ttpen = TTGlyphPen(glyphSet)
pen = Qu2CuPen(ttpen, **qu2cu_kwargs)
glyph.draw(pen)
glyf[glyphName] = ttpen.glyph(dropImpliedOnCurves=True)
font["head"].glyphDataFormat = 1
if kwargs["dump_stats"]:
logger.info("Stats: %s", stats)
logger.info("Saving %s", output_path)
font.save(output_path)
def main(args=None):
parser = argparse.ArgumentParser(prog="qu2cu")
parser.add_argument("--version", action="version", version=fontTools.__version__)
parser.add_argument(
"infiles",
nargs="+",
metavar="INPUT",
help="one or more input TTF source file(s).",
)
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument(
"-e",
"--conversion-error",
type=float,
metavar="ERROR",
default=0.001,
help="maxiumum approximation error measured in EM (default: 0.001)",
)
parser.add_argument(
"-c",
"--all-cubic",
default=False,
action="store_true",
help="whether to only use cubic curves",
)
output_parser = parser.add_mutually_exclusive_group()
output_parser.add_argument(
"-o",
"--output-file",
default=None,
metavar="OUTPUT",
help=("output filename for the converted TTF."),
)
output_parser.add_argument(
"-d",
"--output-dir",
default=None,
metavar="DIRECTORY",
help="output directory where to save converted TTFs",
)
options = parser.parse_args(args)
if not options.verbose:
level = "WARNING"
elif options.verbose == 1:
level = "INFO"
else:
level = "DEBUG"
logging.basicConfig(level=level)
if len(options.infiles) > 1 and options.output_file:
parser.error("-o/--output-file can't be used with multile inputs")
if options.output_dir:
output_dir = options.output_dir
if not os.path.exists(output_dir):
os.mkdir(output_dir)
elif not os.path.isdir(output_dir):
parser.error("'%s' is not a directory" % output_dir)
output_paths = [
os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
]
elif options.output_file:
output_paths = [options.output_file]
else:
output_paths = [
makeOutputFileName(p, overWrite=True, suffix=".cubic")
for p in options.infiles
]
kwargs = dict(
dump_stats=options.verbose > 0,
max_err_em=options.conversion_error,
all_cubic=options.all_cubic,
)
for input_path, output_path in zip(options.infiles, output_paths):
_font_to_cubic(input_path, output_path, **kwargs)

View File

@ -0,0 +1,409 @@
# cython: language_level=3
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
# Copyright 2023 Google Inc. All Rights Reserved.
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
try:
import cython
COMPILED = cython.compiled
except (AttributeError, ImportError):
# if cython not installed, use mock module with no-op decorators and types
from fontTools.misc import cython
COMPILED = False
from fontTools.misc.bezierTools import splitCubicAtTC
from collections import namedtuple
import math
from typing import (
List,
Tuple,
Union,
)
__all__ = ["quadratic_to_curves"]
# Copied from cu2qu
@cython.cfunc
@cython.returns(cython.int)
@cython.locals(
tolerance=cython.double,
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
)
@cython.locals(mid=cython.complex, deriv3=cython.complex)
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
"""Check if a cubic Bezier lies within a given distance of the origin.
"Origin" means *the* origin (0,0), not the start of the curve. Note that no
checks are made on the start and end positions of the curve; this function
only checks the inside of the curve.
Args:
p0 (complex): Start point of curve.
p1 (complex): First handle of curve.
p2 (complex): Second handle of curve.
p3 (complex): End point of curve.
tolerance (double): Distance from origin.
Returns:
bool: True if the cubic Bezier ``p`` entirely lies within a distance
``tolerance`` of the origin, False otherwise.
"""
# First check p2 then p1, as p2 has higher error early on.
if abs(p2) <= tolerance and abs(p1) <= tolerance:
return True
# Split.
mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
if abs(mid) > tolerance:
return False
deriv3 = (p3 + p2 - p1 - p0) * 0.125
return cubic_farthest_fit_inside(
p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance
) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance)
@cython.locals(_1_3=cython.double, _2_3=cython.double)
@cython.locals(
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p1_2_3=cython.complex,
)
def elevate_quadratic(p0, p1, p2, _1_3=1 / 3, _2_3=2 / 3):
"""Given a quadratic bezier curve, return its degree-elevated cubic."""
# https://pomax.github.io/bezierinfo/#reordering
p1_2_3 = p1 * _2_3
return (
p0,
(p0 * _1_3 + p1_2_3),
(p2 * _1_3 + p1_2_3),
p2,
)
@cython.locals(
start=cython.int,
n=cython.int,
k=cython.int,
prod_ratio=cython.double,
sum_ratio=cython.double,
ratio=cython.double,
t=cython.double,
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
)
def merge_curves(curves, start, n):
"""Give a cubic-Bezier spline, reconstruct one cubic-Bezier
that has the same endpoints and tangents and approxmates
the spline."""
# Reconstruct the t values of the cut segments
prod_ratio = 1.0
sum_ratio = 1.0
ts = [1]
for k in range(1, n):
ck = curves[start + k]
c_before = curves[start + k - 1]
# |t_(k+1) - t_k| / |t_k - t_(k - 1)| = ratio
assert ck[0] == c_before[3]
ratio = abs(ck[1] - ck[0]) / abs(c_before[3] - c_before[2])
prod_ratio *= ratio
sum_ratio += prod_ratio
ts.append(sum_ratio)
# (t(n) - t(n - 1)) / (t_(1) - t(0)) = prod_ratio
ts = [t / sum_ratio for t in ts[:-1]]
p0 = curves[start][0]
p1 = curves[start][1]
p2 = curves[start + n - 1][2]
p3 = curves[start + n - 1][3]
# Build the curve by scaling the control-points.
p1 = p0 + (p1 - p0) / (ts[0] if ts else 1)
p2 = p3 + (p2 - p3) / ((1 - ts[-1]) if ts else 1)
curve = (p0, p1, p2, p3)
return curve, ts
@cython.locals(
count=cython.int,
num_offcurves=cython.int,
i=cython.int,
off1=cython.complex,
off2=cython.complex,
on=cython.complex,
)
def add_implicit_on_curves(p):
q = list(p)
count = 0
num_offcurves = len(p) - 2
for i in range(1, num_offcurves):
off1 = p[i]
off2 = p[i + 1]
on = off1 + (off2 - off1) * 0.5
q.insert(i + 1 + count, on)
count += 1
return q
Point = Union[Tuple[float, float], complex]
@cython.locals(
cost=cython.int,
is_complex=cython.int,
)
def quadratic_to_curves(
quads: List[List[Point]],
max_err: float = 0.5,
all_cubic: bool = False,
) -> List[Tuple[Point, ...]]:
"""Converts a connecting list of quadratic splines to a list of quadratic
and cubic curves.
A quadratic spline is specified as a list of points. Either each point is
a 2-tuple of X,Y coordinates, or each point is a complex number with
real/imaginary components representing X,Y coordinates.
The first and last points are on-curve points and the rest are off-curve
points, with an implied on-curve point in the middle between every two
consequtive off-curve points.
Returns:
The output is a list of tuples of points. Points are represented
in the same format as the input, either as 2-tuples or complex numbers.
Each tuple is either of length three, for a quadratic curve, or four,
for a cubic curve. Each curve's last point is the same as the next
curve's first point.
Args:
quads: quadratic splines
max_err: absolute error tolerance; defaults to 0.5
all_cubic: if True, only cubic curves are generated; defaults to False
"""
is_complex = type(quads[0][0]) is complex
if not is_complex:
quads = [[complex(x, y) for (x, y) in p] for p in quads]
q = [quads[0][0]]
costs = [1]
cost = 1
for p in quads:
assert q[-1] == p[0]
for i in range(len(p) - 2):
cost += 1
costs.append(cost)
costs.append(cost)
qq = add_implicit_on_curves(p)[1:]
costs.pop()
q.extend(qq)
cost += 1
costs.append(cost)
curves = spline_to_curves(q, costs, max_err, all_cubic)
if not is_complex:
curves = [tuple((c.real, c.imag) for c in curve) for curve in curves]
return curves
Solution = namedtuple("Solution", ["num_points", "error", "start_index", "is_cubic"])
@cython.locals(
i=cython.int,
j=cython.int,
k=cython.int,
start=cython.int,
i_sol_count=cython.int,
j_sol_count=cython.int,
this_sol_count=cython.int,
tolerance=cython.double,
err=cython.double,
error=cython.double,
i_sol_error=cython.double,
j_sol_error=cython.double,
all_cubic=cython.int,
is_cubic=cython.int,
count=cython.int,
p0=cython.complex,
p1=cython.complex,
p2=cython.complex,
p3=cython.complex,
v=cython.complex,
u=cython.complex,
)
def spline_to_curves(q, costs, tolerance=0.5, all_cubic=False):
"""
q: quadratic spline with alternating on-curve / off-curve points.
costs: cumulative list of encoding cost of q in terms of number of
points that need to be encoded. Implied on-curve points do not
contribute to the cost. If all points need to be encoded, then
costs will be range(1, len(q)+1).
"""
assert len(q) >= 3, "quadratic spline requires at least 3 points"
# Elevate quadratic segments to cubic
elevated_quadratics = [
elevate_quadratic(*q[i : i + 3]) for i in range(0, len(q) - 2, 2)
]
# Find sharp corners; they have to be oncurves for sure.
forced = set()
for i in range(1, len(elevated_quadratics)):
p0 = elevated_quadratics[i - 1][2]
p1 = elevated_quadratics[i][0]
p2 = elevated_quadratics[i][1]
if abs(p1 - p0) + abs(p2 - p1) > tolerance + abs(p2 - p0):
forced.add(i)
# Dynamic-Programming to find the solution with fewest number of
# cubic curves, and within those the one with smallest error.
sols = [Solution(0, 0, 0, False)]
impossible = Solution(len(elevated_quadratics) * 3 + 1, 0, 1, False)
start = 0
for i in range(1, len(elevated_quadratics) + 1):
best_sol = impossible
for j in range(start, i):
j_sol_count, j_sol_error = sols[j].num_points, sols[j].error
if not all_cubic:
# Solution with quadratics between j:i
this_count = costs[2 * i - 1] - costs[2 * j] + 1
i_sol_count = j_sol_count + this_count
i_sol_error = j_sol_error
i_sol = Solution(i_sol_count, i_sol_error, i - j, False)
if i_sol < best_sol:
best_sol = i_sol
if this_count <= 3:
# Can't get any better than this in the path below
continue
# Fit elevated_quadratics[j:i] into one cubic
try:
curve, ts = merge_curves(elevated_quadratics, j, i - j)
except ZeroDivisionError:
continue
# Now reconstruct the segments from the fitted curve
reconstructed_iter = splitCubicAtTC(*curve, *ts)
reconstructed = []
# Knot errors
error = 0
for k, reconst in enumerate(reconstructed_iter):
orig = elevated_quadratics[j + k]
err = abs(reconst[3] - orig[3])
error = max(error, err)
if error > tolerance:
break
reconstructed.append(reconst)
if error > tolerance:
# Not feasible
continue
# Interior errors
for k, reconst in enumerate(reconstructed):
orig = elevated_quadratics[j + k]
p0, p1, p2, p3 = tuple(v - u for v, u in zip(reconst, orig))
if not cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
error = tolerance + 1
break
if error > tolerance:
# Not feasible
continue
# Save best solution
i_sol_count = j_sol_count + 3
i_sol_error = max(j_sol_error, error)
i_sol = Solution(i_sol_count, i_sol_error, i - j, True)
if i_sol < best_sol:
best_sol = i_sol
if i_sol_count == 3:
# Can't get any better than this
break
sols.append(best_sol)
if i in forced:
start = i
# Reconstruct solution
splits = []
cubic = []
i = len(sols) - 1
while i:
count, is_cubic = sols[i].start_index, sols[i].is_cubic
splits.append(i)
cubic.append(is_cubic)
i -= count
curves = []
j = 0
for i, is_cubic in reversed(list(zip(splits, cubic))):
if is_cubic:
curves.append(merge_curves(elevated_quadratics, j, i - j)[0])
else:
for k in range(j, i):
curves.append(q[k * 2 : k * 2 + 3])
j = i
return curves
def main():
from fontTools.cu2qu.benchmark import generate_curve
from fontTools.cu2qu import curve_to_quadratic
tolerance = 0.05
reconstruct_tolerance = tolerance * 1
curve = generate_curve()
quadratics = curve_to_quadratic(curve, tolerance)
print(
"cu2qu tolerance %g. qu2cu tolerance %g." % (tolerance, reconstruct_tolerance)
)
print("One random cubic turned into %d quadratics." % len(quadratics))
curves = quadratic_to_curves([quadratics], reconstruct_tolerance)
print("Those quadratics turned back into %d cubics. " % len(curves))
print("Original curve:", curve)
print("Reconstructed curve(s):", curves)
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More