universal-render 0.1.0__py3-none-any.whl

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,128 @@
1
+ # universal_render/compat.py
2
+ """
3
+ bangla_render compatibility layer.
4
+
5
+ Existing bangla-render code keeps working after switching the import:
6
+
7
+ # old: import bangla_render as br
8
+ import universal_render.compat as br
9
+
10
+ br.set_bangla_title(ax, "বাংলা শিরোনাম")
11
+ br.set_bangla_xticks(ax, [1, 2], ["এক", "দুই"])
12
+
13
+ Every set_bangla_* name maps to its set_multilingual_* equivalent; the
14
+ behavior is identical for Bengali text (same Qt pipeline, same layout
15
+ manager), with automatic script-aware font fallback as a bonus.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from .backend import ( # noqa: F401
20
+ check_environment,
21
+ ensure_qt_application,
22
+ get_renderer_status,
23
+ init_renderer,
24
+ is_colab_environment,
25
+ is_headless_environment,
26
+ is_kaggle_environment,
27
+ is_notebook_environment,
28
+ reset_renderer_state,
29
+ )
30
+ from .fonts import ( # noqa: F401
31
+ get_default_font,
32
+ list_available_fonts,
33
+ list_registered_fonts,
34
+ register_font,
35
+ register_fonts,
36
+ resolve_font,
37
+ set_default_font,
38
+ validate_font,
39
+ font_info,
40
+ SCRIPT_FONT_CANDIDATES,
41
+ )
42
+ from .renderer import ( # noqa: F401
43
+ clear_render_cache,
44
+ get_render_cache_info,
45
+ get_render_defaults,
46
+ measure_text,
47
+ render_paragraph,
48
+ render_paragraph_qimage,
49
+ render_text,
50
+ render_text_qimage,
51
+ set_render_cache_maxsize,
52
+ set_render_defaults,
53
+ )
54
+ from .layout import ( # noqa: F401
55
+ clear_layout_manager,
56
+ get_layout_manager,
57
+ )
58
+ from .scripts import to_native_numerals
59
+ from .mpl_support import (
60
+ add_multilingual_cell_text,
61
+ annotate_multilingual,
62
+ apply_multilingual_layout,
63
+ multilingual_paragraph,
64
+ multilingual_text,
65
+ set_multilingual_legend,
66
+ set_multilingual_numeric_ticks,
67
+ set_multilingual_suptitle,
68
+ set_multilingual_title,
69
+ set_multilingual_xlabel,
70
+ set_multilingual_xticks,
71
+ set_multilingual_ylabel,
72
+ set_multilingual_yticks,
73
+ )
74
+
75
+ # Bengali-candidate list under its historical name
76
+ BANGLA_FONT_CANDIDATES = list(SCRIPT_FONT_CANDIDATES["Bengali"])
77
+
78
+
79
+ def to_bangla_numerals(value):
80
+ """Convert ASCII digits to Bengali digits (bangla_render API)."""
81
+ return to_native_numerals(value, "Bengali")
82
+
83
+
84
+ # Drop-in aliases -----------------------------------------------------
85
+
86
+ set_bangla_title = set_multilingual_title
87
+ set_bangla_xlabel = set_multilingual_xlabel
88
+ set_bangla_ylabel = set_multilingual_ylabel
89
+ set_bangla_suptitle = set_multilingual_suptitle
90
+ set_bangla_xticks = set_multilingual_xticks
91
+ set_bangla_yticks = set_multilingual_yticks
92
+ set_bangla_legend = set_multilingual_legend
93
+ bangla_text = multilingual_text
94
+ annotate_bangla = annotate_multilingual
95
+ add_bangla_in_cell = add_multilingual_cell_text
96
+ bangla_paragraph = multilingual_paragraph
97
+ apply_bangla_layout = apply_multilingual_layout
98
+
99
+
100
+ def set_bangla_numeric_ticks(ax, *args, **kwargs):
101
+ """bangla_render numeric ticks: always Bengali digits."""
102
+ kwargs.setdefault("script", "Bengali")
103
+ return set_multilingual_numeric_ticks(ax, *args, **kwargs)
104
+
105
+
106
+ def text(ax, *args, **kwargs):
107
+ """Alias for bangla_text()/multilingual_text()."""
108
+ return multilingual_text(ax, *args, **kwargs)
109
+
110
+
111
+ __all__ = [
112
+ "BANGLA_FONT_CANDIDATES",
113
+ "to_bangla_numerals",
114
+ "set_bangla_title",
115
+ "set_bangla_xlabel",
116
+ "set_bangla_ylabel",
117
+ "set_bangla_suptitle",
118
+ "set_bangla_xticks",
119
+ "set_bangla_yticks",
120
+ "set_bangla_legend",
121
+ "set_bangla_numeric_ticks",
122
+ "bangla_text",
123
+ "annotate_bangla",
124
+ "add_bangla_in_cell",
125
+ "bangla_paragraph",
126
+ "apply_bangla_layout",
127
+ "text",
128
+ ]
@@ -0,0 +1,249 @@
1
+ # universal_render/diagnostics.py
2
+ """
3
+ Self-testing, benchmarking, and evaluation figure generation.
4
+
5
+ - self_test() : does each script actually render here?
6
+ - benchmark_render() : cold/warm render latency statistics
7
+ - save_comparison_figure() : side-by-side default-Matplotlib vs
8
+ universal_render figure (paper-ready)
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Any, Dict, List, Optional, Sequence
14
+
15
+ import numpy as np
16
+
17
+ from .fonts import auto_font_fallback, font_covers_text
18
+ from .renderer import (
19
+ clear_render_cache,
20
+ render_text_array,
21
+ )
22
+ from .scripts import detect_script, script_direction
23
+
24
+
25
+ #: Known-tricky sample words per script: conjuncts, reordering vowels,
26
+ #: contextual joining, stacked marks. Used by self_test() and the
27
+ #: comparison figure.
28
+ SCRIPT_TEST_SAMPLES: Dict[str, str] = {
29
+ "Bengali": "বিশ্ববিদ্যালয়ের শিক্ষার্থী",
30
+ "Devanagari": "विश्वविद्यालय क्षेत्र",
31
+ "Tamil": "பல்கலைக்கழகம்",
32
+ "Telugu": "విశ్వవిద్యాలయం",
33
+ "Kannada": "ವಿಶ್ವವಿದ್ಯಾಲಯ",
34
+ "Malayalam": "സർവ്വകലാശാല",
35
+ "Gujarati": "યુનિવર્સિટી",
36
+ "Gurmukhi": "ਯੂਨੀਵਰਸਿਟੀ",
37
+ "Odia": "ବିଶ୍ୱବିଦ୍ୟାଳୟ",
38
+ "Sinhala": "විශ්වවිද්‍යාලය",
39
+ "Arabic": "جامعة القاهرة",
40
+ "Hebrew": "אוניברסיטה",
41
+ "Thai": "มหาวิทยาลัย",
42
+ "Lao": "ມະຫາວິທະຍາໄລ",
43
+ "Khmer": "សាកលវិទ្យាល័យ",
44
+ "Myanmar": "တက္ကသိုလ်",
45
+ "Han": "大学研究",
46
+ "Hangul": "대학교 연구",
47
+ "Hiragana": "だいがく",
48
+ "Greek": "Πανεπιστήμιο",
49
+ "Cyrillic": "Университет",
50
+ "Latin": "University",
51
+ }
52
+
53
+
54
+ # ---------------------------------------------------------------------
55
+ # Self test
56
+ # ---------------------------------------------------------------------
57
+
58
+ def _ink_pixels(arr: np.ndarray) -> int:
59
+ """Count non-transparent pixels in an RGBA uint8 array."""
60
+ return int((arr[:, :, 3] > 0).sum())
61
+
62
+
63
+ def self_test(
64
+ scripts: Optional[Sequence[str]] = None,
65
+ font_size: int = 32,
66
+ verbose: bool = True,
67
+ ) -> Dict[str, Dict[str, Any]]:
68
+ """
69
+ Render a known-tricky sample for each script and report whether this
70
+ environment can display it.
71
+
72
+ For each script: the resolved font, whether that font actually has
73
+ the glyphs (coverage), whether the render produced visible ink, and
74
+ a pass/fail flag. Returns {script: report} and prints a table when
75
+ ``verbose``.
76
+ """
77
+ chosen = list(scripts) if scripts else list(SCRIPT_TEST_SAMPLES)
78
+ results: Dict[str, Dict[str, Any]] = {}
79
+
80
+ for script in chosen:
81
+ sample = SCRIPT_TEST_SAMPLES.get(script)
82
+ if sample is None:
83
+ results[script] = {"ok": False, "error": "no test sample defined"}
84
+ continue
85
+
86
+ report: Dict[str, Any] = {"sample": sample}
87
+ try:
88
+ report["detected_script"] = detect_script(sample)
89
+ report["direction"] = script_direction(script)
90
+ family = auto_font_fallback(sample)
91
+ report["font_family"] = family
92
+ report["glyph_coverage"] = font_covers_text(family, sample)
93
+ arr = render_text_array(sample, font_size=font_size)
94
+ ink = _ink_pixels(arr)
95
+ report["image_size"] = (arr.shape[1], arr.shape[0])
96
+ report["ink_pixels"] = ink
97
+ report["rendered_nonempty"] = ink > 0
98
+ report["ok"] = bool(
99
+ report["detected_script"] == script
100
+ and report["glyph_coverage"]
101
+ and report["rendered_nonempty"]
102
+ )
103
+ except Exception as e:
104
+ report["ok"] = False
105
+ report["error"] = str(e)
106
+ results[script] = report
107
+
108
+ if verbose:
109
+ print(f"{'script':<12} {'ok':<4} {'font':<24} {'coverage':<9} {'ink_px':>8}")
110
+ print("-" * 62)
111
+ for script, r in results.items():
112
+ print(
113
+ f"{script:<12} "
114
+ f"{'yes' if r.get('ok') else 'NO ':<4} "
115
+ f"{str(r.get('font_family', '-')):<24} "
116
+ f"{str(r.get('glyph_coverage', '-')):<9} "
117
+ f"{r.get('ink_pixels', 0):>8}"
118
+ )
119
+ n_ok = sum(1 for r in results.values() if r.get("ok"))
120
+ print("-" * 62)
121
+ print(f"{n_ok}/{len(results)} scripts render correctly in this environment")
122
+
123
+ return results
124
+
125
+
126
+ # ---------------------------------------------------------------------
127
+ # Benchmark
128
+ # ---------------------------------------------------------------------
129
+
130
+ def benchmark_render(
131
+ texts: Optional[Sequence[str]] = None,
132
+ font_size: int = 32,
133
+ repeats: int = 20,
134
+ ) -> Dict[str, Any]:
135
+ """
136
+ Measure render latency, cold (cache cleared each call) and warm
137
+ (cache hits). Returns per-text and aggregate statistics in
138
+ milliseconds — the raw material for the paper's performance table.
139
+ """
140
+ if texts is None:
141
+ texts = list(SCRIPT_TEST_SAMPLES.values())
142
+
143
+ per_text: List[Dict[str, Any]] = []
144
+
145
+ for text in texts:
146
+ cold: List[float] = []
147
+ for _ in range(repeats):
148
+ clear_render_cache()
149
+ t0 = time.perf_counter()
150
+ render_text_array(text, font_size=font_size)
151
+ cold.append((time.perf_counter() - t0) * 1000.0)
152
+
153
+ # Prime the cache once, then measure hits
154
+ render_text_array(text, font_size=font_size)
155
+ warm: List[float] = []
156
+ for _ in range(repeats):
157
+ t0 = time.perf_counter()
158
+ render_text_array(text, font_size=font_size)
159
+ warm.append((time.perf_counter() - t0) * 1000.0)
160
+
161
+ per_text.append({
162
+ "text": text,
163
+ "script": detect_script(text),
164
+ "cold_ms_mean": float(np.mean(cold)),
165
+ "cold_ms_std": float(np.std(cold)),
166
+ "warm_ms_mean": float(np.mean(warm)),
167
+ "warm_ms_std": float(np.std(warm)),
168
+ })
169
+
170
+ return {
171
+ "font_size": font_size,
172
+ "repeats": repeats,
173
+ "per_text": per_text,
174
+ "cold_ms_mean_overall": float(np.mean([r["cold_ms_mean"] for r in per_text])),
175
+ "warm_ms_mean_overall": float(np.mean([r["warm_ms_mean"] for r in per_text])),
176
+ }
177
+
178
+
179
+ # ---------------------------------------------------------------------
180
+ # Before/after comparison figure
181
+ # ---------------------------------------------------------------------
182
+
183
+ def save_comparison_figure(
184
+ output_path: str,
185
+ samples: Optional[Dict[str, str]] = None,
186
+ font_size: int = 30,
187
+ dpi: int = 200,
188
+ left_title: str = "Matplotlib default",
189
+ right_title: str = "universal_render",
190
+ ):
191
+ """
192
+ Produce a two-column figure: each sample rendered by Matplotlib's
193
+ own text engine (left) and by universal_render (right). This is the
194
+ before/after evidence figure for each script family.
195
+
196
+ ``samples`` maps a row label to the text, defaulting to
197
+ SCRIPT_TEST_SAMPLES. Returns the Matplotlib figure.
198
+
199
+ Matplotlib emits "glyph missing" warnings for the left panel; that
200
+ is the defect being demonstrated, so those warnings are suppressed.
201
+ """
202
+ import warnings
203
+
204
+ import matplotlib.pyplot as plt
205
+ from matplotlib.offsetbox import AnnotationBbox, OffsetImage
206
+
207
+ if samples is None:
208
+ samples = dict(SCRIPT_TEST_SAMPLES)
209
+
210
+ n = len(samples)
211
+ fig_h = max(2.0, 0.62 * n + 1.0)
212
+ fig, axes = plt.subplots(1, 2, figsize=(11, fig_h))
213
+
214
+ for ax, title in zip(axes, (left_title, right_title)):
215
+ ax.set_xlim(0, 1)
216
+ ax.set_ylim(0, n)
217
+ ax.set_title(title, fontsize=13)
218
+ ax.axis("off")
219
+
220
+ for i, (label, text) in enumerate(samples.items()):
221
+ y = n - i - 0.5
222
+
223
+ # Row label on the left margin of the left panel
224
+ axes[0].text(
225
+ -0.02, y, label, fontsize=10, ha="right", va="center",
226
+ transform=axes[0].transData, clip_on=False,
227
+ )
228
+
229
+ # Left: Matplotlib's own text engine (typically broken shaping)
230
+ axes[0].text(
231
+ 0.5, y, text, fontsize=font_size * 0.55,
232
+ ha="center", va="center",
233
+ )
234
+
235
+ # Right: Qt-shaped image artist
236
+ arr = render_text_array(text, font_size=font_size) / 255.0
237
+ oi = OffsetImage(arr, zoom=0.5)
238
+ ab = AnnotationBbox(
239
+ oi, (0.5, y), frameon=False, box_alignment=(0.5, 0.5),
240
+ annotation_clip=False,
241
+ )
242
+ axes[1].add_artist(ab)
243
+
244
+ with warnings.catch_warnings():
245
+ warnings.filterwarnings("ignore", message=".*[Gg]lyph.*missing.*")
246
+ warnings.filterwarnings("ignore", message=".*does not support.*")
247
+ fig.tight_layout()
248
+ fig.savefig(output_path, dpi=dpi, bbox_inches="tight")
249
+ return fig