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