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.
- khatt-0.1.0/.gitattributes +9 -0
- khatt-0.1.0/.github/workflows/ci.yml +40 -0
- khatt-0.1.0/.github/workflows/release.yml +34 -0
- khatt-0.1.0/.gitignore +30 -0
- khatt-0.1.0/.pre-commit-config.yaml +24 -0
- khatt-0.1.0/.python-version +1 -0
- khatt-0.1.0/CHANGELOG.md +35 -0
- khatt-0.1.0/CONTRIBUTING.md +58 -0
- khatt-0.1.0/LICENSE +27 -0
- khatt-0.1.0/PKG-INFO +211 -0
- khatt-0.1.0/PLAN.md +125 -0
- khatt-0.1.0/README.md +176 -0
- khatt-0.1.0/ROADMAP.md +18 -0
- khatt-0.1.0/app.py +91 -0
- khatt-0.1.0/khatt/__init__.py +67 -0
- khatt-0.1.0/khatt/cli.py +114 -0
- khatt-0.1.0/khatt/fonts/Amiri-Regular.ttf +0 -0
- khatt-0.1.0/khatt/fonts/Cairo-Regular.ttf +0 -0
- khatt-0.1.0/khatt/fonts/LICENSES/OFL-Amiri.txt +94 -0
- khatt-0.1.0/khatt/fonts/LICENSES/OFL-Cairo.txt +93 -0
- khatt-0.1.0/khatt/fonts/LICENSES/OFL-NotoNaskhArabic.txt +93 -0
- khatt-0.1.0/khatt/fonts/LICENSES/OFL-Rubik.txt +93 -0
- khatt-0.1.0/khatt/fonts/NotoNaskhArabic-Regular.ttf +0 -0
- khatt-0.1.0/khatt/fonts/Rubik-Regular.ttf +0 -0
- khatt-0.1.0/khatt/fonts/__init__.py +35 -0
- khatt-0.1.0/khatt/mapper.py +113 -0
- khatt-0.1.0/khatt/py.typed +0 -0
- khatt-0.1.0/khatt/raster.py +123 -0
- khatt-0.1.0/khatt/shaping.py +190 -0
- khatt-0.1.0/pyproject.toml +108 -0
- khatt-0.1.0/requirements.txt +3 -0
- khatt-0.1.0/tests/conftest.py +47 -0
- khatt-0.1.0/tests/golden/alqahira_ascii_amiri.txt +10 -0
- khatt-0.1.0/tests/golden/alqahira_blocks_amiri.txt +10 -0
- khatt-0.1.0/tests/golden/alqahira_braille_amiri.txt +10 -0
- khatt-0.1.0/tests/golden/khatt_ascii_amiri.txt +12 -0
- khatt-0.1.0/tests/golden/khatt_blocks_amiri.txt +12 -0
- khatt-0.1.0/tests/golden/khatt_braille_amiri.txt +12 -0
- khatt-0.1.0/tests/golden/marhaba_ascii_amiri.txt +11 -0
- khatt-0.1.0/tests/golden/marhaba_blocks_amiri.txt +11 -0
- khatt-0.1.0/tests/golden/marhaba_braille_amiri.txt +11 -0
- khatt-0.1.0/tests/test_api.py +65 -0
- khatt-0.1.0/tests/test_cli.py +103 -0
- khatt-0.1.0/tests/test_edge_cases.py +49 -0
- khatt-0.1.0/tests/test_fonts.py +31 -0
- khatt-0.1.0/tests/test_golden.py +29 -0
- khatt-0.1.0/tests/test_mapper.py +101 -0
- khatt-0.1.0/tests/test_raster.py +62 -0
- khatt-0.1.0/tests/test_shaping.py +111 -0
- khatt-0.1.0/uv.lock +2633 -0
|
@@ -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
|
khatt-0.1.0/CHANGELOG.md
ADDED
|
@@ -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
|
+
[](https://github.com/DrEssam97/khatt/actions/workflows/ci.yml)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](pyproject.toml)
|
|
41
|
+
[](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.
|