├── .github └── workflows │ ├── codeql-analysis.yml │ ├── docs-build-test.yml │ ├── docs.yml │ ├── python-publish.yml │ └── testing.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── 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/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 | python3 -m pip install Jinja2==3.0.0 18 | - name: Try Docs build 19 | run: mkdocs build 20 | -------------------------------------------------------------------------------- /.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 | python3 -m pip install Jinja2==3.0.0 25 | - name: Git setup and update 26 | run: | 27 | git config user.name "GitHub Action" && git config user.email "github-action@github.com" 28 | git fetch origin 29 | - name: Build Docs for main 30 | run: mkdocs build 31 | - name: Build Docs for dev 32 | run: | 33 | cd devbranch 34 | mkdocs build 35 | mv site dev 36 | cd .. 37 | mv devbranch/dev site/ 38 | - name: Add latest web build and deploy 39 | run: | 40 | mkdocs gh-deploy --dirty 41 | -------------------------------------------------------------------------------- /.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 | workflow_dispatch: 8 | release: 9 | types: [released] 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.8' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: __token__ 29 | TWINE_PASSWORD: ${{ secrets.PYPI_HANDWRITE }} 30 | run: | 31 | python setup.py sdist bdist_wheel 32 | twine upload dist/* 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Handwrite - Code of Conduct 2 | 3 | Please refer to our [Builtree Community Code of Conduct](https://github.com/bui`ltree/builtree/blob/main/governance/CODE-OF-CONDUCT.md) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This Project welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the Project's license. You represent that if you do not own copyright in the code that you have the authority to submit it under the Project's license. All feedback, suggestions, or contributions are not confidential. 4 | 5 | ## Organisation Contributing 6 | The Project abides by the Organization's code of conduct and trademark policy. Please refer to our [Builtree Contributing Guidelines](https://github.com/bui`ltree/builtree/blob/main/governance/CODE-OF-CONDUCT.md). 7 | 8 | # Contributing to Handwrite 9 | 10 | Contributions can come in many forms, we always need help in improving the project. If you find issues with the documentation, usability, code, or even a question, please open an [issue](https://github.com/builtree/handwrite/issues) to let us know and or post in [Builtree Discord Server](https://discord.gg/9BtRZhJb9G)'s relevant channel. 11 | 12 | ## We Develop with Github 13 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## We Use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), So All Code Changes Happen Through Pull Requests 16 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow)). We actively welcome your pull requests: 17 | 18 | 1. Make sure there is an issue existing for your pull request. If not, get in touch with a maintainer/mentor and create one. 19 | 2. Make sure that issue has been acknowledged by the maintainer/mentor before you start working. 20 | 3. Fork the repo and create your branch from `main`. 21 | 4. If you've added code that should be tested, add tests (if the project supports tests or has a testing guideline). Ensure the test suite passes. 22 | 5. If you've changed a feature,API,etc., update the documentation. 23 | 6. Make sure your code lints. 24 | 7. Properly attach that pull request to its issue! 25 | 26 | ## Any contributions you make will be under the MIT License 27 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](./LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. 28 | 29 | ## Report bugs using Github's [issues](https://github.com/builtree/handwrite/issues) 30 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/builtree/handwrite/issues/new/choose); it's that easy! 31 | 32 | ## Write bug reports with detail 33 | Make sure the bug reports are detaild and follow the bug issue template in the project. 34 | 35 | **Great Bug Reports** tend to have: 36 | 37 | - A quick summary and/or background 38 | - Steps to reproduce 39 | - Be specific! 40 | - What you expected would happen 41 | - What actually happens 42 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 43 | 44 | People *love* thorough bug reports! 45 | 46 | ## Coding Style 47 | 48 | * Style point 1 49 | * Style point 2 50 | 51 | --- 52 | Part of MVG-0.1-beta. 53 | Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). 54 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance Policy 2 | 3 | This document provides the governance policy for the Project. Maintainers agree to this policy and to abide by all Project polices, including the code of conduct, trademark policy, and antitrust policy by adding their name to the maintainers.md file. 4 | 5 | ## 1. Roles. 6 | 7 | This project **may** include the following roles. Additional roles may be adopted and documented by the Project. 8 | 9 | **1.1. Maintainers**. Maintainers are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are also responsible for determining consensus. This Project may add or remove Maintainers with the approval of the current Maintainers. 10 | 11 | **1.2. Open Source Product Managers**. (OSPMs). OSPMs are responsible for understanding the project completely, researching and determining a constantly evolving vision/roadmap the project can follow. OSPMs will help those involved in the project priotize what is important moving forward with the project. 12 | 13 | **1.3. Mentors**. Mentors are responsible for mentoring contributors by engaging the contributors and helping them with their contributions by providing subtle guidance. Mentors can also be contributors in the project. 14 | 15 | **1.4. Contributors**. Contributors are those that have made contributions to the Project. 16 | 17 | ## 2. Decisions. 18 | 19 | **2.1. Consensus-Based Decision Making**. Projects make decisions through consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Contributors and nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements. 20 | 21 | **2.2. Appeal Process**. Decisions may be appealed by opening an issue and that appeal will be considered by the Maintainers in good faith, who will respond in writing within a reasonable time. If the Maintainers deny the appeal, the appeal my be brought before the Organization Steering Committee, who will also respond in writing in a reasonable time. 22 | 23 | ## 3. How We Work. 24 | 25 | **3.1. Openness**. Participation is open to anyone who is directly and materially affected by the activity in question. There shall be no undue financial barriers to participation. 26 | 27 | **3.2. Balance**. The development process should balance the interests of Contributors and other stakeholders. Contributors from diverse interest categories shall be sought with the objective of achieving balance. 28 | 29 | **3.3. Coordination and Harmonization**. Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in this Project. 30 | 31 | **3.4. Consideration of Views and Objections**. Prompt consideration shall be given to the written views and objections of all Contributors. 32 | 33 | **3.5. Written procedures**. This governance document and other materials documenting this project's development process shall be available to any interested person. 34 | 35 | ## 4. No Confidentiality. 36 | 37 | Information disclosed in connection with any Project activity, including but not limited to meetings, contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. 38 | 39 | ## 5. Trademarks. 40 | 41 | Any names, trademarks, logos, or goodwill developed by and associated with the Project (the "Marks") are controlled by the Organization. Maintainers may only use these Marks in accordance with the Organization's trademark policy. If a Maintainer resigns or is removed, any rights the Maintainer may have in the Marks revert to the Organization. 42 | 43 | ## 6. Amendments. 44 | 45 | Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee. 46 | 47 | --- 48 | Part of MVG-0.1-beta. 49 | Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Builtree 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 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the Governance document. By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the code of conduct, trademark policy, and antitrust policy. If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies. 4 | 5 | | **NAME** | **Organization** | **Username** | 6 | | --- | --- | --- | 7 | | Saksham Arora | Builtree | [sakshamarora1](https://github.com/sakshamarora1) | 8 | | Yash Lamba | Builtree | [yashlamba](https://github.com/yashlamba) | 9 | 10 | --- 11 | Part of MVG-0.1-beta. 12 | Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include handwrite/default.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 |

7 | 8 | [![Tests](https://github.com/builtree/handwrite/workflows/Tests/badge.svg)](https://github.com/builtree/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/builtree/handwrite/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/builtree/handwrite/actions/workflows/codeql-analysis.yml) 14 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/builtree/handwrite.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/builtree/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://builtree.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/builtree/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/builtree/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 | 93 | 94 | 95 | ## Setting Up Docs 96 | 97 | 1. If you haven't done a developer install of handwrite, you will need to install mkdocs and its requirements: 98 | ```bash 99 | pip install mkdocs pymdown-extensions mkdocs-material mkdocs-git-revision-date-localized-plugin 100 | ``` 101 | 102 | 2. Check the installations by executing this command: 103 | ```bash 104 | mkdocs --version 105 | ``` 106 | 107 | !!! warning "" 108 | If this doesn't work, try restarting the terminal 109 | 110 | 3. Use the below command to host the documentation on local server 111 | ```bash 112 | mkdocs serve --dev-addr 127.0.0.1:8000 113 | ``` 114 | {== MkDocs supports live reload so you don't have to run the server again and again. Just save the changes in the docs and you'll see the change immediately. ==} 115 | 116 | 4. All the documentation is present in the `docs` directory. 117 | -------------------------------------------------------------------------------- /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/builtree/handwrite/workflows/Tests/badge.svg)](https://github.com/builtree/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/builtree/handwrite/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/builtree/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/builtree/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/builtree/handwrite/blob/main/handwrite/default.json) is used. 34 | 35 | 4. Your font will be created as `OUTPUT DIRECTORY/OUTPUT FONT NAME.ttf`. Install the font in your system. 36 | 37 | 5. Select your font in your word processor and get to work! 38 | Here's the end result! 39 | 40 |

41 | 42 | 43 |

44 | 45 | ## Configuring 46 | 47 | TO DO 48 | -------------------------------------------------------------------------------- /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://builtree.github.io/handwrite" 5 | 6 | # Repository 7 | repo_name: "builtree/handwrite" 8 | repo_url: "https://github.com/builtree/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 | -------------------------------------------------------------------------------- /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.1", 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/builtree/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/builtree/handwrite/ec476e4f02c5d005f749b35d3db09823b8666ea7/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/builtree/handwrite/ec476e4f02c5d005f749b35d3db09823b8666ea7/tests/test_data/pngtosvg/33.png -------------------------------------------------------------------------------- /tests/test_data/pngtosvg/34/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/builtree/handwrite/ec476e4f02c5d005f749b35d3db09823b8666ea7/tests/test_data/pngtosvg/34/34.png -------------------------------------------------------------------------------- /tests/test_data/pngtosvg/45.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/builtree/handwrite/ec476e4f02c5d005f749b35d3db09823b8666ea7/tests/test_data/pngtosvg/45.bmp -------------------------------------------------------------------------------- /tests/test_data/sheettopng/excellent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/builtree/handwrite/ec476e4f02c5d005f749b35d3db09823b8666ea7/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 | --------------------------------------------------------------------------------