├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── docs-build-test.yml │ ├── docs.yml │ ├── python-publish.yml │ └── testing.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── api │ ├── pngtosvg.md │ ├── sheettopng.md │ └── svgtottf.md ├── contributing.md ├── credits.md ├── index.md ├── installation.md └── usage.md ├── handwrite ├── __init__.py ├── cli.py ├── default.json ├── pngtosvg.py ├── sheettopng.py └── svgtottf.py ├── handwrite_sample.pdf ├── mkdocs.yml ├── setup.py └── tests ├── __init__.py ├── test_cli.py ├── test_data ├── config_data │ └── default.json ├── pngtosvg │ ├── 34 │ │ └── 34.png │ ├── 33.png │ └── 45.bmp └── sheettopng │ └── excellent.jpg ├── test_pngtosvg.py ├── test_sheettopng.py └── test_svgtottf.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: ['https://www.buymeacoffee.com/sakshamarora1', 'https://www.buymeacoffee.com/yashlamba', 'https://paypal.me/yashlamba'] 3 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | pull_request: 7 | branches: [main, dev] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | strategy: 18 | fail-fast: false 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | - name: Set up Python 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: '3.8' 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install .[dev] 32 | echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV 33 | 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v1 36 | with: 37 | languages: python 38 | setup-python-dependencies: false 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v1 42 | -------------------------------------------------------------------------------- /.github/workflows/docs-build-test.yml: -------------------------------------------------------------------------------- 1 | name: Docs test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout Repo 8 | uses: actions/checkout@v2 9 | - name: Setup Python 10 | uses: actions/setup-python@v2 11 | with: 12 | python-version: '3.8' 13 | - name: Install dependencies 14 | run: | 15 | python3 -m pip install --upgrade pip 16 | python3 -m pip install -e .[dev] 17 | - name: Try Docs build 18 | run: mkdocs build 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs Deploy 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout main 8 | uses: actions/checkout@v2 9 | with: 10 | ref: main 11 | - name: Checkout dev 12 | uses: actions/checkout@v2 13 | with: 14 | ref: dev 15 | path: devbranch 16 | - name: Setup Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.8' 20 | - name: Install dependencies 21 | run: | 22 | python3 -m pip install --upgrade pip 23 | python3 -m pip install -e .[dev] 24 | - name: Git setup and update 25 | run: | 26 | git config user.name "GitHub Action" && git config user.email "github-action@github.com" 27 | git fetch origin 28 | - name: Build Docs for main 29 | run: mkdocs build 30 | - name: Build Docs for dev 31 | run: | 32 | cd devbranch 33 | mkdocs build 34 | mv site dev 35 | cd .. 36 | mv devbranch/dev site/ 37 | - name: Add latest web build and deploy 38 | run: | 39 | mkdocs gh-deploy --dirty 40 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [released] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: __token__ 28 | TWINE_PASSWORD: ${{ secrets.PYPI_HANDWRITE }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | lint: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout full upstream repo 12 | uses: actions/checkout@v2 13 | - name: Set up Python 3.8 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | - name: Check formatting with Black 18 | uses: psf/black@stable 19 | 20 | test: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [windows-latest, ubuntu-latest] 25 | python-version: [3.7, 3.8] 26 | 27 | steps: 28 | - name: Checkout full upstream repo 29 | uses: actions/checkout@v2 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install fontforge (Linux) 35 | if: matrix.os == 'ubuntu-latest' 36 | run: | 37 | wget -O fontforge https://github.com/fontforge/fontforge/releases/download/20201107/FontForge-2020-11-07-21ad4a1-x86_64.AppImage 38 | chmod +x fontforge 39 | sudo mv fontforge /usr/bin/ 40 | - name: Install fontforge (Windows) 41 | if: matrix.os == 'windows-latest' 42 | run: | 43 | Invoke-WebRequest -Uri https://github.com/fontforge/fontforge/releases/download/20201107/FontForge-2020-11-07-Windows.exe -OutFile fontforge.exe 44 | .\fontforge.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES /NOCANCEL | Out-Null 45 | echo "C:\Program Files (x86)\FontForgeBuilds\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 46 | - name: Install Potrace 47 | if: matrix.os == 'ubuntu-latest' 48 | run: | 49 | sudo apt install -y potrace 50 | - name: Install Handwrite 51 | run: | 52 | pip install -e . 53 | - name: Test 54 | run: | 55 | python setup.py test 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | tests/test_data/config_data/config/excellent.json 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # ttf files 133 | *.ttf 134 | 135 | # IDE 136 | .vscode -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: mixed-line-ending 7 | - repo: https://github.com/psf/black 8 | rev: 20.8b1 9 | hooks: 10 | - id: black 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 codEd 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include handwrite/default.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 |

7 | 8 | [![Tests](https://github.com/cod-ed/handwrite/workflows/Tests/badge.svg)](https://github.com/cod-ed/handwrite/actions) 9 | [![PyPI version](https://img.shields.io/pypi/v/handwrite.svg)](https://pypi.org/project/handwrite) 10 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/codEd-org/handwrite) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 12 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 13 | [![CodeQL](https://github.com/cod-ed/handwrite/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cod-ed/handwrite/actions/workflows/codeql-analysis.yml) 14 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/cod-ed/handwrite.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/cod-ed/handwrite/context:python) 15 | 16 | # Handwrite - Type in your Handwriting! 17 | 18 | Ever had those long-winded assignments, that the teacher always wants handwritten? 19 | Is your written work messy, cos you think faster than you can write? 20 | Now, you can finish them with the ease of typing in your own font! 21 | 22 |

23 | 24 | 25 |

26 | 27 | Handwrite makes typing written assignments efficient, convenient and authentic. 28 | 29 | Handwrite generates a custom font based on your handwriting sample, which can easily be used in text editors and word processors like Microsoft Word & Libre Office Word! 30 | 31 | Handwrite is also helpful for those with dysgraphia. 32 | 33 | You can get started with Handwrite [here](https://cod-ed.github.io/handwrite/). 34 | 35 | ## Sample 36 | 37 | You just need to fill up a form: 38 | 39 |

40 | 41 | 42 |

43 | 44 | Here's the end result! 45 | 46 |

47 | 48 | 49 |

50 | 51 | ## Credits and Reference 52 | 53 | 1. [Potrace](http://potrace.sourceforge.net/) algorithm and package has been immensely helpful. 54 | 55 | 2. [Fontforge](https://fontforge.org/en-US/) for packaging and adjusting font parameters. 56 | 57 | 3. [Sacha Chua's](https://github.com/sachac) [project](https://github.com/sachac/sachac-hand/) proved to be a great reference for fontforge python. 58 | 59 | 4. All credit for svgtottf converter goes to this [project](https://github.com/pteromys/svgs2ttf) by [pteromys](https://github.com/pteromys). We made a quite a lot of modifications of our own, but the base script idea was derived from here. 60 | -------------------------------------------------------------------------------- /docs/api/pngtosvg.md: -------------------------------------------------------------------------------- 1 | ::: handwrite.pngtosvg.PNGtoSVG 2 | selection: 3 | docstring_style: numpy -------------------------------------------------------------------------------- /docs/api/sheettopng.md: -------------------------------------------------------------------------------- 1 | ::: handwrite.sheettopng.SHEETtoPNG 2 | selection: 3 | docstring_style: numpy -------------------------------------------------------------------------------- /docs/api/svgtottf.md: -------------------------------------------------------------------------------- 1 | ::: handwrite.svgtottf.SVGtoTTF 2 | selection: 3 | docstring_style: numpy -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Linux 4 | 5 | 1. Install Potrace using apt 6 | 7 | ```console 8 | sudo apt-get install potrace 9 | ``` 10 | 11 | 2. Install fontforge 12 | 13 | ```console 14 | sudo apt-get install fontforge 15 | ``` 16 | 17 | ???+ warning 18 | Since the PPA for fontforge is no longer maintained, apt might not work for some users. 19 | The preferred way to install is using the AppImage from: https://fontforge.org/en-US/downloads/ 20 | 21 | 3. Clone the repository or your fork 22 | 23 | ```console 24 | git clone https://github.com/cod-ed/handwrite 25 | ``` 26 | 27 | 4. (Optional) Make a virtual environment and activate it 28 | 29 | ```console 30 | python -m venv .venv 31 | source .venv/bin/activate 32 | ``` 33 | 34 | 5. In the project directory run: 35 | 36 | ```console 37 | pip install -e .[dev] 38 | ``` 39 | 40 | 6. Make sure the tests run: 41 | 42 | ```console 43 | python setup.py test 44 | ``` 45 | 46 | 7. Install pre-commit hooks before contributing: 47 | 48 | ```console 49 | pre-commit install 50 | ``` 51 | 52 | You are ready to go! 53 | 54 | ## Windows 55 | 56 | 1. Install [Potrace](http://potrace.sourceforge.net/#downloading) and make sure it's in your PATH. 57 | 58 | 2. Install [fontforge](https://fontforge.org/en-US/downloads/) and make sure scripting is enabled. 59 | 60 | 3. Clone the repository or your fork 61 | 62 | ```console 63 | git clone https://github.com/cod-ed/handwrite 64 | ``` 65 | 66 | 4. (Optional) Make a virtual environment and activate it 67 | 68 | ```console 69 | python -m venv .venv 70 | .venv\Scripts\activate 71 | ``` 72 | 73 | 5. In the project directory run: 74 | 75 | ```console 76 | pip install -e .[dev] 77 | ``` 78 | 79 | 6. Make sure the tests run: 80 | 81 | ```console 82 | python setup.py test 83 | ``` 84 | 85 | 7. Install pre-commit hooks before contributing: 86 | 87 | ```console 88 | pre-commit install 89 | ``` 90 | 91 | You are ready to go! 92 | -------------------------------------------------------------------------------- /docs/credits.md: -------------------------------------------------------------------------------- 1 | ## Credits and References 2 | 3 | 1. [Potrace](http://potrace.sourceforge.net/) algorithm and package has been immensely helpful. 4 | 5 | 2. [Fontforge](https://fontforge.org/en-US/) for packaging and adjusting font parameters. 6 | 7 | 3. [Sacha Chua's](https://github.com/sachac) [project](https://github.com/sachac/sachac-hand/) proved to be a great reference for fontforge python. 8 | 9 | 4. All credit for svgtottf converter goes to this [project](https://github.com/pteromys/svgs2ttf) by [pteromys](https://github.com/pteromys). We made a quite a lot of modifications of our own, but the base script idea was derived from here. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 |

7 | 8 | [![Tests](https://github.com/cod-ed/handwrite/workflows/Tests/badge.svg)](https://github.com/cod-ed/handwrite/actions) 9 | [![PyPI version](https://img.shields.io/pypi/v/handwrite.svg)](https://pypi.org/project/handwrite) 10 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/codEd-org/handwrite) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 12 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 13 | [![CodeQL](https://github.com/cod-ed/handwrite/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cod-ed/handwrite/actions/workflows/codeql-analysis.yml) 14 | 15 | # Handwrite - Type in your Handwriting! 16 | 17 | Ever had those long-winded assignments, that the teacher always wants handwritten? 18 | Is your written work messy, cos you think faster than you can write? 19 | Now, you can finish them with the ease of typing in your own font! 20 | 21 |

22 | 23 | 24 |

25 | 26 | Handwrite makes typing written assignments efficient, convenient and authentic. 27 | 28 | Handwrite generates a custom font based on your handwriting sample, which can easily be used in text editors and word processors like Microsoft Word & Libre Office Word! 29 | 30 | Handwrite is also helpful for those with dysgraphia. 31 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing Handwrite 2 | 3 | 1. Install [fontforge](https://fontforge.org/en-US/) 4 | 5 | 2. Install [Potrace](http://potrace.sourceforge.net/) 6 | 7 | 3. Install handwrite: 8 | 9 | ```console 10 | pip install handwrite 11 | ``` -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Handwrite! 2 | 3 | ## Creating your Handwritten Sample 4 | 5 | 1. Take a printout of the [sample form](https://github.com/cod-ed/handwrite/raw/main/handwrite_sample.pdf). 6 | 7 | 2. Fill the form using the image below as a reference. 8 | 9 | 3. Scan the filled form using a scanner, or Adobe Scan in your phone. 10 | 11 | 4. Save the `.jpg` image in your system. 12 | 13 | Your form should look like this: 14 | 15 |

16 | 17 | 18 |

19 | 20 | ## Creating your font 21 | 22 | 1. Make sure you have installed `handwrite`, `potrace` & `fontforge`. 23 | 24 | 2. In a terminal type `handwrite [PATH TO IMAGE] [OUTPUT DIRECTORY]`. 25 | (You can also type `handwrite -h`, to see all the arguments you can use). 26 | 27 | 3. (Optional) Config file containing custom options for your font can also be passed using 28 | the `--config [CONFIG FILE]` argument. 29 | 30 | ???+ note 31 | - If you expicitly pass the metadata (filename, family or style) as CLI arguments, they are given a preference over the default config file data. 32 | 33 | - If no config file is provided for an input then the [default config file](https://github.com/cod-ed/handwrite/blob/main/handwrite/default.json) is used. 34 | 35 | 36 | 4. Your font will be created as `OUTPUT DIRECTORY/OUTPUT FONT NAME.ttf`. Install the font in your system. 37 | 38 | 5. Select your font in your word processor and get to work! 39 | Here's the end result! 40 | 41 |

42 | 43 | 44 |

45 | 46 | ## Configuring 47 | 48 | TO DO -------------------------------------------------------------------------------- /handwrite/__init__.py: -------------------------------------------------------------------------------- 1 | from handwrite.sheettopng import SHEETtoPNG 2 | from handwrite.pngtosvg import PNGtoSVG 3 | from handwrite.svgtottf import SVGtoTTF 4 | from handwrite.cli import converters 5 | -------------------------------------------------------------------------------- /handwrite/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import argparse 4 | import tempfile 5 | 6 | from handwrite import SHEETtoPNG 7 | from handwrite import PNGtoSVG 8 | from handwrite import SVGtoTTF 9 | 10 | 11 | def run(sheet, output_directory, characters_dir, config, metadata): 12 | SHEETtoPNG().convert(sheet, characters_dir, config) 13 | PNGtoSVG().convert(directory=characters_dir) 14 | SVGtoTTF().convert(characters_dir, output_directory, config, metadata) 15 | 16 | 17 | def converters(sheet, output_directory, directory=None, config=None, metadata=None): 18 | if not directory: 19 | directory = tempfile.mkdtemp() 20 | isTempdir = True 21 | else: 22 | isTempdir = False 23 | 24 | if config is None: 25 | config = os.path.join( 26 | os.path.dirname(os.path.realpath(__file__)), "default.json" 27 | ) 28 | if os.path.isdir(config): 29 | raise IsADirectoryError("Config parameter should not be a directory.") 30 | 31 | if os.path.isdir(sheet): 32 | raise IsADirectoryError("Sheet parameter should not be a directory.") 33 | else: 34 | run(sheet, output_directory, directory, config, metadata) 35 | 36 | if isTempdir: 37 | shutil.rmtree(directory) 38 | 39 | 40 | def main(): 41 | parser = argparse.ArgumentParser() 42 | parser.add_argument("input_path", help="Path to sample sheet") 43 | parser.add_argument("output_directory", help="Directory Path to save font output") 44 | parser.add_argument( 45 | "--directory", 46 | help="Generate additional files to this path (Temp by default)", 47 | default=None, 48 | ) 49 | parser.add_argument("--config", help="Use custom configuration file", default=None) 50 | parser.add_argument("--filename", help="Font File name", default=None) 51 | parser.add_argument("--family", help="Font Family name", default=None) 52 | parser.add_argument("--style", help="Font Style name", default=None) 53 | 54 | args = parser.parse_args() 55 | metadata = {"filename": args.filename, "family": args.family, "style": args.style} 56 | converters( 57 | args.input_path, args.output_directory, args.directory, args.config, metadata 58 | ) 59 | -------------------------------------------------------------------------------- /handwrite/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "threshold_value": 200, 3 | "props": { 4 | "ascent": 800, 5 | "descent": 200, 6 | "em": 1000, 7 | "encoding": "UnicodeFull", 8 | "lang": "English (US)", 9 | "filename": "MyFont", 10 | "style": "Regular" 11 | }, 12 | "sfnt_names": { 13 | "Copyright": "Copyright (c) 2021 by Nobody", 14 | "Family": "MyFont", 15 | "SubFamily": "Regular", 16 | "UniqueID": "MyFont 2021-02-04", 17 | "Fullname": "MyFont Regular", 18 | "Version": "Version 1.0", 19 | "PostScriptName": "MyFont-Regular" 20 | }, 21 | "glyphs": [ 22 | 65, 23 | 66, 24 | 67, 25 | 68, 26 | 69, 27 | 70, 28 | 71, 29 | 72, 30 | 73, 31 | 74, 32 | 75, 33 | 76, 34 | 77, 35 | 78, 36 | 79, 37 | 80, 38 | 81, 39 | 82, 40 | 83, 41 | 84, 42 | 85, 43 | 86, 44 | 87, 45 | 88, 46 | 89, 47 | 90, 48 | 97, 49 | 98, 50 | 99, 51 | 100, 52 | 101, 53 | 102, 54 | 103, 55 | 104, 56 | 105, 57 | 106, 58 | 107, 59 | 108, 60 | 109, 61 | 110, 62 | 111, 63 | 112, 64 | 113, 65 | 114, 66 | 115, 67 | 116, 68 | 117, 69 | 118, 70 | 119, 71 | 120, 72 | 121, 73 | 122, 74 | 48, 75 | 49, 76 | 50, 77 | 51, 78 | 52, 79 | 53, 80 | 54, 81 | 55, 82 | 56, 83 | 57, 84 | 46, 85 | 44, 86 | 59, 87 | 58, 88 | 33, 89 | 63, 90 | 34, 91 | 39, 92 | 45, 93 | 43, 94 | 61, 95 | 47, 96 | 37, 97 | 38, 98 | 40, 99 | 41, 100 | 91, 101 | 93 102 | ], 103 | "typography_parameters": { 104 | "bearing_table": { 105 | "Default": [60, 60], 106 | "A": [60, -50], 107 | "a": [30, 40], 108 | "B": [60, 0], 109 | "C": [60, -30], 110 | "c": [null, 40], 111 | "b": [null, 40], 112 | "D": [null, 10], 113 | "d": [30, -20], 114 | "e": [30, 40], 115 | "E": [70, 10], 116 | "F": [70, 0], 117 | "f": [0, -20], 118 | "G": [60, 30], 119 | "g": [20, 60], 120 | "h": [40, 40], 121 | "I": [80, 50], 122 | "i": [null, 60], 123 | "J": [40, 30], 124 | "j": [-70, 40], 125 | "k": [40, 20], 126 | "K": [80, 0], 127 | "H": [null, 10], 128 | "L": [80, 10], 129 | "l": [null, 0], 130 | "M": [60, 30], 131 | "m": [40, null], 132 | "N": [70, 10], 133 | "n": [30, 40], 134 | "O": [70, 10], 135 | "o": [40, 40], 136 | "P": [70, 0], 137 | "p": [null, 40], 138 | "Q": [70, 10], 139 | "q": [20, 30], 140 | "R": [70, -10], 141 | "r": [null, 40], 142 | "S": [60, 60], 143 | "s": [20, 40], 144 | "T": [null, -10], 145 | "t": [-10, 20], 146 | "U": [70, 20], 147 | "u": [40, 40], 148 | "V": [null, -10], 149 | "v": [20, 20], 150 | "W": [70, 20], 151 | "w": [40, 40], 152 | "X": [null, -10], 153 | "x": [10, 20], 154 | "y": [20, 30], 155 | "Y": [40, 0], 156 | "Z": [null, -10], 157 | "z": [10, 20], 158 | "1": [-10, 30], 159 | "2": [-10, 30], 160 | "3": [10, 40], 161 | "4": [30, 30], 162 | "5": [30, 40], 163 | "6": [20, 20], 164 | "7": [30, 20], 165 | "8": [30, 20], 166 | "9": [30, 30], 167 | "0": [50, 40], 168 | ".": [null, 10], 169 | ",": [null, 10], 170 | ";": [null, 10], 171 | ":": [null, 20], 172 | "!": [null, 20], 173 | "?": [null, 30], 174 | "\"": [null, 20], 175 | "'": [null, 10], 176 | "-": [null, 20], 177 | "+": [null, 20], 178 | "=": [null, 20], 179 | "/": [null, 20], 180 | "%": [40, 40], 181 | "&": [40, 40], 182 | "(": [10, 10], 183 | ")": [10, 10], 184 | "[": [10, 10], 185 | "]": [10, 10] 186 | }, 187 | "kerning_table": { 188 | "autokern": true, 189 | "seperation": 0, 190 | "rows": [ 191 | null, 192 | "f-+=/?", 193 | "t", 194 | "i", 195 | "r", 196 | "k", 197 | "l.,;:!\"'()[]", 198 | "v", 199 | "bop%&", 200 | "nm", 201 | "a", 202 | "W", 203 | "T", 204 | "F", 205 | "P", 206 | "g", 207 | "qdhyj", 208 | "cesuwxz", 209 | "V", 210 | "A", 211 | "Y", 212 | "MNHI", 213 | "OQDU", 214 | "J", 215 | "C", 216 | "E", 217 | "L", 218 | "P", 219 | "KR", 220 | "G", 221 | "BSXZ" 222 | ], 223 | "cols": [ 224 | null, 225 | "oacedgqw%&", 226 | "ft-+=/?", 227 | "xvz", 228 | "hbli.,;:!\"'()[]", 229 | "j", 230 | "mnpru", 231 | "k", 232 | "y", 233 | "s", 234 | "T", 235 | "F", 236 | "Zero" 237 | ], 238 | "table": [ 239 | [ 240 | [0, 0, 0, 0, 0, 0, 0, null, null, 0, 0, null, 0], 241 | [0, -30, -61, -20, null, 0, null, null, null, 0, -150, null, -70], 242 | [0, -50, -41, -20, null, 0, 0, null, null, 0, -150, null, -10], 243 | [ 244 | null, 245 | null, 246 | -40, 247 | null, 248 | null, 249 | null, 250 | null, 251 | null, 252 | null, 253 | null, 254 | -150, 255 | null, 256 | null 257 | ], 258 | [0, -32, -40, null, null, 0, null, null, null, 0, -170, null, 29], 259 | [0, -10, -50, null, null, 0, null, null, null, -48, -150, null, -79], 260 | [0, -10, -20, null, 0, 0, 0, null, null, 0, -110, null, -20], 261 | [0, -40, -35, -15, null, 0, 0, null, null, 0, -170, null, 30], 262 | [0, null, -40, null, 0, 0, 0, null, null, 0, -170, null, 43], 263 | [ 264 | null, 265 | null, 266 | -30, 267 | null, 268 | null, 269 | null, 270 | null, 271 | null, 272 | null, 273 | null, 274 | -170, 275 | null, 276 | null 277 | ], 278 | [0, -23, -30, null, 0, 0, 0, null, null, 0, -170, null, 7], 279 | [0, -40, -30, -10, null, 0, 0, null, null, 0, null, null, null], 280 | [0, -150, -120, -120, -30, -40, -130, null, -100, -80, 0, null, null], 281 | [0, -90, -90, -70, -30, 0, -70, null, -50, -80, -40, null, null], 282 | [0, -100, -70, -50, null, 0, -70, null, -30, -80, -20, null, null], 283 | [ 284 | null, 285 | null, 286 | null, 287 | null, 288 | null, 289 | 40, 290 | null, 291 | null, 292 | null, 293 | null, 294 | -120, 295 | null, 296 | null 297 | ], 298 | [null, null, null, null, 30, 30, 30, 30, 30, null, -100, null, null], 299 | [ 300 | null, 301 | null, 302 | null, 303 | null, 304 | null, 305 | null, 306 | null, 307 | null, 308 | null, 309 | null, 310 | -120, 311 | null, 312 | null 313 | ], 314 | [null, -70, 30, 30, null, -80, -20, null, -40, -40, -10, null, null], 315 | [null, 30, 60, 30, 30, null, 20, 40, 20, -80, -120, 20, 20], 316 | [null, 20, 60, 30, 30, null, 20, 20, 40, 20, -10, null, null], 317 | [null, 20, 10, 40, 30, null, 10, 20, 20, null, null, null, null], 318 | [null, null, 50, 40, 30, -20, 30, 20, 30, null, -70, null, null], 319 | [null, null, 40, 20, 20, -20, 10, 10, 30, null, -30, null, null], 320 | [null, 10, 40, 10, 30, null, 30, 30, 20, null, -30, null, null], 321 | [null, -10, 50, null, 10, -20, 10, null, 20, null, null, null, null], 322 | [ 323 | null, 324 | -10, 325 | -10, 326 | null, 327 | null, 328 | -30, 329 | null, 330 | null, 331 | 20, 332 | null, 333 | -90, 334 | null, 335 | null 336 | ], 337 | [null, -50, 30, 20, 20, null, null, 20, 20, null, -30, null, null], 338 | [null, 20, 20, 20, 10, null, 20, 20, 20, null, -60, null, null], 339 | [null, 20, 40, 30, 30, null, 20, 20, 20, null, -100, 10, null], 340 | [null, 20, 40, 30, 30, null, 20, 20, 20, 20, -20, 10, null] 341 | ] 342 | ] 343 | } 344 | }, 345 | "# vim: set et sw=2 ts=2 sts=2:": false 346 | } 347 | -------------------------------------------------------------------------------- /handwrite/pngtosvg.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageChops 2 | import os 3 | import shutil 4 | import subprocess 5 | 6 | 7 | class PotraceNotFound(Exception): 8 | pass 9 | 10 | 11 | class PNGtoSVG: 12 | """Converter class to convert character PNGs to BMPs and SVGs.""" 13 | 14 | def convert(self, directory): 15 | """Call converters on each .png in the provider directory. 16 | 17 | Walk through the custom directory containing all .png files 18 | from sheettopng and convert them to png -> bmp -> svg. 19 | """ 20 | path = os.walk(directory) 21 | for root, dirs, files in path: 22 | for f in files: 23 | if f.endswith(".png"): 24 | self.pngToBmp(root + "/" + f) 25 | # self.trim(root + "/" + f[0:-4] + ".bmp") 26 | self.bmpToSvg(root + "/" + f[0:-4] + ".bmp") 27 | 28 | def bmpToSvg(self, path): 29 | """Convert .bmp image to .svg using potrace. 30 | 31 | Converts the passed .bmp file to .svg using the potrace 32 | (http://potrace.sourceforge.net/). Each .bmp is passed as 33 | a parameter to potrace which is called as a subprocess. 34 | 35 | Parameters 36 | ---------- 37 | path : str 38 | Path to the bmp file to be converted. 39 | 40 | Raises 41 | ------ 42 | PotraceNotFound 43 | Raised if potrace not found in path by shutil.which() 44 | """ 45 | if shutil.which("potrace") is None: 46 | raise PotraceNotFound("Potrace is either not installed or not in path") 47 | else: 48 | subprocess.run(["potrace", path, "-b", "svg", "-o", path[0:-4] + ".svg"]) 49 | 50 | def pngToBmp(self, path): 51 | """Convert .bmp image to .svg using potrace. 52 | 53 | Converts the passed .bmp file to .svg using the potrace 54 | (http://potrace.sourceforge.net/). Each .bmp is passed as 55 | a parameter to potrace which is called as a subprocess. 56 | 57 | Parameters 58 | ---------- 59 | path : str 60 | Path to the bmp file to be converted. 61 | 62 | Raises 63 | ------ 64 | PotraceNotFound 65 | Raised if potrace not found in path by shutil.which() 66 | """ 67 | img = Image.open(path).convert("RGBA").resize((100, 100)) 68 | 69 | # Threshold image to convert each pixel to either black or white 70 | threshold = 200 71 | data = [] 72 | for pix in list(img.getdata()): 73 | if pix[0] >= threshold and pix[1] >= threshold and pix[3] >= threshold: 74 | data.append((255, 255, 255, 0)) 75 | else: 76 | data.append((0, 0, 0, 1)) 77 | img.putdata(data) 78 | img.save(path[0:-4] + ".bmp") 79 | 80 | def trim(self, im_path): 81 | im = Image.open(im_path) 82 | bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) 83 | diff = ImageChops.difference(im, bg) 84 | bbox = list(diff.getbbox()) 85 | bbox[0] -= 1 86 | bbox[1] -= 1 87 | bbox[2] += 1 88 | bbox[3] += 1 89 | cropped_im = im.crop(bbox) 90 | cropped_im.save(im_path) 91 | -------------------------------------------------------------------------------- /handwrite/sheettopng.py: -------------------------------------------------------------------------------- 1 | import os 2 | import itertools 3 | import json 4 | 5 | import cv2 6 | 7 | # Seq: A-Z, a-z, 0-9, SPECIAL_CHARS 8 | ALL_CHARS = list( 9 | itertools.chain( 10 | range(65, 91), 11 | range(97, 123), 12 | range(48, 58), 13 | [ord(i) for i in ".,;:!?\"'-+=/%&()[]"], 14 | ) 15 | ) 16 | 17 | 18 | class SHEETtoPNG: 19 | """Converter class to convert input sample sheet to character PNGs.""" 20 | 21 | def convert(self, sheet, characters_dir, config, cols=8, rows=10): 22 | """Convert a sheet of sample writing input to a custom directory structure of PNGs. 23 | 24 | Detect all characters in the sheet as a separate contours and convert each to 25 | a PNG image in a temp/user provided directory. 26 | 27 | Parameters 28 | ---------- 29 | sheet : str 30 | Path to the sheet file to be converted. 31 | characters_dir : str 32 | Path to directory to save characters in. 33 | config: str 34 | Path to config file. 35 | cols : int, default=8 36 | Number of columns of expected contours. Defaults to 8 based on the default sample. 37 | rows : int, default=10 38 | Number of rows of expected contours. Defaults to 10 based on the default sample. 39 | """ 40 | with open(config) as f: 41 | threshold_value = json.load(f).get("threshold_value", 200) 42 | if os.path.isdir(sheet): 43 | raise IsADirectoryError("Sheet parameter should not be a directory.") 44 | characters = self.detect_characters( 45 | sheet, threshold_value, cols=cols, rows=rows 46 | ) 47 | self.save_images( 48 | characters, 49 | characters_dir, 50 | ) 51 | 52 | def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10): 53 | """Detect contours on the input image and filter them to get only characters. 54 | 55 | Uses opencv to threshold the image for better contour detection. After finding all 56 | contours, they are filtered based on area, cropped and then sorted sequentially based 57 | on coordinates. Finally returs the cols*rows top candidates for being the character 58 | containing contours. 59 | 60 | Parameters 61 | ---------- 62 | sheet_image : str 63 | Path to the sheet file to be converted. 64 | threshold_value : int 65 | Value to adjust thresholding of the image for better contour detection. 66 | cols : int, default=8 67 | Number of columns of expected contours. Defaults to 8 based on the default sample. 68 | rows : int, default=10 69 | Number of rows of expected contours. Defaults to 10 based on the default sample. 70 | 71 | Returns 72 | ------- 73 | sorted_characters : list of list 74 | Final rows*cols contours in form of list of list arranged as: 75 | sorted_characters[x][y] denotes contour at x, y position in the input grid. 76 | """ 77 | # TODO Raise errors and suggest where the problem might be 78 | 79 | # Read the image and convert to grayscale 80 | image = cv2.imread(sheet_image) 81 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 82 | 83 | # Threshold and filter the image for better contour detection 84 | _, thresh = cv2.threshold(gray, threshold_value, 255, 1) 85 | close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 86 | close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2) 87 | 88 | # Search for contours. 89 | contours, h = cv2.findContours( 90 | close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE 91 | ) 92 | 93 | # Filter contours based on number of sides and then reverse sort by area. 94 | contours = sorted( 95 | filter( 96 | lambda cnt: len( 97 | cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True) 98 | ) 99 | == 4, 100 | contours, 101 | ), 102 | key=cv2.contourArea, 103 | reverse=True, 104 | ) 105 | 106 | # Calculate the bounding of the first contour and approximate the height 107 | # and width for final cropping. 108 | x, y, w, h = cv2.boundingRect(contours[0]) 109 | space_h, space_w = 7 * h // 16, 7 * w // 16 110 | 111 | # Since amongst all the contours, the expected case is that the 4 sided contours 112 | # containing the characters should have the maximum area, so we loop through the first 113 | # rows*colums contours and add them to final list after cropping. 114 | characters = [] 115 | for i in range(rows * cols): 116 | x, y, w, h = cv2.boundingRect(contours[i]) 117 | cx, cy = x + w // 2, y + h // 2 118 | 119 | roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w] 120 | characters.append([roi, cx, cy]) 121 | 122 | # Now we have the characters but since they are all mixed up we need to position them. 123 | # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then 124 | # sort each group based on the 'x' coordinate. 125 | characters.sort(key=lambda x: x[2]) 126 | sorted_characters = [] 127 | for k in range(rows): 128 | sorted_characters.extend( 129 | sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1]) 130 | ) 131 | 132 | return sorted_characters 133 | 134 | def save_images(self, characters, characters_dir): 135 | """Create directory for each character and save as PNG. 136 | 137 | Creates directory and PNG file for each image as following: 138 | 139 | characters_dir/ord(character)/ord(character).png (SINGLE SHEET INPUT) 140 | characters_dir/sheet_filename/ord(character)/ord(character).png (MULTIPLE SHEETS INPUT) 141 | 142 | Parameters 143 | ---------- 144 | characters : list of list 145 | Sorted list of character images each inner list representing a row of images. 146 | characters_dir : str 147 | Path to directory to save characters in. 148 | """ 149 | os.makedirs(characters_dir, exist_ok=True) 150 | 151 | # Create directory for each character and save the png for the characters 152 | # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).png 153 | # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).png 154 | for k, images in enumerate(characters): 155 | character = os.path.join(characters_dir, str(ALL_CHARS[k])) 156 | if not os.path.exists(character): 157 | os.mkdir(character) 158 | cv2.imwrite( 159 | os.path.join(character, str(ALL_CHARS[k]) + ".png"), 160 | images[0], 161 | ) 162 | -------------------------------------------------------------------------------- /handwrite/svgtottf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import uuid 5 | 6 | 7 | class SVGtoTTF: 8 | def convert(self, directory, outdir, config, metadata=None): 9 | """Convert a directory with SVG images to TrueType Font. 10 | 11 | Calls a subprocess to the run this script with Fontforge Python 12 | environment. 13 | 14 | Parameters 15 | ---------- 16 | directory : str 17 | Path to directory with SVGs to be converted. 18 | outdir : str 19 | Path to output directory. 20 | config : str 21 | Path to config file. 22 | metadata : dict 23 | Dictionary containing the metadata (filename, family or style) 24 | """ 25 | import subprocess 26 | import platform 27 | 28 | subprocess.run( 29 | ( 30 | ["ffpython"] 31 | if platform.system() == "Windows" 32 | else ["fontforge", "-script"] 33 | ) 34 | + [ 35 | os.path.abspath(__file__), 36 | config, 37 | directory, 38 | outdir, 39 | json.dumps(metadata), 40 | ] 41 | ) 42 | 43 | def set_properties(self): 44 | """Set metadata of the font from config.""" 45 | props = self.config["props"] 46 | lang = props.get("lang", "English (US)") 47 | fontname = self.metadata.get("filename", None) or props.get( 48 | "filename", "Example" 49 | ) 50 | family = self.metadata.get("family", None) or fontname 51 | style = self.metadata.get("style", None) or props.get("style", "Regular") 52 | 53 | self.font.familyname = fontname 54 | self.font.fontname = fontname + "-" + style 55 | self.font.fullname = fontname + " " + style 56 | self.font.encoding = props.get("encoding", "UnicodeFull") 57 | 58 | for k, v in props.items(): 59 | if hasattr(self.font, k): 60 | if isinstance(v, list): 61 | v = tuple(v) 62 | setattr(self.font, k, v) 63 | 64 | if self.config.get("sfnt_names", None): 65 | self.config["sfnt_names"]["Family"] = family 66 | self.config["sfnt_names"]["Fullname"] = family + " " + style 67 | self.config["sfnt_names"]["PostScriptName"] = family + "-" + style 68 | self.config["sfnt_names"]["SubFamily"] = style 69 | 70 | self.config["sfnt_names"]["UniqueID"] = family + " " + str(uuid.uuid4()) 71 | 72 | for k, v in self.config.get("sfnt_names", {}).items(): 73 | self.font.appendSFNTName(str(lang), str(k), str(v)) 74 | 75 | def add_glyphs(self, directory): 76 | """Read and add SVG images as glyphs to the font. 77 | 78 | Walks through the provided directory and uses each ord(character).svg file 79 | as glyph for the character. Then using the provided config, set the font 80 | parameters and export TTF file to outdir. 81 | 82 | Parameters 83 | ---------- 84 | directory : str 85 | Path to directory with SVGs to be converted. 86 | """ 87 | space = self.font.createMappedChar(ord(" ")) 88 | space.width = 500 89 | 90 | for k in self.config["glyphs"]: 91 | # Create character glyph 92 | g = self.font.createMappedChar(k) 93 | self.unicode_mapping.setdefault(k, g.glyphname) 94 | # Get outlines 95 | src = "{}/{}.svg".format(k, k) 96 | src = directory + os.sep + src 97 | g.importOutlines(src, ("removeoverlap", "correctdir")) 98 | g.removeOverlap() 99 | 100 | def set_bearings(self, bearings): 101 | """Add left and right bearing from config 102 | 103 | Parameters 104 | ---------- 105 | bearings : dict 106 | Map from character: [left bearing, right bearing] 107 | """ 108 | default = bearings.get("Default", [60, 60]) 109 | 110 | for k, v in bearings.items(): 111 | if v[0] is None: 112 | v[0] = default[0] 113 | if v[1] is None: 114 | v[1] = default[1] 115 | 116 | if k != "Default": 117 | glyph_name = self.unicode_mapping[ord(str(k))] 118 | self.font[glyph_name].left_side_bearing = v[0] 119 | self.font[glyph_name].right_side_bearing = v[1] 120 | 121 | def set_kerning(self, table): 122 | """Set kerning values in the font. 123 | 124 | Parameters 125 | ---------- 126 | table : dict 127 | Config dictionary with kerning values/autokern bool. 128 | """ 129 | rows = table["rows"] 130 | rows = [list(i) if i != None else None for i in rows] 131 | cols = table["cols"] 132 | cols = [list(i) if i != None else None for i in cols] 133 | 134 | self.font.addLookup("kern", "gpos_pair", 0, [["kern", [["latn", ["dflt"]]]]]) 135 | 136 | if table.get("autokern", True): 137 | self.font.addKerningClass( 138 | "kern", "kern-1", table.get("seperation", 0), rows, cols, True 139 | ) 140 | else: 141 | kerning_table = table.get("table", False) 142 | if not kerning_table: 143 | raise ValueError("Kerning offsets not found in the config file.") 144 | flatten_list = ( 145 | lambda y: [x for a in y for x in flatten_list(a)] 146 | if type(y) is list 147 | else [y] 148 | ) 149 | offsets = [0 if x is None else x for x in flatten_list(kerning_table)] 150 | self.font.addKerningClass("kern", "kern-1", rows, cols, offsets) 151 | 152 | def generate_font_file(self, filename, outdir, config_file): 153 | """Output TTF file. 154 | 155 | Additionally checks for multiple outputs and duplicates. 156 | 157 | Parameters 158 | ---------- 159 | filename : str 160 | Output filename. 161 | outdir : str 162 | Path to output directory. 163 | config_file : str 164 | Path to config file. 165 | """ 166 | if filename is None: 167 | raise NameError("filename not found in config file.") 168 | 169 | outfile = str( 170 | outdir 171 | + os.sep 172 | + (filename + ".ttf" if not filename.endswith(".ttf") else filename) 173 | ) 174 | 175 | while os.path.exists(outfile): 176 | outfile = os.path.splitext(outfile)[0] + " (1).ttf" 177 | 178 | sys.stderr.write("\nGenerating %s...\n" % outfile) 179 | self.font.generate(outfile) 180 | 181 | def convert_main(self, config_file, directory, outdir, metadata): 182 | try: 183 | self.font = fontforge.font() 184 | except: 185 | import fontforge 186 | 187 | with open(config_file) as f: 188 | self.config = json.load(f) 189 | self.metadata = json.loads(metadata) or {} 190 | 191 | self.font = fontforge.font() 192 | self.unicode_mapping = {} 193 | self.set_properties() 194 | self.add_glyphs(directory) 195 | 196 | # bearing table 197 | self.set_bearings(self.config["typography_parameters"].get("bearing_table", {})) 198 | 199 | # kerning table 200 | self.set_kerning(self.config["typography_parameters"].get("kerning_table", {})) 201 | 202 | # Generate font and save as a .ttf file 203 | filename = self.metadata.get("filename", None) or self.config["props"].get( 204 | "filename", None 205 | ) 206 | self.generate_font_file(str(filename), outdir, config_file) 207 | 208 | 209 | if __name__ == "__main__": 210 | if len(sys.argv) != 5: 211 | raise ValueError("Incorrect call to SVGtoTTF") 212 | SVGtoTTF().convert_main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) 213 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'Handwrite' 2 | site_description: 'Official Site for Handwrite' 3 | site_author: 'Team Handwrite' 4 | site_url: 'https://cod-ed.github.io/handwrite' 5 | 6 | # Repository 7 | repo_name: 'cod-ed/handwrite' 8 | repo_url: 'https://github.com/cod-ed/handwrite' 9 | 10 | nav: 11 | - Home: 'index.md' 12 | - Installation: 'installation.md' 13 | - Usage: 'usage.md' 14 | - Contributing: 'contributing.md' 15 | - Credits: 'credits.md' 16 | - Documentation: 17 | - Converters: 18 | - SHEETtoPNG: 'api/sheettopng.md' 19 | - PNGtoSVG: 'api/pngtosvg.md' 20 | - SVGtoTTF: 'api/svgtottf.md' 21 | 22 | theme: 23 | name: material 24 | features: 25 | - navigation.sections 26 | # - navigation.tabs 27 | palette: 28 | primary: 'black' 29 | accent: 'white' 30 | font: 31 | text: 'Ubuntu' 32 | code: 'Ubuntu Mono' 33 | icon: 34 | logo: fontawesome/solid/pen-square 35 | 36 | plugins: 37 | - mkdocstrings 38 | 39 | markdown_extensions: 40 | - admonition 41 | - codehilite: 42 | guess_lang: false 43 | - toc: 44 | permalink: true 45 | - pymdownx.arithmatex 46 | - pymdownx.betterem: 47 | smart_enable: all 48 | - pymdownx.caret 49 | - pymdownx.critic 50 | - pymdownx.details 51 | - pymdownx.inlinehilite 52 | - pymdownx.magiclink 53 | - pymdownx.mark 54 | - pymdownx.smartsymbols 55 | - pymdownx.superfences 56 | - footnotes 57 | - pymdownx.tasklist: 58 | custom_checkbox: true 59 | - pymdownx.tabbed 60 | - pymdownx.tilde 61 | - attr_list 62 | 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="handwrite", 8 | version="0.3.0", 9 | author="Yash Lamba, Saksham Arora, Aryan Gupta", 10 | author_email="yashlamba2000@gmail.com, sakshamarora1001@gmail.com, aryangupta973@gmail.com", 11 | description="Convert text to custom handwriting", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/cod-ed/handwrite", 15 | packages=setuptools.find_packages(), 16 | install_requires=["opencv-python", "Pillow"], 17 | extras_require={ 18 | "dev": [ 19 | "pre-commit", 20 | "black", 21 | "mkdocs==1.2.2", 22 | "mkdocs-material==6.1.0", 23 | "pymdown-extensions==8.2", 24 | "mkdocstrings>=0.16.1", 25 | "pytkdocs[numpy-style]", 26 | ] 27 | }, 28 | entry_points={ 29 | "console_scripts": ["handwrite = handwrite.cli:main"], 30 | }, 31 | include_package_data=True, 32 | classifiers=[ 33 | "Programming Language :: Python :: 3", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: OS Independent", 36 | ], 37 | python_requires=">=3.7", 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | import subprocess 6 | import filecmp 7 | 8 | from handwrite.sheettopng import ALL_CHARS 9 | 10 | 11 | class TestCLI(unittest.TestCase): 12 | def setUp(self): 13 | self.file_dir = os.path.dirname(os.path.abspath(__file__)) 14 | self.temp_dir = tempfile.mkdtemp() 15 | self.sheets_dir = os.path.join(self.file_dir, "test_data", "sheettopng") 16 | 17 | def tearDown(self): 18 | shutil.rmtree(self.temp_dir) 19 | 20 | def test_single_input(self): 21 | # Check working with excellent input and no optional parameters 22 | subprocess.call( 23 | [ 24 | "handwrite", 25 | os.path.join(self.file_dir, "test_data", "sheettopng", "excellent.jpg"), 26 | self.temp_dir, 27 | ] 28 | ) 29 | self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "MyFont.ttf"))) 30 | 31 | def test_single_input_with_optional_parameters(self): 32 | # Check working with optional parameters 33 | subprocess.call( 34 | [ 35 | "handwrite", 36 | os.path.join(self.file_dir, "test_data", "sheettopng", "excellent.jpg"), 37 | self.temp_dir, 38 | "--directory", 39 | self.temp_dir, 40 | "--config", 41 | os.path.join(self.file_dir, "test_data", "config_data", "default.json"), 42 | "--filename", 43 | "CustomFont", 44 | ] 45 | ) 46 | for i in ALL_CHARS: 47 | for suffix in [".bmp", ".png", ".svg"]: 48 | self.assertTrue( 49 | os.path.exists(os.path.join(self.temp_dir, f"{i}", f"{i}{suffix}")) 50 | ) 51 | self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "CustomFont.ttf"))) 52 | 53 | def test_multiple_inputs(self): 54 | # Check working with multiple inputs 55 | try: 56 | subprocess.check_call( 57 | [ 58 | "handwrite", 59 | self.sheets_dir, 60 | self.temp_dir, 61 | ] 62 | ) 63 | except subprocess.CalledProcessError as e: 64 | self.assertNotEqual(e.returncode, 0) 65 | 66 | def test_multiple_config(self): 67 | # Check working with multiple config files 68 | try: 69 | subprocess.check_call( 70 | [ 71 | "handwrite", 72 | self.sheets_dir, 73 | self.temp_dir, 74 | "--config", 75 | os.path.join(self.file_dir, "test_data", "config_data"), 76 | ] 77 | ) 78 | except subprocess.CalledProcessError as e: 79 | self.assertNotEqual(e.returncode, 0) 80 | -------------------------------------------------------------------------------- /tests/test_data/config_data/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "threshold_value": 200, 3 | "props": { 4 | "ascent": 800, 5 | "descent": 200, 6 | "em": 1000, 7 | "encoding": "UnicodeFull", 8 | "lang": "English (US)", 9 | "filename": "MyFont", 10 | "style": "Regular" 11 | }, 12 | "sfnt_names": { 13 | "Copyright": "Copyright (c) 2021 by Nobody", 14 | "Family": "MyFont", 15 | "SubFamily": "Regular", 16 | "UniqueID": "MyFont 2021-02-04", 17 | "Fullname": "MyFont Regular", 18 | "Version": "Version 001.000", 19 | "PostScriptName": "MyFont-Regular" 20 | }, 21 | "glyphs": [ 22 | 65, 23 | 66, 24 | 67, 25 | 68, 26 | 69, 27 | 70, 28 | 71, 29 | 72, 30 | 73, 31 | 74, 32 | 75, 33 | 76, 34 | 77, 35 | 78, 36 | 79, 37 | 80, 38 | 81, 39 | 82, 40 | 83, 41 | 84, 42 | 85, 43 | 86, 44 | 87, 45 | 88, 46 | 89, 47 | 90, 48 | 97, 49 | 98, 50 | 99, 51 | 100, 52 | 101, 53 | 102, 54 | 103, 55 | 104, 56 | 105, 57 | 106, 58 | 107, 59 | 108, 60 | 109, 61 | 110, 62 | 111, 63 | 112, 64 | 113, 65 | 114, 66 | 115, 67 | 116, 68 | 117, 69 | 118, 70 | 119, 71 | 120, 72 | 121, 73 | 122, 74 | 48, 75 | 49, 76 | 50, 77 | 51, 78 | 52, 79 | 53, 80 | 54, 81 | 55, 82 | 56, 83 | 57, 84 | 46, 85 | 44, 86 | 59, 87 | 58, 88 | 33, 89 | 63, 90 | 34, 91 | 39, 92 | 45, 93 | 43, 94 | 61, 95 | 47, 96 | 37, 97 | 38, 98 | 40, 99 | 41, 100 | 91, 101 | 93 102 | ], 103 | "typography_parameters": { 104 | "bearing_table": { 105 | "Default": [60, 60], 106 | "A": [60, -50], 107 | "a": [30, 40], 108 | "B": [60, 0], 109 | "C": [60, -30], 110 | "c": [null, 40], 111 | "b": [null, 40], 112 | "D": [null, 10], 113 | "d": [30, -20], 114 | "e": [30, 40], 115 | "E": [70, 10], 116 | "F": [70, 0], 117 | "f": [0, -20], 118 | "G": [60, 30], 119 | "g": [20, 60], 120 | "h": [40, 40], 121 | "I": [80, 50], 122 | "i": [null, 60], 123 | "J": [40, 30], 124 | "j": [-70, 40], 125 | "k": [40, 20], 126 | "K": [80, 0], 127 | "H": [null, 10], 128 | "L": [80, 10], 129 | "l": [null, 0], 130 | "M": [60, 30], 131 | "m": [40, null], 132 | "N": [70, 10], 133 | "n": [30, 40], 134 | "O": [70, 10], 135 | "o": [40, 40], 136 | "P": [70, 0], 137 | "p": [null, 40], 138 | "Q": [70, 10], 139 | "q": [20, 30], 140 | "R": [70, -10], 141 | "r": [null, 40], 142 | "S": [60, 60], 143 | "s": [20, 40], 144 | "T": [null, -10], 145 | "t": [-10, 20], 146 | "U": [70, 20], 147 | "u": [40, 40], 148 | "V": [null, -10], 149 | "v": [20, 20], 150 | "W": [70, 20], 151 | "w": [40, 40], 152 | "X": [null, -10], 153 | "x": [10, 20], 154 | "y": [20, 30], 155 | "Y": [40, 0], 156 | "Z": [null, -10], 157 | "z": [10, 20], 158 | "1": [-10, 30], 159 | "2": [-10, 30], 160 | "3": [10, 40], 161 | "4": [30, 30], 162 | "5": [30, 40], 163 | "6": [20, 20], 164 | "7": [30, 20], 165 | "8": [30, 20], 166 | "9": [30, 30], 167 | "0": [50, 40], 168 | ".": [null, 10], 169 | ",": [null, 10], 170 | ";": [null, 10], 171 | ":": [null, 20], 172 | "!": [null, 20], 173 | "?": [null, 30], 174 | "\"": [null, 20], 175 | "'": [null, 10], 176 | "-": [null, 20], 177 | "+": [null, 20], 178 | "=": [null, 20], 179 | "/": [null, 20], 180 | "%": [40, 40], 181 | "&": [40, 40], 182 | "(": [10, 10], 183 | ")": [10, 10], 184 | "[": [10, 10], 185 | "]": [10, 10] 186 | }, 187 | "kerning_table": { 188 | "autokern": true, 189 | "seperation": 0, 190 | "rows": [ 191 | null, 192 | "f-+=/?", 193 | "t", 194 | "i", 195 | "r", 196 | "k", 197 | "l.,;:!\"'()[]", 198 | "v", 199 | "bop%&", 200 | "nm", 201 | "a", 202 | "W", 203 | "T", 204 | "F", 205 | "P", 206 | "g", 207 | "qdhyj", 208 | "cesuwxz", 209 | "V", 210 | "A", 211 | "Y", 212 | "MNHI", 213 | "OQDU", 214 | "J", 215 | "C", 216 | "E", 217 | "L", 218 | "P", 219 | "KR", 220 | "G", 221 | "BSXZ" 222 | ], 223 | "cols": [ 224 | null, 225 | "oacedgqw%&", 226 | "ft-+=/?", 227 | "xvz", 228 | "hbli.,;:!\"'()[]", 229 | "j", 230 | "mnpru", 231 | "k", 232 | "y", 233 | "s", 234 | "T", 235 | "F", 236 | "Zero" 237 | ], 238 | "table": [ 239 | [ 240 | [0, 0, 0, 0, 0, 0, 0, null, null, 0, 0, null, 0], 241 | [0, -30, -61, -20, null, 0, null, null, null, 0, -150, null, -70], 242 | [0, -50, -41, -20, null, 0, 0, null, null, 0, -150, null, -10], 243 | [ 244 | null, 245 | null, 246 | -40, 247 | null, 248 | null, 249 | null, 250 | null, 251 | null, 252 | null, 253 | null, 254 | -150, 255 | null, 256 | null 257 | ], 258 | [0, -32, -40, null, null, 0, null, null, null, 0, -170, null, 29], 259 | [0, -10, -50, null, null, 0, null, null, null, -48, -150, null, -79], 260 | [0, -10, -20, null, 0, 0, 0, null, null, 0, -110, null, -20], 261 | [0, -40, -35, -15, null, 0, 0, null, null, 0, -170, null, 30], 262 | [0, null, -40, null, 0, 0, 0, null, null, 0, -170, null, 43], 263 | [ 264 | null, 265 | null, 266 | -30, 267 | null, 268 | null, 269 | null, 270 | null, 271 | null, 272 | null, 273 | null, 274 | -170, 275 | null, 276 | null 277 | ], 278 | [0, -23, -30, null, 0, 0, 0, null, null, 0, -170, null, 7], 279 | [0, -40, -30, -10, null, 0, 0, null, null, 0, null, null, null], 280 | [0, -150, -120, -120, -30, -40, -130, null, -100, -80, 0, null, null], 281 | [0, -90, -90, -70, -30, 0, -70, null, -50, -80, -40, null, null], 282 | [0, -100, -70, -50, null, 0, -70, null, -30, -80, -20, null, null], 283 | [ 284 | null, 285 | null, 286 | null, 287 | null, 288 | null, 289 | 40, 290 | null, 291 | null, 292 | null, 293 | null, 294 | -120, 295 | null, 296 | null 297 | ], 298 | [null, null, null, null, 30, 30, 30, 30, 30, null, -100, null, null], 299 | [ 300 | null, 301 | null, 302 | null, 303 | null, 304 | null, 305 | null, 306 | null, 307 | null, 308 | null, 309 | null, 310 | -120, 311 | null, 312 | null 313 | ], 314 | [null, -70, 30, 30, null, -80, -20, null, -40, -40, -10, null, null], 315 | [null, 30, 60, 30, 30, null, 20, 40, 20, -80, -120, 20, 20], 316 | [null, 20, 60, 30, 30, null, 20, 20, 40, 20, -10, null, null], 317 | [null, 20, 10, 40, 30, null, 10, 20, 20, null, null, null, null], 318 | [null, null, 50, 40, 30, -20, 30, 20, 30, null, -70, null, null], 319 | [null, null, 40, 20, 20, -20, 10, 10, 30, null, -30, null, null], 320 | [null, 10, 40, 10, 30, null, 30, 30, 20, null, -30, null, null], 321 | [null, -10, 50, null, 10, -20, 10, null, 20, null, null, null, null], 322 | [ 323 | null, 324 | -10, 325 | -10, 326 | null, 327 | null, 328 | -30, 329 | null, 330 | null, 331 | 20, 332 | null, 333 | -90, 334 | null, 335 | null 336 | ], 337 | [null, -50, 30, 20, 20, null, null, 20, 20, null, -30, null, null], 338 | [null, 20, 20, 20, 10, null, 20, 20, 20, null, -60, null, null], 339 | [null, 20, 40, 30, 30, null, 20, 20, 20, null, -100, 10, null], 340 | [null, 20, 40, 30, 30, null, 20, 20, 20, 20, -20, 10, null] 341 | ] 342 | ] 343 | } 344 | }, 345 | "# vim: set et sw=2 ts=2 sts=2:": false 346 | } 347 | -------------------------------------------------------------------------------- /tests/test_data/pngtosvg/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/33.png -------------------------------------------------------------------------------- /tests/test_data/pngtosvg/34/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/34/34.png -------------------------------------------------------------------------------- /tests/test_data/pngtosvg/45.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/45.bmp -------------------------------------------------------------------------------- /tests/test_data/sheettopng/excellent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/sheettopng/excellent.jpg -------------------------------------------------------------------------------- /tests/test_pngtosvg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from handwrite.pngtosvg import PNGtoSVG 5 | 6 | 7 | class TestPNGtoSVG(unittest.TestCase): 8 | def setUp(self): 9 | self.directory = os.path.join( 10 | os.path.dirname(os.path.abspath(__file__)), 11 | "test_data" + os.sep + "pngtosvg", 12 | ) 13 | self.converter = PNGtoSVG() 14 | 15 | def test_bmpToSvg(self): 16 | self.converter.bmpToSvg(self.directory + os.sep + "45.bmp") 17 | self.assertTrue(os.path.exists(self.directory + os.sep + "45.svg")) 18 | os.remove(self.directory + os.sep + "45.svg") 19 | 20 | def test_convert(self): 21 | self.converter.convert(self.directory) 22 | path = os.walk(self.directory) 23 | for root, dirs, files in path: 24 | for f in files: 25 | if f[-4:] == ".png": 26 | self.assertTrue(os.path.exists(root + os.sep + f[0:-4] + ".bmp")) 27 | self.assertTrue(os.path.exists(root + os.sep + f[0:-4] + ".svg")) 28 | os.remove(root + os.sep + f[0:-4] + ".bmp") 29 | os.remove(root + os.sep + f[0:-4] + ".svg") 30 | -------------------------------------------------------------------------------- /tests/test_sheettopng.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | 6 | from handwrite.sheettopng import SHEETtoPNG, ALL_CHARS 7 | 8 | 9 | class TestSHEETtoPNG(unittest.TestCase): 10 | def setUp(self): 11 | self.directory = tempfile.mkdtemp() 12 | self.sheets_path = os.path.join( 13 | os.path.dirname(os.path.abspath(__file__)), 14 | "test_data" + os.sep + "sheettopng", 15 | ) 16 | self.converter = SHEETtoPNG() 17 | 18 | def tearDown(self): 19 | shutil.rmtree(self.directory) 20 | 21 | def test_convert(self): 22 | # Single sheet input 23 | excellent_scan = os.path.join(self.sheets_path, "excellent.jpg") 24 | config = os.path.join( 25 | os.path.dirname(os.path.abspath(__file__)), 26 | "test_data", 27 | "config_data", 28 | "default.json", 29 | ) 30 | self.converter.convert(excellent_scan, self.directory, config) 31 | for i in ALL_CHARS: 32 | self.assertTrue( 33 | os.path.exists(os.path.join(self.directory, f"{i}", f"{i}.png")) 34 | ) 35 | 36 | # TODO Once all the errors are done for detect_characters 37 | # Write tests to check each kind of scan and whether it raises 38 | # helpful errors, Boilerplate below: 39 | # def test_detect_characters(self): 40 | # scans = ["excellent", "good", "average"] 41 | # for scan in scans: 42 | # detected_chars = self.converter.detect_characters( 43 | # os.path.join(self.sheets_path, f"{scan}.jpg") 44 | # ) 45 | -------------------------------------------------------------------------------- /tests/test_svgtottf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | 6 | from handwrite import SHEETtoPNG, SVGtoTTF, PNGtoSVG 7 | 8 | 9 | class TestSVGtoTTF(unittest.TestCase): 10 | def setUp(self): 11 | self.temp = tempfile.mkdtemp() 12 | self.characters_dir = tempfile.mkdtemp(dir=self.temp) 13 | self.sheet_path = os.path.join( 14 | os.path.dirname(os.path.abspath(__file__)), 15 | "test_data", 16 | "sheettopng", 17 | "excellent.jpg", 18 | ) 19 | self.config = os.path.join( 20 | os.path.dirname(os.path.abspath(__file__)), 21 | "test_data", 22 | "config_data", 23 | "default.json", 24 | ) 25 | SHEETtoPNG().convert(self.sheet_path, self.characters_dir, self.config) 26 | PNGtoSVG().convert(directory=self.characters_dir) 27 | self.converter = SVGtoTTF() 28 | self.metadata = {"filename": "CustomFont"} 29 | 30 | def tearDown(self): 31 | shutil.rmtree(self.temp) 32 | 33 | def test_convert(self): 34 | self.converter.convert( 35 | self.characters_dir, self.temp, self.config, self.metadata 36 | ) 37 | self.assertTrue(os.path.exists(os.path.join(self.temp, "CustomFont.ttf"))) 38 | # os.remove(os.join()) 39 | 40 | def test_convert_duplicate(self): 41 | fake_ttf = tempfile.NamedTemporaryFile( 42 | suffix=".ttf", dir=self.temp, delete=False 43 | ) 44 | fake_ttf.close() # Doesn't keep open 45 | os.rename(fake_ttf.name, os.path.join(self.temp, "MyFont.ttf")) 46 | self.converter.convert(self.characters_dir, self.temp, self.config) 47 | self.assertTrue(os.path.exists(os.path.join(self.temp, "MyFont (1).ttf"))) 48 | self.converter.convert(self.characters_dir, self.temp, self.config) 49 | self.assertTrue(os.path.exists(os.path.join(self.temp, "MyFont (1) (1).ttf"))) 50 | --------------------------------------------------------------------------------