pax_global_header 0000666 0000000 0000000 00000000064 13617262337 0014524 g ustar 00root root 0000000 0000000 52 comment=eaeef05bf07b317b4ec45d24975c45731e995e97
cu2qu-1.6.7/ 0000775 0000000 0000000 00000000000 13617262337 0012576 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/.codecov.yml 0000664 0000000 0000000 00000000115 13617262337 0015016 0 ustar 00root root 0000000 0000000 comment: false
coverage:
status:
project: off
patch: off
cu2qu-1.6.7/.coveragerc 0000664 0000000 0000000 00000001623 13617262337 0014721 0 ustar 00root root 0000000 0000000 [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
cu2qu-1.6.7/.gitignore 0000664 0000000 0000000 00000000534 13617262337 0014570 0 ustar 00root root 0000000 0000000 # 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
cu2qu-1.6.7/.pyup.yml 0000664 0000000 0000000 00000000250 13617262337 0014371 0 ustar 00root root 0000000 0000000 # controls the frequency of updates (undocumented beta feature)
schedule: every week
# do not pin dependencies unless they have explicit version specifiers
pin: False
cu2qu-1.6.7/.travis.yml 0000664 0000000 0000000 00000002453 13617262337 0014713 0 ustar 00root root 0000000 0000000 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
cu2qu-1.6.7/CONTRIBUTING.md 0000664 0000000 0000000 00000002652 13617262337 0015034 0 ustar 00root root 0000000 0000000 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).
cu2qu-1.6.7/LICENSE 0000664 0000000 0000000 00000026135 13617262337 0013612 0 ustar 00root root 0000000 0000000 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.
cu2qu-1.6.7/Lib/ 0000775 0000000 0000000 00000000000 13617262337 0013304 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/Lib/cu2qu/ 0000775 0000000 0000000 00000000000 13617262337 0014343 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/Lib/cu2qu/__init__.py 0000664 0000000 0000000 00000001432 13617262337 0016454 0 ustar 00root root 0000000 0000000 # 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
try:
from ._version import version as __version__
except ImportError:
__version__ = "0.0.0+unknown"
from .cu2qu import *
cu2qu-1.6.7/Lib/cu2qu/__main__.py 0000664 0000000 0000000 00000000130 13617262337 0016427 0 ustar 00root root 0000000 0000000 import sys
from cu2qu.cli import main
if __name__ == "__main__":
sys.exit(main())
cu2qu-1.6.7/Lib/cu2qu/cli.py 0000664 0000000 0000000 00000012150 13617262337 0015463 0 ustar 00root root 0000000 0000000 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)
cu2qu-1.6.7/Lib/cu2qu/cu2qu.py 0000664 0000000 0000000 00000030242 13617262337 0015755 0 ustar 00root root 0000000 0000000 #cython: language_level=3
#distutils: define_macros=CYTHON_TRACE_NOGIL=1
# 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
try:
import cython
except ImportError:
# if not installed, use the embedded (no-op) copy of Cython.Shadow
from . import cython
import math
from .errors import Error as Cu2QuError, ApproxNotFoundError
__all__ = ['curve_to_quadratic', 'curves_to_quadratic']
MAX_N = 100
NAN = float("NaN")
if cython.compiled:
# Yep, I'm compiled.
COMPILED = True
else:
# Just a lowly interpreted script.
COMPILED = False
@cython.cfunc
@cython.inline
@cython.returns(cython.double)
@cython.locals(v1=cython.complex, v2=cython.complex)
def dot(v1, v2):
"""Return the dot product of two vectors."""
return (v1 * v2.conjugate()).real
@cython.cfunc
@cython.inline
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex)
def calc_cubic_points(a, b, c, d):
_1 = d
_2 = (c / 3.0) + d
_3 = (b + c) / 3.0 + _2
_4 = a + d + c + b
return _1, _2, _3, _4
@cython.cfunc
@cython.inline
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
def calc_cubic_parameters(p0, p1, p2, p3):
c = (p1 - p0) * 3.0
b = (p2 - p1) * 3.0 - c
d = p0
a = p3 - d - c - b
return a, b, c, d
@cython.cfunc
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
def split_cubic_into_n_iter(p0, p1, p2, p3, n):
# Hand-coded special-cases
if n == 2:
return iter(split_cubic_into_two(p0, p1, p2, p3))
if n == 3:
return iter(split_cubic_into_three(p0, p1, p2, p3))
if n == 4:
a, b = split_cubic_into_two(p0, p1, p2, p3)
return iter(split_cubic_into_two(*a) + split_cubic_into_two(*b))
if n == 6:
a, b = split_cubic_into_two(p0, p1, p2, p3)
return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b))
return _split_cubic_into_n_gen(p0,p1,p2,p3,n)
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int)
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int)
@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex)
def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3)
dt = 1 / n
delta_2 = dt * dt
delta_3 = dt * delta_2
for i in range(n):
t1 = i * dt
t1_2 = t1 * t1
# calc new a, b, c and d
a1 = a * delta_3
b1 = (3*a*t1 + b) * delta_2
c1 = (2*b*t1 + c + 3*a*t1_2) * dt
d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d
yield calc_cubic_points(a1, b1, c1, d1)
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(mid=cython.complex, deriv3=cython.complex)
def split_cubic_into_two(p0, p1, p2, p3):
mid = (p0 + 3 * (p1 + p2) + p3) * .125
deriv3 = (p3 + p2 - p1 - p0) * .125
return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
(mid, mid + deriv3, (p2 + p3) * .5, p3))
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double)
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex)
def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
# we define 1/27 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27
deriv1 = (p3 + 3*p2 - 4*p0) * _27
mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27
deriv2 = (4*p3 - 3*p1 - p0) * _27
return ((p0, (2*p0 + p1) / 3.0, mid1 - deriv1, mid1),
(mid1, mid1 + deriv1, mid2 - deriv2, mid2),
(mid2, mid2 + deriv2, (p2 + 2*p3) / 3.0, p3))
@cython.returns(cython.complex)
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(_p1=cython.complex, _p2=cython.complex)
def cubic_approx_control(t, p0, p1, p2, p3):
"""Approximate a cubic bezier curve with a quadratic one.
Returns the candidate control point."""
_p1 = p0 + (p1 - p0) * 1.5
_p2 = p3 + (p2 - p3) * 1.5
return _p1 + (_p2 - _p1) * t
@cython.returns(cython.complex)
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
def calc_intersect(a, b, c, d):
"""Calculate the intersection of ab and cd, given a, b, c, d."""
ab = b - a
cd = d - c
p = ab * 1j
try:
h = dot(p, a - c) / dot(p, cd)
except ZeroDivisionError:
return complex(NAN, NAN)
return c + cd * h
@cython.cfunc
@cython.returns(cython.int)
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(mid=cython.complex, deriv3=cython.complex)
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
"""Returns True if the cubic Bezier p entirely lies within a distance
tolerance of origin, False otherwise. Assumes that p0 and p3 do fit
within tolerance of origin, and just checks the inside of the curve."""
# First check p2 then p1, as p2 has higher error early on.
if abs(p2) <= tolerance and abs(p1) <= tolerance:
return True
# Split.
mid = (p0 + 3 * (p1 + p2) + p3) * .125
if abs(mid) > tolerance:
return False
deriv3 = (p3 + p2 - p1 - p0) * .125
return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and
cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance))
@cython.cfunc
@cython.locals(tolerance=cython.double, _2_3=cython.double)
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
"""Return the uniq quadratic approximating cubic that maintains
endpoint tangents if that is within tolerance, None otherwise."""
# we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
q1 = calc_intersect(*cubic)
if math.isnan(q1.imag):
return None
c0 = cubic[0]
c3 = cubic[3]
c1 = c0 + (q1 - c0) * _2_3
c2 = c3 + (q1 - c3) * _2_3
if not cubic_farthest_fit_inside(0,
c1 - cubic[1],
c2 - cubic[2],
0, tolerance):
return None
return c0, q1, c3
@cython.cfunc
@cython.locals(n=cython.int, tolerance=cython.double, _2_3=cython.double)
@cython.locals(i=cython.int)
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex)
def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
"""Approximate a cubic bezier curve with a spline of n quadratics.
Returns None if no quadratic approximation is found which lies entirely
within a distance `tolerance` from the original curve.
"""
# we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
if n == 1:
return cubic_approx_quadratic(cubic, tolerance)
cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n)
# calculate the spline of quadratics and check errors at the same time.
next_cubic = next(cubics)
next_q1 = cubic_approx_control(0, *next_cubic)
q2 = cubic[0]
d1 = 0j
spline = [cubic[0], next_q1]
for i in range(1, n+1):
# Current cubic to convert
c0, c1, c2, c3 = next_cubic
# Current quadratic approximation of current cubic
q0 = q2
q1 = next_q1
if i < n:
next_cubic = next(cubics)
next_q1 = cubic_approx_control(i / (n-1), *next_cubic)
spline.append(next_q1)
q2 = (q1 + next_q1) * .5
else:
q2 = c3
# End-point deltas
d0 = d1
d1 = q2 - c3
if (abs(d1) > tolerance or
not cubic_farthest_fit_inside(d0,
q0 + (q1 - q0) * _2_3 - c1,
q2 + (q1 - q2) * _2_3 - c2,
d1,
tolerance)):
return None
spline.append(cubic[3])
return spline
@cython.locals(max_err=cython.double)
@cython.locals(n=cython.int)
def curve_to_quadratic(curve, max_err):
"""Return a quadratic spline approximating this cubic bezier.
Raise 'ApproxNotFoundError' if no suitable approximation can be found
with the given parameters.
"""
curve = [complex(*p) for p in curve]
for n in range(1, MAX_N + 1):
spline = cubic_approx_spline(curve, n, max_err)
if spline is not None:
# done. go home
return [(s.real, s.imag) for s in spline]
raise ApproxNotFoundError(curve)
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
def curves_to_quadratic(curves, max_errors):
"""Return quadratic splines approximating these cubic beziers.
Raise 'ApproxNotFoundError' if no suitable approximation can be found
for all curves with the given parameters.
"""
curves = [[complex(*p) for p in curve] for curve in curves]
assert len(max_errors) == len(curves)
l = len(curves)
splines = [None] * l
last_i = i = 0
n = 1
while True:
spline = cubic_approx_spline(curves[i], n, max_errors[i])
if spline is None:
if n == MAX_N:
break
n += 1
last_i = i
continue
splines[i] = spline
i = (i + 1) % l
if i == last_i:
# done. go home
return [[(s.real, s.imag) for s in spline] for spline in splines]
raise ApproxNotFoundError(curves)
if __name__ == '__main__':
import random
import timeit
MAX_ERR = 5
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=5, number=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='')
def wrapper(function, setup_func):
function = globals()[function]
setup_func = globals()[setup_func]
def wrapped():
return function(*setup_func())
return wrapped
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
print('\t%5.1fus' % (min(results) * 1000000. / number))
def main():
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic')
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic')
random.seed(1)
main()
cu2qu-1.6.7/Lib/cu2qu/cython.py 0000664 0000000 0000000 00000031526 13617262337 0016230 0 ustar 00root root 0000000 0000000 """ 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.
"""
# cython.* namespace for pure mode.
from __future__ import absolute_import
__version__ = "0.29.14"
try:
from __builtin__ import basestring
except ImportError:
basestring = str
# BEGIN shameless copy from Cython/minivect/minitypes.py
class _ArrayType(object):
is_array = True
subtypes = ['dtype']
def __init__(self, dtype, ndim, is_c_contig=False, is_f_contig=False,
inner_contig=False, broadcasting=None):
self.dtype = dtype
self.ndim = ndim
self.is_c_contig = is_c_contig
self.is_f_contig = is_f_contig
self.inner_contig = inner_contig or is_c_contig or is_f_contig
self.broadcasting = broadcasting
def __repr__(self):
axes = [":"] * self.ndim
if self.is_c_contig:
axes[-1] = "::1"
elif self.is_f_contig:
axes[0] = "::1"
return "%s[%s]" % (self.dtype, ", ".join(axes))
def index_type(base_type, item):
"""
Support array type creation by slicing, e.g. double[:, :] specifies
a 2D strided array of doubles. The syntax is the same as for
Cython memoryviews.
"""
class InvalidTypeSpecification(Exception):
pass
def verify_slice(s):
if s.start or s.stop or s.step not in (None, 1):
raise InvalidTypeSpecification(
"Only a step of 1 may be provided to indicate C or "
"Fortran contiguity")
if isinstance(item, tuple):
step_idx = None
for idx, s in enumerate(item):
verify_slice(s)
if s.step and (step_idx or idx not in (0, len(item) - 1)):
raise InvalidTypeSpecification(
"Step may only be provided once, and only in the "
"first or last dimension.")
if s.step == 1:
step_idx = idx
return _ArrayType(base_type, len(item),
is_c_contig=step_idx == len(item) - 1,
is_f_contig=step_idx == 0)
elif isinstance(item, slice):
verify_slice(item)
return _ArrayType(base_type, 1, is_c_contig=bool(item.step))
else:
# int[8] etc.
assert int(item) == item # array size must be a plain integer
array(base_type, item)
# END shameless copy
compiled = False
_Unspecified = object()
# Function decorators
def _empty_decorator(x):
return x
def locals(**arg_types):
return _empty_decorator
def test_assert_path_exists(*paths):
return _empty_decorator
def test_fail_if_path_exists(*paths):
return _empty_decorator
class _EmptyDecoratorAndManager(object):
def __call__(self, x):
return x
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
pass
class _Optimization(object):
pass
cclass = ccall = cfunc = _EmptyDecoratorAndManager()
returns = wraparound = boundscheck = initializedcheck = nonecheck = \
embedsignature = cdivision = cdivision_warnings = \
always_allows_keywords = profile = linetrace = infer_types = \
unraisable_tracebacks = freelist = \
lambda _: _EmptyDecoratorAndManager()
exceptval = lambda _=None, check=True: _EmptyDecoratorAndManager()
overflowcheck = lambda _: _EmptyDecoratorAndManager()
optimization = _Optimization()
overflowcheck.fold = optimization.use_switch = \
optimization.unpack_method_calls = lambda arg: _EmptyDecoratorAndManager()
final = internal = type_version_tag = no_gc_clear = no_gc = _empty_decorator
_cython_inline = None
def inline(f, *args, **kwds):
if isinstance(f, basestring):
global _cython_inline
if _cython_inline is None:
from Cython.Build.Inline import cython_inline as _cython_inline
return _cython_inline(f, *args, **kwds)
else:
assert len(args) == len(kwds) == 0
return f
def compile(f):
from Cython.Build.Inline import RuntimeCompiledFunction
return RuntimeCompiledFunction(f)
# Special functions
def cdiv(a, b):
q = a / b
if q < 0:
q += 1
return q
def cmod(a, b):
r = a % b
if (a*b) < 0:
r -= b
return r
# Emulated language constructs
def cast(type, *args, **kwargs):
kwargs.pop('typecheck', None)
assert not kwargs
if hasattr(type, '__call__'):
return type(*args)
else:
return args[0]
def sizeof(arg):
return 1
def typeof(arg):
return arg.__class__.__name__
# return type(arg)
def address(arg):
return pointer(type(arg))([arg])
def declare(type=None, value=_Unspecified, **kwds):
if type not in (None, object) and hasattr(type, '__call__'):
if value is not _Unspecified:
return type(value)
else:
return type()
else:
return value
class _nogil(object):
"""Support for 'with nogil' statement and @nogil decorator.
"""
def __call__(self, x):
if callable(x):
# Used as function decorator => return the function unchanged.
return x
# Used as conditional context manager or to create an "@nogil(True/False)" decorator => keep going.
return self
def __enter__(self):
pass
def __exit__(self, exc_class, exc, tb):
return exc_class is None
nogil = _nogil()
gil = _nogil()
del _nogil
# Emulated types
class CythonMetaType(type):
def __getitem__(type, ix):
return array(type, ix)
CythonTypeObject = CythonMetaType('CythonTypeObject', (object,), {})
class CythonType(CythonTypeObject):
def _pointer(self, n=1):
for i in range(n):
self = pointer(self)
return self
class PointerType(CythonType):
def __init__(self, value=None):
if isinstance(value, (ArrayType, PointerType)):
self._items = [cast(self._basetype, a) for a in value._items]
elif isinstance(value, list):
self._items = [cast(self._basetype, a) for a in value]
elif value is None or value == 0:
self._items = []
else:
raise ValueError
def __getitem__(self, ix):
if ix < 0:
raise IndexError("negative indexing not allowed in C")
return self._items[ix]
def __setitem__(self, ix, value):
if ix < 0:
raise IndexError("negative indexing not allowed in C")
self._items[ix] = cast(self._basetype, value)
def __eq__(self, value):
if value is None and not self._items:
return True
elif type(self) != type(value):
return False
else:
return not self._items and not value._items
def __repr__(self):
return "%s *" % (self._basetype,)
class ArrayType(PointerType):
def __init__(self):
self._items = [None] * self._n
class StructType(CythonType):
def __init__(self, cast_from=_Unspecified, **data):
if cast_from is not _Unspecified:
# do cast
if len(data) > 0:
raise ValueError('Cannot accept keyword arguments when casting.')
if type(cast_from) is not type(self):
raise ValueError('Cannot cast from %s'%cast_from)
for key, value in cast_from.__dict__.items():
setattr(self, key, value)
else:
for key, value in data.items():
setattr(self, key, value)
def __setattr__(self, key, value):
if key in self._members:
self.__dict__[key] = cast(self._members[key], value)
else:
raise AttributeError("Struct has no member '%s'" % key)
class UnionType(CythonType):
def __init__(self, cast_from=_Unspecified, **data):
if cast_from is not _Unspecified:
# do type cast
if len(data) > 0:
raise ValueError('Cannot accept keyword arguments when casting.')
if isinstance(cast_from, dict):
datadict = cast_from
elif type(cast_from) is type(self):
datadict = cast_from.__dict__
else:
raise ValueError('Cannot cast from %s'%cast_from)
else:
datadict = data
if len(datadict) > 1:
raise AttributeError("Union can only store one field at a time.")
for key, value in datadict.items():
setattr(self, key, value)
def __setattr__(self, key, value):
if key in '__dict__':
CythonType.__setattr__(self, key, value)
elif key in self._members:
self.__dict__ = {key: cast(self._members[key], value)}
else:
raise AttributeError("Union has no member '%s'" % key)
def pointer(basetype):
class PointerInstance(PointerType):
_basetype = basetype
return PointerInstance
def array(basetype, n):
class ArrayInstance(ArrayType):
_basetype = basetype
_n = n
return ArrayInstance
def struct(**members):
class StructInstance(StructType):
_members = members
for key in members:
setattr(StructInstance, key, None)
return StructInstance
def union(**members):
class UnionInstance(UnionType):
_members = members
for key in members:
setattr(UnionInstance, key, None)
return UnionInstance
class typedef(CythonType):
def __init__(self, type, name=None):
self._basetype = type
self.name = name
def __call__(self, *arg):
value = cast(self._basetype, *arg)
return value
def __repr__(self):
return self.name or str(self._basetype)
__getitem__ = index_type
class _FusedType(CythonType):
pass
def fused_type(*args):
if not args:
raise TypeError("Expected at least one type as argument")
# Find the numeric type with biggest rank if all types are numeric
rank = -1
for type in args:
if type not in (py_int, py_long, py_float, py_complex):
break
if type_ordering.index(type) > rank:
result_type = type
else:
return result_type
# Not a simple numeric type, return a fused type instance. The result
# isn't really meant to be used, as we can't keep track of the context in
# pure-mode. Casting won't do anything in this case.
return _FusedType()
def _specialized_from_args(signatures, args, kwargs):
"Perhaps this should be implemented in a TreeFragment in Cython code"
raise Exception("yet to be implemented")
py_int = typedef(int, "int")
try:
py_long = typedef(long, "long")
except NameError: # Py3
py_long = typedef(int, "long")
py_float = typedef(float, "float")
py_complex = typedef(complex, "double complex")
# Predefined types
int_types = ['char', 'short', 'Py_UNICODE', 'int', 'Py_UCS4', 'long', 'longlong', 'Py_ssize_t', 'size_t']
float_types = ['longdouble', 'double', 'float']
complex_types = ['longdoublecomplex', 'doublecomplex', 'floatcomplex', 'complex']
other_types = ['bint', 'void', 'Py_tss_t']
to_repr = {
'longlong': 'long long',
'longdouble': 'long double',
'longdoublecomplex': 'long double complex',
'doublecomplex': 'double complex',
'floatcomplex': 'float complex',
}.get
gs = globals()
# note: cannot simply name the unicode type here as 2to3 gets in the way and replaces it by str
try:
import __builtin__ as builtins
except ImportError: # Py3
import builtins
gs['unicode'] = typedef(getattr(builtins, 'unicode', str), 'unicode')
del builtins
for name in int_types:
reprname = to_repr(name, name)
gs[name] = typedef(py_int, reprname)
if name not in ('Py_UNICODE', 'Py_UCS4') and not name.endswith('size_t'):
gs['u'+name] = typedef(py_int, "unsigned " + reprname)
gs['s'+name] = typedef(py_int, "signed " + reprname)
for name in float_types:
gs[name] = typedef(py_float, to_repr(name, name))
for name in complex_types:
gs[name] = typedef(py_complex, to_repr(name, name))
bint = typedef(bool, "bint")
void = typedef(None, "void")
Py_tss_t = typedef(None, "Py_tss_t")
for t in int_types + float_types + complex_types + other_types:
for i in range(1, 4):
gs["%s_%s" % ('p'*i, t)] = gs[t]._pointer(i)
NULL = gs['p_void'](0)
# looks like 'gs' has some users out there by now...
#del gs
integral = floating = numeric = _FusedType()
type_ordering = [py_int, py_long, py_float, py_complex]
class CythonDotParallel(object):
"""
The cython.parallel module.
"""
__all__ = ['parallel', 'prange', 'threadid']
def parallel(self, num_threads=None):
return nogil
def prange(self, start=0, stop=None, step=1, nogil=False, schedule=None, chunksize=None, num_threads=None):
if stop is None:
stop = start
start = 0
return range(start, stop, step)
def threadid(self):
return 0
# def threadsavailable(self):
# return 1
import sys
sys.modules['cython.parallel'] = CythonDotParallel()
del sys
cu2qu-1.6.7/Lib/cu2qu/errors.py 0000664 0000000 0000000 00000003601 13617262337 0016231 0 ustar 00root root 0000000 0000000 from __future__ import print_function, absolute_import, division
class Error(Exception):
"""Base Cu2Qu exception class for all other errors."""
class ApproxNotFoundError(Error):
def __init__(self, curve):
message = "no approximation found: %s" % curve
super(Error, self).__init__(message)
self.curve = curve
class UnequalZipLengthsError(Error):
pass
class IncompatibleGlyphsError(Error):
def __init__(self, glyphs):
assert len(glyphs) > 1
self.glyphs = glyphs
names = set(repr(g.name) for g in glyphs)
if len(names) > 1:
self.combined_name = "{%s}" % ", ".join(sorted(names))
else:
self.combined_name = names.pop()
def __repr__(self):
return "<%s %s>" % (type(self).__name__, self.combined_name)
class IncompatibleSegmentNumberError(IncompatibleGlyphsError):
def __str__(self):
return "Glyphs named %s have different number of segments" % (
self.combined_name
)
class IncompatibleSegmentTypesError(IncompatibleGlyphsError):
def __init__(self, glyphs, segments):
IncompatibleGlyphsError.__init__(self, glyphs)
self.segments = segments
def __str__(self):
lines = []
ndigits = len(str(max(self.segments)))
for i, tags in sorted(self.segments.items()):
lines.append(
"%s: (%s)" % (str(i).rjust(ndigits), ", ".join(repr(t) for t in tags))
)
return "Glyphs named %s have incompatible segment types:\n %s" % (
self.combined_name,
"\n ".join(lines),
)
class IncompatibleFontsError(Error):
def __init__(self, glyph_errors):
self.glyph_errors = glyph_errors
def __str__(self):
return "fonts contains incompatible glyphs: %s" % (
", ".join(repr(g) for g in sorted(self.glyph_errors.keys()))
)
cu2qu-1.6.7/Lib/cu2qu/pens.py 0000664 0000000 0000000 00000023565 13617262337 0015675 0 ustar 00root root 0000000 0000000 from __future__ import print_function, division, absolute_import
from cu2qu import curve_to_quadratic
from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment
from fontTools.pens.reverseContourPen import ReverseContourPen
from fontTools.pens.pointPen import BasePointToSegmentPen
from fontTools.pens.pointPen import ReverseContourPointPen
class Cu2QuPen(AbstractPen):
""" A filter pen to convert cubic bezier curves to quadratic b-splines
using the FontTools SegmentPen protocol.
other_pen: another SegmentPen used to draw the transformed outline.
max_err: maximum approximation error in font units.
reverse_direction: flip the contours' direction but keep starting point.
stats: a dictionary counting the point numbers of quadratic segments.
ignore_single_points: don't emit contours containing only a single point
NOTE: The "ignore_single_points" argument is deprecated since v1.3.0,
which dropped Robofab subpport. It's no longer needed to special-case
UFO2-style anchors (aka "named points") when using ufoLib >= 2.0,
as these are no longer drawn onto pens as single-point contours,
but are handled separately as anchors.
"""
def __init__(self, other_pen, max_err, reverse_direction=False,
stats=None, ignore_single_points=False):
if reverse_direction:
self.pen = ReverseContourPen(other_pen)
else:
self.pen = other_pen
self.max_err = max_err
self.stats = stats
if ignore_single_points:
import warnings
warnings.warn("ignore_single_points is deprecated and "
"will be removed in future versions",
UserWarning, stacklevel=2)
self.ignore_single_points = ignore_single_points
self.start_pt = None
self.current_pt = None
def _check_contour_is_open(self):
if self.current_pt is None:
raise AssertionError("moveTo is required")
def _check_contour_is_closed(self):
if self.current_pt is not None:
raise AssertionError("closePath or endPath is required")
def _add_moveTo(self):
if self.start_pt is not None:
self.pen.moveTo(self.start_pt)
self.start_pt = None
def moveTo(self, pt):
self._check_contour_is_closed()
self.start_pt = self.current_pt = pt
if not self.ignore_single_points:
self._add_moveTo()
def lineTo(self, pt):
self._check_contour_is_open()
self._add_moveTo()
self.pen.lineTo(pt)
self.current_pt = pt
def qCurveTo(self, *points):
self._check_contour_is_open()
n = len(points)
if n == 1:
self.lineTo(points[0])
elif n > 1:
self._add_moveTo()
self.pen.qCurveTo(*points)
self.current_pt = points[-1]
else:
raise AssertionError("illegal qcurve segment point count: %d" % n)
def _curve_to_quadratic(self, pt1, pt2, pt3):
curve = (self.current_pt, pt1, pt2, pt3)
quadratic = curve_to_quadratic(curve, self.max_err)
if self.stats is not None:
n = str(len(quadratic) - 2)
self.stats[n] = self.stats.get(n, 0) + 1
self.qCurveTo(*quadratic[1:])
def curveTo(self, *points):
self._check_contour_is_open()
n = len(points)
if n == 3:
# this is the most common case, so we special-case it
self._curve_to_quadratic(*points)
elif n > 3:
for segment in decomposeSuperBezierSegment(points):
self._curve_to_quadratic(*segment)
elif n == 2:
self.qCurveTo(*points)
elif n == 1:
self.lineTo(points[0])
else:
raise AssertionError("illegal curve segment point count: %d" % n)
def closePath(self):
self._check_contour_is_open()
if self.start_pt is None:
# if 'start_pt' is _not_ None, we are ignoring single-point paths
self.pen.closePath()
self.current_pt = self.start_pt = None
def endPath(self):
self._check_contour_is_open()
if self.start_pt is None:
self.pen.endPath()
self.current_pt = self.start_pt = None
def addComponent(self, glyphName, transformation):
self._check_contour_is_closed()
self.pen.addComponent(glyphName, transformation)
class Cu2QuPointPen(BasePointToSegmentPen):
""" A filter pen to convert cubic bezier curves to quadratic b-splines
using the RoboFab PointPen protocol.
other_point_pen: another PointPen used to draw the transformed outline.
max_err: maximum approximation error in font units.
reverse_direction: reverse the winding direction of all contours.
stats: a dictionary counting the point numbers of quadratic segments.
"""
def __init__(self, other_point_pen, max_err, reverse_direction=False,
stats=None):
BasePointToSegmentPen.__init__(self)
if reverse_direction:
self.pen = ReverseContourPointPen(other_point_pen)
else:
self.pen = other_point_pen
self.max_err = max_err
self.stats = stats
def _flushContour(self, segments):
assert len(segments) >= 1
closed = segments[0][0] != "move"
new_segments = []
prev_points = segments[-1][1]
prev_on_curve = prev_points[-1][0]
for segment_type, points in segments:
if segment_type == 'curve':
for sub_points in self._split_super_bezier_segments(points):
on_curve, smooth, name, kwargs = sub_points[-1]
bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
cubic = [prev_on_curve, bcp1, bcp2, on_curve]
quad = curve_to_quadratic(cubic, self.max_err)
if self.stats is not None:
n = str(len(quad) - 2)
self.stats[n] = self.stats.get(n, 0) + 1
new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
new_points.append((on_curve, smooth, name, kwargs))
new_segments.append(["qcurve", new_points])
prev_on_curve = sub_points[-1][0]
else:
new_segments.append([segment_type, points])
prev_on_curve = points[-1][0]
if closed:
# the BasePointToSegmentPen.endPath method that calls _flushContour
# rotates the point list of closed contours so that they end with
# the first on-curve point. We restore the original starting point.
new_segments = new_segments[-1:] + new_segments[:-1]
self._drawPoints(new_segments)
def _split_super_bezier_segments(self, points):
sub_segments = []
# n is the number of control points
n = len(points) - 1
if n == 2:
# a simple bezier curve segment
sub_segments.append(points)
elif n > 2:
# a "super" bezier; decompose it
on_curve, smooth, name, kwargs = points[-1]
num_sub_segments = n - 1
for i, sub_points in enumerate(decomposeSuperBezierSegment([
pt for pt, _, _, _ in points])):
new_segment = []
for point in sub_points[:-1]:
new_segment.append((point, False, None, {}))
if i == (num_sub_segments - 1):
# the last on-curve keeps its original attributes
new_segment.append((on_curve, smooth, name, kwargs))
else:
# on-curves of sub-segments are always "smooth"
new_segment.append((sub_points[-1], True, None, {}))
sub_segments.append(new_segment)
else:
raise AssertionError(
"expected 2 control points, found: %d" % n)
return sub_segments
def _drawPoints(self, segments):
pen = self.pen
pen.beginPath()
last_offcurves = []
for i, (segment_type, points) in enumerate(segments):
if segment_type in ("move", "line"):
assert len(points) == 1, (
"illegal line segment point count: %d" % len(points))
pt, smooth, name, kwargs = points[0]
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
elif segment_type == "qcurve":
assert len(points) >= 2, (
"illegal qcurve segment point count: %d" % len(points))
offcurves = points[:-1]
if offcurves:
if i == 0:
# any off-curve points preceding the first on-curve
# will be appended at the end of the contour
last_offcurves = offcurves
else:
for (pt, smooth, name, kwargs) in offcurves:
pen.addPoint(pt, None, smooth, name, **kwargs)
pt, smooth, name, kwargs = points[-1]
if pt is None:
# special quadratic contour with no on-curve points:
# we need to skip the "None" point. See also the Pen
# protocol's qCurveTo() method and fontTools.pens.basePen
pass
else:
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
else:
# 'curve' segments must have been converted to 'qcurve' by now
raise AssertionError(
"unexpected segment type: %r" % segment_type)
for (pt, smooth, name, kwargs) in last_offcurves:
pen.addPoint(pt, None, smooth, name, **kwargs)
pen.endPath()
def addComponent(self, baseGlyphName, transformation):
assert self.currentPath is None
self.pen.addComponent(baseGlyphName, transformation)
cu2qu-1.6.7/Lib/cu2qu/ufo.py 0000664 0000000 0000000 00000025610 13617262337 0015512 0 ustar 00root root 0000000 0000000 # 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)
cu2qu-1.6.7/MANIFEST.in 0000664 0000000 0000000 00000000530 13617262337 0014332 0 ustar 00root root 0000000 0000000 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
cu2qu-1.6.7/README.rst 0000664 0000000 0000000 00000007560 13617262337 0014275 0 ustar 00root root 0000000 0000000 |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
cu2qu-1.6.7/pyproject.toml 0000664 0000000 0000000 00000000211 13617262337 0015504 0 ustar 00root root 0000000 0000000 [build-system]
requires = [
"setuptools",
"wheel",
"setuptools_scm",
"cython",
]
build-backend = "setuptools.build_meta"
cu2qu-1.6.7/requirements.txt 0000664 0000000 0000000 00000000045 13617262337 0016061 0 ustar 00root root 0000000 0000000 fonttools[ufo]==3.32.0
defcon==0.6.0
cu2qu-1.6.7/setup.cfg 0000664 0000000 0000000 00000000532 13617262337 0014417 0 ustar 00root root 0000000 0000000 [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
cu2qu-1.6.7/setup.py 0000664 0000000 0000000 00000021031 13617262337 0014305 0 ustar 00root root 0000000 0000000 # 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},
)
cu2qu-1.6.7/test-requirements.txt 0000664 0000000 0000000 00000000020 13617262337 0017027 0 ustar 00root root 0000000 0000000 coverage
pytest
cu2qu-1.6.7/tests/ 0000775 0000000 0000000 00000000000 13617262337 0013740 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/__init__.py 0000664 0000000 0000000 00000001024 13617262337 0016046 0 ustar 00root root 0000000 0000000 import os
from fontTools.ufoLib.glifLib import GlyphSet
import pkg_resources
DATADIR = os.path.join(os.path.dirname(__file__), 'data')
CUBIC_GLYPHS = GlyphSet(os.path.join(DATADIR, 'cubic'))
QUAD_GLYPHS = GlyphSet(os.path.join(DATADIR, 'quadratic'))
import unittest
# Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires
# deprecation warnings if a program uses the old name.
if not hasattr(unittest.TestCase, 'assertRaisesRegex'):
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
cu2qu-1.6.7/tests/cli_test.py 0000664 0000000 0000000 00000005045 13617262337 0016124 0 ustar 00root root 0000000 0000000 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])
cu2qu-1.6.7/tests/cu2qu_test.py 0000664 0000000 0000000 00000013415 13617262337 0016414 0 ustar 00root root 0000000 0000000 # 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 collections
import math
import unittest
import os
import json
from cu2qu import curve_to_quadratic, curves_to_quadratic
from . import DATADIR
MAX_ERR = 5
class CurveToQuadraticTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Do the curve conversion ahead of time, and run tests on results."""
with open(os.path.join(DATADIR, "curves.json"), "r") as fp:
curves = json.load(fp)
cls.single_splines = [
curve_to_quadratic(c, MAX_ERR) for c in curves]
cls.single_errors = [
cls.curve_spline_dist(c, s)
for c, s in zip(curves, cls.single_splines)]
curve_groups = [curves[i:i + 3] for i in range(0, 300, 3)]
cls.compat_splines = [
curves_to_quadratic(c, [MAX_ERR] * 3) for c in curve_groups]
cls.compat_errors = [
[cls.curve_spline_dist(c, s) for c, s in zip(curve_group, splines)]
for curve_group, splines in zip(curve_groups, cls.compat_splines)]
cls.results = []
@classmethod
def tearDownClass(cls):
"""Print stats from conversion, as determined during tests."""
for tag, results in cls.results:
print('\n%s\n%s' % (
tag, '\n'.join(
'%s: %s (%d)' % (k, '#' * (v // 10 + 1), v)
for k, v in sorted(results.items()))))
def test_results_unchanged(self):
"""Tests that the results of conversion haven't changed since the time
of this test's writing. Useful as a quick check whenever one modifies
the conversion algorithm.
"""
expected = {
2: 6,
3: 26,
4: 82,
5: 232,
6: 360,
7: 266,
8: 28}
results = collections.defaultdict(int)
for spline in self.single_splines:
n = len(spline) - 2
results[n] += 1
self.assertEqual(results, expected)
self.results.append(('single spline lengths', results))
def test_results_unchanged_multiple(self):
"""Test that conversion results are unchanged for multiple curves."""
expected = {
5: 11,
6: 35,
7: 49,
8: 5}
results = collections.defaultdict(int)
for splines in self.compat_splines:
n = len(splines[0]) - 2
for spline in splines[1:]:
self.assertEqual(len(spline) - 2, n,
'Got incompatible conversion results')
results[n] += 1
self.assertEqual(results, expected)
self.results.append(('compatible spline lengths', results))
def test_does_not_exceed_tolerance(self):
"""Test that conversion results do not exceed given error tolerance."""
results = collections.defaultdict(int)
for error in self.single_errors:
results[round(error, 1)] += 1
self.assertLessEqual(error, MAX_ERR)
self.results.append(('single errors', results))
def test_does_not_exceed_tolerance_multiple(self):
"""Test that error tolerance isn't exceeded for multiple curves."""
results = collections.defaultdict(int)
for errors in self.compat_errors:
for error in errors:
results[round(error, 1)] += 1
self.assertLessEqual(error, MAX_ERR)
self.results.append(('compatible errors', results))
@classmethod
def curve_spline_dist(cls, bezier, spline, total_steps=20):
"""Max distance between a bezier and quadratic spline at sampled points."""
error = 0
n = len(spline) - 2
steps = total_steps // n
for i in range(0, n - 1):
p1 = spline[0] if i == 0 else p3
p2 = spline[i + 1]
if i < n - 1:
p3 = cls.lerp(spline[i + 1], spline[i + 2], 0.5)
else:
p3 = spline[n + 2]
segment = p1, p2, p3
for j in range(steps):
error = max(error, cls.dist(
cls.cubic_bezier_at(bezier, (j / steps + i) / n),
cls.quadratic_bezier_at(segment, j / steps)))
return error
@classmethod
def lerp(cls, p1, p2, t):
(x1, y1), (x2, y2) = p1, p2
return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t
@classmethod
def dist(cls, p1, p2):
(x1, y1), (x2, y2) = p1, p2
return math.hypot(x1 - x2, y1 - y2)
@classmethod
def quadratic_bezier_at(cls, b, t):
(x1, y1), (x2, y2), (x3, y3) = b
_t = 1 - t
t2 = t * t
_t2 = _t * _t
_2_t_t = 2 * t * _t
return (_t2 * x1 + _2_t_t * x2 + t2 * x3,
_t2 * y1 + _2_t_t * y2 + t2 * y3)
@classmethod
def cubic_bezier_at(cls, b, t):
(x1, y1), (x2, y2), (x3, y3), (x4, y4) = b
_t = 1 - t
t2 = t * t
_t2 = _t * _t
t3 = t * t2
_t3 = _t * _t2
_3_t2_t = 3 * t2 * _t
_3_t_t2 = 3 * t * _t2
return (_t3 * x1 + _3_t_t2 * x2 + _3_t2_t * x3 + t3 * x4,
_t3 * y1 + _3_t_t2 * y2 + _3_t2_t * y3 + t3 * y4)
if __name__ == '__main__':
unittest.main()
cu2qu-1.6.7/tests/data/ 0000775 0000000 0000000 00000000000 13617262337 0014651 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/ 0000775 0000000 0000000 00000000000 13617262337 0020751 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/fontinfo.plist 0000664 0000000 0000000 00000012061 13617262337 0023650 0 ustar 00root root 0000000 0000000
ascender
2146
capHeight
1456
copyright
Copyright 2011 Google Inc. All Rights Reserved.
descender
-555
familyName
RobotoSubset
italicAngle
0
openTypeHeadCreated
2008/09/12 12:29:34
openTypeHeadFlags
0
1
3
4
openTypeHeadLowestRecPPEM
9
openTypeHheaAscender
1900
openTypeHheaDescender
-500
openTypeHheaLineGap
0
openTypeNameDescription
openTypeNameDesigner
openTypeNameDesignerURL
openTypeNameLicense
openTypeNameLicenseURL
openTypeNameManufacturer
openTypeNameManufacturerURL
openTypeNameSampleText
openTypeOS2CodePageRanges
0
1
2
3
4
7
8
29
openTypeOS2FamilyClass
0
0
openTypeOS2Panose
0
0
0
0
0
0
0
0
0
0
openTypeOS2Selection
openTypeOS2StrikeoutPosition
512
openTypeOS2StrikeoutSize
102
openTypeOS2SubscriptXOffset
0
openTypeOS2SubscriptXSize
1434
openTypeOS2SubscriptYOffset
287
openTypeOS2SubscriptYSize
1331
openTypeOS2SuperscriptXOffset
0
openTypeOS2SuperscriptXSize
1434
openTypeOS2SuperscriptYOffset
977
openTypeOS2SuperscriptYSize
1331
openTypeOS2Type
openTypeOS2TypoAscender
2146
openTypeOS2TypoDescender
-555
openTypeOS2TypoLineGap
0
openTypeOS2UnicodeRanges
0
1
2
3
4
5
6
7
9
11
29
30
31
32
33
34
35
36
37
38
40
45
60
62
64
69
openTypeOS2VendorID
GOOG
openTypeOS2WeightClass
700
openTypeOS2WidthClass
5
openTypeOS2WinAscent
2146
openTypeOS2WinDescent
555
postscriptBlueFuzz
1
postscriptBlueScale
0.039625
postscriptBlueShift
7
postscriptBlueValues
-20
0
1082
1102
1456
1476
postscriptDefaultCharacter
space
postscriptForceBold
postscriptIsFixedPitch
postscriptOtherBlues
-436
-416
postscriptStemSnapH
250
postscriptStemSnapV
316
postscriptUnderlinePosition
-150
postscriptUnderlineThickness
100
postscriptUniqueID
-1
styleName
Bold
trademark
unitsPerEm
2048
versionMajor
1
versionMinor
0
xHeight
1082
year
2017
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/ 0000775 0000000 0000000 00000000000 13617262337 0022257 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/A_.glif 0000664 0000000 0000000 00000001416 13617262337 0023443 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/B_.glif 0000664 0000000 0000000 00000003212 13617262337 0023440 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/C_.glif 0000664 0000000 0000000 00000002256 13617262337 0023450 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/D_.glif 0000664 0000000 0000000 00000002072 13617262337 0023445 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/E_.glif 0000664 0000000 0000000 00000001573 13617262337 0023453 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/F_.glif 0000664 0000000 0000000 00000001304 13617262337 0023444 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/G_.glif 0000664 0000000 0000000 00000002550 13617262337 0023451 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/H_.glif 0000664 0000000 0000000 00000001276 13617262337 0023456 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/I_.glif 0000664 0000000 0000000 00000000511 13617262337 0023446 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/J_.glif 0000664 0000000 0000000 00000001355 13617262337 0023456 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/K_.glif 0000664 0000000 0000000 00000001414 13617262337 0023453 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/L_.glif 0000664 0000000 0000000 00000001001 13617262337 0023444 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/M_.glif 0000664 0000000 0000000 00000001612 13617262337 0023455 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/N_.glif 0000664 0000000 0000000 00000001072 13617262337 0023456 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/O_.glif 0000664 0000000 0000000 00000002307 13617262337 0023461 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/P_.glif 0000664 0000000 0000000 00000001571 13617262337 0023464 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/Q_.glif 0000664 0000000 0000000 00000002602 13617262337 0023461 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/R_.glif 0000664 0000000 0000000 00000002315 13617262337 0023463 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/S_.glif 0000664 0000000 0000000 00000002747 13617262337 0023475 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/T_.glif 0000664 0000000 0000000 00000001007 13617262337 0023462 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/U_.glif 0000664 0000000 0000000 00000001534 13617262337 0023470 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/V_.glif 0000664 0000000 0000000 00000001120 13617262337 0023460 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/W_.glif 0000664 0000000 0000000 00000002151 13617262337 0023466 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/X_.glif 0000664 0000000 0000000 00000001210 13617262337 0023462 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/Y_.glif 0000664 0000000 0000000 00000001024 13617262337 0023466 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/Z_.glif 0000664 0000000 0000000 00000001417 13617262337 0023475 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/a.glif 0000664 0000000 0000000 00000003432 13617262337 0023344 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/b.glif 0000664 0000000 0000000 00000002673 13617262337 0023353 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/c.glif 0000664 0000000 0000000 00000002245 13617262337 0023347 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000004535 13617262337 0025200 0 ustar 00root root 0000000 0000000
A
A_.glif
B
B_.glif
C
C_.glif
D
D_.glif
E
E_.glif
F
F_.glif
G
G_.glif
H
H_.glif
I
I_.glif
J
J_.glif
K
K_.glif
L
L_.glif
M
M_.glif
N
N_.glif
O
O_.glif
P
P_.glif
Q
Q_.glif
R
R_.glif
S
S_.glif
T
T_.glif
U
U_.glif
V
V_.glif
W
W_.glif
X
X_.glif
Y
Y_.glif
Z
Z_.glif
a
a.glif
b
b.glif
c
c.glif
d
d.glif
e
e.glif
f
f.glif
g
g.glif
h
h.glif
i
i.glif
j
j.glif
k
k.glif
l
l.glif
m
m.glif
n
n.glif
o
o.glif
p
p.glif
q
q.glif
r
r.glif
s
s.glif
space
space.glif
t
t.glif
u
u.glif
v
v.glif
w
w.glif
x
x.glif
y
y.glif
z
z.glif
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/d.glif 0000664 0000000 0000000 00000002665 13617262337 0023356 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/e.glif 0000664 0000000 0000000 00000002620 13617262337 0023346 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/f.glif 0000664 0000000 0000000 00000001670 13617262337 0023353 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/g.glif 0000664 0000000 0000000 00000003572 13617262337 0023357 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/h.glif 0000664 0000000 0000000 00000001673 13617262337 0023360 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/i.glif 0000664 0000000 0000000 00000001443 13617262337 0023354 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/j.glif 0000664 0000000 0000000 00000002321 13617262337 0023351 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/k.glif 0000664 0000000 0000000 00000001414 13617262337 0023354 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/l.glif 0000664 0000000 0000000 00000000511 13617262337 0023352 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/m.glif 0000664 0000000 0000000 00000003111 13617262337 0023352 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/n.glif 0000664 0000000 0000000 00000001711 13617262337 0023357 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/o.glif 0000664 0000000 0000000 00000002244 13617262337 0023362 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/p.glif 0000664 0000000 0000000 00000002701 13617262337 0023361 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/q.glif 0000664 0000000 0000000 00000002673 13617262337 0023372 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/r.glif 0000664 0000000 0000000 00000001543 13617262337 0023366 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/s.glif 0000664 0000000 0000000 00000002741 13617262337 0023370 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/space.glif 0000664 0000000 0000000 00000001345 13617262337 0024220 0 ustar 00root root 0000000 0000000
com.typemytype.robofont.guides
angle
0
isGlobal
magnetic
5
x
0
y
901
angle
0
isGlobal
magnetic
5
x
0
y
555
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/t.glif 0000664 0000000 0000000 00000001641 13617262337 0023367 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/u.glif 0000664 0000000 0000000 00000001711 13617262337 0023366 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/v.glif 0000664 0000000 0000000 00000001120 13617262337 0023361 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/w.glif 0000664 0000000 0000000 00000002145 13617262337 0023372 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/x.glif 0000664 0000000 0000000 00000001210 13617262337 0023363 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/y.glif 0000664 0000000 0000000 00000002021 13617262337 0023365 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/glyphs/z.glif 0000664 0000000 0000000 00000001406 13617262337 0023374 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/layercontents.plist 0000664 0000000 0000000 00000000423 13617262337 0024717 0 ustar 00root root 0000000 0000000
public.default
glyphs
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/lib.plist 0000664 0000000 0000000 00000002604 13617262337 0022576 0 ustar 00root root 0000000 0000000
public.glyphOrder
space
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
cu2qu-1.6.7/tests/data/RobotoSubset-Bold.ufo/metainfo.plist 0000664 0000000 0000000 00000000453 13617262337 0023632 0 ustar 00root root 0000000 0000000
creator
org.robofab.ufoLib
formatVersion
3
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/ 0000775 0000000 0000000 00000000000 13617262337 0021472 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/fontinfo.plist 0000664 0000000 0000000 00000012626 13617262337 0024400 0 ustar 00root root 0000000 0000000
ascender
2146
capHeight
1456
copyright
Copyright 2011 Google Inc. All Rights Reserved.
descender
-555
familyName
RobotoSubset
guidelines
italicAngle
0
openTypeHeadCreated
2008/09/12 12:29:34
openTypeHeadFlags
0
1
3
4
openTypeHeadLowestRecPPEM
9
openTypeHheaAscender
1900
openTypeHheaDescender
-500
openTypeHheaLineGap
0
openTypeNameDescription
openTypeNameDesigner
openTypeNameDesignerURL
openTypeNameLicense
openTypeNameLicenseURL
openTypeNameManufacturer
openTypeNameManufacturerURL
openTypeNameSampleText
openTypeOS2CodePageRanges
0
1
2
3
4
7
8
29
openTypeOS2FamilyClass
0
0
openTypeOS2Panose
2
0
0
0
0
0
0
0
0
0
openTypeOS2Selection
openTypeOS2StrikeoutPosition
512
openTypeOS2StrikeoutSize
102
openTypeOS2SubscriptXOffset
0
openTypeOS2SubscriptXSize
1434
openTypeOS2SubscriptYOffset
287
openTypeOS2SubscriptYSize
1331
openTypeOS2SuperscriptXOffset
0
openTypeOS2SuperscriptXSize
1434
openTypeOS2SuperscriptYOffset
977
openTypeOS2SuperscriptYSize
1331
openTypeOS2Type
openTypeOS2TypoAscender
2146
openTypeOS2TypoDescender
-555
openTypeOS2TypoLineGap
0
openTypeOS2UnicodeRanges
0
1
2
3
4
5
6
7
9
11
29
30
31
32
33
34
35
36
37
38
40
45
60
62
64
69
openTypeOS2VendorID
GOOG
openTypeOS2WeightClass
400
openTypeOS2WidthClass
5
openTypeOS2WinAscent
2146
openTypeOS2WinDescent
555
postscriptBlueFuzz
1
postscriptBlueScale
0.039625
postscriptBlueShift
7
postscriptBlueValues
-20
0
1082
1102
1456
1476
postscriptDefaultCharacter
space
postscriptFamilyBlues
postscriptFamilyOtherBlues
postscriptForceBold
postscriptIsFixedPitch
postscriptOtherBlues
-436
-416
postscriptStemSnapH
154
postscriptStemSnapV
196
postscriptUnderlinePosition
-150
postscriptUnderlineThickness
100
postscriptUniqueID
-1
styleName
Regular
trademark
unitsPerEm
2048
versionMajor
1
versionMinor
0
xHeight
1082
year
2017
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/ 0000775 0000000 0000000 00000000000 13617262337 0023000 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/A_.glif 0000664 0000000 0000000 00000001417 13617262337 0024165 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/B_.glif 0000664 0000000 0000000 00000003210 13617262337 0024157 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/C_.glif 0000664 0000000 0000000 00000002266 13617262337 0024172 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/D_.glif 0000664 0000000 0000000 00000002076 13617262337 0024172 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/E_.glif 0000664 0000000 0000000 00000001571 13617262337 0024172 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/F_.glif 0000664 0000000 0000000 00000001302 13617262337 0024163 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/G_.glif 0000664 0000000 0000000 00000002564 13617262337 0024177 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/H_.glif 0000664 0000000 0000000 00000001300 13617262337 0024163 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/I_.glif 0000664 0000000 0000000 00000000511 13617262337 0024167 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/J_.glif 0000664 0000000 0000000 00000001352 13617262337 0024174 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/K_.glif 0000664 0000000 0000000 00000001415 13617262337 0024175 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/L_.glif 0000664 0000000 0000000 00000001001 13617262337 0024165 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/M_.glif 0000664 0000000 0000000 00000001611 13617262337 0024175 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/N_.glif 0000664 0000000 0000000 00000001076 13617262337 0024203 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/O_.glif 0000664 0000000 0000000 00000002317 13617262337 0024203 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/P_.glif 0000664 0000000 0000000 00000001576 13617262337 0024212 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/Q_.glif 0000664 0000000 0000000 00000002613 13617262337 0024204 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/R_.glif 0000664 0000000 0000000 00000002302 13617262337 0024200 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/S_.glif 0000664 0000000 0000000 00000002732 13617262337 0024210 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/T_.glif 0000664 0000000 0000000 00000001007 13617262337 0024203 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/U_.glif 0000664 0000000 0000000 00000001533 13617262337 0024210 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/V_.glif 0000664 0000000 0000000 00000001122 13617262337 0024203 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/W_.glif 0000664 0000000 0000000 00000002151 13617262337 0024207 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/X_.glif 0000664 0000000 0000000 00000001210 13617262337 0024203 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/Y_.glif 0000664 0000000 0000000 00000001024 13617262337 0024207 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/Z_.glif 0000664 0000000 0000000 00000001417 13617262337 0024216 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/a.glif 0000664 0000000 0000000 00000003422 13617262337 0024064 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/b.glif 0000664 0000000 0000000 00000002641 13617262337 0024067 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/c.glif 0000664 0000000 0000000 00000002245 13617262337 0024070 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000004535 13617262337 0025721 0 ustar 00root root 0000000 0000000
A
A_.glif
B
B_.glif
C
C_.glif
D
D_.glif
E
E_.glif
F
F_.glif
G
G_.glif
H
H_.glif
I
I_.glif
J
J_.glif
K
K_.glif
L
L_.glif
M
M_.glif
N
N_.glif
O
O_.glif
P
P_.glif
Q
Q_.glif
R
R_.glif
S
S_.glif
T
T_.glif
U
U_.glif
V
V_.glif
W
W_.glif
X
X_.glif
Y
Y_.glif
Z
Z_.glif
a
a.glif
b
b.glif
c
c.glif
d
d.glif
e
e.glif
f
f.glif
g
g.glif
h
h.glif
i
i.glif
j
j.glif
k
k.glif
l
l.glif
m
m.glif
n
n.glif
o
o.glif
p
p.glif
q
q.glif
r
r.glif
s
s.glif
space
space.glif
t
t.glif
u
u.glif
v
v.glif
w
w.glif
x
x.glif
y
y.glif
z
z.glif
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/d.glif 0000664 0000000 0000000 00000002633 13617262337 0024072 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/e.glif 0000664 0000000 0000000 00000002521 13617262337 0024067 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/f.glif 0000664 0000000 0000000 00000001654 13617262337 0024076 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/g.glif 0000664 0000000 0000000 00000003542 13617262337 0024075 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/h.glif 0000664 0000000 0000000 00000001672 13617262337 0024100 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/i.glif 0000664 0000000 0000000 00000001377 13617262337 0024103 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/j.glif 0000664 0000000 0000000 00000002321 13617262337 0024072 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/k.glif 0000664 0000000 0000000 00000001413 13617262337 0024074 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/l.glif 0000664 0000000 0000000 00000000511 13617262337 0024073 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/m.glif 0000664 0000000 0000000 00000003104 13617262337 0024075 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/n.glif 0000664 0000000 0000000 00000001742 13617262337 0024104 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/o.glif 0000664 0000000 0000000 00000002244 13617262337 0024103 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/p.glif 0000664 0000000 0000000 00000002647 13617262337 0024113 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/q.glif 0000664 0000000 0000000 00000002641 13617262337 0024106 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/r.glif 0000664 0000000 0000000 00000001543 13617262337 0024107 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/s.glif 0000664 0000000 0000000 00000002744 13617262337 0024114 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/space.glif 0000664 0000000 0000000 00000000177 13617262337 0024743 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/t.glif 0000664 0000000 0000000 00000001636 13617262337 0024114 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/u.glif 0000664 0000000 0000000 00000001712 13617262337 0024110 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/v.glif 0000664 0000000 0000000 00000001117 13617262337 0024110 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/w.glif 0000664 0000000 0000000 00000002146 13617262337 0024114 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/x.glif 0000664 0000000 0000000 00000001206 13617262337 0024111 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/y.glif 0000664 0000000 0000000 00000002030 13617262337 0024106 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/glyphs/z.glif 0000664 0000000 0000000 00000001406 13617262337 0024115 0 ustar 00root root 0000000 0000000
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/layercontents.plist 0000664 0000000 0000000 00000000423 13617262337 0025440 0 ustar 00root root 0000000 0000000
public.default
glyphs
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/lib.plist 0000664 0000000 0000000 00000002604 13617262337 0023317 0 ustar 00root root 0000000 0000000
public.glyphOrder
space
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
cu2qu-1.6.7/tests/data/RobotoSubset-Regular.ufo/metainfo.plist 0000664 0000000 0000000 00000000453 13617262337 0024353 0 ustar 00root root 0000000 0000000
creator
org.robofab.ufoLib
formatVersion
3
cu2qu-1.6.7/tests/data/cubic/ 0000775 0000000 0000000 00000000000 13617262337 0015736 5 ustar 00root root 0000000 0000000 cu2qu-1.6.7/tests/data/cubic/A_.glif 0000775 0000000 0000000 00000003441 13617262337 0017125 0 ustar 00root root 0000000 0000000