Skip to main content

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.

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

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.

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.
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.

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.

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.

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.

Command line

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.

Limits

  • A font patch can fail and give no error. If the font shapes with the AAT morx table, the patch does nothing. Many macOS fonts do this. Test the patch:

    before = [g.name for g in shape("ممتاز", font)]
    after = [g.name for g in shape("ممتاز", patch_font(font, [rule]))]
    assert before != after, "the rule did not operate"
    
  • Each font has a different set of letters that can sweep. Amiri gives ر ز و ژ ۏ ؤ ڑ. Run examples/other_fonts.py to find the set for a font.

  • A kashida needs a font that has flat connections. Nastaliq fonts do not work.

  • The library does not do bidi. Divide a line of mixed Arabic and Latin text yourself.

  • A letter from a fallback font does not join to the adjacent letters.

  • The library uses a variable font at its default instance.

  • One size value gives a different ink height in each font. Measure the ink, then scale each font.

More

Development

uv sync
uv run 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).

Metadata

Release files for textsynth 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for textsynth 0.1.3
File Size Uploaded Provenance Actions
textsynth-0.1.3.tar.gz 2.0 MB Without provenance Details for textsynth-0.1.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for textsynth 0.1.3
File Interpreter ABI Platform Actions
textsynth-0.1.3-py3-none-any.whl Python 3 none any Details for textsynth-0.1.3-py3-none-any.whl

Total release size: 2.1 MB

Release files / textsynth-0.1.3.tar.gz

Download URL textsynth-0.1.3.tar.gz
Size 2.0 MB
Tags Source
SHA-256 checksum
How to use checksums
5cf1ffba12ab2ed3e77d0286f47c42c3f1940b05b9b4796ccd52c0f5aa1e8f9f
BLAKE2b-256 checksum
How to use checksums
cfcfc4b530a79b71fa84e255eeaeb6675cd4e5f2bb882bc98410f08a91fc2364
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / textsynth-0.1.3-py3-none-any.whl

Download URL textsynth-0.1.3-py3-none-any.whl
Size 62.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5c0ec196126230a166593f639c33da0ab2887421fc7ed673c5ec6751651c62b9
BLAKE2b-256 checksum
How to use checksums
34264025e515816cf3e3deb21f9b07b563a727c6b4b33aa68e3a3c6f66a6d038
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for textsynth 0.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page