├── .github
├── FUNDING.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── docs-build-test.yml
│ ├── docs.yml
│ ├── python-publish.yml
│ └── testing.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs
├── api
│ ├── pngtosvg.md
│ ├── sheettopng.md
│ └── svgtottf.md
├── contributing.md
├── credits.md
├── index.md
├── installation.md
└── usage.md
├── handwrite
├── __init__.py
├── cli.py
├── default.json
├── pngtosvg.py
├── sheettopng.py
└── svgtottf.py
├── handwrite_sample.pdf
├── mkdocs.yml
├── setup.py
└── tests
├── __init__.py
├── test_cli.py
├── test_data
├── config_data
│ └── default.json
├── pngtosvg
│ ├── 34
│ │ └── 34.png
│ ├── 33.png
│ └── 45.bmp
└── sheettopng
│ └── excellent.jpg
├── test_pngtosvg.py
├── test_sheettopng.py
└── test_svgtottf.py
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 | custom: ['https://www.buymeacoffee.com/sakshamarora1', 'https://www.buymeacoffee.com/yashlamba', 'https://paypal.me/yashlamba']
3 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [main, dev]
6 | pull_request:
7 | branches: [main, dev]
8 |
9 | jobs:
10 | analyze:
11 | name: Analyze
12 | runs-on: ubuntu-latest
13 | permissions:
14 | actions: read
15 | contents: read
16 | security-events: write
17 | strategy:
18 | fail-fast: false
19 |
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v2
23 | - name: Set up Python
24 | uses: actions/setup-python@v2
25 | with:
26 | python-version: '3.8'
27 |
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip
31 | pip install .[dev]
32 | echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV
33 |
34 | - name: Initialize CodeQL
35 | uses: github/codeql-action/init@v1
36 | with:
37 | languages: python
38 | setup-python-dependencies: false
39 |
40 | - name: Perform CodeQL Analysis
41 | uses: github/codeql-action/analyze@v1
42 |
--------------------------------------------------------------------------------
/.github/workflows/docs-build-test.yml:
--------------------------------------------------------------------------------
1 | name: Docs test
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout Repo
8 | uses: actions/checkout@v2
9 | - name: Setup Python
10 | uses: actions/setup-python@v2
11 | with:
12 | python-version: '3.8'
13 | - name: Install dependencies
14 | run: |
15 | python3 -m pip install --upgrade pip
16 | python3 -m pip install -e .[dev]
17 | - name: Try Docs build
18 | run: mkdocs build
19 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs Deploy
2 | on: push
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout main
8 | uses: actions/checkout@v2
9 | with:
10 | ref: main
11 | - name: Checkout dev
12 | uses: actions/checkout@v2
13 | with:
14 | ref: dev
15 | path: devbranch
16 | - name: Setup Python
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: '3.8'
20 | - name: Install dependencies
21 | run: |
22 | python3 -m pip install --upgrade pip
23 | python3 -m pip install -e .[dev]
24 | - name: Git setup and update
25 | run: |
26 | git config user.name "GitHub Action" && git config user.email "github-action@github.com"
27 | git fetch origin
28 | - name: Build Docs for main
29 | run: mkdocs build
30 | - name: Build Docs for dev
31 | run: |
32 | cd devbranch
33 | mkdocs build
34 | mv site dev
35 | cd ..
36 | mv devbranch/dev site/
37 | - name: Add latest web build and deploy
38 | run: |
39 | mkdocs gh-deploy --dirty
40 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [released]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: '3.8'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install setuptools wheel twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: __token__
28 | TWINE_PASSWORD: ${{ secrets.PYPI_HANDWRITE }}
29 | run: |
30 | python setup.py sdist bdist_wheel
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | lint:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout full upstream repo
12 | uses: actions/checkout@v2
13 | - name: Set up Python 3.8
14 | uses: actions/setup-python@v2
15 | with:
16 | python-version: 3.8
17 | - name: Check formatting with Black
18 | uses: psf/black@stable
19 |
20 | test:
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | matrix:
24 | os: [windows-latest, ubuntu-latest]
25 | python-version: [3.7, 3.8]
26 |
27 | steps:
28 | - name: Checkout full upstream repo
29 | uses: actions/checkout@v2
30 | - name: Set up Python ${{ matrix.python-version }}
31 | uses: actions/setup-python@v2
32 | with:
33 | python-version: ${{ matrix.python-version }}
34 | - name: Install fontforge (Linux)
35 | if: matrix.os == 'ubuntu-latest'
36 | run: |
37 | wget -O fontforge https://github.com/fontforge/fontforge/releases/download/20201107/FontForge-2020-11-07-21ad4a1-x86_64.AppImage
38 | chmod +x fontforge
39 | sudo mv fontforge /usr/bin/
40 | - name: Install fontforge (Windows)
41 | if: matrix.os == 'windows-latest'
42 | run: |
43 | Invoke-WebRequest -Uri https://github.com/fontforge/fontforge/releases/download/20201107/FontForge-2020-11-07-Windows.exe -OutFile fontforge.exe
44 | .\fontforge.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES /NOCANCEL | Out-Null
45 | echo "C:\Program Files (x86)\FontForgeBuilds\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
46 | - name: Install Potrace
47 | if: matrix.os == 'ubuntu-latest'
48 | run: |
49 | sudo apt install -y potrace
50 | - name: Install Handwrite
51 | run: |
52 | pip install -e .
53 | - name: Test
54 | run: |
55 | python setup.py test
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | tests/test_data/config_data/config/excellent.json
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | .python-version
87 |
88 | # pipenv
89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
92 | # install all needed dependencies.
93 | #Pipfile.lock
94 |
95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
96 | __pypackages__/
97 |
98 | # Celery stuff
99 | celerybeat-schedule
100 | celerybeat.pid
101 |
102 | # SageMath parsed files
103 | *.sage.py
104 |
105 | # Environments
106 | .env
107 | .venv
108 | env/
109 | venv/
110 | ENV/
111 | env.bak/
112 | venv.bak/
113 |
114 | # Spyder project settings
115 | .spyderproject
116 | .spyproject
117 |
118 | # Rope project settings
119 | .ropeproject
120 |
121 | # mkdocs documentation
122 | /site
123 |
124 | # mypy
125 | .mypy_cache/
126 | .dmypy.json
127 | dmypy.json
128 |
129 | # Pyre type checker
130 | .pyre/
131 |
132 | # ttf files
133 | *.ttf
134 |
135 | # IDE
136 | .vscode
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v3.2.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: mixed-line-ending
7 | - repo: https://github.com/psf/black
8 | rev: 20.8b1
9 | hooks:
10 | - id: black
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 codEd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include handwrite/default.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://github.com/cod-ed/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/cod-ed/handwrite/actions/workflows/codeql-analysis.yml)
14 | [](https://lgtm.com/projects/g/cod-ed/handwrite/context:python)
15 |
16 | # Handwrite - Type in your Handwriting!
17 |
18 | Ever had those long-winded assignments, that the teacher always wants handwritten?
19 | Is your written work messy, cos you think faster than you can write?
20 | Now, you can finish them with the ease of typing in your own font!
21 |
22 |
23 |
24 |
25 |
26 |
27 | Handwrite makes typing written assignments efficient, convenient and authentic.
28 |
29 | Handwrite generates a custom font based on your handwriting sample, which can easily be used in text editors and word processors like Microsoft Word & Libre Office Word!
30 |
31 | Handwrite is also helpful for those with dysgraphia.
32 |
33 | You can get started with Handwrite [here](https://cod-ed.github.io/handwrite/).
34 |
35 | ## Sample
36 |
37 | You just need to fill up a form:
38 |
39 |
40 |
41 |
42 |
43 |
44 | Here's the end result!
45 |
46 |
47 |
48 |
49 |
50 |
51 | ## Credits and Reference
52 |
53 | 1. [Potrace](http://potrace.sourceforge.net/) algorithm and package has been immensely helpful.
54 |
55 | 2. [Fontforge](https://fontforge.org/en-US/) for packaging and adjusting font parameters.
56 |
57 | 3. [Sacha Chua's](https://github.com/sachac) [project](https://github.com/sachac/sachac-hand/) proved to be a great reference for fontforge python.
58 |
59 | 4. All credit for svgtottf converter goes to this [project](https://github.com/pteromys/svgs2ttf) by [pteromys](https://github.com/pteromys). We made a quite a lot of modifications of our own, but the base script idea was derived from here.
60 |
--------------------------------------------------------------------------------
/docs/api/pngtosvg.md:
--------------------------------------------------------------------------------
1 | ::: handwrite.pngtosvg.PNGtoSVG
2 | selection:
3 | docstring_style: numpy
--------------------------------------------------------------------------------
/docs/api/sheettopng.md:
--------------------------------------------------------------------------------
1 | ::: handwrite.sheettopng.SHEETtoPNG
2 | selection:
3 | docstring_style: numpy
--------------------------------------------------------------------------------
/docs/api/svgtottf.md:
--------------------------------------------------------------------------------
1 | ::: handwrite.svgtottf.SVGtoTTF
2 | selection:
3 | docstring_style: numpy
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | # Development
2 |
3 | ## Linux
4 |
5 | 1. Install Potrace using apt
6 |
7 | ```console
8 | sudo apt-get install potrace
9 | ```
10 |
11 | 2. Install fontforge
12 |
13 | ```console
14 | sudo apt-get install fontforge
15 | ```
16 |
17 | ???+ warning
18 | Since the PPA for fontforge is no longer maintained, apt might not work for some users.
19 | The preferred way to install is using the AppImage from: https://fontforge.org/en-US/downloads/
20 |
21 | 3. Clone the repository or your fork
22 |
23 | ```console
24 | git clone https://github.com/cod-ed/handwrite
25 | ```
26 |
27 | 4. (Optional) Make a virtual environment and activate it
28 |
29 | ```console
30 | python -m venv .venv
31 | source .venv/bin/activate
32 | ```
33 |
34 | 5. In the project directory run:
35 |
36 | ```console
37 | pip install -e .[dev]
38 | ```
39 |
40 | 6. Make sure the tests run:
41 |
42 | ```console
43 | python setup.py test
44 | ```
45 |
46 | 7. Install pre-commit hooks before contributing:
47 |
48 | ```console
49 | pre-commit install
50 | ```
51 |
52 | You are ready to go!
53 |
54 | ## Windows
55 |
56 | 1. Install [Potrace](http://potrace.sourceforge.net/#downloading) and make sure it's in your PATH.
57 |
58 | 2. Install [fontforge](https://fontforge.org/en-US/downloads/) and make sure scripting is enabled.
59 |
60 | 3. Clone the repository or your fork
61 |
62 | ```console
63 | git clone https://github.com/cod-ed/handwrite
64 | ```
65 |
66 | 4. (Optional) Make a virtual environment and activate it
67 |
68 | ```console
69 | python -m venv .venv
70 | .venv\Scripts\activate
71 | ```
72 |
73 | 5. In the project directory run:
74 |
75 | ```console
76 | pip install -e .[dev]
77 | ```
78 |
79 | 6. Make sure the tests run:
80 |
81 | ```console
82 | python setup.py test
83 | ```
84 |
85 | 7. Install pre-commit hooks before contributing:
86 |
87 | ```console
88 | pre-commit install
89 | ```
90 |
91 | You are ready to go!
92 |
--------------------------------------------------------------------------------
/docs/credits.md:
--------------------------------------------------------------------------------
1 | ## Credits and References
2 |
3 | 1. [Potrace](http://potrace.sourceforge.net/) algorithm and package has been immensely helpful.
4 |
5 | 2. [Fontforge](https://fontforge.org/en-US/) for packaging and adjusting font parameters.
6 |
7 | 3. [Sacha Chua's](https://github.com/sachac) [project](https://github.com/sachac/sachac-hand/) proved to be a great reference for fontforge python.
8 |
9 | 4. All credit for svgtottf converter goes to this [project](https://github.com/pteromys/svgs2ttf) by [pteromys](https://github.com/pteromys). We made a quite a lot of modifications of our own, but the base script idea was derived from here.
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://github.com/cod-ed/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/cod-ed/handwrite/actions/workflows/codeql-analysis.yml)
14 |
15 | # Handwrite - Type in your Handwriting!
16 |
17 | Ever had those long-winded assignments, that the teacher always wants handwritten?
18 | Is your written work messy, cos you think faster than you can write?
19 | Now, you can finish them with the ease of typing in your own font!
20 |
21 |
22 |
23 |
24 |
25 |
26 | Handwrite makes typing written assignments efficient, convenient and authentic.
27 |
28 | Handwrite generates a custom font based on your handwriting sample, which can easily be used in text editors and word processors like Microsoft Word & Libre Office Word!
29 |
30 | Handwrite is also helpful for those with dysgraphia.
31 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installing Handwrite
2 |
3 | 1. Install [fontforge](https://fontforge.org/en-US/)
4 |
5 | 2. Install [Potrace](http://potrace.sourceforge.net/)
6 |
7 | 3. Install handwrite:
8 |
9 | ```console
10 | pip install handwrite
11 | ```
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Handwrite!
2 |
3 | ## Creating your Handwritten Sample
4 |
5 | 1. Take a printout of the [sample form](https://github.com/cod-ed/handwrite/raw/main/handwrite_sample.pdf).
6 |
7 | 2. Fill the form using the image below as a reference.
8 |
9 | 3. Scan the filled form using a scanner, or Adobe Scan in your phone.
10 |
11 | 4. Save the `.jpg` image in your system.
12 |
13 | Your form should look like this:
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Creating your font
21 |
22 | 1. Make sure you have installed `handwrite`, `potrace` & `fontforge`.
23 |
24 | 2. In a terminal type `handwrite [PATH TO IMAGE] [OUTPUT DIRECTORY]`.
25 | (You can also type `handwrite -h`, to see all the arguments you can use).
26 |
27 | 3. (Optional) Config file containing custom options for your font can also be passed using
28 | the `--config [CONFIG FILE]` argument.
29 |
30 | ???+ note
31 | - If you expicitly pass the metadata (filename, family or style) as CLI arguments, they are given a preference over the default config file data.
32 |
33 | - If no config file is provided for an input then the [default config file](https://github.com/cod-ed/handwrite/blob/main/handwrite/default.json) is used.
34 |
35 |
36 | 4. Your font will be created as `OUTPUT DIRECTORY/OUTPUT FONT NAME.ttf`. Install the font in your system.
37 |
38 | 5. Select your font in your word processor and get to work!
39 | Here's the end result!
40 |
41 |
42 |
43 |
44 |
45 |
46 | ## Configuring
47 |
48 | TO DO
--------------------------------------------------------------------------------
/handwrite/__init__.py:
--------------------------------------------------------------------------------
1 | from handwrite.sheettopng import SHEETtoPNG
2 | from handwrite.pngtosvg import PNGtoSVG
3 | from handwrite.svgtottf import SVGtoTTF
4 | from handwrite.cli import converters
5 |
--------------------------------------------------------------------------------
/handwrite/cli.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import argparse
4 | import tempfile
5 |
6 | from handwrite import SHEETtoPNG
7 | from handwrite import PNGtoSVG
8 | from handwrite import SVGtoTTF
9 |
10 |
11 | def run(sheet, output_directory, characters_dir, config, metadata):
12 | SHEETtoPNG().convert(sheet, characters_dir, config)
13 | PNGtoSVG().convert(directory=characters_dir)
14 | SVGtoTTF().convert(characters_dir, output_directory, config, metadata)
15 |
16 |
17 | def converters(sheet, output_directory, directory=None, config=None, metadata=None):
18 | if not directory:
19 | directory = tempfile.mkdtemp()
20 | isTempdir = True
21 | else:
22 | isTempdir = False
23 |
24 | if config is None:
25 | config = os.path.join(
26 | os.path.dirname(os.path.realpath(__file__)), "default.json"
27 | )
28 | if os.path.isdir(config):
29 | raise IsADirectoryError("Config parameter should not be a directory.")
30 |
31 | if os.path.isdir(sheet):
32 | raise IsADirectoryError("Sheet parameter should not be a directory.")
33 | else:
34 | run(sheet, output_directory, directory, config, metadata)
35 |
36 | if isTempdir:
37 | shutil.rmtree(directory)
38 |
39 |
40 | def main():
41 | parser = argparse.ArgumentParser()
42 | parser.add_argument("input_path", help="Path to sample sheet")
43 | parser.add_argument("output_directory", help="Directory Path to save font output")
44 | parser.add_argument(
45 | "--directory",
46 | help="Generate additional files to this path (Temp by default)",
47 | default=None,
48 | )
49 | parser.add_argument("--config", help="Use custom configuration file", default=None)
50 | parser.add_argument("--filename", help="Font File name", default=None)
51 | parser.add_argument("--family", help="Font Family name", default=None)
52 | parser.add_argument("--style", help="Font Style name", default=None)
53 |
54 | args = parser.parse_args()
55 | metadata = {"filename": args.filename, "family": args.family, "style": args.style}
56 | converters(
57 | args.input_path, args.output_directory, args.directory, args.config, metadata
58 | )
59 |
--------------------------------------------------------------------------------
/handwrite/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "threshold_value": 200,
3 | "props": {
4 | "ascent": 800,
5 | "descent": 200,
6 | "em": 1000,
7 | "encoding": "UnicodeFull",
8 | "lang": "English (US)",
9 | "filename": "MyFont",
10 | "style": "Regular"
11 | },
12 | "sfnt_names": {
13 | "Copyright": "Copyright (c) 2021 by Nobody",
14 | "Family": "MyFont",
15 | "SubFamily": "Regular",
16 | "UniqueID": "MyFont 2021-02-04",
17 | "Fullname": "MyFont Regular",
18 | "Version": "Version 1.0",
19 | "PostScriptName": "MyFont-Regular"
20 | },
21 | "glyphs": [
22 | 65,
23 | 66,
24 | 67,
25 | 68,
26 | 69,
27 | 70,
28 | 71,
29 | 72,
30 | 73,
31 | 74,
32 | 75,
33 | 76,
34 | 77,
35 | 78,
36 | 79,
37 | 80,
38 | 81,
39 | 82,
40 | 83,
41 | 84,
42 | 85,
43 | 86,
44 | 87,
45 | 88,
46 | 89,
47 | 90,
48 | 97,
49 | 98,
50 | 99,
51 | 100,
52 | 101,
53 | 102,
54 | 103,
55 | 104,
56 | 105,
57 | 106,
58 | 107,
59 | 108,
60 | 109,
61 | 110,
62 | 111,
63 | 112,
64 | 113,
65 | 114,
66 | 115,
67 | 116,
68 | 117,
69 | 118,
70 | 119,
71 | 120,
72 | 121,
73 | 122,
74 | 48,
75 | 49,
76 | 50,
77 | 51,
78 | 52,
79 | 53,
80 | 54,
81 | 55,
82 | 56,
83 | 57,
84 | 46,
85 | 44,
86 | 59,
87 | 58,
88 | 33,
89 | 63,
90 | 34,
91 | 39,
92 | 45,
93 | 43,
94 | 61,
95 | 47,
96 | 37,
97 | 38,
98 | 40,
99 | 41,
100 | 91,
101 | 93
102 | ],
103 | "typography_parameters": {
104 | "bearing_table": {
105 | "Default": [60, 60],
106 | "A": [60, -50],
107 | "a": [30, 40],
108 | "B": [60, 0],
109 | "C": [60, -30],
110 | "c": [null, 40],
111 | "b": [null, 40],
112 | "D": [null, 10],
113 | "d": [30, -20],
114 | "e": [30, 40],
115 | "E": [70, 10],
116 | "F": [70, 0],
117 | "f": [0, -20],
118 | "G": [60, 30],
119 | "g": [20, 60],
120 | "h": [40, 40],
121 | "I": [80, 50],
122 | "i": [null, 60],
123 | "J": [40, 30],
124 | "j": [-70, 40],
125 | "k": [40, 20],
126 | "K": [80, 0],
127 | "H": [null, 10],
128 | "L": [80, 10],
129 | "l": [null, 0],
130 | "M": [60, 30],
131 | "m": [40, null],
132 | "N": [70, 10],
133 | "n": [30, 40],
134 | "O": [70, 10],
135 | "o": [40, 40],
136 | "P": [70, 0],
137 | "p": [null, 40],
138 | "Q": [70, 10],
139 | "q": [20, 30],
140 | "R": [70, -10],
141 | "r": [null, 40],
142 | "S": [60, 60],
143 | "s": [20, 40],
144 | "T": [null, -10],
145 | "t": [-10, 20],
146 | "U": [70, 20],
147 | "u": [40, 40],
148 | "V": [null, -10],
149 | "v": [20, 20],
150 | "W": [70, 20],
151 | "w": [40, 40],
152 | "X": [null, -10],
153 | "x": [10, 20],
154 | "y": [20, 30],
155 | "Y": [40, 0],
156 | "Z": [null, -10],
157 | "z": [10, 20],
158 | "1": [-10, 30],
159 | "2": [-10, 30],
160 | "3": [10, 40],
161 | "4": [30, 30],
162 | "5": [30, 40],
163 | "6": [20, 20],
164 | "7": [30, 20],
165 | "8": [30, 20],
166 | "9": [30, 30],
167 | "0": [50, 40],
168 | ".": [null, 10],
169 | ",": [null, 10],
170 | ";": [null, 10],
171 | ":": [null, 20],
172 | "!": [null, 20],
173 | "?": [null, 30],
174 | "\"": [null, 20],
175 | "'": [null, 10],
176 | "-": [null, 20],
177 | "+": [null, 20],
178 | "=": [null, 20],
179 | "/": [null, 20],
180 | "%": [40, 40],
181 | "&": [40, 40],
182 | "(": [10, 10],
183 | ")": [10, 10],
184 | "[": [10, 10],
185 | "]": [10, 10]
186 | },
187 | "kerning_table": {
188 | "autokern": true,
189 | "seperation": 0,
190 | "rows": [
191 | null,
192 | "f-+=/?",
193 | "t",
194 | "i",
195 | "r",
196 | "k",
197 | "l.,;:!\"'()[]",
198 | "v",
199 | "bop%&",
200 | "nm",
201 | "a",
202 | "W",
203 | "T",
204 | "F",
205 | "P",
206 | "g",
207 | "qdhyj",
208 | "cesuwxz",
209 | "V",
210 | "A",
211 | "Y",
212 | "MNHI",
213 | "OQDU",
214 | "J",
215 | "C",
216 | "E",
217 | "L",
218 | "P",
219 | "KR",
220 | "G",
221 | "BSXZ"
222 | ],
223 | "cols": [
224 | null,
225 | "oacedgqw%&",
226 | "ft-+=/?",
227 | "xvz",
228 | "hbli.,;:!\"'()[]",
229 | "j",
230 | "mnpru",
231 | "k",
232 | "y",
233 | "s",
234 | "T",
235 | "F",
236 | "Zero"
237 | ],
238 | "table": [
239 | [
240 | [0, 0, 0, 0, 0, 0, 0, null, null, 0, 0, null, 0],
241 | [0, -30, -61, -20, null, 0, null, null, null, 0, -150, null, -70],
242 | [0, -50, -41, -20, null, 0, 0, null, null, 0, -150, null, -10],
243 | [
244 | null,
245 | null,
246 | -40,
247 | null,
248 | null,
249 | null,
250 | null,
251 | null,
252 | null,
253 | null,
254 | -150,
255 | null,
256 | null
257 | ],
258 | [0, -32, -40, null, null, 0, null, null, null, 0, -170, null, 29],
259 | [0, -10, -50, null, null, 0, null, null, null, -48, -150, null, -79],
260 | [0, -10, -20, null, 0, 0, 0, null, null, 0, -110, null, -20],
261 | [0, -40, -35, -15, null, 0, 0, null, null, 0, -170, null, 30],
262 | [0, null, -40, null, 0, 0, 0, null, null, 0, -170, null, 43],
263 | [
264 | null,
265 | null,
266 | -30,
267 | null,
268 | null,
269 | null,
270 | null,
271 | null,
272 | null,
273 | null,
274 | -170,
275 | null,
276 | null
277 | ],
278 | [0, -23, -30, null, 0, 0, 0, null, null, 0, -170, null, 7],
279 | [0, -40, -30, -10, null, 0, 0, null, null, 0, null, null, null],
280 | [0, -150, -120, -120, -30, -40, -130, null, -100, -80, 0, null, null],
281 | [0, -90, -90, -70, -30, 0, -70, null, -50, -80, -40, null, null],
282 | [0, -100, -70, -50, null, 0, -70, null, -30, -80, -20, null, null],
283 | [
284 | null,
285 | null,
286 | null,
287 | null,
288 | null,
289 | 40,
290 | null,
291 | null,
292 | null,
293 | null,
294 | -120,
295 | null,
296 | null
297 | ],
298 | [null, null, null, null, 30, 30, 30, 30, 30, null, -100, null, null],
299 | [
300 | null,
301 | null,
302 | null,
303 | null,
304 | null,
305 | null,
306 | null,
307 | null,
308 | null,
309 | null,
310 | -120,
311 | null,
312 | null
313 | ],
314 | [null, -70, 30, 30, null, -80, -20, null, -40, -40, -10, null, null],
315 | [null, 30, 60, 30, 30, null, 20, 40, 20, -80, -120, 20, 20],
316 | [null, 20, 60, 30, 30, null, 20, 20, 40, 20, -10, null, null],
317 | [null, 20, 10, 40, 30, null, 10, 20, 20, null, null, null, null],
318 | [null, null, 50, 40, 30, -20, 30, 20, 30, null, -70, null, null],
319 | [null, null, 40, 20, 20, -20, 10, 10, 30, null, -30, null, null],
320 | [null, 10, 40, 10, 30, null, 30, 30, 20, null, -30, null, null],
321 | [null, -10, 50, null, 10, -20, 10, null, 20, null, null, null, null],
322 | [
323 | null,
324 | -10,
325 | -10,
326 | null,
327 | null,
328 | -30,
329 | null,
330 | null,
331 | 20,
332 | null,
333 | -90,
334 | null,
335 | null
336 | ],
337 | [null, -50, 30, 20, 20, null, null, 20, 20, null, -30, null, null],
338 | [null, 20, 20, 20, 10, null, 20, 20, 20, null, -60, null, null],
339 | [null, 20, 40, 30, 30, null, 20, 20, 20, null, -100, 10, null],
340 | [null, 20, 40, 30, 30, null, 20, 20, 20, 20, -20, 10, null]
341 | ]
342 | ]
343 | }
344 | },
345 | "# vim: set et sw=2 ts=2 sts=2:": false
346 | }
347 |
--------------------------------------------------------------------------------
/handwrite/pngtosvg.py:
--------------------------------------------------------------------------------
1 | from PIL import Image, ImageChops
2 | import os
3 | import shutil
4 | import subprocess
5 |
6 |
7 | class PotraceNotFound(Exception):
8 | pass
9 |
10 |
11 | class PNGtoSVG:
12 | """Converter class to convert character PNGs to BMPs and SVGs."""
13 |
14 | def convert(self, directory):
15 | """Call converters on each .png in the provider directory.
16 |
17 | Walk through the custom directory containing all .png files
18 | from sheettopng and convert them to png -> bmp -> svg.
19 | """
20 | path = os.walk(directory)
21 | for root, dirs, files in path:
22 | for f in files:
23 | if f.endswith(".png"):
24 | self.pngToBmp(root + "/" + f)
25 | # self.trim(root + "/" + f[0:-4] + ".bmp")
26 | self.bmpToSvg(root + "/" + f[0:-4] + ".bmp")
27 |
28 | def bmpToSvg(self, path):
29 | """Convert .bmp image to .svg using potrace.
30 |
31 | Converts the passed .bmp file to .svg using the potrace
32 | (http://potrace.sourceforge.net/). Each .bmp is passed as
33 | a parameter to potrace which is called as a subprocess.
34 |
35 | Parameters
36 | ----------
37 | path : str
38 | Path to the bmp file to be converted.
39 |
40 | Raises
41 | ------
42 | PotraceNotFound
43 | Raised if potrace not found in path by shutil.which()
44 | """
45 | if shutil.which("potrace") is None:
46 | raise PotraceNotFound("Potrace is either not installed or not in path")
47 | else:
48 | subprocess.run(["potrace", path, "-b", "svg", "-o", path[0:-4] + ".svg"])
49 |
50 | def pngToBmp(self, path):
51 | """Convert .bmp image to .svg using potrace.
52 |
53 | Converts the passed .bmp file to .svg using the potrace
54 | (http://potrace.sourceforge.net/). Each .bmp is passed as
55 | a parameter to potrace which is called as a subprocess.
56 |
57 | Parameters
58 | ----------
59 | path : str
60 | Path to the bmp file to be converted.
61 |
62 | Raises
63 | ------
64 | PotraceNotFound
65 | Raised if potrace not found in path by shutil.which()
66 | """
67 | img = Image.open(path).convert("RGBA").resize((100, 100))
68 |
69 | # Threshold image to convert each pixel to either black or white
70 | threshold = 200
71 | data = []
72 | for pix in list(img.getdata()):
73 | if pix[0] >= threshold and pix[1] >= threshold and pix[3] >= threshold:
74 | data.append((255, 255, 255, 0))
75 | else:
76 | data.append((0, 0, 0, 1))
77 | img.putdata(data)
78 | img.save(path[0:-4] + ".bmp")
79 |
80 | def trim(self, im_path):
81 | im = Image.open(im_path)
82 | bg = Image.new(im.mode, im.size, im.getpixel((0, 0)))
83 | diff = ImageChops.difference(im, bg)
84 | bbox = list(diff.getbbox())
85 | bbox[0] -= 1
86 | bbox[1] -= 1
87 | bbox[2] += 1
88 | bbox[3] += 1
89 | cropped_im = im.crop(bbox)
90 | cropped_im.save(im_path)
91 |
--------------------------------------------------------------------------------
/handwrite/sheettopng.py:
--------------------------------------------------------------------------------
1 | import os
2 | import itertools
3 | import json
4 |
5 | import cv2
6 |
7 | # Seq: A-Z, a-z, 0-9, SPECIAL_CHARS
8 | ALL_CHARS = list(
9 | itertools.chain(
10 | range(65, 91),
11 | range(97, 123),
12 | range(48, 58),
13 | [ord(i) for i in ".,;:!?\"'-+=/%&()[]"],
14 | )
15 | )
16 |
17 |
18 | class SHEETtoPNG:
19 | """Converter class to convert input sample sheet to character PNGs."""
20 |
21 | def convert(self, sheet, characters_dir, config, cols=8, rows=10):
22 | """Convert a sheet of sample writing input to a custom directory structure of PNGs.
23 |
24 | Detect all characters in the sheet as a separate contours and convert each to
25 | a PNG image in a temp/user provided directory.
26 |
27 | Parameters
28 | ----------
29 | sheet : str
30 | Path to the sheet file to be converted.
31 | characters_dir : str
32 | Path to directory to save characters in.
33 | config: str
34 | Path to config file.
35 | cols : int, default=8
36 | Number of columns of expected contours. Defaults to 8 based on the default sample.
37 | rows : int, default=10
38 | Number of rows of expected contours. Defaults to 10 based on the default sample.
39 | """
40 | with open(config) as f:
41 | threshold_value = json.load(f).get("threshold_value", 200)
42 | if os.path.isdir(sheet):
43 | raise IsADirectoryError("Sheet parameter should not be a directory.")
44 | characters = self.detect_characters(
45 | sheet, threshold_value, cols=cols, rows=rows
46 | )
47 | self.save_images(
48 | characters,
49 | characters_dir,
50 | )
51 |
52 | def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10):
53 | """Detect contours on the input image and filter them to get only characters.
54 |
55 | Uses opencv to threshold the image for better contour detection. After finding all
56 | contours, they are filtered based on area, cropped and then sorted sequentially based
57 | on coordinates. Finally returs the cols*rows top candidates for being the character
58 | containing contours.
59 |
60 | Parameters
61 | ----------
62 | sheet_image : str
63 | Path to the sheet file to be converted.
64 | threshold_value : int
65 | Value to adjust thresholding of the image for better contour detection.
66 | cols : int, default=8
67 | Number of columns of expected contours. Defaults to 8 based on the default sample.
68 | rows : int, default=10
69 | Number of rows of expected contours. Defaults to 10 based on the default sample.
70 |
71 | Returns
72 | -------
73 | sorted_characters : list of list
74 | Final rows*cols contours in form of list of list arranged as:
75 | sorted_characters[x][y] denotes contour at x, y position in the input grid.
76 | """
77 | # TODO Raise errors and suggest where the problem might be
78 |
79 | # Read the image and convert to grayscale
80 | image = cv2.imread(sheet_image)
81 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
82 |
83 | # Threshold and filter the image for better contour detection
84 | _, thresh = cv2.threshold(gray, threshold_value, 255, 1)
85 | close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
86 | close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2)
87 |
88 | # Search for contours.
89 | contours, h = cv2.findContours(
90 | close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
91 | )
92 |
93 | # Filter contours based on number of sides and then reverse sort by area.
94 | contours = sorted(
95 | filter(
96 | lambda cnt: len(
97 | cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True)
98 | )
99 | == 4,
100 | contours,
101 | ),
102 | key=cv2.contourArea,
103 | reverse=True,
104 | )
105 |
106 | # Calculate the bounding of the first contour and approximate the height
107 | # and width for final cropping.
108 | x, y, w, h = cv2.boundingRect(contours[0])
109 | space_h, space_w = 7 * h // 16, 7 * w // 16
110 |
111 | # Since amongst all the contours, the expected case is that the 4 sided contours
112 | # containing the characters should have the maximum area, so we loop through the first
113 | # rows*colums contours and add them to final list after cropping.
114 | characters = []
115 | for i in range(rows * cols):
116 | x, y, w, h = cv2.boundingRect(contours[i])
117 | cx, cy = x + w // 2, y + h // 2
118 |
119 | roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w]
120 | characters.append([roi, cx, cy])
121 |
122 | # Now we have the characters but since they are all mixed up we need to position them.
123 | # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then
124 | # sort each group based on the 'x' coordinate.
125 | characters.sort(key=lambda x: x[2])
126 | sorted_characters = []
127 | for k in range(rows):
128 | sorted_characters.extend(
129 | sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1])
130 | )
131 |
132 | return sorted_characters
133 |
134 | def save_images(self, characters, characters_dir):
135 | """Create directory for each character and save as PNG.
136 |
137 | Creates directory and PNG file for each image as following:
138 |
139 | characters_dir/ord(character)/ord(character).png (SINGLE SHEET INPUT)
140 | characters_dir/sheet_filename/ord(character)/ord(character).png (MULTIPLE SHEETS INPUT)
141 |
142 | Parameters
143 | ----------
144 | characters : list of list
145 | Sorted list of character images each inner list representing a row of images.
146 | characters_dir : str
147 | Path to directory to save characters in.
148 | """
149 | os.makedirs(characters_dir, exist_ok=True)
150 |
151 | # Create directory for each character and save the png for the characters
152 | # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).png
153 | # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).png
154 | for k, images in enumerate(characters):
155 | character = os.path.join(characters_dir, str(ALL_CHARS[k]))
156 | if not os.path.exists(character):
157 | os.mkdir(character)
158 | cv2.imwrite(
159 | os.path.join(character, str(ALL_CHARS[k]) + ".png"),
160 | images[0],
161 | )
162 |
--------------------------------------------------------------------------------
/handwrite/svgtottf.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import json
4 | import uuid
5 |
6 |
7 | class SVGtoTTF:
8 | def convert(self, directory, outdir, config, metadata=None):
9 | """Convert a directory with SVG images to TrueType Font.
10 |
11 | Calls a subprocess to the run this script with Fontforge Python
12 | environment.
13 |
14 | Parameters
15 | ----------
16 | directory : str
17 | Path to directory with SVGs to be converted.
18 | outdir : str
19 | Path to output directory.
20 | config : str
21 | Path to config file.
22 | metadata : dict
23 | Dictionary containing the metadata (filename, family or style)
24 | """
25 | import subprocess
26 | import platform
27 |
28 | subprocess.run(
29 | (
30 | ["ffpython"]
31 | if platform.system() == "Windows"
32 | else ["fontforge", "-script"]
33 | )
34 | + [
35 | os.path.abspath(__file__),
36 | config,
37 | directory,
38 | outdir,
39 | json.dumps(metadata),
40 | ]
41 | )
42 |
43 | def set_properties(self):
44 | """Set metadata of the font from config."""
45 | props = self.config["props"]
46 | lang = props.get("lang", "English (US)")
47 | fontname = self.metadata.get("filename", None) or props.get(
48 | "filename", "Example"
49 | )
50 | family = self.metadata.get("family", None) or fontname
51 | style = self.metadata.get("style", None) or props.get("style", "Regular")
52 |
53 | self.font.familyname = fontname
54 | self.font.fontname = fontname + "-" + style
55 | self.font.fullname = fontname + " " + style
56 | self.font.encoding = props.get("encoding", "UnicodeFull")
57 |
58 | for k, v in props.items():
59 | if hasattr(self.font, k):
60 | if isinstance(v, list):
61 | v = tuple(v)
62 | setattr(self.font, k, v)
63 |
64 | if self.config.get("sfnt_names", None):
65 | self.config["sfnt_names"]["Family"] = family
66 | self.config["sfnt_names"]["Fullname"] = family + " " + style
67 | self.config["sfnt_names"]["PostScriptName"] = family + "-" + style
68 | self.config["sfnt_names"]["SubFamily"] = style
69 |
70 | self.config["sfnt_names"]["UniqueID"] = family + " " + str(uuid.uuid4())
71 |
72 | for k, v in self.config.get("sfnt_names", {}).items():
73 | self.font.appendSFNTName(str(lang), str(k), str(v))
74 |
75 | def add_glyphs(self, directory):
76 | """Read and add SVG images as glyphs to the font.
77 |
78 | Walks through the provided directory and uses each ord(character).svg file
79 | as glyph for the character. Then using the provided config, set the font
80 | parameters and export TTF file to outdir.
81 |
82 | Parameters
83 | ----------
84 | directory : str
85 | Path to directory with SVGs to be converted.
86 | """
87 | space = self.font.createMappedChar(ord(" "))
88 | space.width = 500
89 |
90 | for k in self.config["glyphs"]:
91 | # Create character glyph
92 | g = self.font.createMappedChar(k)
93 | self.unicode_mapping.setdefault(k, g.glyphname)
94 | # Get outlines
95 | src = "{}/{}.svg".format(k, k)
96 | src = directory + os.sep + src
97 | g.importOutlines(src, ("removeoverlap", "correctdir"))
98 | g.removeOverlap()
99 |
100 | def set_bearings(self, bearings):
101 | """Add left and right bearing from config
102 |
103 | Parameters
104 | ----------
105 | bearings : dict
106 | Map from character: [left bearing, right bearing]
107 | """
108 | default = bearings.get("Default", [60, 60])
109 |
110 | for k, v in bearings.items():
111 | if v[0] is None:
112 | v[0] = default[0]
113 | if v[1] is None:
114 | v[1] = default[1]
115 |
116 | if k != "Default":
117 | glyph_name = self.unicode_mapping[ord(str(k))]
118 | self.font[glyph_name].left_side_bearing = v[0]
119 | self.font[glyph_name].right_side_bearing = v[1]
120 |
121 | def set_kerning(self, table):
122 | """Set kerning values in the font.
123 |
124 | Parameters
125 | ----------
126 | table : dict
127 | Config dictionary with kerning values/autokern bool.
128 | """
129 | rows = table["rows"]
130 | rows = [list(i) if i != None else None for i in rows]
131 | cols = table["cols"]
132 | cols = [list(i) if i != None else None for i in cols]
133 |
134 | self.font.addLookup("kern", "gpos_pair", 0, [["kern", [["latn", ["dflt"]]]]])
135 |
136 | if table.get("autokern", True):
137 | self.font.addKerningClass(
138 | "kern", "kern-1", table.get("seperation", 0), rows, cols, True
139 | )
140 | else:
141 | kerning_table = table.get("table", False)
142 | if not kerning_table:
143 | raise ValueError("Kerning offsets not found in the config file.")
144 | flatten_list = (
145 | lambda y: [x for a in y for x in flatten_list(a)]
146 | if type(y) is list
147 | else [y]
148 | )
149 | offsets = [0 if x is None else x for x in flatten_list(kerning_table)]
150 | self.font.addKerningClass("kern", "kern-1", rows, cols, offsets)
151 |
152 | def generate_font_file(self, filename, outdir, config_file):
153 | """Output TTF file.
154 |
155 | Additionally checks for multiple outputs and duplicates.
156 |
157 | Parameters
158 | ----------
159 | filename : str
160 | Output filename.
161 | outdir : str
162 | Path to output directory.
163 | config_file : str
164 | Path to config file.
165 | """
166 | if filename is None:
167 | raise NameError("filename not found in config file.")
168 |
169 | outfile = str(
170 | outdir
171 | + os.sep
172 | + (filename + ".ttf" if not filename.endswith(".ttf") else filename)
173 | )
174 |
175 | while os.path.exists(outfile):
176 | outfile = os.path.splitext(outfile)[0] + " (1).ttf"
177 |
178 | sys.stderr.write("\nGenerating %s...\n" % outfile)
179 | self.font.generate(outfile)
180 |
181 | def convert_main(self, config_file, directory, outdir, metadata):
182 | try:
183 | self.font = fontforge.font()
184 | except:
185 | import fontforge
186 |
187 | with open(config_file) as f:
188 | self.config = json.load(f)
189 | self.metadata = json.loads(metadata) or {}
190 |
191 | self.font = fontforge.font()
192 | self.unicode_mapping = {}
193 | self.set_properties()
194 | self.add_glyphs(directory)
195 |
196 | # bearing table
197 | self.set_bearings(self.config["typography_parameters"].get("bearing_table", {}))
198 |
199 | # kerning table
200 | self.set_kerning(self.config["typography_parameters"].get("kerning_table", {}))
201 |
202 | # Generate font and save as a .ttf file
203 | filename = self.metadata.get("filename", None) or self.config["props"].get(
204 | "filename", None
205 | )
206 | self.generate_font_file(str(filename), outdir, config_file)
207 |
208 |
209 | if __name__ == "__main__":
210 | if len(sys.argv) != 5:
211 | raise ValueError("Incorrect call to SVGtoTTF")
212 | SVGtoTTF().convert_main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
213 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: 'Handwrite'
2 | site_description: 'Official Site for Handwrite'
3 | site_author: 'Team Handwrite'
4 | site_url: 'https://cod-ed.github.io/handwrite'
5 |
6 | # Repository
7 | repo_name: 'cod-ed/handwrite'
8 | repo_url: 'https://github.com/cod-ed/handwrite'
9 |
10 | nav:
11 | - Home: 'index.md'
12 | - Installation: 'installation.md'
13 | - Usage: 'usage.md'
14 | - Contributing: 'contributing.md'
15 | - Credits: 'credits.md'
16 | - Documentation:
17 | - Converters:
18 | - SHEETtoPNG: 'api/sheettopng.md'
19 | - PNGtoSVG: 'api/pngtosvg.md'
20 | - SVGtoTTF: 'api/svgtottf.md'
21 |
22 | theme:
23 | name: material
24 | features:
25 | - navigation.sections
26 | # - navigation.tabs
27 | palette:
28 | primary: 'black'
29 | accent: 'white'
30 | font:
31 | text: 'Ubuntu'
32 | code: 'Ubuntu Mono'
33 | icon:
34 | logo: fontawesome/solid/pen-square
35 |
36 | plugins:
37 | - mkdocstrings
38 |
39 | markdown_extensions:
40 | - admonition
41 | - codehilite:
42 | guess_lang: false
43 | - toc:
44 | permalink: true
45 | - pymdownx.arithmatex
46 | - pymdownx.betterem:
47 | smart_enable: all
48 | - pymdownx.caret
49 | - pymdownx.critic
50 | - pymdownx.details
51 | - pymdownx.inlinehilite
52 | - pymdownx.magiclink
53 | - pymdownx.mark
54 | - pymdownx.smartsymbols
55 | - pymdownx.superfences
56 | - footnotes
57 | - pymdownx.tasklist:
58 | custom_checkbox: true
59 | - pymdownx.tabbed
60 | - pymdownx.tilde
61 | - attr_list
62 |
63 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r", encoding="utf-8") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="handwrite",
8 | version="0.3.0",
9 | author="Yash Lamba, Saksham Arora, Aryan Gupta",
10 | author_email="yashlamba2000@gmail.com, sakshamarora1001@gmail.com, aryangupta973@gmail.com",
11 | description="Convert text to custom handwriting",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/cod-ed/handwrite",
15 | packages=setuptools.find_packages(),
16 | install_requires=["opencv-python", "Pillow"],
17 | extras_require={
18 | "dev": [
19 | "pre-commit",
20 | "black",
21 | "mkdocs==1.2.2",
22 | "mkdocs-material==6.1.0",
23 | "pymdown-extensions==8.2",
24 | "mkdocstrings>=0.16.1",
25 | "pytkdocs[numpy-style]",
26 | ]
27 | },
28 | entry_points={
29 | "console_scripts": ["handwrite = handwrite.cli:main"],
30 | },
31 | include_package_data=True,
32 | classifiers=[
33 | "Programming Language :: Python :: 3",
34 | "License :: OSI Approved :: MIT License",
35 | "Operating System :: OS Independent",
36 | ],
37 | python_requires=">=3.7",
38 | )
39 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | import unittest
5 | import subprocess
6 | import filecmp
7 |
8 | from handwrite.sheettopng import ALL_CHARS
9 |
10 |
11 | class TestCLI(unittest.TestCase):
12 | def setUp(self):
13 | self.file_dir = os.path.dirname(os.path.abspath(__file__))
14 | self.temp_dir = tempfile.mkdtemp()
15 | self.sheets_dir = os.path.join(self.file_dir, "test_data", "sheettopng")
16 |
17 | def tearDown(self):
18 | shutil.rmtree(self.temp_dir)
19 |
20 | def test_single_input(self):
21 | # Check working with excellent input and no optional parameters
22 | subprocess.call(
23 | [
24 | "handwrite",
25 | os.path.join(self.file_dir, "test_data", "sheettopng", "excellent.jpg"),
26 | self.temp_dir,
27 | ]
28 | )
29 | self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "MyFont.ttf")))
30 |
31 | def test_single_input_with_optional_parameters(self):
32 | # Check working with optional parameters
33 | subprocess.call(
34 | [
35 | "handwrite",
36 | os.path.join(self.file_dir, "test_data", "sheettopng", "excellent.jpg"),
37 | self.temp_dir,
38 | "--directory",
39 | self.temp_dir,
40 | "--config",
41 | os.path.join(self.file_dir, "test_data", "config_data", "default.json"),
42 | "--filename",
43 | "CustomFont",
44 | ]
45 | )
46 | for i in ALL_CHARS:
47 | for suffix in [".bmp", ".png", ".svg"]:
48 | self.assertTrue(
49 | os.path.exists(os.path.join(self.temp_dir, f"{i}", f"{i}{suffix}"))
50 | )
51 | self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "CustomFont.ttf")))
52 |
53 | def test_multiple_inputs(self):
54 | # Check working with multiple inputs
55 | try:
56 | subprocess.check_call(
57 | [
58 | "handwrite",
59 | self.sheets_dir,
60 | self.temp_dir,
61 | ]
62 | )
63 | except subprocess.CalledProcessError as e:
64 | self.assertNotEqual(e.returncode, 0)
65 |
66 | def test_multiple_config(self):
67 | # Check working with multiple config files
68 | try:
69 | subprocess.check_call(
70 | [
71 | "handwrite",
72 | self.sheets_dir,
73 | self.temp_dir,
74 | "--config",
75 | os.path.join(self.file_dir, "test_data", "config_data"),
76 | ]
77 | )
78 | except subprocess.CalledProcessError as e:
79 | self.assertNotEqual(e.returncode, 0)
80 |
--------------------------------------------------------------------------------
/tests/test_data/config_data/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "threshold_value": 200,
3 | "props": {
4 | "ascent": 800,
5 | "descent": 200,
6 | "em": 1000,
7 | "encoding": "UnicodeFull",
8 | "lang": "English (US)",
9 | "filename": "MyFont",
10 | "style": "Regular"
11 | },
12 | "sfnt_names": {
13 | "Copyright": "Copyright (c) 2021 by Nobody",
14 | "Family": "MyFont",
15 | "SubFamily": "Regular",
16 | "UniqueID": "MyFont 2021-02-04",
17 | "Fullname": "MyFont Regular",
18 | "Version": "Version 001.000",
19 | "PostScriptName": "MyFont-Regular"
20 | },
21 | "glyphs": [
22 | 65,
23 | 66,
24 | 67,
25 | 68,
26 | 69,
27 | 70,
28 | 71,
29 | 72,
30 | 73,
31 | 74,
32 | 75,
33 | 76,
34 | 77,
35 | 78,
36 | 79,
37 | 80,
38 | 81,
39 | 82,
40 | 83,
41 | 84,
42 | 85,
43 | 86,
44 | 87,
45 | 88,
46 | 89,
47 | 90,
48 | 97,
49 | 98,
50 | 99,
51 | 100,
52 | 101,
53 | 102,
54 | 103,
55 | 104,
56 | 105,
57 | 106,
58 | 107,
59 | 108,
60 | 109,
61 | 110,
62 | 111,
63 | 112,
64 | 113,
65 | 114,
66 | 115,
67 | 116,
68 | 117,
69 | 118,
70 | 119,
71 | 120,
72 | 121,
73 | 122,
74 | 48,
75 | 49,
76 | 50,
77 | 51,
78 | 52,
79 | 53,
80 | 54,
81 | 55,
82 | 56,
83 | 57,
84 | 46,
85 | 44,
86 | 59,
87 | 58,
88 | 33,
89 | 63,
90 | 34,
91 | 39,
92 | 45,
93 | 43,
94 | 61,
95 | 47,
96 | 37,
97 | 38,
98 | 40,
99 | 41,
100 | 91,
101 | 93
102 | ],
103 | "typography_parameters": {
104 | "bearing_table": {
105 | "Default": [60, 60],
106 | "A": [60, -50],
107 | "a": [30, 40],
108 | "B": [60, 0],
109 | "C": [60, -30],
110 | "c": [null, 40],
111 | "b": [null, 40],
112 | "D": [null, 10],
113 | "d": [30, -20],
114 | "e": [30, 40],
115 | "E": [70, 10],
116 | "F": [70, 0],
117 | "f": [0, -20],
118 | "G": [60, 30],
119 | "g": [20, 60],
120 | "h": [40, 40],
121 | "I": [80, 50],
122 | "i": [null, 60],
123 | "J": [40, 30],
124 | "j": [-70, 40],
125 | "k": [40, 20],
126 | "K": [80, 0],
127 | "H": [null, 10],
128 | "L": [80, 10],
129 | "l": [null, 0],
130 | "M": [60, 30],
131 | "m": [40, null],
132 | "N": [70, 10],
133 | "n": [30, 40],
134 | "O": [70, 10],
135 | "o": [40, 40],
136 | "P": [70, 0],
137 | "p": [null, 40],
138 | "Q": [70, 10],
139 | "q": [20, 30],
140 | "R": [70, -10],
141 | "r": [null, 40],
142 | "S": [60, 60],
143 | "s": [20, 40],
144 | "T": [null, -10],
145 | "t": [-10, 20],
146 | "U": [70, 20],
147 | "u": [40, 40],
148 | "V": [null, -10],
149 | "v": [20, 20],
150 | "W": [70, 20],
151 | "w": [40, 40],
152 | "X": [null, -10],
153 | "x": [10, 20],
154 | "y": [20, 30],
155 | "Y": [40, 0],
156 | "Z": [null, -10],
157 | "z": [10, 20],
158 | "1": [-10, 30],
159 | "2": [-10, 30],
160 | "3": [10, 40],
161 | "4": [30, 30],
162 | "5": [30, 40],
163 | "6": [20, 20],
164 | "7": [30, 20],
165 | "8": [30, 20],
166 | "9": [30, 30],
167 | "0": [50, 40],
168 | ".": [null, 10],
169 | ",": [null, 10],
170 | ";": [null, 10],
171 | ":": [null, 20],
172 | "!": [null, 20],
173 | "?": [null, 30],
174 | "\"": [null, 20],
175 | "'": [null, 10],
176 | "-": [null, 20],
177 | "+": [null, 20],
178 | "=": [null, 20],
179 | "/": [null, 20],
180 | "%": [40, 40],
181 | "&": [40, 40],
182 | "(": [10, 10],
183 | ")": [10, 10],
184 | "[": [10, 10],
185 | "]": [10, 10]
186 | },
187 | "kerning_table": {
188 | "autokern": true,
189 | "seperation": 0,
190 | "rows": [
191 | null,
192 | "f-+=/?",
193 | "t",
194 | "i",
195 | "r",
196 | "k",
197 | "l.,;:!\"'()[]",
198 | "v",
199 | "bop%&",
200 | "nm",
201 | "a",
202 | "W",
203 | "T",
204 | "F",
205 | "P",
206 | "g",
207 | "qdhyj",
208 | "cesuwxz",
209 | "V",
210 | "A",
211 | "Y",
212 | "MNHI",
213 | "OQDU",
214 | "J",
215 | "C",
216 | "E",
217 | "L",
218 | "P",
219 | "KR",
220 | "G",
221 | "BSXZ"
222 | ],
223 | "cols": [
224 | null,
225 | "oacedgqw%&",
226 | "ft-+=/?",
227 | "xvz",
228 | "hbli.,;:!\"'()[]",
229 | "j",
230 | "mnpru",
231 | "k",
232 | "y",
233 | "s",
234 | "T",
235 | "F",
236 | "Zero"
237 | ],
238 | "table": [
239 | [
240 | [0, 0, 0, 0, 0, 0, 0, null, null, 0, 0, null, 0],
241 | [0, -30, -61, -20, null, 0, null, null, null, 0, -150, null, -70],
242 | [0, -50, -41, -20, null, 0, 0, null, null, 0, -150, null, -10],
243 | [
244 | null,
245 | null,
246 | -40,
247 | null,
248 | null,
249 | null,
250 | null,
251 | null,
252 | null,
253 | null,
254 | -150,
255 | null,
256 | null
257 | ],
258 | [0, -32, -40, null, null, 0, null, null, null, 0, -170, null, 29],
259 | [0, -10, -50, null, null, 0, null, null, null, -48, -150, null, -79],
260 | [0, -10, -20, null, 0, 0, 0, null, null, 0, -110, null, -20],
261 | [0, -40, -35, -15, null, 0, 0, null, null, 0, -170, null, 30],
262 | [0, null, -40, null, 0, 0, 0, null, null, 0, -170, null, 43],
263 | [
264 | null,
265 | null,
266 | -30,
267 | null,
268 | null,
269 | null,
270 | null,
271 | null,
272 | null,
273 | null,
274 | -170,
275 | null,
276 | null
277 | ],
278 | [0, -23, -30, null, 0, 0, 0, null, null, 0, -170, null, 7],
279 | [0, -40, -30, -10, null, 0, 0, null, null, 0, null, null, null],
280 | [0, -150, -120, -120, -30, -40, -130, null, -100, -80, 0, null, null],
281 | [0, -90, -90, -70, -30, 0, -70, null, -50, -80, -40, null, null],
282 | [0, -100, -70, -50, null, 0, -70, null, -30, -80, -20, null, null],
283 | [
284 | null,
285 | null,
286 | null,
287 | null,
288 | null,
289 | 40,
290 | null,
291 | null,
292 | null,
293 | null,
294 | -120,
295 | null,
296 | null
297 | ],
298 | [null, null, null, null, 30, 30, 30, 30, 30, null, -100, null, null],
299 | [
300 | null,
301 | null,
302 | null,
303 | null,
304 | null,
305 | null,
306 | null,
307 | null,
308 | null,
309 | null,
310 | -120,
311 | null,
312 | null
313 | ],
314 | [null, -70, 30, 30, null, -80, -20, null, -40, -40, -10, null, null],
315 | [null, 30, 60, 30, 30, null, 20, 40, 20, -80, -120, 20, 20],
316 | [null, 20, 60, 30, 30, null, 20, 20, 40, 20, -10, null, null],
317 | [null, 20, 10, 40, 30, null, 10, 20, 20, null, null, null, null],
318 | [null, null, 50, 40, 30, -20, 30, 20, 30, null, -70, null, null],
319 | [null, null, 40, 20, 20, -20, 10, 10, 30, null, -30, null, null],
320 | [null, 10, 40, 10, 30, null, 30, 30, 20, null, -30, null, null],
321 | [null, -10, 50, null, 10, -20, 10, null, 20, null, null, null, null],
322 | [
323 | null,
324 | -10,
325 | -10,
326 | null,
327 | null,
328 | -30,
329 | null,
330 | null,
331 | 20,
332 | null,
333 | -90,
334 | null,
335 | null
336 | ],
337 | [null, -50, 30, 20, 20, null, null, 20, 20, null, -30, null, null],
338 | [null, 20, 20, 20, 10, null, 20, 20, 20, null, -60, null, null],
339 | [null, 20, 40, 30, 30, null, 20, 20, 20, null, -100, 10, null],
340 | [null, 20, 40, 30, 30, null, 20, 20, 20, 20, -20, 10, null]
341 | ]
342 | ]
343 | }
344 | },
345 | "# vim: set et sw=2 ts=2 sts=2:": false
346 | }
347 |
--------------------------------------------------------------------------------
/tests/test_data/pngtosvg/33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/33.png
--------------------------------------------------------------------------------
/tests/test_data/pngtosvg/34/34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/34/34.png
--------------------------------------------------------------------------------
/tests/test_data/pngtosvg/45.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/pngtosvg/45.bmp
--------------------------------------------------------------------------------
/tests/test_data/sheettopng/excellent.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cod-ed/handwrite/440f59153fe02fc96503fd87f9c64b105c8ceb71/tests/test_data/sheettopng/excellent.jpg
--------------------------------------------------------------------------------
/tests/test_pngtosvg.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | from handwrite.pngtosvg import PNGtoSVG
5 |
6 |
7 | class TestPNGtoSVG(unittest.TestCase):
8 | def setUp(self):
9 | self.directory = os.path.join(
10 | os.path.dirname(os.path.abspath(__file__)),
11 | "test_data" + os.sep + "pngtosvg",
12 | )
13 | self.converter = PNGtoSVG()
14 |
15 | def test_bmpToSvg(self):
16 | self.converter.bmpToSvg(self.directory + os.sep + "45.bmp")
17 | self.assertTrue(os.path.exists(self.directory + os.sep + "45.svg"))
18 | os.remove(self.directory + os.sep + "45.svg")
19 |
20 | def test_convert(self):
21 | self.converter.convert(self.directory)
22 | path = os.walk(self.directory)
23 | for root, dirs, files in path:
24 | for f in files:
25 | if f[-4:] == ".png":
26 | self.assertTrue(os.path.exists(root + os.sep + f[0:-4] + ".bmp"))
27 | self.assertTrue(os.path.exists(root + os.sep + f[0:-4] + ".svg"))
28 | os.remove(root + os.sep + f[0:-4] + ".bmp")
29 | os.remove(root + os.sep + f[0:-4] + ".svg")
30 |
--------------------------------------------------------------------------------
/tests/test_sheettopng.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | import unittest
5 |
6 | from handwrite.sheettopng import SHEETtoPNG, ALL_CHARS
7 |
8 |
9 | class TestSHEETtoPNG(unittest.TestCase):
10 | def setUp(self):
11 | self.directory = tempfile.mkdtemp()
12 | self.sheets_path = os.path.join(
13 | os.path.dirname(os.path.abspath(__file__)),
14 | "test_data" + os.sep + "sheettopng",
15 | )
16 | self.converter = SHEETtoPNG()
17 |
18 | def tearDown(self):
19 | shutil.rmtree(self.directory)
20 |
21 | def test_convert(self):
22 | # Single sheet input
23 | excellent_scan = os.path.join(self.sheets_path, "excellent.jpg")
24 | config = os.path.join(
25 | os.path.dirname(os.path.abspath(__file__)),
26 | "test_data",
27 | "config_data",
28 | "default.json",
29 | )
30 | self.converter.convert(excellent_scan, self.directory, config)
31 | for i in ALL_CHARS:
32 | self.assertTrue(
33 | os.path.exists(os.path.join(self.directory, f"{i}", f"{i}.png"))
34 | )
35 |
36 | # TODO Once all the errors are done for detect_characters
37 | # Write tests to check each kind of scan and whether it raises
38 | # helpful errors, Boilerplate below:
39 | # def test_detect_characters(self):
40 | # scans = ["excellent", "good", "average"]
41 | # for scan in scans:
42 | # detected_chars = self.converter.detect_characters(
43 | # os.path.join(self.sheets_path, f"{scan}.jpg")
44 | # )
45 |
--------------------------------------------------------------------------------
/tests/test_svgtottf.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | import unittest
5 |
6 | from handwrite import SHEETtoPNG, SVGtoTTF, PNGtoSVG
7 |
8 |
9 | class TestSVGtoTTF(unittest.TestCase):
10 | def setUp(self):
11 | self.temp = tempfile.mkdtemp()
12 | self.characters_dir = tempfile.mkdtemp(dir=self.temp)
13 | self.sheet_path = os.path.join(
14 | os.path.dirname(os.path.abspath(__file__)),
15 | "test_data",
16 | "sheettopng",
17 | "excellent.jpg",
18 | )
19 | self.config = os.path.join(
20 | os.path.dirname(os.path.abspath(__file__)),
21 | "test_data",
22 | "config_data",
23 | "default.json",
24 | )
25 | SHEETtoPNG().convert(self.sheet_path, self.characters_dir, self.config)
26 | PNGtoSVG().convert(directory=self.characters_dir)
27 | self.converter = SVGtoTTF()
28 | self.metadata = {"filename": "CustomFont"}
29 |
30 | def tearDown(self):
31 | shutil.rmtree(self.temp)
32 |
33 | def test_convert(self):
34 | self.converter.convert(
35 | self.characters_dir, self.temp, self.config, self.metadata
36 | )
37 | self.assertTrue(os.path.exists(os.path.join(self.temp, "CustomFont.ttf")))
38 | # os.remove(os.join())
39 |
40 | def test_convert_duplicate(self):
41 | fake_ttf = tempfile.NamedTemporaryFile(
42 | suffix=".ttf", dir=self.temp, delete=False
43 | )
44 | fake_ttf.close() # Doesn't keep open
45 | os.rename(fake_ttf.name, os.path.join(self.temp, "MyFont.ttf"))
46 | self.converter.convert(self.characters_dir, self.temp, self.config)
47 | self.assertTrue(os.path.exists(os.path.join(self.temp, "MyFont (1).ttf")))
48 | self.converter.convert(self.characters_dir, self.temp, self.config)
49 | self.assertTrue(os.path.exists(os.path.join(self.temp, "MyFont (1) (1).ttf")))
50 |
--------------------------------------------------------------------------------