├── .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 | [](https://pypi.org/project/font-line)
4 | 
5 | 
6 | [](https://codecov.io/github/source-foundry/font-line?branch=master)
7 | [](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 |
--------------------------------------------------------------------------------