├── .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 | [](https://github.com/builtree/handwrite/actions)
9 | [](https://pypi.org/project/handwrite)
10 | [](https://gitter.im/codEd-org/handwrite)
11 | [](https://opensource.org/licenses/MIT)
12 | [](https://github.com/psf/black)
13 | [](https://github.com/builtree/handwrite/actions/workflows/codeql-analysis.yml)
14 | [](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 | [](https://github.com/builtree/handwrite/actions)
9 | [](https://pypi.org/project/handwrite)
10 | [](https://gitter.im/codEd-org/handwrite)
11 | [](https://opensource.org/licenses/MIT)
12 | [](https://github.com/psf/black)
13 | [](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 |
--------------------------------------------------------------------------------