├── LICENSE.txt ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── setup.py └── src └── otf2ttf ├── __init__.py └── cli.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: clean-build clean-pyc clean-test 2 | 3 | clean-build: 4 | rm -fr build/ 5 | rm -fr dist/ 6 | rm -fr .eggs/ 7 | find . -name '*.egg-info' -exec rm -fr {} + 8 | find . -name '*.egg' -exec rm -f {} + 9 | 10 | clean-pyc: 11 | find . -name '*.pyc' -exec rm -f {} + 12 | find . -name '*.pyo' -exec rm -f {} + 13 | find . -name '*~' -exec rm -f {} + 14 | find . -name '__pycache__' -exec rm -fr {} + 15 | 16 | clean-test: 17 | rm -fr .cache 18 | rm -fr .mypy_cache 19 | rm -fr .pytest_cache 20 | rm -f .coverage 21 | rm -fr htmlcov/ 22 | 23 | publish: 24 | pipenv run python setup.py sdist 25 | pipenv run python setup.py bdist_wheel --universal 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | otf2ttf = {editable = true, path = "./"} 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e0a366326674ac5711fa86cc64f706adefec039f95aad761641c064fbbc80203" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "appdirs": { 20 | "hashes": [ 21 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 22 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 23 | ], 24 | "version": "==1.4.3" 25 | }, 26 | "cu2qu": { 27 | "hashes": [ 28 | "sha256:01a70624163b1a73792ef0f585b614e71e5c4885d06f545aebae85202f71ba62", 29 | "sha256:0ff0161a4dbc4d84d1d38a17647332d304fbbb0f4536677ce83257d5921ad3fd", 30 | "sha256:1ee2503516892af930b96fdc1176151314387a015b6af897bd6fa456084260cc", 31 | "sha256:309d40e11702034f3c6770db20be11712a26d2e2911e436ac1ecb1769f3024d6", 32 | "sha256:321b62d398c6c39507acc223315b845a54c255ae7c4190289e2584a655b5e160", 33 | "sha256:4b3bc12761c6ac5cfa3cc95babde8cbec5cd0041bc311a33f403efd7765d8cad", 34 | "sha256:57ed7856ee520df2cf10e288dbaeba2f9e54da918c12f2ac83d164055067295a", 35 | "sha256:6bf3dbd5daa5c3ec5e1d6efb1eed36ed2563304398d2100424b673de9f64fd79", 36 | "sha256:77fa964432afbc7501d60246b87a82de2cf99711eabb60121ce14dcf99f300f9", 37 | "sha256:7a2f024e3a01fdba1a85290a0d7141abd8a375af80fb178d2412c1cf1e3f86df", 38 | "sha256:92e8db2338bde72df830556afb5d33cbd3c052c197b3262856646ff2308a2d0e", 39 | "sha256:b3b4a0d80798489906ca503c4a7a749e0a360670aa2f10f21dd01682e37b8293", 40 | "sha256:c9197aa2471112c2a7c87c8b509d81ca0daaadbe77a73ba6e0e9a72be3f58f20", 41 | "sha256:d6727edac77b2d8d3e56b04fe3237c089c2b151db03d6ee777cbb163c52c1c0a", 42 | "sha256:f10cd6088b7ee83b1a8f7eca886f4355e2152b47432a8e4e7b05eb6390cc7db4", 43 | "sha256:f3050119434c15cf47896fc50fd9ff2f46f18db3ada31fd8a9c996f456879c24", 44 | "sha256:f68bfc3c75dfa77dce9813671c86fe1bda2ed0e732dd5818566193097f038e74", 45 | "sha256:ffc205cc5895c8a39563c63dd9b215345014aff2877155e9a33ee48408dbc352" 46 | ], 47 | "version": "==1.6.5" 48 | }, 49 | "fonttools": { 50 | "extras": [ 51 | "ufo" 52 | ], 53 | "hashes": [ 54 | "sha256:9415fda795f4ff8e89d41ab907388ec5a4c236f4774ea65a746d8c1e4e839d3d", 55 | "sha256:c095fc06da714c8364d5f44278afa05f4c70c8db7f36d1664aceb15d7be08d31" 56 | ], 57 | "version": "==4.0.0" 58 | }, 59 | "fs": { 60 | "hashes": [ 61 | "sha256:cc99d476b500f993df8ef697b96dc70928ca2946a455c396a566efe021126767", 62 | "sha256:cd6b178f373a0370feac8612fd3c142aa6a5cadd3d471b525b08db4d3b511c9c" 63 | ], 64 | "version": "==2.4.11" 65 | }, 66 | "otf2ttf": { 67 | "editable": true, 68 | "path": "./" 69 | }, 70 | "pytz": { 71 | "hashes": [ 72 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 73 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 74 | ], 75 | "version": "==2019.2" 76 | }, 77 | "six": { 78 | "hashes": [ 79 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 80 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 81 | ], 82 | "version": "==1.12.0" 83 | } 84 | }, 85 | "develop": {} 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # otf2ttf 2 | 3 | This is a simple command=line script that can be installed and used to 4 | convert OTF (OpenType Font) and TTF (TrueType Font) file formats. 5 | 6 | How to use it: 7 | 8 | $ pip install otf2ttf 9 | $ otf2ttf MyFont.ttf 10 | 11 | 12 | ## Background 13 | 14 | Currently these 2 PDF libraries, require TTF fonts, which is why this script 15 | was packaged up for ease of use: 16 | 17 | - [ReportLab](https://www.reportlab.com/) 18 | - [FPDF](https://github.com/reingart/pyfpdf) 19 | 20 | This package was built using a sample script from the `fonttools` project. 21 | 22 | Below is a link to the original script: 23 | 24 | - [otf2ttf.py](https://github.com/fonttools/fonttools/blob/master/Snippets/otf2ttf.py) 25 | 26 | This package was created to ease the use of non-programmers looking to 27 | convert their files. 28 | 29 | More information about this script can be found in 30 | [Issue #1283](https://github.com/fonttools/fonttools/issues/1283). 31 | 32 | 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # read the contents of your README file 4 | from os import path 5 | 6 | this_directory = path.abspath(path.dirname(__file__)) 7 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | 11 | setup( 12 | name="otf2ttf", 13 | version="0.2", 14 | packages=find_packages("src/"), 15 | package_dir={"": "src"}, 16 | package_data={}, 17 | include_package_data=True, 18 | entry_points={"console_scripts": ["otf2ttf=otf2ttf:main"]}, 19 | install_requires=["fonttools", "cu2qu"], 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | author="Awesome Toolbox", 23 | author_email="info@awesometoolbox.com", 24 | description="command-line tool for converting OTF to TTF", 25 | license="MIT", 26 | keywords="font convert otf ttf", 27 | url="https://github.com/awesometoolbox/otf2ttf", 28 | classifiers=[ 29 | "Development Status :: 5 - Production/Stable", 30 | "Environment :: Console", 31 | "Environment :: Other Environment", 32 | "Intended Audience :: Developers", 33 | "Intended Audience :: End Users/Desktop", 34 | "License :: OSI Approved :: MIT License", 35 | "Natural Language :: English", 36 | "Operating System :: OS Independent", 37 | "Programming Language :: Python", 38 | "Programming Language :: Python :: 2", 39 | "Programming Language :: Python :: 3", 40 | "Topic :: Text Processing :: Fonts", 41 | "Topic :: Multimedia :: Graphics", 42 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /src/otf2ttf/__init__.py: -------------------------------------------------------------------------------- 1 | from .cli import main 2 | 3 | __all__ = ("main",) 4 | -------------------------------------------------------------------------------- /src/otf2ttf/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import os 6 | import sys 7 | 8 | from cu2qu.pens import Cu2QuPen 9 | from fontTools import configLogger 10 | from fontTools.misc.cliTools import makeOutputFileName 11 | from fontTools.pens.ttGlyphPen import TTGlyphPen 12 | from fontTools.ttLib import TTFont, newTable 13 | 14 | 15 | log = logging.getLogger() 16 | 17 | # default approximation error, measured in UPEM 18 | MAX_ERR = 1.0 19 | 20 | # default 'post' table format 21 | POST_FORMAT = 2.0 22 | 23 | # assuming the input contours' direction is correctly set (counter-clockwise), 24 | # we just flip it to clockwise 25 | REVERSE_DIRECTION = True 26 | 27 | 28 | def glyphs_to_quadratic( 29 | glyphs, max_err=MAX_ERR, reverse_direction=REVERSE_DIRECTION 30 | ): 31 | quadGlyphs = {} 32 | for gname in glyphs.keys(): 33 | glyph = glyphs[gname] 34 | ttPen = TTGlyphPen(glyphs) 35 | cu2quPen = Cu2QuPen( 36 | ttPen, max_err, reverse_direction=reverse_direction 37 | ) 38 | glyph.draw(cu2quPen) 39 | quadGlyphs[gname] = ttPen.glyph() 40 | return quadGlyphs 41 | 42 | 43 | def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs): 44 | assert ttFont.sfntVersion == "OTTO" 45 | assert "CFF " in ttFont 46 | 47 | glyphOrder = ttFont.getGlyphOrder() 48 | 49 | ttFont["loca"] = newTable("loca") 50 | ttFont["glyf"] = glyf = newTable("glyf") 51 | glyf.glyphOrder = glyphOrder 52 | glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs) 53 | del ttFont["CFF "] 54 | glyf.compile(ttFont) 55 | 56 | ttFont["maxp"] = maxp = newTable("maxp") 57 | maxp.tableVersion = 0x00010000 58 | maxp.maxZones = 1 59 | maxp.maxTwilightPoints = 0 60 | maxp.maxStorage = 0 61 | maxp.maxFunctionDefs = 0 62 | maxp.maxInstructionDefs = 0 63 | maxp.maxStackElements = 0 64 | maxp.maxSizeOfInstructions = 0 65 | maxp.maxComponentElements = max( 66 | len(g.components if hasattr(g, "components") else []) 67 | for g in glyf.glyphs.values() 68 | ) 69 | maxp.compile(ttFont) 70 | 71 | post = ttFont["post"] 72 | post.formatType = post_format 73 | post.extraNames = [] 74 | post.mapping = {} 75 | post.glyphOrder = glyphOrder 76 | try: 77 | post.compile(ttFont) 78 | except OverflowError: 79 | post.formatType = 3 80 | log.warning("Dropping glyph names, they do not fit in 'post' table.") 81 | 82 | ttFont.sfntVersion = "\000\001\000\000" 83 | 84 | 85 | def main(args=None): 86 | configLogger(logger=log) 87 | 88 | parser = argparse.ArgumentParser() 89 | parser.add_argument("input", nargs="+", metavar="INPUT") 90 | parser.add_argument("-o", "--output") 91 | parser.add_argument("-e", "--max-error", type=float, default=MAX_ERR) 92 | parser.add_argument("--post-format", type=float, default=POST_FORMAT) 93 | parser.add_argument( 94 | "--keep-direction", dest="reverse_direction", action="store_false" 95 | ) 96 | parser.add_argument("--face-index", type=int, default=0) 97 | parser.add_argument("--overwrite", action="store_true") 98 | options = parser.parse_args(args) 99 | 100 | if options.output and len(options.input) > 1: 101 | if not os.path.isdir(options.output): 102 | parser.error( 103 | "-o/--output option must be a directory when " 104 | "processing multiple fonts" 105 | ) 106 | 107 | for path in options.input: 108 | if options.output and not os.path.isdir(options.output): 109 | output = options.output 110 | else: 111 | output = makeOutputFileName( 112 | path, 113 | outputDir=options.output, 114 | extension=".ttf", 115 | overWrite=options.overwrite, 116 | ) 117 | 118 | font = TTFont(path, fontNumber=options.face_index) 119 | otf_to_ttf( 120 | font, 121 | post_format=options.post_format, 122 | max_err=options.max_error, 123 | reverse_direction=options.reverse_direction, 124 | ) 125 | font.save(output) 126 | 127 | 128 | if __name__ == "__main__": 129 | sys.exit(main()) 130 | --------------------------------------------------------------------------------