├── tests ├── test_files │ ├── text │ │ ├── byte.txt │ │ ├── test.txt │ │ └── kb.txt │ └── fonts │ │ ├── OpenSans-VF.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Ubuntu-Regular.ttf │ │ ├── NotoSans-Regular.ttf │ │ ├── NotoSans-Regular-dehinted.ttf │ │ ├── Roboto-Regular-dehinted.ttf │ │ ├── README.md │ │ └── licenses │ │ ├── OpenSans-License.txt │ │ ├── Noto-License.txt │ │ ├── Ubuntu-License.txt │ │ └── Roboto-License.txt ├── test_bitops.py ├── test_system.py ├── test_paths.py ├── test_font.py └── test_main.py ├── MANIFEST.in ├── tox.ini ├── .github ├── dependabot.yml └── workflows │ ├── py-typecheck.yml │ ├── py-lint.yml │ ├── publish-release.yml │ ├── py-ci.yml │ ├── py-coverage.yml │ └── codeql-analysis.yml ├── requirements.txt ├── docs └── DEVDOCS.md ├── setup.cfg ├── codecov.yml ├── lib └── dehinter │ ├── __init__.py │ ├── bitops.py │ ├── paths.py │ ├── system.py │ ├── __main__.py │ └── font.py ├── Makefile ├── .gitignore ├── setup.py ├── CHANGELOG.md ├── README.md └── LICENSE /tests/test_files/text/byte.txt: -------------------------------------------------------------------------------- 1 | c -------------------------------------------------------------------------------- /tests/test_files/text/test.txt: -------------------------------------------------------------------------------- 1 | test file -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include LICENSE 3 | include README.md 4 | 5 | include *requirements.txt -------------------------------------------------------------------------------- /tests/test_files/fonts/OpenSans-VF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/OpenSans-VF.ttf -------------------------------------------------------------------------------- /tests/test_files/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /tests/test_files/fonts/Ubuntu-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/Ubuntu-Regular.ttf -------------------------------------------------------------------------------- /tests/test_files/fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310 3 | 4 | [testenv] 5 | commands = 6 | py.test 7 | deps = 8 | -rrequirements.txt 9 | pytest 10 | -------------------------------------------------------------------------------- /tests/test_files/fonts/NotoSans-Regular-dehinted.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/NotoSans-Regular-dehinted.ttf -------------------------------------------------------------------------------- /tests/test_files/fonts/Roboto-Regular-dehinted.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/dehinter/HEAD/tests/test_files/fonts/Roboto-Regular-dehinted.ttf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | fonttools==4.38.0 # via dehinter (setup.py) 8 | -------------------------------------------------------------------------------- /docs/DEVDOCS.md: -------------------------------------------------------------------------------- 1 | # Developer Documentation 2 | 3 | ## Versioning 4 | 5 | Version numbers are set in the `dehinter.__init__.py` file and read into the `setup.py` file and the executable from this file. -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 0 3 | 4 | [tool:pytest] 5 | filterwarnings = 6 | ignore:fromstring:DeprecationWarning 7 | ignore:tostring:DeprecationWarning 8 | 9 | [flake8] 10 | max-line-length = 90 -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | max_report_age: off 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | # basic 9 | target: auto 10 | threshold: 2% 11 | base: auto 12 | 13 | comment: off 14 | -------------------------------------------------------------------------------- /tests/test_bitops.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dehinter.bitops import is_bit_k_set, clear_bit_k, set_bit_k 4 | 5 | 6 | def test_is_bit_k_set_true(): 7 | assert is_bit_k_set(15, 1) is True 8 | 9 | 10 | def test_is_bit_k_set_false(): 11 | assert is_bit_k_set(13, 1) is False 12 | 13 | 14 | def test_clear_bit_k(): 15 | # clearing bit 1 on 0b1111 is 0b1101 = 13 16 | assert clear_bit_k(15, 1) == 13 17 | 18 | 19 | def test_set_bit_k(): 20 | # setting bit 1 on 0b1101 is 0b1111 = 15 21 | assert set_bit_k(13, 1) == 15 22 | -------------------------------------------------------------------------------- /tests/test_system.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dehinter.system import get_filesize 4 | 5 | 6 | def test_get_filesize_bytes(): 7 | byte_file = os.path.join("tests", "test_files", "text", "byte.txt") 8 | assert get_filesize(byte_file) == ("1.00", "B") 9 | 10 | 11 | def test_get_filesize_kilobytes(): 12 | kilobyte_file = os.path.join("tests", "test_files", "text", "kb.txt") 13 | assert get_filesize(kilobyte_file) == ("1.00", "KB") 14 | 15 | 16 | def test_get_filesize_megabytes(): 17 | megabyte_file = os.path.join("tests", "test_files", "text", "mb.txt") 18 | assert get_filesize(megabyte_file) == ("1.00", "MB") 19 | -------------------------------------------------------------------------------- /.github/workflows/py-typecheck.yml: -------------------------------------------------------------------------------- 1 | name: Python Type Checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | python-version: ["3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install Python testing dependencies 20 | run: pip install --upgrade mypy 21 | - name: Static type checks 22 | run: mypy lib/dehinter 23 | -------------------------------------------------------------------------------- /lib/dehinter/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version = __version__ = "4.0.0" 16 | -------------------------------------------------------------------------------- /.github/workflows/py-lint.yml: -------------------------------------------------------------------------------- 1 | name: Python Lints 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | python-version: ["3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install Python testing dependencies 20 | run: pip install --upgrade flake8 21 | - name: flake8 Lint 22 | uses: py-actions/flake8@v1 23 | with: 24 | max-line-length: "90" 25 | path: "lib/dehinter" 26 | -------------------------------------------------------------------------------- /tests/test_files/fonts/README.md: -------------------------------------------------------------------------------- 1 | ## Test Font Licenses 2 | 3 | The fonts used in the testing of this project include the following: 4 | 5 | 1. OpenSans-VF.ttf 6 | 2. Roboto-Regular.ttf 7 | 3. Roboto-Regular-dehinted.ttf (ttfautohint dehinted derivative of Roboto-Regular.ttf) 8 | 4. NotoSans-Regular.ttf 9 | 5. NotoSans-Regular-dehinted.ttf (ttfautohint dehinted derivative of NotoSans-Regular.ttf) 10 | 6. Ubuntu-Regular.ttf 11 | 12 | Open Sans is licensed under the [SIL Open Font License 1.1](https://github.com/googlefonts/opensans/blob/main/OFL.txt) 13 | 14 | Roboto is licensed under the [Apache License v2.0](https://github.com/google/roboto/blob/master/LICENSE). 15 | 16 | Noto Sans is licensed under the [SIL Open Font License 1.1](https://github.com/googlefonts/noto-fonts/blob/master/LICENSE). 17 | 18 | Ubuntu is licensed under the [Ubuntu Font License](https://ubuntu.com/legal/font-licence). 19 | 20 | You may find the full text of these licenses in the `licenses` subdirectory. 21 | -------------------------------------------------------------------------------- /tests/test_paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dehinter.paths import filepath_exists, get_default_out_path 4 | 5 | import pytest 6 | 7 | 8 | def test_filepath_exists_good_filepath(): 9 | good_path = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 10 | assert filepath_exists(good_path) is True 11 | 12 | 13 | def test_filepath_exists_bad_filepath(): 14 | bad_path = "bogus_file.txt" 15 | assert filepath_exists(bad_path) is False 16 | 17 | 18 | def test_get_default_filepath_without_dir(): 19 | path = "Roboto-Regular.ttf" 20 | default_path = get_default_out_path(path) 21 | assert default_path == "Roboto-Regular-dehinted.ttf" 22 | 23 | 24 | def test_get_default_filepath_with_dir(): 25 | path = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 26 | default_path = get_default_out_path(path) 27 | assert default_path == os.path.join( 28 | "tests", "test_files", "fonts", "Roboto-Regular-dehinted.ttf" 29 | ) 30 | -------------------------------------------------------------------------------- /tests/test_files/text/kb.txt: -------------------------------------------------------------------------------- 1 | ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install 2 | 3 | black: 4 | black lib/dehinter/*.py 5 | 6 | import-sort: 7 | isort lib/dehinter/*.py 8 | 9 | format: import-sort black 10 | 11 | clean: 12 | - rm dist/*.whl dist/*.tar.gz dist/*.zip 13 | 14 | dist-build: clean 15 | python3 setup.py sdist bdist_wheel 16 | 17 | dist-push: 18 | twine upload dist/*.whl dist/*.tar.gz 19 | 20 | install: 21 | pip3 install --ignore-installed -r requirements.txt . 22 | 23 | install-dev: 24 | pip3 install --ignore-installed -r requirements.txt -e ".[dev]" 25 | 26 | install-user: 27 | pip3 install --ignore-installed --user . 28 | 29 | test: test-lint test-type-check test-unit 30 | 31 | test-coverage: 32 | coverage run --source dehinter -m py.test 33 | coverage report -m 34 | # coverage html 35 | 36 | test-lint: 37 | flake8 --ignore=E501,W50 lib/dehinter 38 | 39 | test-type-check: 40 | mypy lib/dehinter 41 | 42 | test-unit: 43 | tox 44 | 45 | uninstall: 46 | pip3 uninstall --yes dehinter 47 | 48 | .PHONY: all black clean dist-build dist-push install install-dev install-user test test-lint test-type-check test-unit uninstall -------------------------------------------------------------------------------- /lib/dehinter/bitops.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def is_bit_k_set(int_val: int, k: int) -> bool: 17 | """Tests if the value of bit at offset k in an integer is set""" 18 | return (int_val & (1 << k)) != 0 19 | 20 | 21 | def clear_bit_k(int_val: int, k: int) -> int: 22 | """Clears the bit at offset k""" 23 | return int_val & ~(1 << k) 24 | 25 | 26 | def set_bit_k(int_val: int, k: int) -> int: 27 | """Sets the bit at offset k""" 28 | return int_val | (1 << k) 29 | -------------------------------------------------------------------------------- /lib/dehinter/paths.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from typing import Union 17 | 18 | 19 | def filepath_exists(filepath: Union[bytes, str, "os.PathLike[str]"]) -> bool: 20 | """Tests a file path string to confirm that the file exists on the file system""" 21 | return os.path.isfile(filepath) 22 | 23 | 24 | def get_default_out_path( 25 | filepath: os.PathLike, 26 | ) -> Union[str, bytes]: 27 | """Returns an updated file path that is used as dehinted file default when user 28 | does not specify an out file path.""" 29 | dir_path, file_path = os.path.split(filepath) 30 | file_name, file_extension = os.path.splitext(file_path) 31 | default_file_name = file_name + "-dehinted" + file_extension 32 | return os.path.join(dir_path, default_file_name) 33 | -------------------------------------------------------------------------------- /lib/dehinter/system.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from typing import Tuple, Union 17 | 18 | 19 | def get_filesize(filepath: Union[str, bytes, "os.PathLike[str]"]) -> Tuple[str, str]: 20 | """Returns formatted file size tuple fit for printing to end user.""" 21 | filesize = os.path.getsize(filepath) 22 | kb_factor = 1 << 10 23 | mb_factor = 1 << 20 24 | 25 | if filesize < kb_factor: 26 | size_string = "B" 27 | formatted_filesize = float(filesize) 28 | elif kb_factor <= filesize < mb_factor: 29 | size_string = "KB" 30 | formatted_filesize = filesize / float(kb_factor) 31 | else: 32 | size_string = "MB" 33 | formatted_filesize = filesize / float(mb_factor) 34 | 35 | return "{0:.2f}".format(formatted_filesize), size_string 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Create and Publish Release 8 | 9 | jobs: 10 | build: 11 | name: Create and Publish Release 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.10"] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install --upgrade setuptools wheel twine 26 | - name: Create GitHub release 27 | id: create_release 28 | uses: actions/create-release@v1 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | with: 32 | tag_name: ${{ github.ref }} 33 | release_name: ${{ github.ref }} 34 | body: | 35 | Please see the root of the repository for the CHANGELOG.md 36 | draft: false 37 | prerelease: false 38 | - name: Build and publish to PyPI 39 | env: 40 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 41 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 42 | run: | 43 | make dist-build 44 | twine upload dist/* 45 | -------------------------------------------------------------------------------- /.github/workflows/py-ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Display Python version & architecture 20 | run: | 21 | python -c "import sys; print(sys.version)" 22 | python -c "import struct; print(struct.calcsize('P') * 8)" 23 | - name: Get pip cache dir 24 | id: pip-cache 25 | run: | 26 | pip install --upgrade pip 27 | echo "::set-output name=dir::$(pip cache dir)" 28 | - name: pip cache 29 | uses: actions/cache@v2 30 | with: 31 | path: ${{ steps.pip-cache.outputs.dir }} 32 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | - name: Install Python testing dependencies 36 | run: pip install --upgrade pytest 37 | - name: Install Python project dependencies 38 | uses: py-actions/py-dependency-install@v2 39 | with: 40 | update-pip: "true" 41 | update-setuptools: "true" 42 | update-wheel: "true" 43 | - name: Install project 44 | run: pip install -r requirements.txt . 45 | - name: Python unit tests 46 | run: pytest 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # macOS specific 106 | .DS_Store 107 | 108 | # PyCharm 109 | .idea 110 | 111 | # pytype 112 | .pytype 113 | 114 | .vscode -------------------------------------------------------------------------------- /.github/workflows/py-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | python-version: ["3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Display Python version & architecture 20 | run: | 21 | python -c "import sys; print(sys.version)" 22 | python -c "import struct; print(struct.calcsize('P') * 8)" 23 | - name: Get pip cache dir 24 | id: pip-cache 25 | run: | 26 | pip install --upgrade pip 27 | echo "::set-output name=dir::$(pip cache dir)" 28 | - name: pip cache 29 | uses: actions/cache@v2 30 | with: 31 | path: ${{ steps.pip-cache.outputs.dir }} 32 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | - name: Install Python testing dependencies 36 | run: pip install --upgrade pytest 37 | - name: Install Python project dependencies 38 | uses: py-actions/py-dependency-install@v2 39 | with: 40 | update-pip: "true" 41 | update-setuptools: "true" 42 | update-wheel: "true" 43 | - name: Install project 44 | run: pip install . 45 | - name: Generate coverage report 46 | run: | 47 | pip install --upgrade coverage 48 | coverage run --source dehinter -m py.test 49 | coverage report -m 50 | coverage xml 51 | - name: Upload coverage to Codecov 52 | uses: codecov/codecov-action@v1 53 | with: 54 | file: ./coverage.xml 55 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '39 23 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import sys 4 | from setuptools import setup, find_packages 5 | 6 | # Package meta-data. 7 | NAME = "dehinter" 8 | DESCRIPTION = "A tool for the removal of TrueType instruction sets (hints) in fonts" 9 | LICENSE = "Apache License v2.0" 10 | URL = "https://github.com/source-foundry/dehinter" 11 | EMAIL = "chris@sourcefoundry.org" 12 | AUTHOR = "Source Foundry Authors and Contributors" 13 | REQUIRES_PYTHON = ">=3.7.0" 14 | 15 | INSTALL_REQUIRES = [ 16 | "fontTools", 17 | ] 18 | # Optional packages 19 | EXTRAS_REQUIRES = { 20 | # for developer installs 21 | "dev": ["coverage", "pytest", "tox", "flake8", "mypy", "isort"], 22 | # for maintainer installs 23 | "maintain": ["wheel", "setuptools", "twine"], 24 | } 25 | 26 | this_file_path = os.path.abspath(os.path.dirname(__file__)) 27 | 28 | # Version 29 | main_namespace = {} 30 | version_fp = os.path.join(this_file_path, "lib", "dehinter", "__init__.py") 31 | try: 32 | with io.open(version_fp) as v: 33 | exec(v.read(), main_namespace) 34 | except IOError as version_e: 35 | sys.stderr.write( 36 | "[ERROR] setup.py: Failed to read the version data for the version definition: {}".format( 37 | str(version_e) 38 | ) 39 | ) 40 | raise version_e 41 | 42 | # Use repository Markdown README.md for PyPI long description 43 | try: 44 | with io.open("README.md", encoding="utf-8") as f: 45 | readme = f.read() 46 | except IOError as readme_e: 47 | sys.stderr.write( 48 | "[ERROR] setup.py: Failed to read the README.md file for the long description definition: {}".format( 49 | str(readme_e) 50 | ) 51 | ) 52 | raise readme_e 53 | 54 | setup( 55 | name=NAME, 56 | version=main_namespace["__version__"], 57 | description=DESCRIPTION, 58 | author=AUTHOR, 59 | author_email=EMAIL, 60 | url=URL, 61 | license=LICENSE, 62 | platforms=["Any"], 63 | long_description=readme, 64 | long_description_content_type="text/markdown", 65 | package_dir={"": "lib"}, 66 | packages=find_packages("lib"), 67 | include_package_data=True, 68 | install_requires=INSTALL_REQUIRES, 69 | extras_require=EXTRAS_REQUIRES, 70 | python_requires=REQUIRES_PYTHON, 71 | entry_points={"console_scripts": ["dehinter = dehinter.__main__:main"]}, 72 | classifiers=[ 73 | "Development Status :: 5 - Production/Stable", 74 | "Environment :: Console", 75 | "Environment :: Other Environment", 76 | "Intended Audience :: Developers", 77 | "Intended Audience :: End Users/Desktop", 78 | "License :: OSI Approved :: Apache Software License", 79 | "Natural Language :: English", 80 | "Operating System :: OS Independent", 81 | "Programming Language :: Python", 82 | "Programming Language :: Python :: 3", 83 | "Programming Language :: Python :: 3.7", 84 | "Programming Language :: Python :: 3.8", 85 | "Programming Language :: Python :: 3.9", 86 | "Programming Language :: Python :: 3.10", 87 | "Topic :: Text Processing :: Fonts", 88 | "Topic :: Multimedia :: Graphics", 89 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 90 | ], 91 | ) 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.0.0 4 | 5 | - remove Python 3.6 support (this was eliminated in our fontTools dependency) 6 | - update fonttools dependency to v4.28.1 7 | - update GitHub Actions CI workflow to include Python 3.10 testing 8 | - update GitHub Actions workflows to use cPython 3.10 runner by default 9 | 10 | ## v3.1.0 11 | 12 | - add a `dehinter.font.dehint` function to be used by programs that import dehinter as a module 13 | - add `--quiet` flag to suppress standard output reporting 14 | - update fonttools dependency to v4.22.1 15 | 16 | ## v3.0.0 17 | 18 | - add support for cvar table removal in variable fonts (backward incompatible change) 19 | - add new `--keep-cvar` option to toggle cvar table removal off 20 | - modify default gasp approach to add a gasp table when it is not available in a font (backward incompatible change) 21 | - add new source formatting Makefile targets 22 | - add isort package to setup.py extras_requires dev dependencies 23 | - source import statement formatting 24 | - update fonttools dependency to v4.22.0 25 | - update GitHub Actions workflows to use cPython 3.9 (from cPython 3.8) 26 | 27 | ## v2.0.4 28 | 29 | - dependency update patch 30 | - update fonttools dependency to v4.17.1 31 | 32 | ## v2.0.3 33 | 34 | - add cPython 3.9 interpreter testing 35 | - add CodeQL static source testing 36 | - update fonttools dependency to v4.16.1 37 | 38 | ## v2.0.2 39 | 40 | - refactor string formatting to f-strings 41 | - added GitHub Action CI workflows 42 | - removed Travis CI testing 43 | - removed Appveyor CI testing 44 | 45 | ## v2.0.1 46 | 47 | - update setup.py classifiers to properly define this project as status 5: Production / Stable 48 | - update README.md with transition to `mypy` as our static type check tool 49 | 50 | ## v2.0.0 51 | 52 | Backwards incompatible gasp table change introduced in this release 53 | 54 | - modified dehinted gasp table definition to grayscale, symmetric smoothing behavior (bit flag 0x000A). The previous default was bit flag 0x000f which defines gridfit, grayscale, symmetric gridfit, symmetric smoothing. Our previous default is the *default* behavior in `ttfautohint -d` to our knowledge. This change is a departure from the `ttfautohint -d` default behavior. (pull request #39, thanks Aaron!) 55 | - added type hints and mypy static type checking 56 | - updated fontTools dependency to v4.14.0 57 | - black source formatting applied to Python sources 58 | 59 | ## v1.0.0 60 | 61 | - updated fontTools and associated dependencies to v4.6.0 release 62 | - this update adds Unicode 13 support 63 | - add Python3.8 CI testing support 64 | 65 | ## v0.4.3 66 | 67 | - escalated fontTools dependency version to v4.2.4 68 | 69 | ## v0.4.2 70 | 71 | - fix Travis CI testing error on macOS platform with move to `pip3` from `pip` 72 | - update fontTools dependency to v4.0.1 73 | 74 | ## v0.4.1 75 | 76 | - fix Makefile uninstall target error 77 | - PEP8 source formatting edits 78 | 79 | ## v0.4.0 80 | 81 | - changed min Python version to Python 3.6+ interpreters 82 | - added support for default VDMX table removal 83 | - added `--keep-vdmx` option to keep original VDMX table 84 | - updated fontTools dependency to v4.0.0 85 | - changed configuration to build wheels for Py3 only 86 | - setup.py file updated with new classifiers 87 | - added new Ubuntu-Regular.ttf testing file with the UFL license 88 | - added the Roboto testing file license 89 | 90 | ## v0.3.0 91 | 92 | - modified `glyf` table instruction set bytecode removal approach for composite glyphs 93 | - fixed: `head` table flags bit 4 is now cleared only when `hdmx` and `LTSH` tables are removed (or were not present and bit is set in the font) 94 | 95 | ## v0.2.0 96 | 97 | - add `--keep-cvt` option to keep original cvt table 98 | - add `--keep-fpgm` option to keep original fpgm table 99 | - add `--keep-gasp` option to keep original gasp table 100 | - add `--keep-glyf` option to keep original glyf table 101 | - add `--keep-hdmx` option to keep original hdmx table 102 | - add `--keep-head` option to keep original head table 103 | - add `--keep-ltsh` option to keep original LTSH table 104 | - add `--keep-maxp` option to keep original maxp table 105 | - add `--keep-prep` option to keep original prep table 106 | - add `--keep-ttfa` option to keep original TTFA table 107 | 108 | ## v0.1.0 109 | 110 | - initial beta release 111 | 112 | ## v0.0.1 113 | 114 | - pre-release for PyPI naming 115 | -------------------------------------------------------------------------------- /tests/test_files/fonts/licenses/OpenSans-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) 2 | 3 | ----------------------------------------------------------- 4 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 5 | ----------------------------------------------------------- 6 | 7 | PREAMBLE 8 | The goals of the Open Font License (OFL) are to stimulate worldwide 9 | development of collaborative font projects, to support the font 10 | creation efforts of academic and linguistic communities, and to 11 | provide a free and open framework in which fonts may be shared and 12 | improved in partnership with others. 13 | 14 | The OFL allows the licensed fonts to be used, studied, modified and 15 | redistributed freely as long as they are not sold by themselves. The 16 | fonts, including any derivative works, can be bundled, embedded, 17 | redistributed and/or sold with any software provided that any reserved 18 | names are not used by derivative works. The fonts and derivatives, 19 | however, cannot be released under any other type of license. The 20 | requirement for fonts to remain under this license does not apply to 21 | any document created using the fonts or their derivatives. 22 | 23 | DEFINITIONS 24 | "Font Software" refers to the set of files released by the Copyright 25 | Holder(s) under this license and clearly marked as such. This may 26 | include source files, build scripts and documentation. 27 | 28 | "Reserved Font Name" refers to any names specified as such after the 29 | copyright statement(s). 30 | 31 | "Original Version" refers to the collection of Font Software 32 | components as distributed by the Copyright Holder(s). 33 | 34 | "Modified Version" refers to any derivative made by adding to, 35 | deleting, or substituting -- in part or in whole -- any of the 36 | components of the Original Version, by changing formats or by porting 37 | the Font Software to a new environment. 38 | 39 | "Author" refers to any designer, engineer, programmer, technical 40 | writer or other person who contributed to the Font Software. 41 | 42 | PERMISSION & CONDITIONS 43 | Permission is hereby granted, free of charge, to any person obtaining 44 | a copy of the Font Software, to use, study, copy, merge, embed, 45 | modify, redistribute, and sell modified and unmodified copies of the 46 | Font Software, subject to the following conditions: 47 | 48 | 1) Neither the Font Software nor any of its individual components, in 49 | Original or Modified Versions, may be sold by itself. 50 | 51 | 2) Original or Modified Versions of the Font Software may be bundled, 52 | redistributed and/or sold with any software, provided that each copy 53 | contains the above copyright notice and this license. These can be 54 | included either as stand-alone text files, human-readable headers or 55 | in the appropriate machine-readable metadata fields within text or 56 | binary files as long as those fields can be easily viewed by the user. 57 | 58 | 3) No Modified Version of the Font Software may use the Reserved Font 59 | Name(s) unless explicit written permission is granted by the 60 | corresponding Copyright Holder. This restriction only applies to the 61 | primary font name as presented to the users. 62 | 63 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 64 | Software shall not be used to promote, endorse or advertise any 65 | Modified Version, except to acknowledge the contribution(s) of the 66 | Copyright Holder(s) and the Author(s) or with their explicit written 67 | permission. 68 | 69 | 5) The Font Software, modified or unmodified, in part or in whole, 70 | must be distributed entirely under this license, and must not be 71 | distributed under any other license. The requirement for fonts to 72 | remain under this license does not apply to any document created using 73 | the Font Software. 74 | 75 | TERMINATION 76 | This license becomes null and void if any of the above conditions are 77 | not met. 78 | 79 | DISCLAIMER 80 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 81 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 82 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 83 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 84 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 85 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 86 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 87 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 88 | OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /tests/test_files/fonts/licenses/Noto-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts) 2 | 3 | This Font Software is licensed under the SIL Open Font License, 4 | Version 1.1. 5 | 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font 16 | creation efforts of academic and linguistic communities, and to 17 | provide a free and open framework in which fonts may be shared and 18 | improved in partnership with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply to 27 | any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software 38 | components as distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, 41 | deleting, or substituting -- in part or in whole -- any of the 42 | components of the Original Version, by changing formats or by porting 43 | the Font Software to a new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, 51 | modify, redistribute, and sell modified and unmodified copies of the 52 | Font Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, in 55 | Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the 66 | corresponding Copyright Holder. This restriction only applies to the 67 | primary font name as presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created using 79 | the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /tests/test_files/fonts/licenses/Ubuntu-License.txt: -------------------------------------------------------------------------------- 1 | ------------------------------- 2 | UBUNTU FONT LICENCE Version 1.0 3 | ------------------------------- 4 | 5 | PREAMBLE 6 | This licence allows the licensed fonts to be used, studied, modified and 7 | redistributed freely. The fonts, including any derivative works, can be 8 | bundled, embedded, and redistributed provided the terms of this licence 9 | are met. The fonts and derivatives, however, cannot be released under 10 | any other licence. The requirement for fonts to remain under this 11 | licence does not require any document created using the fonts or their 12 | derivatives to be published under this licence, as long as the primary 13 | purpose of the document is not to be a vehicle for the distribution of 14 | the fonts. 15 | 16 | DEFINITIONS 17 | "Font Software" refers to the set of files released by the Copyright 18 | Holder(s) under this licence and clearly marked as such. This may 19 | include source files, build scripts and documentation. 20 | 21 | "Original Version" refers to the collection of Font Software components 22 | as received under this licence. 23 | 24 | "Modified Version" refers to any derivative made by adding to, deleting, 25 | or substituting -- in part or in whole -- any of the components of the 26 | Original Version, by changing formats or by porting the Font Software to 27 | a new environment. 28 | 29 | "Copyright Holder(s)" refers to all individuals and companies who have a 30 | copyright ownership of the Font Software. 31 | 32 | "Substantially Changed" refers to Modified Versions which can be easily 33 | identified as dissimilar to the Font Software by users of the Font 34 | Software comparing the Original Version with the Modified Version. 35 | 36 | To "Propagate" a work means to do anything with it that, without 37 | permission, would make you directly or secondarily liable for 38 | infringement under applicable copyright law, except executing it on a 39 | computer or modifying a private copy. Propagation includes copying, 40 | distribution (with or without modification and with or without charging 41 | a redistribution fee), making available to the public, and in some 42 | countries other activities as well. 43 | 44 | PERMISSION & CONDITIONS 45 | This licence does not grant any rights under trademark law and all such 46 | rights are reserved. 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a 49 | copy of the Font Software, to propagate the Font Software, subject to 50 | the below conditions: 51 | 52 | 1) Each copy of the Font Software must contain the above copyright 53 | notice and this licence. These can be included either as stand-alone 54 | text files, human-readable headers or in the appropriate machine- 55 | readable metadata fields within text or binary files as long as those 56 | fields can be easily viewed by the user. 57 | 58 | 2) The font name complies with the following: 59 | (a) The Original Version must retain its name, unmodified. 60 | (b) Modified Versions which are Substantially Changed must be renamed to 61 | avoid use of the name of the Original Version or similar names entirely. 62 | (c) Modified Versions which are not Substantially Changed must be 63 | renamed to both (i) retain the name of the Original Version and (ii) add 64 | additional naming elements to distinguish the Modified Version from the 65 | Original Version. The name of such Modified Versions must be the name of 66 | the Original Version, with "derivative X" where X represents the name of 67 | the new work, appended to that name. 68 | 69 | 3) The name(s) of the Copyright Holder(s) and any contributor to the 70 | Font Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except (i) as required by this licence, (ii) to 72 | acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with 73 | their explicit written permission. 74 | 75 | 4) The Font Software, modified or unmodified, in part or in whole, must 76 | be distributed entirely under this licence, and must not be distributed 77 | under any other licence. The requirement for fonts to remain under this 78 | licence does not affect any document created using the Font Software, 79 | except any version of the Font Software extracted from a document 80 | created using the Font Software may only be distributed under this 81 | licence. 82 | 83 | TERMINATION 84 | This licence becomes null and void if any of the above conditions are 85 | not met. 86 | 87 | DISCLAIMER 88 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF 91 | COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 92 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 93 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 94 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 95 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER 96 | DEALINGS IN THE FONT SOFTWARE. 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | [![PyPI](https://img.shields.io/pypi/v/dehinter?color=blueviolet&label=PyPI&logo=python&logoColor=white)](https://pypi.org/project/dehinter/) 5 | ![Python CI](https://github.com/source-foundry/dehinter/workflows/Python%20CI/badge.svg) 6 | ![Python Lints](https://github.com/source-foundry/dehinter/workflows/Python%20Lints/badge.svg) 7 | ![Python Type Checks](https://github.com/source-foundry/dehinter/workflows/Python%20Type%20Checks/badge.svg) 8 | [![codecov](https://codecov.io/gh/source-foundry/dehinter/branch/master/graph/badge.svg)](https://codecov.io/gh/source-foundry/dehinter) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a2f54fac2c544f389e0066cfa159dfe8)](https://www.codacy.com/app/SourceFoundry/dehinter?utm_source=github.com&utm_medium=referral&utm_content=source-foundry/dehinter&utm_campaign=Badge_Grade) 10 | 11 | ## About 12 | 13 | `dehinter` is a Python command line application that removes TrueType instruction sets, global hinting tables, and other associated OpenType table data in font files. The tool provides cross-platform support on macOS, Windows, and Linux systems with a Python v3.7+ interpreter. 14 | 15 | ## What it does 16 | 17 | - Removes OpenType [glyf table](https://docs.microsoft.com/en-us/typography/opentype/spec/glyf) instruction set bytecode data 18 | - Removes OpenType and other TTF hinting related tables - [cvt table](https://docs.microsoft.com/en-us/typography/opentype/spec/cvt) - [fpgm table](https://docs.microsoft.com/en-us/typography/opentype/spec/fpgm) - [hdmx table](https://docs.microsoft.com/en-us/typography/opentype/spec/hdmx) - [LTSH table](https://docs.microsoft.com/en-us/typography/opentype/spec/ltsh) - [prep table](https://docs.microsoft.com/en-us/typography/opentype/spec/prep) - [TTFA table](https://www.freetype.org/ttfautohint/doc/ttfautohint.html#add-ttfa-info-table) (not part of the OpenType specification) - [VDMX table](https://docs.microsoft.com/en-us/typography/opentype/spec/vdmx) 19 | - Removes OpenType [cvar table](https://docs.microsoft.com/en-us/typography/opentype/spec/cvar) from variable fonts 20 | - Updates [gasp table](https://docs.microsoft.com/en-us/typography/opentype/spec/gasp) values 21 | - Updates [maxp table](https://docs.microsoft.com/en-us/typography/opentype/spec/maxp) values 22 | - Updates [head table](https://docs.microsoft.com/en-us/typography/opentype/spec/head) bit flags 23 | - Displays file sizes of the hinted and dehinted versions of the fonts 24 | 25 | Options allow you to maintain the original version of any of these tables. 26 | 27 | ## Installation 28 | 29 | `dehinter` requires a Python 3.7+ interpreter. 30 | 31 | Installation in a [Python3 virtual environment](https://docs.python.org/3/library/venv.html) is recommended as dependencies are pinned to versions that are confirmed to work with this project. 32 | 33 | Use any of the following installation approaches: 34 | 35 | ### pip install from PyPI 36 | 37 | ``` 38 | $ pip3 install dehinter 39 | ``` 40 | 41 | ### pip install from source 42 | 43 | ``` 44 | $ git clone https://github.com/source-foundry/dehinter.git 45 | $ cd dehinter 46 | $ pip3 install . 47 | ``` 48 | 49 | ### Developer install from source 50 | 51 | The following approach installs the project and associated optional developer dependencies so that source changes are available without the need for re-installation. 52 | 53 | ``` 54 | $ git clone https://github.com/source-foundry/dehinter.git 55 | $ cd dehinter 56 | $ pip3 install --ignore-installed -r requirements.txt -e ".[dev]" 57 | ``` 58 | 59 | ## Usage 60 | 61 | ``` 62 | $ dehinter [OPTIONS] [HINTED FILE PATH] 63 | ``` 64 | 65 | By default, a new dehinted font build write occurs on the path `[ORIGINAL HINTED FONT NAME]-dehinted.ttf` in the `[HINTED FILE PATH]` directory. 66 | 67 | Use `dehinter -h` to view available options. 68 | 69 | ## Issues 70 | 71 | Please report issues on the [project issue tracker](https://github.com/source-foundry/dehinter/issues). 72 | 73 | ## Contributing 74 | 75 | Contributions are warmly welcomed. A development dependency environment can be installed in editable mode with the developer installation documentation above. 76 | 77 | Please use the standard Github pull request approach to propose source changes. 78 | 79 | ### Source file linting 80 | 81 | Python source files are linted with `flake8`. See the Makefile `test-lint` target for details. 82 | 83 | ### Source file static type checks 84 | 85 | Static type checks are performed on Python source files with `mypy` and are based on type annotations in the Python source files. See the Makefile `test-type-check` target for details. 86 | 87 | ### Testing 88 | 89 | The project runs continuous integration testing on GitHub Actions runners with the `pytest` testing toolchain. Test modules are located in the `tests` directory of the repository. 90 | 91 | Local testing by Python interpreter version can be performed with the following command executed from the root of the repository: 92 | 93 | ``` 94 | $ tox -e [PYTHON INTERPRETER VERSION] 95 | ``` 96 | 97 | Please see the `tox` documentation for additional details. 98 | 99 | ### Test coverage 100 | 101 | Unit test coverage is executed with the `coverage` tool. See the Makefile `test-coverage` target for details. 102 | 103 | ## Acknowledgments 104 | 105 | `dehinter` is built with the fantastic [fontTools free software library](https://github.com/fonttools/fonttools) and is based on the dehinting approach used in the [`ttfautohint` free software project](https://www.freetype.org/ttfautohint/). 106 | 107 | ## License 108 | 109 | Copyright 2019 Source Foundry Authors and Contributors 110 | 111 | Licensed under the Apache License, Version 2.0 (the "License"); 112 | you may not use this file except in compliance with the License. 113 | You may obtain a copy of the License at 114 | 115 | http://www.apache.org/licenses/LICENSE-2.0 116 | 117 | Unless required by applicable law or agreed to in writing, software 118 | distributed under the License is distributed on an "AS IS" BASIS, 119 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 120 | See the License for the specific language governing permissions and 121 | limitations under the License. 122 | -------------------------------------------------------------------------------- /lib/dehinter/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import argparse 16 | import os 17 | import sys 18 | from typing import List 19 | 20 | from fontTools.ttLib import TTFont # type: ignore 21 | 22 | from dehinter import __version__ 23 | from dehinter.font import dehint, is_truetype_font 24 | from dehinter.paths import filepath_exists, get_default_out_path 25 | from dehinter.system import get_filesize 26 | 27 | 28 | def main() -> None: # pragma: no cover 29 | run(sys.argv[1:]) 30 | 31 | 32 | def run(argv: List[str]) -> None: 33 | # =========================================================== 34 | # argparse command line argument definitions 35 | # =========================================================== 36 | parser = argparse.ArgumentParser( 37 | description="A tool for the removal of TrueType instruction sets (hints) in fonts" 38 | ) 39 | parser.add_argument( 40 | "--version", action="version", version="dehinter v{}".format(__version__) 41 | ) 42 | parser.add_argument("-o", "--out", help="out file path (dehinted font)") 43 | parser.add_argument("--keep-cvar", help="keep cvar table", action="store_true") 44 | parser.add_argument("--keep-cvt", help="keep cvt table", action="store_true") 45 | parser.add_argument("--keep-fpgm", help="keep fpgm table", action="store_true") 46 | parser.add_argument("--keep-hdmx", help="keep hdmx table", action="store_true") 47 | parser.add_argument("--keep-ltsh", help="keep LTSH table", action="store_true") 48 | parser.add_argument("--keep-prep", help="keep prep table", action="store_true") 49 | parser.add_argument("--keep-ttfa", help="keep TTFA table", action="store_true") 50 | parser.add_argument("--keep-vdmx", help="keep VDMX table", action="store_true") 51 | parser.add_argument( 52 | "--keep-glyf", help="do not modify glyf table", action="store_true" 53 | ) 54 | parser.add_argument( 55 | "--keep-gasp", help="do not modify gasp table", action="store_true" 56 | ) 57 | parser.add_argument( 58 | "--keep-maxp", help="do not modify maxp table", action="store_true" 59 | ) 60 | parser.add_argument( 61 | "--keep-head", help="do not modify head table", action="store_true" 62 | ) 63 | parser.add_argument("--quiet", help="silence standard output", action="store_true") 64 | parser.add_argument("INFILE", help="in file path (hinted font)") 65 | 66 | args = parser.parse_args(argv) 67 | 68 | # =========================================================== 69 | # Command line logic 70 | # =========================================================== 71 | # 72 | # Validations 73 | # ----------- 74 | # (1) file path request is a file 75 | if not filepath_exists(args.INFILE): 76 | sys.stderr.write( 77 | f"[!] Error: '{args.INFILE}' is not a valid file path.{os.linesep}" 78 | ) 79 | sys.stderr.write(f"[!] Request canceled.{os.linesep}") 80 | sys.exit(1) 81 | # (2) the file is a ttf font file (based on the 4 byte file signature 82 | if not is_truetype_font(args.INFILE): 83 | sys.stderr.write( 84 | f"[!] Error: '{args.INFILE}' does not appear to be a TrueType font " 85 | f"file.{os.linesep}" 86 | ) 87 | sys.stderr.write(f"[!] Request canceled.{os.linesep}") 88 | sys.exit(1) 89 | # (3) confirm that out path is not the same as in path 90 | # This tool does not support writing dehinted files in place over hinted version 91 | if args.INFILE == args.out: 92 | sys.stderr.write( 93 | f"[!] Error: You are attempting to overwrite the hinted file with the " 94 | f"dehinted file. This is not supported. Please choose a different file " 95 | f"path for the dehinted file.{os.linesep}" 96 | ) 97 | sys.exit(1) 98 | # Execution 99 | # --------- 100 | # (1) OpenType table removal 101 | try: 102 | tt = TTFont(args.INFILE) 103 | except Exception as e: 104 | sys.stderr.write( 105 | f"[!] Error: Unable to create font object with '{args.INFILE}' -> {str(e)}" 106 | ) 107 | sys.exit(1) 108 | 109 | use_verbose_output = not args.quiet 110 | 111 | dehint( 112 | tt, 113 | keep_cvar=args.keep_cvar, 114 | keep_cvt=args.keep_cvt, 115 | keep_fpgm=args.keep_fpgm, 116 | keep_gasp=args.keep_gasp, 117 | keep_glyf=args.keep_glyf, 118 | keep_hdmx=args.keep_hdmx, 119 | keep_head=args.keep_head, 120 | keep_ltsh=args.keep_ltsh, 121 | keep_maxp=args.keep_maxp, 122 | keep_prep=args.keep_prep, 123 | keep_ttfa=args.keep_ttfa, 124 | keep_vdmx=args.keep_vdmx, 125 | verbose=use_verbose_output, 126 | ) 127 | 128 | # File write 129 | # ---------- 130 | if args.out: 131 | # validation performed above to prevent this file path definition from 132 | # being the same as the in file path. Write in place over a hinted 133 | # file is not supported 134 | outpath = args.out 135 | else: 136 | outpath = get_default_out_path(args.INFILE) 137 | 138 | try: 139 | tt.save(outpath) 140 | if use_verbose_output: 141 | print(f"{os.linesep}[+] Saved dehinted font as '{outpath}'") 142 | except Exception as e: # pragma: no cover 143 | sys.stderr.write( 144 | f"[!] Error: Unable to save dehinted font file: {str(e)}{os.linesep}" 145 | ) 146 | 147 | # File size comparison 148 | # -------------------- 149 | if use_verbose_output: 150 | infile_size_tuple = get_filesize(args.INFILE) 151 | outfile_size_tuple = get_filesize( 152 | outpath 153 | ) # depends on outpath definition defined during file write 154 | print(f"{os.linesep}[*] File sizes:") 155 | print(f" {infile_size_tuple[0]}{infile_size_tuple[1]} (hinted)") 156 | print(f" {outfile_size_tuple[0]}{outfile_size_tuple[1]} (dehinted)") 157 | -------------------------------------------------------------------------------- /tests/test_font.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dehinter.font import ( 4 | is_truetype_font, 5 | has_cvar_table, 6 | has_cvt_table, 7 | has_fpgm_table, 8 | has_gasp_table, 9 | has_hdmx_table, 10 | has_ltsh_table, 11 | has_prep_table, 12 | has_ttfa_table, 13 | has_vdmx_table, 14 | ) 15 | from dehinter.font import ( 16 | remove_cvar_table, 17 | remove_cvt_table, 18 | remove_fpgm_table, 19 | remove_hdmx_table, 20 | remove_ltsh_table, 21 | remove_prep_table, 22 | remove_ttfa_table, 23 | remove_vdmx_table, 24 | remove_glyf_instructions, 25 | ) 26 | from dehinter.font import update_gasp_table, update_head_table_flags, update_maxp_table 27 | 28 | import pytest 29 | from fontTools.ttLib import TTFont 30 | 31 | FILEPATH_TEST_TEXT = os.path.join("tests", "test_files", "text", "test.txt") 32 | FILEPATH_HINTED_TTF = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 33 | FILEPATH_DEHINTED_TTF = os.path.join( 34 | "tests", "test_files", "fonts", "Roboto-Regular-dehinted.ttf" 35 | ) 36 | FILEPATH_HINTED_TTF_2 = os.path.join( 37 | "tests", "test_files", "fonts", "NotoSans-Regular.ttf" 38 | ) 39 | FILEPATH_DEHINTED_TTF_2 = os.path.join( 40 | "tests", "test_files", "fonts", "NotoSans-Regular-dehinted.ttf" 41 | ) 42 | 43 | FILEPATH_HINTED_TTF_3 = os.path.join("tests", "test_files", "fonts", "Ubuntu-Regular.ttf") 44 | 45 | FILEPATH_HINTED_TTF_VF = os.path.join("tests", "test_files", "fonts", "OpenSans-VF.ttf") 46 | 47 | # ======================================================== 48 | # Utilities 49 | # ======================================================== 50 | 51 | 52 | def test_has_cvar_table_true(): 53 | tt = TTFont(FILEPATH_HINTED_TTF_VF) 54 | assert has_cvar_table(tt) is True 55 | 56 | 57 | def test_has_cvar_table_false(): 58 | tt = TTFont(FILEPATH_HINTED_TTF) 59 | assert has_cvar_table(tt) is False 60 | 61 | 62 | def test_has_cvt_table_true(): 63 | tt = TTFont(FILEPATH_HINTED_TTF) 64 | assert has_cvt_table(tt) is True 65 | 66 | 67 | def test_has_cvt_table_false(): 68 | tt = TTFont(FILEPATH_DEHINTED_TTF) 69 | assert has_cvt_table(tt) is False 70 | 71 | 72 | def test_has_fpgm_table_true(): 73 | tt = TTFont(FILEPATH_HINTED_TTF) 74 | assert has_fpgm_table(tt) is True 75 | 76 | 77 | def test_has_fpgm_table_false(): 78 | tt = TTFont(FILEPATH_DEHINTED_TTF) 79 | assert has_fpgm_table(tt) is False 80 | 81 | 82 | def test_has_gasp_table_true(): 83 | tt = TTFont(FILEPATH_HINTED_TTF) 84 | assert has_gasp_table(tt) is True 85 | 86 | 87 | def test_has_hdmx_table_true(): 88 | tt = TTFont(FILEPATH_HINTED_TTF) 89 | assert has_hdmx_table(tt) is True 90 | 91 | 92 | def test_has_hdmx_table_false(): 93 | tt = TTFont(FILEPATH_DEHINTED_TTF) 94 | assert has_hdmx_table(tt) is False 95 | 96 | 97 | def test_has_ltsh_table_true(): 98 | tt = TTFont(FILEPATH_HINTED_TTF) 99 | assert has_ltsh_table(tt) is True 100 | 101 | 102 | def test_has_ltsh_table_false(): 103 | tt = TTFont(FILEPATH_DEHINTED_TTF) 104 | assert has_ltsh_table(tt) is False 105 | 106 | 107 | def test_has_prep_table_true(): 108 | tt = TTFont(FILEPATH_HINTED_TTF) 109 | assert has_prep_table(tt) is True 110 | 111 | 112 | def test_has_prep_table_false(): 113 | tt = TTFont(FILEPATH_DEHINTED_TTF) 114 | assert has_prep_table(tt) is False 115 | 116 | 117 | def test_has_ttfa_table_true(): 118 | tt = TTFont(FILEPATH_HINTED_TTF_2) # tested in Noto Sans font 119 | assert has_ttfa_table(tt) is True 120 | 121 | 122 | def test_has_ttfa_table_false(): 123 | tt = TTFont(FILEPATH_DEHINTED_TTF_2) # tested in Noto Sans font 124 | assert has_ttfa_table(tt) is False 125 | 126 | 127 | def test_has_vdmx_table_true(): 128 | tt = TTFont(FILEPATH_HINTED_TTF_3) 129 | assert has_vdmx_table(tt) is True 130 | 131 | 132 | def test_has_vdmx_table_false(): 133 | tt = TTFont(FILEPATH_HINTED_TTF) 134 | assert has_vdmx_table(tt) is False 135 | 136 | 137 | def test_is_truetype_font_for_ttf(): 138 | assert is_truetype_font(FILEPATH_HINTED_TTF) is True 139 | 140 | 141 | def test_is_truetype_font_for_not_ttf(): 142 | assert is_truetype_font(FILEPATH_TEST_TEXT) is False 143 | 144 | 145 | # ======================================================== 146 | # OpenType table removal 147 | # ======================================================== 148 | def test_delete_cvar_table(): 149 | tt = TTFont(FILEPATH_HINTED_TTF_VF) 150 | assert ("cvar" in tt) is True 151 | remove_cvar_table(tt) 152 | assert ("cvar" in tt) is False 153 | 154 | 155 | def test_delete_cvar_table_when_not_vf(): 156 | tt = TTFont(FILEPATH_HINTED_TTF) 157 | assert ("cvar" in tt) is False 158 | remove_cvar_table(tt) 159 | assert ("cvar" in tt) is False 160 | 161 | 162 | def test_delete_cvt_table(): 163 | tt = TTFont(FILEPATH_HINTED_TTF) 164 | assert ("cvt " in tt) is True 165 | remove_cvt_table(tt) 166 | assert ("cvt " in tt) is False 167 | 168 | 169 | def test_delete_cvt_table_missing_table(): 170 | tt = TTFont(FILEPATH_DEHINTED_TTF) 171 | assert ("cvt " in tt) is False 172 | remove_cvt_table(tt) 173 | assert ("cvt " in tt) is False 174 | 175 | 176 | def test_delete_fpgm_table(): 177 | tt = TTFont(FILEPATH_HINTED_TTF) 178 | assert ("fpgm" in tt) is True 179 | remove_fpgm_table(tt) 180 | assert ("fpgm" in tt) is False 181 | 182 | 183 | def test_delete_fpgm_table_missing_table(): 184 | tt = TTFont(FILEPATH_DEHINTED_TTF) 185 | assert ("fpgm" in tt) is False 186 | remove_fpgm_table(tt) 187 | assert ("fpgm" in tt) is False 188 | 189 | 190 | def test_delete_hdmx_table(): 191 | tt = TTFont(FILEPATH_HINTED_TTF) 192 | assert ("hdmx" in tt) is True 193 | remove_hdmx_table(tt) 194 | assert ("hdmx" in tt) is False 195 | 196 | 197 | def test_delete_hdmtx_table_missing_table(): 198 | tt = TTFont(FILEPATH_DEHINTED_TTF) 199 | assert ("hdmx" in tt) is False 200 | remove_hdmx_table(tt) 201 | assert ("hdmx" in tt) is False 202 | 203 | 204 | def test_delete_ltsh_table(): 205 | tt = TTFont(FILEPATH_HINTED_TTF) 206 | assert ("LTSH" in tt) is True 207 | remove_ltsh_table(tt) 208 | assert ("LTSH" in tt) is False 209 | 210 | 211 | def test_delete_ltsh_table_missing_table(): 212 | tt = TTFont(FILEPATH_DEHINTED_TTF) 213 | assert ("LTSH" in tt) is False 214 | remove_ltsh_table(tt) 215 | assert ("LTSH" in tt) is False 216 | 217 | 218 | def test_delete_prep_table(): 219 | tt = TTFont(FILEPATH_HINTED_TTF) 220 | assert ("prep" in tt) is True 221 | remove_prep_table(tt) 222 | assert ("prep" in tt) is False 223 | 224 | 225 | def test_delete_prep_table_missing_table(): 226 | tt = TTFont(FILEPATH_DEHINTED_TTF) 227 | assert ("prep" in tt) is False 228 | remove_prep_table(tt) 229 | assert ("prep" in tt) is False 230 | 231 | 232 | def test_delete_ttfa_table(): 233 | tt = TTFont(FILEPATH_HINTED_TTF_2) # tested in Noto Sans 234 | assert ("TTFA" in tt) is True 235 | remove_ttfa_table(tt) 236 | assert ("TTFA" in tt) is False 237 | 238 | 239 | def test_delete_ttfa_table_missing_table(): 240 | tt = TTFont(FILEPATH_DEHINTED_TTF_2) # tested in Noto Sans 241 | assert ("TTFA" in tt) is False 242 | remove_ttfa_table(tt) 243 | assert ("TTFA" in tt) is False 244 | 245 | 246 | def test_delete_vdmx_table(): 247 | tt = TTFont(FILEPATH_HINTED_TTF_3) # tested in Ubuntu 248 | assert ("VDMX" in tt) is True 249 | remove_vdmx_table(tt) 250 | assert ("VDMX" in tt) is False 251 | 252 | 253 | def test_delete_vdmx_table_missing_table(): 254 | tt = TTFont(FILEPATH_DEHINTED_TTF) # tested in Roboto 255 | assert ("VDMX" in tt) is False 256 | remove_vdmx_table(tt) 257 | assert ("VDMX" in tt) is False 258 | 259 | 260 | # ======================================================== 261 | # glyf table instruction set bytecode removal 262 | # ======================================================== 263 | def test_remove_glyf_instructions_hinted_font(): 264 | tt = TTFont(FILEPATH_HINTED_TTF) 265 | number_removed = remove_glyf_instructions(tt) 266 | assert number_removed == 2717 267 | 268 | 269 | def test_remove_glyf_instructions_dehinted_font(): 270 | tt = TTFont(FILEPATH_DEHINTED_TTF) 271 | number_removed = remove_glyf_instructions(tt) 272 | assert number_removed == 0 273 | 274 | 275 | # ======================================================== 276 | # gasp table edits 277 | # ======================================================== 278 | def test_update_gasp_table(): 279 | tt = TTFont(FILEPATH_HINTED_TTF) 280 | assert update_gasp_table(tt) is True 281 | assert tt["gasp"].gaspRange == {65535: 0x000A} 282 | 283 | 284 | def test_update_gasp_table_no_gasp(): 285 | # the Open Sans var font does not have a gasp table 286 | # execution here adds one when not present 287 | tt = TTFont(FILEPATH_HINTED_TTF_VF) 288 | assert update_gasp_table(tt) is True 289 | assert tt["gasp"].gaspRange == {65535: 0x000A} 290 | 291 | 292 | def test_update_gasp_table_previous_correct_definition(): 293 | tt = TTFont(FILEPATH_DEHINTED_TTF_2) 294 | assert update_gasp_table(tt) is False 295 | assert tt["gasp"].gaspRange == {65535: 0x000A} 296 | 297 | 298 | # ========================================= 299 | # maxp table edits 300 | # ========================================= 301 | def test_update_maxp_table(): 302 | tt = TTFont( 303 | FILEPATH_HINTED_TTF_2 304 | ) # test in Noto Sans as all values are modified there 305 | assert update_maxp_table(tt) is True 306 | assert tt["maxp"].maxZones == 0 307 | assert tt["maxp"].maxTwilightPoints == 0 308 | assert tt["maxp"].maxStorage == 0 309 | assert tt["maxp"].maxFunctionDefs == 0 310 | assert tt["maxp"].maxStackElements == 0 311 | assert tt["maxp"].maxSizeOfInstructions == 0 312 | 313 | 314 | # ========================================= 315 | # head table edits 316 | # ========================================= 317 | def test_update_head_table_flags_without_ltsh_hdmx(): 318 | tt = TTFont(FILEPATH_HINTED_TTF) 319 | assert (tt["head"].flags & (1 << 4)) != 0 320 | remove_hdmx_table(tt) 321 | remove_ltsh_table(tt) 322 | response = update_head_table_flags(tt) 323 | assert response is True 324 | assert (tt["head"].flags & (1 << 4)) == 0 325 | 326 | 327 | def test_update_head_table_flags_with_ltsh_hdmx(): 328 | tt = TTFont(FILEPATH_HINTED_TTF) 329 | assert (tt["head"].flags & (1 << 4)) != 0 330 | response = update_head_table_flags(tt) 331 | assert response is False 332 | assert (tt["head"].flags & (1 << 4)) != 0 333 | 334 | 335 | def test_update_head_table_flags_with_ltsh(): 336 | tt = TTFont(FILEPATH_HINTED_TTF) 337 | assert (tt["head"].flags & (1 << 4)) != 0 338 | remove_hdmx_table(tt) 339 | response = update_head_table_flags(tt) 340 | assert response is False 341 | assert (tt["head"].flags & (1 << 4)) != 0 342 | 343 | 344 | def test_update_head_table_flags_with_hdmx(): 345 | tt = TTFont(FILEPATH_HINTED_TTF) 346 | assert (tt["head"].flags & (1 << 4)) != 0 347 | remove_ltsh_table(tt) 348 | response = update_head_table_flags(tt) 349 | assert response is False 350 | assert (tt["head"].flags & (1 << 4)) != 0 351 | 352 | 353 | def test_update_head_table_flags_previously_cleared(): 354 | tt = TTFont(FILEPATH_HINTED_TTF_2) 355 | assert (tt["head"].flags & (1 << 4)) == 0 356 | response = update_head_table_flags(tt) 357 | assert response is False 358 | assert (tt["head"].flags & (1 << 4)) == 0 359 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/test_files/fonts/licenses/Roboto-License.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /lib/dehinter/font.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Source Foundry Authors and Contributors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import array 16 | import os 17 | import pprint 18 | import sys 19 | from typing import Union 20 | 21 | from fontTools import ttLib # type: ignore 22 | 23 | from dehinter.bitops import clear_bit_k, is_bit_k_set 24 | 25 | # instantiate pretty printer 26 | pp = pprint.PrettyPrinter(indent=4) 27 | 28 | 29 | def _report_actions(table, has_table): 30 | if not has_table: 31 | print(f"[-] Removed {table} table") 32 | else: # pragma: no cover 33 | sys.stderr.write( 34 | f"[!] Error: failed to remove {table} table from font{os.linesep}" 35 | ) 36 | 37 | 38 | # ======================================================== 39 | # Core dehinting routine 40 | # ======================================================== 41 | def dehint( 42 | tt, 43 | keep_cvar=False, 44 | keep_cvt=False, 45 | keep_fpgm=False, 46 | keep_gasp=False, 47 | keep_glyf=False, 48 | keep_hdmx=False, 49 | keep_head=False, 50 | keep_ltsh=False, 51 | keep_maxp=False, 52 | keep_prep=False, 53 | keep_ttfa=False, 54 | keep_vdmx=False, 55 | verbose=True, 56 | ): 57 | 58 | if is_variable_font(tt) and not keep_cvar: 59 | if has_cvar_table(tt): 60 | remove_cvar_table(tt) 61 | if verbose: 62 | _report_actions("cvar", has_cvar_table(tt)) 63 | 64 | if not keep_cvt: 65 | if has_cvt_table(tt): 66 | remove_cvt_table(tt) 67 | if verbose: 68 | _report_actions("cvt", has_cvt_table(tt)) 69 | 70 | if not keep_fpgm: 71 | if has_fpgm_table(tt): 72 | remove_fpgm_table(tt) 73 | if verbose: 74 | _report_actions("fpgm", has_fpgm_table(tt)) 75 | 76 | if not keep_hdmx: 77 | if has_hdmx_table(tt): 78 | remove_hdmx_table(tt) 79 | if verbose: 80 | _report_actions("hdmx", has_hdmx_table(tt)) 81 | 82 | if not keep_ltsh: 83 | if has_ltsh_table(tt): 84 | remove_ltsh_table(tt) 85 | if verbose: 86 | _report_actions("LTSH", has_ltsh_table(tt)) 87 | 88 | if not keep_prep: 89 | if has_prep_table(tt): 90 | remove_prep_table(tt) 91 | if verbose: 92 | _report_actions("prep", has_prep_table(tt)) 93 | 94 | if not keep_ttfa: 95 | if has_ttfa_table(tt): 96 | remove_ttfa_table(tt) 97 | if verbose: 98 | _report_actions("ttfa", has_ttfa_table(tt)) 99 | 100 | if not keep_vdmx: 101 | if has_vdmx_table(tt): 102 | remove_vdmx_table(tt) 103 | if verbose: 104 | _report_actions("VDMX", has_vdmx_table(tt)) 105 | 106 | # (2) Remove glyf table instruction set bytecode 107 | if not keep_glyf: 108 | number_glyfs_edited = remove_glyf_instructions(tt) 109 | if number_glyfs_edited > 0: 110 | if verbose: 111 | print( 112 | f"[-] Removed glyf table instruction bytecode from " 113 | f"{number_glyfs_edited} glyphs" 114 | ) 115 | 116 | # (3) Edit gasp table 117 | if not keep_gasp: 118 | if update_gasp_table(tt): 119 | gasp_string = pp.pformat(tt["gasp"].__dict__) 120 | if verbose: 121 | print(f"[Δ] New gasp table values:{os.linesep} {gasp_string}") 122 | 123 | # (4) Edit maxp table 124 | if not keep_maxp: 125 | if update_maxp_table(tt): 126 | maxp_string = pp.pformat(tt["maxp"].__dict__) 127 | if verbose: 128 | print(f"[Δ] New maxp table values:{os.linesep} {maxp_string}") 129 | 130 | # (5) Edit head table flags to clear bit 4 131 | if not keep_head: 132 | if update_head_table_flags(tt): 133 | if verbose: 134 | print("[Δ] Cleared bit 4 in head table flags") 135 | 136 | 137 | # ======================================================== 138 | # Utilities 139 | # ======================================================== 140 | def has_cvar_table(tt) -> bool: 141 | """Tests for the presence of a cvat table in a TrueType variable font.""" 142 | return "cvar" in tt 143 | 144 | 145 | def has_cvt_table(tt) -> bool: 146 | """Tests for the presence of a cvt table in a TrueType font.""" 147 | return "cvt " in tt 148 | 149 | 150 | def has_fpgm_table(tt) -> bool: 151 | """Tests for the presence of a fpgm table in a TrueType font.""" 152 | return "fpgm" in tt 153 | 154 | 155 | def has_gasp_table(tt) -> bool: 156 | """Tests for the presence of a gasp table in a TrueType font.""" 157 | return "gasp" in tt 158 | 159 | 160 | def has_hdmx_table(tt) -> bool: 161 | """Tests for the presence of a hdmx table in a TrueType font.""" 162 | return "hdmx" in tt 163 | 164 | 165 | def has_ltsh_table(tt) -> bool: 166 | """Tests for the presence of a LTSH table in a TrueType font.""" 167 | return "LTSH" in tt 168 | 169 | 170 | def has_prep_table(tt) -> bool: 171 | """Tests for the presence of a prep table in a TrueType font.""" 172 | return "prep" in tt 173 | 174 | 175 | def has_ttfa_table(tt) -> bool: 176 | """Tests for the presence of a TTFA table in a TrueType font.""" 177 | return "TTFA" in tt 178 | 179 | 180 | def has_vdmx_table(tt) -> bool: 181 | """Tests for the presence of a VDMX table in a TrueType font.""" 182 | return "VDMX" in tt 183 | 184 | 185 | def is_truetype_font(filepath: Union[bytes, str, "os.PathLike[str]"]) -> bool: 186 | """Tests that a font has the TrueType file signature of either: 187 | 1) b'\x00\x01\x00\x00' 188 | 2) b'\x74\x72\x75\x65' == 'true'""" 189 | with open(filepath, "rb") as f: 190 | file_signature: bytes = f.read(4) 191 | 192 | return file_signature in (b"\x00\x01\x00\x00", b"\x74\x72\x75\x65") 193 | 194 | 195 | def is_variable_font(tt) -> bool: 196 | """Tests for the presence of a fvar table to confirm that a file is 197 | a variable font.""" 198 | return "fvar" in tt 199 | 200 | 201 | # ======================================================== 202 | # OpenType table removal 203 | # ======================================================== 204 | def remove_cvar_table(tt) -> None: 205 | """Removes cvt table from a fontTools.ttLib.TTFont object""" 206 | try: 207 | del tt["cvar"] 208 | except KeyError: 209 | # do nothing if table is not present in the font 210 | pass 211 | 212 | 213 | def remove_cvt_table(tt) -> None: 214 | """Removes cvt table from a fontTools.ttLib.TTFont object""" 215 | try: 216 | del tt["cvt "] 217 | except KeyError: 218 | # do nothing if table is not present in the font 219 | pass 220 | 221 | 222 | def remove_fpgm_table(tt) -> None: 223 | """Removes fpgm table from a fontTools.ttLib.TTFont object""" 224 | try: 225 | del tt["fpgm"] 226 | except KeyError: 227 | # do nothing if table is not present in the font 228 | pass 229 | 230 | 231 | def remove_hdmx_table(tt) -> None: 232 | """Removes hdmx table from a fontTools.ttLib.TTFont object""" 233 | try: 234 | del tt["hdmx"] 235 | except KeyError: 236 | # do nothing if table is not present in the font 237 | pass 238 | 239 | 240 | def remove_ltsh_table(tt) -> None: 241 | """Removes LTSH table from a fontTools.ttLib.TTFont object.""" 242 | try: 243 | del tt["LTSH"] 244 | except KeyError: 245 | # do nothing if table is not present in the font 246 | pass 247 | 248 | 249 | def remove_prep_table(tt) -> None: 250 | """Removes prep table from a fontTools.ttLib.TTFont object""" 251 | try: 252 | del tt["prep"] 253 | except KeyError: 254 | # do nothing if table is not present in the font 255 | pass 256 | 257 | 258 | def remove_ttfa_table(tt) -> None: 259 | """Removes TTFA table from a fontTools.ttLib.TTFont object""" 260 | try: 261 | del tt["TTFA"] 262 | except KeyError: 263 | # do nothing if table is not present in the font 264 | pass 265 | 266 | 267 | def remove_vdmx_table(tt) -> None: 268 | """Removes TTFA table from a fontTools.ttLib.TTFont object""" 269 | try: 270 | del tt["VDMX"] 271 | except KeyError: 272 | # do nothing if table is not present in the font 273 | pass 274 | 275 | 276 | # ======================================================== 277 | # glyf table instruction set bytecode removal 278 | # ======================================================== 279 | def remove_glyf_instructions(tt) -> int: 280 | """Removes instruction set bytecode from glyph definitions in the glyf table.""" 281 | glyph_number: int = 0 282 | for glyph in tt["glyf"].glyphs.values(): 283 | glyph.expand(tt["glyf"]) 284 | if hasattr(glyph, "program") and glyph.program.bytecode != array.array("B", []): 285 | if glyph.isComposite(): 286 | del glyph.program 287 | glyph_number += 1 288 | else: 289 | glyph.program.bytecode = array.array("B", []) 290 | glyph_number += 1 291 | return glyph_number 292 | 293 | 294 | # ======================================================== 295 | # gasp table edit 296 | # ======================================================== 297 | def update_gasp_table(tt) -> bool: 298 | """Modifies the following gasp table fields: 299 | 1) rangeMaxPPEM changed to 65535 300 | 2) rangeGaspBehavior changed to 0x000a (symmetric grayscale, no gridfit)""" 301 | if "gasp" not in tt: 302 | tt["gasp"] = ttLib.newTable("gasp") 303 | tt["gasp"].gaspRange = {} 304 | if tt["gasp"].gaspRange != {65535: 0x000A}: 305 | tt["gasp"].gaspRange = {65535: 0x000A} 306 | return True 307 | else: 308 | return False 309 | 310 | 311 | # ========================================= 312 | # maxp table edits 313 | # ========================================= 314 | def update_maxp_table(tt) -> bool: 315 | """Update the maxp table with new values based on elimination of instruction sets.""" 316 | changed: bool = False 317 | if tt["maxp"].maxZones != 0: 318 | tt["maxp"].maxZones = 0 319 | changed = True 320 | if tt["maxp"].maxTwilightPoints != 0: 321 | tt["maxp"].maxTwilightPoints = 0 322 | changed = True 323 | if tt["maxp"].maxStorage != 0: 324 | tt["maxp"].maxStorage = 0 325 | changed = True 326 | if tt["maxp"].maxFunctionDefs != 0: 327 | tt["maxp"].maxFunctionDefs = 0 328 | changed = True 329 | if tt["maxp"].maxStackElements != 0: 330 | tt["maxp"].maxStackElements = 0 331 | changed = True 332 | if tt["maxp"].maxSizeOfInstructions != 0: 333 | tt["maxp"].maxSizeOfInstructions = 0 334 | changed = True 335 | return changed 336 | 337 | 338 | # ========================================= 339 | # head table edits 340 | # ========================================= 341 | def update_head_table_flags(tt) -> bool: 342 | if is_bit_k_set(tt["head"].flags, 4): 343 | # confirm that there is no LTSH or hdmx table 344 | # bit 4 should be set if either of these tables are present in font 345 | if has_hdmx_table(tt) or has_ltsh_table(tt): 346 | return False 347 | else: 348 | new_flags = clear_bit_k(tt["head"].flags, 4) 349 | tt["head"].flags = new_flags 350 | return True 351 | else: 352 | return False 353 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import array 2 | import os 3 | import shutil 4 | 5 | from fontTools.ttLib import TTFont 6 | 7 | from dehinter.__main__ import run 8 | 9 | import pytest 10 | 11 | 12 | # 13 | # Integration tests 14 | # 15 | 16 | 17 | def font_validator(filepath): 18 | assert os.path.exists(filepath) 19 | tt = TTFont(filepath) 20 | assert "cvt " not in tt 21 | assert "fpgm" not in tt 22 | assert "hdmx" not in tt 23 | assert "LTSH" not in tt 24 | assert "prep" not in tt 25 | assert "TTFA" not in tt 26 | assert "VDMX" not in tt 27 | for glyph in tt["glyf"].glyphs.values(): 28 | glyph.expand(tt["glyf"]) 29 | if glyph.isComposite(): 30 | assert not hasattr(glyph, "program") 31 | if hasattr(glyph, "program"): 32 | assert glyph.program.bytecode == array.array("B", []) 33 | assert tt["gasp"].gaspRange == {65535: 0x000A} 34 | assert tt["maxp"].maxZones == 0 35 | assert tt["maxp"].maxTwilightPoints == 0 36 | assert tt["maxp"].maxStorage == 0 37 | assert tt["maxp"].maxStackElements == 0 38 | assert tt["maxp"].maxSizeOfInstructions == 0 39 | assert (tt["head"].flags & 1 << 4) == 0 40 | 41 | 42 | def test_default_run_roboto(capsys): 43 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 44 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 45 | test_inpath = os.path.join( 46 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 47 | ) 48 | test_outpath = os.path.join( 49 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 50 | ) 51 | test_args = [test_inpath] 52 | 53 | # setup 54 | if os.path.isdir(test_dir): 55 | shutil.rmtree(test_dir) 56 | os.mkdir(test_dir) 57 | shutil.copyfile(notouch_inpath, test_inpath) 58 | 59 | # execute 60 | run(test_args) 61 | captured = capsys.readouterr() 62 | assert "Saved dehinted font as" in captured.out 63 | 64 | # test 65 | font_validator(test_outpath) 66 | 67 | # tear down 68 | shutil.rmtree(test_dir) 69 | 70 | 71 | def test_default_run_noto(): 72 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 73 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "NotoSans-Regular.ttf") 74 | test_inpath = os.path.join( 75 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 76 | ) 77 | test_outpath = os.path.join( 78 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular-dehinted.ttf" 79 | ) 80 | test_args = [test_inpath] 81 | 82 | # setup 83 | if os.path.isdir(test_dir): 84 | shutil.rmtree(test_dir) 85 | os.mkdir(test_dir) 86 | shutil.copyfile(notouch_inpath, test_inpath) 87 | 88 | # execute 89 | run(test_args) 90 | 91 | # test 92 | font_validator(test_outpath) 93 | 94 | # tear down 95 | shutil.rmtree(test_dir) 96 | 97 | 98 | def test_default_run_ubuntu(): 99 | """This is used to test VDMX table removal""" 100 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 101 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Ubuntu-Regular.ttf") 102 | test_inpath = os.path.join( 103 | "tests", "test_files", "fonts", "temp", "Ubuntu-Regular.ttf" 104 | ) 105 | test_outpath = os.path.join( 106 | "tests", "test_files", "fonts", "temp", "Ubuntu-Regular-dehinted.ttf" 107 | ) 108 | test_args = [test_inpath] 109 | 110 | # setup 111 | if os.path.isdir(test_dir): 112 | shutil.rmtree(test_dir) 113 | os.mkdir(test_dir) 114 | shutil.copyfile(notouch_inpath, test_inpath) 115 | 116 | # execute 117 | run(test_args) 118 | 119 | # test 120 | font_validator(test_outpath) 121 | 122 | # tear down 123 | shutil.rmtree(test_dir) 124 | 125 | 126 | def test_default_run_opensans_vf(): 127 | """This is used to test cvar table removal in a var font""" 128 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 129 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "OpenSans-VF.ttf") 130 | test_inpath = os.path.join("tests", "test_files", "fonts", "temp", "OpenSans.ttf") 131 | test_outpath = os.path.join( 132 | "tests", "test_files", "fonts", "temp", "OpenSans-dehinted.ttf" 133 | ) 134 | test_args = [test_inpath] 135 | 136 | # setup 137 | if os.path.isdir(test_dir): 138 | shutil.rmtree(test_dir) 139 | os.mkdir(test_dir) 140 | shutil.copyfile(notouch_inpath, test_inpath) 141 | 142 | # execute 143 | run(test_args) 144 | 145 | # test 146 | font_validator(test_outpath) 147 | assert "cvar" not in TTFont(test_outpath) 148 | 149 | # tear down 150 | shutil.rmtree(test_dir) 151 | 152 | 153 | def test_default_run_opensans_vf_keep_cvar(): 154 | """This is used to test cvar table removal in a var font""" 155 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 156 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "OpenSans-VF.ttf") 157 | test_inpath = os.path.join("tests", "test_files", "fonts", "temp", "OpenSans.ttf") 158 | test_outpath = os.path.join( 159 | "tests", "test_files", "fonts", "temp", "OpenSans-dehinted.ttf" 160 | ) 161 | test_args = [test_inpath, "--keep-cvar"] 162 | 163 | # setup 164 | if os.path.isdir(test_dir): 165 | shutil.rmtree(test_dir) 166 | os.mkdir(test_dir) 167 | shutil.copyfile(notouch_inpath, test_inpath) 168 | 169 | # execute 170 | run(test_args) 171 | 172 | # test 173 | font_validator(test_outpath) 174 | assert "cvar" in TTFont(test_outpath) 175 | 176 | # tear down 177 | shutil.rmtree(test_dir) 178 | 179 | 180 | def test_run_roboto_keep_cvt(): 181 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 182 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 183 | test_inpath = os.path.join( 184 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 185 | ) 186 | test_outpath = os.path.join( 187 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 188 | ) 189 | test_args = [test_inpath, "--keep-cvt"] 190 | 191 | # setup 192 | if os.path.isdir(test_dir): 193 | shutil.rmtree(test_dir) 194 | os.mkdir(test_dir) 195 | shutil.copyfile(notouch_inpath, test_inpath) 196 | 197 | # execute 198 | run(test_args) 199 | 200 | # test 201 | tt = TTFont(test_outpath) 202 | assert "cvt " in tt 203 | 204 | # tear down 205 | shutil.rmtree(test_dir) 206 | 207 | 208 | def test_run_roboto_keep_fpgm(): 209 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 210 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 211 | test_inpath = os.path.join( 212 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 213 | ) 214 | test_outpath = os.path.join( 215 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 216 | ) 217 | test_args = [test_inpath, "--keep-fpgm"] 218 | 219 | # setup 220 | if os.path.isdir(test_dir): 221 | shutil.rmtree(test_dir) 222 | os.mkdir(test_dir) 223 | shutil.copyfile(notouch_inpath, test_inpath) 224 | 225 | # execute 226 | run(test_args) 227 | 228 | # test 229 | tt = TTFont(test_outpath) 230 | assert "fpgm" in tt 231 | 232 | # tear down 233 | shutil.rmtree(test_dir) 234 | 235 | 236 | def test_run_roboto_keep_hdmx(): 237 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 238 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 239 | test_inpath = os.path.join( 240 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 241 | ) 242 | test_outpath = os.path.join( 243 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 244 | ) 245 | test_args = [test_inpath, "--keep-hdmx"] 246 | 247 | # setup 248 | if os.path.isdir(test_dir): 249 | shutil.rmtree(test_dir) 250 | os.mkdir(test_dir) 251 | shutil.copyfile(notouch_inpath, test_inpath) 252 | 253 | # execute 254 | run(test_args) 255 | 256 | # test 257 | tt = TTFont(test_outpath) 258 | assert "hdmx" in tt 259 | 260 | # tear down 261 | shutil.rmtree(test_dir) 262 | 263 | 264 | def test_run_roboto_keep_ltsh(): 265 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 266 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 267 | test_inpath = os.path.join( 268 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 269 | ) 270 | test_outpath = os.path.join( 271 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 272 | ) 273 | test_args = [test_inpath, "--keep-ltsh"] 274 | 275 | # setup 276 | if os.path.isdir(test_dir): 277 | shutil.rmtree(test_dir) 278 | os.mkdir(test_dir) 279 | shutil.copyfile(notouch_inpath, test_inpath) 280 | 281 | # execute 282 | run(test_args) 283 | 284 | # test 285 | tt = TTFont(test_outpath) 286 | assert "LTSH" in tt 287 | 288 | # tear down 289 | shutil.rmtree(test_dir) 290 | 291 | 292 | def test_run_roboto_keep_prep(): 293 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 294 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 295 | test_inpath = os.path.join( 296 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 297 | ) 298 | test_outpath = os.path.join( 299 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 300 | ) 301 | test_args = [test_inpath, "--keep-prep"] 302 | 303 | # setup 304 | if os.path.isdir(test_dir): 305 | shutil.rmtree(test_dir) 306 | os.mkdir(test_dir) 307 | shutil.copyfile(notouch_inpath, test_inpath) 308 | 309 | # execute 310 | run(test_args) 311 | 312 | # test 313 | tt = TTFont(test_outpath) 314 | assert "prep" in tt 315 | 316 | # tear down 317 | shutil.rmtree(test_dir) 318 | 319 | 320 | def test_run_noto_keep_ttfa(): # this has to be tested in Noto as it contains a TTFA table 321 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 322 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "NotoSans-Regular.ttf") 323 | test_inpath = os.path.join( 324 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 325 | ) 326 | test_outpath = os.path.join( 327 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular-dehinted.ttf" 328 | ) 329 | test_args = [test_inpath, "--keep-ttfa"] 330 | 331 | # setup 332 | if os.path.isdir(test_dir): 333 | shutil.rmtree(test_dir) 334 | os.mkdir(test_dir) 335 | shutil.copyfile(notouch_inpath, test_inpath) 336 | 337 | # execute 338 | run(test_args) 339 | 340 | # test 341 | tt = TTFont(test_outpath) 342 | assert "TTFA" in tt 343 | 344 | # tear down 345 | shutil.rmtree(test_dir) 346 | 347 | 348 | def test_default_run_ubuntu_keep_vdmx(): 349 | """This is used to test VDMX table removal""" 350 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 351 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Ubuntu-Regular.ttf") 352 | test_inpath = os.path.join( 353 | "tests", "test_files", "fonts", "temp", "Ubuntu-Regular.ttf" 354 | ) 355 | test_outpath = os.path.join( 356 | "tests", "test_files", "fonts", "temp", "Ubuntu-Regular-dehinted.ttf" 357 | ) 358 | test_args = [test_inpath, "--keep-vdmx"] 359 | 360 | # setup 361 | if os.path.isdir(test_dir): 362 | shutil.rmtree(test_dir) 363 | os.mkdir(test_dir) 364 | shutil.copyfile(notouch_inpath, test_inpath) 365 | 366 | # execute 367 | run(test_args) 368 | 369 | # test 370 | tt = TTFont(test_outpath) 371 | assert "VDMX" in tt 372 | 373 | # tear down 374 | shutil.rmtree(test_dir) 375 | 376 | 377 | def test_run_roboto_keep_glyf(): 378 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 379 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 380 | test_inpath = os.path.join( 381 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 382 | ) 383 | test_outpath = os.path.join( 384 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 385 | ) 386 | test_args = [test_inpath, "--keep-glyf"] 387 | 388 | # setup 389 | if os.path.isdir(test_dir): 390 | shutil.rmtree(test_dir) 391 | os.mkdir(test_dir) 392 | shutil.copyfile(notouch_inpath, test_inpath) 393 | 394 | # execute 395 | run(test_args) 396 | 397 | # test 398 | tt_pre = TTFont(test_inpath) 399 | tt_post = TTFont(test_outpath) 400 | assert tt_pre["glyf"] == tt_post["glyf"] 401 | 402 | # tear down 403 | shutil.rmtree(test_dir) 404 | 405 | 406 | def test_run_roboto_keep_gasp(): 407 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 408 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 409 | test_inpath = os.path.join( 410 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 411 | ) 412 | test_outpath = os.path.join( 413 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 414 | ) 415 | test_args = [test_inpath, "--keep-gasp"] 416 | 417 | # setup 418 | if os.path.isdir(test_dir): 419 | shutil.rmtree(test_dir) 420 | os.mkdir(test_dir) 421 | shutil.copyfile(notouch_inpath, test_inpath) 422 | 423 | # execute 424 | run(test_args) 425 | 426 | # test 427 | tt = TTFont(test_outpath) 428 | assert tt["gasp"].gaspRange == {8: 2, 65535: 15} # unmodified value in Roboto 429 | 430 | # tear down 431 | shutil.rmtree(test_dir) 432 | 433 | 434 | def test_run_noto_keep_maxp(): 435 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 436 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "NotoSans-Regular.ttf") 437 | test_inpath = os.path.join( 438 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 439 | ) 440 | test_outpath = os.path.join( 441 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular-dehinted.ttf" 442 | ) 443 | test_args = [test_inpath, "--keep-maxp"] 444 | 445 | # setup 446 | if os.path.isdir(test_dir): 447 | shutil.rmtree(test_dir) 448 | os.mkdir(test_dir) 449 | shutil.copyfile(notouch_inpath, test_inpath) 450 | 451 | # execute 452 | run(test_args) 453 | 454 | # test 455 | tt = TTFont(test_outpath) 456 | assert tt["maxp"].maxZones != 0 457 | assert tt["maxp"].maxTwilightPoints != 0 458 | assert tt["maxp"].maxStorage != 0 459 | assert tt["maxp"].maxFunctionDefs != 0 460 | assert tt["maxp"].maxStackElements != 0 461 | assert tt["maxp"].maxSizeOfInstructions != 0 462 | 463 | # tear down 464 | shutil.rmtree(test_dir) 465 | 466 | 467 | def test_run_roboto_keep_head(): 468 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 469 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 470 | test_inpath = os.path.join( 471 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 472 | ) 473 | test_outpath = os.path.join( 474 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 475 | ) 476 | test_args = [test_inpath, "--keep-head"] 477 | 478 | # setup 479 | if os.path.isdir(test_dir): 480 | shutil.rmtree(test_dir) 481 | os.mkdir(test_dir) 482 | shutil.copyfile(notouch_inpath, test_inpath) 483 | 484 | # execute 485 | run(test_args) 486 | 487 | # test 488 | tt = TTFont(test_outpath) 489 | assert (tt["head"].flags & 1 << 4) != 0 490 | 491 | # tear down 492 | shutil.rmtree(test_dir) 493 | 494 | 495 | def test_run_with_outfile_path_roboto(): 496 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 497 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 498 | test_inpath = os.path.join( 499 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 500 | ) 501 | test_outpath = os.path.join( 502 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehintilio.ttf" 503 | ) 504 | test_args = [test_inpath, "--out", test_outpath] 505 | 506 | # setup 507 | if os.path.isdir(test_dir): 508 | shutil.rmtree(test_dir) 509 | os.mkdir(test_dir) 510 | shutil.copyfile(notouch_inpath, test_inpath) 511 | 512 | # execute 513 | run(test_args) 514 | 515 | # test 516 | font_validator(test_outpath) 517 | 518 | # tear down 519 | shutil.rmtree(test_dir) 520 | 521 | 522 | def test_run_with_outfile_path_noto(): 523 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 524 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "NotoSans-Regular.ttf") 525 | test_inpath = os.path.join( 526 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 527 | ) 528 | test_outpath = os.path.join( 529 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular-dehintilio.ttf" 530 | ) 531 | test_args = [test_inpath, "-o", test_outpath] 532 | 533 | # setup 534 | if os.path.isdir(test_dir): 535 | shutil.rmtree(test_dir) 536 | os.mkdir(test_dir) 537 | shutil.copyfile(notouch_inpath, test_inpath) 538 | 539 | # execute 540 | run(test_args) 541 | 542 | # test 543 | font_validator(test_outpath) 544 | 545 | # tear down 546 | shutil.rmtree(test_dir) 547 | 548 | 549 | def test_default_run_roboto_quiet_flag_stdout_test(capsys): 550 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 551 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "Roboto-Regular.ttf") 552 | test_inpath = os.path.join( 553 | "tests", "test_files", "fonts", "temp", "Roboto-Regular.ttf" 554 | ) 555 | test_outpath = os.path.join( 556 | "tests", "test_files", "fonts", "temp", "Roboto-Regular-dehinted.ttf" 557 | ) 558 | test_args = ["--quiet", test_inpath] 559 | 560 | # setup 561 | if os.path.isdir(test_dir): 562 | shutil.rmtree(test_dir) 563 | os.mkdir(test_dir) 564 | shutil.copyfile(notouch_inpath, test_inpath) 565 | 566 | # execute 567 | run(test_args) 568 | captured = capsys.readouterr() 569 | assert captured.out == "" 570 | 571 | # test 572 | font_validator(test_outpath) 573 | 574 | # tear down 575 | shutil.rmtree(test_dir) 576 | 577 | 578 | # 579 | # Validation error testing 580 | # 581 | 582 | 583 | def test_run_with_invalid_filepath(): 584 | with pytest.raises(SystemExit): 585 | run(["bogusfile.txt"]) 586 | 587 | 588 | def test_run_with_non_font_file(): 589 | with pytest.raises(SystemExit): 590 | run([os.path.join("tests", "test_files", "text", "test.txt")]) 591 | 592 | 593 | def test_run_dehinted_file_write_inplace(): 594 | test_dir = os.path.join("tests", "test_files", "fonts", "temp") 595 | notouch_inpath = os.path.join("tests", "test_files", "fonts", "NotoSans-Regular.ttf") 596 | test_inpath = os.path.join( 597 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 598 | ) 599 | test_outpath = os.path.join( 600 | "tests", "test_files", "fonts", "temp", "NotoSans-Regular.ttf" 601 | ) 602 | test_args = [test_inpath, "-o", test_outpath] 603 | 604 | # setup 605 | if os.path.isdir(test_dir): 606 | shutil.rmtree(test_dir) 607 | os.mkdir(test_dir) 608 | shutil.copyfile(notouch_inpath, test_inpath) 609 | 610 | # execute 611 | with pytest.raises(SystemExit): 612 | run(test_args) 613 | 614 | # tear down 615 | shutil.rmtree(test_dir) 616 | --------------------------------------------------------------------------------