Merge branch 'main' into avar2
This commit is contained in:
commit
fd822a2602
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# First blackening of code
|
||||||
|
d584daa8fdc71030f92ee665472d6c7cddd49283
|
24
.github/workflows/test.yml
vendored
24
.github/workflows/test.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.",
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"]
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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
@ -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)))
|
||||||
|
@ -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()
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
|
|
||||||
class ColorLibError(Exception):
|
class ColorLibError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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)
|
||||||
|
@ -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) + ";"
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"""
|
||||||
|
|
||||||
|
@ -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_(";")
|
||||||
|
@ -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"):
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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 = """
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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("&", "&")
|
data = data.replace("&", "&")
|
||||||
data = data.replace("<", "<")
|
data = data.replace("<", "<")
|
||||||
data = data.replace(">", ">")
|
data = data.replace(">", ">")
|
||||||
data = data.replace("\r", " ")
|
data = data.replace("\r", " ")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def escapeattr(data):
|
def escapeattr(data):
|
||||||
data = escape(data)
|
data = escape(data)
|
||||||
data = data.replace('"', """)
|
data = data.replace('"', """)
|
||||||
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
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
105
Lib/fontTools/pens/qu2cuPen.py
Normal file
105
Lib/fontTools/pens/qu2cuPen.py
Normal 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
|
@ -42,4 +42,3 @@ class QuartzPen(BasePen):
|
|||||||
|
|
||||||
def _closePath(self):
|
def _closePath(self):
|
||||||
CGPathCloseSubpath(self.path)
|
CGPathCloseSubpath(self.path)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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", ()
|
||||||
|
@ -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:])
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
15
Lib/fontTools/qu2cu/__init__.py
Normal file
15
Lib/fontTools/qu2cu/__init__.py
Normal 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 *
|
7
Lib/fontTools/qu2cu/__main__.py
Normal file
7
Lib/fontTools/qu2cu/__main__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
57
Lib/fontTools/qu2cu/benchmark.py
Normal file
57
Lib/fontTools/qu2cu/benchmark.py
Normal 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
124
Lib/fontTools/qu2cu/cli.py
Normal 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)
|
409
Lib/fontTools/qu2cu/qu2cu.py
Normal file
409
Lib/fontTools/qu2cu/qu2cu.py
Normal 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
Loading…
x
Reference in New Issue
Block a user