nima-persianguard 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nima Esmaili
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.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: nima-persianguard
3
+ Version: 1.0.0
4
+ Summary: PersianGuard: modular Persian shaping/layout validation and infographic renderer with a single-pass RTL contract.
5
+ Author: Nima Esmaili
6
+ License: MIT
7
+ Keywords: persian,rtl,bidi,arabic-reshaper,infographic,pillow
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Topic :: Multimedia :: Graphics
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: Pillow>=9.0
15
+ Provides-Extra: rtl
16
+ Requires-Dist: arabic-reshaper>=3.0; extra == "rtl"
17
+ Requires-Dist: python-bidi>=0.4; extra == "rtl"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Requires-Dist: arabic-reshaper>=3.0; extra == "dev"
21
+ Requires-Dist: python-bidi>=0.4; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # nima_persianguard
25
+
26
+ A clean, modular Python package extracted from the original PersianGuard
27
+ infographic scripts (`nima_persianguard.py`,
28
+ `Persian_Infographic-Nima_PersianGuard.py`). It preserves the exact text
29
+ content, layout constants and rendering behavior while separating concerns
30
+ into validation, layout, shaping and rendering modules with a stable public
31
+ API and CLI.
32
+
33
+ ## Single-pass RTL contract
34
+
35
+ The package enforces the audited contract from
36
+ `shaping_bidi_review_report.txt`:
37
+
38
+ 1. **Logical source only** — every text entering the engine is validated by
39
+ `validate_source`: Arabic presentation forms (`U+FB50..U+FDFF`,
40
+ `U+FE70..U+FEFF`) and bidi control characters (`LRE/RLE/LRO/RLO/PDF/
41
+ LRI/RLI/FSI/PDI`) are rejected, so pre-shaped or visual-order text can
42
+ never enter the pipeline.
43
+ 2. **Shaping exactly once** — `arabic_reshaper.reshape` and
44
+ `bidi.algorithm.get_display` are combined only inside `shape()`. The final
45
+ display strings are produced once per line in the layout pass and cached
46
+ on the block (`display_lines`); the render pass draws the cached strings
47
+ verbatim and never reshapes. Intermediate measurement/wrapping passes are
48
+ direction-agnostic (they shape candidates only transiently to obtain
49
+ metrics and never store them).
50
+ 3. **No mirroring** — no canvas/coordinate flip or `ImageOps.mirror` exists;
51
+ directional work happens solely through Pillow text anchors
52
+ (`"ra"`, `"ma"`, `"la"`).
53
+
54
+ The pytest suite (`tests/`) asserts these properties directly, e.g. that the
55
+ render pass never re-applies `get_display` to already-final display lines and
56
+ that a double-shaped source is rejected by `validate_source`.
57
+
58
+ ## Public API
59
+
60
+ ```python
61
+ from nima_persianguard import NimaPersianGuard, build_scene
62
+
63
+ guard = build_scene(3000, 2000) # full default infographic
64
+ image = guard.render() # validates + renders (single pass)
65
+ guard.update_text("title", "...") # any change marks the scene dirty
66
+ image = guard.render() # full re-validation + re-render
67
+ ```
68
+
69
+ Other entry points:
70
+
71
+ - `NimaPersianGuard(width, height, font_path)` — empty canvas engine;
72
+ `add_text`, `update_text`, `set_font_size`, `resize_canvas`, `validate`,
73
+ `render`, `save`.
74
+ - `nima_persianguard.validation.validate_source(text)` — logical-Unicode gate.
75
+ - `nima_persianguard.textops.shape(text)` — the single shaping point
76
+ (`get_display(reshape(text))`).
77
+
78
+ ## CLI
79
+
80
+ ```
81
+ python -m nima_persianguard --width 3000 --height 2000 -o out.png
82
+ ```
83
+
84
+ Running the module rebuilds the default infographic, performs the mandatory
85
+ resize/re-render invalidation check (2700×1800 → 3000×2000), exercises the
86
+ font-size / text-update hooks and writes an optimized PNG.
87
+
88
+ ## Dependencies
89
+
90
+ - Required: `Pillow`
91
+ - Optional extras (recommended, used by the original renderer when
92
+ available): `arabic-reshaper`, `python-bidi`
93
+
94
+ ```
95
+ pip install Pillow
96
+ pip install arabic-reshaper python-bidi # optional
97
+ ```
98
+
99
+ Font: the original scripts use `/usr/share/fonts/truetype/Vazir.ttf`
100
+ (`Vazir.ttf`); pass an alternative path via `NimaPersianGuard(font_path=...)`
101
+ or `--font`.
102
+
103
+ ## Example
104
+
105
+ `examples/recreate_infographic.py` recreates the original infographic with
106
+ the packaged API:
107
+
108
+ ```
109
+ python examples/recreate_infographic.py -o persian_infographic.png
110
+ ```
111
+
112
+ ## Tests
113
+
114
+ ```
115
+ pytest tests/
116
+ ```
117
+
118
+ Tests focus on the single-pass RTL contract: `shape` is the only shaping
119
+ point, final display lines are cached and never reshaped again, and the
120
+ render pass consumes cached strings.
121
+
122
+ ## Zeno v2 submission note
123
+
124
+ For the Zeno v2 submission of *Nima Persiangard*:
125
+
126
+ 1. **What to submit**
127
+ - the full source archive `nima_persianguard_package.zip` (this package:
128
+ `nima_persianguard/`, `examples/`, `tests/`, `pyproject.toml`,
129
+ `README.md`, `LICENSE`), and
130
+ - the final rendered PNG produced by the example/CLI
131
+ (`persian_infographic_persianguard_v2.png`, 3000×2000, optimized).
132
+ 2. **How to describe the single-pass RTL guarantee** (use this wording or a
133
+ close paraphrase):
134
+ > "Every Persian string is supplied as logical Unicode and is validated at
135
+ > the boundary (`validate_source` rejects presentation forms and bidi
136
+ > control characters). Shaping (`arabic_reshaper.reshape`) followed by bidi
137
+ > reordering (`bidi.algorithm.get_display`) is applied exactly once, only
138
+ > in the final render pass, on cached per-line display strings. All
139
+ > intermediate measurement and layout passes are direction-agnostic and
140
+ > never produce or mutate display strings. There is no canvas flip, no
141
+ > mirror and no second shaping, so text cannot be reversed or
142
+ > double-shaped; this is enforced by automated contract tests in
143
+ > `tests/test_contract.py`."
144
+ 3. Reference `docs/shaping_bidi_review_report.txt` (verbatim audit report)
145
+ and `docs/current_texts.txt` (verbatim user Persian text) as supporting
146
+ evidence.
147
+
148
+ ## License
149
+
150
+ MIT — see `LICENSE`.
@@ -0,0 +1,127 @@
1
+ # nima_persianguard
2
+
3
+ A clean, modular Python package extracted from the original PersianGuard
4
+ infographic scripts (`nima_persianguard.py`,
5
+ `Persian_Infographic-Nima_PersianGuard.py`). It preserves the exact text
6
+ content, layout constants and rendering behavior while separating concerns
7
+ into validation, layout, shaping and rendering modules with a stable public
8
+ API and CLI.
9
+
10
+ ## Single-pass RTL contract
11
+
12
+ The package enforces the audited contract from
13
+ `shaping_bidi_review_report.txt`:
14
+
15
+ 1. **Logical source only** — every text entering the engine is validated by
16
+ `validate_source`: Arabic presentation forms (`U+FB50..U+FDFF`,
17
+ `U+FE70..U+FEFF`) and bidi control characters (`LRE/RLE/LRO/RLO/PDF/
18
+ LRI/RLI/FSI/PDI`) are rejected, so pre-shaped or visual-order text can
19
+ never enter the pipeline.
20
+ 2. **Shaping exactly once** — `arabic_reshaper.reshape` and
21
+ `bidi.algorithm.get_display` are combined only inside `shape()`. The final
22
+ display strings are produced once per line in the layout pass and cached
23
+ on the block (`display_lines`); the render pass draws the cached strings
24
+ verbatim and never reshapes. Intermediate measurement/wrapping passes are
25
+ direction-agnostic (they shape candidates only transiently to obtain
26
+ metrics and never store them).
27
+ 3. **No mirroring** — no canvas/coordinate flip or `ImageOps.mirror` exists;
28
+ directional work happens solely through Pillow text anchors
29
+ (`"ra"`, `"ma"`, `"la"`).
30
+
31
+ The pytest suite (`tests/`) asserts these properties directly, e.g. that the
32
+ render pass never re-applies `get_display` to already-final display lines and
33
+ that a double-shaped source is rejected by `validate_source`.
34
+
35
+ ## Public API
36
+
37
+ ```python
38
+ from nima_persianguard import NimaPersianGuard, build_scene
39
+
40
+ guard = build_scene(3000, 2000) # full default infographic
41
+ image = guard.render() # validates + renders (single pass)
42
+ guard.update_text("title", "...") # any change marks the scene dirty
43
+ image = guard.render() # full re-validation + re-render
44
+ ```
45
+
46
+ Other entry points:
47
+
48
+ - `NimaPersianGuard(width, height, font_path)` — empty canvas engine;
49
+ `add_text`, `update_text`, `set_font_size`, `resize_canvas`, `validate`,
50
+ `render`, `save`.
51
+ - `nima_persianguard.validation.validate_source(text)` — logical-Unicode gate.
52
+ - `nima_persianguard.textops.shape(text)` — the single shaping point
53
+ (`get_display(reshape(text))`).
54
+
55
+ ## CLI
56
+
57
+ ```
58
+ python -m nima_persianguard --width 3000 --height 2000 -o out.png
59
+ ```
60
+
61
+ Running the module rebuilds the default infographic, performs the mandatory
62
+ resize/re-render invalidation check (2700×1800 → 3000×2000), exercises the
63
+ font-size / text-update hooks and writes an optimized PNG.
64
+
65
+ ## Dependencies
66
+
67
+ - Required: `Pillow`
68
+ - Optional extras (recommended, used by the original renderer when
69
+ available): `arabic-reshaper`, `python-bidi`
70
+
71
+ ```
72
+ pip install Pillow
73
+ pip install arabic-reshaper python-bidi # optional
74
+ ```
75
+
76
+ Font: the original scripts use `/usr/share/fonts/truetype/Vazir.ttf`
77
+ (`Vazir.ttf`); pass an alternative path via `NimaPersianGuard(font_path=...)`
78
+ or `--font`.
79
+
80
+ ## Example
81
+
82
+ `examples/recreate_infographic.py` recreates the original infographic with
83
+ the packaged API:
84
+
85
+ ```
86
+ python examples/recreate_infographic.py -o persian_infographic.png
87
+ ```
88
+
89
+ ## Tests
90
+
91
+ ```
92
+ pytest tests/
93
+ ```
94
+
95
+ Tests focus on the single-pass RTL contract: `shape` is the only shaping
96
+ point, final display lines are cached and never reshaped again, and the
97
+ render pass consumes cached strings.
98
+
99
+ ## Zeno v2 submission note
100
+
101
+ For the Zeno v2 submission of *Nima Persiangard*:
102
+
103
+ 1. **What to submit**
104
+ - the full source archive `nima_persianguard_package.zip` (this package:
105
+ `nima_persianguard/`, `examples/`, `tests/`, `pyproject.toml`,
106
+ `README.md`, `LICENSE`), and
107
+ - the final rendered PNG produced by the example/CLI
108
+ (`persian_infographic_persianguard_v2.png`, 3000×2000, optimized).
109
+ 2. **How to describe the single-pass RTL guarantee** (use this wording or a
110
+ close paraphrase):
111
+ > "Every Persian string is supplied as logical Unicode and is validated at
112
+ > the boundary (`validate_source` rejects presentation forms and bidi
113
+ > control characters). Shaping (`arabic_reshaper.reshape`) followed by bidi
114
+ > reordering (`bidi.algorithm.get_display`) is applied exactly once, only
115
+ > in the final render pass, on cached per-line display strings. All
116
+ > intermediate measurement and layout passes are direction-agnostic and
117
+ > never produce or mutate display strings. There is no canvas flip, no
118
+ > mirror and no second shaping, so text cannot be reversed or
119
+ > double-shaped; this is enforced by automated contract tests in
120
+ > `tests/test_contract.py`."
121
+ 3. Reference `docs/shaping_bidi_review_report.txt` (verbatim audit report)
122
+ and `docs/current_texts.txt` (verbatim user Persian text) as supporting
123
+ evidence.
124
+
125
+ ## License
126
+
127
+ MIT — see `LICENSE`.
@@ -0,0 +1,41 @@
1
+ """nima_persianguard - modular Persian shaping/layout validation + infographic renderer.
2
+
3
+ Public API::
4
+
5
+ from nima_persianguard import NimaPersianGuard, build_scene, render_infographic
6
+ """
7
+
8
+ from .engine import NimaPersianGuard, TextItem
9
+ from .scene import build_scene, SCENE_WIDTH, SCENE_HEIGHT
10
+ from .textops import (TextSourceError, ReorderingSkipped, validate_source,
11
+ shape, TextBlock, stats as text_stats)
12
+ from .validator import ValidationReport, ValidationError, validate_layout
13
+
14
+ __version__ = "1.0.0"
15
+
16
+ __all__ = [
17
+ "NimaPersianGuard",
18
+ "TextItem",
19
+ "build_scene",
20
+ "render_infographic",
21
+ "SCENE_WIDTH",
22
+ "SCENE_HEIGHT",
23
+ "validate_source",
24
+ "shape",
25
+ "TextBlock",
26
+ "TextSourceError",
27
+ "ReorderingSkipped",
28
+ "ValidationReport",
29
+ "ValidationError",
30
+ "validate_layout",
31
+ "text_stats",
32
+ "__version__",
33
+ ]
34
+
35
+
36
+ def render_infographic(path: str, width: int = SCENE_WIDTH,
37
+ height: int = SCENE_HEIGHT,
38
+ font_path: str = None) -> str:
39
+ """Build the default 3000x2000 infographic and save it as PNG."""
40
+ guard = build_scene(width, height, font_path=font_path)
41
+ return guard.save(path)
@@ -0,0 +1,60 @@
1
+ """Command-line entrypoint: ``nima-persianguard render`` / ``validate``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import List, Optional
8
+
9
+ from .engine import NimaPersianGuard, DEFAULT_FONT_CANDIDATES
10
+ from .scene import build_scene, SCENE_WIDTH, SCENE_HEIGHT
11
+
12
+
13
+ def _common_args(parser: argparse.ArgumentParser) -> None:
14
+ parser.add_argument("--width", type=int, default=SCENE_WIDTH)
15
+ parser.add_argument("--height", type=int, default=SCENE_HEIGHT)
16
+ parser.add_argument("--font", default=None,
17
+ help="path to a Persian-capable TTF (default: auto)")
18
+ parser.add_argument("--lenient", action="store_true",
19
+ help="do not raise on validation issues; report only")
20
+
21
+
22
+ def main(argv: Optional[List[str]] = None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ prog="nima-persianguard",
25
+ description="PersianGuard: single-pass Persian shaping, validation "
26
+ "and infographic rendering.")
27
+ sub = parser.add_subparsers(dest="command", required=True)
28
+
29
+ p_render = sub.add_parser("render", help="render the default infographic")
30
+ _common_args(p_render)
31
+ p_render.add_argument("-o", "--output", default="persian_infographic.png")
32
+
33
+ p_validate = sub.add_parser("validate", help="validate and print a report")
34
+ _common_args(p_validate)
35
+
36
+ args = parser.parse_args(argv)
37
+ guard = build_scene(args.width, args.height, font_path=args.font)
38
+ guard.strict = not args.lenient
39
+
40
+ if args.command == "validate":
41
+ report = guard.validate()
42
+ print(report.summary())
43
+ return 0 if report.ok else 1
44
+
45
+ # mandatory resize/re-render invalidation check
46
+ w0, h0 = guard.width, guard.height
47
+ guard.resize_canvas(int(w0 * 0.9), int(h0 * 0.9))
48
+ guard.resize_canvas(w0, h0)
49
+ report = guard.validate()
50
+ if not report.ok:
51
+ print(report.summary(), file=sys.stderr)
52
+ return 1
53
+ path = guard.save(args.output)
54
+ print("rendered %s (%dx%d, %d text blocks, state=%s)"
55
+ % (path, guard.width, guard.height, len(guard), guard.state))
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__": # pragma: no cover
60
+ raise SystemExit(main())