penmimic 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- penmimic-0.1.0/HOW_IT_WORKS.md +87 -0
- penmimic-0.1.0/LICENSE +21 -0
- penmimic-0.1.0/MANIFEST.in +4 -0
- penmimic-0.1.0/PKG-INFO +130 -0
- penmimic-0.1.0/README.md +103 -0
- penmimic-0.1.0/examples/quickstart.py +14 -0
- penmimic-0.1.0/pyproject.toml +46 -0
- penmimic-0.1.0/setup.cfg +4 -0
- penmimic-0.1.0/src/penmimic/__init__.py +9 -0
- penmimic-0.1.0/src/penmimic/cli.py +54 -0
- penmimic-0.1.0/src/penmimic/glyphs.py +345 -0
- penmimic-0.1.0/src/penmimic/layout.py +169 -0
- penmimic-0.1.0/src/penmimic/py.typed +0 -0
- penmimic-0.1.0/src/penmimic/render.py +72 -0
- penmimic-0.1.0/src/penmimic/template.py +159 -0
- penmimic-0.1.0/src/penmimic/writer.py +108 -0
- penmimic-0.1.0/src/penmimic.egg-info/PKG-INFO +130 -0
- penmimic-0.1.0/src/penmimic.egg-info/SOURCES.txt +28 -0
- penmimic-0.1.0/src/penmimic.egg-info/dependency_links.txt +1 -0
- penmimic-0.1.0/src/penmimic.egg-info/entry_points.txt +2 -0
- penmimic-0.1.0/src/penmimic.egg-info/requires.txt +12 -0
- penmimic-0.1.0/src/penmimic.egg-info/top_level.txt +1 -0
- penmimic-0.1.0/tests/conftest.py +17 -0
- penmimic-0.1.0/tests/helpers.py +50 -0
- penmimic-0.1.0/tests/test_cursive.py +38 -0
- penmimic-0.1.0/tests/test_demo.py +38 -0
- penmimic-0.1.0/tests/test_glyphs.py +58 -0
- penmimic-0.1.0/tests/test_layout.py +70 -0
- penmimic-0.1.0/tests/test_template.py +28 -0
- penmimic-0.1.0/tests/test_writer.py +71 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# How penmimic works
|
|
2
|
+
|
|
3
|
+
Written for a smart student who has not seen the code.
|
|
4
|
+
|
|
5
|
+
## The big idea
|
|
6
|
+
1. You write every character five times on a printed sheet.
|
|
7
|
+
2. We photograph the sheet, find each box, and cut out the ink. That gives a "font" made of your real letters.
|
|
8
|
+
3. To write a sentence, we pick a letter image for every character, set them in a row, add small random
|
|
9
|
+
wobbles, and paste them onto paper.
|
|
10
|
+
|
|
11
|
+
No machine learning is used in version 0.1; it is image processing and layout.
|
|
12
|
+
|
|
13
|
+
## Block vs cursive
|
|
14
|
+
There are two printable templates with the **same grid** (same box positions, same markers), so one reader handles both:
|
|
15
|
+
- **Block**: plain boxes with a dotted baseline.
|
|
16
|
+
- **Cursive**: same boxes, plus a solid black **style bar** in the header, an x-height line, and a filled start dot
|
|
17
|
+
and hollow end dot on each a-z row.
|
|
18
|
+
|
|
19
|
+
How the program tells them apart: `detect_template_style` checks whether the style bar is black. Pick the style you
|
|
20
|
+
want with `Writer.from_samples(folder, style="cursive")`. Pages of the other style in the same folder are skipped.
|
|
21
|
+
|
|
22
|
+
Cursive extras:
|
|
23
|
+
1. `remove_cursive_join_guides` erases the printed dots so they are not read as ink.
|
|
24
|
+
2. `trim_cursive_connector_tails` trims long thin lead-in/exit strokes near the baseline, keeping the letter body, loops and descenders.
|
|
25
|
+
3. In `layout.py`, two lowercase letters in the same word get a smaller gap (they overlap slightly so strokes meet), and cursive uses less tilt jitter so words stay on the line.
|
|
26
|
+
|
|
27
|
+
## Files (in `src/penmimic/`)
|
|
28
|
+
|
|
29
|
+
### `template.py` - the sheet
|
|
30
|
+
Draws the printable sheet and defines where every box is (page size, box size, rows per page).
|
|
31
|
+
**Both** the sheet and the photo reader import these numbers, so they always agree.
|
|
32
|
+
Each page has:
|
|
33
|
+
- four solid black squares in the corners (so we can straighten the photo),
|
|
34
|
+
- small header squares: one square = page 1, two = page 2 ... (so we know which page a photo is),
|
|
35
|
+
- a row per character with five boxes, each with a pale dotted baseline to write on.
|
|
36
|
+
|
|
37
|
+
### `glyphs.py` - from photo to letters
|
|
38
|
+
1. **`find_marker_centers`** turns the photo black-and-white (Otsu's method picks the cut-off automatically),
|
|
39
|
+
finds blobs, and keeps the four biggest solid squares. A solid square fills its bounding box; a letter does not.
|
|
40
|
+
2. **`rectify`** uses a *perspective transform*: we know where the four squares should be on a flat page and where
|
|
41
|
+
they are in the photo, so OpenCV computes the warp that flattens the photo into a perfect A4 page.
|
|
42
|
+
3. **`normalise_lighting`** estimates how bright the blank paper is in each place (a morphological *close* erases
|
|
43
|
+
the thin pen strokes, then we blur) and divides the photo by it. Shadows disappear and paper becomes white everywhere.
|
|
44
|
+
4. **`read_page_number`** counts the header squares.
|
|
45
|
+
5. **`cut_glyph`** looks inside one box (ignoring a margin so the printed border is not included), keeps dark pixels,
|
|
46
|
+
throws away tiny specks, crops tightly, and saves an **alpha mask** (how much ink at each pixel, with soft edges).
|
|
47
|
+
It also records the **baseline**: how many pixels down from the top of the crop the dotted line was.
|
|
48
|
+
This is why a comma hangs below the line and an apostrophe floats above it.
|
|
49
|
+
6. **`save_glyphs` / `load_glyphs`** write one transparent PNG per letter (`U0041_v2.png` = "A", variant 2) plus `glyphs.json`.
|
|
50
|
+
|
|
51
|
+
### `layout.py` - where everything goes (no drawing here)
|
|
52
|
+
- Picks a random variant for each character, avoiding the same variant twice in a row.
|
|
53
|
+
- **Jitter** multiplies small random numbers: size +-8%, tilt +-3 degrees, height +-6% of a letter, gap +-40%.
|
|
54
|
+
`jitter=0` turns it all off.
|
|
55
|
+
- Measures words; if a word would cross the right margin it moves to the next line. A word too long for a line is broken by letter.
|
|
56
|
+
- Fallbacks: curly quotes become straight ones; a missing character tries the other case, then `?`, then leaves a gap.
|
|
57
|
+
- Uses a seeded random generator so `seed=42` gives the same page every time.
|
|
58
|
+
|
|
59
|
+
### `render.py` - drawing
|
|
60
|
+
Draws paper (plain or ruled), then for each letter: scale and tilt its mask, line its baseline up with the line,
|
|
61
|
+
and paste a block of the ink colour through the mask. Because the glyph is only a mask, any ink colour works.
|
|
62
|
+
For the GIF, we paste letters one by one and save a snapshot every few letters.
|
|
63
|
+
|
|
64
|
+
### `writer.py` - the public API
|
|
65
|
+
`Writer.from_samples(folder)` runs the extraction. `Writer.write(text, ...)` does layout and returns a `Page`;
|
|
66
|
+
`Page.save()` writes a PNG and `Page.animate()` writes a GIF.
|
|
67
|
+
|
|
68
|
+
### `cli.py`
|
|
69
|
+
The `penmimic` command.
|
|
70
|
+
|
|
71
|
+
### `demo/`
|
|
72
|
+
A tiny Flask site. The server loads your letters once, and `/render` returns a PNG. Text length, size and
|
|
73
|
+
colour are validated so the server can't be overloaded or sent bad input.
|
|
74
|
+
|
|
75
|
+
### `tests/`
|
|
76
|
+
We cannot ship someone's real handwriting, so tests make fake handwriting from a computer font, "photograph" it
|
|
77
|
+
with perspective, shadows and noise, and check that all 365 boxes are recovered, baselines are right, text wraps
|
|
78
|
+
inside the margins, and the same seed gives the same page.
|
|
79
|
+
|
|
80
|
+
## Design choices
|
|
81
|
+
- One file defines the page geometry, so the printer and the reader cannot drift apart.
|
|
82
|
+
- Markers + perspective warp, instead of asking for a perfectly scanned page.
|
|
83
|
+
- Masks instead of coloured images, so ink colour is free and edges stay smooth.
|
|
84
|
+
- Layout is separate from drawing, so layout can be tested without images.
|
|
85
|
+
- Clear errors (`corner squares`, `which template page`) instead of silent bad output.
|
|
86
|
+
|
|
87
|
+
|
penmimic-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ezzah Najmal
|
|
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.
|
penmimic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: penmimic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write any text in your own handwriting, from photos of a filled-in template sheet.
|
|
5
|
+
Author: Ezzah Najmal
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Ezzah-2k9/penmimic
|
|
8
|
+
Project-URL: Issues, https://github.com/Ezzah-2k9/penmimic/issues
|
|
9
|
+
Keywords: handwriting,opencv,pillow,image,text-to-handwriting
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: opencv-python-headless>=4.8
|
|
18
|
+
Requires-Dist: Pillow>=10.1
|
|
19
|
+
Provides-Extra: demo
|
|
20
|
+
Requires-Dist: flask>=3.0; extra == "demo"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: flask>=3.0; extra == "dev"
|
|
24
|
+
Requires-Dist: build; extra == "dev"
|
|
25
|
+
Requires-Dist: twine; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# penmimic
|
|
29
|
+
|
|
30
|
+
Write any text in **your own handwriting** from Python. Fill in a printable template, photograph it, and
|
|
31
|
+
`penmimic` cuts out your letters and uses them to write whatever you like, as a PNG or an animated GIF.
|
|
32
|
+
|
|
33
|
+
## The problem
|
|
34
|
+
Typed text feels impersonal. Existing handwriting tools are closed, limited, or hard to use from code.
|
|
35
|
+
|
|
36
|
+
## The solution
|
|
37
|
+
A small library with a short API:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from penmimic import Writer
|
|
41
|
+
|
|
42
|
+
w = Writer.from_samples("my_samples/")
|
|
43
|
+
w.write("Hello world").save("note.png")
|
|
44
|
+
w.write("Hello world").animate("note.gif")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each letter is picked at random from five of your own versions, then given a tiny random change in size,
|
|
48
|
+
tilt, height and spacing, so the page does not look copy-pasted.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install penmimic
|
|
54
|
+
```
|
|
55
|
+
Python 3.10+. For the optional web demo: `pip install "penmimic[demo]"`.
|
|
56
|
+
|
|
57
|
+
## Use it
|
|
58
|
+
|
|
59
|
+
**1. Print the template(s)** (5 A4 pages each). There are two: **block** letters and **cursive**.
|
|
60
|
+
```bash
|
|
61
|
+
penmimic --make-templates templates/ # makes both PDFs
|
|
62
|
+
penmimic --make-template block.pdf # just block
|
|
63
|
+
penmimic --make-template cursive.pdf --style cursive # just cursive
|
|
64
|
+
```
|
|
65
|
+
In the cursive sheet, every a-z box has a dotted baseline, an x-height line, a filled dot (start) and a hollow
|
|
66
|
+
dot (end). Write one natural letter with a short lead-in at the filled dot and a short flick at the hollow dot.
|
|
67
|
+
You do not have to fill in both sheets: fill the style(s) you want. Photos of both can sit in the same folder.
|
|
68
|
+
**2. Fill it in** with a dark pen. Write every character in every box, sitting on the dotted line.
|
|
69
|
+
|
|
70
|
+
**3. Photograph each page.** Whole page in view, flat, good light, plain surface, held upright (portrait).
|
|
71
|
+
Put the photos in a folder, e.g. `my_samples/`. File names do not matter: each page is recognised from the
|
|
72
|
+
small squares in its header.
|
|
73
|
+
|
|
74
|
+
**4. Write.**
|
|
75
|
+
```python
|
|
76
|
+
from penmimic import Writer
|
|
77
|
+
w = Writer.from_samples("my_samples/", style="cursive") # or "block" (default); letters saved in my_samples/glyphs_cursive/
|
|
78
|
+
w.write("Dear diary,\nToday was good.", size=1.2, ink="navy", paper="lined",
|
|
79
|
+
line_spacing=1.8, jitter=0.8, seed=42).save("diary.png")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Command line:
|
|
83
|
+
```bash
|
|
84
|
+
penmimic "Hello world" --samples my_samples --style cursive -o note.png
|
|
85
|
+
penmimic "Hello world" --samples my_samples -o note.gif --paper lined --ink navy
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Options for `write()`
|
|
89
|
+
| Option | Default | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `letter_spacing` | 1.0 | 0-3. Gap between letters (in cursive, above 1 opens the joins) |
|
|
92
|
+
| `word_spacing` | 1.0 | 0-3. Gap between words |
|
|
93
|
+
| `size` | 1.0 | Text size (1.0 = as written on the template) |
|
|
94
|
+
| `ink` | `#1b1b3a` | Ink colour: name or hex |
|
|
95
|
+
| `line_spacing` | 1.6 | Gap between lines, as a multiple of a typical letter height |
|
|
96
|
+
| `jitter` | 1.0 | 0 = identical letters, 1 = natural, 2 = messy (max) |
|
|
97
|
+
| `paper` | `plain` | `plain` or `lined` |
|
|
98
|
+
| `width` / `margin` | 1600 / 80 | Page size in pixels |
|
|
99
|
+
| `seed` | None | Same seed = same result every time |
|
|
100
|
+
|
|
101
|
+
## Demo website
|
|
102
|
+
```bash
|
|
103
|
+
pip install -e ".[demo]"
|
|
104
|
+
export PENMIMIC_SAMPLES=my_samples # Windows PowerShell: $env:PENMIMIC_SAMPLES="my_samples"
|
|
105
|
+
python demo/app.py # open http://127.0.0.1:5000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Results
|
|
109
|
+
- 44 automated tests pass (`pytest`). They use *synthetic* "handwriting" made from a computer font, photographed
|
|
110
|
+
with simulated perspective, shadow and noise. They prove the pipeline works, not that it looks good on every
|
|
111
|
+
real hand.
|
|
112
|
+
- TODO: after trying it with your own handwriting, record here what you saw (e.g. which characters needed
|
|
113
|
+
re-writing, how long extraction takes on your machine). Report only what you measured.
|
|
114
|
+
|
|
115
|
+
## Limitations
|
|
116
|
+
- Needs a clean template: a pen that is too light, boxes crossed by ink, or a dark/uneven background can break extraction.
|
|
117
|
+
- Cursive mode overlaps and tightens letters inside a word so they touch, but it still pastes separate letter images; it does not draw one continuous pen stroke.
|
|
118
|
+
- Only the characters on the template (A-Z, a-z, 0-9, `.,!?'"-:;()`). Others fall back to the other case,
|
|
119
|
+
then `?`, then a blank gap.
|
|
120
|
+
- Photos must show the whole page, roughly upright. Very strong shadows or extreme angles fail with a clear error.
|
|
121
|
+
- It copies your letters; it does not learn how you write. A neural stroke model is planned for version 0.2.
|
|
122
|
+
- Left-to-right scripts only.
|
|
123
|
+
|
|
124
|
+
## Credits
|
|
125
|
+
- Built by Ezzah Najmal.
|
|
126
|
+
- Code drafted with help from Claude (Anthropic), then reviewed and tested by the author.
|
|
127
|
+
- Libraries: OpenCV, Pillow, NumPy, Flask, pytest.
|
|
128
|
+
|
|
129
|
+
## Licence
|
|
130
|
+
MIT. See [LICENSE](LICENSE). Handwriting samples are personal data: only use your own or ones you have permission to use.
|
penmimic-0.1.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# penmimic
|
|
2
|
+
|
|
3
|
+
Write any text in **your own handwriting** from Python. Fill in a printable template, photograph it, and
|
|
4
|
+
`penmimic` cuts out your letters and uses them to write whatever you like, as a PNG or an animated GIF.
|
|
5
|
+
|
|
6
|
+
## The problem
|
|
7
|
+
Typed text feels impersonal. Existing handwriting tools are closed, limited, or hard to use from code.
|
|
8
|
+
|
|
9
|
+
## The solution
|
|
10
|
+
A small library with a short API:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from penmimic import Writer
|
|
14
|
+
|
|
15
|
+
w = Writer.from_samples("my_samples/")
|
|
16
|
+
w.write("Hello world").save("note.png")
|
|
17
|
+
w.write("Hello world").animate("note.gif")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Each letter is picked at random from five of your own versions, then given a tiny random change in size,
|
|
21
|
+
tilt, height and spacing, so the page does not look copy-pasted.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install penmimic
|
|
27
|
+
```
|
|
28
|
+
Python 3.10+. For the optional web demo: `pip install "penmimic[demo]"`.
|
|
29
|
+
|
|
30
|
+
## Use it
|
|
31
|
+
|
|
32
|
+
**1. Print the template(s)** (5 A4 pages each). There are two: **block** letters and **cursive**.
|
|
33
|
+
```bash
|
|
34
|
+
penmimic --make-templates templates/ # makes both PDFs
|
|
35
|
+
penmimic --make-template block.pdf # just block
|
|
36
|
+
penmimic --make-template cursive.pdf --style cursive # just cursive
|
|
37
|
+
```
|
|
38
|
+
In the cursive sheet, every a-z box has a dotted baseline, an x-height line, a filled dot (start) and a hollow
|
|
39
|
+
dot (end). Write one natural letter with a short lead-in at the filled dot and a short flick at the hollow dot.
|
|
40
|
+
You do not have to fill in both sheets: fill the style(s) you want. Photos of both can sit in the same folder.
|
|
41
|
+
**2. Fill it in** with a dark pen. Write every character in every box, sitting on the dotted line.
|
|
42
|
+
|
|
43
|
+
**3. Photograph each page.** Whole page in view, flat, good light, plain surface, held upright (portrait).
|
|
44
|
+
Put the photos in a folder, e.g. `my_samples/`. File names do not matter: each page is recognised from the
|
|
45
|
+
small squares in its header.
|
|
46
|
+
|
|
47
|
+
**4. Write.**
|
|
48
|
+
```python
|
|
49
|
+
from penmimic import Writer
|
|
50
|
+
w = Writer.from_samples("my_samples/", style="cursive") # or "block" (default); letters saved in my_samples/glyphs_cursive/
|
|
51
|
+
w.write("Dear diary,\nToday was good.", size=1.2, ink="navy", paper="lined",
|
|
52
|
+
line_spacing=1.8, jitter=0.8, seed=42).save("diary.png")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Command line:
|
|
56
|
+
```bash
|
|
57
|
+
penmimic "Hello world" --samples my_samples --style cursive -o note.png
|
|
58
|
+
penmimic "Hello world" --samples my_samples -o note.gif --paper lined --ink navy
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Options for `write()`
|
|
62
|
+
| Option | Default | Meaning |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `letter_spacing` | 1.0 | 0-3. Gap between letters (in cursive, above 1 opens the joins) |
|
|
65
|
+
| `word_spacing` | 1.0 | 0-3. Gap between words |
|
|
66
|
+
| `size` | 1.0 | Text size (1.0 = as written on the template) |
|
|
67
|
+
| `ink` | `#1b1b3a` | Ink colour: name or hex |
|
|
68
|
+
| `line_spacing` | 1.6 | Gap between lines, as a multiple of a typical letter height |
|
|
69
|
+
| `jitter` | 1.0 | 0 = identical letters, 1 = natural, 2 = messy (max) |
|
|
70
|
+
| `paper` | `plain` | `plain` or `lined` |
|
|
71
|
+
| `width` / `margin` | 1600 / 80 | Page size in pixels |
|
|
72
|
+
| `seed` | None | Same seed = same result every time |
|
|
73
|
+
|
|
74
|
+
## Demo website
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ".[demo]"
|
|
77
|
+
export PENMIMIC_SAMPLES=my_samples # Windows PowerShell: $env:PENMIMIC_SAMPLES="my_samples"
|
|
78
|
+
python demo/app.py # open http://127.0.0.1:5000
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Results
|
|
82
|
+
- 44 automated tests pass (`pytest`). They use *synthetic* "handwriting" made from a computer font, photographed
|
|
83
|
+
with simulated perspective, shadow and noise. They prove the pipeline works, not that it looks good on every
|
|
84
|
+
real hand.
|
|
85
|
+
- TODO: after trying it with your own handwriting, record here what you saw (e.g. which characters needed
|
|
86
|
+
re-writing, how long extraction takes on your machine). Report only what you measured.
|
|
87
|
+
|
|
88
|
+
## Limitations
|
|
89
|
+
- Needs a clean template: a pen that is too light, boxes crossed by ink, or a dark/uneven background can break extraction.
|
|
90
|
+
- Cursive mode overlaps and tightens letters inside a word so they touch, but it still pastes separate letter images; it does not draw one continuous pen stroke.
|
|
91
|
+
- Only the characters on the template (A-Z, a-z, 0-9, `.,!?'"-:;()`). Others fall back to the other case,
|
|
92
|
+
then `?`, then a blank gap.
|
|
93
|
+
- Photos must show the whole page, roughly upright. Very strong shadows or extreme angles fail with a clear error.
|
|
94
|
+
- It copies your letters; it does not learn how you write. A neural stroke model is planned for version 0.2.
|
|
95
|
+
- Left-to-right scripts only.
|
|
96
|
+
|
|
97
|
+
## Credits
|
|
98
|
+
- Built by Ezzah Najmal.
|
|
99
|
+
- Code drafted with help from Claude (Anthropic), then reviewed and tested by the author.
|
|
100
|
+
- Libraries: OpenCV, Pillow, NumPy, Flask, pytest.
|
|
101
|
+
|
|
102
|
+
## Licence
|
|
103
|
+
MIT. See [LICENSE](LICENSE). Handwriting samples are personal data: only use your own or ones you have permission to use.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from penmimic import Writer
|
|
3
|
+
|
|
4
|
+
parser = argparse.ArgumentParser(description="Render text from your handwriting samples.")
|
|
5
|
+
parser.add_argument("--samples", default="my_samples", help="folder containing filled template page images")
|
|
6
|
+
parser.add_argument("--style", choices=["block", "cursive"], default="block")
|
|
7
|
+
parser.add_argument("--output", default="handwriting.png")
|
|
8
|
+
args = parser.parse_args()
|
|
9
|
+
|
|
10
|
+
writer = Writer.from_samples(args.samples, style=args.style)
|
|
11
|
+
writer.write("Hello World.\nThis is PenMimic.", size=0.9, jitter=0.25, seed=42).save(args.output)
|
|
12
|
+
print(f"Saved {args.output}")
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "penmimic"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Write any text in your own handwriting, from photos of a filled-in template sheet."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Ezzah Najmal" }]
|
|
14
|
+
keywords = ["handwriting", "opencv", "pillow", "image", "text-to-handwriting"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Multimedia :: Graphics",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=1.24",
|
|
22
|
+
"opencv-python-headless>=4.8",
|
|
23
|
+
"Pillow>=10.1",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
demo = ["flask>=3.0"]
|
|
28
|
+
dev = ["pytest>=8.0", "flask>=3.0", "build", "twine"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
penmimic = "penmimic.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Ezzah-2k9/penmimic"
|
|
35
|
+
Issues = "https://github.com/Ezzah-2k9/penmimic/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
penmimic = ["py.typed"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
|
|
46
|
+
|
penmimic-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""penmimic - write any text in your own handwriting."""
|
|
2
|
+
|
|
3
|
+
from .template import make_both_templates, make_template
|
|
4
|
+
from .writer import Page, Writer
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["Writer", "Page", "make_template", "make_both_templates", "__version__"]
|
|
8
|
+
|
|
9
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Command line: penmimic "Hello world" --samples my_samples -o note.png"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .template import make_both_templates, make_template
|
|
7
|
+
from .writer import Writer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
p = argparse.ArgumentParser(prog="penmimic", description="Write text in your own handwriting.")
|
|
12
|
+
p.add_argument("text", nargs="?", help="the text to write")
|
|
13
|
+
p.add_argument("--samples", help="folder with photos of your filled-in template (or extracted glyphs)")
|
|
14
|
+
p.add_argument("--style", choices=["block", "cursive"], default="block", help="handwriting template style")
|
|
15
|
+
p.add_argument("-o", "--output", default="out.png", help="output file (.png, or .gif to animate)")
|
|
16
|
+
p.add_argument("--make-template", metavar="PDF",
|
|
17
|
+
help="create ONE printable template (use --style block|cursive) and exit")
|
|
18
|
+
p.add_argument("--make-templates", metavar="DIR",
|
|
19
|
+
help="create BOTH templates (block + cursive PDFs) in DIR and exit")
|
|
20
|
+
p.add_argument("--size", type=float, default=1.0)
|
|
21
|
+
p.add_argument("--ink", default="#1b1b3a")
|
|
22
|
+
p.add_argument("--line-spacing", type=float, default=1.6)
|
|
23
|
+
p.add_argument("--jitter", type=float, default=1.0)
|
|
24
|
+
p.add_argument("--letter-spacing", type=float, default=1.0, help="0-3, 1 = natural")
|
|
25
|
+
p.add_argument("--word-spacing", type=float, default=1.0, help="0-3, 1 = natural")
|
|
26
|
+
p.add_argument("--paper", choices=["plain", "lined"], default="plain")
|
|
27
|
+
p.add_argument("--width", type=int, default=1600)
|
|
28
|
+
p.add_argument("--seed", type=int, default=None)
|
|
29
|
+
return p
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> int:
|
|
33
|
+
args = build_parser().parse_args(argv)
|
|
34
|
+
if args.make_templates:
|
|
35
|
+
for path in make_both_templates(args.make_templates):
|
|
36
|
+
print(f"Template written to {path}")
|
|
37
|
+
return 0
|
|
38
|
+
if args.make_template:
|
|
39
|
+
print(f"Template written to {make_template(args.make_template, args.style)}")
|
|
40
|
+
return 0
|
|
41
|
+
if not args.text or not args.samples:
|
|
42
|
+
print("error: give the text and --samples (or use --make-template)", file=sys.stderr)
|
|
43
|
+
return 2
|
|
44
|
+
writer = Writer.from_samples(args.samples, style=args.style)
|
|
45
|
+
page = writer.write(args.text, size=args.size, ink=args.ink, line_spacing=args.line_spacing,
|
|
46
|
+
jitter=args.jitter, paper=args.paper,
|
|
47
|
+
letter_spacing=args.letter_spacing, word_spacing=args.word_spacing, width=args.width, seed=args.seed)
|
|
48
|
+
out = page.animate(args.output) if args.output.lower().endswith(".gif") else page.save(args.output)
|
|
49
|
+
print(f"Saved {out}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
raise SystemExit(main())
|