diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b10c9e5..cfbfc63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.x" @@ -41,7 +38,7 @@ jobs: matrix: os: [ "ubuntu-latest", "macos-13", "windows-latest" ] env: - CIBW_ENVIRONMENT: TTFAUTOHINTPY_BUNDLE_DLL=1 + CIBW_ENVIRONMENT: TTFAUTOHINTPY_BUNDLE_EXE=1 CIBW_BEFORE_ALL_LINUX: sh ci/docker-fixes.sh CIBW_BEFORE_ALL_MACOS: sh ci/osx-fixes.sh CIBW_ARCHS_LINUX: x86_64 @@ -58,9 +55,6 @@ jobs: CIBW_TEST_COMMAND_WINDOWS: cd /d {project} && coverage run --parallel-mode -m pytest steps: - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - name: "Set up MSYS2 (Windows)" uses: msys2/setup-msys2@v2 if: startsWith(matrix.os, 'windows') @@ -115,7 +109,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !startsWith(matrix.os, 'ubuntu') }} - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: file: coverage.xml diff --git a/.gitignore b/.gitignore index c588e31..1b537d1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,14 @@ htmlcov/ # OSX Finder .DS_Store + +# autogenerated vesion file +src/python/ttfautohint/_version.py + +# executable built in-place +src/python/ttfautohint/ttfautohint + +# uncompressed source tarballs +src/c/ttfautohint/ +src/c/freetype/ +src/c/harfbuzz/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..db51f3b --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +![CI Status](https://github.com/fonttools/ttfautohint-py/actions/workflows/ci.yml/badge.svg?branch=main) +# ttfautohint-py + +`ttfautohint-py` is a Python wrapper for [ttfautohint](https://freetype.org/ttfautohint/), a free auto-hinter for TrueType fonts created by Werner Lemberg ([@lemzwerg](https://github.com/lemzwerg)). + +As of v0.6, it runs the `ttfautohint` executable as a subprocess. Previous versions used [ctypes](https://docs.python.org/3/library/ctypes.html) to load the `libttfautohint` shared library, but that was hard to maintain and complicated to keep up to date with upstream `ttfautohint` hence we decided to switch to a simpler `subprocess` approach (cf. #15). + +Binary "wheel" packages are available for Linux (`manylinux2014`), macOS and Windows, for Python 3.8+, with 32 and 64 bit architecture. They can be installed from the Python Package Index ([PyPI](https://pypi.python.org/pypi/ttfautohint-py)) using the pip installer. + + $ pip install ttfautohint-py + +The wheels include a precompiled `ttfautohint` executable which has no other dependency apart from system libraries. The [FreeType](https://www.freetype.org/) and the [HarfBuzz](https://github.com/harfbuzz/harfbuzz) libraries are compiled from source as static libraries and embedded in `ttfautohint`. + +To compile the `ttfautohint-py` package from source on Windows, you need to install [MSYS2](http://www.msys2.org/) and the latest MinGW-w64 toolchain. This is because the `ttfautohint` build system is based on autotools and thus requires a Unix-like environment. + +A `Makefile` is used to build the library and its static dependencies, thus the GNU [make](https://www.gnu.org/software/make/) executable must be on the `$PATH`, as this is called upon by the `setup.py` script. \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index b06b513..0000000 --- a/README.rst +++ /dev/null @@ -1,49 +0,0 @@ -|GitHub CI Status| |PyPI| |Codecov| - -ttfautohint-py -~~~~~~~~~~~~~~ - -``ttfautohint-py`` is a Python wrapper for `ttfautohint -`__, a free auto-hinter for TrueType fonts -created by Werner Lemberg (`@lemzwerg `__). - -It uses `ctypes `__ to load the -``libttfautohint`` shared library and call the ``TTF_autohint`` function. - -Binary "wheel" packages are available for Linux (``manylinux1``), macOS and -Windows, for both Python 2.7 and Python 3.x, with 32 and 64 bit architecture. -They can be installed from the Python Package Index -(`PyPI `__) using the -`pip `__ installer. - -.. code:: sh - - $ pip install ttfautohint-py - -The wheels include a precompiled ``libttfautohint.so`` (``*.dylib`` on -macOS, or ``*.dll`` on Windows) shared library which has no other dependency -apart from system libraries. The `FreeType `__ and -the `HarfBuzz `__ libraries are compiled -from source as static libraries and embedded in ``libttfautohint``. - -To compile the ``libttfautohint.dll`` from source on Windows, you need to -install `MSYS2 `__ and the latest MinGW-w64 toolchain. -This is because the ``ttfautohint`` build system is based on autotools and -thus requires a Unix-like environment. - -A ``Makefile`` is used to build the library and its static dependencies, thus -the GNU `make `__ executable must be on the -``$PATH``, as this is called upon by the ``setup.py`` script. - -Because we build ``freetype``, ``harfbuzz`` and ``ttfautohint`` from their git -source (checked in as git submodules), some relatively recent versions of the -following development tools are also required: ``autoconf``, ``automake``, -``libtool``, ``flex``, ``bison`` and ``ragel``. Please check the respective -documentation of these libraries for more information. - -.. |Github CI Status| image:: https://img.shields.io/github/workflow/status/fonttools/ttfautohint-py/ci.yml?branch=main - :target: https://github.com/fonttools/ttfautohint-py/actions/workflows/ci.yml?branch=main -.. |PyPI| image:: https://img.shields.io/pypi/v/ttfautohint-py.svg - :target: https://pypi.python.org/pypi/ttfautohint-py -.. |Codecov| image:: https://codecov.io/gh/fonttools/ttfautohint-py/branch/master/graph/badge.svg - :target: https://codecov.io/gh/fonttools/ttfautohint-py diff --git a/setup.cfg b/setup.cfg index cd574b2..6f7dd63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,11 @@ +[download] +freetype_version = 2.13.3 +freetype_sha256 = 0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289 +harfbuzz_version = 8.5.0 +harfbuzz_sha256 = 77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27 +ttfautohint_version = 1.8.4 +ttfautohint_sha256 = 8a876117fa6ebfd2ffe1b3682a9a98c802c0f47189f57d3db4b99774206832e1 + [bdist_wheel] universal = 1 diff --git a/setup.py b/setup.py index 0babf08..5a30af4 100644 --- a/setup.py +++ b/setup.py @@ -1,124 +1,288 @@ -from __future__ import print_function, absolute_import -from setuptools import setup, find_packages, Extension +from setuptools import setup, find_packages, Extension, Command from setuptools.command.build_ext import build_ext +from setuptools.command.bdist_wheel import bdist_wheel +from setuptools.command.egg_info import egg_info +from distutils.command.clean import clean +from distutils.errors import DistutilsSetupError from distutils.file_util import copy_file -from distutils.dir_util import mkpath +from distutils.dir_util import mkpath, remove_tree from distutils import log import os -import sys import subprocess -from io import open -cmdclass = {} -ext_modules = [] -if os.environ.get("TTFAUTOHINTPY_BUNDLE_DLL", "0") in {"1", "yes", "true"}: - try: - from wheel.bdist_wheel import bdist_wheel - except ImportError: - print("warning: wheel package is not installed", file=sys.stderr) - else: - class UniversalBdistWheel(bdist_wheel): +class UniversalBdistWheel(bdist_wheel): + def get_tag(self): + return ("py3", "none") + bdist_wheel.get_tag(self)[2:] - def get_tag(self): - return ('py2.py3', 'none',) + bdist_wheel.get_tag(self)[2:] - cmdclass['bdist_wheel'] = UniversalBdistWheel +class Download(Command): + user_options = [ + ("ttfautohint-version=", None, "ttfautohint source version number to download"), + ( + "ttfautohint-sha256=", + None, + "expected SHA-256 hash of the ttfautohint source archive", + ), + ("freetype-version=", None, "freetype version to download"), + ("freetype-sha256=", None, "expected SHA-256 hash of the freetype archive"), + ("harfbuzz-version=", None, "harfbuzz version to download"), + ("harfbuzz-sha256=", None, "expected SHA-256 hash of the harfbuzz archive"), + ( + "download-dir=", + "d", + "where to unpack the 'ttfautohint' dir (default: src/c)", + ), + ("clean", None, "remove existing directory before downloading"), + ] + boolean_options = ["clean"] + TTFAUTOHINT_URL_TEMPLATE = ( + "https://download.savannah.gnu.org/releases/freetype/" + "ttfautohint-{ttfautohint_version}.tar.gz" + ) + FREETYPE_URL_TEMPLATE = ( + "https://download.savannah.gnu.org/releases/freetype/" + "freetype-{freetype_version}.tar.xz" + ) + HARFBUZZ_URL_TEMPLATE = ( + "https://github.com/harfbuzz/harfbuzz/releases/download/{harfbuzz_version}/" + "harfbuzz-{harfbuzz_version}.tar.xz" + ) - class SharedLibrary(Extension): + def initialize_options(self): + self.ttfautohint_version = None + self.ttfautohint_sha256 = None + self.freetype_version = None + self.freetype_sha256 = None + self.harfbuzz_version = None + self.harfbuzz_sha256 = None + self.download_dir = None + self.clean = False - if sys.platform == "darwin": - suffix = ".dylib" - elif sys.platform == "win32": - suffix = ".dll" - else: - suffix = ".so" + def finalize_options(self): + if self.ttfautohint_version is None: + raise DistutilsSetupError("must specify --ttfautohint-version to download") + if self.freetype_version is None: + raise DistutilsSetupError("must specify --freetype-version to download") + if self.harfbuzz_version is None: + raise DistutilsSetupError("must specify --harfbuzz-version to download") - def __init__(self, name, cmd, cwd=".", output_dir=".", env=None): - Extension.__init__(self, name, sources=[]) - self.cmd = cmd - self.cwd = os.path.normpath(cwd) - self.output_dir = os.path.normpath(output_dir) - self.env = env or dict(os.environ) + if self.ttfautohint_sha256 is None: + raise DistutilsSetupError( + "must specify --ttfautohint-sha256 of downloaded file" + ) + if self.freetype_sha256 is None: + raise DistutilsSetupError( + "must specify --freetype-sha256 of downloaded file" + ) + if self.harfbuzz_sha256 is None: + raise DistutilsSetupError( + "must specify --harfbuzz-sha256 of downloaded file" + ) + if self.download_dir is None: + self.download_dir = os.path.join("src", "c") - class SharedLibBuildExt(build_ext): + self.to_download = { + "ttfautohint": self.TTFAUTOHINT_URL_TEMPLATE.format(**vars(self)), + "freetype": self.FREETYPE_URL_TEMPLATE.format(**vars(self)), + "harfbuzz": self.HARFBUZZ_URL_TEMPLATE.format(**vars(self)), + } - def get_ext_filename(self, ext_name): - for ext in self.extensions: - if isinstance(ext, SharedLibrary): - return os.path.join(*ext_name.split('.')) + ext.suffix - return build_ext.get_ext_filename(self, ext_name) + def run(self): + from urllib.request import urlopen + from io import BytesIO + import tarfile + import gzip + import lzma + import hashlib - def build_extension(self, ext): - if not isinstance(ext, SharedLibrary): - build_ext.build_extension(self, ext) - return + for download_name, url in self.to_download.items(): + output_dir = os.path.join(self.download_dir, download_name) + if self.clean and os.path.isdir(output_dir): + remove_tree(output_dir, verbose=self.verbose, dry_run=self.dry_run) - log.info("running '%s'" % " ".join(ext.cmd)) - if not self.dry_run: - rv = subprocess.Popen(ext.cmd, - cwd=ext.cwd, - env=ext.env, - shell=True).wait() - if rv != 0: - sys.exit(rv) + if os.path.isdir(output_dir): + log.info("{} was already downloaded".format(output_dir)) + else: + archive_name = url.rsplit("/", 1)[-1] - lib_name = ext.name.split(".")[-1] + ext.suffix - lib_fullpath = os.path.join(ext.output_dir, lib_name) + mkpath(self.download_dir, verbose=self.verbose, dry_run=self.dry_run) - dest_path = self.get_ext_fullpath(ext.name) - mkpath(os.path.dirname(dest_path), - verbose=self.verbose, dry_run=self.dry_run) + log.info("downloading {}".format(url)) + if not self.dry_run: + # response is not seekable so we first download *.tar.gz to an + # in-memory file, and then extract all files to the output_dir + f = BytesIO() + with urlopen(url) as response: + f.write(response.read()) + f.seek(0) - copy_file(lib_fullpath, dest_path, - verbose=self.verbose, dry_run=self.dry_run) + actual_sha256 = hashlib.sha256(f.getvalue()).hexdigest() + expected_sha256 = getattr(self, download_name + "_sha256") + if actual_sha256 != expected_sha256: + from distutils.errors import DistutilsSetupError + raise DistutilsSetupError( + "invalid SHA-256 checksum:\n" + "actual: {}\n" + "expected: {}".format(actual_sha256, expected_sha256) + ) - cmdclass['build_ext'] = SharedLibBuildExt + log.info("unarchiving {} to {}".format(archive_name, output_dir)) + if not self.dry_run: + ext = os.path.splitext(archive_name)[-1] + if ext == ".xz": + compression_module = lzma + elif ext == ".gz": + compression_module = gzip + else: + raise NotImplementedError( + f"Don't know how to decompress archive with {ext} extension" + ) + with compression_module.open(f) as archive: + with tarfile.open(fileobj=archive) as tar: + filelist = tar.getmembers() + first = filelist[0] + if not ( + first.isdir() and first.name.startswith(download_name) + ): + from distutils.errors import DistutilsSetupError - env = dict(os.environ) - if sys.platform == "win32": - import struct + raise DistutilsSetupError( + "The downloaded archive is not recognized as " + "a valid ttfautohint source tarball" + ) + # strip the root directory before extracting + rootdir = first.name + "/" + to_extract = [] + for member in filelist[1:]: + if member.name.startswith(rootdir): + member.name = member.name[len(rootdir) :] + to_extract.append(member) + tar.extractall(output_dir, members=to_extract) - msys2_root = os.path.abspath(env.get("MSYS2ROOT", "C:\\msys64")) - msys2_bin = os.path.join(msys2_root, "usr", "bin") - # select mingw32 or mingw64 toolchain depending on python architecture - bits = struct.calcsize("P") * 8 - toolchain = "mingw%d" % bits - mingw_bin = os.path.join(msys2_root, toolchain, "bin") - PATH = os.pathsep.join([mingw_bin, msys2_bin, env["PATH"]]) - env.update( - PATH=PATH, - MSYSTEM=toolchain.upper(), - # this tells bash to keep the current working directory - CHERE_INVOKING="1", - ) - # we need to run make from an msys2 login shell. - # We do 'make clean' because libraries are built in-place and we want - # to make sure previous builds don't leave anything behind. - cmd = ["bash", "-lc", "make clean all"] + +class Executable(Extension): + if os.name == "nt": + suffix = ".exe" else: - cmd = ["make", "clean", "all"] + suffix = "" + + def __init__(self, name, output_dir=".", cwd=None, env=None): + Extension.__init__(self, name, sources=[]) + self.target = self.name.split(".")[-1] + self.suffix + self.output_dir = output_dir + self.cwd = cwd + self.env = env + + +class ExecutableBuildExt(build_ext): + def finalize_options(self): + from distutils.ccompiler import get_default_compiler + + build_ext.finalize_options(self) + + if self.compiler is None: + self.compiler = get_default_compiler(os.name) + self._compiler_env = dict(os.environ) + + def get_ext_filename(self, ext_name): + for ext in self.extensions: + if isinstance(ext, Executable): + return os.path.join(*ext_name.split(".")) + ext.suffix + return build_ext.get_ext_filename(self, ext_name) + + def run(self): + self.run_command("download") + + if self.compiler == "msvc": + self.call_vcvarsall_bat() + + build_ext.run(self) + + def call_vcvarsall_bat(self): + import struct + from distutils._msvccompiler import _get_vc_env - libttfautohint = SharedLibrary("ttfautohint.libttfautohint", - cmd=cmd, - cwd="src/c", - env=env, - output_dir="build/local/lib") - ext_modules.append(libttfautohint) + arch = "x64" if struct.calcsize("P") * 8 == 64 else "x86" + vc_env = _get_vc_env(arch) + self._compiler_env.update(vc_env) + def build_extension(self, ext): + if not isinstance(ext, Executable): + build_ext.build_extension(self, ext) + return + + cmd = ["make"] + [ext.target] + log.debug("running '{}'".format(" ".join(cmd))) + if not self.dry_run: + env = self._compiler_env.copy() + if ext.env: + env.update(ext.env) + if self.force: + subprocess.call(["make", "clean"], cwd=ext.cwd, env=env) + p = subprocess.run(cmd, cwd=ext.cwd, env=env) + if p.returncode != 0: + from distutils.errors import DistutilsExecError + + raise DistutilsExecError("running 'make' failed") + + exe_fullpath = os.path.join(ext.output_dir, ext.target) + + dest_path = self.get_ext_fullpath(ext.name) + mkpath(os.path.dirname(dest_path), verbose=self.verbose, dry_run=self.dry_run) + + copy_file(exe_fullpath, dest_path, verbose=self.verbose, dry_run=self.dry_run) + + +class CustomEggInfo(egg_info): + def run(self): + # make sure the ttfautohint source is downloaded before creating sdist manifest + self.run_command("download") + egg_info.run(self) + + +class CustomClean(clean): + def run(self): + clean.run(self) + # also remove downloaded sources and all build byproducts + for path in ["src/c/ttfautohint", "src/c/freetype", "src/c/harfbuzz"]: + if os.path.isdir(path): + remove_tree(path, self.verbose, self.dry_run) + else: + log.info("'{}' does not exist -- can't clean it".format(path)) + if not self.dry_run: + subprocess.call(["make", "clean"], cwd="src/c") + + +ttfautohint_exe = Executable( + "ttfautohint.ttfautohint", + cwd="src/c", + output_dir=os.path.join("build/local/bin"), +) + +cmdclass = {} +ext_modules = [] +for env_var in ("TTFAUTOHINTPY_BUNDLE_DLL", "TTFAUTOHINTPY_BUNDLE_EXE"): + if os.environ.get(env_var, "0") in {"1", "yes", "true"}: + cmdclass["bdist_wheel"] = UniversalBdistWheel + cmdclass["download"] = Download + cmdclass["build_ext"] = ExecutableBuildExt + cmdclass["egg_info"] = CustomEggInfo + cmdclass["clean"] = CustomClean + ext_modules = [ttfautohint_exe] -with open("README.rst", "r", encoding="utf-8") as readme: +with open("README.md", "r", encoding="utf-8") as readme: long_description = readme.read() setup( - name="ttfautohint-py", - use_scm_version=True, - description=("Python wrapper for ttfautohint, " - "a free auto-hinter for TrueType fonts"), + name="ttfautohint", + use_scm_version={"write_to": "src/python/ttfautohint/_version.py"}, + description=("Python wrapper for ttfautohint"), long_description=long_description, + long_description_content_type="text/markdown", author="Cosimo Lupo", author_email="cosimo@anthrotype.com", url="https://github.com/fonttools/ttfautohint-py", @@ -127,9 +291,11 @@ def build_extension(self, ext): package_dir={"": "src/python"}, packages=find_packages("src/python"), ext_modules=ext_modules, - zip_safe=False, + zip_safe=any(ext_modules), cmdclass=cmdclass, - setup_requires=['setuptools_scm'], + setup_requires=["setuptools_scm"], + extras_require={"testing": ["pytest", "coverage", "fontTools"]}, + python_requires=">=3.8", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -140,7 +306,6 @@ def build_extension(self, ext): "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Text Processing :: Fonts", "Topic :: Multimedia :: Graphics", diff --git a/src/c/Makefile b/src/c/Makefile index b3c882e..df73bad 100644 --- a/src/c/Makefile +++ b/src/c/Makefile @@ -1,5 +1,3 @@ -LIB_NAME := "libttfautohint" - SRC := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ROOT := $(shell dirname $(shell dirname $(SRC))) BUILD := $(ROOT)/build @@ -11,32 +9,17 @@ CFLAGS := -g -O2 -fPIC CXXFLAGS := -g -O2 -fPIC LDFLAGS := -fPIC -L$(PREFIX)/lib -L$(PREFIX)/lib64 -# on Windows, libtool cannot be used to build the ttfautohint.dll, we run -# dllwrap ourselves on the static libraries, so we --disable-shared -# https://lists.gnu.org/archive/html/freetype-devel/2017-12/msg00013.html -# http://lists.gnu.org/archive/html/libtool/2017-12/msg00003.html -LIBTTFAUTOHINT_OPTIONS := --enable-static -ifeq ($(OS), Windows_NT) - LIBTTFAUTOHINT_OPTIONS += --disable-shared - LIBTTFAUTOHINT := "$(LIB_NAME).dll" -else - LIBTTFAUTOHINT_OPTIONS += --enable-shared - ifeq ($(shell uname -s), Darwin) - LIBTTFAUTOHINT := "$(LIB_NAME).dylib" - # on macOS, we want a 64-bit only lib targeting >= 10.9, since harfbuzz >= 2.4 - # requires c++11 - MACOSX_DEPLOYMENT_TARGET ?= 10.9 - CFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) - CXXFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) - LDFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) - else ifeq ($(shell uname -s), Linux) - LIBTTFAUTOHINT := "$(LIB_NAME).so" - endif +LIBTTFAUTOHINT_OPTIONS := --enable-static --disable-shared +ifeq ($(shell uname -s), Darwin) + # on macOS, we want a 64-bit only lib targeting >= 10.9, since harfbuzz >= 2.4 + # requires c++11 + MACOSX_DEPLOYMENT_TARGET ?= 10.9 + CFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) + CXXFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) + LDFLAGS += -m64 -arch x86_64 -arch arm64 -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) endif -all: libttfautohint - -libttfautohint: $(PREFIX)/lib/$(LIBTTFAUTOHINT) +all: ttfautohint freetype: $(TMP)/.freetype-stamp @@ -46,8 +29,7 @@ ttfautohint: $(TMP)/.ttfautohint-stamp $(TMP)/.freetype-stamp: @mkdir -p $(TMP) - cd $(SRC)/freetype2; ./autogen.sh - cd $(SRC)/freetype2; ./configure \ + cd $(SRC)/freetype; ./configure \ --without-bzip2 \ --without-png \ --without-zlib \ @@ -61,13 +43,12 @@ $(TMP)/.freetype-stamp: CFLAGS="$(CPPFLAGS) $(CFLAGS)" \ CXXFLAGS="$(CPPFLAGS) $(CXXFLAGS)" \ LDFLAGS="$(LDFLAGS)" - cd $(SRC)/freetype2; make - cd $(SRC)/freetype2; make install + cd $(SRC)/freetype; make + cd $(SRC)/freetype; make install @touch $(TMP)/.freetype-stamp $(TMP)/.harfbuzz-stamp: $(TMP)/.freetype-stamp @mkdir -p $(TMP) - cd $(SRC)/harfbuzz; NOCONFIGURE=1 ./autogen.sh cd $(SRC)/harfbuzz; ./configure \ --disable-dependency-tracking \ --disable-gtk-doc-html \ @@ -90,7 +71,6 @@ $(TMP)/.harfbuzz-stamp: $(TMP)/.freetype-stamp $(TMP)/.ttfautohint-stamp: $(TMP)/.harfbuzz-stamp @mkdir -p $(TMP) - cd $(SRC)/ttfautohint; ./bootstrap cd $(SRC)/ttfautohint; ./configure \ --disable-dependency-tracking \ --without-qt \ @@ -109,16 +89,7 @@ $(TMP)/.ttfautohint-stamp: $(TMP)/.harfbuzz-stamp cd $(SRC)/ttfautohint; make install @touch $(TMP)/.ttfautohint-stamp -$(PREFIX)/lib/$(LIBTTFAUTOHINT): $(TMP)/.ttfautohint-stamp -ifeq ($(OS), Windows_NT) - dllwrap -v --def $(SRC)/ttfautohint.def -o $@ \ - $(PREFIX)/lib/libttfautohint.a \ - $(PREFIX)/lib/libharfbuzz.a \ - $(PREFIX)/lib/libfreetype.a -endif - clean: - @git submodule foreach git clean -fdx . @rm -rf $(TMP) $(PREFIX) -.PHONY: clean all libttfautohint freetype harfbuzz ttfautohint +.PHONY: clean all freetype harfbuzz ttfautohint diff --git a/src/c/ttfautohint.def b/src/c/ttfautohint.def deleted file mode 100644 index 2438145..0000000 --- a/src/c/ttfautohint.def +++ /dev/null @@ -1,5 +0,0 @@ -LIBRARY ttfautohint.dll -EXPORTS -TTF_autohint -TTF_autohint_version -TTF_autohint_version_string diff --git a/src/python/ttfautohint/__init__.py b/src/python/ttfautohint/__init__.py index ec26c91..9139cb0 100644 --- a/src/python/ttfautohint/__init__.py +++ b/src/python/ttfautohint/__init__.py @@ -1,147 +1,94 @@ -from __future__ import print_function, division, absolute_import - -from ctypes import ( - cdll, POINTER, c_char, c_char_p, c_size_t, c_int, byref, -) -from ctypes.util import find_library - -from io import open -import sys +import io import os +import subprocess +import sys +from importlib.resources import as_file, files, is_resource from ttfautohint._version import __version__ -from ttfautohint import memory -from ttfautohint.options import validate_options, format_varargs, StemWidthMode -from ttfautohint import info -from ttfautohint import progress -from ttfautohint import errors from ttfautohint.errors import TAError -from ttfautohint import cli +from ttfautohint.options import validate_options, format_kwargs, StemWidthMode -__all__ = ["ttfautohint", "TAError", "StemWidthMode"] +__all__ = ["__version__", "ttfautohint", "TAError", "StemWidthMode", "run"] -class TALibrary(object): +EXECUTABLE = "ttfautohint" +if sys.platform == "win32": + EXECUTABLE += ".exe" - def __init__(self, path=None, **kwargs): - """ Initialize a new handle to the libttfautohint shared library. - If no path is provided, by default the embedded shared library that - comes with the binary wheel is loaded first. If this is not found, - then `ctypes.util.find_library` function is used to search in the - system's default search paths. - """ - if path is None: - if sys.platform == "win32": - name = "libttfautohint.dll" - elif sys.platform == "darwin": - name = "libttfautohint.dylib" - else: - name = "libttfautohint.so" - path = os.path.join(os.path.dirname(__file__), name) - if not os.path.isfile(path): - path = find_library("ttfautohint") - if not path: - raise OSError("cannot find '%s'" % name) - self.lib = lib = cdll.LoadLibrary(path, **kwargs) - self.path = path - - lib.TTF_autohint_version.argtypes = [POINTER(c_int)] * 3 - lib.TTF_autohint_version.restype = None - _major, _minor, _revision = c_int(), c_int(), c_int() - lib.TTF_autohint_version(_major, _minor, _revision) - self.major = _major.value - self.minor = _minor.value - self.revision = _revision.value - - lib.TTF_autohint_version_string.restype = c_char_p - version_string = lib.TTF_autohint_version_string().decode('ascii') - self.version_string = version_string - - # In the M1 ABI, ctypes counts the number of fixed arguments - # when building the varargs array. See https://bugs.python.org/issue42880 - lib.TTF_autohint.argtypes = [c_char_p] - - def _build_info_data(self, options): - # as a side effect, these arguments are popped from the options dict - # as they are not part of TTF_autohint API - family_suffix = options.pop("family_suffix") - no_info = options.pop("no_info") - detailed_info = options.pop("detailed_info") - if no_info: - info_string = None - else: - info_string = info.build_info_string(self.version_string, - detailed_info, **options) - return info.InfoData(info_string, family_suffix) +HAS_BUNDLED_EXE = None - def ttfautohint(self, **kwargs): - options = validate_options(kwargs) - info_data = self._build_info_data(options) +def _has_bundled_exe(): + global HAS_BUNDLED_EXE - if info_data.family_suffix: - info_post_callback = info.info_post_callback - else: - info_post_callback = None + if HAS_BUNDLED_EXE is None: + HAS_BUNDLED_EXE = is_resource(__name__, EXECUTABLE) + + return HAS_BUNDLED_EXE + + +def run(args, **kwargs): + """Run the 'ttfautohint' executable with the list of positional arguments. + + All keyword arguments are forwarded to subprocess.run function. - if options.pop("verbose"): - # by default, it prints to stderr like ttfautohint.exe - # TODO: figure out a way to implement progress using logging? - printer = progress.ProgressPrinter() - progress_callback = printer.callback + The bundled copy of the 'ttfautohint' executable is tried first; if this + was not included at installation, the version which is on $PATH is used. + + Return: + subprocess.CompletedProcess object with the following attributes: + args, returncode, stdout, stderr. + """ + if _has_bundled_exe(): + with as_file(files(__name__).joinpath(EXECUTABLE)) as bundled_exe: + return subprocess.run([str(bundled_exe)] + list(args), **kwargs) + else: + return subprocess.run([EXECUTABLE] + list(args), **kwargs) + + +# TODO: add docstring +def ttfautohint(**kwargs): + options = validate_options(kwargs) + + in_buffer = options.pop("in_buffer") + out_file = options.pop("out_file") + + capture_output = True + stdout = None + should_close_stdout = False + if out_file is not None: + if isinstance(out_file, (str, bytes, os.PathLike)): + stdout, out_file = open(out_file, "w"), None + should_close_stdout = True + capture_output = False else: - progress_callback = None - progress_callback_data = progress.ProgressData() - - error_callback = errors.error_callback - control_name = options.pop("control_name", None) - error_callback_data = errors.ErrorData(control_name) - - # pop 'out_file' from options dict since we use 'out_buffer' - out_file = options.pop('out_file') - - out_buffer_p = POINTER(c_char)() - out_buffer_len = c_size_t(0) - - option_keys, option_values = format_varargs( - out_buffer=byref(out_buffer_p), - out_buffer_len=byref(out_buffer_len), - alloc_func=memory.alloc_callback, - free_func=memory.free_callback, - info_callback=info.info_callback, - info_post_callback=info_post_callback, - info_callback_data=byref(info_data), - progress_callback=progress_callback, - progress_callback_data=byref(progress_callback_data), - error_callback=error_callback, - error_callback_data=byref(error_callback_data), - **options - ) - - rv = self.lib.TTF_autohint(option_keys, *option_values) - if rv: - raise TAError(rv, **error_callback_data.kwargs) - - assert out_buffer_len.value - - data = out_buffer_p[:out_buffer_len.value] - assert len(data) == out_buffer_len.value - - if out_buffer_p: - memory.free(out_buffer_p) - out_buffer_p = None - - if out_file is not None: try: - return out_file.write(data) - except AttributeError: - with open(out_file, 'wb') as f: - return f.write(data) - else: - return data + out_file.fileno() + except io.UnsupportedOperation: + if not out_file.writable(): + raise TypeError(f"{out_file} is not writable") + else: + stdout, out_file = out_file, None + capture_output = False + + args = format_kwargs(**options) + + result = run( + args, + input=in_buffer, + capture_output=capture_output, + stdout=stdout, + ) + if result.returncode != 0: + raise TAError(result.returncode, result.stderr) + + output_data = result.stdout + if output_data and out_file is not None: + out_file.write(output_data) -libttfautohint = TALibrary() + if stdout is not None and should_close_stdout: + stdout.close() -ttfautohint = libttfautohint.ttfautohint + return output_data diff --git a/src/python/ttfautohint/__main__.py b/src/python/ttfautohint/__main__.py index 68400b3..898b706 100644 --- a/src/python/ttfautohint/__main__.py +++ b/src/python/ttfautohint/__main__.py @@ -1,4 +1,12 @@ import sys -from ttfautohint.cli import main +from ttfautohint import run -sys.exit(main()) + +def main(args=None): + if args is None: + args = sys.argv[1:] + return run(args).returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/python/ttfautohint/_compat.py b/src/python/ttfautohint/_compat.py index 235c3dc..95ad107 100644 --- a/src/python/ttfautohint/_compat.py +++ b/src/python/ttfautohint/_compat.py @@ -1,21 +1,5 @@ -import sys - - -PY3 = sys.version_info[0] >= 3 - -if PY3: - text_type = basestring = str - iterbytes = iter -else: # PY2 - text_type = unicode - basestring = basestring - import itertools - import functools - iterbytes = functools.partial(itertools.imap, ord) - - def ensure_binary(s, encoding="ascii", errors="strict"): - if isinstance(s, text_type): + if isinstance(s, str): return s.encode(encoding, errors) elif isinstance(s, bytes): return s @@ -26,79 +10,7 @@ def ensure_binary(s, encoding="ascii", errors="strict"): def ensure_text(s, encoding="ascii", errors="strict"): if isinstance(s, bytes): return s.decode(encoding, errors) - elif isinstance(s, text_type): + elif isinstance(s, str): return s else: raise TypeError("not expecting type '%s'" % type(s)) - - -# for python 2 on Windows we need to wrap the io.open function in order -# to be able to reopen the standard stdin/stout streams in binary mode. -# By detault, they are opened in 'text' mode by the MSVC runtime, so -# we need to call setmode with the O_BINARY flag. -# In Python 3 the binary flag is always set (python itself takes care -# of the newline translation of standard streams) -if PY3: - open = open -else: - import io - try: - from msvcrt import setmode # only available on Windows - except ImportError: - # on non-Windows platforms we can use the regular io.open - open = io.open - else: - import os - - def open(file, mode='r', buffering=-1, encoding=None, errors=None, - newline=None, closefd=True): - if isinstance(file, int): - # the 'file' argument is an integer file descriptor - fd = file - if fd < 0: - raise ValueError('negative file descriptor') - if setmode: - # `setmode` function sets the line-end translation and - # returns the value of the previous mode - fdcopy = os.dup(fd) - current_mode = setmode(fdcopy, os.O_BINARY) - if not (current_mode & os.O_BINARY): - # the binary mode was not set: use the copy - file = fdcopy - if closefd: - # close the original file descriptor - os.close(fd) - else: - # ensure the copy is closed when file is closed - closefd = True - else: - # original file already had binary flag, close copy - os.close(fdcopy) - - return io.open( - file, mode, buffering, encoding, errors, newline, closefd) - - -try: - from enum import IntEnum -except ImportError: - from collections import OrderedDict, namedtuple - - # make do without the real Enum type, python3 only... :( - def IntEnum(typename, field_names, start=1): - - @property - def __members__(self): - return OrderedDict([(k, getattr(self, k)) - for k in self._fields]) - - def __call__(self, value): - if value not in self: - raise ValueError("%s is not a valid %s" % (value, typename)) - return value - - base = namedtuple(typename, field_names) - attributes = {"__members__": __members__, - "__call__": __call__} - klass = type(typename, (base,), attributes) - return klass._make(range(start, len(field_names) + start)) diff --git a/src/python/ttfautohint/_version.py b/src/python/ttfautohint/_version.py index 7ce9afc..42c71e6 100644 --- a/src/python/ttfautohint/_version.py +++ b/src/python/ttfautohint/_version.py @@ -1,11 +1,16 @@ -try: - from pkg_resources import get_distribution, DistributionNotFound - __version__ = get_distribution("ttfautohint-py").version -except (ImportError, DistributionNotFound): - # either pkg_resources is missing or package is not installed - import warnings - warnings.warn( - "'ttfautohint-py' is missing the required distribution metadata. " - "Please make sure it was installed correctly.", UserWarning, - stacklevel=2) - __version__ = "0.0.0" +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.5.2.dev4+g36886f5.d20241113' +__version_tuple__ = version_tuple = (0, 5, 2, 'dev4', 'g36886f5.d20241113') diff --git a/src/python/ttfautohint/cli.py b/src/python/ttfautohint/cli.py index eeccfa7..fff4745 100644 --- a/src/python/ttfautohint/cli.py +++ b/src/python/ttfautohint/cli.py @@ -1,32 +1,15 @@ -from __future__ import print_function +import sys import ttfautohint -import logging - -log = logging.getLogger("ttfautohint") def main(args=None): - options = ttfautohint.options.parse_args(args) - - # `parse_args` can return None instead of raising SystemExit on invalid - # arguments, when `args` are passed. When it's called with args=None - # (e.g. from a console script's `main()`), SystemExit is propagated. - if options is None: - return 2 - - logging.basicConfig( - level=("DEBUG" if options["debug"] else - "INFO" if options["verbose"] else "WARNING"), - format="%(name)s: %(levelname)s: %(message)s", - ) - - try: - ttfautohint.ttfautohint(**options) - except ttfautohint.TAError as e: - log.error(e) - return e.rv + if args is None: + args = sys.argv[1:] + return ttfautohint.run(*args).returncode +# The following constants are only kept only for backward-compatibility +# to support the deprecated ttfautohint.options.parse_args function USAGE = "ttfautohint [OPTION]... [IN-FILE [OUT-FILE]]" DESCRIPTION = """\ diff --git a/src/python/ttfautohint/errors.py b/src/python/ttfautohint/errors.py index f214a3c..22bcd31 100644 --- a/src/python/ttfautohint/errors.py +++ b/src/python/ttfautohint/errors.py @@ -1,155 +1,12 @@ -from ctypes import ( - CFUNCTYPE, Structure, c_int, c_char_p, c_uint, c_void_p, cast, POINTER, - py_object, string_at, c_char, addressof, -) - - -# error codes from ttfautohint-errors.h -TA_Err_Ok = 0x00 -TA_Err_Invalid_FreeType_Version = 0x0E -TA_Err_Missing_Legal_Permission = 0x0F -TA_Err_Invalid_Stream_Write = 0x5F -TA_Err_Hinter_Overflow = 0xF0 -TA_Err_Missing_Glyph = 0xF1 -TA_Err_Missing_Unicode_CMap = 0xF2 -TA_Err_Missing_Symbol_CMap = 0xF3 -TA_Err_Canceled = 0xF4 -TA_Err_Already_Processed = 0xF5 -TA_Err_Invalid_Font_Type = 0xF6 -TA_Err_Unknown_Argument = 0xF7 -TA_Err_XHeightSnapping_Invalid_Character = 0x101 -TA_Err_XHeightSnapping_Overflow = 0x102 -TA_Err_XHeightSnapping_Invalid_Range = 0x103 -TA_Err_XHeightSnapping_Overlapping_Ranges = 0x104 -TA_Err_XHeightSnapping_Not_Ascending = 0x105 -TA_Err_XHeightSnapping_Allocation_Error = 0x106 -TA_Err_Control_Syntax_Error = 0x201 -TA_Err_Control_Invalid_Font_Index = 0x202 -TA_Err_Control_Invalid_Glyph_Index = 0x203 -TA_Err_Control_Invalid_Glyph_Name = 0x204 -TA_Err_Control_Invalid_Character = 0x205 -TA_Err_Control_Invalid_Style = 0x206 -TA_Err_Control_Invalid_Script = 0x207 -TA_Err_Control_Invalid_Feature = 0x208 -TA_Err_Control_Invalid_Shift = 0x209 -TA_Err_Control_Invalid_Offset = 0x20A -TA_Err_Control_Invalid_Range = 0x20B -TA_Err_Control_Invalid_Glyph = 0x20C -TA_Err_Control_Overflow = 0x20D -TA_Err_Control_Overlapping_Ranges = 0x20E -TA_Err_Control_Ranges_Not_Ascending = 0x20F -TA_Err_Control_Allocation_Error = 0x210 -TA_Err_Control_Flex_Error = 0x211 -TA_Err_Control_Too_Much_Widths = 0x212 - - class TAError(Exception): - - def __init__(self, rv, error_string=None, control_name=None, errlinenum=0, - errline=None, errpos=-1): + def __init__(self, rv, error_string): self.rv = int(rv) - - if error_string is not None: - error_string = error_string.decode("utf-8", errors="replace") - self.error_string = error_string - - self.control_name = control_name - self.errlinenum = int(errlinenum) - - if errline is not None: - errline = errline.decode("utf-8", errors="replace") - self.errline = errline - self.errpos = int(errpos) + self.error_string = error_string.decode("utf-8", errors="replace") def __str__(self): error = self.rv error_string = self.error_string - errlinenum = self.errlinenum - errline = self.errline - errpos = self.errpos - - if error == TA_Err_Invalid_FreeType_Version: - s = ("FreeType version 2.4.5 or higher is needed.\n" - "Perhaps using a wrong FreeType DLL?") - elif error == TA_Err_Invalid_Font_Type: - s = ("This font is not a valid font in SFNT format with " - "TrueType outlines.\n" - "In particular, CFF outlines are not supported.") - elif error == TA_Err_Already_Processed: - s = "This font has already been processed with ttfautohint" - elif error == TA_Err_Missing_Legal_Permission: - s = ("Bit 1 in the `fsType' field of the `OS/2' table is set:\n" - "This font must not be modified without permission of the " - "legal owner.\n" - "Use command line option `-i' to continue if you have such " - "a permission.") - elif error == TA_Err_Missing_Unicode_CMap: - s = "No Unicode character map" - elif error == TA_Err_Missing_Symbol_CMap: - s = "No symbol character map" - elif error == TA_Err_Missing_Glyph: - s = ("No glyph for a standard character to derive standard " - "width and height.\n" - "Please check the documentation for a list of script-" - "specific standard characters,\n" - "or use option `--symbol'.") - if error >= 0x100 and error < 0x200: - s = ("An error with code 0x%03X occurred while parsing the " - "argument of option `-X'" % error) - s += (":" if errline else ".") - if errline: - s += "\n %s" % errline - if errpos > -1: - s += "\n %s^" % (" "*errpos) - elif error >= 0x200 and error < 0x300: - s = "%s:" % self.control_name - if errlinenum > -1: - s += "%d:" % errlinenum - if errpos > -1 and errline: - s += "%r:" % errpos - if error_string: - s += " %s" % error_string - s += " (0x%02X)" % error - if errline: - s += "\n %s" % errline - if errpos > -1: - s += "\n %s^" % (" "*errpos) - elif error >= 0x300 and error < 0x400: - error -= 0x300 - s = "error while loading the reference font" - if error_string: - s += ": %s" % error_string - s += " (0x%02X)" % error - else: - s = "0x%02X" % error - if error_string: - s += ": %s" % error_string - + s = "0x%02X" % error + if error_string: + s += ": %s" % error_string return s - - -class ErrorData(Structure): - - _fields_ = [ - ("kwargs", py_object), - ] - - def __init__(self, control_name=None): - kwargs = dict(control_name=control_name) - super(ErrorData, self).__init__(kwargs) - - -@CFUNCTYPE(None, c_int, c_char_p, c_uint, POINTER(c_char), POINTER(c_char), - c_void_p) -def error_callback(error, error_string, errlinenum, errline, errpos, user): - e = cast(user, POINTER(ErrorData))[0] - if not error: - return - e.kwargs["error_string"] = error_string - e.kwargs["errlinenum"] = errlinenum - if not errline: - return - e.kwargs["errline"] = string_at(errline) - if errpos: - e.kwargs["errpos"] = (addressof(errpos.contents) - - addressof(errline.contents) + 1) diff --git a/src/python/ttfautohint/info.py b/src/python/ttfautohint/info.py deleted file mode 100644 index c02b75f..0000000 --- a/src/python/ttfautohint/info.py +++ /dev/null @@ -1,315 +0,0 @@ -from __future__ import absolute_import -from ctypes import ( - c_int, c_ushort, c_ubyte, c_void_p, c_wchar_p, POINTER, CFUNCTYPE, cast, - Structure, memmove, py_object, -) -import sys -import os -import array - -from ._compat import ensure_text, iterbytes, PY3 -from . import memory -from .options import ( - USER_OPTIONS, CONTROL_NAME_FALLBACK, StemWidthMode, - STEM_WIDTH_MODE_OPTIONS, -) - - -TA_Info_Func_Proto = CFUNCTYPE( - c_int, # (return value) - c_ushort, # platform_id - c_ushort, # encoding_id - c_ushort, # language_id - c_ushort, # name_id - POINTER(c_ushort), # str_len - POINTER(POINTER(c_ubyte)), # str - c_void_p # info_data -) - - -TA_Info_Post_Func_Proto = CFUNCTYPE(c_int, c_void_p) - - -INFO_PREFIX = u"; ttfautohint" - -# map StemWidthMode values to lowercased initials of enum members -_mode_letters = {v: k[0].lower() - for k, v in StemWidthMode.__members__.items()} - - -def build_info_string(version, detailed_info=True, control_name=None, - **kwargs): - options = {k: kwargs.get(k, USER_OPTIONS[k]) for k in USER_OPTIONS} - s = INFO_PREFIX + " (v%s)" % version - - if not detailed_info: - return s - - if options.get("dehint"): - s += " -d" - return s - - s += " -l %d" % options["hinting_range_min"] - s += " -r %d" % options["hinting_range_max"] - s += " -G %d" % options["hinting_limit"] - s += " -x %d" % options["increase_x_height"] - if options.get("fallback_stem_width"): - s += " -H %d" % options["fallback_stem_width"] - s += " -D %s" % ensure_text(options["default_script"]) - s += " -f %s" % ensure_text(options["fallback_script"]) - - if control_name and control_name != CONTROL_NAME_FALLBACK: - s += ' -m "%s"' % os.path.basename( - ensure_text(control_name, sys.getfilesystemencoding())) - - reference_name = options.get("reference_name") - if reference_name: - s += ' -R "%s"' % os.path.basename( - ensure_text(reference_name, sys.getfilesystemencoding())) - - if options.get("reference_index"): - s += " -Z %d" % options["reference_index"] - - stem_width_modes = [] - for mode_option in STEM_WIDTH_MODE_OPTIONS: - mode_value = options[mode_option] - stem_width_modes.append(_mode_letters[mode_value]) - s += " -a %s" % "".join(stem_width_modes) - - if options.get("windows_compatibility"): - s += " -W" - if options.get("adjust_subglyphs"): - s += " -p" - if options.get("hint_composites"): - s += " -c" - if options.get("symbol"): - s += " -s" - if options.get("fallback_scaling"): - s += " -S" - if options.get("TTFA_info"): - s += " -t" - x_excepts = ensure_text(options.get("x_height_snapping_exceptions", "")) - s += ' -X "%s"' % x_excepts - - return s - - -class InfoData(Structure): - - _fields_ = [ - ("info_string", c_wchar_p), - ("family_suffix", c_wchar_p), - ("family_data", py_object), - ] - - def __init__(self, info_string=None, family_suffix=None, family_data=None): - if family_data is None: - family_data = {} - super(InfoData, self).__init__(info_string, family_suffix, family_data) - - -class MutableByteString(object): - - max_length = 0xFFFF - StringPtr = POINTER(POINTER(c_ubyte)) - LengthPtr = POINTER(c_ushort) - - def __init__(self, string_p, length_p): - if not isinstance(string_p, self.StringPtr): - raise TypeError("expected %s, found %s" % ( - self.StringPtr.__name__, type(string_p).__name__)) - if not string_p: - raise ValueError("string_p must not be NULL") - elif not string_p[0]: - raise ValueError("string_p[0] must not be NULL") - self.string_p = string_p - if not isinstance(length_p, self.LengthPtr): - raise TypeError("expected %s, found %s" %( - self.LengthPtr.__name__, type(length_p).__name__)) - if not length_p: - raise ValueError("length_p must not be NULL") - self.length_p = length_p - - def __len__(self): - return self.length_p[0] - - def tobytes(self): - size = len(self) - if not size: - return b"" - a = array.array("B", self.string_p[0][:size]) - # tobytes is PY3 only; the equivalent tostring is deprecated :( - return a.tobytes() if PY3 else a.tostring() - - def frombytes(self, s): - new_len = len(s) - if new_len > self.max_length: - raise OverflowError("string exceeds the maximum length") - string_p = self.string_p - current_len = len(self) - if new_len > current_len: - void_p = memory.realloc(string_p[0], new_len) - if not void_p: # pragma: no cover - # realloc failed (unlikely) - raise MemoryError() - string_p[0] = cast(void_p, POINTER(c_ubyte)) - string = string_p[0] - for i, b in enumerate(iterbytes(s)): - string[i] = b - self.length_p[0] = new_len - - -def name_string_is_wide(platform_id, encoding_id): - # True if the platform_id/encoding_id tuple uses UTF-16BE - return not (platform_id == 1 or - (platform_id == 3 and not ( - encoding_id == 1 or encoding_id == 10))) - - -def info_name_id_5(platform_id, encoding_id, name_string, data): - string = name_string.tobytes() - - # encode our info string as ASCII for name records that use single - # or multi-byte encodings, or as (two-byte) UTF-16BE for everything else - if name_string_is_wide(platform_id, encoding_id): - encoding = "utf-16be" - offset = 2 - else: - encoding = "ascii" - offset = 1 - - info_string = data.info_string.encode(encoding) - info_prefix = INFO_PREFIX.encode(encoding) - semicolon = u";".encode(encoding) - # if we already have an ttfautohint info string, remove it up to a - # following `;' character (or end of string) - start = string.find(info_prefix) - if start != -1: - new_string = string[:start] + info_string - string_end = string[start+offset:] - last_semicolon_index = string_end.rfind(semicolon) - if last_semicolon_index != -1: - new_string += string_end[last_semicolon_index:] - else: - new_string = string + info_string - - try: - name_string.frombytes(new_string) - except OverflowError: - # do nothing if the string would become too long - pass - except MemoryError: # pragma: no cover - # return non-zero in the unlikely event of landing on water - return 1 - return 0 - - -class Family(object): - - related_name_ids = frozenset([1, 4, 6, 16, 21]) - - def __init__(self): - for name_id in self.related_name_ids: - setattr(self, "name_id_%d" % name_id, None) - - -def _info_callback(platform_id, encoding_id, language_id, name_id, str_len_p, - string_p, info_data_p): - # cast void pointer to a pointer to InfoData struct - data = cast(info_data_p, POINTER(InfoData))[0] - - # if ID is a version string, append our data - if data.info_string and name_id == 5: - name_string = MutableByteString(string_p, str_len_p) - return info_name_id_5(platform_id, - encoding_id, - name_string, - data) - - # if ID is related to a family name, collect the data - if data.family_suffix and name_id in Family.related_name_ids: - triplet = (platform_id, encoding_id, language_id) - family = data.family_data.setdefault(triplet, Family()) - name_string = MutableByteString(string_p, str_len_p) - setattr(family, "name_id_%d" % name_id, name_string) - - return 0 - - -info_callback = TA_Info_Func_Proto(_info_callback) - - -def insert_suffix(suffix, family_name, name_string): - string = name_string.tobytes() - - # check whether family_name is a substring - start = string.find(family_name) - if start != -1: - # insert suffix after the family_name substring - end = start + len(family_name) - new_string = string[:end] + suffix + string[end:] - else: - # it's not, we just append the suffix at the end - new_string = string + suffix - - try: - name_string.frombytes(new_string) - except (OverflowError, MemoryError): - pass # warn? - - -def _info_post_callback(info_data_p): - # cast void pointer to a pointer to InfoData struct - data = cast(info_data_p, POINTER(InfoData))[0] - family_data = data.family_data - - family_suffix = data.family_suffix.encode("ascii") - family_suffix_wide = data.family_suffix.encode("utf-16be") - - family_suffix_stripped = data.family_suffix.replace(" ", "") - family_ps_suffix = family_suffix_stripped.encode("ascii") - family_ps_suffix_wide = family_suffix_stripped.encode("utf-16be") - - for family in family_data.values(): - if family.name_id_16: - family.family_name = family.name_id_16.tobytes() - elif family.name_id_1: - family.family_name = family.name_id_1.tobytes() - - for (plat_id, enc_id, lang_id), family in family_data.items(): - if hasattr(family, "family_name"): - family_name = family.family_name - else: - for (pid, eid, _), f in family_data.items(): - if (pid == family.platform_id and - eid == family.encoding_id and - hasattr(f, "family_name")): - family_name = f.family_name - break - else: - continue - - if name_string_is_wide(plat_id, enc_id): - suffix = family_suffix_wide - ps_suffix = family_ps_suffix_wide - family_ps_name = family_name.replace(b"\0 ", b"") - else: - suffix = family_suffix - ps_suffix = family_ps_suffix - family_ps_name = family_name.replace(b" ", b"") - - if family.name_id_1: - insert_suffix(suffix, family_name, family.name_id_1) - if family.name_id_4: - insert_suffix(suffix, family_name, family.name_id_4) - if family.name_id_6: - insert_suffix(ps_suffix, family_ps_name, family.name_id_6) - if family.name_id_16: - insert_suffix(suffix, family_name, family.name_id_16) - if family.name_id_21: - insert_suffix(suffix, family_name, family.name_id_21) - - return 0 - - -info_post_callback = TA_Info_Post_Func_Proto(_info_post_callback) diff --git a/src/python/ttfautohint/memory.py b/src/python/ttfautohint/memory.py deleted file mode 100644 index 5fcd737..0000000 --- a/src/python/ttfautohint/memory.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -from ctypes import cdll, c_size_t, c_void_p, CFUNCTYPE -from ctypes.util import find_library - - -# we load the libc to get the standard malloc and free functions. -# They will be used anyway by libttfautohint if we didn't provide the -# 'alloc-func' and 'free-func' callbacks. However, by explicitly passing -# them, we ensure that both libttfautohint and cytpes will use the same -# memory allocation functions. E.g. on Windows, a DLL may be linked -# with a different version of the C runtime library than the application -# which loads the DLL. -if sys.platform == "win32": - libc = cdll.msvcrt -else: - libc_path = find_library("c") - if libc_path is None: - raise OSError("Could not find the libc shared library") - libc = cdll.LoadLibrary(libc_path) - -malloc = libc.malloc -malloc.argtypes = [c_size_t] -malloc.restype = c_void_p - -free = libc.free -free.argtypes = [c_void_p] -free.restype = None - -realloc = libc.realloc -realloc.argtypes = [c_void_p, c_size_t] -realloc.restype = c_void_p - -TA_Alloc_Func_Proto = CFUNCTYPE(c_void_p, c_size_t) -alloc_callback = TA_Alloc_Func_Proto(lambda size: malloc(size)) - -TA_Free_Func_Proto = CFUNCTYPE(None, c_void_p) -free_callback = TA_Free_Func_Proto(lambda p: free(p)) diff --git a/src/python/ttfautohint/options.py b/src/python/ttfautohint/options.py index 9a7bbd5..0d5700c 100644 --- a/src/python/ttfautohint/options.py +++ b/src/python/ttfautohint/options.py @@ -1,9 +1,9 @@ import sys import os +import tempfile from collections import OrderedDict -from ttfautohint._compat import ( - ensure_binary, ensure_text, basestring, open, IntEnum, -) +from enum import IntEnum +from ttfautohint._compat import ensure_binary, ensure_text USER_OPTIONS = dict( in_file=None, @@ -14,7 +14,6 @@ reference_file=None, reference_buffer=None, reference_index=0, - reference_name=None, hinting_range_min=8, hinting_range_max=50, hinting_limit=200, @@ -27,7 +26,7 @@ fallback_script="none", fallback_scaling=False, symbol=False, - fallback_stem_width=0, + fallback_stem_width=None, ignore_restrictions=False, family_suffix=None, detailed_info=False, @@ -39,19 +38,23 @@ verbose=False, ) -StemWidthMode = IntEnum("StemWidthMode", - [ - "NATURAL", # -1 - "QUANTIZED", # 0 - "STRONG", # 1 - ], - start=-1) +StemWidthMode = IntEnum( + "StemWidthMode", + [ + "NATURAL", # -1 + "QUANTIZED", # 0 + "STRONG", # 1 + ], + start=-1, +) -STEM_WIDTH_MODE_OPTIONS = OrderedDict([ - ("gray_stem_width_mode", StemWidthMode.QUANTIZED), - ("gdi_cleartype_stem_width_mode", StemWidthMode.STRONG), - ("dw_cleartype_stem_width_mode", StemWidthMode.QUANTIZED), -]) +STEM_WIDTH_MODE_OPTIONS = OrderedDict( + [ + ("gray_stem_width_mode", StemWidthMode.QUANTIZED), + ("gdi_cleartype_stem_width_mode", StemWidthMode.STRONG), + ("dw_cleartype_stem_width_mode", StemWidthMode.QUANTIZED), + ] +) USER_OPTIONS.update(STEM_WIDTH_MODE_OPTIONS) @@ -62,37 +65,14 @@ dw_cleartype_strong_stem_width=False, ) -PRIVATE_OPTIONS = frozenset([ - "in_buffer_len", - "control_buffer_len", - "reference_buffer_len", - "out_buffer", - "out_buffer_len", - "error_string", - "alloc_func", - "free_func", - "info_callback", - "info_post_callback", - "info_callback_data", - "progress_callback", - "progress_callback_data", - "error_callback", - "error_callback_data", -]) - -ALL_OPTIONS = frozenset(USER_OPTIONS) | PRIVATE_OPTIONS - -# used when the control file does not have a name on the filesystem -CONTROL_NAME_FALLBACK = u"" - def validate_options(kwargs): opts = {k: kwargs.pop(k, USER_OPTIONS[k]) for k in USER_OPTIONS} if kwargs: raise TypeError( - "unknown keyword argument%s: %s" % ( - "s" if len(kwargs) > 1 else "", - ", ".join(repr(k) for k in kwargs))) + "unknown keyword argument%s: %s" + % ("s" if len(kwargs) > 1 else "", ", ".join(repr(k) for k in kwargs)) + ) if opts["no_info"] and opts["detailed_info"]: raise ValueError("no_info and detailed_info are mutually exclusive") @@ -109,73 +89,44 @@ def validate_options(kwargs): with open(in_file, "rb") as f: in_buffer = f.read() if not isinstance(in_buffer, bytes): - raise TypeError("in_buffer type must be bytes, not %s" - % type(in_buffer).__name__) - opts['in_buffer'] = in_buffer - opts['in_buffer_len'] = len(in_buffer) + raise TypeError( + "in_buffer type must be bytes, not %s" % type(in_buffer).__name__ + ) + opts["in_buffer"] = in_buffer - control_file = opts.pop('control_file') - control_buffer = opts.pop('control_buffer') - if control_file is not None: - if control_buffer is not None: - raise ValueError( - "control_file and control_buffer are mutually exclusive") - try: - control_buffer = control_file.read() - except AttributeError: - with open(control_file, "rt", encoding="utf-8") as f: - control_buffer = f.read() - opts["control_name"] = control_file - else: - try: - opts["control_name"] = control_file.name - except AttributeError: - pass + control_file = opts.pop("control_file") + control_buffer = opts.pop("control_buffer") if control_buffer is not None: - opts['control_buffer'] = ensure_binary(control_buffer, "utf-8") - opts['control_buffer_len'] = len(control_buffer) - if "control_name" in opts: - opts["control_name"] = ensure_text( - opts["control_name"], encoding=sys.getfilesystemencoding()) - else: - opts["control_name"] = CONTROL_NAME_FALLBACK + if control_file is not None: + raise ValueError("control_file and control_buffer are mutually exclusive") + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(ensure_binary(control_buffer, "utf-8")) + control_file = tmp.name + if control_file is not None: + opts["control_file"] = control_file - reference_file = opts.pop('reference_file') - reference_buffer = opts.pop('reference_buffer') - if reference_file is not None: - if reference_buffer is not None: - raise ValueError( - "reference_file and reference_buffer are mutually exclusive") - try: - reference_buffer = reference_file.read() - except AttributeError: - with open(reference_file, "rb") as f: - reference_buffer = f.read() - if opts["reference_name"] is None: - opts["reference_name"] = reference_file - else: - if opts["reference_name"] is None: - try: - opts["reference_name"] = reference_file.name - except AttributeError: - pass + reference_file = opts.pop("reference_file") + reference_buffer = opts.pop("reference_buffer") if reference_buffer is not None: + if reference_file is not None: + raise ValueError( + "reference_file and reference_buffer are mutually exclusive" + ) if not isinstance(reference_buffer, bytes): - raise TypeError("reference_buffer type must be bytes, not %s" - % type(reference_buffer).__name__) - opts['reference_buffer'] = reference_buffer - opts['reference_buffer_len'] = len(reference_buffer) - if opts["reference_name"] is not None: - opts["reference_name"] = ensure_binary( - opts["reference_name"], encoding=sys.getfilesystemencoding()) - - for key in ('default_script', 'fallback_script', - 'x_height_snapping_exceptions'): - opts[key] = ensure_binary(opts[key]) - - if opts['epoch'] is not None: + raise TypeError( + "reference_buffer type must be bytes, not %s" + % type(reference_buffer).__name__ + ) + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(ensure_binary(reference_buffer, "utf-8")) + reference_file = tmp.name + if reference_file is not None: + opts["reference_file"] = reference_file + + if opts["epoch"] is not None: from ctypes import c_ulonglong - opts['epoch'] = c_ulonglong(opts['epoch']) + + opts["epoch"] = c_ulonglong(opts["epoch"]) if opts["family_suffix"] is not None: opts["family_suffix"] = ensure_text(opts["family_suffix"]) @@ -187,59 +138,94 @@ def validate_options(kwargs): return opts -def format_varargs(**options): - items = sorted((k, v) for k, v in options.items() - if k in ALL_OPTIONS and v is not None) - format_string = b", ".join(ensure_binary(k.replace("_", "-")) - for k, v in items) - values = tuple(v for k, v in items) - return format_string, values +def format_kwargs(**options): + result = [] + modes = {} + for key, value in options.items(): + if key not in USER_OPTIONS: + raise ValueError(f"unknown keyword argument: {key}") + if value is None: + continue + if key in STEM_WIDTH_MODE_OPTIONS: + modes[key] = value + continue + opt = f"--{key.replace('_', '-')}" + if isinstance(value, bool): + if value: + result.append(opt) + elif isinstance(value, (int, float, str)): + if value != USER_OPTIONS[key]: + result.append(opt) + result.append(str(value)) + else: + raise TypeError(f"{key}: {type(value)}") + if modes: + result.extend(["--stem-width-mode", format_stem_width_modes(**modes)]) + return result def strong_stem_width(s): if len(s) > 3: import argparse - raise argparse.ArgumentTypeError( - "string can only contain up to 3 letters") + + raise argparse.ArgumentTypeError("string can only contain up to 3 letters") valid = { "g": "gray_stem_width_mode", "G": "gdi_cleartype_stem_width_mode", - "D": "dw_cleartype_stem_width_mode"} + "D": "dw_cleartype_stem_width_mode", + } chars = set(s) invalid = chars - set(valid) if invalid: import argparse + raise argparse.ArgumentTypeError( - "invalid value: %s" % ", ".join( - repr(v) for v in sorted(invalid))) + "invalid value: %s" % ", ".join(repr(v) for v in sorted(invalid)) + ) result = {} for char, opt_name in valid.items(): is_strong = char in chars - result[opt_name] = (StemWidthMode.STRONG if is_strong - else StemWidthMode.QUANTIZED) + result[opt_name] = ( + StemWidthMode.STRONG if is_strong else StemWidthMode.QUANTIZED + ) return result def stem_width_mode(s): if len(s) != 3: import argparse + raise argparse.ArgumentTypeError( - "Stem width mode string must consist of exactly three letters") - modes = {k[0].lower(): v - for k, v in StemWidthMode.__members__.items()} + "Stem width mode string must consist of exactly three letters" + ) + modes = {k[0].lower(): v for k, v in StemWidthMode.__members__.items()} result = {} for i, option in enumerate(STEM_WIDTH_MODE_OPTIONS): m = s[i] if m not in modes: import argparse + letters = sorted(repr(k) for k in modes) raise argparse.ArgumentTypeError( "Stem width mode letter for %s must be %s, or %s" - % (option, ", ".join(letters[:-1]), letters[-1])) + % (option, ", ".join(letters[:-1]), letters[-1]) + ) result[option] = modes[m] return result +def format_stem_width_modes( + gray_stem_width_mode, + gdi_cleartype_stem_width_mode, + dw_cleartype_stem_width_mode, +): + return ( + gray_stem_width_mode.name[0].lower() + + gdi_cleartype_stem_width_mode.name[0].lower() + + dw_cleartype_stem_width_mode.name[0].lower() + ) + + def stdin_or_input_path_type(s): # the special argument "-" means sys.stdin if s == "-": @@ -275,7 +261,7 @@ def _windows_cmdline2list(cmdline): Borrowed from Jython source code: https://github.com/jython/jython/blob/50729e6/Lib/subprocess.py#L668-L722 """ - whitespace = ' \t' + whitespace = " \t" # count of preceding '\' bs_count = 0 in_quotes = False @@ -286,10 +272,10 @@ def _windows_cmdline2list(cmdline): if ch in whitespace and not in_quotes: if arg: # finalize arg and reset - argv.append(''.join(arg)) + argv.append("".join(arg)) arg = [] bs_count = 0 - elif ch == '\\': + elif ch == "\\": arg.append(ch) bs_count += 1 elif ch == '"': @@ -297,13 +283,13 @@ def _windows_cmdline2list(cmdline): # Even number of '\' followed by a '"'. Place one # '\' for every pair and treat '"' as a delimiter if bs_count: - del arg[-(bs_count / 2):] + del arg[-(bs_count / 2) :] in_quotes = not in_quotes else: # Odd number of '\' followed by a '"'. Place one '\' # for every pair and treat '"' as an escape sequence # by the remaining '\' - del arg[-(bs_count / 2 + 1):] + del arg[-(bs_count / 2 + 1) :] arg.append(ch) bs_count = 0 else: @@ -313,11 +299,27 @@ def _windows_cmdline2list(cmdline): # A single trailing '"' delimiter yields an empty arg if arg or in_quotes: - argv.append(''.join(arg)) + argv.append("".join(arg)) return argv +def _parse_ttfautohint_version_string(): + from ttfautohint import run + + result = run(["--version"], capture_output=True, check=True) + + output = result.stdout + if not output: + raise ValueError("Could not parse ttfautohint --version") + + first_line = result.stdout.decode("utf-8").splitlines()[0] + if not first_line.startswith("ttfautohint "): + raise ValueError(f"ttfautohint --version has unexpected format: {first_line}") + + return first_line[12:] + + def parse_args(args=None, splitfunc=None): """Parse command line arguments and return a dictionary of options for ttfautohint.ttfautohint function. @@ -334,22 +336,32 @@ def parse_args(args=None, splitfunc=None): a `None` value is returned. """ import argparse - from ttfautohint import __version__, libttfautohint + from ttfautohint import __version__ from ttfautohint.cli import USAGE, DESCRIPTION, EPILOG + import warnings + + warnings.warn( + "`ttfautohint.options.parse_args` is deprecated and will be removed " + "in a future release. Use `ttfautohint.run` instead.", + DeprecationWarning, + ) version_string = "ttfautohint-py %s (libttfautohint %s)" % ( - __version__, libttfautohint.version_string) + __version__, + _parse_ttfautohint_version_string(), + ) if args is None: capture_sys_exit = False else: capture_sys_exit = True - if isinstance(args, basestring): + if isinstance(args, str): if splitfunc is None: if sys.platform == "win32": splitfunc = _windows_cmdline2list else: import shlex + splitfunc = shlex.split args = splitfunc(args) @@ -361,119 +373,222 @@ def parse_args(args=None, splitfunc=None): formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( - "in_file", nargs="?", metavar="IN-FILE", default="-", + "in_file", + nargs="?", + metavar="IN-FILE", + default="-", type=stdin_or_input_path_type, - help="input file (default: standard input)") + help="input file (default: standard input)", + ) parser.add_argument( - "out_file", nargs="?", metavar="OUT-FILE", default="-", + "out_file", + nargs="?", + metavar="OUT-FILE", + default="-", type=stdout_or_output_path_type, - help="output file (default: standard output)") + help="output file (default: standard output)", + ) parser.add_argument( - "--debug", action="store_true", help="print debugging information") + "--debug", action="store_true", help="print debugging information" + ) stem_width_group = parser.add_mutually_exclusive_group(required=False) stem_width_group.add_argument( - "-a", "--stem-width-mode", type=stem_width_mode, metavar="S", + "-a", + "--stem-width-mode", + type=stem_width_mode, + metavar="S", default=STEM_WIDTH_MODE_OPTIONS, - help=("select stem width mode for grayscale, GDI ClearType, and DW " - "ClearType, where S is a string of three letters with possible " - "values 'n' for natural, 'q' for quantized, and 's' for strong " - "(default: qsq)")) + help=( + "select stem width mode for grayscale, GDI ClearType, and DW " + "ClearType, where S is a string of three letters with possible " + "values 'n' for natural, 'q' for quantized, and 's' for strong " + "(default: qsq)" + ), + ) stem_width_group.add_argument( # deprecated - "-w", "--strong-stem-width", type=strong_stem_width, metavar="S", - help=argparse.SUPPRESS) + "-w", + "--strong-stem-width", + type=strong_stem_width, + metavar="S", + help=argparse.SUPPRESS, + ) parser.add_argument( - "-c", "--composites", dest="hint_composites", action="store_true", - help="hint glyph composites also") - parser.add_argument( - "-d", "--dehint", action="store_true", help="remove all hints") + "-c", + "--composites", + dest="hint_composites", + action="store_true", + help="hint glyph composites also", + ) + parser.add_argument("-d", "--dehint", action="store_true", help="remove all hints") parser.add_argument( - "-D", "--default-script", metavar="SCRIPT", + "-D", + "--default-script", + metavar="SCRIPT", default=USER_OPTIONS["default_script"], - help="set default OpenType script (default: %(default)s)") + help="set default OpenType script (default: %(default)s)", + ) parser.add_argument( - "-f", "--fallback-script", metavar="SCRIPT", + "-f", + "--fallback-script", + metavar="SCRIPT", default=USER_OPTIONS["fallback_script"], - help="set fallback script (default: %(default)s)") + help="set fallback script (default: %(default)s)", + ) parser.add_argument( - "-F", "--family-suffix", metavar="SUFFIX", - help="append SUFFIX to the family name string(s) in the `name' table") + "-F", + "--family-suffix", + metavar="SUFFIX", + help="append SUFFIX to the family name string(s) in the `name' table", + ) parser.add_argument( - "-G", "--hinting-limit", type=int, metavar="PPEM", + "-G", + "--hinting-limit", + type=int, + metavar="PPEM", default=USER_OPTIONS["hinting_limit"], - help=("switch off hinting above this PPEM value (default: " - "%(default)s); value 0 means no limit")) + help=( + "switch off hinting above this PPEM value (default: " + "%(default)s); value 0 means no limit" + ), + ) parser.add_argument( - "-H", "--fallback-stem-width", type=int, metavar="UNITS", + "-H", + "--fallback-stem-width", + type=int, + metavar="UNITS", default=USER_OPTIONS["fallback_stem_width"], - help=("set fallback stem width (default: %(default)s font units at " - "2048 UPEM)")) + help=("set fallback stem width (default: 50 font units at 2048 UPEM)"), + ) parser.add_argument( - "-i", "--ignore-restrictions", action="store_true", - help="override font license restrictions") + "-i", + "--ignore-restrictions", + action="store_true", + help="override font license restrictions", + ) parser.add_argument( - "-I", "--detailed-info", action="store_true", - help=("add detailed ttfautohint info to the version string(s) in " - "the `name' table")) + "-I", + "--detailed-info", + action="store_true", + help=( + "add detailed ttfautohint info to the version string(s) in " + "the `name' table" + ), + ) parser.add_argument( - "-l", "--hinting-range-min", type=int, metavar="PPEM", + "-l", + "--hinting-range-min", + type=int, + metavar="PPEM", default=USER_OPTIONS["hinting_range_min"], - help="the minimum PPEM value for hint sets (default: %(default)s)") + help="the minimum PPEM value for hint sets (default: %(default)s)", + ) parser.add_argument( - "-m", "--control-file", metavar="FILE", - help="get control instructions from FILE") + "-m", + "--control-file", + metavar="FILE", + help="get control instructions from FILE", + ) parser.add_argument( - "-n", "--no-info", action="store_true", - help=("don't add ttfautohint info to the version string(s) in the " - "`name' table")) + "-n", + "--no-info", + action="store_true", + help=( + "don't add ttfautohint info to the version string(s) in the " "`name' table" + ), + ) parser.add_argument( - "-p", "--adjust-subglyphs", action="store_true", - help="handle subglyph adjustments in exotic fonts") + "-p", + "--adjust-subglyphs", + action="store_true", + help="handle subglyph adjustments in exotic fonts", + ) parser.add_argument( - "-r", "--hinting-range-max", type=int, metavar="PPEM", + "-r", + "--hinting-range-max", + type=int, + metavar="PPEM", default=USER_OPTIONS["hinting_range_max"], - help="the maximum PPEM value for hint sets (default: %(default)s)") + help="the maximum PPEM value for hint sets (default: %(default)s)", + ) parser.add_argument( - "-R", "--reference", dest="reference_file", metavar="FILE", - help="derive blue zones from reference font FILE") + "-R", + "--reference", + dest="reference_file", + metavar="FILE", + help="derive blue zones from reference font FILE", + ) parser.add_argument( - "-s", "--symbol", action="store_true", - help="input is symbol font") + "-s", "--symbol", action="store_true", help="input is symbol font" + ) parser.add_argument( - "-S", "--fallback-scaling", action="store_true", - help="use fallback scaling, not hinting") + "-S", + "--fallback-scaling", + action="store_true", + help="use fallback scaling, not hinting", + ) parser.add_argument( - "-t", "--ttfa-table", action="store_true", dest="TTFA_info", - help="add TTFA information table") + "-t", + "--ttfa-table", + action="store_true", + dest="TTFA_info", + help="add TTFA information table", + ) parser.add_argument( - "-T", "--ttfa-info", dest="show_TTFA_info", action="store_true", - help="display TTFA table in IN-FILE and exit") + "-T", + "--ttfa-info", + dest="show_TTFA_info", + action="store_true", + help="display TTFA table in IN-FILE and exit", + ) parser.add_argument( - "-v", "--verbose", action="store_true", - help="show progress information") + "-v", "--verbose", action="store_true", help="show progress information" + ) parser.add_argument( - "-V", "--version", action="version", + "-V", + "--version", + action="version", version=version_string, - help="print version information and exit") + help="print version information and exit", + ) parser.add_argument( - "-W", "--windows-compatibility", action="store_true", - help=("add blue zones for `usWinAscent' and `usWinDescent' to avoid " - "clipping")) + "-W", + "--windows-compatibility", + action="store_true", + help=( + "add blue zones for `usWinAscent' and `usWinDescent' to avoid " "clipping" + ), + ) parser.add_argument( - "-x", "--increase-x-height", type=int, metavar="PPEM", + "-x", + "--increase-x-height", + type=int, + metavar="PPEM", default=USER_OPTIONS["increase_x_height"], - help=("increase x height for sizes in the range 6<=PPEM<=N; value " - "0 switches off this feature (default: %(default)s)")) + help=( + "increase x height for sizes in the range 6<=PPEM<=N; value " + "0 switches off this feature (default: %(default)s)" + ), + ) parser.add_argument( - "-X", "--x-height-snapping-exceptions", metavar="STRING", + "-X", + "--x-height-snapping-exceptions", + metavar="STRING", default=USER_OPTIONS["x_height_snapping_exceptions"], - help=('specify a comma-separated list of x-height snapping exceptions' - ', for example "-9, 13-17, 19" (default: "%(default)s")')) + help=( + "specify a comma-separated list of x-height snapping exceptions" + ', for example "-9, 13-17, 19" (default: "%(default)s")' + ), + ) parser.add_argument( - "-Z", "--reference-index", type=int, metavar="NUMBER", + "-Z", + "--reference-index", + type=int, + metavar="NUMBER", default=USER_OPTIONS["reference_index"], - help="face index of reference font (default: %(default)s)") + help="face index of reference font (default: %(default)s)", + ) try: options = vars(parser.parse_args(args)) @@ -483,8 +598,9 @@ def parse_args(args=None, splitfunc=None): raise # if either input/output are interactive, print help and exit - if (not capture_sys_exit and - (options["in_file"] is None or options["out_file"] is None)): + if not capture_sys_exit and ( + options["in_file"] is None or options["out_file"] is None + ): parser.print_help() parser.exit(1) @@ -495,8 +611,10 @@ def parse_args(args=None, splitfunc=None): options["epoch"] = int(source_date_epoch) except ValueError: import warnings + warnings.warn( - UserWarning("invalid SOURCE_DATE_EPOCH: %r" % source_date_epoch)) + UserWarning("invalid SOURCE_DATE_EPOCH: %r" % source_date_epoch) + ) if options.pop("show_TTFA_info"): # TODO use fonttools to dump TTFA table? @@ -506,8 +624,8 @@ def parse_args(args=None, splitfunc=None): strong_stem_width_options = options.pop("strong_stem_width") if strong_stem_width_options: import warnings - warnings.warn( - UserWarning("Option '-w' is deprecated! Use option '-a' instead")) + + warnings.warn(UserWarning("Option '-w' is deprecated! Use option '-a' instead")) stem_width_options = strong_stem_width_options options.update(stem_width_options) diff --git a/src/python/ttfautohint/progress.py b/src/python/ttfautohint/progress.py deleted file mode 100644 index ca7a698..0000000 --- a/src/python/ttfautohint/progress.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import division -import sys -from ctypes import ( - Structure, c_long, c_bool, c_int, c_void_p, CFUNCTYPE, cast, POINTER, -) - - -class ProgressData(Structure): - - _fields_ = [ - ("last_sfnt", c_long), - ("begin", c_bool), - ("last_percent", c_int) - ] - - def __init__(self, last_sfnt=-1, begin=True, last_percent=0): - super(ProgressData, self).__init__(last_sfnt, begin, last_percent) - - -class ProgressPrinter(object): - - def __init__(self, file=sys.stderr): - self.file = file - - @property - def callback(self): - - _write = self.file.write - - @CFUNCTYPE(c_int, c_long, c_long, c_long, c_long, c_void_p) - def progress_callback(curr_idx, num_glyphs, curr_sfnt, num_sfnts, - user): - data = cast(user, POINTER(ProgressData))[0] - - if num_sfnts > 1 and curr_sfnt != data.last_sfnt: - _write("subfont %d of %d\n" % (curr_sfnt+1, num_sfnts)) - data.last_sfnt = curr_sfnt - data.last_percent = 0 - data.begin = True - - if data.begin: - _write(" %d glyphs\n" - " " % num_glyphs) - data.begin = False - - # print progress approx. every 10% - curr_percent = curr_idx * 100 // num_glyphs - curr_diff = curr_percent - data.last_percent - - if curr_diff >= 10: - _write(" %d%%" % curr_percent) - data.last_percent = curr_percent - curr_percent % 10 - - if curr_idx + 1 == num_glyphs: - _write("\n") - - return 0 - - return progress_callback diff --git a/tests/test_info.py b/tests/test_info.py deleted file mode 100644 index d533f10..0000000 --- a/tests/test_info.py +++ /dev/null @@ -1,218 +0,0 @@ -import os -from contextlib import contextmanager - -from ctypes import ( - c_ushort, c_ubyte, POINTER, cast, -) - -from ttfautohint._compat import iterbytes -from ttfautohint import memory, StemWidthMode -from ttfautohint.info import ( - MutableByteString, InfoData, info_name_id_5, build_info_string, - name_string_is_wide, INFO_PREFIX, insert_suffix -) - -import pytest - - -@contextmanager -def create_ubyte_buffer(init): - size = c_ushort(len(init)) - - void_p = memory.malloc(size.value) - if not void_p: # pragma: no cover - raise MemoryError() - p = cast(void_p, POINTER(c_ubyte)) - - for i, b in enumerate(iterbytes(init)): - p[i] = b - - string_p = POINTER(POINTER(c_ubyte))(p) - size_p = POINTER(c_ushort)(size) - - yield MutableByteString(string_p, size_p) - - memory.free(p) - - -@pytest.mark.parametrize( - "input_string", - [b"hello world", b""], - ids=["non-empty", "empty"] -) -class TestMutableByteString(object): - - def test_tobytes(self, input_string): - with create_ubyte_buffer(input_string) as buf: - string = buf.tobytes() - assert isinstance(string, bytes) - assert string == input_string - assert len(string) == len(buf) - - def test_frombytes(self, input_string): - with create_ubyte_buffer(input_string) as buf: - string = buf.tobytes() - suffix = b" abc" - new_string = string + suffix - - buf.frombytes(string + suffix) - - assert buf.tobytes().endswith(suffix) - assert len(buf) == (len(new_string)) - - -TEST_VERSION = u"1.7" -TEST_INFO = INFO_PREFIX + u" (v%s)" % TEST_VERSION -TEST_INFO_DETAILED = TEST_INFO + ( - u' -l 8 -r 50 -G 200 -x 14 -D latn -f none -w G -X ""') - - -@pytest.mark.parametrize( - "detailed_info", - [True, False], - ids=["detailed", "no_detailed"] -) -@pytest.mark.parametrize( - "font_version, previous_info, appendix", - [ - ("Version 1.000", "", ""), - ("Version 1.000", "; ttfautohint (v1.5)", ""), - ("Version 1.000", "; ttfautohint (v1.5)", "; foo bar"), - ], - ids=[ - "no-previous-info", - "previous-info-last", - "previous-info-not-last", - ] -) -@pytest.mark.parametrize( - "plat_id, enc_id", - [(1, 0), (3, 1), (3, 10)], -) -def test_info_name_id_5(plat_id, enc_id, detailed_info, font_version, - previous_info, appendix): - info_string = TEST_INFO_DETAILED if detailed_info else TEST_INFO - info_data = InfoData(info_string) - initial_string = font_version + previous_info + appendix - encoding = "utf-16be" if name_string_is_wide(plat_id, enc_id) else "ascii" - - with create_ubyte_buffer(initial_string.encode(encoding)) as buf: - info_name_id_5(plat_id, enc_id, buf, info_data) - new_string = buf.tobytes().decode(encoding) - - assert new_string == (font_version + info_string + appendix) - - -def test_info_name_id_5_overflow(): - # we don't modify the string if it would overflow max length - size = MutableByteString.max_length - len(TEST_INFO) + 1 - string = b"\0" * size - with create_ubyte_buffer(string) as buf: - info_name_id_5(1, 0, buf, InfoData(TEST_INFO)) - - assert buf.tobytes() == string - - -def test_build_info_string_no_detail(): - s = build_info_string(TEST_VERSION, detailed_info=False) - assert s == TEST_INFO - - -@pytest.mark.parametrize( - "options, expected", - [ - ({}, ' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -X ""'), - ({"dehint": True}, " -d"), - ({"fallback_stem_width": 200}, ( - ' -l 8 -r 50 -G 200 -x 14 -H 200 -D latn -f none -a qsq -X ""')), - ({"control_name": os.path.join("src", "my_control_file.txt")}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none' - ' -m "my_control_file.txt" -a qsq -X ""')), - ({"reference_name": os.path.join("build", "MyFont-Regular.ttf"), - "reference_index": 1}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none' - ' -R "MyFont-Regular.ttf" -Z 1 -a qsq -X ""')), - ({"gray_stem_width_mode": StemWidthMode.NATURAL}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a nsq -X ""')), - ({"gray_stem_width_mode": StemWidthMode.STRONG}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a ssq -X ""')), - ({"gdi_cleartype_stem_width_mode": StemWidthMode.NATURAL}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qnq -X ""')), - ({"gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qqq -X ""')), - ({"dw_cleartype_stem_width_mode": StemWidthMode.NATURAL}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsn -X ""')), - ({"dw_cleartype_stem_width_mode": StemWidthMode.STRONG}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qss -X ""')), - ({"windows_compatibility": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -W -X ""')), - ({"adjust_subglyphs": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -p -X ""')), - ({"hint_composites": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -c -X ""')), - ({"symbol": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -s -X ""')), - ({"fallback_scaling": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -S -X ""')), - ({"TTFA_info": True}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -t -X ""')), - ({"x_height_snapping_exceptions": "6,13-17"}, - (' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -X "6,13-17"')), - ], - ids=[ - "default", - "dehint", - "fallback_stem_width", - "control_name", - "reference_name_and_index", - "gray_stem_width_mode_NATURAL", - "gray_stem_width_mode_STRONG", - "gdi_cleartype_stem_width_mode_NATURAL", - "gdi_cleartype_stem_width_mode_QUANTIZED", - "dw_cleartype_stem_width_mode_NATURAL", - "dw_cleartype_stem_width_mode_STRONG", - "windows_compatibility", - "adjust_subglyphs", - "hint_composites", - "symbol", - "fallback_scaling", - "TTFA_info", - "x_height_snapping_exceptions", - ] -) -def test_build_info_string_detailed(options, expected): - s = build_info_string(TEST_VERSION, detailed_info=True, **options) - assert s == TEST_INFO + expected - - -@pytest.mark.parametrize( - "suffix, family_name, string, expected", - [ - (b" Hinted", b"New Font", b"New Font", b"New Font Hinted"), - (b" Hinted", b"New Font", b"New Font Condensed", - b"New Font Hinted Condensed"), - (b" Hinted", b"New Font", b"FooBar", - b"FooBar Hinted"), - ], - ids=[ - "is-substring", - "insert-after-substring", - "no-substring", - ] -) -def test_insert_suffix(suffix, family_name, string, expected): - with create_ubyte_buffer(string) as buf: - insert_suffix(suffix, family_name, buf) - new_string = buf.tobytes() - - assert suffix in new_string - assert new_string == expected - - -def test_insert_suffix_overflows(): - s = b"\0" * 0xFFFE - with create_ubyte_buffer(s) as buf: - insert_suffix(b"-H", b"Foo Bar", buf) - new_string = buf.tobytes() - - assert new_string == s diff --git a/tests/test_options.py b/tests/test_options.py index f3a86ad..63532d4 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -2,21 +2,24 @@ from io import StringIO, BytesIO import argparse import os -import logging import pytest from ctypes import c_ulonglong -from ttfautohint._compat import ensure_binary, text_type +from ttfautohint._compat import ensure_binary from ttfautohint.options import ( - validate_options, format_varargs, strong_stem_width, - stdin_or_input_path_type, stdout_or_output_path_type, parse_args, - stem_width_mode, StemWidthMode, _windows_cmdline2list + validate_options, + strong_stem_width, + stdin_or_input_path_type, + stdout_or_output_path_type, + parse_args, + stem_width_mode, + StemWidthMode, + _windows_cmdline2list, ) class TestValidateOptions(object): - def test_no_input(self): with pytest.raises(ValueError, match="No input file"): validate_options({}) @@ -28,8 +31,7 @@ def test_unknown_keyword(self): # 's' for plural kwargs = dict(foo="bar", baz=False) - with pytest.raises(TypeError, - match="unknown keyword arguments: 'foo', 'baz'"): + with pytest.raises(TypeError, match="unknown keyword arguments: 'foo', 'baz'"): validate_options(kwargs) def test_no_info_or_detailed_info(self, tmpdir): @@ -48,18 +50,20 @@ def test_in_file_or_in_buffer(self, tmpdir): def test_control_file_or_control_buffer(self, tmpdir): msg = "control_file and control_buffer are mutually exclusive" control_file = (tmpdir / "ta_ctrl.txt").ensure() - kwargs = dict(in_buffer=b"\0\1\0\0", - control_file=control_file, - control_buffer=b"abcd") + kwargs = dict( + in_buffer=b"\0\1\0\0", control_file=control_file, control_buffer=b"abcd" + ) with pytest.raises(ValueError, match=msg): validate_options(kwargs) def test_reference_file_or_reference_buffer(self, tmpdir): msg = "reference_file and reference_buffer are mutually exclusive" reference_file = (tmpdir / "ref.ttf").ensure() - kwargs = dict(in_buffer=b"\0\1\0\0", - reference_file=reference_file, - reference_buffer=b"\x00\x01\x00\x00") + kwargs = dict( + in_buffer=b"\0\1\0\0", + reference_file=reference_file, + reference_buffer=b"\x00\x01\x00\x00", + ) with pytest.raises(ValueError, match=msg): validate_options(kwargs) @@ -69,115 +73,40 @@ def test_in_file_to_in_buffer(self, tmpdir): in_file.write_binary(data) # 'in_file' is a file-like object - options = validate_options({'in_file': in_file.open(mode="rb")}) + options = validate_options({"in_file": in_file.open(mode="rb")}) assert options["in_buffer"] == data assert "in_file" not in options - assert options["in_buffer_len"] == len(data) # 'in_file' is a path string options = validate_options({"in_file": str(in_file)}) assert options["in_buffer"] == data assert "in_file" not in options - assert options["in_buffer_len"] == len(data) def test_in_buffer_is_bytes(self, tmpdir): with pytest.raises(TypeError, match="in_buffer type must be bytes"): - validate_options({"in_buffer": u"abcd"}) - - def test_control_file_to_control_buffer(self, tmpdir): - control_file = tmpdir / "ta_ctrl.txt" - data = u"abcd" - control_file.write_text(data, encoding="utf-8") - - # 'control_file' is a file object opened in text mode - with control_file.open(mode="rt", encoding="utf-8") as f: - kwargs = {'in_buffer': b"\0", 'control_file': f} - options = validate_options(kwargs) - assert options["control_buffer"] == data.encode("utf-8") - assert "control_file" not in options - assert options["control_buffer_len"] == len(data) - assert options["control_name"] == str(control_file) - - # 'control_file' is a path string - kwargs = {'in_buffer': b"\0", 'control_file': str(control_file)} - options = validate_options(kwargs) - assert options["control_buffer"] == data.encode("utf-8") - assert "control_file" not in options - assert options["control_buffer_len"] == len(data) - assert options["control_name"] == str(control_file) + validate_options({"in_buffer": "abcd"}) - # 'control_file' is a file-like stream - kwargs = {'in_buffer': b"\0", 'control_file': StringIO(data)} - options = validate_options(kwargs) - assert options["control_buffer"] == data.encode("utf-8") - assert "control_file" not in options - assert options["control_buffer_len"] == len(data) - # the stream doesn't have a 'name' attribute; using fallback - assert options["control_name"] == u"" - - def test_control_buffer_name(self, tmpdir): - kwargs = {"in_buffer": b"\0", "control_buffer": b"abcd"} + def test_control_buffer_to_control_file(self, tmpdir): + kwargs = {"in_buffer": b"\0", "control_buffer": "abcd"} options = validate_options(kwargs) - assert options["control_name"] == u"" - def test_reference_file_to_reference_buffer(self, tmpdir): - reference_file = tmpdir / "font.ttf" - data = b"\0\1\0\0" - reference_file.write_binary(data) - encoded_filename = ensure_binary( - str(reference_file), encoding=sys.getfilesystemencoding()) - - # 'reference_file' is a file object opened in binary mode - with reference_file.open(mode="rb") as f: - kwargs = {'in_buffer': b"\0", 'reference_file': f} - options = validate_options(kwargs) - assert options["reference_buffer"] == data - assert "reference_file" not in options - assert options["reference_buffer_len"] == len(data) - assert options["reference_name"] == encoded_filename - - # 'reference_file' is a path string - kwargs = {'in_buffer': b"\0", 'reference_file': str(reference_file)} - options = validate_options(kwargs) - assert options["reference_buffer"] == data - assert "reference_file" not in options - assert options["reference_buffer_len"] == len(data) - assert options["reference_name"] == encoded_filename + assert "control_buffer" not in options + assert isinstance(options["control_file"], str) + with open(options["control_file"], "r") as f: + assert f.read() == "abcd" - # 'reference_file' is a file-like stream - kwargs = {'in_buffer': b"\0", 'reference_file': BytesIO(data)} + def test_reference_buffer_to_reference_file(self, tmpdir): + kwargs = {"in_buffer": b"\0", "reference_buffer": b"\0\1\0\0"} options = validate_options(kwargs) - assert options["reference_buffer"] == data - assert "reference_file" not in options - assert options["reference_buffer_len"] == len(data) - # the stream doesn't have a 'name' attribute, no reference_name - assert options["reference_name"] is None - - def test_custom_reference_name(self, tmpdir): - reference_file = tmpdir / "font.ttf" - data = b"\0\1\0\0" - reference_file.write_binary(data) - expected = u"Some Font".encode(sys.getfilesystemencoding()) - with reference_file.open(mode="rb") as f: - kwargs = {'in_buffer': b"\0", - 'reference_file': f, - 'reference_name': u"Some Font"} - options = validate_options(kwargs) - - assert options["reference_name"] == expected - - kwargs = {'in_buffer': b"\0", - 'reference_file': str(reference_file), - 'reference_name': u"Some Font"} - options = validate_options(kwargs) - - assert options["reference_name"] == expected + assert "reference_buffer" not in options + assert isinstance(options["reference_file"], str) + with open(options["reference_file"], "rb") as f: + assert f.read() == b"\0\1\0\0" def test_reference_buffer_is_bytes(self, tmpdir): - with pytest.raises(TypeError, - match="reference_buffer type must be bytes"): - validate_options({"in_buffer": b"\0", "reference_buffer": u""}) + with pytest.raises(TypeError, match="reference_buffer type must be bytes"): + validate_options({"in_buffer": b"\0", "reference_buffer": ""}) def test_epoch(self): options = validate_options({"in_buffer": b"\0", "epoch": 0}) @@ -185,97 +114,116 @@ def test_epoch(self): assert options["epoch"].value == 0 def test_family_suffix(self): - options = validate_options({"in_buffer": b"\0", - "family_suffix": b"-TA"}) - assert isinstance(options["family_suffix"], text_type) - assert options["family_suffix"] == u"-TA" - - -@pytest.mark.parametrize( - "options, expected", - [ - ( - {}, - (b"", ()) - ), - ( - { - "in_buffer": b"\0\1\0\0", - "in_buffer_len": 4, - "out_buffer": None, - "out_buffer_len": None, - "error_string": None, - "alloc_func": None, - "free_func": None, - "info_callback": None, - "info_post_callback": None, - "progress_callback": None, - "progress_callback_data": None, - "error_callback": None, - "error_callback_data": None, - "control_buffer": b"abcd", - "control_buffer_len": 4, - "reference_buffer": b"\0\1\0\0", - "reference_buffer_len": 4, - "reference_index": 1, - "reference_name": b"/path/to/font.ttf", - "hinting_range_min": 8, - "hinting_range_max": 50, - "hinting_limit": 200, - "hint_composites": False, - "adjust_subglyphs": False, - "increase_x_height": 14, - "x_height_snapping_exceptions": b"6,15-18", - "windows_compatibility": True, - "default_script": b"grek", - "fallback_script": b"latn", - "fallback_scaling": False, - "symbol": True, - "fallback_stem_width": 100, - "ignore_restrictions": True, - "family_suffix": b"-Hinted", - "detailed_info": True, - "no_info": False, - "TTFA_info": True, - "dehint": False, - "epoch": 1513955869, - "debug": False, - "verbose": True, - }, - ((b"TTFA-info, adjust-subglyphs, control-buffer, " - b"control-buffer-len, debug, default-script, dehint, " - b"detailed-info, epoch, fallback-scaling, fallback-script, " - b"fallback-stem-width, family-suffix, hint-composites, " - b"hinting-limit, hinting-range-max, hinting-range-min, " - b"ignore-restrictions, in-buffer, in-buffer-len, " - b"increase-x-height, no-info, reference-buffer, " - b"reference-buffer-len, reference-index, reference-name, " - b"symbol, verbose, windows-compatibility, " - b"x-height-snapping-exceptions"), - (True, False, b'abcd', - 4, False, b'grek', False, - True, 1513955869, False, b'latn', - 100, b'-Hinted', False, - 200, 50, 8, - True, b'\x00\x01\x00\x00', 4, - 14, False, b'\x00\x01\x00\x00', - 4, 1, b'/path/to/font.ttf', - True, True, True, - b'6,15-18')) - ), - ( - {"unkown_option": 1}, - (b"", ()) - ) - ], - ids=[ - "empty", - "full-options", - "unknown-option", - ] -) -def test_format_varargs(options, expected): - assert format_varargs(**options) == expected + options = validate_options({"in_buffer": b"\0", "family_suffix": b"-TA"}) + assert isinstance(options["family_suffix"], str) + assert options["family_suffix"] == "-TA" + + +# @pytest.mark.parametrize( +# "options, expected", +# [ +# ({}, (b"", ())), +# ( +# { +# "in_buffer": b"\0\1\0\0", +# "in_buffer_len": 4, +# "out_buffer": None, +# "out_buffer_len": None, +# "error_string": None, +# "alloc_func": None, +# "free_func": None, +# "info_callback": None, +# "info_post_callback": None, +# "progress_callback": None, +# "progress_callback_data": None, +# "error_callback": None, +# "error_callback_data": None, +# "control_buffer": b"abcd", +# "control_buffer_len": 4, +# "reference_buffer": b"\0\1\0\0", +# "reference_buffer_len": 4, +# "reference_index": 1, +# "reference_name": b"/path/to/font.ttf", +# "hinting_range_min": 8, +# "hinting_range_max": 50, +# "hinting_limit": 200, +# "hint_composites": False, +# "adjust_subglyphs": False, +# "increase_x_height": 14, +# "x_height_snapping_exceptions": b"6,15-18", +# "windows_compatibility": True, +# "default_script": b"grek", +# "fallback_script": b"latn", +# "fallback_scaling": False, +# "symbol": True, +# "fallback_stem_width": 100, +# "ignore_restrictions": True, +# "family_suffix": b"-Hinted", +# "detailed_info": True, +# "no_info": False, +# "TTFA_info": True, +# "dehint": False, +# "epoch": 1513955869, +# "debug": False, +# "verbose": True, +# }, +# ( +# ( +# b"TTFA-info, adjust-subglyphs, control-buffer, " +# b"control-buffer-len, debug, default-script, dehint, " +# b"detailed-info, epoch, fallback-scaling, fallback-script, " +# b"fallback-stem-width, family-suffix, hint-composites, " +# b"hinting-limit, hinting-range-max, hinting-range-min, " +# b"ignore-restrictions, in-buffer, in-buffer-len, " +# b"increase-x-height, no-info, reference-buffer, " +# b"reference-buffer-len, reference-index, reference-name, " +# b"symbol, verbose, windows-compatibility, " +# b"x-height-snapping-exceptions" +# ), +# ( +# True, +# False, +# b"abcd", +# 4, +# False, +# b"grek", +# False, +# True, +# 1513955869, +# False, +# b"latn", +# 100, +# b"-Hinted", +# False, +# 200, +# 50, +# 8, +# True, +# b"\x00\x01\x00\x00", +# 4, +# 14, +# False, +# b"\x00\x01\x00\x00", +# 4, +# 1, +# b"/path/to/font.ttf", +# True, +# True, +# True, +# b"6,15-18", +# ), +# ), +# ), +# ({"unkown_option": 1}, (b"", ())), +# ], +# ids=[ +# "empty", +# "full-options", +# "unknown-option", +# ], +# ) +# def test_format_varargs(options, expected): +# assert format_varargs(**options) == expected @pytest.mark.parametrize( @@ -286,61 +234,55 @@ def test_format_varargs(options, expected): { "gray_stem_width_mode": StemWidthMode.QUANTIZED, "gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, - "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED - } + "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, + }, ), ( "g", { "gray_stem_width_mode": StemWidthMode.STRONG, "gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, - "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED - } + "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, + }, ), ( "G", { "gray_stem_width_mode": StemWidthMode.QUANTIZED, "gdi_cleartype_stem_width_mode": StemWidthMode.STRONG, - "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED - } + "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, + }, ), ( "D", { "gray_stem_width_mode": StemWidthMode.QUANTIZED, "gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, - "dw_cleartype_stem_width_mode": StemWidthMode.STRONG - } + "dw_cleartype_stem_width_mode": StemWidthMode.STRONG, + }, ), ( "DGg", { "gray_stem_width_mode": StemWidthMode.STRONG, "gdi_cleartype_stem_width_mode": StemWidthMode.STRONG, - "dw_cleartype_stem_width_mode": StemWidthMode.STRONG - } + "dw_cleartype_stem_width_mode": StemWidthMode.STRONG, + }, ), ], - ids=[ - "empty-string", - "only-gray", - "only-gdi", - "only-dw", - "all" - ] + ids=["empty-string", "only-gray", "only-gdi", "only-dw", "all"], ) def test_strong_stem_width(string, expected): assert strong_stem_width(string) == expected def test_strong_stem_width_invalid(): - with pytest.raises(argparse.ArgumentTypeError, - match="string can only contain up to 3 letters"): + with pytest.raises( + argparse.ArgumentTypeError, match="string can only contain up to 3 letters" + ): strong_stem_width("GGGG") - with pytest.raises(argparse.ArgumentTypeError, - match="invalid value: 'a'"): + with pytest.raises(argparse.ArgumentTypeError, match="invalid value: 'a'"): strong_stem_width("a") @@ -352,60 +294,61 @@ def test_strong_stem_width_invalid(): { "gray_stem_width_mode": StemWidthMode.NATURAL, "gdi_cleartype_stem_width_mode": StemWidthMode.NATURAL, - "dw_cleartype_stem_width_mode": StemWidthMode.NATURAL - } + "dw_cleartype_stem_width_mode": StemWidthMode.NATURAL, + }, ), ( "qqq", { "gray_stem_width_mode": StemWidthMode.QUANTIZED, "gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, - "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED - } + "dw_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, + }, ), ( "sss", { "gray_stem_width_mode": StemWidthMode.STRONG, "gdi_cleartype_stem_width_mode": StemWidthMode.STRONG, - "dw_cleartype_stem_width_mode": StemWidthMode.STRONG - } + "dw_cleartype_stem_width_mode": StemWidthMode.STRONG, + }, ), ( "nqs", { "gray_stem_width_mode": StemWidthMode.NATURAL, "gdi_cleartype_stem_width_mode": StemWidthMode.QUANTIZED, - "dw_cleartype_stem_width_mode": StemWidthMode.STRONG - } + "dw_cleartype_stem_width_mode": StemWidthMode.STRONG, + }, ), ], - ids=["nnn", "qqq", "sss", "nqs"] + ids=["nnn", "qqq", "sss", "nqs"], ) def test_stem_width_mode(string, expected): assert stem_width_mode(string) == expected def test_stem_width_mode_invalid(): - with pytest.raises(argparse.ArgumentTypeError, - match="must consist of exactly three letters"): + with pytest.raises( + argparse.ArgumentTypeError, match="must consist of exactly three letters" + ): stem_width_mode("nnnn") - with pytest.raises(argparse.ArgumentTypeError, - match="Stem width mode letter for .* must be"): + with pytest.raises( + argparse.ArgumentTypeError, match="Stem width mode letter for .* must be" + ): stem_width_mode("zzz") @pytest.fixture( params=[True, False], - ids=['tty', 'pipe'], + ids=["tty", "pipe"], ) def isatty(request): return request.param class MockFile(object): - def __init__(self, f, isatty): self._file = f self._isatty = isatty @@ -460,7 +403,6 @@ def test_path_output_type(tmpdir): class TestParseArgs(object): - argv0 = "python -m ttfautohint" def test_unrecognized_arguments(self, monkeypatch, capsys): @@ -520,8 +462,7 @@ def test_source_date_epoch_invalid(self, monkeypatch): env["SOURCE_DATE_EPOCH"] = invalid_epoch monkeypatch.setattr(os, "environ", env) - with pytest.warns(UserWarning, - match="invalid SOURCE_DATE_EPOCH: 'foobar'"): + with pytest.warns(UserWarning, match="invalid SOURCE_DATE_EPOCH: 'foobar'"): options = parse_args([]) assert "epoch" not in options diff --git a/tests/test_ttfautohint.py b/tests/test_ttfautohint.py index 44b8552..fb24879 100644 --- a/tests/test_ttfautohint.py +++ b/tests/test_ttfautohint.py @@ -22,22 +22,18 @@ def autohint_font(ttfont, **options): return TTFont(BytesIO(data)) -@pytest.fixture( - params=UNHINTED_TTFS, - ids=lambda p: os.path.basename(p) -) +@pytest.fixture(params=UNHINTED_TTFS, ids=lambda p: os.path.basename(p)) def unhinted(request): return TTFont(request.param) class TestTTFAutohint(object): - def test_simple(self, unhinted): for tag in GLOBAL_HINTING_TABLES: assert tag not in unhinted assert not unhinted["glyf"]["a"].program nameID5 = unhinted["name"].getName(5, 3, 1).toUnicode() - assert u"; ttfautohint" not in nameID5 + assert "; ttfautohint" not in nameID5 hinted = autohint_font(unhinted) @@ -46,7 +42,7 @@ def test_simple(self, unhinted): assert hinted["glyf"]["a"].program nameID5 = hinted["name"].getName(5, 3, 1).toUnicode() - assert u"; ttfautohint" in nameID5 + assert "; ttfautohint" in nameID5 def test_in_and_out_file_paths(self, tmpdir, unhinted): in_file = tmpdir / "unhinted.ttf" @@ -55,25 +51,23 @@ def test_in_and_out_file_paths(self, tmpdir, unhinted): with in_file.open("wb") as f: unhinted.save(f) - n = ttfautohint(in_file=str(in_file), out_file=str(out_file)) + ttfautohint(in_file=str(in_file), out_file=str(out_file)) - assert n > 0 + assert os.path.getsize(str(out_file)) > 0 def test_no_info(self, unhinted): hinted = autohint_font(unhinted, no_info=True) nameID5 = hinted["name"].getName(5, 3, 1).toUnicode() - assert u"; ttfautohint" not in nameID5 + assert "; ttfautohint" not in nameID5 def test_detailed_info(self, unhinted): hinted = autohint_font(unhinted, detailed_info=True) nameID5 = hinted["name"].getName(5, 3, 1).toUnicode() - assert u"; ttfautohint" in nameID5 + assert "; ttfautohint" in nameID5 - assert ( - u' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -X ""' - in nameID5) + assert ' -l 8 -r 50 -G 200 -x 14 -D latn -f none -a qsq -X ""' in nameID5 def test_family_suffix(self, unhinted): suffix = " Hinted"