Revert "Remove all top-level cu2qu files that conflict with fonttools merge"
This reverts commit 09257ab57b03cef5c73557ece77ca048db2fc78d.
This commit is contained in:
parent
09257ab57b
commit
75c14ae7e9
5
.codecov.yml
Normal file
5
.codecov.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
comment: false
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project: off
|
||||||
|
patch: off
|
35
.coveragerc
Normal file
35
.coveragerc
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[run]
|
||||||
|
# measure 'branch' coverage in addition to 'statement' coverage
|
||||||
|
# See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch
|
||||||
|
branch = True
|
||||||
|
|
||||||
|
# list of directories or packages to measure
|
||||||
|
source = cu2qu
|
||||||
|
|
||||||
|
# this is simply vendored, no need to include in coverage report
|
||||||
|
omit =
|
||||||
|
*/cu2qu/cython.py
|
||||||
|
|
||||||
|
# these are treated as equivalent when combining data
|
||||||
|
[paths]
|
||||||
|
source =
|
||||||
|
Lib/cu2qu
|
||||||
|
.tox/*/lib/python*/site-packages/cu2qu
|
||||||
|
.tox/pypy*/site-packages/cu2qu
|
||||||
|
|
||||||
|
[report]
|
||||||
|
# Regexes for lines to exclude from consideration
|
||||||
|
exclude_lines =
|
||||||
|
# keywords to use in inline comments to skip coverage
|
||||||
|
pragma: no cover
|
||||||
|
|
||||||
|
# don't complain if tests don't hit defensive assertion code
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# don't complain if non-runnable code isn't run
|
||||||
|
if 0:
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
|
||||||
|
# ignore source code that can’t be found
|
||||||
|
ignore_errors = True
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Byte-compiled and optimized files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# cython generated C/HTML files
|
||||||
|
Lib/cu2qu/*.c
|
||||||
|
Lib/cu2qu/*.html
|
||||||
|
|
||||||
|
# Packaging
|
||||||
|
*.egg-info
|
||||||
|
*.eggs
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Unit test and coverage files
|
||||||
|
.cache
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.tox
|
||||||
|
htmlcov
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# OS X Finder
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# auto-generated version file
|
||||||
|
Lib/cu2qu/_version.py
|
5
.pyup.yml
Normal file
5
.pyup.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# controls the frequency of updates (undocumented beta feature)
|
||||||
|
schedule: every week
|
||||||
|
|
||||||
|
# do not pin dependencies unless they have explicit version specifiers
|
||||||
|
pin: False
|
37
.travis.yml
Normal file
37
.travis.yml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
sudo: false
|
||||||
|
language: python
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- TWINE_USERNAME="anthrotype"
|
||||||
|
- secure: uIlWYz63F0Y/pDZawW2mS5DolWghdIodM8VtJtGbyIYB3fL3/edwTG5z+FWKaNeRtNfTAGDMs0y2dF45IJ7ZqTzw4yotgHz0uzLqjGjQ1/MOu99tppoXA6wQocZvmrdZpUcRbvBzJQzpAUcjldPLAP200mV9cG8+LfODn8Di2eJ329Ts3aA140pjUF8791jRLHBhUTpxK5RDPn1Q7OlugjryS7yNVIfT1/DaNDXAu4OZ8oNkygioRcyZ9QiFLjv5yBZ7uHB4UXWxw89RYPyz4NfHwyzDR38X/A6vfP19w2V0kecAK5BVBUE+WI7d26XjQzxDuH6Z0phB3x6MFuCXrX/pdrvNr7hs5kTAdQ7R1YA6MH4lPa+7oXha1/j353StzDMUKByXGVHyLAv7Ct2RSXOHUM6hAB4T+JbyJp/YkPWh968GKpl1lwvziKTi7K1qpngbXCdYIYKJ78IbmDcxzmQ+3j+fsXt9+gArZW7ICLgWrs+Lr1FiJsBOKLmqigOSmqrjHa+ef+wjieFSgzCVIfr9zvibzCEtYeqkuJuDQcSBIS0JG/heOfBGQ6FIxSXzwvICyWpldmfP67nBjVOVzPcyEAT8w+45LOM0HPCm6+Xjn7mKstc7x9TD7dVjWeyKJzZuKSmuAFA4UtGGKDQ93YQlAY2SCW4irYsusj80LHU=
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- env: TOXENV=py27-nocy
|
||||||
|
python: 2.7
|
||||||
|
- env: TOXENV=py36-nocy
|
||||||
|
python: 3.6
|
||||||
|
- env: TOXENV=py27-cy
|
||||||
|
python: 2.7
|
||||||
|
- env:
|
||||||
|
- TOXENV=py36-cy
|
||||||
|
- BUILD_DIST=true
|
||||||
|
python: 3.6
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- /^v\d+\.\d+.*$/
|
||||||
|
|
||||||
|
install: pip install tox
|
||||||
|
|
||||||
|
script: tox
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- if [ -z "$TRAVIS_TAG" ]; then tox -e codecov; fi
|
||||||
|
# deploy to PyPI on tags
|
||||||
|
- |
|
||||||
|
if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "googlefonts/cu2qu" ] && [ "$BUILD_DIST" == true ]; then
|
||||||
|
tox -e pypi
|
||||||
|
fi
|
28
CONTRIBUTING.md
Normal file
28
CONTRIBUTING.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Want to contribute? Great! First, read this page (including the small print
|
||||||
|
at the end).
|
||||||
|
|
||||||
|
### Before you contribute
|
||||||
|
Before we can use your code, you must sign the
|
||||||
|
[Google Individual Contributor License
|
||||||
|
Agreement](https://cla.developers.google.com/about/google-individual)
|
||||||
|
(CLA), which you can do online. The CLA is necessary mainly because you own the
|
||||||
|
copyright to your changes, even after your contribution becomes part of our
|
||||||
|
codebase, so we need your permission to use and distribute your code. We also
|
||||||
|
need to be sure of various other things—for instance that you'll tell us if you
|
||||||
|
know that your code infringes on other people's patents. You don't have to sign
|
||||||
|
the CLA until after you've submitted your code for review and a member has
|
||||||
|
approved it, but you must do it before we can put your code into our codebase.
|
||||||
|
Before you start working on a larger contribution, you should get in touch with
|
||||||
|
us first through the issue tracker with your idea so that we can help out and
|
||||||
|
possibly guide you. Coordinating up front makes it much easier to avoid
|
||||||
|
frustration later on.
|
||||||
|
|
||||||
|
### Code reviews
|
||||||
|
All submissions, including submissions by project members, require review. We
|
||||||
|
use Github pull requests for this purpose.
|
||||||
|
|
||||||
|
### The small print
|
||||||
|
Contributions made by corporations are covered by a different agreement than
|
||||||
|
the one above, the
|
||||||
|
[Software Grant and Corporate Contributor License
|
||||||
|
Agreement](https://cla.developers.google.com/about/google-corporate).
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
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.
|
6
Lib/cu2qu/__main__.py
Normal file
6
Lib/cu2qu/__main__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
from cu2qu.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
161
Lib/cu2qu/cli.py
Normal file
161
Lib/cu2qu/cli.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import multiprocessing as mp
|
||||||
|
from contextlib import closing
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import cu2qu
|
||||||
|
from cu2qu.ufo import font_to_quadratic, fonts_to_quadratic
|
||||||
|
|
||||||
|
import defcon
|
||||||
|
|
||||||
|
logger = logging.getLogger("cu2qu")
|
||||||
|
|
||||||
|
|
||||||
|
def _cpu_count():
|
||||||
|
try:
|
||||||
|
return mp.cpu_count()
|
||||||
|
except NotImplementedError: # pragma: no cover
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def _font_to_quadratic(zipped_paths, **kwargs):
|
||||||
|
input_path, output_path = zipped_paths
|
||||||
|
ufo = defcon.Font(input_path)
|
||||||
|
logger.info('Converting curves for %s', input_path)
|
||||||
|
if font_to_quadratic(ufo, **kwargs):
|
||||||
|
logger.info("Saving %s", output_path)
|
||||||
|
ufo.save(output_path)
|
||||||
|
else:
|
||||||
|
_copytree(input_path, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _samepath(path1, path2):
|
||||||
|
# TODO on python3+, there's os.path.samefile
|
||||||
|
path1 = os.path.normcase(os.path.abspath(os.path.realpath(path1)))
|
||||||
|
path2 = os.path.normcase(os.path.abspath(os.path.realpath(path2)))
|
||||||
|
return path1 == path2
|
||||||
|
|
||||||
|
|
||||||
|
def _copytree(input_path, output_path):
|
||||||
|
if _samepath(input_path, output_path):
|
||||||
|
logger.debug("input and output paths are the same file; skipped copy")
|
||||||
|
return
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
shutil.rmtree(output_path)
|
||||||
|
shutil.copytree(input_path, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
parser = argparse.ArgumentParser(prog="cu2qu")
|
||||||
|
parser.add_argument(
|
||||||
|
"--version", action="version", version=cu2qu.__version__)
|
||||||
|
parser.add_argument(
|
||||||
|
"infiles",
|
||||||
|
nargs="+",
|
||||||
|
metavar="INPUT",
|
||||||
|
help="one or more input UFO source file(s).")
|
||||||
|
parser.add_argument("-v", "--verbose", action="count", default=0)
|
||||||
|
parser.add_argument(
|
||||||
|
"-e",
|
||||||
|
"--conversion-error",
|
||||||
|
type=float,
|
||||||
|
metavar="ERROR",
|
||||||
|
default=None,
|
||||||
|
help="maxiumum approximation error measured in EM (default: 0.001)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--keep-direction",
|
||||||
|
dest="reverse_direction",
|
||||||
|
action="store_false",
|
||||||
|
help="do not reverse the contour direction")
|
||||||
|
|
||||||
|
mode_parser = parser.add_mutually_exclusive_group()
|
||||||
|
mode_parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--interpolatable",
|
||||||
|
action="store_true",
|
||||||
|
help="whether curve conversion should keep interpolation compatibility"
|
||||||
|
)
|
||||||
|
mode_parser.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--jobs",
|
||||||
|
type=int,
|
||||||
|
nargs="?",
|
||||||
|
default=1,
|
||||||
|
const=_cpu_count(),
|
||||||
|
metavar="N",
|
||||||
|
help="Convert using N multiple processes (default: %(default)s)")
|
||||||
|
|
||||||
|
output_parser = parser.add_mutually_exclusive_group()
|
||||||
|
output_parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output-file",
|
||||||
|
default=None,
|
||||||
|
metavar="OUTPUT",
|
||||||
|
help=("output filename for the converted UFO. By default fonts are "
|
||||||
|
"modified in place. This only works with a single input."))
|
||||||
|
output_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--output-dir",
|
||||||
|
default=None,
|
||||||
|
metavar="DIRECTORY",
|
||||||
|
help="output directory where to save converted UFOs")
|
||||||
|
|
||||||
|
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:
|
||||||
|
# save in-place
|
||||||
|
output_paths = list(options.infiles)
|
||||||
|
|
||||||
|
kwargs = dict(dump_stats=options.verbose > 0,
|
||||||
|
max_err_em=options.conversion_error,
|
||||||
|
reverse_direction=options.reverse_direction)
|
||||||
|
|
||||||
|
if options.interpolatable:
|
||||||
|
logger.info('Converting curves compatibly')
|
||||||
|
ufos = [defcon.Font(infile) for infile in options.infiles]
|
||||||
|
if fonts_to_quadratic(ufos, **kwargs):
|
||||||
|
for ufo, output_path in zip(ufos, output_paths):
|
||||||
|
logger.info("Saving %s", output_path)
|
||||||
|
ufo.save(output_path)
|
||||||
|
else:
|
||||||
|
for input_path, output_path in zip(options.infiles, output_paths):
|
||||||
|
_copytree(input_path, output_path)
|
||||||
|
else:
|
||||||
|
jobs = min(len(options.infiles),
|
||||||
|
options.jobs) if options.jobs > 1 else 1
|
||||||
|
if jobs > 1:
|
||||||
|
func = partial(_font_to_quadratic, **kwargs)
|
||||||
|
logger.info('Running %d parallel processes', jobs)
|
||||||
|
with closing(mp.Pool(jobs)) as pool:
|
||||||
|
# can't use Pool.starmap as it's 3.3+ only
|
||||||
|
pool.map(func, zip(options.infiles, output_paths))
|
||||||
|
else:
|
||||||
|
for paths in zip(options.infiles, output_paths):
|
||||||
|
_font_to_quadratic(paths, **kwargs)
|
324
Lib/cu2qu/ufo.py
Normal file
324
Lib/cu2qu/ufo.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
|
||||||
|
|
||||||
|
"""Converts cubic bezier curves to quadratic splines.
|
||||||
|
|
||||||
|
Conversion is performed such that the quadratic splines keep the same end-curve
|
||||||
|
tangents as the original cubics. The approach is iterative, increasing the
|
||||||
|
number of segments for a spline until the error gets below a bound.
|
||||||
|
|
||||||
|
Respective curves from multiple fonts will be converted at once to ensure that
|
||||||
|
the resulting splines are interpolation-compatible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fontTools.pens.basePen import AbstractPen
|
||||||
|
from fontTools.pens.pointPen import PointToSegmentPen
|
||||||
|
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||||
|
|
||||||
|
from cu2qu import curves_to_quadratic
|
||||||
|
from cu2qu.errors import (
|
||||||
|
UnequalZipLengthsError, IncompatibleSegmentNumberError,
|
||||||
|
IncompatibleSegmentTypesError, IncompatibleGlyphsError,
|
||||||
|
IncompatibleFontsError)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['fonts_to_quadratic', 'font_to_quadratic']
|
||||||
|
|
||||||
|
DEFAULT_MAX_ERR = 0.001
|
||||||
|
CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_zip = zip
|
||||||
|
def zip(*args):
|
||||||
|
"""Ensure each argument to zip has the same length. Also make sure a list is
|
||||||
|
returned for python 2/3 compatibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(set(len(a) for a in args)) != 1:
|
||||||
|
raise UnequalZipLengthsError(*args)
|
||||||
|
return list(_zip(*args))
|
||||||
|
|
||||||
|
|
||||||
|
class GetSegmentsPen(AbstractPen):
|
||||||
|
"""Pen to collect segments into lists of points for conversion.
|
||||||
|
|
||||||
|
Curves always include their initial on-curve point, so some points are
|
||||||
|
duplicated between segments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._last_pt = None
|
||||||
|
self.segments = []
|
||||||
|
|
||||||
|
def _add_segment(self, tag, *args):
|
||||||
|
if tag in ['move', 'line', 'qcurve', 'curve']:
|
||||||
|
self._last_pt = args[-1]
|
||||||
|
self.segments.append((tag, args))
|
||||||
|
|
||||||
|
def moveTo(self, pt):
|
||||||
|
self._add_segment('move', pt)
|
||||||
|
|
||||||
|
def lineTo(self, pt):
|
||||||
|
self._add_segment('line', pt)
|
||||||
|
|
||||||
|
def qCurveTo(self, *points):
|
||||||
|
self._add_segment('qcurve', self._last_pt, *points)
|
||||||
|
|
||||||
|
def curveTo(self, *points):
|
||||||
|
self._add_segment('curve', self._last_pt, *points)
|
||||||
|
|
||||||
|
def closePath(self):
|
||||||
|
self._add_segment('close')
|
||||||
|
|
||||||
|
def endPath(self):
|
||||||
|
self._add_segment('end')
|
||||||
|
|
||||||
|
def addComponent(self, glyphName, transformation):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_segments(glyph):
|
||||||
|
"""Get a glyph's segments as extracted by GetSegmentsPen."""
|
||||||
|
|
||||||
|
pen = GetSegmentsPen()
|
||||||
|
# glyph.draw(pen)
|
||||||
|
# We can't simply draw the glyph with the pen, but we must initialize the
|
||||||
|
# PointToSegmentPen explicitly with outputImpliedClosingLine=True.
|
||||||
|
# By default PointToSegmentPen does not outputImpliedClosingLine -- unless
|
||||||
|
# last and first point on closed contour are duplicated. Because we are
|
||||||
|
# converting multiple glyphs at the same time, we want to make sure
|
||||||
|
# this function returns the same number of segments, whether or not
|
||||||
|
# the last and first point overlap.
|
||||||
|
# https://github.com/googlefonts/fontmake/issues/572
|
||||||
|
# https://github.com/fonttools/fonttools/pull/1720
|
||||||
|
pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=True)
|
||||||
|
glyph.drawPoints(pointPen)
|
||||||
|
return pen.segments
|
||||||
|
|
||||||
|
|
||||||
|
def _set_segments(glyph, segments, reverse_direction):
|
||||||
|
"""Draw segments as extracted by GetSegmentsPen back to a glyph."""
|
||||||
|
|
||||||
|
glyph.clearContours()
|
||||||
|
pen = glyph.getPen()
|
||||||
|
if reverse_direction:
|
||||||
|
pen = ReverseContourPen(pen)
|
||||||
|
for tag, args in segments:
|
||||||
|
if tag == 'move':
|
||||||
|
pen.moveTo(*args)
|
||||||
|
elif tag == 'line':
|
||||||
|
pen.lineTo(*args)
|
||||||
|
elif tag == 'curve':
|
||||||
|
pen.curveTo(*args[1:])
|
||||||
|
elif tag == 'qcurve':
|
||||||
|
pen.qCurveTo(*args[1:])
|
||||||
|
elif tag == 'close':
|
||||||
|
pen.closePath()
|
||||||
|
elif tag == 'end':
|
||||||
|
pen.endPath()
|
||||||
|
else:
|
||||||
|
raise AssertionError('Unhandled segment type "%s"' % tag)
|
||||||
|
|
||||||
|
|
||||||
|
def _segments_to_quadratic(segments, max_err, stats):
|
||||||
|
"""Return quadratic approximations of cubic segments."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
n = len(new_points[0])
|
||||||
|
assert all(len(s) == n for s in new_points[1:]), 'Converted incompatibly'
|
||||||
|
|
||||||
|
spline_length = str(n - 2)
|
||||||
|
stats[spline_length] = stats.get(spline_length, 0) + 1
|
||||||
|
|
||||||
|
return [('qcurve', p) for p in new_points]
|
||||||
|
|
||||||
|
|
||||||
|
def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats):
|
||||||
|
"""Do the actual conversion of a set of compatible glyphs, after arguments
|
||||||
|
have been set up.
|
||||||
|
|
||||||
|
Return True if the glyphs were modified, else return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
segments_by_location = zip(*[_get_segments(g) for g in glyphs])
|
||||||
|
except UnequalZipLengthsError:
|
||||||
|
raise IncompatibleSegmentNumberError(glyphs)
|
||||||
|
if not any(segments_by_location):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# always modify input glyphs if reverse_direction is True
|
||||||
|
glyphs_modified = reverse_direction
|
||||||
|
|
||||||
|
new_segments_by_location = []
|
||||||
|
incompatible = {}
|
||||||
|
for i, segments in enumerate(segments_by_location):
|
||||||
|
tag = segments[0][0]
|
||||||
|
if not all(s[0] == tag for s in segments[1:]):
|
||||||
|
incompatible[i] = [s[0] for s in segments]
|
||||||
|
elif tag == 'curve':
|
||||||
|
segments = _segments_to_quadratic(segments, max_err, stats)
|
||||||
|
glyphs_modified = True
|
||||||
|
new_segments_by_location.append(segments)
|
||||||
|
|
||||||
|
if glyphs_modified:
|
||||||
|
new_segments_by_glyph = zip(*new_segments_by_location)
|
||||||
|
for glyph, new_segments in zip(glyphs, new_segments_by_glyph):
|
||||||
|
_set_segments(glyph, new_segments, reverse_direction)
|
||||||
|
|
||||||
|
if incompatible:
|
||||||
|
raise IncompatibleSegmentTypesError(glyphs, segments=incompatible)
|
||||||
|
return glyphs_modified
|
||||||
|
|
||||||
|
|
||||||
|
def glyphs_to_quadratic(
|
||||||
|
glyphs, max_err=None, reverse_direction=False, stats=None):
|
||||||
|
"""Convert the curves of a set of compatible of glyphs to quadratic.
|
||||||
|
|
||||||
|
All curves will be converted to quadratic at once, ensuring interpolation
|
||||||
|
compatibility. If this is not required, calling glyphs_to_quadratic with one
|
||||||
|
glyph at a time may yield slightly more optimized results.
|
||||||
|
|
||||||
|
Return True if glyphs were modified, else return False.
|
||||||
|
|
||||||
|
Raises IncompatibleGlyphsError if glyphs have non-interpolatable outlines.
|
||||||
|
"""
|
||||||
|
if stats is None:
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
if not max_err:
|
||||||
|
# assume 1000 is the default UPEM
|
||||||
|
max_err = DEFAULT_MAX_ERR * 1000
|
||||||
|
|
||||||
|
if isinstance(max_err, (list, tuple)):
|
||||||
|
max_errors = max_err
|
||||||
|
else:
|
||||||
|
max_errors = [max_err] * len(glyphs)
|
||||||
|
assert len(max_errors) == len(glyphs)
|
||||||
|
|
||||||
|
return _glyphs_to_quadratic(glyphs, max_errors, reverse_direction, stats)
|
||||||
|
|
||||||
|
|
||||||
|
def fonts_to_quadratic(
|
||||||
|
fonts, max_err_em=None, max_err=None, reverse_direction=False,
|
||||||
|
stats=None, dump_stats=False, remember_curve_type=True):
|
||||||
|
"""Convert the curves of a collection of fonts to quadratic.
|
||||||
|
|
||||||
|
All curves will be converted to quadratic at once, ensuring interpolation
|
||||||
|
compatibility. If this is not required, calling fonts_to_quadratic with one
|
||||||
|
font at a time may yield slightly more optimized results.
|
||||||
|
|
||||||
|
Return True if fonts were modified, else return False.
|
||||||
|
|
||||||
|
By default, cu2qu stores the curve type in the fonts' lib, under a private
|
||||||
|
key "com.github.googlei18n.cu2qu.curve_type", and will not try to convert
|
||||||
|
them again if the curve type is already set to "quadratic".
|
||||||
|
Setting 'remember_curve_type' to False disables this optimization.
|
||||||
|
|
||||||
|
Raises IncompatibleFontsError if same-named glyphs from different fonts
|
||||||
|
have non-interpolatable outlines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if remember_curve_type:
|
||||||
|
curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts}
|
||||||
|
if len(curve_types) == 1:
|
||||||
|
curve_type = next(iter(curve_types))
|
||||||
|
if curve_type == "quadratic":
|
||||||
|
logger.info("Curves already converted to quadratic")
|
||||||
|
return False
|
||||||
|
elif curve_type == "cubic":
|
||||||
|
pass # keep converting
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(curve_type)
|
||||||
|
elif len(curve_types) > 1:
|
||||||
|
# going to crash later if they do differ
|
||||||
|
logger.warning("fonts may contain different curve types")
|
||||||
|
|
||||||
|
if stats is None:
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
if max_err_em and max_err:
|
||||||
|
raise TypeError('Only one of max_err and max_err_em can be specified.')
|
||||||
|
if not (max_err_em or max_err):
|
||||||
|
max_err_em = DEFAULT_MAX_ERR
|
||||||
|
|
||||||
|
if isinstance(max_err, (list, tuple)):
|
||||||
|
assert len(max_err) == len(fonts)
|
||||||
|
max_errors = max_err
|
||||||
|
elif max_err:
|
||||||
|
max_errors = [max_err] * len(fonts)
|
||||||
|
|
||||||
|
if isinstance(max_err_em, (list, tuple)):
|
||||||
|
assert len(fonts) == len(max_err_em)
|
||||||
|
max_errors = [f.info.unitsPerEm * e
|
||||||
|
for f, e in zip(fonts, max_err_em)]
|
||||||
|
elif max_err_em:
|
||||||
|
max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
|
||||||
|
|
||||||
|
modified = False
|
||||||
|
glyph_errors = {}
|
||||||
|
for name in set().union(*(f.keys() for f in fonts)):
|
||||||
|
glyphs = []
|
||||||
|
cur_max_errors = []
|
||||||
|
for font, error in zip(fonts, max_errors):
|
||||||
|
if name in font:
|
||||||
|
glyphs.append(font[name])
|
||||||
|
cur_max_errors.append(error)
|
||||||
|
try:
|
||||||
|
modified |= _glyphs_to_quadratic(
|
||||||
|
glyphs, cur_max_errors, reverse_direction, stats)
|
||||||
|
except IncompatibleGlyphsError as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
glyph_errors[name] = exc
|
||||||
|
|
||||||
|
if glyph_errors:
|
||||||
|
raise IncompatibleFontsError(glyph_errors)
|
||||||
|
|
||||||
|
if modified and dump_stats:
|
||||||
|
spline_lengths = sorted(stats.keys())
|
||||||
|
logger.info('New spline lengths: %s' % (', '.join(
|
||||||
|
'%s: %d' % (l, stats[l]) for l in spline_lengths)))
|
||||||
|
|
||||||
|
if remember_curve_type:
|
||||||
|
for font in fonts:
|
||||||
|
curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic")
|
||||||
|
if curve_type != "quadratic":
|
||||||
|
font.lib[CURVE_TYPE_LIB_KEY] = "quadratic"
|
||||||
|
modified = True
|
||||||
|
return modified
|
||||||
|
|
||||||
|
|
||||||
|
def glyph_to_quadratic(glyph, **kwargs):
|
||||||
|
"""Convenience wrapper around glyphs_to_quadratic, for just one glyph.
|
||||||
|
Return True if the glyph was modified, else return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return glyphs_to_quadratic([glyph], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def font_to_quadratic(font, **kwargs):
|
||||||
|
"""Convenience wrapper around fonts_to_quadratic, for just one font.
|
||||||
|
Return True if the font was modified, else return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return fonts_to_quadratic([font], **kwargs)
|
12
MANIFEST.in
Normal file
12
MANIFEST.in
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
include LICENSE
|
||||||
|
include README.rst
|
||||||
|
include CONTRIBUTING.md
|
||||||
|
include requirements.txt
|
||||||
|
include test-requirements.txt
|
||||||
|
include tox.ini
|
||||||
|
include .coveragerc
|
||||||
|
recursive-include tests *.py
|
||||||
|
recursive-include tests/data *.json
|
||||||
|
recursive-include tests/data */*.glif
|
||||||
|
recursive-include tests/data */*.plist */*/*.glif */*/*.plist
|
||||||
|
recursive-include tools *.py
|
122
README.rst
Normal file
122
README.rst
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
|Build Status| |PyPI Version| |Coverage|
|
||||||
|
|
||||||
|
cu2qu
|
||||||
|
=====
|
||||||
|
|
||||||
|
This library provides functions which take in UFO objects (Defcon Fonts
|
||||||
|
or Robofab RFonts) and converts any cubic curves to quadratic. The most
|
||||||
|
useful function is probably ``fonts_to_quadratic``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from defcon import Font
|
||||||
|
from cu2qu.ufo import fonts_to_quadratic
|
||||||
|
thin_font = Font('MyFont-Thin.ufo')
|
||||||
|
bold_font = Font('MyFont-Bold.ufo')
|
||||||
|
fonts_to_quadratic([thin_font, bold_font])
|
||||||
|
|
||||||
|
Interpolation compatibility is guaranteed during conversion. If it's not
|
||||||
|
needed, converting one font at a time may yield more optimized results:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
for font in [thin_font, bold_font]:
|
||||||
|
fonts_to_quadratic([font])
|
||||||
|
|
||||||
|
Some fonts may need a different error threshold than the default (0.001
|
||||||
|
em). This can also be provided by the caller:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
fonts_to_quadratic([thin_font, bold_font], max_err_em=0.005)
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
for font in [thin_font, bold_font]:
|
||||||
|
fonts_to_quadratic([font], max_err_em=0.001)
|
||||||
|
|
||||||
|
``fonts_to_quadratic`` can print a string reporting the number of curves
|
||||||
|
of each length. For example
|
||||||
|
``fonts_to_quadratic([font], dump_stats=True)`` may print something
|
||||||
|
like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
3: 1000
|
||||||
|
4: 2000
|
||||||
|
5: 100
|
||||||
|
|
||||||
|
meaning that the font now contains 1000 curves with three points, 2000
|
||||||
|
with four points, and 100 with five. Given multiple fonts, the function
|
||||||
|
will report the total counts across all fonts. You can also accumulate
|
||||||
|
statistics between calls by providing your own report dictionary:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
for font in [thin_font, bold_font]:
|
||||||
|
fonts_to_quadratic([font], stats=stats)
|
||||||
|
# "stats" will report combined statistics for both fonts
|
||||||
|
|
||||||
|
The library also provides a command-line script also named ``cu2qu``.
|
||||||
|
Check its ``--help`` to see all the options.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
You can install/upgrade cu2qu using pip, like any other Python package.
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
$ pip install --upgrade cu2qu
|
||||||
|
|
||||||
|
This will download the latest stable version available from the Python
|
||||||
|
Package Index (PyPI).
|
||||||
|
|
||||||
|
If you wish to modify the sources in-place, you can clone the git repository
|
||||||
|
from Github and install in ``--editable`` (or ``-e``) mode:
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
$ git clone https://github.com/googlefonts/cu2qu
|
||||||
|
$ cd cu2qu
|
||||||
|
$ pip install --editable .
|
||||||
|
|
||||||
|
Optionally, you can build an optimized version of cu2qu which uses Cython_
|
||||||
|
to compile Python to C. The extension module thus created is *more than
|
||||||
|
twice as fast* than its pure-Python equivalent.
|
||||||
|
|
||||||
|
When installing cu2qu from PyPI using pip, as long as you have a C compiler
|
||||||
|
available, the cu2qu setup script will automatically attempt to build a
|
||||||
|
C/Python extension module. If the compilation fails for any reasons, an error
|
||||||
|
is printed and cu2qu will be installed as pure-Python, without the optimized
|
||||||
|
extension.
|
||||||
|
|
||||||
|
If you have cloned the git repository, the C source files are not present and
|
||||||
|
need to be regenerated. To do that, you need to install the latest Cython
|
||||||
|
(as usual, ``pip install -U cython``), and then use the global option
|
||||||
|
``--with-cython`` when invoking the ``setup.py`` script. You can also export
|
||||||
|
a ``CU2QU_WITH_CYTHON=1`` environment variable if you prefer.
|
||||||
|
|
||||||
|
For example, to build the cu2qu extension module in-place (i.e. in the same
|
||||||
|
source directory):
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
$ python setup.py --with-cython build_ext --inplace
|
||||||
|
|
||||||
|
You can also pass ``--global-option`` when installing with pip from a local
|
||||||
|
source checkout, like so:
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
$ pip install --global-option="--with-cython" -e .
|
||||||
|
|
||||||
|
|
||||||
|
.. _Cython: https://github.com/cython/cython
|
||||||
|
.. |Build Status| image:: https://travis-ci.org/googlefonts/cu2qu.svg
|
||||||
|
:target: https://travis-ci.org/googlefonts/cu2qu
|
||||||
|
.. |PyPI Version| image:: https://img.shields.io/pypi/v/cu2qu.svg
|
||||||
|
:target: https://pypi.org/project/cu2qu/
|
||||||
|
.. |Coverage| image:: https://codecov.io/gh/googlefonts/cu2qu/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/googlefonts/cu2qu
|
8
pyproject.toml
Normal file
8
pyproject.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = [
|
||||||
|
"setuptools",
|
||||||
|
"wheel",
|
||||||
|
"setuptools_scm",
|
||||||
|
"cython",
|
||||||
|
]
|
||||||
|
build-backend = "setuptools.build_meta"
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fonttools[ufo]==3.32.0
|
||||||
|
defcon==0.6.0
|
28
setup.cfg
Normal file
28
setup.cfg
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[sdist]
|
||||||
|
formats = zip
|
||||||
|
|
||||||
|
[aliases]
|
||||||
|
test = pytest
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
license_file = LICENSE
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
minversion = 3.0
|
||||||
|
testpaths =
|
||||||
|
tests
|
||||||
|
python_files =
|
||||||
|
*_test.py
|
||||||
|
python_classes =
|
||||||
|
*Test
|
||||||
|
addopts =
|
||||||
|
-s
|
||||||
|
-v
|
||||||
|
-r a
|
||||||
|
--doctest-modules
|
||||||
|
--doctest-ignore-import-errors
|
||||||
|
filterwarnings:
|
||||||
|
ignore:.*bytes:DeprecationWarning:fs.base
|
241
setup.py
Normal file
241
setup.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# Copyright 2015 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 setuptools import setup, find_packages, Extension
|
||||||
|
from setuptools.command.build_ext import build_ext as _build_ext
|
||||||
|
from setuptools.command.sdist import sdist as _sdist
|
||||||
|
import pkg_resources
|
||||||
|
from distutils import log
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from io import open
|
||||||
|
|
||||||
|
|
||||||
|
needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
|
||||||
|
pytest_runner = ['pytest_runner'] if needs_pytest else []
|
||||||
|
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
|
||||||
|
wheel = ['wheel'] if needs_wheel else []
|
||||||
|
|
||||||
|
# Check if minimum required Cython is available.
|
||||||
|
# For consistency, we require the same as our vendored Cython.Shadow module
|
||||||
|
cymod = "Lib/cu2qu/cython.py"
|
||||||
|
cython_version_re = re.compile('__version__ = ["\']([0-9][0-9\w\.]+)["\']')
|
||||||
|
with open(cymod, "r", encoding="utf-8") as fp:
|
||||||
|
for line in fp:
|
||||||
|
m = cython_version_re.match(line)
|
||||||
|
if m:
|
||||||
|
cython_min_version = m.group(1)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
sys.exit("error: failed to parse cython version in '%s'" % cymod)
|
||||||
|
|
||||||
|
required_cython = "cython >= %s" % cython_min_version
|
||||||
|
try:
|
||||||
|
pkg_resources.require(required_cython)
|
||||||
|
except pkg_resources.ResolutionError:
|
||||||
|
has_cython = False
|
||||||
|
else:
|
||||||
|
has_cython = True
|
||||||
|
|
||||||
|
# First, check if the CU2QU_WITH_CYTHON environment variable is set.
|
||||||
|
# Values "1", "true" or "yes" mean that Cython is required and will be used
|
||||||
|
# to regenerate the *.c sources from which the native extension is built;
|
||||||
|
# "0", "false" or "no" mean that Cython is not required and no extension
|
||||||
|
# module will be compiled (i.e. the wheel is pure-python and universal).
|
||||||
|
# If the variable is not set, then the pre-generated *.c sources that
|
||||||
|
# are included in the sdist package will be used to try build the extension.
|
||||||
|
# However, if any error occurs during compilation (e.g. the host
|
||||||
|
# machine doesn't have the required compiler toolchain installed), the
|
||||||
|
# installation proceeds without the compiled extensions, but will only have
|
||||||
|
# the pure-python module.
|
||||||
|
env_with_cython = os.environ.get("CU2QU_WITH_CYTHON")
|
||||||
|
with_cython = (
|
||||||
|
True if env_with_cython in {"1", "true", "yes"}
|
||||||
|
else False if env_with_cython in {"0", "false", "no"}
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# command line options --with-cython and --without-cython are also supported.
|
||||||
|
# They override the environment variable
|
||||||
|
opt_with_cython = {'--with-cython'}.intersection(sys.argv)
|
||||||
|
opt_without_cython = {'--without-cython'}.intersection(sys.argv)
|
||||||
|
if opt_with_cython and opt_without_cython:
|
||||||
|
sys.exit(
|
||||||
|
"error: the options '--with-cython' and '--without-cython' are "
|
||||||
|
"mutually exclusive"
|
||||||
|
)
|
||||||
|
elif opt_with_cython:
|
||||||
|
sys.argv.remove("--with-cython")
|
||||||
|
with_cython = True
|
||||||
|
elif opt_without_cython:
|
||||||
|
sys.argv.remove("--without-cython")
|
||||||
|
with_cython = False
|
||||||
|
|
||||||
|
|
||||||
|
class cython_build_ext(_build_ext):
|
||||||
|
"""Compile *.pyx source files to *.c using cythonize if Cython is
|
||||||
|
installed, else use the pre-generated *.c sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
if with_cython:
|
||||||
|
if not has_cython:
|
||||||
|
from distutils.errors import DistutilsSetupError
|
||||||
|
|
||||||
|
raise DistutilsSetupError(
|
||||||
|
"%s is required when using --with-cython" % required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
|
# optionally enable line tracing for test coverage support
|
||||||
|
linetrace = os.environ.get("CYTHON_TRACE") == "1"
|
||||||
|
|
||||||
|
self.distribution.ext_modules[:] = cythonize(
|
||||||
|
self.distribution.ext_modules,
|
||||||
|
force=linetrace or self.force,
|
||||||
|
annotate=os.environ.get("CYTHON_ANNOTATE") == "1",
|
||||||
|
quiet=not self.verbose,
|
||||||
|
compiler_directives={
|
||||||
|
"linetrace": linetrace,
|
||||||
|
"language_level": 3,
|
||||||
|
"embedsignature": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# replace *.py/.pyx sources with their pre-generated *.c versions
|
||||||
|
for ext in self.distribution.ext_modules:
|
||||||
|
ext.sources = [re.sub("\.pyx?$", ".c", n) for n in ext.sources]
|
||||||
|
|
||||||
|
_build_ext.finalize_options(self)
|
||||||
|
|
||||||
|
def build_extensions(self):
|
||||||
|
if not has_cython:
|
||||||
|
log.info(
|
||||||
|
"%s is not installed. Pre-generated *.c sources will be "
|
||||||
|
"will be used to build the extensions." % required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_build_ext.build_extensions(self)
|
||||||
|
except Exception as e:
|
||||||
|
if with_cython:
|
||||||
|
raise
|
||||||
|
from distutils.errors import DistutilsModuleError
|
||||||
|
|
||||||
|
# optional compilation failed: we delete 'ext_modules' and make sure
|
||||||
|
# the generated wheel is 'pure'
|
||||||
|
del self.distribution.ext_modules[:]
|
||||||
|
try:
|
||||||
|
bdist_wheel = self.get_finalized_command("bdist_wheel")
|
||||||
|
except DistutilsModuleError:
|
||||||
|
# 'bdist_wheel' command not available as wheel is not installed
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bdist_wheel.root_is_pure = True
|
||||||
|
log.error('error: building extensions failed: %s' % e)
|
||||||
|
|
||||||
|
def get_source_files(self):
|
||||||
|
filenames = _build_ext.get_source_files(self)
|
||||||
|
|
||||||
|
# include pre-generated *.c sources inside sdist, but only if cython is
|
||||||
|
# installed (and hence they will be updated upon making the sdist)
|
||||||
|
if has_cython:
|
||||||
|
for ext in self.extensions:
|
||||||
|
filenames.extend(
|
||||||
|
[re.sub("\.pyx?$", ".c", n) for n in ext.sources]
|
||||||
|
)
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
|
||||||
|
class cython_sdist(_sdist):
|
||||||
|
""" Run 'cythonize' on *.pyx sources to ensure the *.c files included
|
||||||
|
in the source distribution are up-to-date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if with_cython and not has_cython:
|
||||||
|
from distutils.errors import DistutilsSetupError
|
||||||
|
|
||||||
|
raise DistutilsSetupError(
|
||||||
|
"%s is required when creating sdist --with-cython"
|
||||||
|
% required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_cython:
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
|
cythonize(
|
||||||
|
self.distribution.ext_modules,
|
||||||
|
force=True, # always regenerate *.c sources
|
||||||
|
quiet=not self.verbose,
|
||||||
|
compiler_directives={
|
||||||
|
"language_level": 3,
|
||||||
|
"embedsignature": True
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_sdist.run(self)
|
||||||
|
|
||||||
|
|
||||||
|
# don't build extensions if user explicitly requested --without-cython
|
||||||
|
if with_cython is False:
|
||||||
|
extensions = []
|
||||||
|
else:
|
||||||
|
extensions = [
|
||||||
|
Extension("cu2qu.cu2qu", ["Lib/cu2qu/cu2qu.py"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
with open('README.rst', 'r') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='cu2qu',
|
||||||
|
use_scm_version={"write_to": "Lib/cu2qu/_version.py"},
|
||||||
|
description='Cubic-to-quadratic bezier curve conversion',
|
||||||
|
author="James Godfrey-Kittle, Behdad Esfahbod",
|
||||||
|
author_email="jamesgk@google.com",
|
||||||
|
url="https://github.com/googlefonts",
|
||||||
|
license="Apache License, Version 2.0",
|
||||||
|
long_description=long_description,
|
||||||
|
packages=find_packages('Lib'),
|
||||||
|
package_dir={'': 'Lib'},
|
||||||
|
ext_modules=extensions,
|
||||||
|
include_package_data=True,
|
||||||
|
setup_requires=pytest_runner + wheel + ["setuptools_scm"],
|
||||||
|
tests_require=[
|
||||||
|
'pytest>=2.8',
|
||||||
|
],
|
||||||
|
install_requires=[
|
||||||
|
"fonttools[ufo] >= 3.32.0",
|
||||||
|
],
|
||||||
|
extras_require={"cli": ["defcon >= 0.6.0"]},
|
||||||
|
entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]},
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: Apache Software License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Topic :: Scientific/Engineering :: Mathematics',
|
||||||
|
'Topic :: Multimedia :: Graphics :: Graphics Conversion',
|
||||||
|
'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
],
|
||||||
|
cmdclass={"build_ext": cython_build_ext, "sdist": cython_sdist},
|
||||||
|
)
|
2
test-requirements.txt
Normal file
2
test-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
coverage
|
||||||
|
pytest
|
85
tests/cli_test.py
Normal file
85
tests/cli_test.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
|
import defcon
|
||||||
|
|
||||||
|
from . import DATADIR
|
||||||
|
import pytest
|
||||||
|
import py
|
||||||
|
|
||||||
|
from cu2qu.ufo import CURVE_TYPE_LIB_KEY
|
||||||
|
from cu2qu.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
TEST_UFOS = [
|
||||||
|
py.path.local(DATADIR).join("RobotoSubset-Regular.ufo"),
|
||||||
|
py.path.local(DATADIR).join("RobotoSubset-Bold.ufo"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_paths(tmpdir):
|
||||||
|
result = []
|
||||||
|
for path in TEST_UFOS:
|
||||||
|
new_path = tmpdir / path.basename
|
||||||
|
path.copy(new_path)
|
||||||
|
result.append(new_path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class MainTest(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_main(*args):
|
||||||
|
main([str(p) for p in args if p])
|
||||||
|
|
||||||
|
def test_single_input_no_output(self, test_paths):
|
||||||
|
ufo_path = test_paths[0]
|
||||||
|
|
||||||
|
self.run_main(ufo_path)
|
||||||
|
|
||||||
|
font = defcon.Font(str(ufo_path))
|
||||||
|
assert font.lib[CURVE_TYPE_LIB_KEY] == "quadratic"
|
||||||
|
|
||||||
|
def test_single_input_output_file(self, tmpdir):
|
||||||
|
input_path = TEST_UFOS[0]
|
||||||
|
output_path = tmpdir / input_path.basename
|
||||||
|
self.run_main('-o', output_path, input_path)
|
||||||
|
|
||||||
|
assert output_path.check(dir=1)
|
||||||
|
|
||||||
|
def test_multiple_inputs_output_dir(self, tmpdir):
|
||||||
|
output_dir = tmpdir / "output_dir"
|
||||||
|
self.run_main('-d', output_dir, *TEST_UFOS)
|
||||||
|
|
||||||
|
assert output_dir.check(dir=1)
|
||||||
|
outputs = set(p.basename for p in output_dir.listdir())
|
||||||
|
assert "RobotoSubset-Regular.ufo" in outputs
|
||||||
|
assert "RobotoSubset-Bold.ufo" in outputs
|
||||||
|
|
||||||
|
def test_interpolatable_inplace(self, test_paths):
|
||||||
|
self.run_main('-i', *test_paths)
|
||||||
|
self.run_main('-i', *test_paths) # idempotent
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode", ["", "-i"], ids=["normal", "interpolatable"])
|
||||||
|
def test_copytree(self, mode, tmpdir):
|
||||||
|
output_dir = tmpdir / "output_dir"
|
||||||
|
self.run_main(mode, '-d', output_dir, *TEST_UFOS)
|
||||||
|
|
||||||
|
output_dir_2 = tmpdir / "output_dir_2"
|
||||||
|
# no conversion when curves are already quadratic, just copy
|
||||||
|
self.run_main(mode, '-d', output_dir_2, *output_dir.listdir())
|
||||||
|
# running again overwrites existing with the copy
|
||||||
|
self.run_main(mode, '-d', output_dir_2, *output_dir.listdir())
|
||||||
|
|
||||||
|
def test_multiprocessing(self, tmpdir, test_paths):
|
||||||
|
self.run_main(*(test_paths + ["-j"]))
|
||||||
|
|
||||||
|
def test_keep_direction(self, test_paths):
|
||||||
|
self.run_main('--keep-direction', *test_paths)
|
||||||
|
|
||||||
|
def test_conversion_error(self, test_paths):
|
||||||
|
self.run_main('--conversion-error', 0.002, *test_paths)
|
||||||
|
|
||||||
|
def test_conversion_error_short(self, test_paths):
|
||||||
|
self.run_main('-e', 0.003, test_paths[0])
|
285
tests/ufo_test.py
Normal file
285
tests/ufo_test.py
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
import os
|
||||||
|
|
||||||
|
from fontTools.misc.loggingTools import CapturingLogHandler
|
||||||
|
from defcon import Font, Glyph
|
||||||
|
from cu2qu.ufo import (
|
||||||
|
fonts_to_quadratic,
|
||||||
|
font_to_quadratic,
|
||||||
|
glyphs_to_quadratic,
|
||||||
|
glyph_to_quadratic,
|
||||||
|
logger,
|
||||||
|
CURVE_TYPE_LIB_KEY,
|
||||||
|
)
|
||||||
|
from cu2qu.errors import (
|
||||||
|
IncompatibleSegmentNumberError,
|
||||||
|
IncompatibleSegmentTypesError,
|
||||||
|
IncompatibleFontsError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import DATADIR
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
TEST_UFOS = [
|
||||||
|
os.path.join(DATADIR, "RobotoSubset-Regular.ufo"),
|
||||||
|
os.path.join(DATADIR, "RobotoSubset-Bold.ufo"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fonts():
|
||||||
|
return [Font(ufo) for ufo in TEST_UFOS]
|
||||||
|
|
||||||
|
|
||||||
|
class FontsToQuadraticTest(object):
|
||||||
|
|
||||||
|
def test_modified(self, fonts):
|
||||||
|
modified = fonts_to_quadratic(fonts)
|
||||||
|
assert modified
|
||||||
|
|
||||||
|
def test_stats(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
fonts_to_quadratic(fonts, stats=stats)
|
||||||
|
assert stats == {'1': 1, '2': 79, '3': 130, '4': 2}
|
||||||
|
|
||||||
|
def test_dump_stats(self, fonts):
|
||||||
|
with CapturingLogHandler(logger, "INFO") as captor:
|
||||||
|
fonts_to_quadratic(fonts, dump_stats=True)
|
||||||
|
assert captor.assertRegex("New spline lengths:")
|
||||||
|
|
||||||
|
def test_remember_curve_type(self, fonts):
|
||||||
|
fonts_to_quadratic(fonts, remember_curve_type=True)
|
||||||
|
assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "quadratic"
|
||||||
|
with CapturingLogHandler(logger, "INFO") as captor:
|
||||||
|
fonts_to_quadratic(fonts, remember_curve_type=True)
|
||||||
|
assert captor.assertRegex("already converted")
|
||||||
|
|
||||||
|
def test_no_remember_curve_type(self, fonts):
|
||||||
|
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
|
||||||
|
fonts_to_quadratic(fonts, remember_curve_type=False)
|
||||||
|
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
|
||||||
|
|
||||||
|
def test_different_glyphsets(self, fonts):
|
||||||
|
del fonts[0]['a']
|
||||||
|
assert 'a' not in fonts[0]
|
||||||
|
assert 'a' in fonts[1]
|
||||||
|
assert fonts_to_quadratic(fonts)
|
||||||
|
|
||||||
|
def test_max_err_em_float(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
fonts_to_quadratic(fonts, max_err_em=0.002, stats=stats)
|
||||||
|
assert stats == {'1': 5, '2': 193, '3': 14}
|
||||||
|
|
||||||
|
def test_max_err_em_list(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
fonts_to_quadratic(fonts, max_err_em=[0.002, 0.002], stats=stats)
|
||||||
|
assert stats == {'1': 5, '2': 193, '3': 14}
|
||||||
|
|
||||||
|
def test_max_err_float(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
fonts_to_quadratic(fonts, max_err=4.096, stats=stats)
|
||||||
|
assert stats == {'1': 5, '2': 193, '3': 14}
|
||||||
|
|
||||||
|
def test_max_err_list(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
fonts_to_quadratic(fonts, max_err=[4.096, 4.096], stats=stats)
|
||||||
|
assert stats == {'1': 5, '2': 193, '3': 14}
|
||||||
|
|
||||||
|
def test_both_max_err_and_max_err_em(self, fonts):
|
||||||
|
with pytest.raises(TypeError, match="Only one .* can be specified"):
|
||||||
|
fonts_to_quadratic(fonts, max_err=1.000, max_err_em=0.001)
|
||||||
|
|
||||||
|
def test_single_font(self, fonts):
|
||||||
|
assert font_to_quadratic(fonts[0], max_err_em=0.002,
|
||||||
|
reverse_direction=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GlyphsToQuadraticTest(object):
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["glyph", "expected"],
|
||||||
|
[('A', False), # contains no curves, it is not modified
|
||||||
|
('a', True)],
|
||||||
|
ids=['lines-only', 'has-curves']
|
||||||
|
)
|
||||||
|
def test_modified(self, fonts, glyph, expected):
|
||||||
|
glyphs = [f[glyph] for f in fonts]
|
||||||
|
assert glyphs_to_quadratic(glyphs) == expected
|
||||||
|
|
||||||
|
def test_stats(self, fonts):
|
||||||
|
stats = {}
|
||||||
|
glyphs_to_quadratic([f['a'] for f in fonts], stats=stats)
|
||||||
|
assert stats == {'2': 1, '3': 7, '4': 3, '5': 1}
|
||||||
|
|
||||||
|
def test_max_err_float(self, fonts):
|
||||||
|
glyphs = [f['a'] for f in fonts]
|
||||||
|
stats = {}
|
||||||
|
glyphs_to_quadratic(glyphs, max_err=4.096, stats=stats)
|
||||||
|
assert stats == {'2': 11, '3': 1}
|
||||||
|
|
||||||
|
def test_max_err_list(self, fonts):
|
||||||
|
glyphs = [f['a'] for f in fonts]
|
||||||
|
stats = {}
|
||||||
|
glyphs_to_quadratic(glyphs, max_err=[4.096, 4.096], stats=stats)
|
||||||
|
assert stats == {'2': 11, '3': 1}
|
||||||
|
|
||||||
|
def test_reverse_direction(self, fonts):
|
||||||
|
glyphs = [f['A'] for f in fonts]
|
||||||
|
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
|
||||||
|
|
||||||
|
def test_single_glyph(self, fonts):
|
||||||
|
assert glyph_to_quadratic(fonts[0]['a'], max_err=4.096,
|
||||||
|
reverse_direction=True)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["outlines", "exception", "message"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[
|
||||||
|
('moveTo', ((0, 0),)),
|
||||||
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
||||||
|
('curveTo', ((4, 4), (5, 5), (6, 6))),
|
||||||
|
('closePath', ()),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
('moveTo', ((7, 7),)),
|
||||||
|
('curveTo', ((8, 8), (9, 9), (10, 10))),
|
||||||
|
('closePath', ()),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
IncompatibleSegmentNumberError,
|
||||||
|
"have different number of segments",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
|
||||||
|
[
|
||||||
|
('moveTo', ((0, 0),)),
|
||||||
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
||||||
|
('closePath', ()),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
('moveTo', ((4, 4),)),
|
||||||
|
('lineTo', ((5, 5),)),
|
||||||
|
('closePath', ()),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
IncompatibleSegmentTypesError,
|
||||||
|
"have incompatible segment types",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"unequal-length",
|
||||||
|
"different-segment-types",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_incompatible_glyphs(self, outlines, exception, message):
|
||||||
|
glyphs = []
|
||||||
|
for i, outline in enumerate(outlines):
|
||||||
|
glyph = Glyph()
|
||||||
|
glyph.name = "glyph%d" % i
|
||||||
|
pen = glyph.getPen()
|
||||||
|
for operator, args in outline:
|
||||||
|
getattr(pen, operator)(*args)
|
||||||
|
glyphs.append(glyph)
|
||||||
|
with pytest.raises(exception) as excinfo:
|
||||||
|
glyphs_to_quadratic(glyphs)
|
||||||
|
assert excinfo.match(message)
|
||||||
|
|
||||||
|
def test_incompatible_fonts(self):
|
||||||
|
font1 = Font()
|
||||||
|
font1.info.unitsPerEm = 1000
|
||||||
|
glyph1 = font1.newGlyph("a")
|
||||||
|
pen1 = glyph1.getPen()
|
||||||
|
for operator, args in [("moveTo", ((0, 0),)),
|
||||||
|
("lineTo", ((1, 1),)),
|
||||||
|
("endPath", ())]:
|
||||||
|
getattr(pen1, operator)(*args)
|
||||||
|
|
||||||
|
font2 = Font()
|
||||||
|
font2.info.unitsPerEm = 1000
|
||||||
|
glyph2 = font2.newGlyph("a")
|
||||||
|
pen2 = glyph2.getPen()
|
||||||
|
for operator, args in [("moveTo", ((0, 0),)),
|
||||||
|
("curveTo", ((1, 1), (2, 2), (3, 3))),
|
||||||
|
("endPath", ())]:
|
||||||
|
getattr(pen2, operator)(*args)
|
||||||
|
|
||||||
|
with pytest.raises(IncompatibleFontsError) as excinfo:
|
||||||
|
fonts_to_quadratic([font1, font2])
|
||||||
|
assert excinfo.match("fonts contains incompatible glyphs: 'a'")
|
||||||
|
|
||||||
|
assert hasattr(excinfo.value, "glyph_errors")
|
||||||
|
error = excinfo.value.glyph_errors['a']
|
||||||
|
assert isinstance(error, IncompatibleSegmentTypesError)
|
||||||
|
assert error.segments == {1: ["line", "curve"]}
|
||||||
|
|
||||||
|
def test_already_quadratic(self):
|
||||||
|
glyph = Glyph()
|
||||||
|
pen = glyph.getPen()
|
||||||
|
pen.moveTo((0, 0))
|
||||||
|
pen.qCurveTo((1, 1), (2, 2))
|
||||||
|
pen.closePath()
|
||||||
|
assert not glyph_to_quadratic(glyph)
|
||||||
|
|
||||||
|
def test_open_paths(self):
|
||||||
|
glyph = Glyph()
|
||||||
|
pen = glyph.getPen()
|
||||||
|
pen.moveTo((0, 0))
|
||||||
|
pen.lineTo((1, 1))
|
||||||
|
pen.curveTo((2, 2), (3, 3), (4, 4))
|
||||||
|
pen.endPath()
|
||||||
|
assert glyph_to_quadratic(glyph)
|
||||||
|
# open contour is still open
|
||||||
|
assert glyph[-1][0].segmentType == "move"
|
||||||
|
|
||||||
|
def test_ignore_components(self):
|
||||||
|
glyph = Glyph()
|
||||||
|
pen = glyph.getPen()
|
||||||
|
pen.addComponent('a', (1, 0, 0, 1, 0, 0))
|
||||||
|
pen.moveTo((0, 0))
|
||||||
|
pen.curveTo((1, 1), (2, 2), (3, 3))
|
||||||
|
pen.closePath()
|
||||||
|
assert glyph_to_quadratic(glyph)
|
||||||
|
assert len(glyph.components) == 1
|
||||||
|
|
||||||
|
def test_overlapping_start_end_points(self):
|
||||||
|
# https://github.com/googlefonts/fontmake/issues/572
|
||||||
|
glyph1 = Glyph()
|
||||||
|
pen = glyph1.getPointPen()
|
||||||
|
pen.beginPath()
|
||||||
|
pen.addPoint((0, 651), segmentType="line")
|
||||||
|
pen.addPoint((0, 101), segmentType="line")
|
||||||
|
pen.addPoint((0, 101), segmentType="line")
|
||||||
|
pen.addPoint((0, 651), segmentType="line")
|
||||||
|
pen.endPath()
|
||||||
|
|
||||||
|
glyph2 = Glyph()
|
||||||
|
pen = glyph2.getPointPen()
|
||||||
|
pen.beginPath()
|
||||||
|
pen.addPoint((1, 651), segmentType="line")
|
||||||
|
pen.addPoint((2, 101), segmentType="line")
|
||||||
|
pen.addPoint((3, 101), segmentType="line")
|
||||||
|
pen.addPoint((4, 651), segmentType="line")
|
||||||
|
pen.endPath()
|
||||||
|
|
||||||
|
glyphs = [glyph1, glyph2]
|
||||||
|
|
||||||
|
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
|
||||||
|
|
||||||
|
assert [[(p.x, p.y) for p in glyph[0]] for glyph in glyphs] == [
|
||||||
|
[
|
||||||
|
(0, 651),
|
||||||
|
(0, 651),
|
||||||
|
(0, 101),
|
||||||
|
(0, 101),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
(1, 651),
|
||||||
|
(4, 651),
|
||||||
|
(3, 101),
|
||||||
|
(2, 101)
|
||||||
|
],
|
||||||
|
]
|
72
tools/benchmark.py
Normal file
72
tools/benchmark.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Copyright 2015 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 __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
|
import random
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
MAX_ERR = 5
|
||||||
|
|
||||||
|
SETUP_CODE = '''
|
||||||
|
from %(module)s import %(function)s
|
||||||
|
from %(benchmark_module)s import %(setup_function)s
|
||||||
|
args = %(setup_function)s()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def generate_curve():
|
||||||
|
return [
|
||||||
|
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
||||||
|
for point in range(4)]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_curve_to_quadratic():
|
||||||
|
return generate_curve(), MAX_ERR
|
||||||
|
|
||||||
|
|
||||||
|
def setup_curves_to_quadratic():
|
||||||
|
num_curves = 3
|
||||||
|
return (
|
||||||
|
[generate_curve() for curve in range(num_curves)],
|
||||||
|
[MAX_ERR] * num_curves)
|
||||||
|
|
||||||
|
|
||||||
|
def run_benchmark(
|
||||||
|
benchmark_module, module, function, setup_suffix='', repeat=1000):
|
||||||
|
setup_func = 'setup_' + function
|
||||||
|
if setup_suffix:
|
||||||
|
print('%s with %s:' % (function, setup_suffix), end='')
|
||||||
|
setup_func += '_' + setup_suffix
|
||||||
|
else:
|
||||||
|
print('%s:' % function, end='')
|
||||||
|
results = timeit.repeat(
|
||||||
|
'%s(*args)' % function,
|
||||||
|
setup=(SETUP_CODE % {
|
||||||
|
'benchmark_module': benchmark_module, 'setup_function': setup_func,
|
||||||
|
'module': module, 'function': function}),
|
||||||
|
repeat=repeat, number=1)
|
||||||
|
print('\tavg=%dus' % (sum(results) / len(results) * 1000000.),
|
||||||
|
'\tmin=%dus' % (min(results) * 1000000.))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
run_benchmark('benchmark', 'cu2qu', 'curve_to_quadratic')
|
||||||
|
run_benchmark('benchmark', 'cu2qu', 'curves_to_quadratic')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
random.seed(1)
|
||||||
|
main()
|
42
tools/ufo_benchmark.py
Normal file
42
tools/ufo_benchmark.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 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 __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from benchmark import run_benchmark
|
||||||
|
|
||||||
|
MAX_ERR_EM = 0.002
|
||||||
|
DATADIR = os.path.join(
|
||||||
|
os.path.dirname(__file__), os.path.pardir, 'tests', 'data')
|
||||||
|
|
||||||
|
|
||||||
|
def setup_fonts_to_quadratic_defcon():
|
||||||
|
from defcon import Font
|
||||||
|
return [[Font(os.path.join(DATADIR, 'RobotoSubset-Regular.ufo'))],
|
||||||
|
MAX_ERR_EM]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
run_benchmark(
|
||||||
|
'ufo_benchmark', 'cu2qu.ufo', 'fonts_to_quadratic',
|
||||||
|
setup_suffix='defcon', repeat=10)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
random.seed(1)
|
||||||
|
main()
|
32
tools/update_cython_shadow.py
Normal file
32
tools/update_cython_shadow.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
""" Update the embedded Lib/cu2qu/cython.py module with the contents of
|
||||||
|
the latest cython repository.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
$ python tools/update_cython_shadow.py 0.28.5
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
header = b'''\
|
||||||
|
""" This module is copied verbatim from the "Cython.Shadow" module:
|
||||||
|
https://github.com/cython/cython/blob/master/Cython/Shadow.py
|
||||||
|
|
||||||
|
Cython is licensed under the Apache 2.0 Software License.
|
||||||
|
"""
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = sys.argv[1]
|
||||||
|
except IndexError:
|
||||||
|
version = "master"
|
||||||
|
|
||||||
|
CYTHON_SHADOW_URL = (
|
||||||
|
"https://raw.githubusercontent.com/cython/cython/%s/Cython/Shadow.py"
|
||||||
|
) % version
|
||||||
|
|
||||||
|
r = requests.get(CYTHON_SHADOW_URL, allow_redirects=True)
|
||||||
|
with open("Lib/cu2qu/cython.py", "wb") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(r.content)
|
97
tox.ini
Normal file
97
tox.ini
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
[tox]
|
||||||
|
envlist = py{27,37}-{cy,nocy}, htmlcov
|
||||||
|
package_name = cu2qu
|
||||||
|
; we skip tox's own sdist generation as we need to pass different environment
|
||||||
|
; variables for testing buiding with and without cython
|
||||||
|
skipsdist = true
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
setenv =
|
||||||
|
nocy: CU2QU_WITH_CYTHON=0
|
||||||
|
cy: CU2QU_WITH_CYTHON=1
|
||||||
|
cy: CYTHON_TRACE=1
|
||||||
|
cy: CYTHON_ANNOTATE=1
|
||||||
|
; download the latest pip, setuptools and wheel when creating the venv
|
||||||
|
download = true
|
||||||
|
deps =
|
||||||
|
-rtest-requirements.txt
|
||||||
|
-rrequirements.txt
|
||||||
|
cy: cython
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
# create source distribution in a temp dir
|
||||||
|
python setup.py --quiet sdist --dist-dir {envtmpdir}
|
||||||
|
|
||||||
|
# install from sdist
|
||||||
|
python -m pip install --no-build-isolation --ignore-installed --pre --no-deps --no-cache-dir --find-links {envtmpdir} {[tox]package_name}
|
||||||
|
|
||||||
|
# ensure we are running the requested cu2qu version (compiled vs interpreted)
|
||||||
|
nocy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED and sys.exit(1)"
|
||||||
|
cy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED or sys.exit(1)"
|
||||||
|
|
||||||
|
# run tests with code coverage enabled
|
||||||
|
coverage run --parallel-mode -m pytest {posargs}
|
||||||
|
|
||||||
|
[testenv:htmlcov]
|
||||||
|
deps =
|
||||||
|
coverage
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
coverage combine
|
||||||
|
coverage report
|
||||||
|
coverage html
|
||||||
|
|
||||||
|
[testenv:codecov]
|
||||||
|
passenv = *
|
||||||
|
deps =
|
||||||
|
coverage
|
||||||
|
codecov
|
||||||
|
ignore_outcome = true
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
coverage combine
|
||||||
|
codecov --env TRAVIS_PYTHON_VERSION
|
||||||
|
|
||||||
|
[testenv:update-cython]
|
||||||
|
deps = requests
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
python tools/update_cython_shadow.py {posargs}
|
||||||
|
|
||||||
|
[testenv:sdist]
|
||||||
|
deps =
|
||||||
|
setuptools
|
||||||
|
cython
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)'
|
||||||
|
python setup.py --with-cython sdist --dist-dir dist
|
||||||
|
|
||||||
|
[testenv:pure-wheel]
|
||||||
|
deps =
|
||||||
|
{[testenv:sdist]deps}
|
||||||
|
pip
|
||||||
|
wheel
|
||||||
|
setenv = CU2QU_WITH_CYTHON=0
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
{[testenv:sdist]commands}
|
||||||
|
pip wheel --pre --no-deps --no-cache-dir --wheel-dir dist --find-links dist \
|
||||||
|
--no-binary {[tox]package_name} {[tox]package_name}
|
||||||
|
|
||||||
|
[testenv:native-wheel]
|
||||||
|
deps = {[testenv:pure-wheel]deps}
|
||||||
|
setenv = CU2QU_WITH_CYTHON=1
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands = {[testenv:pure-wheel]commands}
|
||||||
|
|
||||||
|
; we only upload the source distribution to PyPI (for now)
|
||||||
|
[testenv:pypi]
|
||||||
|
deps =
|
||||||
|
{[testenv:sdist]deps}
|
||||||
|
twine
|
||||||
|
passenv = TWINE_USERNAME TWINE_PASSWORD
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
{[testenv:sdist]commands}
|
||||||
|
twine upload dist/*.zip
|
Loading…
x
Reference in New Issue
Block a user