khatt 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.
Files changed (50) hide show
  1. khatt-0.1.0/.gitattributes +9 -0
  2. khatt-0.1.0/.github/workflows/ci.yml +40 -0
  3. khatt-0.1.0/.github/workflows/release.yml +34 -0
  4. khatt-0.1.0/.gitignore +30 -0
  5. khatt-0.1.0/.pre-commit-config.yaml +24 -0
  6. khatt-0.1.0/.python-version +1 -0
  7. khatt-0.1.0/CHANGELOG.md +35 -0
  8. khatt-0.1.0/CONTRIBUTING.md +58 -0
  9. khatt-0.1.0/LICENSE +27 -0
  10. khatt-0.1.0/PKG-INFO +211 -0
  11. khatt-0.1.0/PLAN.md +125 -0
  12. khatt-0.1.0/README.md +176 -0
  13. khatt-0.1.0/ROADMAP.md +18 -0
  14. khatt-0.1.0/app.py +91 -0
  15. khatt-0.1.0/khatt/__init__.py +67 -0
  16. khatt-0.1.0/khatt/cli.py +114 -0
  17. khatt-0.1.0/khatt/fonts/Amiri-Regular.ttf +0 -0
  18. khatt-0.1.0/khatt/fonts/Cairo-Regular.ttf +0 -0
  19. khatt-0.1.0/khatt/fonts/LICENSES/OFL-Amiri.txt +94 -0
  20. khatt-0.1.0/khatt/fonts/LICENSES/OFL-Cairo.txt +93 -0
  21. khatt-0.1.0/khatt/fonts/LICENSES/OFL-NotoNaskhArabic.txt +93 -0
  22. khatt-0.1.0/khatt/fonts/LICENSES/OFL-Rubik.txt +93 -0
  23. khatt-0.1.0/khatt/fonts/NotoNaskhArabic-Regular.ttf +0 -0
  24. khatt-0.1.0/khatt/fonts/Rubik-Regular.ttf +0 -0
  25. khatt-0.1.0/khatt/fonts/__init__.py +35 -0
  26. khatt-0.1.0/khatt/mapper.py +113 -0
  27. khatt-0.1.0/khatt/py.typed +0 -0
  28. khatt-0.1.0/khatt/raster.py +123 -0
  29. khatt-0.1.0/khatt/shaping.py +190 -0
  30. khatt-0.1.0/pyproject.toml +108 -0
  31. khatt-0.1.0/requirements.txt +3 -0
  32. khatt-0.1.0/tests/conftest.py +47 -0
  33. khatt-0.1.0/tests/golden/alqahira_ascii_amiri.txt +10 -0
  34. khatt-0.1.0/tests/golden/alqahira_blocks_amiri.txt +10 -0
  35. khatt-0.1.0/tests/golden/alqahira_braille_amiri.txt +10 -0
  36. khatt-0.1.0/tests/golden/khatt_ascii_amiri.txt +12 -0
  37. khatt-0.1.0/tests/golden/khatt_blocks_amiri.txt +12 -0
  38. khatt-0.1.0/tests/golden/khatt_braille_amiri.txt +12 -0
  39. khatt-0.1.0/tests/golden/marhaba_ascii_amiri.txt +11 -0
  40. khatt-0.1.0/tests/golden/marhaba_blocks_amiri.txt +11 -0
  41. khatt-0.1.0/tests/golden/marhaba_braille_amiri.txt +11 -0
  42. khatt-0.1.0/tests/test_api.py +65 -0
  43. khatt-0.1.0/tests/test_cli.py +103 -0
  44. khatt-0.1.0/tests/test_edge_cases.py +49 -0
  45. khatt-0.1.0/tests/test_fonts.py +31 -0
  46. khatt-0.1.0/tests/test_golden.py +29 -0
  47. khatt-0.1.0/tests/test_mapper.py +101 -0
  48. khatt-0.1.0/tests/test_raster.py +62 -0
  49. khatt-0.1.0/tests/test_shaping.py +111 -0
  50. khatt-0.1.0/uv.lock +2633 -0
@@ -0,0 +1,9 @@
1
+ # Normalize all text files to LF in the repo and working tree.
2
+ # Golden test snapshots must be byte-identical across platforms.
3
+ * text=auto eol=lf
4
+
5
+ # Binary assets
6
+ *.ttf binary
7
+ *.otf binary
8
+ *.png binary
9
+ *.gif binary
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: py${{ matrix.python-version }} / ${{ matrix.os }}
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, windows-latest]
16
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
17
+ env:
18
+ PYTHONUTF8: "1"
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --locked
29
+
30
+ - name: Lint (ruff check)
31
+ run: uv run ruff check .
32
+
33
+ - name: Format check (ruff format)
34
+ run: uv run ruff format --check .
35
+
36
+ - name: Type check (mypy)
37
+ run: uv run mypy khatt
38
+
39
+ - name: Tests (pytest + coverage)
40
+ run: uv run pytest
@@ -0,0 +1,34 @@
1
+ # PyPI release via trusted publishing (OIDC — no API tokens).
2
+ #
3
+ # Fires when a v* tag is pushed, or on a manual dispatch from the Actions tab.
4
+ # Requires a trusted publisher to be configured for the khatt project on PyPI
5
+ # (Account → Publishing): owner DrEssam97, repo khatt, workflow release.yml,
6
+ # environment pypi.
7
+
8
+ name: Release
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+ workflow_dispatch:
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ environment: pypi
19
+ permissions:
20
+ id-token: write # trusted publishing (OIDC), no API token needed
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v5
26
+
27
+ - name: Build wheel and sdist
28
+ run: uv build
29
+
30
+ - name: Check distribution metadata
31
+ run: uvx twine check dist/*
32
+
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
khatt-0.1.0/.gitignore ADDED
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+
11
+ # Tooling caches
12
+ .mypy_cache/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # Local tooling
19
+ .claude/
20
+
21
+ # Editors / OS
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ Thumbs.db
26
+ .DS_Store
27
+
28
+ # Gradio
29
+ .gradio/
30
+ flagged/
@@ -0,0 +1,24 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ exclude: ^khatt/fonts/LICENSES/
7
+ - id: end-of-file-fixer
8
+ exclude: ^khatt/fonts/LICENSES/
9
+
10
+ - repo: https://github.com/astral-sh/ruff-pre-commit
11
+ rev: v0.15.21
12
+ hooks:
13
+ - id: ruff
14
+ args: [--fix]
15
+ - id: ruff-format
16
+
17
+ - repo: local
18
+ hooks:
19
+ - id: uv-lock-check
20
+ name: uv lock --check
21
+ entry: uv lock --check
22
+ language: system
23
+ pass_filenames: false
24
+ files: ^(pyproject\.toml|uv\.lock)$
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-11
11
+
12
+ ### Added
13
+
14
+ - **Engine A — rasterize-then-map rendering pipeline**: HarfBuzz shaping
15
+ (`uharfbuzz` + `python-bidi`) → FreeType rasterization (`freetype-py`) →
16
+ density mapping to character ramps.
17
+ - Correct Arabic shaping: positional forms, the mandatory لا ligature, mixed
18
+ Arabic/Latin/digit visual ordering, and a `keep_tashkeel` flag (diacritics
19
+ are stripped by default).
20
+ - Three output styles: `blocks` (░▒▓█), `ascii` (density ramp), `braille`
21
+ (2×4 dot patterns), all with an optional coverage `threshold`.
22
+ - Four bundled OFL fonts: Amiri (default), Cairo, Noto Naskh Arabic, Rubik,
23
+ with license texts preserved under `khatt/fonts/LICENSES/`.
24
+ - Public API: `khatt.render(text, font, style, width, threshold,
25
+ keep_tashkeel)`, `khatt.list_fonts()`, `khatt.list_styles()`.
26
+ - `khatt` CLI (typer): `--font/--style/--width/--threshold/--keep-tashkeel`,
27
+ `--list-fonts`, `--list-styles`, `--output` (text art or PNG of the
28
+ rendered text), stdin input, UTF-8 output on Windows consoles.
29
+ - Single-file Gradio app (`app.py`) with live preview, deployable unchanged
30
+ to a Hugging Face Space (`[app]` extra).
31
+ - Golden-file snapshot tests with a `pytest --update-golden` refresh flow;
32
+ 83-test suite at ≥85% enforced coverage on a 3.10–3.13 × Ubuntu/Windows
33
+ CI matrix.
34
+
35
+ [0.1.0]: https://github.com/DrEssam97/khatt/releases/tag/v0.1.0
@@ -0,0 +1,58 @@
1
+ # Contributing to Khatt
2
+
3
+ Thanks for helping! This is a small project with strict hygiene — the rules below keep
4
+ it that way.
5
+
6
+ ## Setup
7
+
8
+ ```bash
9
+ git clone https://github.com/DrEssam97/khatt.git && cd khatt
10
+ uv sync --all-extras # runtime + dev + gradio
11
+ uv run pre-commit install
12
+ ```
13
+
14
+ Python 3.10–3.13 are supported; local development is pinned to 3.13 via
15
+ `.python-version`. Everything runs through [uv](https://docs.astral.sh/uv/) — there is
16
+ no `requirements-dev.txt` and no manual venv management.
17
+
18
+ ## Workflow
19
+
20
+ - **Never commit to `main`.** Branch per change: `feat/…`, `fix/…`, `test/…`,
21
+ `docs/…`, `ci/…`, `chore/…`.
22
+ - **Conventional Commits** for every message: `feat: …`, `fix: …`, `test: …`, etc.
23
+ - Before every merge/push, the full gate must be green:
24
+
25
+ ```bash
26
+ uv run ruff check .
27
+ uv run ruff format --check .
28
+ uv run mypy khatt
29
+ uv run pytest
30
+ ```
31
+
32
+ - Coverage must stay ≥ 85% (enforced by pytest's `--cov-fail-under`).
33
+
34
+ ## Tests
35
+
36
+ Write tests **with** the feature, not after. Shaping assertions are glyph-ID
37
+ relational (the bundled font builds have no `post` name table) — see
38
+ `tests/test_shaping.py` for the pattern.
39
+
40
+ If you intentionally change rendering output, regenerate the golden snapshots and
41
+ commit the resulting diff — reviewers judge the art change from it:
42
+
43
+ ```bash
44
+ uv run pytest --update-golden
45
+ ```
46
+
47
+ ## Fonts
48
+
49
+ Bundled fonts must be OFL-licensed, with the complete license text added under
50
+ `khatt/fonts/LICENSES/` (that directory is excluded from whitespace-normalization
51
+ hooks — license texts stay verbatim). Register new fonts in
52
+ `khatt/fonts/__init__.py`.
53
+
54
+ ## Scope
55
+
56
+ v0.1.x is Engine A (rasterize-then-map) only. Hand-drawn FIGlet-style connected
57
+ Arabic fonts (Engine B), ANSI color, animation, and image-to-ASCII are out of scope —
58
+ see [ROADMAP.md](ROADMAP.md) before proposing features.
khatt-0.1.0/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammed Essam
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
+
23
+ ---
24
+
25
+ The Arabic fonts bundled under khatt/fonts/ are NOT covered by this license.
26
+ They are licensed under the SIL Open Font License (OFL) 1.1; the full license
27
+ text for each font family is preserved in khatt/fonts/LICENSES/.
khatt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: khatt
3
+ Version: 0.1.0
4
+ Summary: Render Arabic text as ASCII/Unicode-block art — the Arabic FIGlet
5
+ Project-URL: Homepage, https://github.com/DrEssam97/khatt
6
+ Project-URL: Repository, https://github.com/DrEssam97/khatt
7
+ Project-URL: Issues, https://github.com/DrEssam97/khatt/issues
8
+ Project-URL: Changelog, https://github.com/DrEssam97/khatt/blob/main/CHANGELOG.md
9
+ Author-email: Mohammed Essam <mohammedessam613@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: arabic,ascii-art,figlet,harfbuzz,rtl,text-art,unicode
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Natural Language :: Arabic
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Artistic Software
25
+ Classifier: Topic :: Text Processing :: Fonts
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: freetype-py>=2.5.1
28
+ Requires-Dist: pillow>=12.3.0
29
+ Requires-Dist: python-bidi>=0.6.11
30
+ Requires-Dist: typer>=0.26.8
31
+ Requires-Dist: uharfbuzz>=0.55.0
32
+ Provides-Extra: app
33
+ Requires-Dist: gradio>=6.20.0; extra == 'app'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # خَطّ — Khatt
37
+
38
+ [![CI](https://github.com/DrEssam97/khatt/actions/workflows/ci.yml/badge.svg)](https://github.com/DrEssam97/khatt/actions/workflows/ci.yml)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
40
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](pyproject.toml)
41
+ [![Fonts: OFL](https://img.shields.io/badge/fonts-SIL%20OFL%201.1-lightgrey.svg)](khatt/fonts/LICENSES/)
42
+
43
+ **Render Arabic text as ASCII/Unicode-block art — the Arabic FIGlet that FIGlet can't be.**
44
+
45
+ ```text
46
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⡄
47
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢺⣿⣿⣄
48
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠟
49
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿
50
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿
51
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣄⡀
52
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⡗
53
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⢿⣿⠟
54
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈
55
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣄⣀
56
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡏⠀⠀⠀⠀⣀⣤⣶⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⣠⣶⣿⣿⣷⣶⣤⣄⣀
57
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠇⠀⣀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⣀
58
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣤⣾⣿⠟⠋⠉⠀⠀⠀⠀⠙⢿⣿⣿⡟⠀⠀⠘⠉⠉⠉⠉⠉⠉⠙⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⡶
59
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠟⠋⠀⠀⠀⠀⠀⠀⣀⣠⣤⣶⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁
60
+ ⣀⣀⣀⣀⡀⠀⣀⣀⣀⣴⣿⣿⣥⣤⣤⣤⣴⣶⣶⣿⣿⣿⣿⣿⣿⣋⣀⡀⠀⠀⣀⣀⣀⣀⣤⣤⣶⣾⣿⣿⣿⡿⠿⠛⠋⠉⠁⠀⠀⠀⠀⠀⠀⠈
61
+ ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠉
62
+ ⠀⠀⠙⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠛⠛⠉⠛⠛⠻⠿⠿⠿⠿⠿⠿⠿⠿⠟⠛⠉⠁
63
+ ```
64
+
65
+ *(that's «خط» — "khatt", meaning line/script/calligraphy — Amiri font, `braille` style)*
66
+
67
+ ```text
68
+ -%+.
69
+ :@. =: *+-- =@ *# =@
70
+ -@. +: += =@ #% +@
71
+ %+ -@. =% =**+***: @# =@ *% -*****%+ -%. +@
72
+ @* -@. +@ +@. %* =@ *% -@- @* -@. +@
73
+ @+ -@. +@ .@- %* =@ *% +@ @* -@. +@
74
+ @+ -@. +@ :@- %# =@ #% :@- @* -@. +@
75
+ @* :%****##**********%: +**%#***#% +# -*****%#**+*% =%
76
+ .@= :- :- -.
77
+ +*%+ -* =+ *:
78
+ ```
79
+
80
+ *(«ايه الأخبار» — "What's the news?" — Cairo font, `ascii` style)*
81
+
82
+ ## Why FIGlet can't do this
83
+
84
+ FIGlet fonts map **one character to one fixed glyph**. Arabic doesn't work that way:
85
+
86
+ - Every letter has up to four **positional forms** (isolated / initial / medial / final).
87
+ - Some letter pairs form **mandatory ligatures** — ل + ا must become لا.
88
+ - The script is **cursive**: letters connect, and the connections carry the word's shape.
89
+ - Text runs **right-to-left**, and real text mixes RTL words with LTR numbers and names.
90
+
91
+ Khatt sidesteps glyph tables entirely with a **rasterize-then-map** pipeline:
92
+
93
+ ```
94
+ text ──► HarfBuzz shaping ──► FreeType rasterization ──► density mapping ──► art
95
+ (positional forms, (real Arabic fonts, (blocks / ascii /
96
+ ligatures, bidi) hi-res grayscale) braille ramps)
97
+ ```
98
+
99
+ The result is genuine calligraphic letterforms — from real fonts — approximated in
100
+ terminal characters.
101
+
102
+ ## Install
103
+
104
+ ```bash
105
+ pip install khatt # or: uv add khatt
106
+ ```
107
+
108
+ From source:
109
+
110
+ ```bash
111
+ git clone https://github.com/DrEssam97/khatt.git && cd khatt
112
+ uv sync
113
+ ```
114
+
115
+ ## Quickstart
116
+
117
+ ### Python API
118
+
119
+ ```python
120
+ import khatt
121
+
122
+ print(khatt.render("مرحبا")) # blocks style, 80 columns
123
+ print(khatt.render("خط", style="braille", width=60)) # high effective resolution
124
+ print(khatt.render("القاهرة", font="cairo", style="ascii", threshold=0.4))
125
+
126
+ khatt.list_fonts() # ['amiri', 'cairo', 'noto-naskh', 'rubik']
127
+ khatt.list_styles() # ['ascii', 'blocks', 'braille']
128
+ ```
129
+
130
+ ### CLI
131
+
132
+ ```bash
133
+ khatt "مرحبا" # render to the terminal
134
+ khatt "خط" --style braille --width 60
135
+ khatt "القاهرة" -f cairo -s ascii -t 0.4
136
+ echo "سلام" | khatt # stdin
137
+ khatt "خط" --output art.txt # save the art
138
+ khatt "خط" --output khatt.png # save the rendered text as an image
139
+ khatt --list-fonts
140
+ khatt --list-styles
141
+ ```
142
+
143
+ Arabic diacritics (tashkeel) are stripped by default — they're illegible at
144
+ character-grid resolution. Pass `--keep-tashkeel` (API: `keep_tashkeel=True`) to
145
+ render them.
146
+
147
+ ### Gradio app
148
+
149
+ ```bash
150
+ uv sync --extra app
151
+ uv run python app.py
152
+ ```
153
+
154
+ ## Bundled fonts and styles
155
+
156
+ | Font | Family | Character |
157
+ |---|---|---|
158
+ | `amiri` (default) | Amiri | Classical naskh, rich calligraphic detail |
159
+ | `cairo` | Cairo | Geometric, modern, bold strokes |
160
+ | `noto-naskh` | Noto Naskh Arabic | Clean, highly legible naskh |
161
+ | `rubik` | Rubik | Strong Latin + Arabic pairing for mixed text |
162
+
163
+ All fonts are licensed under the SIL Open Font License 1.1, preserved verbatim in
164
+ [`khatt/fonts/LICENSES/`](khatt/fonts/LICENSES/).
165
+
166
+ | Style | Ramp | Notes |
167
+ |---|---|---|
168
+ | `blocks` | `░ ▒ ▓ █` | Default; reads well at any size |
169
+ | `ascii` | `. : - = + * # % @` | Pure ASCII, maximum compatibility |
170
+ | `braille` | `⠁ … ⣿` | 2×4 dots per cell — 8× the effective resolution |
171
+
172
+ ## Deploy the app to a Hugging Face Space
173
+
174
+ `app.py` follows the Spaces convention (single file at the repo root), so the whole
175
+ repository deploys unchanged with one command:
176
+
177
+ ```bash
178
+ uv run --extra app gradio deploy
179
+ ```
180
+
181
+ This prompts for a Space name on first run (it uses your `huggingface-cli login`
182
+ credentials) and pushes the repo as a Gradio-SDK Space. Alternatively, create a Gradio
183
+ Space on huggingface.co and `git push` this repository to it — `app.py` is picked up
184
+ automatically and dependencies install via `requirements.txt` (which just points at
185
+ `pyproject.toml`).
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ uv sync --all-extras
191
+ uv run pre-commit install
192
+ ```
193
+
194
+ The gate that must be green before every merge:
195
+
196
+ ```bash
197
+ uv run ruff check . && uv run ruff format --check . && uv run mypy khatt && uv run pytest
198
+ ```
199
+
200
+ Rendering changed intentionally? Regenerate the golden snapshots and commit the diff:
201
+
202
+ ```bash
203
+ uv run pytest --update-golden
204
+ ```
205
+
206
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [PLAN.md](PLAN.md) and [ROADMAP.md](ROADMAP.md).
207
+
208
+ ## License
209
+
210
+ MIT for the code — see [LICENSE](LICENSE). Bundled fonts are under the SIL Open Font
211
+ License 1.1 — see [`khatt/fonts/LICENSES/`](khatt/fonts/LICENSES/).
khatt-0.1.0/PLAN.md ADDED
@@ -0,0 +1,125 @@
1
+ # PLAN — Khatt (خَطّ) v0.1.0
2
+
3
+ > Approved implementation plan for v0.1.0. Decisions settled during planning:
4
+ > **rasterization = uharfbuzz + freetype-py** (freetype-py approved as the one dependency
5
+ > beyond the original spec), and **local-only git for now** (full GitHub Flow discipline
6
+ > kept locally; remote/PRs activate when a GitHub remote is connected).
7
+
8
+ ## Context
9
+
10
+ Arabic has no FIGlet equivalent: FIGlet's per-character glyph model can't express contextual shaping (isolated/initial/medial/final forms), mandatory ligatures (لا), cursive joining, or RTL. Khatt sidesteps FIGlet entirely with a **rasterize-then-map** pipeline: shape the text properly with a real shaping engine, render it to a bitmap with a real Arabic font, then map pixel density onto character ramps. Deliverables for v0.1.0: a Python library (`khatt.render`), a `khatt` CLI, and a single-file Gradio app deployable to a Hugging Face Space.
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ text ──► shaping.py ──► raster.py ──► mapper.py ──► str
16
+ (HarfBuzz: (FreeType (density →
17
+ bidi runs, glyph ramp chars:
18
+ positional bitmaps → blocks/ascii/
19
+ forms, hi-res PIL braille)
20
+ ligatures) grayscale →
21
+ downsample
22
+ to grid)
23
+ ```
24
+
25
+ 1. **`khatt/shaping.py`** — `python-bidi` splits mixed-direction text into visual-order runs; each run is shaped with `uharfbuzz` (Arabic runs RTL + `language="ar"`, Latin/digit runs LTR). Output: `list[ShapedGlyph]` — dataclass with `gid`, `glyph_name`, `cluster`, `x_advance/y_advance`, `x_offset/y_offset`. Glyph names (e.g. Amiri's `uni0645.init`) are what the shaping tests assert against. A documented `tashkeel` flag controls whether diacritics are kept or stripped before shaping (proposed default: **strip**, because tashkeel is illegible at character-grid resolution; `--keep-tashkeel` to override — see Open Questions).
26
+ 2. **`khatt/raster.py`** — `freetype-py` renders each shaped glyph's bitmap at the HarfBuzz-computed pen positions onto a high-resolution Pillow grayscale canvas (hinting disabled, fixed integer ppem → deterministic). The canvas is auto-cropped to ink extents then downsampled with LANCZOS to the target grid: `(width, rows)` cells for blocks/ascii, `(width*2, rows*4)` dots for braille. Rows derive from the ink aspect ratio corrected by a terminal cell aspect of ~0.5 (cells are twice as tall as wide). Determinism note: `freetype-py` wheels bundle their own libfreetype and `uv.lock` pins the version, so output is byte-identical across Ubuntu/Windows — this is what makes golden tests viable on both CI runners.
27
+ 3. **`khatt/mapper.py`** — named styles registry: `blocks` (` ░▒▓█`), `ascii` (` .:-=+*#%@`), `braille` (U+2800 + 8-dot bitmask per 2×4 pixel block). `threshold: float | None` in [0,1]: pixels below it are forced blank; ramp mapping over the remaining range (documented, monotonic: darker → denser).
28
+ 4. **`khatt/fonts/`** — bundled OFL fonts + registry (`khatt/fonts/__init__.py` maps name → path): **Amiri** (default, naskh, rich ligatures), **Cairo** (geometric kufi-ish, bold weights rasterize well at low res), **Noto Naskh Arabic**, **Rubik** (Arabic + Latin coverage for mixed text). Full OFL texts preserved in `khatt/fonts/LICENSES/`.
29
+ 5. **`khatt/__init__.py`** — public API exactly as specced: `render(text, font="amiri", style="blocks", width=80, threshold=None) -> str`, plus `list_fonts()`, `list_styles()`, `__version__`. Ships `py.typed`.
30
+ 6. **`khatt/cli.py`** — `typer` app, entry point `khatt` via `[project.scripts]`. Flags: `--font/-f`, `--style/-s`, `--width/-w`, `--threshold/-t`, `--list-fonts`, `--list-styles`, `--output FILE` (`.txt` writes text; `.png` renders the text art string to an image via Pillow), reads stdin when no arg or arg is `-`. Windows UTF-8: `sys.stdout.reconfigure(encoding="utf-8")` (+ same for stderr) before any output.
31
+ 7. **`app.py`** (repo root, single file, HF Spaces convention) — Gradio Blocks: RTL textbox (`rtl=True`), font/style dropdowns, width + threshold sliders, live monospace preview (`gr.Code`/styled `gr.Textbox`), copy button, PNG download (`gr.File`). README gets a "Deploy to HF Spaces" section (one command via `huggingface_hub`/`gradio deploy`).
32
+
33
+ **Out of scope (recorded in `ROADMAP.md`, not implemented):** Engine B (hand-crafted connected FIGlet-style fonts), ANSI color, animation, image-to-ASCII.
34
+
35
+ ## File tree
36
+
37
+ ```
38
+ Khatt/
39
+ ├── pyproject.toml # all metadata + ruff/mypy/pytest/coverage config
40
+ ├── uv.lock # committed
41
+ ├── LICENSE # MIT (code only)
42
+ ├── README.md # demo media, install, quickstart, FIGlet comparison, Spaces deploy
43
+ ├── CHANGELOG.md # Keep a Changelog
44
+ ├── CONTRIBUTING.md
45
+ ├── ROADMAP.md # out-of-scope items above
46
+ ├── PLAN.md # this document
47
+ ├── app.py # single-file Gradio app
48
+ ├── .gitignore
49
+ ├── .pre-commit-config.yaml # ruff, ruff-format, trailing-whitespace, end-of-file-fixer, uv lock --check
50
+ ├── .github/workflows/ci.yml # runs on push to GitHub (later); + release.yml in PR 6 (disabled)
51
+ ├── khatt/
52
+ │ ├── __init__.py # render(), list_fonts(), list_styles(), __version__
53
+ │ ├── py.typed
54
+ │ ├── shaping.py
55
+ │ ├── raster.py
56
+ │ ├── mapper.py
57
+ │ ├── cli.py
58
+ │ └── fonts/
59
+ │ ├── __init__.py # font registry
60
+ │ ├── Amiri-Regular.ttf, Cairo-Bold.ttf, NotoNaskhArabic-Regular.ttf, Rubik-Bold.ttf
61
+ │ └── LICENSES/ # OFL texts per family
62
+ └── tests/
63
+ ├── conftest.py # --update-golden pytest option, shared fixtures
64
+ ├── test_shaping.py
65
+ ├── test_raster.py
66
+ ├── test_mapper.py
67
+ ├── test_api.py
68
+ ├── test_cli.py
69
+ ├── test_edge_cases.py
70
+ └── golden/ # word × style × font snapshots (*.txt)
71
+ ```
72
+
73
+ Note: spec calls for flat `khatt/` package paths, so the build config uses flat layout rather than `uv init --package`'s default `src/` layout.
74
+
75
+ ## Dependencies
76
+
77
+ | Package | Why | Group |
78
+ |---|---|---|
79
+ | `uharfbuzz` | Real shaping engine: positional forms, mandatory ligatures, kerning — the reason Khatt can exist. Wheels on all target platforms/versions. | runtime |
80
+ | `freetype-py` | Rasterizes glyphs **by glyph ID** at HarfBuzz positions (Pillow can't). Bundles libfreetype → deterministic cross-platform output. *(The one dep beyond your list — approved.)* | runtime |
81
+ | `python-bidi` | UBA run ordering for mixed Arabic/Latin/digit strings. | runtime |
82
+ | `Pillow` | Canvas compositing, downsampling, PNG output. | runtime |
83
+ | `typer` | CLI. | runtime |
84
+ | `gradio` | App only — optional extra `khatt[app]`, not a base install requirement. | extra |
85
+ | `pytest`, `pytest-cov` | Test suite + coverage gate (`--cov-fail-under=85`). | dev |
86
+ | `ruff`, `mypy`, `pre-commit` | Lint/format, type-check, hooks. | dev |
87
+ | `twine` | `twine check dist/*` packaging validation in PR 6. | dev |
88
+
89
+ Not used: `arabic-reshaper` — unnecessary given uharfbuzz works; noted in README as the inferior fallback approach.
90
+
91
+ Python 3.10–3.13. Everything through `uv` (`uv add`, `uv run`, `uv build`); `uv.lock` committed.
92
+
93
+ ## Git workflow (local-mode adaptation)
94
+
95
+ `git init` with `main`; **never commit to main directly**. Each phase = one branch (`chore/scaffold`, `feat/shaping`, …), Conventional Commits throughout. Since there's no remote yet: each phase merges to main locally with `--no-ff` and a PR-style merge commit body (what changed / how tested / tradeoffs) — preserving reviewable history so branches can be pushed and retro-PR'd when you connect GitHub. The "before every push" gate becomes **before every merge to main**: `uv run ruff check .` + `uv run ruff format --check .` + `uv run mypy khatt` + `uv run pytest` — never merge red. CI workflow files are committed from PR 1 and will run on first GitHub push.
96
+
97
+ ## Milestones → branches
98
+
99
+ 1. **`chore/scaffold`** — uv project (flat layout), full `pyproject.toml` (metadata, scripts, ruff/mypy/pytest/coverage config), `.pre-commit-config.yaml`, `ci.yml` (matrix: py 3.10–3.13 × ubuntu/windows; ruff → mypy → pytest+coverage), MIT LICENSE, empty package skeleton + placeholder test, PLAN.md, ROADMAP.md. Gate green on empty suite.
100
+ 2. **`feat/shaping`** — tests first: lam-alef ligature (glyph count + name), «محمد» positional forms (`.init/.medi/.fina` glyph-name assertions), mixed-direction visual order, tashkeel flag behavior. Then `shaping.py` until green.
101
+ 3. **`feat/raster-mapper`** — download/bundle 4 fonts + OFL licenses; `raster.py` (deterministic output, grid-dimension tests, all-fonts-load test); `mapper.py` (ramp-membership, monotonicity, threshold tests); golden harness (`--update-golden` in conftest) + goldens for «مرحبا» «خط» «القاهرة» × 3 styles × Amiri.
102
+ 4. **`feat/api-cli`** — `render()` + `cli.py`, `typer.testing.CliRunner` tests (flags, stdin, txt/png output, non-Arabic warning path), edge-case suite (empty, single letter, long-vs-width, tashkeel-only, emoji → graceful warning), coverage gate ≥85% enforced.
103
+ 5. **`feat/gradio-app`** — `app.py` + `[app]` extra + README Spaces-deploy section. Manual verification: `uv run --extra app python app.py`.
104
+ 6. **`docs/release-0.1`** — README (Arabic demo media at top, install, API/CLI/Gradio quickstarts, FIGlet comparison), CHANGELOG, CONTRIBUTING, `uv build` + `twine check dist/*`, clean-venv wheel install smoke test (`khatt "مرحبا"`), tag `v0.1.0`. `release.yml` (PyPI trusted publishing) committed **but not enabled** — needs your go-ahead + a GitHub remote anyway.
105
+
106
+ ## Test strategy
107
+
108
+ - **Shaping (critical):** glyph-name-based assertions via HarfBuzz — ligature presence, positional-form suffixes, run order for mixed-direction input, tashkeel flag. No pixels involved → fast and unambiguous.
109
+ - **Raster:** byte-identical output for pinned inputs (determinism guaranteed by bundled fonts + wheel-bundled libfreetype + `uv.lock`); exact grid dimensions per requested width; every bundled font loads and renders non-empty ink.
110
+ - **Mapper:** output charset ⊆ declared ramp per style; synthetic gradient input → monotonic density; threshold semantics.
111
+ - **Golden files:** committed snapshots under `tests/golden/`, diffed on every run; regenerate deliberately via `pytest --update-golden`. If a platform-specific diff ever appears despite the determinism design, fallback is documented in conftest: per-platform goldens for the offending case.
112
+ - **CLI:** `CliRunner` — every flag, stdin, file outputs, error paths.
113
+ - **Edge cases:** empty string, single letter, long string vs width, tashkeel-only string, emoji/unsupported chars → warning + graceful degradation, never a crash.
114
+ - **Coverage:** `pytest-cov`, `--cov=khatt --cov-fail-under=85` in config and CI.
115
+
116
+ ## Definition of done
117
+
118
+ All six phases merged to main via the local gate, coverage ≥85%, `uv build` yields valid wheel+sdist (`twine check` passes), wheel installed into a clean venv runs `khatt "مرحبا"` with correct connected-script output, `app.py` runs locally, CI workflows committed and ready for first GitHub push, tag `v0.1.0`.
119
+
120
+ ## Open questions
121
+
122
+ 1. **Tashkeel default:** I propose **strip by default** (diacritics are noise at grid resolution) with `--keep-tashkeel` / `tashkeel=True` to render them. OK, or prefer keep-by-default?
123
+ 2. **PyPI name:** `khatt` availability on PyPI is unverified (no network check done in planning). If taken at release time: fallback candidates `khatt-art`, `pykhatt` — your call then; doesn't block PRs 1–5.
124
+ 3. **Fourth font:** Rubik chosen for strong Latin+Arabic dual coverage (good for mixed strings). Swap for something else (e.g. Lateef, Reem Kufi) if you'd rather.
125
+ 4. **Terminal cell aspect:** default 0.5 (cell twice as tall as wide) — standard assumption, exposed as an internal constant, not a public API param in v0.1.0.