├── .coveragerc ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── fonts ├── Zebrawood-Dots.otf ├── Zebrawood-Fill.otf ├── Zebrawood-Shadow.otf ├── Zebrawood.otf └── test.html ├── imgs ├── step1.png └── step2.png ├── lib └── opentypesvg │ ├── addsvg.py │ ├── dumpsvg.py │ ├── fonts2svg.py │ └── utils.py ├── pytest.ini ├── setup.py └── tests ├── conftest.py ├── dumpsvg_test.py ├── fixtures ├── 1.ttf ├── 12.ttf ├── 1234.ttf ├── 13.ttf ├── 14.ttf ├── 2.ttf ├── 23.ttf ├── 24.ttf ├── 3.ttf ├── 34.ttf ├── 4.ttf ├── A.svg ├── Y.svg ├── Z.svg ├── aA.ttf ├── ab.ttf ├── b.svg ├── bc.ttf ├── blank_glyph.ttf ├── cd.ttf ├── no_glyf_table.ttf └── no_head_table.ttf ├── fonts2svg_test.py ├── options_test.py └── shared_utils_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | # measure 'branch' coverage in addition to 'statement' coverage 3 | # See: http://coverage.readthedocs.io/en/coverage-4.5.1/branch.html 4 | branch = True 5 | parallel = True 6 | 7 | # list of directories or packages to measure 8 | source = 9 | opentypesvg 10 | 11 | omit = 12 | tests 13 | */__version__.py 14 | 15 | [report] 16 | # Regexes for lines to exclude from consideration 17 | exclude_lines = 18 | # keywords to use in inline comments to skip coverage 19 | pragma: no cover 20 | 21 | # don't complain if tests don't hit defensive assertion code 22 | # ... 23 | 24 | # don't complain if non-runnable code isn't run 25 | if __name__ == .__main__.: 26 | 27 | # ignore source code that can’t be found 28 | ignore_errors = True 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Publish release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '[0-9]+.[0-9]+.[0-9a-z]+' 8 | 9 | jobs: 10 | deploy: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel 26 | 27 | - name: Build package 28 | run: python setup.py sdist bdist_wheel 29 | 30 | - name: Publish to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.PYPI_API_TOKEN }} 35 | 36 | - name: Create GitHub release 37 | uses: softprops/action-gh-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | prerelease: true 42 | files: ./dist/* 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test package 3 | 4 | on: 5 | push: 6 | paths-ignore: [ '**.md' ] 7 | 8 | pull_request: 9 | branches: [ master ] 10 | paths-ignore: [ '**.md' ] 11 | 12 | workflow_dispatch: 13 | inputs: 14 | reason: 15 | description: 'Reason for running workflow' 16 | required: true 17 | 18 | jobs: 19 | test: 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | python-version: ["3.8", "3.9", "3.10"] 27 | exclude: 28 | - os: macos-latest 29 | python-version: "3.8" 30 | - os: windows-latest 31 | python-version: "3.8" 32 | 33 | steps: 34 | - name: Log reason (manual run only) 35 | if: github.event_name == 'workflow_dispatch' 36 | run: | 37 | echo "Reason for triggering: ${{ github.event.inputs.reason }}" 38 | 39 | - uses: actions/checkout@v3 40 | 41 | - name: Set up Python ${{ matrix.python-version }} 42 | uses: actions/setup-python@v4 43 | with: 44 | python-version: ${{ matrix.python-version }} 45 | 46 | - name: Install dependencies 47 | run: | 48 | python -m pip install --upgrade pip 49 | python -m pip install wheel flake8 pytest-cov 50 | 51 | - name: Lint with flake8 52 | run: flake8 53 | 54 | - name: Install package 55 | run: python -m pip install . 56 | 57 | - name: Test with pytest 58 | run: pytest -v --cov --cov-report=xml 59 | 60 | - name: Report coverage to Codecov 61 | uses: codecov/codecov-action@v3 62 | with: 63 | name: Python ${{ matrix.python-version }} (${{ matrix.os }}) 64 | files: ./coverage.xml 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build 4 | venv 5 | dist 6 | .eggs 7 | __version__.py 8 | .coverage 9 | htmlcov 10 | xtra 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Adobe Systems Incorporated 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/opentypesvg.svg)](https://pypi.org/project/opentypesvg) 2 | [![Status](https://github.com/adobe-type-tools/opentype-svg/actions/workflows/test.yml/badge.svg)](https://github.com/adobe-type-tools/opentype-svg/actions/workflows/test.yml) 3 | [![Codecov](https://codecov.io/gh/adobe-type-tools/opentype-svg/branch/master/graph/badge.svg)](https://codecov.io/gh/adobe-type-tools/opentype-svg) 4 | 5 | # Tools for making OpenType-SVG fonts 6 | 7 | - **addsvg** adds an SVG table to a font, using SVG files provided. The font's format can be either OpenType or TrueType. 8 | 9 | - **dumpsvg** saves the contents of a font's SVG table as individual SVG files. The font's format can be either OpenType, TrueType, WOFF, or WOFF2. 10 | 11 | - **fonts2svg** generates a set of SVG glyph files from one or more fonts and hex colors for each of them. The fonts' format can be either OpenType, TrueType, WOFF, or WOFF2. 12 | 13 | 14 | ### Dependencies 15 | 16 | - Python 3.6 or higher 17 | 18 | - [FontTools](https://github.com/fonttools/fonttools) 3.1.0 or higher 19 | 20 | 21 | ### Installation instructions 22 | 23 | - Make sure you have Python 3.6 (or higher) installed: 24 | 25 | ```sh 26 | python --version 27 | ``` 28 | 29 | - Setup a virtual environment: 30 | 31 | ```sh 32 | python -m venv ot-svg 33 | ``` 34 | 35 | - Activate the virtual environment: 36 | 37 | - macOS & Linux 38 | 39 | ```sh 40 | source ot-svg/bin/activate 41 | ``` 42 | 43 | - Windows 44 | 45 | ```sh 46 | ot-svg\Scripts\activate.bat 47 | ``` 48 | 49 | - Install **opentypesvg**: 50 | 51 | ```sh 52 | python -m pip install opentypesvg 53 | ``` 54 | 55 | # How to make OpenType-SVG fonts? 56 | 57 | ### Step 1 58 | #### Generate a set of SVG files from a series of fonts and color values. 59 | 60 | ![step1](https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/master/imgs/step1.png "step 1") 61 | 62 | fonts2svg -c 99ccff,ff0066,cc0066 fonts/Zebrawood-Shadow.otf fonts/Zebrawood-Fill.otf fonts/Zebrawood-Dots.otf 63 | 64 | ### Step 2 65 | #### Add a set of SVG files to an existing OpenType (or TrueType) font. 66 | 67 | ![step2](https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/master/imgs/step2.png "step 2") 68 | 69 | addsvg fonts/SVGs fonts/Zebrawood.otf 70 | 71 | --- 72 | 73 | You can use **Step 2** without doing **Step 1**, but there are a few things you need to be aware of when using the **addsvg** tool: 74 | 75 | * After the SVG files are saved with the authoring application (e.g. Adobe Illustrator, CorelDRAW!, Inkscape) they should be put thru a process that optimizes and cleans up the SVG code; this will slim down the file size while keeping the resulting artwork the same. For this step you can use one of these tools: 76 | * [SVG Cleaner](https://github.com/RazrFalcon/svgcleaner-gui/releases) (GUI version) 77 | * [SVG Cleaner](https://github.com/RazrFalcon/svgcleaner) (command line version) 78 | * [SVG Optimizer](https://github.com/svg/svgo) 79 | * [Scour](https://github.com/scour-project/scour) 80 | * [picosvg](https://github.com/googlefonts/picosvg) 81 | 82 | * **addsvg** requires the SVG files to be named according to the glyphs which they are meant to be associated with. For example, if the glyph in the font is named **ampersand**, the SVG file must be named `ampersand.svg`. 83 | 84 | * **addsvg** expects the color artwork to have been designed at the same size as the glyphs in the font, usually 1000 or 2048 UPM. This means 1 point (pt) in the authoring app equals 1 unit in font coordinates. If the artwork's canvas size is not the same as the font's UPM value, use the `-k` option to prevent the tool from removing the SVG's `viewBox` parameter. Retaining the `viewBox` parameter will enable the artwork to be scaled to the full extent of the viewport (i.e. the font's UPM). 85 | 86 | * If the artwork's `` element contains `height` and/or `width` parameters, remove them, otherwise the artwork will have a fixed size and won't be allowed to scale to the full extent of the viewport. 87 | -------------------------------------------------------------------------------- /fonts/Zebrawood-Dots.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/fonts/Zebrawood-Dots.otf -------------------------------------------------------------------------------- /fonts/Zebrawood-Fill.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/fonts/Zebrawood-Fill.otf -------------------------------------------------------------------------------- /fonts/Zebrawood-Shadow.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/fonts/Zebrawood-Shadow.otf -------------------------------------------------------------------------------- /fonts/Zebrawood.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/fonts/Zebrawood.otf -------------------------------------------------------------------------------- /fonts/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SVG font test 6 | 26 | 27 | 28 |

AYZ

29 | 30 | 31 | -------------------------------------------------------------------------------- /imgs/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/imgs/step1.png -------------------------------------------------------------------------------- /imgs/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/imgs/step2.png -------------------------------------------------------------------------------- /lib/opentypesvg/addsvg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2016 Adobe. All rights reserved. 4 | 5 | """ 6 | Adds an SVG table to a font, using SVG files provided. 7 | The font format can be either OpenType or TrueType. 8 | """ 9 | 10 | import argparse 11 | import os 12 | import re 13 | import sys 14 | from shutil import copy2 15 | 16 | from fontTools import ttLib 17 | 18 | from opentypesvg.__version__ import version as __version__ 19 | from opentypesvg.utils import ( 20 | read_file, 21 | split_comma_sequence, 22 | validate_folder_path, 23 | validate_font_paths, 24 | ) 25 | 26 | 27 | def getGlyphNameFromFileName(filePath): 28 | fontFileName = os.path.split(filePath)[1] 29 | return os.path.splitext(fontFileName)[0] 30 | 31 | 32 | reIDvalue = re.compile(r"]+?(id=\".*?\").+?>", re.DOTALL) 33 | 34 | 35 | def setIDvalue(data, gid): 36 | id_value = reIDvalue.search(data) 37 | if id_value: 38 | return re.sub(id_value.group(1), 'id="glyph{}"'.format(gid), data) 39 | return re.sub(')", re.DOTALL) 46 | 47 | 48 | def stripViewBox(svgItemData): 49 | """ 50 | Removes the viewBox parameter from the element. 51 | """ 52 | vb = reViewBox.search(svgItemData) 53 | if vb: 54 | svgItemData = reViewBox.sub(r"\g<1>\g<3>", svgItemData) 55 | return svgItemData 56 | 57 | 58 | reXMLheader = re.compile(r"<\?xml .*\?>") 59 | reEnableBkgrd = re.compile(r"( enable-background=[\"|\'][new\d, ]+[\"|\'])") 60 | reWhiteSpaceBtween = re.compile(r">\s+<", re.MULTILINE) 61 | reWhiteSpaceWithin = re.compile(r"\s+", re.MULTILINE) 62 | 63 | 64 | def cleanupSVGdoc(svgItemData): 65 | # Remove XML header 66 | svgItemData = reXMLheader.sub('', svgItemData) 67 | 68 | # Remove all 'enable-background' parameters 69 | for enableBkgrd in reEnableBkgrd.findall(svgItemData): 70 | svgItemData = svgItemData.replace(enableBkgrd, '') 71 | 72 | # Remove all white space BETWEEN elements 73 | for whiteSpace in reWhiteSpaceBtween.findall(svgItemData): 74 | svgItemData = svgItemData.replace(whiteSpace, '><') 75 | 76 | # Replace all white space WITHIN elements with a single space 77 | for whiteSpace2 in reWhiteSpaceWithin.findall(svgItemData): 78 | svgItemData = svgItemData.replace(whiteSpace2, ' ') 79 | 80 | return svgItemData 81 | 82 | 83 | reCopyCounter = re.compile(r"#\d+$") 84 | 85 | 86 | def makeFontCopyPath(fontPath): 87 | dirName, fileName = os.path.split(fontPath) 88 | fileName, fileExt = os.path.splitext(fileName) 89 | fileName = reCopyCounter.split(fileName)[0] 90 | fontCopyPath = os.path.join(dirName, fileName + fileExt) 91 | n = 0 92 | while os.path.exists(fontCopyPath): 93 | newPath = fileName + "#" + repr(n) + fileExt 94 | fontCopyPath = os.path.join(dirName, newPath) 95 | n += 1 96 | return fontCopyPath 97 | 98 | 99 | def processFont(fontPath, svgFilePathsList, options): 100 | font = ttLib.TTFont(fontPath) 101 | 102 | svgDocsDict = {} 103 | gNamesSeenAlreadyList = [] 104 | 105 | svgGlyphsAdded = 0 106 | 107 | for svgFilePath in svgFilePathsList: 108 | gName = getGlyphNameFromFileName(svgFilePath) 109 | 110 | if gName in options.gnames_to_exclude: 111 | continue 112 | 113 | try: 114 | gid = font.getGlyphID(gName) 115 | except KeyError: 116 | print("WARNING: Could not find a glyph named {} in the font " 117 | "{}".format(gName, os.path.split(fontPath)[1]), 118 | file=sys.stderr) 119 | continue 120 | 121 | if gName in gNamesSeenAlreadyList: 122 | print("WARNING: Skipped a duplicate file named {}.svg at " 123 | "{}".format(gName, svgFilePath), file=sys.stderr) 124 | continue 125 | else: 126 | gNamesSeenAlreadyList.append(gName) 127 | 128 | svgItemData = read_file(svgFilePath) 129 | 130 | # Set id value 131 | svgItemData = setIDvalue(svgItemData, gid) 132 | 133 | # Remove the viewBox parameter 134 | if options.strip_viewbox: 135 | svgItemData = stripViewBox(svgItemData) 136 | 137 | # Clean-up SVG document 138 | svgItemData = cleanupSVGdoc(svgItemData) 139 | 140 | svgDocsDict[gid] = [svgItemData.strip(), gid, gid] 141 | svgGlyphsAdded += 1 142 | 143 | # Don't do any changes to the input font if there's no SVG data 144 | if not svgDocsDict: 145 | print("Could not find any SVG files that can be added to the font.", 146 | file=sys.stdout) 147 | sys.exit(0) 148 | 149 | svgDocsList = [svgDocsDict[index] for index in sorted(svgDocsDict.keys())] 150 | 151 | svgTable = ttLib.newTable('SVG ') 152 | svgTable.compressed = options.compress_svgs 153 | svgTable.docList = svgDocsList 154 | svgTable.colorPalettes = None 155 | font['SVG '] = svgTable 156 | 157 | # Make copy of the original font 158 | if options.make_font_copy: 159 | fontCopyPath = makeFontCopyPath(fontPath) 160 | copy2(fontPath, fontCopyPath) 161 | 162 | font.save(fontPath) 163 | 164 | if options.generate_woffs: 165 | # WOFF files are smaller if SVG table is uncompressed 166 | font['SVG '].compressed = False 167 | for ext in ['woff', 'woff2']: 168 | woffFontPath = os.path.splitext(fontPath)[0] + '.' + ext 169 | font.flavor = ext 170 | font.save(woffFontPath) 171 | 172 | font.close() 173 | 174 | plural = 's were' if svgGlyphsAdded != 1 else ' was' 175 | print("{} SVG glyph{} successfully added to {}".format( 176 | svgGlyphsAdded, plural, os.path.split(fontPath)[1]), file=sys.stdout) 177 | 178 | 179 | reSVGelement = re.compile(r".+?", re.DOTALL) 180 | reTEXTelement = re.compile(r".+?", re.DOTALL) 181 | 182 | 183 | def validateSVGfiles(svgFilePathsList): 184 | """ 185 | Light validation of SVG files. 186 | - checks that there is an element. 187 | - skips files that have a element. 188 | """ 189 | validatedPaths = [] 190 | 191 | for filePath in svgFilePathsList: 192 | # Skip hidden files (filenames that start with period) 193 | fileName = os.path.basename(filePath) 194 | if fileName.startswith('.'): 195 | continue 196 | 197 | # Skip files that don't end with SVG extension 198 | if not fileName.lower().endswith('.svg'): 199 | continue 200 | 201 | if not os.path.isfile(filePath): 202 | raise AssertionError("Not a valid file path: {}".format(filePath)) 203 | data = read_file(filePath) 204 | 205 | # Find blob 206 | svg = reSVGelement.search(data) 207 | if not svg: 208 | print("WARNING: Could not find element in the file. " 209 | "Skiping {}".format(filePath)) 210 | continue 211 | 212 | # Warn about elements 213 | text = reTEXTelement.search(data) 214 | if text: 215 | print("WARNING: Found element in the file. " 216 | "Skiping {}".format(filePath)) 217 | continue 218 | 219 | validatedPaths.append(filePath) 220 | 221 | return validatedPaths 222 | 223 | 224 | def get_options(args): 225 | parser = argparse.ArgumentParser( 226 | formatter_class=argparse.RawTextHelpFormatter, 227 | description=__doc__ 228 | ) 229 | parser.add_argument( 230 | '--version', 231 | action='version', 232 | version=__version__ 233 | ) 234 | parser.add_argument( 235 | '-m', 236 | action='store_false', 237 | dest='make_font_copy', 238 | help='do not make a copy of the input font.' 239 | ) 240 | parser.add_argument( 241 | '-k', 242 | action='store_false', 243 | dest='strip_viewbox', 244 | help="do not strip the 'viewBox' parameter." 245 | ) 246 | parser.add_argument( 247 | '-w', 248 | action='store_true', 249 | dest='generate_woffs', 250 | help='generate WOFF and WOFF2 formats.' 251 | ) 252 | parser.add_argument( 253 | '-x', 254 | metavar='GLYPH_NAMES', 255 | dest='gnames_to_exclude', 256 | type=split_comma_sequence, 257 | default=[], 258 | help='comma-separated sequence of glyph names to exclude.' 259 | ) 260 | parser.add_argument( 261 | '-z', 262 | action='store_true', 263 | dest='compress_svgs', 264 | help='compress the SVG table.' 265 | ) 266 | parser.add_argument( 267 | 'svg_folder_path', 268 | metavar='FOLDER_PATH', 269 | type=validate_folder_path, 270 | help='path to folder containing SVG files.\n' 271 | 'The file names MUST match the names of the\n' 272 | "glyphs they're meant to be associated with." 273 | ) 274 | parser.add_argument( 275 | 'input_path', 276 | metavar='FONT', 277 | help='OTF/TTF font file.', 278 | ) 279 | options = parser.parse_args(args) 280 | 281 | options.font_paths_list = validate_font_paths([options.input_path]) 282 | return options 283 | 284 | 285 | def main(args=None): 286 | opts = get_options(args) 287 | 288 | if not opts.font_paths_list: 289 | return 1 290 | 291 | # Collect the paths to SVG files 292 | svgFilePathsList = [] 293 | for dirName, _, fileList in os.walk(opts.svg_folder_path): 294 | # Support nested folders 295 | for file in fileList: 296 | svgFilePathsList.append(os.path.join(dirName, file)) 297 | 298 | # Validate the SVGs 299 | svgFilePathsList = validateSVGfiles(svgFilePathsList) 300 | 301 | if not svgFilePathsList: 302 | print("No SVG files were found.", file=sys.stdout) 303 | return 1 304 | 305 | processFont(opts.font_paths_list[0], svgFilePathsList, opts) 306 | 307 | 308 | if __name__ == "__main__": 309 | sys.exit(main()) 310 | -------------------------------------------------------------------------------- /lib/opentypesvg/dumpsvg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2016 Adobe. All rights reserved. 4 | 5 | """ 6 | Saves the contents of a font's SVG table as individual SVG files. 7 | The font's format can be either OpenType, TrueType, WOFF, or WOFF2. 8 | """ 9 | 10 | import argparse 11 | import os 12 | import re 13 | import sys 14 | 15 | from fontTools import ttLib 16 | 17 | from opentypesvg.__version__ import version as __version__ 18 | from opentypesvg.utils import ( 19 | create_folder, 20 | create_nested_folder, 21 | final_message, 22 | get_gnames_to_save_in_nested_folder, 23 | get_output_folder_path, 24 | split_comma_sequence, 25 | validate_font_paths, 26 | write_file, 27 | ) 28 | 29 | 30 | reViewBox = re.compile(r"viewBox=[\"|\']([\d, ])+?[\"|\']", re.DOTALL) 31 | 32 | 33 | def resetViewBox(svgDoc): 34 | viewBox = reViewBox.search(svgDoc) 35 | if not viewBox: 36 | return svgDoc 37 | viewBoxPartsList = viewBox.group().split() 38 | viewBoxPartsList[1] = 0 39 | replacement = ' '.join(map(str, viewBoxPartsList)) 40 | svgDocChanged = re.sub(viewBox.group(), replacement, svgDoc) 41 | return svgDocChanged 42 | 43 | 44 | def processFont(fontPath, outputFolderPath, options): 45 | font = ttLib.TTFont(fontPath) 46 | glyphOrder = font.getGlyphOrder() 47 | svgTag = 'SVG ' 48 | 49 | if svgTag not in font: 50 | print("ERROR: The font does not have the {} table.".format( 51 | svgTag.strip()), file=sys.stderr) 52 | sys.exit(1) 53 | 54 | svgTable = font[svgTag] 55 | 56 | if not len(svgTable.docList): 57 | print("ERROR: The {} table has no data that can be output.".format( 58 | svgTag.strip()), file=sys.stderr) 59 | sys.exit(1) 60 | 61 | # Define the list of glyph names to convert to SVG 62 | if options.gnames_to_generate: 63 | glyphNamesList = sorted(options.gnames_to_generate) 64 | else: 65 | glyphNamesList = sorted(glyphOrder) 66 | 67 | # Confirm that there's something to process 68 | if not glyphNamesList: 69 | print("The fonts and options provided can't produce any SVG files.", 70 | file=sys.stdout) 71 | return 72 | 73 | # Define the list of glyph names to skip 74 | glyphNamesToSkipList = [".notdef"] 75 | if options.gnames_to_exclude: 76 | glyphNamesToSkipList.extend(options.gnames_to_exclude) 77 | 78 | # Determine which glyph names need to be saved in a nested folder 79 | glyphNamesToSaveInNestedFolder = get_gnames_to_save_in_nested_folder( 80 | glyphNamesList) 81 | 82 | nestedFolderPath = None 83 | filesSaved = 0 84 | unnamedNum = 1 85 | 86 | # Write the SVG files by iterating over the entries in the SVG table. 87 | # The process assumes that each SVG document contains only one 'id' value, 88 | # i.e. the artwork for two or more glyphs is not included in a single SVG. 89 | for svgItemsList in svgTable.docList: 90 | svgItemData, startGID, endGID = svgItemsList 91 | 92 | if options.reset_viewbox: 93 | svgItemData = resetViewBox(svgItemData) 94 | 95 | while (startGID != endGID + 1): 96 | try: 97 | gName = glyphOrder[startGID] 98 | except IndexError: 99 | gName = "_unnamed{}".format(unnamedNum) 100 | glyphNamesList.append(gName) 101 | unnamedNum += 1 102 | print("WARNING: The SVG table references a glyph ID (#{}) " 103 | "which the font does not contain.\n" 104 | " The artwork will be saved as '{}.svg'.".format( 105 | startGID, gName), file=sys.stderr) 106 | 107 | if gName not in glyphNamesList or gName in glyphNamesToSkipList: 108 | startGID += 1 109 | continue 110 | 111 | # Create the output folder. 112 | # This may be necessary if the folder was not provided. 113 | # The folder is made this late in the process because 114 | # only now it's clear that's needed. 115 | create_folder(outputFolderPath) 116 | 117 | # Create the nested folder, if there are conflicting glyph names. 118 | if gName in glyphNamesToSaveInNestedFolder: 119 | folderPath = create_nested_folder(nestedFolderPath, 120 | outputFolderPath) 121 | else: 122 | folderPath = outputFolderPath 123 | 124 | svgFilePath = os.path.join(folderPath, gName + '.svg') 125 | write_file(svgFilePath, svgItemData) 126 | filesSaved += 1 127 | startGID += 1 128 | 129 | font.close() 130 | final_message(filesSaved) 131 | 132 | 133 | def get_options(args): 134 | parser = argparse.ArgumentParser( 135 | formatter_class=argparse.RawTextHelpFormatter, 136 | description=__doc__ 137 | ) 138 | parser.add_argument( 139 | '--version', 140 | action='version', 141 | version=__version__ 142 | ) 143 | parser.add_argument( 144 | '-o', 145 | metavar='FOLDER_PATH', 146 | dest='output_folder_path', 147 | help='path to folder for outputting the SVG files to.' 148 | ) 149 | parser.add_argument( 150 | '-r', 151 | action='store_true', 152 | dest='reset_viewbox', 153 | help='reset viewBox values.' 154 | ) 155 | parser.add_argument( 156 | '-g', 157 | metavar='GLYPH_NAMES', 158 | dest='gnames_to_generate', 159 | type=split_comma_sequence, 160 | default=[], 161 | help='comma-separated sequence of glyph names to make SVG files from.' 162 | ) 163 | parser.add_argument( 164 | '-x', 165 | metavar='GLYPH_NAMES', 166 | dest='gnames_to_exclude', 167 | type=split_comma_sequence, 168 | default=[], 169 | help='comma-separated sequence of glyph names to exclude.' 170 | ) 171 | parser.add_argument( 172 | 'input_path', 173 | metavar='FONT', 174 | help='OTF/TTF/WOFF/WOFF2 font file.', 175 | ) 176 | options = parser.parse_args(args) 177 | 178 | options.font_paths_list = validate_font_paths([options.input_path]) 179 | return options 180 | 181 | 182 | def main(args=None): 183 | opts = get_options(args) 184 | 185 | if not opts.font_paths_list: 186 | return 1 187 | 188 | first_font_path = opts.font_paths_list[0] 189 | 190 | output_folder_path = get_output_folder_path(opts.output_folder_path, 191 | first_font_path) 192 | 193 | processFont(first_font_path, output_folder_path, opts) 194 | 195 | 196 | if __name__ == "__main__": 197 | sys.exit(main()) 198 | -------------------------------------------------------------------------------- /lib/opentypesvg/fonts2svg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2016 Adobe. All rights reserved. 4 | 5 | """ 6 | Generates a set of SVG glyph files from one or more fonts and hex colors 7 | for each of them. The fonts' format can be either OpenType, TrueType, WOFF, 8 | or WOFF2. 9 | """ 10 | 11 | import argparse 12 | import os 13 | import re 14 | import sys 15 | 16 | from fontTools import ttLib 17 | from fontTools.pens.basePen import BasePen 18 | from fontTools.pens.transformPen import TransformPen 19 | 20 | from opentypesvg.__version__ import version as __version__ 21 | from opentypesvg.utils import ( 22 | create_folder, 23 | create_nested_folder, 24 | final_message, 25 | get_gnames_to_save_in_nested_folder, 26 | get_output_folder_path, 27 | split_comma_sequence, 28 | validate_font_paths, 29 | write_file, 30 | ) 31 | 32 | 33 | class SVGPen(BasePen): 34 | 35 | def __init__(self, glyphSet): 36 | BasePen.__init__(self, glyphSet) 37 | self.d = u'' 38 | self._lastX = self._lastY = None 39 | 40 | def _moveTo(self, pt): 41 | ptx, pty = self._isInt(pt) 42 | self.d += u'M{} {}'.format(ptx, pty) 43 | self._lastX, self._lastY = pt 44 | 45 | def _lineTo(self, pt): 46 | ptx, pty = self._isInt(pt) 47 | if (ptx, pty) == (self._lastX, self._lastY): 48 | return 49 | elif ptx == self._lastX: 50 | self.d += u'V{}'.format(pty) 51 | elif pty == self._lastY: 52 | self.d += u'H{}'.format(ptx) 53 | else: 54 | self.d += u'L{} {}'.format(ptx, pty) 55 | self._lastX, self._lastY = pt 56 | 57 | def _curveToOne(self, pt1, pt2, pt3): 58 | pt1x, pt1y = self._isInt(pt1) 59 | pt2x, pt2y = self._isInt(pt2) 60 | pt3x, pt3y = self._isInt(pt3) 61 | self.d += u'C{} {} {} {} {} {}'.format(pt1x, pt1y, pt2x, pt2y, 62 | pt3x, pt3y) 63 | self._lastX, self._lastY = pt3 64 | 65 | def _qCurveToOne(self, pt1, pt2): 66 | pt1x, pt1y = self._isInt(pt1) 67 | pt2x, pt2y = self._isInt(pt2) 68 | self.d += u'Q{} {} {} {}'.format(pt1x, pt1y, pt2x, pt2y) 69 | self._lastX, self._lastY = pt2 70 | 71 | def _closePath(self): 72 | self.d += u'Z' 73 | self._lastX = self._lastY = None 74 | 75 | def _endPath(self): 76 | self._closePath() 77 | 78 | @staticmethod 79 | def _isInt(tup): 80 | return [int(flt) if (flt).is_integer() else flt for flt in tup] 81 | 82 | 83 | def processFonts(font_paths_list, hex_colors_list, outputFolderPath, options): 84 | glyphSetsList = [] 85 | allGlyphNamesList = [] 86 | 87 | # Load the fonts and collect their glyph sets 88 | for fontPath in font_paths_list: 89 | try: 90 | font = ttLib.TTFont(fontPath) 91 | gSet = font.getGlyphSet() 92 | glyphSetsList.append(gSet) 93 | allGlyphNamesList.append(gSet.keys()) 94 | font.close() 95 | 96 | except ttLib.TTLibError: 97 | print(f"ERROR: {fontPath} cannot be processed.", 98 | file=sys.stderr) 99 | return 1 100 | 101 | # Define the list of glyph names to convert to SVG 102 | if options.gnames_to_generate: 103 | glyphNamesList = sorted(set(options.gnames_to_generate)) 104 | else: 105 | if options.glyphsets_union: 106 | glyphNamesList = sorted( 107 | set.union(*map(set, allGlyphNamesList))) 108 | else: 109 | glyphNamesList = sorted( 110 | set.intersection(*map(set, allGlyphNamesList))) 111 | # Extend the list with additional glyph names 112 | if options.gnames_to_add: 113 | glyphNamesList.extend(options.gnames_to_add) 114 | # Remove any duplicates and sort 115 | glyphNamesList = sorted(set(glyphNamesList)) 116 | 117 | # Remove '.notdef' 118 | if '.notdef' in glyphNamesList: 119 | glyphNamesList.remove('.notdef') 120 | 121 | # Confirm that there's something to process 122 | if not glyphNamesList: 123 | print("The fonts and options provided can't produce any SVG files.", 124 | file=sys.stdout) 125 | return 1 126 | 127 | # Define the list of glyph names to skip 128 | glyphNamesToSkipList = [] 129 | if options.gnames_to_exclude: 130 | glyphNamesToSkipList.extend(options.gnames_to_exclude) 131 | 132 | # Determine which glyph names need to be saved in a nested folder 133 | glyphNamesToSaveInNestedFolder = get_gnames_to_save_in_nested_folder( 134 | glyphNamesList) 135 | 136 | nestedFolderPath = None 137 | filesSaved = 0 138 | 139 | viewbox = viewbox_settings( 140 | font_paths_list[0], 141 | options.adjust_view_box_to_glyph 142 | ) 143 | 144 | # Generate the SVGs 145 | for gName in glyphNamesList: 146 | svgStr = (u"""\n""".format(viewbox)) 148 | 149 | for index, gSet in enumerate(glyphSetsList): 150 | # Skip glyphs that don't exist in the current font, 151 | # or that were requested to be skipped 152 | if gName not in gSet.keys() or gName in glyphNamesToSkipList: 153 | continue 154 | 155 | pen = SVGPen(gSet) 156 | tpen = TransformPen(pen, (1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) 157 | glyph = gSet[gName] 158 | glyph.draw(tpen) 159 | d = pen.d 160 | # Skip glyphs with no contours 161 | if not len(d): 162 | continue 163 | 164 | hex_str = hex_colors_list[index] 165 | opc = '' 166 | if len(hex_str) != 6: 167 | opcHex = hex_str[6:] 168 | hex_str = hex_str[:6] 169 | if opcHex.lower() != 'ff': 170 | opc = ' opacity="{:.2f}"'.format(int(opcHex, 16) / 255) 171 | 172 | svgStr += u'\t\n'.format( 173 | opc, hex_str, d) 174 | svgStr += u'' 175 | 176 | # Skip saving files that have no paths 177 | if ' length_font_paths: 331 | num_xtr_col = length_hex_colors - length_font_paths 332 | print("WARNING: The list of colors got the last {} value(s) truncated:" 333 | " {}".format(num_xtr_col, ' '.join( 334 | hex_colors_list[-num_xtr_col:])), file=sys.stderr) 335 | del hex_colors_list[length_font_paths:] 336 | 337 | output_folder_path = get_output_folder_path(opts.output_folder_path, 338 | font_paths_list[0]) 339 | 340 | return processFonts(font_paths_list, hex_colors_list, output_folder_path, 341 | opts) 342 | 343 | 344 | if __name__ == "__main__": 345 | sys.exit(main()) 346 | -------------------------------------------------------------------------------- /lib/opentypesvg/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Adobe. All rights reserved. 2 | 3 | """ 4 | Module that contains shared functionality. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | 11 | SVG_FOLDER_NAME = "SVGs" 12 | NESTED_FOLDER_NAME = "_moreSVGs_" 13 | 14 | 15 | def read_file(file_path): 16 | with open(file_path, "r") as f: 17 | return f.read() 18 | 19 | 20 | def write_file(file_path, data): 21 | with open(file_path, "w") as f: 22 | f.write(data) 23 | 24 | 25 | def get_font_format(font_file_path): 26 | with open(font_file_path, "rb") as f: 27 | head = f.read(4).decode() 28 | 29 | if head == "OTTO": 30 | return "OTF" 31 | elif head in ("\x00\x01\x00\x00", "true"): 32 | return "TTF" 33 | elif head == "wOFF": 34 | return "WOFF" 35 | elif head == "wOF2": 36 | return "WOFF2" 37 | return None 38 | 39 | 40 | def validate_font_paths(paths_list): 41 | validated_paths_list = [] 42 | for path in paths_list: 43 | path = os.path.realpath(path) 44 | if (os.path.isfile(path) and get_font_format(path) in 45 | ['OTF', 'TTF', 'WOFF', 'WOFF2']): 46 | validated_paths_list.append(path) 47 | else: 48 | print("ERROR: {} is not a valid font file path.".format(path), 49 | file=sys.stderr) 50 | return validated_paths_list 51 | 52 | 53 | def split_comma_sequence(comma_str): 54 | return [item.strip() for item in comma_str.split(',')] 55 | 56 | 57 | def final_message(num_files_saved): 58 | if not num_files_saved: 59 | num_files_saved = 'No' 60 | plural = 's' if num_files_saved != 1 else '' 61 | print("{} SVG file{} saved.".format(num_files_saved, plural), 62 | file=sys.stdout) 63 | 64 | 65 | def create_folder(folder_path): 66 | try: 67 | os.makedirs(folder_path) 68 | except OSError: 69 | if not os.path.isdir(folder_path): 70 | raise 71 | 72 | 73 | def create_nested_folder(nested_folder_path, main_folder_path): 74 | """ 75 | Creates a nested folder and returns its path. 76 | This additional folder is created when file names conflict. 77 | """ 78 | if not nested_folder_path: 79 | nested_folder_path = os.path.join(main_folder_path, NESTED_FOLDER_NAME) 80 | create_folder(nested_folder_path) 81 | return nested_folder_path 82 | 83 | 84 | def validate_folder_path(folder_path): 85 | """ 86 | Validates that the path is a folder. 87 | Returns the complete path. 88 | """ 89 | path = os.path.realpath(folder_path) 90 | if os.path.isdir(path): 91 | return path 92 | else: 93 | print("ERROR: {} is not a valid folder path.".format(path), 94 | file=sys.stderr) 95 | sys.exit(1) 96 | 97 | 98 | def get_output_folder_path(provided_folder_path, first_font_path): 99 | """ 100 | If the path to the output folder was NOT provided, create 101 | a folder in the same directory where the first font is. 102 | If the path was provided, validate it. 103 | Returns a valid output folder. 104 | """ 105 | if provided_folder_path: 106 | return validate_folder_path(provided_folder_path) 107 | return os.path.join(os.path.dirname(first_font_path), SVG_FOLDER_NAME) 108 | 109 | 110 | def get_gnames_to_save_in_nested_folder(gnames_list): 111 | """ 112 | On case-insensitive systems the SVG files cannot be all saved to the 113 | same folder otherwise a.svg and A.svg would be written over each other, 114 | for example. So, pre-process the list of glyph names to find which ones 115 | step on each other, and save half of them in a nested folder. This 116 | approach won't handle the case where a.svg and A.svg are NOT generated 117 | on the same run, but that's fine; the user will have to handle that. 118 | Also, the process below assumes that there are no more than 2 conflicts 119 | per name, i.e. it will handle "the/The" but not "the/The/THE/..."; 120 | this shouldn't be a problem in 99% of the time. 121 | Returns list of glyph names that need to be saved in a nested folder. 122 | """ 123 | unique_names_set = set() 124 | gnames_to_save_in_nested_folder = [] 125 | for gname in gnames_list: 126 | if gname.lower() in unique_names_set: 127 | gnames_to_save_in_nested_folder.append(gname) 128 | unique_names_set.add(gname.lower()) 129 | return gnames_to_save_in_nested_folder 130 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | filterwarnings = 4 | ignore:The py23 module:DeprecationWarning 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="opentypesvg", 8 | use_scm_version={'write_to': 'lib/opentypesvg/__version__.py'}, 9 | setup_requires=["setuptools_scm"], 10 | author="Miguel Sousa", 11 | author_email="msousa@adobe.com", 12 | description="Tools for making OpenType-SVG fonts", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/adobe-type-tools/opentype-svg", 16 | license="MIT", 17 | platforms=["Any"], 18 | package_dir={'': 'lib'}, 19 | packages=['opentypesvg'], 20 | python_requires='>=3.6', 21 | install_requires=['fontTools[woff]>=3.1.0'], 22 | entry_points={ 23 | 'console_scripts': [ 24 | "addsvg = opentypesvg.addsvg:main", 25 | "dumpsvg = opentypesvg.dumpsvg:main", 26 | "fonts2svg = opentypesvg.fonts2svg:main", 27 | ] 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Adobe. All rights reserved. 2 | 3 | import os 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def fonts_dir(): 9 | root_dir = os.path.dirname(os.path.dirname(__file__)) 10 | yield os.path.join(root_dir, 'fonts') 11 | 12 | 13 | @pytest.fixture 14 | def base_font_path(fonts_dir): 15 | yield os.path.join(fonts_dir, 'Zebrawood.otf') 16 | 17 | 18 | @pytest.fixture 19 | def shadow_font_path(fonts_dir): 20 | yield os.path.join(fonts_dir, 'Zebrawood-Shadow.otf') 21 | 22 | 23 | @pytest.fixture 24 | def fill_font_path(fonts_dir): 25 | yield os.path.join(fonts_dir, 'Zebrawood-Fill.otf') 26 | 27 | 28 | @pytest.fixture 29 | def dots_font_path(fonts_dir): 30 | yield os.path.join(fonts_dir, 'Zebrawood-Dots.otf') 31 | 32 | 33 | @pytest.fixture 34 | def fixtures_dir(): 35 | yield os.path.join(os.path.dirname(__file__), 'fixtures') 36 | -------------------------------------------------------------------------------- /tests/dumpsvg_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Adobe. All rights reserved. 2 | 3 | import pytest 4 | 5 | from opentypesvg.dumpsvg import main 6 | 7 | 8 | def test_font_without_svg_table(base_font_path, capsys): 9 | with pytest.raises(SystemExit): 10 | main([base_font_path]) 11 | captured = capsys.readouterr() 12 | assert captured.err == "ERROR: The font does not have the SVG table.\n" 13 | -------------------------------------------------------------------------------- /tests/fixtures/1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/1.ttf -------------------------------------------------------------------------------- /tests/fixtures/12.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/12.ttf -------------------------------------------------------------------------------- /tests/fixtures/1234.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/1234.ttf -------------------------------------------------------------------------------- /tests/fixtures/13.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/13.ttf -------------------------------------------------------------------------------- /tests/fixtures/14.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/14.ttf -------------------------------------------------------------------------------- /tests/fixtures/2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/2.ttf -------------------------------------------------------------------------------- /tests/fixtures/23.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/23.ttf -------------------------------------------------------------------------------- /tests/fixtures/24.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/24.ttf -------------------------------------------------------------------------------- /tests/fixtures/3.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/3.ttf -------------------------------------------------------------------------------- /tests/fixtures/34.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/34.ttf -------------------------------------------------------------------------------- /tests/fixtures/4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/4.ttf -------------------------------------------------------------------------------- /tests/fixtures/A.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/Y.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/Z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/aA.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/aA.ttf -------------------------------------------------------------------------------- /tests/fixtures/ab.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/ab.ttf -------------------------------------------------------------------------------- /tests/fixtures/b.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/fixtures/bc.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/bc.ttf -------------------------------------------------------------------------------- /tests/fixtures/blank_glyph.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/blank_glyph.ttf -------------------------------------------------------------------------------- /tests/fixtures/cd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/cd.ttf -------------------------------------------------------------------------------- /tests/fixtures/no_glyf_table.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/no_glyf_table.ttf -------------------------------------------------------------------------------- /tests/fixtures/no_head_table.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/opentype-svg/538319fa425488baf3263ef2f4d752eb421f3ea3/tests/fixtures/no_head_table.ttf -------------------------------------------------------------------------------- /tests/fonts2svg_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Adobe. All rights reserved. 2 | 3 | import filecmp 4 | import os 5 | import pytest 6 | import shutil 7 | 8 | from opentypesvg.fonts2svg import viewbox_settings, main 9 | from opentypesvg.utils import SVG_FOLDER_NAME, NESTED_FOLDER_NAME 10 | 11 | 12 | @pytest.mark.parametrize('file_name, expected', [ 13 | ("1.ttf", "309 -1108 360 638"), 14 | ("2.ttf", "-904 -1110 416 650"), 15 | ("3.ttf", "-824 310 420 662"), 16 | ("4.ttf", "197 342 452 638"), 17 | ("12.ttf", "-904 -1110 1573 650"), 18 | ("13.ttf", "-824 -1108 1493 2080"), 19 | ("14.ttf", "197 -1108 472 2088"), 20 | ("23.ttf", "-904 -1110 500 2082"), 21 | ("24.ttf", "-904 -1110 1553 2090"), 22 | ("34.ttf", "-824 310 1473 670"), 23 | ("1234.ttf", "-904 -1110 1573 2090"), 24 | ("no_head_table.ttf", "0 -1000 1000 1000"), 25 | ]) 26 | def test_adjust_to_viewbox(file_name, expected, fixtures_dir): 27 | font_path = os.path.join(fixtures_dir, file_name) 28 | viewbox = viewbox_settings(font_path, True) 29 | assert viewbox == expected 30 | 31 | 32 | def test_adjust_to_viewbox_default(base_font_path): 33 | viewbox = viewbox_settings(base_font_path, False) 34 | assert viewbox == "0 -1000 1000 1000" 35 | 36 | 37 | def test_main(shadow_font_path, fill_font_path, dots_font_path, fixtures_dir, 38 | tmp_path): 39 | """ 40 | Generate SVG files to a temp folder and compare with expected fixtures. 41 | """ 42 | output_folder = str(tmp_path) 43 | 44 | main(['-c', '99ccff77,ff0066aA,cc0066FF', 45 | shadow_font_path, fill_font_path, dots_font_path, 46 | '-o', output_folder]) 47 | 48 | for gname in ('A', 'Y', 'Z'): 49 | svg_file_name = f'{gname}.svg' 50 | expected_svg_path = os.path.join(fixtures_dir, svg_file_name) 51 | test_svg_path = os.path.join(output_folder, svg_file_name) 52 | assert os.path.exists(test_svg_path) 53 | assert filecmp.cmp(test_svg_path, expected_svg_path) 54 | 55 | 56 | def test_main_invalid_path(capsys): 57 | file_name = 'not_a_file' 58 | exit_val = main([file_name]) 59 | captured = capsys.readouterr() 60 | assert captured.err == (f"ERROR: {os.path.realpath(file_name)} " 61 | "is not a valid font file path.\n") 62 | assert exit_val == 1 63 | 64 | 65 | def test_main_bad_font(fixtures_dir, capsys): 66 | font_path = os.path.join(fixtures_dir, 'no_glyf_table.ttf') 67 | exit_val = main(['-c', '00ff00', font_path]) 68 | captured = capsys.readouterr() 69 | assert captured.err == (f"ERROR: {os.path.realpath(font_path)} " 70 | "cannot be processed.\n") 71 | assert exit_val == 1 72 | 73 | 74 | def test_main_svgs_folder(fixtures_dir, tmp_path): 75 | """ 76 | Confirm that 'SVG_FOLDER_NAME' is created when output folder is not 77 | provided. 78 | """ 79 | font_name = 'ab.ttf' 80 | temp_dir = str(tmp_path) 81 | fixt_font_path = os.path.join(fixtures_dir, font_name) 82 | temp_font_path = os.path.join(temp_dir, font_name) 83 | 84 | # copy test font to temp dir 85 | assert not os.path.exists(temp_font_path) 86 | shutil.copy2(fixt_font_path, temp_font_path) 87 | assert os.path.exists(temp_font_path) 88 | 89 | # confirm that SVG_FOLDER_NAME doesn't exist 90 | svgs_dir = os.path.join(temp_dir, SVG_FOLDER_NAME) 91 | assert not os.path.exists(svgs_dir) 92 | 93 | main(['-c', '009900', temp_font_path]) 94 | 95 | assert os.path.isdir(svgs_dir) 96 | assert os.path.exists(os.path.join(temp_dir, SVG_FOLDER_NAME, 'a.svg')) 97 | 98 | # test that overlapping contour points are skipped when the glyph is 99 | # written to SVG path 100 | svg_file_name = 'b.svg' 101 | test_svg_path = os.path.join(temp_dir, SVG_FOLDER_NAME, svg_file_name) 102 | assert os.path.exists(test_svg_path) 103 | expected_svg_path = os.path.join(fixtures_dir, svg_file_name) 104 | assert filecmp.cmp(test_svg_path, expected_svg_path) 105 | 106 | 107 | def test_main_nested_folder(fixtures_dir, tmp_path): 108 | """ 109 | Confirm that 'NESTED_FOLDER_NAME' is created when there are glyph names 110 | that can produce conflicting files in case-insensitive systems. 111 | """ 112 | font_name = 'aA.ttf' 113 | temp_dir = str(tmp_path) 114 | fixt_font_path = os.path.join(fixtures_dir, font_name) 115 | 116 | # confirm that NESTED_FOLDER_NAME doesn't exist 117 | more_svgs_dir = os.path.join(temp_dir, NESTED_FOLDER_NAME) 118 | assert not os.path.exists(more_svgs_dir) 119 | 120 | main(['-c', '009900', fixt_font_path, '-o', temp_dir]) 121 | 122 | assert os.path.isdir(more_svgs_dir) 123 | assert os.path.exists(os.path.join(temp_dir, 'A.svg')) 124 | assert os.path.exists(os.path.join(temp_dir, NESTED_FOLDER_NAME, 'a.svg')) 125 | 126 | 127 | def test_main_blank_glyph(fixtures_dir, tmp_path): 128 | """ 129 | Confirm that SVG files are not generated for glyphs without outlines. 130 | """ 131 | font_name = 'blank_glyph.ttf' 132 | temp_dir = str(tmp_path) 133 | fixt_font_path = os.path.join(fixtures_dir, font_name) 134 | 135 | main(['-c', '009900', fixt_font_path, '-o', temp_dir]) 136 | 137 | assert os.path.exists(os.path.join(temp_dir, 'a.svg')) 138 | assert not os.path.exists(os.path.join(temp_dir, 'b.svg')) 139 | 140 | 141 | def test_main_g_opt_and_no_colors(shadow_font_path, capsys, tmp_path): 142 | """ 143 | Only generate 'A.svg' and don't specify the hex color. 144 | """ 145 | output_folder = str(tmp_path) 146 | 147 | main([shadow_font_path, '-g', 'A', '-o', output_folder]) 148 | 149 | assert os.path.exists(os.path.join(output_folder, 'A.svg')) 150 | assert not os.path.exists(os.path.join(output_folder, 'Y.svg')) 151 | assert not os.path.exists(os.path.join(output_folder, 'Z.svg')) 152 | 153 | captured = capsys.readouterr() 154 | assert captured.err == ( 155 | "WARNING: The list of colors was extended with 1 #000000 value(s).\n") 156 | 157 | 158 | def test_main_x_opt_and_extra_colors(shadow_font_path, capsys, tmp_path): 159 | """ 160 | Exclude generating 'A.svg' and specify too many hex colors. 161 | Also specify alpha parameter of hex colors. 162 | """ 163 | output_folder = str(tmp_path) 164 | 165 | main([shadow_font_path, shadow_font_path, 166 | '-x', 'A', '-c', '99ccff,ff0066,cc0066', 167 | '-o', output_folder]) 168 | 169 | assert not os.path.exists(os.path.join(output_folder, 'A.svg')) 170 | assert os.path.exists(os.path.join(output_folder, 'Y.svg')) 171 | assert os.path.exists(os.path.join(output_folder, 'Z.svg')) 172 | 173 | captured = capsys.readouterr() 174 | assert captured.err == ("WARNING: The list of colors got the last 1 " 175 | "value(s) truncated: cc0066\n") 176 | 177 | 178 | def test_main_exclude_all_glyphs(shadow_font_path, capsys, tmp_path): 179 | output_folder = str(tmp_path) 180 | 181 | exit_val = main([shadow_font_path, '-x', 'Z,Y,A', '-c', 'ff00ff', 182 | '-o', output_folder]) 183 | 184 | assert not os.path.exists(os.path.join(output_folder, 'A.svg')) 185 | assert not os.path.exists(os.path.join(output_folder, 'Y.svg')) 186 | assert not os.path.exists(os.path.join(output_folder, 'Z.svg')) 187 | 188 | captured = capsys.readouterr() 189 | assert captured.out == ("No SVG files saved.\n") 190 | 191 | assert exit_val == 0 192 | 193 | 194 | def test_main_one_glyph_in_common(fixtures_dir, tmp_path): 195 | """ 196 | The fonts have one glyph in common; the default is intersection of names, 197 | so only one SVG should be saved. 198 | """ 199 | ab_font_path = os.path.join(fixtures_dir, 'ab.ttf') 200 | bc_font_path = os.path.join(fixtures_dir, 'bc.ttf') 201 | output_folder = str(tmp_path) 202 | 203 | main([ab_font_path, bc_font_path, '-c', 'ff00ff,00ff00', 204 | '-o', output_folder]) 205 | 206 | assert not os.path.exists(os.path.join(output_folder, 'a.svg')) 207 | assert os.path.exists(os.path.join(output_folder, 'b.svg')) 208 | assert not os.path.exists(os.path.join(output_folder, 'c.svg')) 209 | 210 | 211 | def test_main_u_opt_one_glyph_in_common(fixtures_dir, tmp_path): 212 | """ 213 | The fonts have one glyph in common; the option is to use the union of 214 | names, so all three SVGs should be saved. 215 | """ 216 | ab_font_path = os.path.join(fixtures_dir, 'ab.ttf') 217 | bc_font_path = os.path.join(fixtures_dir, 'bc.ttf') 218 | output_folder = str(tmp_path) 219 | 220 | main([ab_font_path, bc_font_path, '-c', 'ff00ff,00ff00', '-u', 221 | '-o', output_folder]) 222 | 223 | assert os.path.exists(os.path.join(output_folder, 'a.svg')) 224 | assert os.path.exists(os.path.join(output_folder, 'b.svg')) 225 | assert os.path.exists(os.path.join(output_folder, 'c.svg')) 226 | 227 | 228 | def test_main_a_opt_one_glyph_in_common(fixtures_dir, tmp_path): 229 | """ 230 | The fonts have one glyph in common; the default is intersection of names, 231 | but one more name is added to the list, so two SVGs should be saved. 232 | """ 233 | ab_font_path = os.path.join(fixtures_dir, 'ab.ttf') 234 | bc_font_path = os.path.join(fixtures_dir, 'bc.ttf') 235 | output_folder = str(tmp_path) 236 | 237 | main([ab_font_path, bc_font_path, '-c', 'ff00ff,00ff00', '-a', 'a', 238 | '-o', output_folder]) 239 | 240 | assert os.path.exists(os.path.join(output_folder, 'a.svg')) 241 | assert os.path.exists(os.path.join(output_folder, 'b.svg')) 242 | assert not os.path.exists(os.path.join(output_folder, 'c.svg')) 243 | 244 | 245 | def test_main_no_glyphs_in_common(fixtures_dir, capsys, tmp_path): 246 | """ 247 | The fonts have no glyphs in common; the default is intersection of names, 248 | so no SVGs should be saved. 249 | """ 250 | ab_font_path = os.path.join(fixtures_dir, 'ab.ttf') 251 | cd_font_path = os.path.join(fixtures_dir, 'cd.ttf') 252 | output_folder = str(tmp_path) 253 | 254 | exit_val = main([ab_font_path, cd_font_path, '-c', 'ff00ff,00ff00', 255 | '-o', output_folder]) 256 | 257 | assert not os.path.exists(os.path.join(output_folder, 'a.svg')) 258 | assert not os.path.exists(os.path.join(output_folder, 'b.svg')) 259 | assert not os.path.exists(os.path.join(output_folder, 'c.svg')) 260 | assert not os.path.exists(os.path.join(output_folder, 'd.svg')) 261 | 262 | captured = capsys.readouterr() 263 | assert captured.out == ( 264 | "The fonts and options provided can't produce any SVG files.\n") 265 | 266 | assert exit_val == 1 267 | -------------------------------------------------------------------------------- /tests/options_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Adobe. All rights reserved. 2 | 3 | import os 4 | import sys 5 | import unittest 6 | 7 | from opentypesvg import addsvg, dumpsvg, fonts2svg 8 | 9 | have_brotli = False 10 | try: 11 | import brotli # noqa 12 | 13 | have_brotli = True 14 | except ImportError: 15 | pass 16 | 17 | # addsvg must be last because of the way some tests prepare the input 18 | ALL_TOOLS = (dumpsvg, fonts2svg, addsvg) 19 | ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) 20 | # addsvg requires a path to a folder in addition to the path to the font 21 | XTRA_ARG = os.path.join(ROOT_DIR, 'fonts') 22 | 23 | 24 | class OptionsTest(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.font_path = os.path.join(ROOT_DIR, 'fonts', 'Zebrawood.otf') 28 | 29 | # ----- 30 | # Tests 31 | # ----- 32 | 33 | def test_get_options_no_args(self): 34 | for tool in ALL_TOOLS: 35 | with self.assertRaises(SystemExit) as cm: 36 | tool.get_options([]) 37 | self.assertEqual(cm.exception.code, 2) 38 | 39 | def test_get_options_help(self): 40 | for tool in ALL_TOOLS: 41 | with self.assertRaises(SystemExit) as cm: 42 | tool.get_options(['-h']) 43 | self.assertEqual(cm.exception.code, 0) 44 | 45 | def test_get_options_version(self): 46 | for tool in ALL_TOOLS: 47 | with self.assertRaises(SystemExit) as cm: 48 | tool.get_options(['--version']) 49 | self.assertEqual(cm.exception.code, 0) 50 | 51 | def test_get_options_bogus_option(self): 52 | args = ['--bogus', 'xfont'] 53 | for tool in ALL_TOOLS: 54 | if tool is addsvg: 55 | args.insert(len(args) - 1, XTRA_ARG) # insert the folder path 56 | with self.assertRaises(SystemExit) as cm: 57 | tool.get_options(args) 58 | self.assertEqual(cm.exception.code, 2) 59 | 60 | def test_get_options_invalid_font_path(self): 61 | args = ['xfont'] 62 | for tool in ALL_TOOLS: 63 | if tool is addsvg: 64 | args.insert(len(args) - 1, XTRA_ARG) # insert the folder path 65 | opts = tool.get_options(args) 66 | self.assertEqual(opts.font_paths_list, []) 67 | 68 | def test_get_options_not_a_font_path(self): 69 | args = [os.path.join('fonts', 'test.html')] 70 | for tool in ALL_TOOLS: 71 | if tool is addsvg: 72 | args.insert(len(args) - 1, XTRA_ARG) # insert the folder path 73 | opts = tool.get_options(args) 74 | self.assertEqual(opts.font_paths_list, []) 75 | 76 | def test_get_options_good_font_path(self): 77 | args = [self.font_path] 78 | for tool in ALL_TOOLS: 79 | if tool is addsvg: 80 | args.insert(len(args) - 1, XTRA_ARG) # insert the folder path 81 | opts = tool.get_options(args) 82 | self.assertEqual(os.path.basename(opts.font_paths_list[0]), 83 | os.path.basename(self.font_path)) 84 | 85 | def test_get_options_addsvg_invalid_folder_path(self): 86 | with self.assertRaises(SystemExit) as cm: 87 | addsvg.get_options(['-s', 'xfolder', 'xfont']) 88 | self.assertEqual(cm.exception.code, 1) 89 | 90 | @unittest.skipIf(have_brotli, "brotli module is installed") 91 | def test_get_options_addsvg_brotli_missing(self): 92 | with self.assertRaises(SystemExit) as cm: 93 | addsvg.get_options([XTRA_ARG, '-w', self.font_path]) 94 | self.assertEqual(cm.exception.code, 1) 95 | 96 | @unittest.skipIf(not have_brotli, "brotli module is not installed") 97 | def test_get_options_addsvg_store_true_opts(self): 98 | args = [XTRA_ARG, '-w', '-z', self.font_path] 99 | opts = addsvg.get_options(args) 100 | attr = ('make_font_copy', 'strip_viewbox', 101 | 'generate_woffs', 'compress_svgs') 102 | result = list(set([getattr(opts, name) for name in attr]))[0] 103 | self.assertTrue(result) 104 | 105 | def test_get_options_addsvg_store_false_opts(self): 106 | args = [XTRA_ARG, '-m', '-k', self.font_path] 107 | opts = addsvg.get_options(args) 108 | attr = ('make_font_copy', 'strip_viewbox', 109 | 'generate_woffs', 'compress_svgs') 110 | result = list(set([getattr(opts, name) for name in attr]))[0] 111 | self.assertFalse(result) 112 | 113 | def test_get_options_dumpsvg_store_true_opts(self): 114 | args = ['-r', self.font_path] 115 | opts = dumpsvg.get_options(args) 116 | result = getattr(opts, 'reset_viewbox') 117 | self.assertTrue(result) 118 | 119 | def test_get_options_fonts2svg_store_true_opts(self): 120 | args = ['-u', self.font_path] 121 | opts = fonts2svg.get_options(args) 122 | result = getattr(opts, 'glyphsets_union') 123 | self.assertTrue(result) 124 | 125 | def test_get_options_addsvg_opts_defaults(self): 126 | dflt = {'make_font_copy': True, 127 | 'strip_viewbox': True, 128 | 'generate_woffs': False, 129 | 'gnames_to_exclude': [], 130 | 'compress_svgs': False} 131 | args = [XTRA_ARG, self.font_path] 132 | opts = addsvg.get_options(args) 133 | for key, val in dflt.items(): 134 | self.assertEqual(getattr(opts, key), val) 135 | 136 | def test_get_options_dumpsvg_opts_defaults(self): 137 | dflt = {'output_folder_path': None, 138 | 'reset_viewbox': False, 139 | 'gnames_to_generate': [], 140 | 'gnames_to_exclude': []} 141 | args = [self.font_path] 142 | opts = dumpsvg.get_options(args) 143 | for key, val in dflt.items(): 144 | self.assertEqual(getattr(opts, key), val) 145 | 146 | def test_get_options_fonts2svg_opts_defaults(self): 147 | dflt = {'colors_list': [], 148 | 'output_folder_path': None, 149 | 'gnames_to_generate': [], 150 | 'gnames_to_add': [], 151 | 'gnames_to_exclude': [], 152 | 'glyphsets_union': False} 153 | args = [self.font_path] 154 | opts = fonts2svg.get_options(args) 155 | for key, val in dflt.items(): 156 | self.assertEqual(getattr(opts, key), val) 157 | 158 | def test_get_options_fonts2svg_invalid_hex_color(self): 159 | invalid_hex_colors = ['xxx', 'xxxxxx', 'aaa', 'aaaxxx'] 160 | for hex_col in invalid_hex_colors: 161 | with self.assertRaises(SystemExit) as cm: 162 | fonts2svg.get_options(['-c', hex_col, self.font_path]) 163 | self.assertEqual(cm.exception.code, 2) 164 | 165 | def test_get_options_fonts2svg_valid_hex_color(self): 166 | valid_hex_colors = ['aaabbb', 'cccdddee', '000000', 'ffffff00'] 167 | for hex_col in valid_hex_colors: 168 | opts = fonts2svg.get_options(['-c', hex_col, self.font_path]) 169 | self.assertEqual(opts.colors_list, [hex_col]) 170 | 171 | def test_get_options_split_comma_sequence(self): 172 | invalid_comma_seqs = ['a,b', 'a, b', 'a ,b', 'a , b'] 173 | for seq in invalid_comma_seqs: 174 | opts = fonts2svg.get_options(['-g', seq, self.font_path]) 175 | self.assertEqual(opts.gnames_to_generate, ['a', 'b']) 176 | 177 | def test_get_options_addsvg_multiple_fonts(self): 178 | args = [XTRA_ARG, self.font_path, self.font_path] 179 | with self.assertRaises(SystemExit) as cm: 180 | addsvg.get_options(args) 181 | self.assertEqual(cm.exception.code, 2) 182 | 183 | def test_get_options_dumpsvg_multiple_fonts(self): 184 | args = [self.font_path, self.font_path] 185 | with self.assertRaises(SystemExit) as cm: 186 | dumpsvg.get_options(args) 187 | self.assertEqual(cm.exception.code, 2) 188 | 189 | def test_get_options_fonts2svg_multiple_fonts(self): 190 | args = [self.font_path, self.font_path, self.font_path] 191 | opts = fonts2svg.get_options(args) 192 | self.assertEqual(len(opts.font_paths_list), 3) 193 | 194 | def test_get_options_adjust_viewbox(self): 195 | opts = fonts2svg.get_options(['-av', 'xfont']) 196 | self.assertTrue(opts.adjust_view_box_to_glyph) 197 | 198 | def test_get_options_adjust_viewbox_2(self): 199 | opts = fonts2svg.get_options(['--adjust-viewbox', 'xfont']) 200 | self.assertTrue(opts.adjust_view_box_to_glyph) 201 | 202 | def test_get_options_adjust_viewbox_not_passed(self): 203 | opts = fonts2svg.get_options(['xfont']) 204 | self.assertFalse(opts.adjust_view_box_to_glyph) 205 | 206 | 207 | if __name__ == "__main__": 208 | sys.exit(unittest.main()) 209 | -------------------------------------------------------------------------------- /tests/shared_utils_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Adobe. All rights reserved. 2 | 3 | import os 4 | import shutil 5 | import sys 6 | import tempfile 7 | import unittest 8 | from io import StringIO 9 | 10 | from opentypesvg import utils as shared_utils 11 | 12 | 13 | class SharedUtilsTest(unittest.TestCase): 14 | 15 | @staticmethod 16 | def write_binary_file(file_path, data): 17 | with open(file_path, "wb") as f: 18 | f.write(data) 19 | 20 | @staticmethod 21 | def reset_stream(stream): 22 | stream.seek(0) 23 | stream.truncate(0) 24 | return stream 25 | 26 | # ----- 27 | # Tests 28 | # ----- 29 | 30 | def test_read_write_file(self): 31 | content = '1st line\n2nd line\n3rd line' 32 | with tempfile.NamedTemporaryFile(delete=False) as tmp: 33 | shared_utils.write_file(tmp.name, content) 34 | result = shared_utils.read_file(tmp.name) 35 | self.assertEqual(result.splitlines()[1], '2nd line') 36 | 37 | def test_get_font_format(self): 38 | in_out = {"OTTO": "OTF", 39 | "\x00\x01\x00\x00": "TTF", 40 | "true": "TTF", 41 | "wOFF": "WOFF", 42 | "wOF2": "WOFF2", 43 | "blah": None} 44 | with tempfile.NamedTemporaryFile(delete=False) as tmp: 45 | for key, val in in_out.items(): 46 | data = bytearray(key, encoding="utf-8") 47 | self.write_binary_file(tmp.name, data) 48 | result = shared_utils.get_font_format(tmp.name) 49 | self.assertEqual(result, val) 50 | 51 | def test_final_message(self): 52 | stream = sys.stdout = StringIO() 53 | shared_utils.final_message(0) 54 | self.assertEqual(stream.getvalue().strip(), 'No SVG files saved.') 55 | 56 | self.reset_stream(stream) 57 | shared_utils.final_message(1) 58 | self.assertEqual(stream.getvalue().strip(), '1 SVG file saved.') 59 | 60 | self.reset_stream(stream) 61 | shared_utils.final_message(2) 62 | self.assertEqual(stream.getvalue().strip(), '2 SVG files saved.') 63 | 64 | def test_create_folder(self): 65 | folder_path = 'new_folder' 66 | shared_utils.create_folder(folder_path) 67 | self.assertTrue(os.path.isdir(folder_path)) 68 | 69 | # change just-created folder to be unaccessible 70 | os.chmod(folder_path, 0o0) 71 | 72 | # try creating a folder inside the unaccessible one 73 | # NOTE: apparently there's no way to make a Windows folder 74 | # read-only programmatically, so first check if the parent 75 | # folder cannot be accessed. See Windows note in os.chmod() 76 | # https://docs.python.org/3/library/os.html#os.chmod 77 | blocked_folder_path = os.path.join(folder_path, 'blocked_folder') 78 | if not os.access(folder_path, os.W_OK): 79 | with self.assertRaises(OSError) as cm: 80 | shared_utils.create_folder(blocked_folder_path) 81 | self.assertEqual(cm.exception.errno, 13) 82 | 83 | # try creating a folder with the same name as an existing file 84 | file_path = 'new_file' 85 | with open(file_path, "w+"): 86 | with self.assertRaises(OSError) as cm: 87 | shared_utils.create_folder(file_path) 88 | self.assertEqual(cm.exception.errno, 17) 89 | 90 | # remove artifacts 91 | os.chmod(folder_path, 0o755) 92 | if os.path.exists(folder_path): 93 | os.rmdir(folder_path) 94 | if os.path.exists(file_path): 95 | os.remove(file_path) 96 | 97 | def test_create_nested_folder(self): 98 | nested_folder_path = 'folder_path' 99 | result = shared_utils.create_nested_folder(nested_folder_path, None) 100 | self.assertEqual(result, nested_folder_path) 101 | 102 | main_folder_path = 'main_folder' 103 | nested_folder_path = os.path.join(main_folder_path, 104 | shared_utils.NESTED_FOLDER_NAME) 105 | shared_utils.create_nested_folder(None, main_folder_path) 106 | self.assertTrue(os.path.isdir(nested_folder_path)) 107 | # remove artifact 108 | if os.path.exists(nested_folder_path): 109 | shutil.rmtree(main_folder_path) 110 | 111 | def test_get_output_folder_path(self): 112 | folder_path = 'fonts' 113 | result = shared_utils.get_output_folder_path(folder_path, None) 114 | self.assertEqual(os.path.basename(result), folder_path) 115 | 116 | font_path = 'font' 117 | result = shared_utils.get_output_folder_path(None, font_path) 118 | self.assertEqual(os.path.basename(result), 119 | shared_utils.SVG_FOLDER_NAME) 120 | 121 | def test_get_gnames_to_save_in_nested_folder(self): 122 | result = shared_utils.get_gnames_to_save_in_nested_folder( 123 | ['a', 'A', 'B', 'b', 'c']) 124 | self.assertEqual(result, ['A', 'b']) 125 | 126 | 127 | if __name__ == "__main__": 128 | sys.exit(unittest.main()) 129 | --------------------------------------------------------------------------------