├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish-release.yml │ ├── py-ci.yml │ ├── py-coverage.yml │ └── py-lint.yml ├── .gitignore ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── codecov.yml ├── docs └── LICENSE.md ├── lib ├── __init__.py └── fontline │ ├── __init__.py │ ├── app.py │ ├── commands.py │ ├── metrics.py │ ├── settings.py │ └── utilities.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_filepaths.py ├── test_main.py ├── test_metrics.py ├── test_percent_cmd.py ├── test_report_cmd.py └── testingfiles │ ├── FiraMono-Regular.ttf │ ├── Hack-Regular-fsSelection-bit7-set.ttf │ ├── Hack-Regular.otf │ ├── Hack-Regular.ttf │ ├── SourceCodePro-Regular.ttf │ ├── backup │ ├── FiraMono-Regular.ttf │ ├── Hack-Regular.otf │ ├── Hack-Regular.ttf │ ├── SourceCodePro-Regular.ttf │ └── bogus.txt │ └── bogus.txt └── tox.ini /.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 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 0 * * 1' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.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 | python setup.py sdist bdist_wheel 44 | twine upload dist/* 45 | -------------------------------------------------------------------------------- /.github/workflows/py-ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: [push] 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.8"] 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 | -------------------------------------------------------------------------------- /.github/workflows/py-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push] 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 fontline -m py.test 49 | coverage report -m 50 | coverage xml 51 | - name: Upload coverage to Codecov 52 | uses: codecov/codecov-action@v2 53 | with: 54 | file: ./coverage.xml 55 | -------------------------------------------------------------------------------- /.github/workflows/py-lint.yml: -------------------------------------------------------------------------------- 1 | name: Python Lints 2 | 3 | on: [push] 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/fontline" 26 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | #Ipython Notebook 61 | .ipynb_checkpoints 62 | 63 | # PyCharm files 64 | .idea/ 65 | 66 | # Project files 67 | tests/runner.py 68 | tests/profiler.py 69 | 70 | # VS Code files 71 | .vscode/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v3.1.4 4 | 5 | - bump fonttools dependency to v4.17.0 6 | - update setup.py classifiers to remove Py2.7, Py < 3.6 7 | - update setup.py classifiers to include Py 3.7, 3.8, 3.9 8 | - remove docs/README.rst (no longer used for PyPI release documentation and not maintained) 9 | 10 | ### v3.1.3 11 | 12 | - add cPython 3.9 unit testing 13 | - add CodeQL static source code testing 14 | - update fonttools dependency to v4.16.1 15 | 16 | ### v3.1.2 17 | 18 | - adjust line length to < 90 19 | - transition to GitHub Actions CI testing service 20 | - update fonttools dependency to v4.14.0 21 | 22 | ### v3.1.1 23 | 24 | - remove Py2 wheels 25 | 26 | ### v3.1.0 27 | 28 | - add requirements.txt file with pinned dependency versions 29 | - update all build dependencies to current release versions 30 | - the fontTools dependency updates add support for Unicode 13 31 | - convert PyPI documentation to repository README Markdown file 32 | - add Py3.8 interpreter to CI testing 33 | 34 | ### v3.0.0 35 | 36 | - added baseline to baseline distance calculations for hhea, typo, and win metrics to reports 37 | - added fsSelection bit 7 `USE_TYPO_METRICS` bit flag setting to report 38 | - standard output report formatting improvements for `report` subcommand 39 | - removed Py3.5 interpreter testing, we will support for Py3.6+ only as of this release 40 | - add support for automated source code coverage push to Codecov from Travis 41 | 42 | ### v2.0.0 43 | 44 | - changed copyright notice from "Christopher Simpkins" to "Source Foundry Authors" 45 | - eliminated Python 2 interpreter support 46 | - updated shebang lines to `#!/usr/bin/env python3` 47 | - black formatting for source files 48 | - refactored Travis CI settings file 49 | - modified Travis CI testing to Python 3.5 - 3.7 50 | - refactored Appveyor CI settings file 51 | - modified Appveyor CI testing to Python 3.5 - 3.7 52 | - 53 | 54 | ### v1.0.1 55 | 56 | - removed unused variables in commands module 57 | - add docs/LICENSE.md to release archives and Python wheels 58 | - remove Pipfile and Pipfile.lock from version control 59 | 60 | ### v1.0.0 61 | 62 | - initial stable/production release 63 | 64 | ### v0.7.1 65 | 66 | - bug fix for report failures when the xHeight and CapHeight values are missing from OpenType OS/2 tables in some fonts 67 | 68 | ### v0.7.0 69 | 70 | - added [OS/2] CapHeight metric to report table 71 | - added [OS/2] xHeight metric to report table 72 | - modified version string parsing for report to support fonts that do not contain langID=0 tables 73 | - added release.sh shell script 74 | - updated Travis CI testing settings 75 | - updated Appveyor CI testing settings 76 | - updated tox.ini Python tox testing settings 77 | 78 | ### v0.6.1 79 | 80 | - minor Python style fixes (PR #8 by @moyogo) 81 | 82 | ### v0.6.0 83 | 84 | - modified percent command calculations to maintain vertical metrics approaches in the original font design 85 | - added vertical metrics modification support for fonts designed with the following vertical metrics approaches: 86 | - Google style metrics where TypoLinegap = hhea linegap = 0, internal leading is present in [OS/2] TypoAsc/TypoDesc, [hhea] Asc/Desc, [OS/2] winAsc/winDesc 87 | - Adobe style metrics where TypoLinegap = hhea linegap = 0, TypoAsc - TypoDesc = UPM, internal leading in [hhea] Asc/Desc & [OS/2] winAsc/winDesc 88 | 89 | ### v0.5.4 90 | 91 | - fix for font argument file path bug OSX/Linux/Win 92 | 93 | ### v0.5.3 94 | 95 | - added [head] yMax metric to report 96 | - added [head] yMin metric to report 97 | - added [OS/2] (TypoAscender + TypoDescender + TypoLineGap) / UPM calculation to report 98 | - added [OS/2] (winAsc + winDesc) / UPM calculation to report 99 | - added [OS/2] (hhea Asc + Desc) / UPM calculation to report 100 | - removed [OS/2] TypoLineGap / UPM from the report 101 | 102 | ### v0.5.2 103 | 104 | - percent command: now forces entry of a percent integer value > 0, reports error & exits with attempts to enter values <= 0 105 | - percent command: added standard output warning if there is an attempt to modify to a percent value > 100% 106 | - minor standard output user message updates 107 | 108 | ### v0.5.1 109 | 110 | - fixed implicit string cast / concatenation bug with Python 3.x interpreters 111 | - updated usage message string formatting 112 | - updated in application message string formatting 113 | 114 | ### v0.5.0 115 | 116 | - initial release 117 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include docs/LICENSE.md 2 | include docs/README.rst 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/font-line?color=blueviolet&label=PyPI&logo=python&logoColor=white)](https://pypi.org/project/font-line) 4 | ![Python CI](https://github.com/source-foundry/font-line/workflows/Python%20CI/badge.svg) 5 | ![Python Lints](https://github.com/source-foundry/font-line/workflows/Python%20Lints/badge.svg) 6 | [![codecov.io](https://codecov.io/github/source-foundry/font-line/coverage.svg?branch=master)](https://codecov.io/github/source-foundry/font-line?branch=master) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/d77b55866c794a5a9dd3b3dfea9ec318)](https://www.codacy.com/app/SourceFoundry/font-line) 8 | 9 | ## About 10 | 11 | font-line is a libre, open source command line tool for OpenType vertical metrics reporting and command line based font line spacing modifications. It supports `.ttf` and `.otf` font builds. 12 | 13 | ## Contents 14 | 15 | - [Install Guide](https://github.com/source-foundry/font-line#install) 16 | - [Usage](https://github.com/source-foundry/font-line#usage) - [Vertical Metrics Reporting](https://github.com/source-foundry/font-line#vertical-metrics-reporting) - [Line Spacing Modifications](https://github.com/source-foundry/font-line#vertical-metrics-modifications) 17 | - [Changelog](https://github.com/source-foundry/font-line/blob/master/CHANGELOG.md) 18 | - [License](https://github.com/source-foundry/font-line/blob/master/docs/LICENSE.md) 19 | 20 | ## Quickstart 21 | 22 | - Install: `$ pip3 install font-line` 23 | - Metrics Report: `$ font-line report [font path]` 24 | - Modify line spacing: `$ font-line percent [integer %] [font path]` 25 | - Help: `$ font-line --help` 26 | 27 | ## Install 28 | 29 | font-line is built with Python and supports Python 3.7+ interpreters. Check your installed Python version on the command line with the command: 30 | 31 | ``` 32 | $ python3 --version 33 | ``` 34 | 35 | Use either of the following methods to install font-line on your system. 36 | 37 | ### pip Install 38 | 39 | The latest font-line release is available through the Python Package Index and can be installed with pip: 40 | 41 | ``` 42 | $ pip3 install font-line 43 | ``` 44 | 45 | To upgrade to a new version of font-line after a pip install, use the command `pip3 install --upgrade font-line`. 46 | 47 | ### Download Project Repository and Install 48 | 49 | The current repository version (which may be ahead of the PyPI release) can be installed by [downloading the repository](https://github.com/source-foundry/font-line/archive/master.zip) or cloning it with git: 50 | 51 | ``` 52 | git clone https://github.com/source-foundry/font-line.git 53 | ``` 54 | 55 | Navigate to the top level repository directory and enter the following command: 56 | 57 | ``` 58 | $ pip3 install . 59 | ``` 60 | 61 | Follow the same instructions to upgrade to a new version of the application if you install with this approach. 62 | 63 | ## Usage 64 | 65 | font-line works via sub-commands to the `font-line` command line executable. The following sub-commands are available: 66 | 67 | - `percent` - modify the line spacing of a font to a percent of the Ascender to Descender distance 68 | - `report` - report OpenType metrics values for a font 69 | 70 | Usage of these sub-commands is described in detail below. 71 | 72 | ### Vertical Metrics Reporting 73 | 74 | The following OpenType vertical metrics values and calculated values derived from these data are displayed with the `report` sub-command: 75 | 76 | - [OS/2] TypoAscender 77 | - [OS/2] TypoDescender 78 | - [OS/2] WinAscent 79 | - [OS/2] WinDescent 80 | - [OS/2] TypoLineGap 81 | - [OS/2] xHeight 82 | - [OS/2] CapHeight 83 | - [hhea] Ascent 84 | - [hhea] Descent 85 | - [hhea] lineGap 86 | - [head] unitsPerEm 87 | - [head] yMax 88 | - [head] yMin 89 | 90 | #### `report` Sub-Command Usage 91 | 92 | Enter one or more font path arguments to the command: 93 | 94 | ``` 95 | $ font-line report [fontpath 1] 96 | ``` 97 | 98 | Here is an example of the report generated with the Hack typeface file `Hack-Regular.ttf` using the command: 99 | 100 | ``` 101 | $ font-line report Hack-Regular.ttf 102 | ``` 103 | 104 | #### Example Font Vertical Metrics Report 105 | 106 | ``` 107 | === Hack-Regular.ttf === 108 | Version 3.003;[3114f1256]-release 109 | SHA1: b1cd50ba36380d6d6ada37facfc954a8f20c15ba 110 | 111 | :::::::::::::::::::::::::::::::::::::::::::::::::: 112 | Metrics 113 | :::::::::::::::::::::::::::::::::::::::::::::::::: 114 | [head] Units per Em: 2048 115 | [head] yMax: 2027 116 | [head] yMin: -605 117 | [OS/2] CapHeight: 1493 118 | [OS/2] xHeight: 1120 119 | [OS/2] TypoAscender: 1556 120 | [OS/2] TypoDescender: -492 121 | [OS/2] WinAscent: 1901 122 | [OS/2] WinDescent: 483 123 | [hhea] Ascent: 1901 124 | [hhea] Descent: -483 125 | 126 | [hhea] LineGap: 0 127 | [OS/2] TypoLineGap: 410 128 | 129 | :::::::::::::::::::::::::::::::::::::::::::::::::: 130 | Ascent to Descent Calculations 131 | :::::::::::::::::::::::::::::::::::::::::::::::::: 132 | [hhea] Ascent to Descent: 2384 133 | [OS/2] TypoAscender to TypoDescender: 2048 134 | [OS/2] WinAscent to WinDescent: 2384 135 | 136 | :::::::::::::::::::::::::::::::::::::::::::::::::: 137 | Delta Values 138 | :::::::::::::::::::::::::::::::::::::::::::::::::: 139 | [hhea] Ascent to [OS/2] TypoAscender: 345 140 | [hhea] Descent to [OS/2] TypoDescender: -9 141 | [OS/2] WinAscent to [OS/2] TypoAscender: 345 142 | [OS/2] WinDescent to [OS/2] TypoDescender: -9 143 | 144 | :::::::::::::::::::::::::::::::::::::::::::::::::: 145 | Baseline to Baseline Distances 146 | :::::::::::::::::::::::::::::::::::::::::::::::::: 147 | hhea metrics: 2384 148 | typo metrics: 2458 149 | win metrics: 2384 150 | 151 | [OS/2] fsSelection USE_TYPO_METRICS bit set: False 152 | 153 | :::::::::::::::::::::::::::::::::::::::::::::::::: 154 | Ratios 155 | :::::::::::::::::::::::::::::::::::::::::::::::::: 156 | hhea metrics / UPM: 1.16 157 | typo metrics / UPM: 1.2 158 | win metrics / UPM: 1.16 159 | ``` 160 | 161 | The report includes the font version string, a SHA-1 hash digest of the font file, and OpenType table metrics that are associated with line spacing in the font. 162 | 163 | Unix/Linux/OS X users can write this report to a file with the `>` command line idiom: 164 | 165 | ``` 166 | $ font-line report TheFont.ttf > font-report.txt 167 | ``` 168 | 169 | Modify the `font-report.txt` file path above to the file path string of your choice. 170 | 171 | #### Baseline to Baseline Distance Calculations 172 | 173 | Baseline to baseline distance (BTBD) calculations are performed according to the [Microsoft Recommendations for OpenType Fonts](https://docs.microsoft.com/en-us/typography/opentype/spec/recom#baseline-to-baseline-distances) and [OpenType OS/2 table specification](https://docs.microsoft.com/en-us/typography/opentype/spec/os2). 174 | 175 | ##### hhea Metrics 176 | 177 | ``` 178 | BTBD = hhea.Ascent + abs(hhea.Descent) + hhea.LineGap 179 | ``` 180 | 181 | ##### typo Metrics 182 | 183 | ``` 184 | BTBD = OS/2.typoAscent + abs(OS/2.typoDescent) + OS/2.typoLineGap 185 | ``` 186 | 187 | ##### win Metrics 188 | 189 | ``` 190 | BTBD = OS/2.winAscent + OS/2.winDescent + [External Leading] 191 | ``` 192 | 193 | where external leading is defined as: 194 | 195 | ``` 196 | MAX(0, hhea.LineGap - ((OS/2.WinAscent + OS/2.winDescent) - (hhea.Ascent - hhea.Descent))) 197 | ``` 198 | 199 | ### Vertical Metrics Modifications 200 | 201 | font-line supports automated line spacing modifications to a user-defined percentage of the units per em metric. This value will be abbreviated as UPM below. 202 | 203 | #### `percent` Sub-Command Usage 204 | 205 | Enter the desired percentage of the UPM as the first argument to the command. This should be _entered as an integer value_. Then enter one or more font paths to which you would like to apply your font metrics changes. 206 | 207 | ``` 208 | $ font-line percent [percent change] [fontpath 1] 209 | ``` 210 | 211 | A common default value used by typeface designers is 20% UPM. To modify a font on the path `TheFont.ttf` to 20% of the UPM metric, you would enter the following command: 212 | 213 | ``` 214 | $ font-line percent 20 TheFont.ttf 215 | ``` 216 | 217 | Increase or decrease the integer value to increase or decrease your line spacing accordingly. 218 | 219 | The original font file is preserved in an unmodified version and the modified file write takes place on a new path defined as `[original filename]-linegap[percent].[ttf|otf]`. The path to the file is reported to you in the standard output after the modification is completed. font-line does not modify the glyph set or hints applied to the font. See the Details section below for a description of the OpenType table modifications that occur when the application is used on a font file. 220 | 221 | You can inspect the vertical metrics in the new font file with the `report` sub-command (see Usage above). 222 | 223 | #### Details of Font Metrics Changes with `percent` Sub-Command 224 | 225 | The interpretation and display of these multiple vertical metrics values is platform and application dependent. [There is no broadly accepted "best" approach](https://github.com/source-foundry/font-line/issues/2). As such, font-line attempts to preserve the original metrics design in the font when modifications are made with the `percent` sub-command. 226 | 227 | font-line currently supports three commonly used vertical metrics approaches. 228 | 229 | **Vertical Metrics Approach 1**: 230 | 231 | Where metrics are defined as: 232 | 233 | - [OS/2] TypoLinegap = 0 234 | - [hhea] linegap = 0 235 | - [OS/2] TypoAscender = [OS/2] winAscent = [hhea] Ascent 236 | - [OS/2] TypoDescender = [OS/2] winDescent = [hhea] Descent 237 | 238 | font-line calculates a delta value for the total expected height based upon the % UPM value defined on the command line. The difference between this value and the observed number of units that span the [OS/2] winAscent to winDescent values is divided by half and then added to (for increased line spacing) or subtracted from (for decreased line spacing) each of the three sets of Ascender/Descender values in the font. The [OS/2] TypoLinegap and [hhea] linegap values are not modified. 239 | 240 | **Vertical Metrics Approach 2** 241 | 242 | Where metrics are defined as: 243 | 244 | - [OS/2] TypoLinegap = 0 245 | - [hhea] linegap = 0 246 | - [OS/2] TypoAscender + TypoDescender = UPM 247 | - [OS/2] winAscent = [hhea] Ascent 248 | - [OS/2] winDescent = [hhea] Descent 249 | 250 | font-line calculates a delta value for the total expected height based upon the % UPM value defined on the command line. The difference between this value and the observed number of units that span the [OS/2] winAscent to winDescent values is divided by half and then added to (for increased line spacing) or subtracted from (for decreased line spacing) the [OS/2] winAsc/winDesc and [hhea] Asc/Desc values. The [OS/2] TypoAsc/TypoDesc values are not modified and maintain a definition of size = UPM value. The [OS/2] TypoLinegap and [hhea] linegap values are not modified. 251 | 252 | **Vertical Metrics Approach 3** 253 | 254 | Where metrics are defined as: 255 | 256 | - [OS/2] TypoAscender + TypoDescender = UPM 257 | - [OS/2] TypoLinegap is set to leading value 258 | - [hhea] linegap = 0 259 | - [OS/2] winAscent = [hhea] Ascent 260 | - [OS/2] winDescent = [hhea] Descent 261 | 262 | _Changes to the metrics values in the font are defined as_: 263 | 264 | - [OS/2] TypoLineGap = x% \* UPM value 265 | - [hhea] Ascent = [OS/2] TypoAscender + 0.5(modified TypoLineGap) 266 | - [hhea] Descent = [OS/2] TypoDescender + 0.5(modified TypoLineGap) 267 | - [OS/2] WinAscent = [OS/2] TypoAscender + 0.5(modified TypoLineGap) 268 | - [OS/2] WinDescent = [OS/2] TypoDescender + 0.5(modified TypoLineGap) 269 | 270 | Note that the internal leading modifications are split evenly across [hhea] Ascent & Descent values, and across [OS/2] WinAscent & WinDescent values. We add half of the new [OS/2] TypoLineGap value to the original [OS/2] TypoAscender or TypoDescender in order to define these new metrics properties. The [hhea] linegap value is always defined as zero. 271 | 272 | ### Important 273 | 274 | The newly defined vertical metrics values can lead to clipping of glyph components if not properly defined. There are no tests in font-line to provide assurance that this does not occur. We assume that the user is versed in these issues before use of the application and leave this testing to the designer / user before the modified fonts are used in a production setting. 275 | 276 | ## Issue Reporting 277 | 278 | Please [submit a new issue report](https://github.com/source-foundry/font-line/issues/new) on the project repository. 279 | 280 | ## Acknowledgments 281 | 282 | font-line is built with the fantastic [fontTools](https://github.com/fonttools/fonttools) Python library. 283 | 284 | ## License 285 | 286 | MIT License. See [LICENSE.md](docs/LICENSE.md) for details. 287 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Source Foundry Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/lib/__init__.py -------------------------------------------------------------------------------- /lib/fontline/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /lib/fontline/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | The app.py module defines a main() function that includes the logic for the `font-line` 5 | command line executable. 6 | 7 | This command line executable modifies the line spacing metrics in one or more fonts that 8 | are provided as command 9 | line arguments to the executable. It currently supports .ttf and .otf font builds. 10 | """ 11 | 12 | import sys 13 | 14 | from commandlines import Command 15 | from standardstreams import stdout, stderr 16 | 17 | from fontline import settings 18 | from fontline.commands import ( 19 | get_font_report, 20 | modify_linegap_percent, 21 | get_linegap_percent_filepath, 22 | ) 23 | from fontline.utilities import file_exists, is_supported_filetype 24 | 25 | 26 | def main(): 27 | """Defines the logic for the `font-line` command line executable""" 28 | c = Command() 29 | 30 | if c.does_not_validate_missing_args(): 31 | stderr( 32 | "[font-line] ERROR: Please include one or more arguments with your command." 33 | ) 34 | sys.exit(1) 35 | 36 | if c.is_help_request(): 37 | stdout(settings.HELP) 38 | sys.exit(0) 39 | elif c.is_version_request(): 40 | stdout(settings.VERSION) 41 | sys.exit(0) 42 | elif c.is_usage_request(): 43 | stdout(settings.USAGE) 44 | sys.exit(0) 45 | 46 | # REPORT sub-command 47 | if c.subcmd == "report": 48 | if c.argc < 2: 49 | stderr( 50 | "[font-line] ERROR: Missing file path argument(s) after the " 51 | "report subcommand." 52 | ) 53 | sys.exit(1) 54 | else: 55 | for fontpath in c.argv[1:]: 56 | # test for existence of file on path 57 | if file_exists(fontpath): 58 | # test that filepath includes file of a supported file type 59 | if is_supported_filetype(fontpath): 60 | stdout(get_font_report(fontpath)) 61 | else: 62 | stderr( 63 | "[font-line] ERROR: '" 64 | + fontpath 65 | + "' does not appear to be a supported font file type." 66 | ) 67 | else: 68 | stderr( 69 | "[font-line] ERROR: '" 70 | + fontpath 71 | + "' does not appear to be a valid filepath." 72 | ) 73 | # PERCENT sub-command 74 | elif c.subcmd == "percent": 75 | if c.argc < 3: 76 | stderr("[font-line] ERROR: Not enough arguments.") 77 | sys.exit(1) 78 | else: 79 | percent = c.argv[1] 80 | # test the percent integer argument 81 | try: 82 | percent_int = int( 83 | percent 84 | ) # test that the argument can be cast to an integer value 85 | if percent_int <= 0: 86 | stderr( 87 | "[font-line] ERROR: Please enter a percent value that is " 88 | "greater than zero." 89 | ) 90 | sys.exit(1) 91 | if percent_int > 100: 92 | stdout( 93 | "[font-line] Warning: You entered a percent value over 100%. " 94 | "Please confirm that this is your intended metrics modification." 95 | ) 96 | except ValueError: 97 | stderr( 98 | "[font-line] ERROR: You entered '" 99 | + percent 100 | + "'. This argument needs to be an integer value." 101 | ) 102 | sys.exit(1) 103 | for fontpath in c.argv[2:]: 104 | if file_exists(fontpath): 105 | if is_supported_filetype(fontpath): 106 | if modify_linegap_percent(fontpath, percent) is True: 107 | outpath = get_linegap_percent_filepath(fontpath, percent) 108 | stdout( 109 | "[font-line] '" 110 | + fontpath 111 | + "' successfully modified to '" 112 | + outpath 113 | + "'." 114 | ) 115 | else: # pragma: no cover 116 | stderr( 117 | "[font-line] ERROR: Unsuccessful modification of '" 118 | + fontpath 119 | + "'." 120 | ) 121 | else: 122 | stderr( 123 | "[font-line] ERROR: '" 124 | + fontpath 125 | + "' does not appear to be a supported font file type." 126 | ) 127 | else: 128 | stderr( 129 | "[font-line] ERROR: '" 130 | + fontpath 131 | + "' does not appear to be a valid filepath." 132 | ) 133 | else: 134 | stderr( 135 | "[font-lines] ERROR: You used an unsupported argument to the executable. " 136 | "Please review the `font-line --help` documentation and try again." 137 | ) 138 | sys.exit(1) 139 | -------------------------------------------------------------------------------- /lib/fontline/commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os.path 5 | 6 | from fontTools import ttLib 7 | from standardstreams import stderr 8 | 9 | 10 | from fontline.metrics import MetricsObject 11 | 12 | 13 | def get_font_report(fontpath): 14 | tt = ttLib.TTFont(fontpath) 15 | metrics = MetricsObject(tt, fontpath) 16 | 17 | # The file path header 18 | report = ["=== " + fontpath + " ==="] 19 | 20 | report.append(metrics.version) 21 | 22 | # The SHA1 string 23 | report.append("SHA1: " + metrics.sha1) 24 | report.append("") 25 | # The vertical metrics strings 26 | report.append(":" * 50) 27 | report.append(" Metrics") 28 | report.append(":" * 50) 29 | report.append("[head] Units per Em: {}".format(metrics.units_per_em)) 30 | report.append("[head] yMax: {}".format(metrics.ymax)) 31 | report.append("[head] yMin: {}".format(metrics.ymin)) 32 | report.append("[OS/2] CapHeight: {}".format(metrics.os2_cap_height)) 33 | report.append("[OS/2] xHeight: {}".format(metrics.os2_x_height)) 34 | report.append("[OS/2] TypoAscender: {}".format(metrics.os2_typo_ascender)) 35 | report.append("[OS/2] TypoDescender: {}".format(metrics.os2_typo_descender)) 36 | report.append("[OS/2] WinAscent: {}".format(metrics.os2_win_ascent)) 37 | report.append("[OS/2] WinDescent: {}".format(metrics.os2_win_descent)) 38 | report.append("[hhea] Ascent: {}".format(metrics.hhea_ascent)) 39 | report.append("[hhea] Descent: {}".format(metrics.hhea_descent)) 40 | report.append("") 41 | report.append("[hhea] LineGap: {}".format(metrics.hhea_linegap)) 42 | report.append("[OS/2] TypoLineGap: {}".format(metrics.os2_typo_linegap)) 43 | report.append("") 44 | 45 | report.append(":" * 50) 46 | report.append(" Ascent to Descent Calculations") 47 | report.append(":" * 50) 48 | report.append( 49 | "[hhea] Ascent to Descent: {}".format(metrics.hhea_total_height) 50 | ) 51 | report.append( 52 | "[OS/2] TypoAscender to TypoDescender: {}".format( 53 | metrics.os2_typo_total_height 54 | ) 55 | ) 56 | report.append( 57 | "[OS/2] WinAscent to WinDescent: {}".format(metrics.os2_win_total_height) 58 | ) 59 | report.append("") 60 | 61 | report.append(":" * 50) 62 | report.append(" Delta Values") 63 | report.append(":" * 50) 64 | report.append( 65 | "[hhea] Ascent to [OS/2] TypoAscender: {}".format( 66 | metrics.hhea_ascent - metrics.os2_typo_ascender 67 | ) 68 | ) 69 | report.append( 70 | "[hhea] Descent to [OS/2] TypoDescender: {}".format( 71 | metrics.os2_typo_descender - metrics.hhea_descent 72 | ) 73 | ) 74 | report.append( 75 | "[OS/2] WinAscent to [OS/2] TypoAscender: {}".format( 76 | metrics.os2_win_ascent - metrics.os2_typo_ascender 77 | ) 78 | ) 79 | report.append( 80 | "[OS/2] WinDescent to [OS/2] TypoDescender: {}".format( 81 | metrics.os2_win_descent + metrics.os2_typo_descender 82 | ) 83 | ) 84 | report.append("") 85 | report.append(":" * 50) 86 | report.append(" Baseline to Baseline Distances") 87 | report.append(":" * 50) 88 | report.append("hhea metrics: {}".format(metrics.hhea_btb_distance)) 89 | report.append("typo metrics: {}".format(metrics.typo_btb_distance)) 90 | report.append("win metrics: {}".format(metrics.win_btb_distance)) 91 | report.append("") 92 | report.append( 93 | "[OS/2] fsSelection USE_TYPO_METRICS bit set: {}".format( 94 | metrics.fsselection_bit7_set 95 | ) 96 | ) 97 | report.append("") 98 | report.append(":" * 50) 99 | report.append(" Ratios") 100 | report.append(":" * 50) 101 | report.append("hhea metrics / UPM: {0:.3g}".format(metrics.hheaascdesc_to_upm)) 102 | report.append("typo metrics / UPM: {0:.3g}".format(metrics.typo_to_upm)) 103 | report.append("win metrics / UPM: {0:.3g}".format(metrics.winascdesc_to_upm)) 104 | 105 | return "\n".join(report) 106 | 107 | 108 | def modify_linegap_percent(fontpath, percent): 109 | try: 110 | tt = ttLib.TTFont(fontpath) 111 | 112 | # get observed start values from the font 113 | os2_typo_ascender = tt["OS/2"].sTypoAscender 114 | os2_typo_descender = tt["OS/2"].sTypoDescender 115 | os2_typo_linegap = tt["OS/2"].sTypoLineGap 116 | hhea_ascent = tt["hhea"].ascent 117 | hhea_descent = tt["hhea"].descent 118 | units_per_em = tt["head"].unitsPerEm 119 | 120 | # calculate necessary delta values 121 | os2_typo_ascdesc_delta = os2_typo_ascender + -(os2_typo_descender) 122 | hhea_ascdesc_delta = hhea_ascent + -(hhea_descent) 123 | 124 | # define percent UPM from command line request 125 | factor = 1.0 * int(percent) / 100 126 | 127 | # define line spacing units 128 | line_spacing_units = int(factor * units_per_em) 129 | 130 | # define total height as UPM + line spacing units 131 | total_height = line_spacing_units + units_per_em 132 | 133 | # height calculations for adjustments 134 | delta_height = total_height - hhea_ascdesc_delta 135 | upper_lower_add_units = int(0.5 * delta_height) 136 | 137 | # redefine hhea linegap to 0 in all cases 138 | hhea_linegap = 0 139 | 140 | # Define metrics based upon original design approach in the font 141 | # Google metrics approach 142 | if os2_typo_linegap == 0 and (os2_typo_ascdesc_delta > units_per_em): 143 | # define values 144 | os2_typo_ascender += upper_lower_add_units 145 | os2_typo_descender -= upper_lower_add_units 146 | hhea_ascent += upper_lower_add_units 147 | hhea_descent -= upper_lower_add_units 148 | os2_win_ascent = hhea_ascent 149 | os2_win_descent = -hhea_descent 150 | # Adobe metrics approach 151 | elif os2_typo_linegap == 0 and (os2_typo_ascdesc_delta == units_per_em): 152 | hhea_ascent += upper_lower_add_units 153 | hhea_descent -= upper_lower_add_units 154 | os2_win_ascent = hhea_ascent 155 | os2_win_descent = -hhea_descent 156 | else: 157 | os2_typo_linegap = line_spacing_units 158 | hhea_ascent = int(os2_typo_ascender + 0.5 * os2_typo_linegap) 159 | hhea_descent = -(total_height - hhea_ascent) 160 | os2_win_ascent = hhea_ascent 161 | os2_win_descent = -hhea_descent 162 | 163 | # define updated values from above calculations 164 | tt["hhea"].lineGap = hhea_linegap 165 | tt["OS/2"].sTypoAscender = os2_typo_ascender 166 | tt["OS/2"].sTypoDescender = os2_typo_descender 167 | tt["OS/2"].sTypoLineGap = os2_typo_linegap 168 | tt["OS/2"].usWinAscent = os2_win_ascent 169 | tt["OS/2"].usWinDescent = os2_win_descent 170 | tt["hhea"].ascent = hhea_ascent 171 | tt["hhea"].descent = hhea_descent 172 | 173 | tt.save(get_linegap_percent_filepath(fontpath, percent)) 174 | return True 175 | except Exception as e: # pragma: no cover 176 | stderr( 177 | "[font-line] ERROR: Unable to modify the line spacing in the font file '" 178 | + fontpath 179 | + "'. " 180 | + str(e) 181 | ) 182 | sys.exit(1) 183 | 184 | 185 | def get_linegap_percent_filepath(fontpath, percent): 186 | fontpath_list = os.path.split(fontpath) 187 | font_dirname = fontpath_list[0] 188 | font_basename = fontpath_list[1] 189 | basepath_list = font_basename.split(".") 190 | outfile_basename = basepath_list[0] + "-linegap" + percent + "." + basepath_list[1] 191 | return os.path.join(font_dirname, outfile_basename) 192 | -------------------------------------------------------------------------------- /lib/fontline/metrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from fontline.utilities import get_sha1 4 | 5 | 6 | class MetricsObject: 7 | def __init__(self, tt, filepath): 8 | self.tt = tt 9 | self.filepath = filepath 10 | self.sha1 = get_sha1(self.filepath) 11 | # Version string 12 | self.version = "" 13 | for needle in tt["name"].names: 14 | if needle.nameID == 5: 15 | self.version = needle.toStr() 16 | break 17 | # Vertical metrics values as integers 18 | self.os2_typo_ascender = tt["OS/2"].sTypoAscender 19 | self.os2_typo_descender = tt["OS/2"].sTypoDescender 20 | self.os2_win_ascent = tt["OS/2"].usWinAscent 21 | self.os2_win_descent = tt["OS/2"].usWinDescent 22 | self.os2_typo_linegap = tt["OS/2"].sTypoLineGap 23 | try: 24 | self.os2_x_height = tt["OS/2"].sxHeight 25 | except AttributeError: 26 | self.os2_x_height = "---- (OS/2 table does not contain sxHeight record)" 27 | try: 28 | self.os2_cap_height = tt["OS/2"].sCapHeight 29 | except AttributeError: 30 | self.os2_cap_height = "---- (OS/2 table does not contain sCapHeight record)" 31 | self.hhea_ascent = tt["hhea"].ascent 32 | self.hhea_descent = tt["hhea"].descent 33 | self.hhea_linegap = tt["hhea"].lineGap 34 | self.ymax = tt["head"].yMax 35 | self.ymin = tt["head"].yMin 36 | self.units_per_em = tt["head"].unitsPerEm 37 | 38 | # Bit flag checks 39 | self.fsselection_bit7_mask = 1 << 7 40 | self.fsselection_bit7_set = ( 41 | tt["OS/2"].fsSelection & self.fsselection_bit7_mask 42 | ) != 0 43 | 44 | # Calculated values 45 | self.os2_typo_total_height = self.os2_typo_ascender + abs(self.os2_typo_descender) 46 | self.os2_win_total_height = self.os2_win_ascent + self.os2_win_descent 47 | self.hhea_total_height = self.hhea_ascent + abs(self.hhea_descent) 48 | 49 | self.hhea_btb_distance = self.hhea_total_height + self.hhea_linegap 50 | self.typo_btb_distance = self.os2_typo_total_height + self.os2_typo_linegap 51 | self.win_external_leading = self.hhea_linegap - ( 52 | (self.os2_win_ascent + self.os2_win_descent) 53 | - (self.hhea_ascent - self.hhea_descent) 54 | ) 55 | if self.win_external_leading < 0: 56 | self.win_external_leading = 0 57 | self.win_btb_distance = ( 58 | self.os2_win_ascent + self.os2_win_descent + self.win_external_leading 59 | ) 60 | 61 | self.typo_to_upm = 1.0 * self.typo_btb_distance / self.units_per_em 62 | self.winascdesc_to_upm = 1.0 * self.win_btb_distance / self.units_per_em 63 | self.hheaascdesc_to_upm = 1.0 * self.hhea_btb_distance / self.units_per_em 64 | -------------------------------------------------------------------------------- /lib/fontline/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # ------------------------------------------------------------------------------ 4 | # Application Name 5 | # ------------------------------------------------------------------------------ 6 | app_name = "font-line" 7 | 8 | # ------------------------------------------------------------------------------ 9 | # Version Number 10 | # ------------------------------------------------------------------------------ 11 | major_version = "3" 12 | minor_version = "1" 13 | patch_version = "4" 14 | 15 | # ------------------------------------------------------------------------------ 16 | # Help String 17 | # ------------------------------------------------------------------------------ 18 | 19 | HELP = """==================================================== 20 | font-line 21 | Copyright 2019 Christopher Simpkins 22 | MIT License 23 | Source: https://github.com/source-foundry/font-line 24 | ==================================================== 25 | 26 | ABOUT 27 | 28 | font-line is a font vertical metrics reporting and line spacing modification tool. 29 | 30 | SUB-COMMANDS 31 | 32 | percent - adjust font line spacing to % of UPM value 33 | report - generate report of font metrics and derived values 34 | 35 | OPTIONS 36 | 37 | -h | --help display application help 38 | -v | --version display application version 39 | --usage display usage information 40 | 41 | USAGE 42 | 43 | $ font-line report [fontpath 1] 44 | $ font-line percent [integer] [fontpath 1] 45 | $ font-line [-v|-h] [--help|--usage|--version] 46 | 47 | Reports are sent to the standard output stream with the `report` sub-command. 48 | 49 | Original font files are not modified when you use the `percent` sub-command. Instead 50 | a new file write occurs on a path that is displayed in the standard output stream when 51 | completed. No modifications are made to the original glyph set or hints associated with 52 | the original font build. 53 | 54 | For more information about the OpenType table modifications that occur, please see the 55 | project documentation at: 56 | 57 | https://github.com/source-foundry/font-line""" 58 | 59 | # ------------------------------------------------------------------------------ 60 | # Version String 61 | # ------------------------------------------------------------------------------ 62 | 63 | VERSION = "font-line v" + major_version + "." + minor_version + "." + patch_version 64 | 65 | 66 | # ------------------------------------------------------------------------------ 67 | # Usage String 68 | # ------------------------------------------------------------------------------ 69 | 70 | USAGE = """ 71 | Usage: font-line [subcommand] [font path 1] """ 72 | -------------------------------------------------------------------------------- /lib/fontline/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import hashlib 5 | 6 | 7 | def file_exists(filepath): 8 | """Tests for existence of a file on the string filepath""" 9 | if os.path.exists(filepath) and os.path.isfile( 10 | filepath 11 | ): # test that exists and is a file 12 | return True 13 | else: 14 | return False 15 | 16 | 17 | def is_supported_filetype(filepath): 18 | """Tests file extension to determine appropriate file type for the application""" 19 | testpath = filepath.lower() 20 | if testpath.endswith(".ttf") or testpath.endswith(".otf"): 21 | return True 22 | else: 23 | return False 24 | 25 | 26 | def get_sha1(filepath): 27 | with open(filepath, "rb") as bin_reader: 28 | data = bin_reader.read() 29 | return hashlib.sha1(data).hexdigest() 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | commandlines==0.4.1 8 | # via font-line (setup.py) 9 | fonttools==4.38.0 10 | # via font-line (setup.py) 11 | standardstreams==0.2.0 12 | # via font-line (setup.py) 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 0 3 | 4 | [metadata] 5 | license_file = docs/LICENSE.md 6 | 7 | [flake8] 8 | max-line-length = 90 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import sys 5 | from setuptools import setup, find_packages 6 | 7 | REQUIRES_PYTHON = ">=3.7.0" 8 | 9 | 10 | # Use repository Markdown README.md for PyPI long description 11 | try: 12 | with io.open("README.md", encoding="utf-8") as f: 13 | readme = f.read() 14 | except IOError as readme_e: 15 | sys.stderr.write( 16 | "[ERROR] setup.py: Failed to read the README.md file for the long description definition: {}".format( 17 | str(readme_e) 18 | ) 19 | ) 20 | raise readme_e 21 | 22 | 23 | def version_read(): 24 | settings_file = open( 25 | os.path.join(os.path.dirname(__file__), "lib", "fontline", "settings.py") 26 | ).read() 27 | major_regex = """major_version\s*?=\s*?["']{1}(\d+)["']{1}""" 28 | minor_regex = """minor_version\s*?=\s*?["']{1}(\d+)["']{1}""" 29 | patch_regex = """patch_version\s*?=\s*?["']{1}(\d+)["']{1}""" 30 | major_match = re.search(major_regex, settings_file) 31 | minor_match = re.search(minor_regex, settings_file) 32 | patch_match = re.search(patch_regex, settings_file) 33 | major_version = major_match.group(1) 34 | minor_version = minor_match.group(1) 35 | patch_version = patch_match.group(1) 36 | if len(major_version) == 0: 37 | major_version = 0 38 | if len(minor_version) == 0: 39 | minor_version = 0 40 | if len(patch_version) == 0: 41 | patch_version = 0 42 | return major_version + "." + minor_version + "." + patch_version 43 | 44 | 45 | setup( 46 | name="font-line", 47 | version=version_read(), 48 | description="A font vertical metrics reporting and line spacing adjustment tool", 49 | long_description=readme, 50 | long_description_content_type="text/markdown", 51 | url="https://github.com/source-foundry/font-line", 52 | license="MIT license", 53 | author="Christopher Simpkins", 54 | author_email="chris@sourcefoundry.org", 55 | platforms=["any"], 56 | python_requires=REQUIRES_PYTHON, 57 | packages=find_packages("lib"), 58 | package_dir={"": "lib"}, 59 | install_requires=["commandlines", "standardstreams", "fontTools"], 60 | entry_points={ 61 | "console_scripts": ["font-line = fontline.app:main"], 62 | }, 63 | keywords="font,typeface,fonts,spacing,line spacing,spaces,vertical metrics,metrics,type", 64 | include_package_data=True, 65 | classifiers=[ 66 | "Development Status :: 5 - Production/Stable", 67 | "Natural Language :: English", 68 | "License :: OSI Approved :: MIT License", 69 | "Operating System :: OS Independent", 70 | "Programming Language :: Python", 71 | "Programming Language :: Python :: 3", 72 | "Programming Language :: Python :: 3.7", 73 | "Programming Language :: Python :: 3.8", 74 | "Programming Language :: Python :: 3.9", 75 | "Programming Language :: Python :: 3.10", 76 | "Topic :: Software Development :: Libraries :: Python Modules", 77 | ], 78 | ) 79 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_filepaths.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import pytest 7 | 8 | from fontline.commands import get_linegap_percent_filepath 9 | 10 | test_filepath_one = "test.ttf" 11 | test_filepath_two = "./test.ttf" 12 | test_filepath_three = "/dir1/dir2/test.ttf" 13 | test_filepath_four = "~/dir1/dir2/test.ttf" 14 | percent_value = "10" 15 | 16 | expected_linegap_basename = "test-linegap" + percent_value + ".ttf" 17 | 18 | expected_testpath_list_one = os.path.split(test_filepath_one) 19 | expected_testpath_list_two = os.path.split(test_filepath_two) 20 | expected_testpath_list_three = os.path.split(test_filepath_three) 21 | expected_testpath_list_four = os.path.split(test_filepath_four) 22 | 23 | expected_testpath_one = os.path.join(expected_testpath_list_one[0], expected_linegap_basename) 24 | expected_testpath_two = os.path.join(expected_testpath_list_two[0], expected_linegap_basename) 25 | expected_testpath_three = os.path.join(expected_testpath_list_three[0], expected_linegap_basename) 26 | expected_testpath_four = os.path.join(expected_testpath_list_four[0], expected_linegap_basename) 27 | 28 | 29 | def test_linegap_outfile_filepath_basename(): 30 | response = get_linegap_percent_filepath(test_filepath_one, percent_value) 31 | assert response == expected_testpath_one 32 | 33 | 34 | def test_linegap_outfile_filepath_samedir_withdotsyntax(): 35 | response = get_linegap_percent_filepath(test_filepath_two, percent_value) 36 | assert response == expected_testpath_two 37 | 38 | 39 | def test_linegap_outfile_filepath_differentdir_fromroot(): 40 | response = get_linegap_percent_filepath(test_filepath_three, percent_value) 41 | assert response == expected_testpath_three 42 | 43 | 44 | def test_linegap_outfile_filepath_differentdir_fromuser(): 45 | response = get_linegap_percent_filepath(test_filepath_four, percent_value) 46 | assert response == expected_testpath_four 47 | 48 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import pytest 6 | 7 | 8 | # /////////////////////////////////////////////////////// 9 | # 10 | # pytest capsys capture tests 11 | # confirms capture of std output and std error streams 12 | # 13 | # /////////////////////////////////////////////////////// 14 | 15 | def test_pytest_capsys(capsys): 16 | print("bogus text for a test") 17 | sys.stderr.write("more text for a test") 18 | out, err = capsys.readouterr() 19 | assert out == "bogus text for a test\n" 20 | assert out != "something else" 21 | assert err == "more text for a test" 22 | assert err != "something else" 23 | 24 | 25 | # /////////////////////////////////////////////////////// 26 | # 27 | # Standard output tests for help, usage, version 28 | # 29 | # /////////////////////////////////////////////////////// 30 | 31 | def test_fontline_commandline_shorthelp(capsys): 32 | with pytest.raises(SystemExit): 33 | from fontline.app import main 34 | sys.argv = ['font-line', '-h'] 35 | main() 36 | out, err = capsys.readouterr() 37 | assert out.startswith("====================================================") is True 38 | assert out.endswith("https://github.com/source-foundry/font-line") is True 39 | 40 | 41 | def test_fontline_commandline_longhelp(capsys): 42 | with pytest.raises(SystemExit): 43 | from fontline.app import main 44 | sys.argv = ['font-line', '--help'] 45 | main() 46 | out, err = capsys.readouterr() 47 | assert out.startswith("====================================================") is True 48 | assert out.endswith("https://github.com/source-foundry/font-line") is True 49 | 50 | 51 | def test_fontline_commandline_longusage(capsys): 52 | with pytest.raises(SystemExit): 53 | from fontline.app import main 54 | sys.argv = ['font-line', '--usage'] 55 | main() 56 | out, err = capsys.readouterr() 57 | assert out.endswith("Usage: font-line [subcommand] [font path 1] ") is True 58 | 59 | 60 | def test_fontline_commandline_shortversion(capsys): 61 | with pytest.raises(SystemExit): 62 | from fontline.app import main 63 | from fontline.app import settings 64 | sys.argv = ['font-line', '-v'] 65 | main() 66 | out, err = capsys.readouterr() 67 | assert out == settings.VERSION 68 | 69 | 70 | def test_fontline_commandline_longversion(capsys): 71 | with pytest.raises(SystemExit): 72 | from fontline.app import main 73 | from fontline.app import settings 74 | sys.argv = ['font-line', '--version'] 75 | main() 76 | out, err = capsys.readouterr() 77 | assert out == settings.VERSION 78 | 79 | 80 | # /////////////////////////////////////////////////////// 81 | # 82 | # Command line argument error test 83 | # 84 | # /////////////////////////////////////////////////////// 85 | 86 | def test_fontline_commandline_notenough_args(capsys): 87 | with pytest.raises(SystemExit): 88 | from fontline.app import main 89 | sys.argv = ['font-line'] 90 | main() 91 | out, err = capsys.readouterr() 92 | assert err == "[font-line] ERROR: Please include one or more arguments with your command." 93 | 94 | 95 | def test_fontline_commandline_bad_subcmd(capsys): 96 | with pytest.raises(SystemExit): 97 | from fontline.app import main 98 | sys.argv = ['font-line', 'bogus'] 99 | main() 100 | out, err = capsys.readouterr() 101 | assert err.startswith("[font-lines] ERROR: You used an unsupported") is True 102 | 103 | 104 | -------------------------------------------------------------------------------- /tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | from fontTools.ttLib import TTFont 7 | 8 | from fontline.metrics import MetricsObject 9 | 10 | 11 | @pytest.fixture() 12 | def metricsobject(): 13 | filepath = os.path.join("tests", "testingfiles", "FiraMono-Regular.ttf") 14 | tt = TTFont(filepath) 15 | yield MetricsObject(tt, filepath) 16 | 17 | 18 | @pytest.fixture() 19 | def metricsobject_bit7_clear(): 20 | filepath = os.path.join("tests", "testingfiles", "Hack-Regular.ttf") 21 | tt = TTFont(filepath) 22 | yield MetricsObject(tt, filepath) 23 | 24 | 25 | def test_mo_metadata(metricsobject): 26 | filepath = os.path.join("tests","testingfiles", "FiraMono-Regular.ttf") 27 | assert metricsobject.filepath == filepath 28 | assert metricsobject.version == "Version 3.206" 29 | assert metricsobject.sha1 == "e2526f6d8ab566afc7cf75ec192c1df30fd5913b" 30 | 31 | 32 | def test_mo_metricsdata(metricsobject): 33 | assert metricsobject.units_per_em == 1000 34 | assert metricsobject.ymax == 1050 35 | assert metricsobject.ymin == -500 36 | assert metricsobject.os2_cap_height == 689 37 | assert metricsobject.os2_x_height == 527 38 | assert metricsobject.os2_typo_ascender == 935 39 | assert metricsobject.os2_typo_descender == -265 40 | assert metricsobject.os2_win_ascent == 935 41 | assert metricsobject.os2_win_descent == 265 42 | assert metricsobject.hhea_ascent == 935 43 | assert metricsobject.hhea_descent == -265 44 | assert metricsobject.hhea_linegap == 0 45 | assert metricsobject.os2_typo_linegap == 0 46 | 47 | 48 | def test_mo_ascent_to_descent_values(metricsobject): 49 | assert metricsobject.hhea_total_height == 1200 50 | assert metricsobject.os2_typo_total_height == 1200 51 | assert metricsobject.os2_win_total_height == 1200 52 | 53 | 54 | def test_mo_b2bd_values(metricsobject): 55 | assert metricsobject.hhea_btb_distance == 1200 56 | assert metricsobject.typo_btb_distance == 1200 57 | assert metricsobject.win_btb_distance == 1200 58 | 59 | 60 | def test_mo_fsselection_bit7_set(metricsobject): 61 | assert metricsobject.fsselection_bit7_set is True 62 | 63 | 64 | def test_mo_fsselection_bit7_clear(metricsobject_bit7_clear): 65 | assert metricsobject_bit7_clear.fsselection_bit7_set is False 66 | 67 | 68 | def test_mo_ratios(metricsobject): 69 | assert metricsobject.hheaascdesc_to_upm == 1.2 70 | assert metricsobject.typo_to_upm == 1.2 71 | assert metricsobject.winascdesc_to_upm == 1.2 72 | 73 | -------------------------------------------------------------------------------- /tests/test_percent_cmd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import pytest 6 | import os.path 7 | import os 8 | import shutil 9 | from fontTools import ttLib 10 | 11 | from fontline.utilities import file_exists 12 | 13 | # /////////////////////////////////////////////////////// 14 | # 15 | # utility functions for module tests 16 | # 17 | # /////////////////////////////////////////////////////// 18 | 19 | 20 | def create_test_file(filepath): 21 | filepath_list = filepath.split(".") 22 | testpath = filepath_list[0] + "-test." + filepath_list[1] 23 | shutil.copyfile(filepath, testpath) 24 | return True 25 | 26 | 27 | def erase_test_file(filepath): 28 | os.remove(filepath) 29 | return True 30 | 31 | 32 | # /////////////////////////////////////////////////////// 33 | # 34 | # percent sub-command command line logic tests 35 | # 36 | # /////////////////////////////////////////////////////// 37 | 38 | 39 | def test_percent_cmd_too_few_args(capsys): 40 | with pytest.raises(SystemExit): 41 | from fontline.app import main 42 | sys.argv = ['font-line', 'percent'] 43 | main() 44 | out, err = capsys.readouterr() 45 | assert err == "[font-line] ERROR: Not enough arguments." 46 | 47 | 48 | def test_percent_cmd_too_few_args_two(capsys): 49 | with pytest.raises(SystemExit): 50 | from fontline.app import main 51 | sys.argv = ['font-line', 'percent', '10'] 52 | main() 53 | out, err = capsys.readouterr() 54 | assert err == "[font-line] ERROR: Not enough arguments." 55 | 56 | 57 | def test_percent_cmd_percent_arg_not_integer(capsys): 58 | with pytest.raises(SystemExit): 59 | from fontline.app import main 60 | sys.argv = ['font-line', 'percent', 'astring', 'Test.ttf'] 61 | main() 62 | out, err = capsys.readouterr() 63 | assert err.startswith("[font-line] ERROR: You entered ") is True 64 | 65 | 66 | def test_percent_cmd_percent_arg_less_zero(capsys): 67 | with pytest.raises(SystemExit): 68 | from fontline.app import main 69 | sys.argv = ['font-line', 'percent', '-1', 'Test.ttf'] 70 | main() 71 | out, err = capsys.readouterr() 72 | assert err == "[font-line] ERROR: Please enter a percent value that is greater than zero." 73 | 74 | 75 | def test_percent_cmd_percent_arg_over_hundred(capsys): 76 | from fontline.app import main 77 | sys.argv = ['font-line', 'percent', '200', 'Test.ttf'] 78 | main() 79 | out, err = capsys.readouterr() 80 | assert out.startswith("[font-line] Warning: You entered a percent value over 100%.") 81 | 82 | 83 | def test_percent_cmd_font_file_missing(capsys): 84 | from fontline.app import main 85 | sys.argv = ['font-line', 'percent', '20', 'Test.ttf'] 86 | main() 87 | out, err = capsys.readouterr() 88 | assert ("does not appear to be a valid filepath" in err) is True 89 | 90 | 91 | def test_percent_cmd_font_file_wrong_filetype(capsys): 92 | from fontline.app import main 93 | testfile_path = os.path.join("tests", "testingfiles", "bogus.txt") 94 | sys.argv = ['font-line', 'percent', '20', testfile_path] 95 | main() 96 | out, err = capsys.readouterr() 97 | assert ("does not appear to be a supported font file type." in err) is True 98 | 99 | 100 | # /////////////////////////////////////////////////////// 101 | # 102 | # percent sub-command command functionality tests 103 | # 104 | # /////////////////////////////////////////////////////// 105 | 106 | # Default approach with TypoLinegap > 0 and TypoAscender + TypoDescender set to UPM height 107 | 108 | def test_percent_cmd_ttf_file_10_percent_default_approach(capsys): 109 | try: 110 | from fontline.app import main 111 | fontpath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 112 | testpath = os.path.join('tests', 'testingfiles', 'Hack-Regular-test.ttf') 113 | newfont_path = os.path.join('tests', 'testingfiles', 'Hack-Regular-test-linegap10.ttf') 114 | create_test_file(fontpath) 115 | assert file_exists(testpath) is True 116 | sys.argv = ['font-line', 'percent', '10', testpath] 117 | main() 118 | 119 | assert file_exists(newfont_path) 120 | 121 | tt = ttLib.TTFont(newfont_path) 122 | 123 | os2_typo_ascender = tt['OS/2'].sTypoAscender 124 | os2_typo_descender = tt['OS/2'].sTypoDescender 125 | os2_win_ascent = tt['OS/2'].usWinAscent 126 | os2_win_descent = tt['OS/2'].usWinDescent 127 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 128 | hhea_ascent = tt['hhea'].ascent 129 | hhea_descent = tt['hhea'].descent 130 | hhea_linegap = tt['hhea'].lineGap 131 | units_per_em = tt['head'].unitsPerEm 132 | 133 | assert os2_typo_ascender == 1556 134 | assert os2_typo_descender == -492 135 | assert os2_win_ascent == 1658 136 | assert os2_win_descent == 594 137 | assert units_per_em == 2048 138 | assert os2_typo_linegap == 204 139 | assert hhea_ascent == 1658 140 | assert hhea_descent == -594 141 | assert hhea_linegap == 0 142 | 143 | erase_test_file(testpath) 144 | erase_test_file(newfont_path) 145 | except Exception as e: 146 | # cleanup test files 147 | if file_exists(testpath): 148 | erase_test_file(testpath) 149 | if file_exists(newfont_path): 150 | erase_test_file(newfont_path) 151 | raise e 152 | 153 | 154 | def test_percent_cmd_otf_file_10_percent_default_approach(capsys): 155 | try: 156 | from fontline.app import main 157 | fontpath = os.path.join('tests', 'testingfiles', 'Hack-Regular.otf') 158 | testpath = os.path.join('tests', 'testingfiles', 'Hack-Regular-test.otf') 159 | newfont_path = os.path.join('tests', 'testingfiles', 'Hack-Regular-test-linegap10.otf') 160 | create_test_file(fontpath) 161 | assert file_exists(testpath) is True 162 | sys.argv = ['font-line', 'percent', '10', testpath] 163 | main() 164 | 165 | assert file_exists(newfont_path) 166 | 167 | tt = ttLib.TTFont(newfont_path) 168 | 169 | os2_typo_ascender = tt['OS/2'].sTypoAscender 170 | os2_typo_descender = tt['OS/2'].sTypoDescender 171 | os2_win_ascent = tt['OS/2'].usWinAscent 172 | os2_win_descent = tt['OS/2'].usWinDescent 173 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 174 | hhea_ascent = tt['hhea'].ascent 175 | hhea_descent = tt['hhea'].descent 176 | hhea_linegap = tt['hhea'].lineGap 177 | units_per_em = tt['head'].unitsPerEm 178 | 179 | assert os2_typo_ascender == 1556 180 | assert os2_typo_descender == -492 181 | assert os2_win_ascent == 1658 182 | assert os2_win_descent == 594 183 | assert units_per_em == 2048 184 | assert os2_typo_linegap == 204 185 | assert hhea_ascent == 1658 186 | assert hhea_descent == -594 187 | assert hhea_linegap == 0 188 | 189 | erase_test_file(testpath) 190 | erase_test_file(newfont_path) 191 | except Exception as e: 192 | # cleanup test files 193 | if file_exists(testpath): 194 | erase_test_file(testpath) 195 | if file_exists(newfont_path): 196 | erase_test_file(newfont_path) 197 | raise e 198 | 199 | 200 | def test_percent_cmd_ttf_file_30_percent_default_approach(capsys): 201 | try: 202 | from fontline.app import main 203 | fontpath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 204 | testpath = os.path.join('tests', 'testingfiles', 'Hack-Regular-test.ttf') 205 | newfont_path = os.path.join('tests', 'testingfiles', 'Hack-Regular-test-linegap30.ttf') 206 | create_test_file(fontpath) 207 | assert file_exists(testpath) is True 208 | sys.argv = ['font-line', 'percent', '30', testpath] 209 | main() 210 | 211 | assert file_exists(newfont_path) 212 | 213 | tt = ttLib.TTFont(newfont_path) 214 | 215 | os2_typo_ascender = tt['OS/2'].sTypoAscender 216 | os2_typo_descender = tt['OS/2'].sTypoDescender 217 | os2_win_ascent = tt['OS/2'].usWinAscent 218 | os2_win_descent = tt['OS/2'].usWinDescent 219 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 220 | hhea_ascent = tt['hhea'].ascent 221 | hhea_descent = tt['hhea'].descent 222 | hhea_linegap = tt['hhea'].lineGap 223 | units_per_em = tt['head'].unitsPerEm 224 | 225 | assert os2_typo_ascender == 1556 226 | assert os2_typo_descender == -492 227 | assert os2_win_ascent == 1863 228 | assert os2_win_descent == 799 229 | assert units_per_em == 2048 230 | assert os2_typo_linegap == 614 231 | assert hhea_ascent == 1863 232 | assert hhea_descent == -799 233 | assert hhea_linegap == 0 234 | 235 | erase_test_file(testpath) 236 | erase_test_file(newfont_path) 237 | except Exception as e: 238 | # cleanup test files 239 | if file_exists(testpath): 240 | erase_test_file(testpath) 241 | if file_exists(newfont_path): 242 | erase_test_file(newfont_path) 243 | raise e 244 | 245 | 246 | # Google metrics approach with TypoLinegap & hhea linegap = 0, all other metrics set to same values of internal leading 247 | 248 | def test_percent_cmd_ttf_file_10_percent_google_approach(capsys): 249 | try: 250 | from fontline.app import main 251 | fontpath = os.path.join('tests', 'testingfiles', 'FiraMono-Regular.ttf') 252 | testpath = os.path.join('tests', 'testingfiles', 'FiraMono-Regular-test.ttf') 253 | newfont_path = os.path.join('tests', 'testingfiles', 'FiraMono-Regular-test-linegap10.ttf') 254 | create_test_file(fontpath) 255 | assert file_exists(testpath) is True 256 | sys.argv = ['font-line', 'percent', '10', testpath] 257 | main() 258 | 259 | assert file_exists(newfont_path) 260 | 261 | tt = ttLib.TTFont(newfont_path) 262 | 263 | os2_typo_ascender = tt['OS/2'].sTypoAscender 264 | os2_typo_descender = tt['OS/2'].sTypoDescender 265 | os2_win_ascent = tt['OS/2'].usWinAscent 266 | os2_win_descent = tt['OS/2'].usWinDescent 267 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 268 | hhea_ascent = tt['hhea'].ascent 269 | hhea_descent = tt['hhea'].descent 270 | hhea_linegap = tt['hhea'].lineGap 271 | units_per_em = tt['head'].unitsPerEm 272 | 273 | assert os2_typo_ascender == 885 274 | assert os2_typo_descender == -215 275 | assert os2_win_ascent == 885 276 | assert os2_win_descent == 215 277 | assert units_per_em == 1000 278 | assert os2_typo_linegap == 0 279 | assert hhea_ascent == 885 280 | assert hhea_descent == -215 281 | assert hhea_linegap == 0 282 | 283 | erase_test_file(testpath) 284 | erase_test_file(newfont_path) 285 | except Exception as e: 286 | # cleanup test files 287 | if file_exists(testpath): 288 | erase_test_file(testpath) 289 | if file_exists(newfont_path): 290 | erase_test_file(newfont_path) 291 | raise e 292 | 293 | 294 | def test_percent_cmd_ttf_file_30_percent_google_approach(capsys): 295 | try: 296 | from fontline.app import main 297 | fontpath = os.path.join('tests', 'testingfiles', 'FiraMono-Regular.ttf') 298 | testpath = os.path.join('tests', 'testingfiles', 'FiraMono-Regular-test.ttf') 299 | newfont_path = os.path.join('tests', 'testingfiles', 'FiraMono-Regular-test-linegap30.ttf') 300 | create_test_file(fontpath) 301 | assert file_exists(testpath) is True 302 | sys.argv = ['font-line', 'percent', '30', testpath] 303 | main() 304 | 305 | assert file_exists(newfont_path) 306 | 307 | tt = ttLib.TTFont(newfont_path) 308 | 309 | os2_typo_ascender = tt['OS/2'].sTypoAscender 310 | os2_typo_descender = tt['OS/2'].sTypoDescender 311 | os2_win_ascent = tt['OS/2'].usWinAscent 312 | os2_win_descent = tt['OS/2'].usWinDescent 313 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 314 | hhea_ascent = tt['hhea'].ascent 315 | hhea_descent = tt['hhea'].descent 316 | hhea_linegap = tt['hhea'].lineGap 317 | units_per_em = tt['head'].unitsPerEm 318 | 319 | assert os2_typo_ascender == 985 320 | assert os2_typo_descender == -315 321 | assert os2_win_ascent == 985 322 | assert os2_win_descent == 315 323 | assert units_per_em == 1000 324 | assert os2_typo_linegap == 0 325 | assert hhea_ascent == 985 326 | assert hhea_descent == -315 327 | assert hhea_linegap == 0 328 | 329 | erase_test_file(testpath) 330 | erase_test_file(newfont_path) 331 | except Exception as e: 332 | # cleanup test files 333 | if file_exists(testpath): 334 | erase_test_file(testpath) 335 | if file_exists(newfont_path): 336 | erase_test_file(newfont_path) 337 | raise e 338 | 339 | 340 | # Adobe metrics approach with TypoLinegap and hhea linegap = 0, TypoAscender + TypoDescender = UPM & 341 | # internal leading added to winAsc/winDesc & hhea Asc/hhea Desc metrics 342 | 343 | def test_percent_cmd_ttf_file_10_percent_adobe_approach(capsys): 344 | try: 345 | from fontline.app import main 346 | fontpath = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular.ttf') 347 | testpath = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular-test.ttf') 348 | newfont_path = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular-test-linegap10.ttf') 349 | create_test_file(fontpath) 350 | assert file_exists(testpath) is True 351 | sys.argv = ['font-line', 'percent', '10', testpath] 352 | main() 353 | 354 | assert file_exists(newfont_path) 355 | 356 | tt = ttLib.TTFont(newfont_path) 357 | 358 | os2_typo_ascender = tt['OS/2'].sTypoAscender 359 | os2_typo_descender = tt['OS/2'].sTypoDescender 360 | os2_win_ascent = tt['OS/2'].usWinAscent 361 | os2_win_descent = tt['OS/2'].usWinDescent 362 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 363 | hhea_ascent = tt['hhea'].ascent 364 | hhea_descent = tt['hhea'].descent 365 | hhea_linegap = tt['hhea'].lineGap 366 | units_per_em = tt['head'].unitsPerEm 367 | 368 | assert os2_typo_ascender == 750 369 | assert os2_typo_descender == -250 370 | assert os2_win_ascent == 906 371 | assert os2_win_descent == 195 372 | assert units_per_em == 1000 373 | assert os2_typo_linegap == 0 374 | assert hhea_ascent == 906 375 | assert hhea_descent == -195 376 | assert hhea_linegap == 0 377 | 378 | erase_test_file(testpath) 379 | erase_test_file(newfont_path) 380 | except Exception as e: 381 | # cleanup test files 382 | if file_exists(testpath): 383 | erase_test_file(testpath) 384 | if file_exists(newfont_path): 385 | erase_test_file(newfont_path) 386 | raise e 387 | 388 | 389 | def test_percent_cmd_ttf_file_30_percent_adobe_approach(capsys): 390 | try: 391 | from fontline.app import main 392 | fontpath = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular.ttf') 393 | testpath = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular-test.ttf') 394 | newfont_path = os.path.join('tests', 'testingfiles', 'SourceCodePro-Regular-test-linegap30.ttf') 395 | create_test_file(fontpath) 396 | assert file_exists(testpath) is True 397 | sys.argv = ['font-line', 'percent', '30', testpath] 398 | main() 399 | 400 | assert file_exists(newfont_path) 401 | 402 | tt = ttLib.TTFont(newfont_path) 403 | 404 | os2_typo_ascender = tt['OS/2'].sTypoAscender 405 | os2_typo_descender = tt['OS/2'].sTypoDescender 406 | os2_win_ascent = tt['OS/2'].usWinAscent 407 | os2_win_descent = tt['OS/2'].usWinDescent 408 | os2_typo_linegap = tt['OS/2'].sTypoLineGap 409 | hhea_ascent = tt['hhea'].ascent 410 | hhea_descent = tt['hhea'].descent 411 | hhea_linegap = tt['hhea'].lineGap 412 | units_per_em = tt['head'].unitsPerEm 413 | 414 | assert os2_typo_ascender == 750 415 | assert os2_typo_descender == -250 416 | assert os2_win_ascent == 1005 417 | assert os2_win_descent == 294 418 | assert units_per_em == 1000 419 | assert os2_typo_linegap == 0 420 | assert hhea_ascent == 1005 421 | assert hhea_descent == -294 422 | assert hhea_linegap == 0 423 | 424 | erase_test_file(testpath) 425 | erase_test_file(newfont_path) 426 | except Exception as e: 427 | # cleanup test files 428 | if file_exists(testpath): 429 | erase_test_file(testpath) 430 | if file_exists(newfont_path): 431 | erase_test_file(newfont_path) 432 | raise e 433 | -------------------------------------------------------------------------------- /tests/test_report_cmd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import pytest 6 | import os.path 7 | import os 8 | 9 | # /////////////////////////////////////////////////////// 10 | # 11 | # report sub-command tests 12 | # 13 | # /////////////////////////////////////////////////////// 14 | 15 | 16 | def test_report_cmd_too_few_args(capsys): 17 | with pytest.raises(SystemExit): 18 | from fontline.app import main 19 | sys.argv = ['font-line', 'report'] 20 | main() 21 | out, err = capsys.readouterr() 22 | assert err == "[font-line] ERROR: Missing file path argument(s) after the report subcommand." 23 | 24 | 25 | def test_report_cmd_missing_file_request(capsys): 26 | from fontline.app import main 27 | sys.argv = ['font-line', 'report', 'missing.txt'] 28 | main() 29 | out, err = capsys.readouterr() 30 | assert err.startswith("[font-line] ERROR: ") 31 | 32 | 33 | def test_report_cmd_unsupported_filetype(capsys): 34 | from fontline.app import main 35 | filepath = os.path.join('tests', 'testingfiles', 'bogus.txt') 36 | sys.argv = ['font-line', 'report', filepath] 37 | main() 38 | out, err = capsys.readouterr() 39 | assert err.startswith("[font-line] ERROR: ") 40 | 41 | 42 | def test_report_cmd_reportstring_filename(capsys): 43 | from fontline.app import main 44 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 45 | sys.argv = ['font-line', 'report', filepath] 46 | main() 47 | out, err = capsys.readouterr() 48 | assert "Hack-Regular.ttf " in out 49 | 50 | 51 | def test_report_cmd_reportstring_version(capsys): 52 | from fontline.app import main 53 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 54 | sys.argv = ['font-line', 'report', filepath] 55 | main() 56 | out, err = capsys.readouterr() 57 | assert "Version 2.020;DEV-03192016;" in out 58 | 59 | 60 | def test_report_cmd_reportstring_sha1(capsys): 61 | from fontline.app import main 62 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 63 | sys.argv = ['font-line', 'report', filepath] 64 | main() 65 | out, err = capsys.readouterr() 66 | assert "SHA1: 638f033cc1b6a21597359278bee62cf7e96557ff" in out 67 | 68 | 69 | def test_report_cmd_reportstring_upm(capsys): 70 | from fontline.app import main 71 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 72 | sys.argv = ['font-line', 'report', filepath] 73 | main() 74 | out, err = capsys.readouterr() 75 | assert "[head] Units per Em: 2048" in out 76 | 77 | 78 | def test_report_cmd_reportstring_ymax(capsys): 79 | from fontline.app import main 80 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 81 | sys.argv = ['font-line', 'report', filepath] 82 | main() 83 | out, err = capsys.readouterr() 84 | assert "[head] yMax: 2001" in out 85 | 86 | 87 | def test_report_cmd_reportstring_ymin(capsys): 88 | from fontline.app import main 89 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 90 | sys.argv = ['font-line', 'report', filepath] 91 | main() 92 | out, err = capsys.readouterr() 93 | assert "[head] yMin: -573" in out 94 | 95 | 96 | def test_report_cmd_reportstring_capheight(capsys): 97 | from fontline.app import main 98 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 99 | sys.argv = ['font-line', 'report', filepath] 100 | main() 101 | out, err = capsys.readouterr() 102 | assert "[OS/2] CapHeight: 1493" in out 103 | 104 | 105 | def test_report_cmd_reportstring_xheight(capsys): 106 | from fontline.app import main 107 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 108 | sys.argv = ['font-line', 'report', filepath] 109 | main() 110 | out, err = capsys.readouterr() 111 | assert "[OS/2] xHeight: 1120" in out 112 | 113 | 114 | def test_report_cmd_reportstring_typoascender(capsys): 115 | from fontline.app import main 116 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 117 | sys.argv = ['font-line', 'report', filepath] 118 | main() 119 | out, err = capsys.readouterr() 120 | assert "[OS/2] TypoAscender: 1556" in out 121 | 122 | 123 | def test_report_cmd_reportstring_typodescender(capsys): 124 | from fontline.app import main 125 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 126 | sys.argv = ['font-line', 'report', filepath] 127 | main() 128 | out, err = capsys.readouterr() 129 | assert "[OS/2] TypoDescender: -492" in out 130 | 131 | 132 | def test_report_cmd_reportstring_winascent(capsys): 133 | from fontline.app import main 134 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 135 | sys.argv = ['font-line', 'report', filepath] 136 | main() 137 | out, err = capsys.readouterr() 138 | assert "[OS/2] WinAscent: 1901" in out 139 | 140 | 141 | def test_report_cmd_reportstring_windescent(capsys): 142 | from fontline.app import main 143 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 144 | sys.argv = ['font-line', 'report', filepath] 145 | main() 146 | out, err = capsys.readouterr() 147 | assert "[OS/2] WinDescent: 483" in out 148 | 149 | 150 | def test_report_cmd_reportstring_ascent(capsys): 151 | from fontline.app import main 152 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 153 | sys.argv = ['font-line', 'report', filepath] 154 | main() 155 | out, err = capsys.readouterr() 156 | assert "[hhea] Ascent: 1901" in out 157 | 158 | 159 | def test_report_cmd_reportstring_descent(capsys): 160 | from fontline.app import main 161 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 162 | sys.argv = ['font-line', 'report', filepath] 163 | main() 164 | out, err = capsys.readouterr() 165 | assert "[hhea] Descent: -483" in out 166 | 167 | 168 | def test_report_cmd_reportstring_linegap(capsys): 169 | from fontline.app import main 170 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 171 | sys.argv = ['font-line', 'report', filepath] 172 | main() 173 | out, err = capsys.readouterr() 174 | assert "[hhea] LineGap: 0" in out 175 | 176 | 177 | def test_report_cmd_reportstring_typolinegap(capsys): 178 | from fontline.app import main 179 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 180 | sys.argv = ['font-line', 'report', filepath] 181 | main() 182 | out, err = capsys.readouterr() 183 | assert "[OS/2] TypoLineGap: 410" in out 184 | 185 | 186 | def test_report_cmd_reportstring_typoA_typodesc(capsys): 187 | from fontline.app import main 188 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 189 | sys.argv = ['font-line', 'report', filepath] 190 | main() 191 | out, err = capsys.readouterr() 192 | assert "[OS/2] TypoAscender to TypoDescender: 2048" in out 193 | 194 | 195 | def test_report_cmd_reportstring_winA_windesc(capsys): 196 | from fontline.app import main 197 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 198 | sys.argv = ['font-line', 'report', filepath] 199 | main() 200 | out, err = capsys.readouterr() 201 | assert "[OS/2] WinAscent to WinDescent: 2384" in out 202 | 203 | 204 | def test_report_cmd_reportstring_ascent_descent(capsys): 205 | from fontline.app import main 206 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 207 | sys.argv = ['font-line', 'report', filepath] 208 | main() 209 | out, err = capsys.readouterr() 210 | assert "[hhea] Ascent to Descent: 2384" in out 211 | 212 | 213 | def test_report_cmd_reportstring_winA_typoasc(capsys): 214 | from fontline.app import main 215 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 216 | sys.argv = ['font-line', 'report', filepath] 217 | main() 218 | out, err = capsys.readouterr() 219 | assert "[hhea] Ascent to [OS/2] TypoAscender: 345" in out 220 | 221 | 222 | def test_report_cmd_reportstring_ascent_typoasc(capsys): 223 | from fontline.app import main 224 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 225 | sys.argv = ['font-line', 'report', filepath] 226 | main() 227 | out, err = capsys.readouterr() 228 | assert "[hhea] Ascent to [OS/2] TypoAscender: 345" in out 229 | 230 | 231 | def test_report_cmd_reportstring_winD_typodesc(capsys): 232 | from fontline.app import main 233 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 234 | sys.argv = ['font-line', 'report', filepath] 235 | main() 236 | out, err = capsys.readouterr() 237 | assert "[OS/2] WinDescent to [OS/2] TypoDescender: -9" in out 238 | 239 | 240 | def test_report_cmd_reportstring_descent_typodesc(capsys): 241 | from fontline.app import main 242 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 243 | sys.argv = ['font-line', 'report', filepath] 244 | main() 245 | out, err = capsys.readouterr() 246 | assert "[hhea] Descent to [OS/2] TypoDescender: -9" in out 247 | 248 | 249 | def test_report_cmd_reportstring_typo_to_upm(capsys): 250 | from fontline.app import main 251 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 252 | sys.argv = ['font-line', 'report', filepath] 253 | main() 254 | out, err = capsys.readouterr() 255 | assert "typo metrics / UPM: 1.2" in out 256 | 257 | 258 | def test_report_cmd_reportstring_win_to_upm(capsys): 259 | from fontline.app import main 260 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 261 | sys.argv = ['font-line', 'report', filepath] 262 | main() 263 | out, err = capsys.readouterr() 264 | assert "win metrics / UPM: 1.16" in out 265 | 266 | 267 | def test_report_cmd_reportstring_hhea_to_upm(capsys): 268 | from fontline.app import main 269 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 270 | sys.argv = ['font-line', 'report', filepath] 271 | main() 272 | out, err = capsys.readouterr() 273 | assert "hhea metrics / UPM: 1.16" in out 274 | 275 | 276 | def test_report_cmd_reportstring_fsselection_bit7_set_true(capsys): 277 | from fontline.app import main 278 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular-fsSelection-bit7-set.ttf') 279 | sys.argv = ['font-line', 'report', filepath] 280 | main() 281 | out, err = capsys.readouterr() 282 | assert "[OS/2] fsSelection USE_TYPO_METRICS bit set: True" in out 283 | 284 | 285 | def test_report_cmd_reportstring_fsselection_bit7_set_false(capsys): 286 | from fontline.app import main 287 | filepath = os.path.join('tests', 'testingfiles', 'Hack-Regular.ttf') 288 | sys.argv = ['font-line', 'report', filepath] 289 | main() 290 | out, err = capsys.readouterr() 291 | assert "[OS/2] fsSelection USE_TYPO_METRICS bit set: False" in out 292 | 293 | 294 | # TODO: add tests for new baseline to baseline distance calculations in report 295 | -------------------------------------------------------------------------------- /tests/testingfiles/FiraMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/FiraMono-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/Hack-Regular-fsSelection-bit7-set.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/Hack-Regular-fsSelection-bit7-set.ttf -------------------------------------------------------------------------------- /tests/testingfiles/Hack-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/Hack-Regular.otf -------------------------------------------------------------------------------- /tests/testingfiles/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/Hack-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/backup/FiraMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/backup/FiraMono-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/backup/Hack-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/backup/Hack-Regular.otf -------------------------------------------------------------------------------- /tests/testingfiles/backup/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/backup/Hack-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/backup/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-line/66941d242b683facde2265a273bb56f340434ffe/tests/testingfiles/backup/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /tests/testingfiles/backup/bogus.txt: -------------------------------------------------------------------------------- 1 | This is a file for testing. -------------------------------------------------------------------------------- /tests/testingfiles/bogus.txt: -------------------------------------------------------------------------------- 1 | This is a file for testing. -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310 3 | 4 | [testenv] 5 | commands = 6 | py.test tests 7 | ;- coverage run --source {{fontline}} -m py.test 8 | ;- coverage report 9 | deps = 10 | -rrequirements.txt 11 | pytest 12 | 13 | [testenv:flake8] 14 | deps = 15 | flake8==2.5.1 16 | pep8==1.7.0 17 | commands = 18 | flake8 lib/fontline setup.py 19 | 20 | ;[testenv:cov-report] 21 | ;commands = py.test --cov={{fontline}} --cov-report=term --cov-report=html 22 | --------------------------------------------------------------------------------