Metadata-Version: 2.5
Name: textsynth
Version: 0.2.0
Summary: Synthesise training images of Perso-Arabic text: shaping-aware layout, in-memory font patching, and ink effects, from a string to a PIL image.
Project-URL: Homepage, https://github.com/hmzdot/textsynth
Project-URL: Repository, https://github.com/hmzdot/textsynth
Project-URL: Issues, https://github.com/hmzdot/textsynth/issues
Author-email: hmzdot <hmzkrb@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: arabic,harfbuzz,htr,jawi,ocr,perso-arabic,synthetic-data,text-rendering
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Arabic
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Scientific/Engineering :: Image Processing
Classifier: Topic :: Text Processing :: Fonts
Requires-Python: >=3.11
Requires-Dist: fonttools>=4.50
Requires-Dist: freetype-py>=2.4
Requires-Dist: numpy>=1.24
Requires-Dist: pillow>=10.0
Requires-Dist: scipy>=1.11
Requires-Dist: uharfbuzz>=0.39
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: notebooks
Requires-Dist: ipykernel>=6.0; extra == 'notebooks'
Requires-Dist: jupyterlab>=4.0; extra == 'notebooks'
Requires-Dist: nbconvert>=7.0; extra == 'notebooks'
Description-Content-Type: text/markdown

# textsynth

textsynth makes images of Perso-Arabic text. You give a string to the library.
The library gives a `PIL.Image` back.

The library can change the text before it draws the text. It can also add ink
damage to the image. Use textsynth to make training data for OCR and HTR
models.

![One line of Jawi, two times. The top line is the font with no rules. The bottom line has one glyph for the pair mim-mim, a kashida, a swept waw, and ink damage.](https://raw.githubusercontent.com/hmzdot/textsynth/main/docs/before-after.png)

The top line is Scheherazade New with no rules. The bottom line is the same
text. It has one glyph for the pair `مم`, a kashida, a swept waw, and ink
damage. `examples/all_tricks.py` makes this image.

## Install

```bash
pip install textsynth      # or: uv add textsynth
```

## Quick start

The library does not include a font. Give the path of a font that has
Perso-Arabic letters.

```python
from textsynth import render

image = render("بسم الله", "Amiri-Regular.ttf", size=72)
image.save("out.png")
```

## The five stages

`Synthesizer` sends a string through five stages. You can use each stage alone.
Only the shaper is necessary.

| Stage | Modules | Function |
| --- | --- | --- |
| text rules | `textrules`, `kashida` | Change the string. |
| font patches | `patching`, `glyphs`, `outlines` | Make a temporary font in memory that has new glyphs. |
| shaping, layout | `shaping`, `raster`, `layout` | Shape the text with HarfBuzz. Then set the position of each glyph. |
| glyph rules | `glyphrules`, `sweeps` | Change the shaped line. The measurements are known at this point. |
| effects | `effects` | Add ink damage: smudge, blot, blob, bleed, erode, speckle, warp. |

```python
from textsynth import (
    Synthesizer, Layout, GlyphSubstitution, BorrowedGlyph,
    Smudge, Blobify, InkBleed, orthographic_variation,
)

synth = Synthesizer(
    font="Amiri-Regular.ttf",
    layout=Layout(size=72, tracking=-1, supersample=2, jitter_y=0.6),
    text_rules=[orthographic_variation(swap=0.3)],
    font_patches=[GlyphSubstitution("مم", BorrowedGlyph("Oblegg-Jawi.otf", "A"))],
    effects=[Smudge(length=7), Blobify(), InkBleed(1.0)],
    seed=7,
)
image = synth("سايا ممباچ سورت خبر")
```

## Elongation

### Kashida

`Kashida` finds each connection in a word that you can extend. Then it puts
tatweels at these positions. `contrast` controls the distribution.

```python
Kashida(frequency=6, contrast=0.8).apply("حكيم", rng)   # 'حــــــكـيم'
```

A tatweel alone is not enough. The font draws a bar next to the letter, but it
does not move the dots. `KashidaForms` makes one glyph from the bar and the
letter, and moves the dots along the extension.

```python
Synthesizer(
    font=FONT,
    text_rules=[Kashida(frequency=6, contrast=0.8)],   # keep this rule last
    font_patches=[KashidaForms(dot_shift=0.5)],        # 0 keeps the dots
)
```

Put `Kashida` last in the text rules. `orthographic_variation` removes
tatweels. A kashida before that rule does not stay in the text.

### Tadakhul

`Tadakhul` continues the tail of a letter below the word. The letters above the
tail move up. Give the length in letters: `length=2` makes the letter as long
as the two letters after it.

```python
Synthesizer(font=FONT, glyph_rules=[TadakhulMerged(letters="و", length=2)])
```

Three rules make this shape:

| Rule | Length | Cost | Result |
| --- | --- | --- | --- |
| `TadakhulMerged` | exact | One font rebuild for each line. | One contour. |
| `TadakhulQuantised` | nearest of N steps | One font rebuild only. | One contour. |
| `TadakhulScaled` | exact | One font rebuild only. | Two glyphs that touch at the cut. |

A tail does not always go to the left. The jim family and the ayn family turn
back to the right. Such a letter sweeps below the letters **before** it. Put
these letters in `reverse_letters`. `REVERSE_SWEEPERS` holds the set for Amiri.

```python
Synthesizer(font=FONT, glyph_rules=[
    TadakhulMerged(letters=SWEEPERS, reverse_letters=REVERSE_SWEEPERS, length=2)
])
```

A letter sweeps one way only, thus the two sets share no letter. In Amiri
`SWEEPERS` is `ر ز و ؤ ژ ۆ ڑ ۏ` and `REVERSE_SWEEPERS` is `ج ح خ چ ع غ ڠ ے`.

## Command line

```bash
textsynth "ممتاز سايا" -f Amiri-Regular.ttf \
    --size 72 --tracking -1 --distress smudged \
    --substitute "مم=Oblegg-Jawi.otf:A" --vary 0.3 -o line.png
```

| Option | Values |
| --- | --- |
| `--distress` | `none`, `light`, `smudged`, `heavy` |
| `--substitute` | `SEQUENCE=file.svg` or `SEQUENCE=font.ttf:CHAR` |
| `--kashida N` | The number of tatweels for each word |
| `--sweep` | `merged`, `quantised`, `scaled` |

Refer to `textsynth --help` for all the options.

## More

- [`docs/design.md`](docs/design.md) — how each stage operates, and why.
- [`examples/`](examples/) — one script for each feature.
- [`notebooks/`](notebooks/) — the notebooks, with their outputs.

## Development

```bash
uv sync
uv run pytest
```

Without uv, install the checkout into a virtual environment first. The code
sits under `src/`, so `import textsynth` and the scripts in `examples/` do not
work from the repository root until you do this.

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
```

## License

The library is under the MIT license.

`assets/fonts/Amiri-Regular.ttf` is Amiri, under the SIL Open Font License
(`Amiri-OFL.txt`).

`assets/fonts/ScheherazadeNew-Regular.ttf` is Scheherazade New, by SIL Global,
under the SIL Open Font License (`ScheherazadeNew-OFL.txt`).
