handwrite.py: рукописный рендер кириллицы синими чернилами

CLI-утилита (Pillow) для превращения текста в картинку рукописного текста. Полная поддержка кириллицы, синие чернила с имитацией письма ручкой (дрейф давления, полупрозрачность, неровный край), корректное выравнивание (б/й/н на baseline, р/у/д с хвостами, точки внизу, спецсимволы), перенос по словам + сохранение переносов строк, тетрадная бумага, --list-fonts. run.sh: команды-обёртки (init/upd/render/lined/big/calm/plain/poetry/render-file/render-stdin/list-fonts/examples). Документация: README.md, AGENTS.md. Примеры в examples/.
This commit is contained in:
2026-08-22 15:12:01 +03:00
commit 9e81d7a3e0
11 changed files with 1122 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
# AGENTS.md — hand-writter
Руководство для агентов (и людей), работающих с репозиторием `hand-writter`.
## О проекте
`handwrite.py` — CLI-утилита на Python, превращающая текст в картинку рукописного
текста с **поддержкой кириллицы** (полный русский алфавит, включая `Ё`, `щ`, `ъ`,
`э`, `ю`, `я`). Единственная зависимость — **Pillow** (`PIL`).
Ключевые особенности:
- Автоподбор рукописного шрифта из системы с проверкой покрытия кириллицы
(без сторонних библиотек — только Pillow: рендер символа и сравнение с
`notdef`-глифом). Приоритет: `Marker Felt`, затем `Snell Roundhand`, затем
любой шрифт с кириллицей.
- Синие чернила по умолчанию (`#1a3b8c`) с имитацией письма ручкой: коррелированный
дрейф «давления» (тёмные/светлые участки), полупрозрачные чернила, неровный край мазка.
- Выравнивание глифов — стандартное PIL ``text((x, line_top))`` (через маску в
``_stamp_glyph``). Все обычные буквы (а, о, н, б, й) стоят на одной baseline,
буквы с нижними выносными (р, у, д, ц, щ) опускают хвосты ниже — строчная
«р» ниже обычных букв, точки/запятые внизу, спецсимволы ($ ~ % @ …) — по
дизайну шрифта. Не вводите ручное смещение «по ink_bottom» — оно ломает
б/й/н и обрезает глифы.
- Перенос по словам + сохранение `\n` из входного файла/STDIN.
- Тетрадная «линейная» бумага (`--lined`), цвета чернил/бумаги/линий,
синие палитры и градиенты оттенков.
## Структура
```
hand-writter/
├── handwrite.py # основной скрипт (точка входа)
├── requirements.txt # Pillow>=9.0.0
├── run.sh # обёртка-команды: init/upd/render/...
├── README.md # пользовательская документация
├── AGENTS.md # этот файл
└── examples/ # примеры вывода
├── blue_pen.png
├── blue_pen_lined.png
├── multiline.png
└── sample_input.txt
```
## Окружение и запуск
- Python 3.8+ (в `run.sh` предполагается `python3.13`; если его нет —
отредактируйте `PY` в `run.sh` или используйте системный `python3`).
- Активация venv: `source .venv/bin/activate` (через `./run.sh init`).
- Прямой запуск: `python3 handwrite.py "Текст" -o out.png`.
- Команды-обёртки: `./run.sh render "Текст"` и др. (см. ниже).
## Правила работы с кодом
1. **Не добавляйте новых зависимостей** без крайней необходимости. Скрипт
намеренно зависит только от Pillow — это упрощает установку в
изолированных средах. Любая новая зависимость должна быть обоснована
и добавлена в `requirements.txt`.
2. **Сохраняйте поддержку кириллицы.** Любое изменение рендера/шрифтов
должно проверяться на полном русском алфавите (см. `CYRILLIC_TEST` в
`handwrite.py`). Не полагайтесь на «визуальную» проверку — шрифт может
отрисовывать `notdef` для отсутствующих глифов; используйте
`_font_covers_cyrillic()`.
3. **Выравнивание — стандартное PIL.** В ``_stamp_glyph`` глиф рисуется маской
с origin в канвасе и композируется так, чтобы совпадать с ``text((x, line_top))``.
НЕ прижимайте ink_bottom вручную к baseline и НЕ сдвигайте глифы по bbox —
это ломает б/й (обрезает) и делает их «слишком низкими». Канвас берётся по
абсолютным границам bbox (включая отрицательные bbox[0]/bbox[1] для Ё, р).
Знаки препинания рисуются с jitter=0 (чёткие точки), но тем же выравниванием.
4. **Сохраняйте переносы строк.** `wrap_text` использует `splitlines()` —
каждая строка входа становится отдельной строкой на картинке. Не
«схлоптывайте» пустые строки без явного флага.
5. **Не ломайте CLI-совместимость.** Новые параметры добавляйте с значениями
по умолчанию, не меняйте семантику существующих.
6. **Внесение изменений:** после правок проверяйте
- `python3 -c "import ast; ast.parse(open('handwrite.py').read())"` — синтаксис;
- запуск на кириллице с точками/запятыми и многострочным файлом;
- что точки-знаки остаются в нижней зоне строки.
## Команды (run.sh)
| Команда | Описание |
|---|---|
| `./run.sh init` | Создать venv и установить зависимости |
| `./run.sh upd` | Обновить зависимости |
| `./run.sh render "текст"` | Рендер текста-аргумента → `out/handwriting.png` |
| `./run.sh lined "текст"` | Рендер на тетрадной бумаге |
| `./run.sh big "текст"` | Крупный шрифт (64pt) на тёплой бумаге |
| `./run.sh calm "текст"` | «Спокойный» почерк (jitter 0.3, seed 42) |
| `./run.sh plain "текст"` | Ровный текст (без дрожания/поворота строк) |
| `./run.sh render-file file.txt` | Рендер файла (с переносами строк) |
| `./run.sh poetry file.txt` | Многострочный файл → тетрадь, крупный шрифт → `out/poetry.png` |
| `./run.sh render-stdin` | Чтение из STDIN (echo "..." \| ./run.sh render-stdin) |
| `./run.sh list-fonts` | Показать доступные рукописные шрифты с кириллицей |
| `./run.sh examples` | Перегенерировать примеры в `examples/` |
Все команды рендера (`render`, `lined`, `big`, `calm`, `plain`, `poetry`,
`render-file`, `render-stdin`) принимают доп. опции `handwrite.py` после `--`,
например: `./run.sh render "Текст" -- --jitter 0.4 --seed 7 --lined`.
## Тестовые сценарии (обязательно прогонять при правках)
```bash
# кириллица с б/й/р, точками и спецсимволами (б/й на baseline, р ниже, точки внизу)
python3 handwrite.py "байты йогурт река. \$ ~ % @ # конец!" -o /tmp/t1.png
# многострочный файл (каждая строка файла → строка на картинке)
printf 'Строка 1.\nСтрока 2.\nСтрока 3.\n' > /tmp/m.txt
python3 handwrite.py -i /tmp/m.txt -o /tmp/t2.png --lined
# синий (по умолчанию) + тетрадь
python3 handwrite.py "Текст" --lined -o /tmp/t3.png
```
## Чего не делать
- Не коммитьте `.DS_Store`, `.venv/`, временные `*.png` вне `examples/`.
- Не удаляйте `examples/` — это демонстрация возможностей.
- Не «упрощайте» проверку глифов кириллицы до `textbbox`/ширины — это
ненадёжно (узкие буквы, например «г», неотличимы от `notdef` по ширине).
- Цвет чернил по умолчанию — синий. Палитры/градиенты намеренно убраны
(раньше были, но усложняли). Не возвращайте мульти-цвет без явной просьбы.