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.
- universal_render/__init__.py +277 -0
- universal_render/backend.py +375 -0
- universal_render/compat.py +128 -0
- universal_render/diagnostics.py +249 -0
- universal_render/fonts.py +516 -0
- universal_render/highlevel.py +378 -0
- universal_render/layout.py +967 -0
- universal_render/mpl_support.py +1144 -0
- universal_render/renderer.py +806 -0
- universal_render/scripts.py +311 -0
- universal_render-0.1.0.dist-info/METADATA +189 -0
- universal_render-0.1.0.dist-info/RECORD +15 -0
- universal_render-0.1.0.dist-info/WHEEL +5 -0
- universal_render-0.1.0.dist-info/licenses/LICENSE +21 -0
- universal_render-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# universal_render/scripts.py
|
|
2
|
+
"""
|
|
3
|
+
Unicode script detection, mixed-text run segmentation, and script metadata.
|
|
4
|
+
|
|
5
|
+
This module is the script-awareness core of universal_render:
|
|
6
|
+
|
|
7
|
+
- detect_script(text) -> dominant script name for a string
|
|
8
|
+
- segment_runs(text) -> list of TextRun(text, script) for mixed content
|
|
9
|
+
- script_direction(script) -> "ltr" or "rtl"
|
|
10
|
+
- to_native_numerals(...) -> digit conversion for scripts with native digits
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from bisect import bisect_right
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------
|
|
21
|
+
# Script range table
|
|
22
|
+
# ---------------------------------------------------------------------
|
|
23
|
+
# (start, end_inclusive, script). Kept to the major blocks needed for
|
|
24
|
+
# scientific figure text; unknown codepoints fall back to "Common".
|
|
25
|
+
|
|
26
|
+
_SCRIPT_RANGES: List[Tuple[int, int, str]] = [
|
|
27
|
+
# Basic Latin letters (digits/punct handled as Common below)
|
|
28
|
+
(0x0041, 0x005A, "Latin"),
|
|
29
|
+
(0x0061, 0x007A, "Latin"),
|
|
30
|
+
(0x00C0, 0x024F, "Latin"),
|
|
31
|
+
(0x1E00, 0x1EFF, "Latin"),
|
|
32
|
+
(0x2C60, 0x2C7F, "Latin"),
|
|
33
|
+
(0xA720, 0xA7FF, "Latin"),
|
|
34
|
+
|
|
35
|
+
(0x0370, 0x03FF, "Greek"),
|
|
36
|
+
(0x1F00, 0x1FFF, "Greek"),
|
|
37
|
+
|
|
38
|
+
(0x0400, 0x052F, "Cyrillic"),
|
|
39
|
+
(0x2DE0, 0x2DFF, "Cyrillic"),
|
|
40
|
+
(0xA640, 0xA69F, "Cyrillic"),
|
|
41
|
+
|
|
42
|
+
(0x0530, 0x058F, "Armenian"),
|
|
43
|
+
|
|
44
|
+
(0x0590, 0x05FF, "Hebrew"),
|
|
45
|
+
(0xFB1D, 0xFB4F, "Hebrew"),
|
|
46
|
+
|
|
47
|
+
(0x0600, 0x06FF, "Arabic"),
|
|
48
|
+
(0x0750, 0x077F, "Arabic"),
|
|
49
|
+
(0x08A0, 0x08FF, "Arabic"),
|
|
50
|
+
(0xFB50, 0xFDFF, "Arabic"),
|
|
51
|
+
(0xFE70, 0xFEFF, "Arabic"),
|
|
52
|
+
|
|
53
|
+
(0x0900, 0x097F, "Devanagari"),
|
|
54
|
+
(0xA8E0, 0xA8FF, "Devanagari"),
|
|
55
|
+
|
|
56
|
+
(0x0980, 0x09FF, "Bengali"),
|
|
57
|
+
(0x0A00, 0x0A7F, "Gurmukhi"),
|
|
58
|
+
(0x0A80, 0x0AFF, "Gujarati"),
|
|
59
|
+
(0x0B00, 0x0B7F, "Odia"),
|
|
60
|
+
(0x0B80, 0x0BFF, "Tamil"),
|
|
61
|
+
(0x0C00, 0x0C7F, "Telugu"),
|
|
62
|
+
(0x0C80, 0x0CFF, "Kannada"),
|
|
63
|
+
(0x0D00, 0x0D7F, "Malayalam"),
|
|
64
|
+
(0x0D80, 0x0DFF, "Sinhala"),
|
|
65
|
+
|
|
66
|
+
(0x0E00, 0x0E7F, "Thai"),
|
|
67
|
+
(0x0E80, 0x0EFF, "Lao"),
|
|
68
|
+
(0x0F00, 0x0FFF, "Tibetan"),
|
|
69
|
+
|
|
70
|
+
(0x1000, 0x109F, "Myanmar"),
|
|
71
|
+
(0xA9E0, 0xA9FF, "Myanmar"),
|
|
72
|
+
(0xAA60, 0xAA7F, "Myanmar"),
|
|
73
|
+
|
|
74
|
+
(0x10A0, 0x10FF, "Georgian"),
|
|
75
|
+
(0x1200, 0x139F, "Ethiopic"),
|
|
76
|
+
(0x2D80, 0x2DDF, "Ethiopic"),
|
|
77
|
+
(0xAB00, 0xAB2F, "Ethiopic"),
|
|
78
|
+
|
|
79
|
+
(0x1780, 0x17FF, "Khmer"),
|
|
80
|
+
(0x19E0, 0x19FF, "Khmer"),
|
|
81
|
+
|
|
82
|
+
(0x3040, 0x309F, "Hiragana"),
|
|
83
|
+
(0x30A0, 0x30FF, "Katakana"),
|
|
84
|
+
(0x31F0, 0x31FF, "Katakana"),
|
|
85
|
+
|
|
86
|
+
(0x3400, 0x4DBF, "Han"),
|
|
87
|
+
(0x4E00, 0x9FFF, "Han"),
|
|
88
|
+
(0xF900, 0xFAFF, "Han"),
|
|
89
|
+
|
|
90
|
+
(0x1100, 0x11FF, "Hangul"),
|
|
91
|
+
(0x3130, 0x318F, "Hangul"),
|
|
92
|
+
(0xA960, 0xA97F, "Hangul"),
|
|
93
|
+
(0xAC00, 0xD7FF, "Hangul"),
|
|
94
|
+
|
|
95
|
+
# Combining marks that inherit the script of their base character
|
|
96
|
+
(0x0300, 0x036F, "Inherited"),
|
|
97
|
+
(0x1AB0, 0x1AFF, "Inherited"),
|
|
98
|
+
(0x20D0, 0x20FF, "Inherited"),
|
|
99
|
+
(0xFE00, 0xFE0F, "Inherited"),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
_SCRIPT_RANGES.sort(key=lambda r: r[0])
|
|
103
|
+
_RANGE_STARTS = [r[0] for r in _SCRIPT_RANGES]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
COMMON = "Common"
|
|
107
|
+
INHERITED = "Inherited"
|
|
108
|
+
|
|
109
|
+
#: Scripts written right-to-left.
|
|
110
|
+
RTL_SCRIPTS = frozenset({"Arabic", "Hebrew"})
|
|
111
|
+
|
|
112
|
+
#: All script names this package knows about.
|
|
113
|
+
KNOWN_SCRIPTS = sorted({r[2] for r in _SCRIPT_RANGES} | {COMMON})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------
|
|
117
|
+
# Native digit tables
|
|
118
|
+
# ---------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
_DIGIT_SETS: Dict[str, str] = {
|
|
121
|
+
"Bengali": "০১২৩৪৫৬৭৮৯",
|
|
122
|
+
"Devanagari": "०१२३४५६७८९",
|
|
123
|
+
"Gurmukhi": "੦੧੨੩੪੫੬੭੮੯",
|
|
124
|
+
"Gujarati": "૦૧૨૩૪૫૬૭૮૯",
|
|
125
|
+
"Odia": "୦୧୨୩୪୫୬୭୮୯",
|
|
126
|
+
"Tamil": "௦௧௨௩௪௫௬௭௮௯",
|
|
127
|
+
"Telugu": "౦౧౨౩౪౫౬౭౮౯",
|
|
128
|
+
"Kannada": "೦೧೨೩೪೫೬೭೮೯",
|
|
129
|
+
"Malayalam": "൦൧൨൩൪൫൬൭൮൯",
|
|
130
|
+
"Arabic": "٠١٢٣٤٥٦٧٨٩",
|
|
131
|
+
"Persian": "۰۱۲۳۴۵۶۷۸۹",
|
|
132
|
+
"Thai": "๐๑๒๓๔๕๖๗๘๙",
|
|
133
|
+
"Lao": "໐໑໒໓໔໕໖໗໘໙",
|
|
134
|
+
"Myanmar": "၀၁၂၃၄၅၆၇၈၉",
|
|
135
|
+
"Khmer": "០១២៣៤៥៦៧៨៩",
|
|
136
|
+
"Tibetan": "༠༡༢༣༤༥༦༧༨༩",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_DIGIT_TRANSLATIONS: Dict[str, Dict[int, str]] = {
|
|
140
|
+
script: {ord(str(i)): digits[i] for i in range(10)}
|
|
141
|
+
for script, digits in _DIGIT_SETS.items()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def scripts_with_native_digits() -> List[str]:
|
|
146
|
+
"""Scripts for which to_native_numerals() can convert 0-9."""
|
|
147
|
+
return sorted(_DIGIT_TRANSLATIONS)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def to_native_numerals(value, script: str) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Convert ASCII digits in ``value`` to the native digits of ``script``.
|
|
153
|
+
|
|
154
|
+
Scripts without a native digit table (e.g. Latin, Han) return the
|
|
155
|
+
string unchanged.
|
|
156
|
+
|
|
157
|
+
>>> to_native_numerals(2026, "Bengali")
|
|
158
|
+
'২০২৬'
|
|
159
|
+
"""
|
|
160
|
+
table = _DIGIT_TRANSLATIONS.get(script)
|
|
161
|
+
s = str(value)
|
|
162
|
+
if table is None:
|
|
163
|
+
return s
|
|
164
|
+
return s.translate(table)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------
|
|
168
|
+
# Per-character detection
|
|
169
|
+
# ---------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def detect_char_script(ch: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Return the script name for a single character.
|
|
174
|
+
|
|
175
|
+
Digits, punctuation, whitespace, and symbols return "Common";
|
|
176
|
+
combining marks return "Inherited".
|
|
177
|
+
"""
|
|
178
|
+
cp = ord(ch[0])
|
|
179
|
+
idx = bisect_right(_RANGE_STARTS, cp) - 1
|
|
180
|
+
if idx >= 0:
|
|
181
|
+
start, end, script = _SCRIPT_RANGES[idx]
|
|
182
|
+
if start <= cp <= end:
|
|
183
|
+
return script
|
|
184
|
+
return COMMON
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------
|
|
188
|
+
# String-level detection
|
|
189
|
+
# ---------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def detect_scripts(text: str) -> Dict[str, int]:
|
|
192
|
+
"""
|
|
193
|
+
Count characters per script in ``text``, excluding Common/Inherited.
|
|
194
|
+
"""
|
|
195
|
+
counts: Counter = Counter()
|
|
196
|
+
for ch in str(text):
|
|
197
|
+
script = detect_char_script(ch)
|
|
198
|
+
if script not in (COMMON, INHERITED):
|
|
199
|
+
counts[script] += 1
|
|
200
|
+
return dict(counts)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def detect_script(text: str, default: str = "Latin") -> str:
|
|
204
|
+
"""
|
|
205
|
+
Return the dominant script of ``text``.
|
|
206
|
+
|
|
207
|
+
The dominant script is the one with the most characters, ignoring
|
|
208
|
+
Common (digits, punctuation, spaces) and Inherited (combining marks)
|
|
209
|
+
characters. If no script characters are present at all — e.g. the
|
|
210
|
+
text is purely numeric — ``default`` is returned.
|
|
211
|
+
"""
|
|
212
|
+
counts = detect_scripts(text)
|
|
213
|
+
if not counts:
|
|
214
|
+
return default
|
|
215
|
+
return max(counts.items(), key=lambda kv: kv[1])[0]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def script_direction(script: str) -> str:
|
|
219
|
+
"""Return "rtl" for right-to-left scripts, else "ltr"."""
|
|
220
|
+
return "rtl" if script in RTL_SCRIPTS else "ltr"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def is_rtl_text(text: str) -> bool:
|
|
224
|
+
"""True when the dominant script of ``text`` is right-to-left."""
|
|
225
|
+
return script_direction(detect_script(text)) == "rtl"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_mixed_script(text: str) -> bool:
|
|
229
|
+
"""True when ``text`` contains characters from 2+ scripts."""
|
|
230
|
+
return len(detect_scripts(text)) >= 2
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------
|
|
234
|
+
# Run segmentation for mixed text
|
|
235
|
+
# ---------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class TextRun:
|
|
239
|
+
"""A maximal substring whose characters share one script."""
|
|
240
|
+
text: str
|
|
241
|
+
script: str
|
|
242
|
+
start: int # index into the original string
|
|
243
|
+
end: int # exclusive
|
|
244
|
+
|
|
245
|
+
def __len__(self) -> int:
|
|
246
|
+
return self.end - self.start
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def segment_runs(text: str, default: str = "Latin") -> List[TextRun]:
|
|
250
|
+
"""
|
|
251
|
+
Split ``text`` into maximal same-script runs.
|
|
252
|
+
|
|
253
|
+
Common characters (spaces, digits, punctuation) and Inherited marks
|
|
254
|
+
are merged into the preceding run so that "বাংলা 2026 data" yields
|
|
255
|
+
runs ["বাংলা 2026 ", "data"] rather than five fragments. A leading
|
|
256
|
+
Common run attaches to the first real script that follows; a string
|
|
257
|
+
with no script characters at all becomes a single ``default`` run.
|
|
258
|
+
"""
|
|
259
|
+
text = str(text)
|
|
260
|
+
if not text:
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
# First pass: raw per-character scripts with Common/Inherited resolved
|
|
264
|
+
# to the previous concrete script when possible.
|
|
265
|
+
raw: List[str] = []
|
|
266
|
+
prev: Optional[str] = None
|
|
267
|
+
for ch in text:
|
|
268
|
+
script = detect_char_script(ch)
|
|
269
|
+
if script in (COMMON, INHERITED):
|
|
270
|
+
raw.append(prev if prev is not None else COMMON)
|
|
271
|
+
else:
|
|
272
|
+
raw.append(script)
|
|
273
|
+
prev = script
|
|
274
|
+
|
|
275
|
+
# Resolve any leading Common region to the first concrete script.
|
|
276
|
+
first_concrete = next((s for s in raw if s != COMMON), default)
|
|
277
|
+
resolved = [first_concrete if s == COMMON else s for s in raw]
|
|
278
|
+
|
|
279
|
+
# Second pass: group consecutive equal scripts into runs.
|
|
280
|
+
runs: List[TextRun] = []
|
|
281
|
+
run_start = 0
|
|
282
|
+
for i in range(1, len(resolved) + 1):
|
|
283
|
+
if i == len(resolved) or resolved[i] != resolved[run_start]:
|
|
284
|
+
runs.append(TextRun(
|
|
285
|
+
text=text[run_start:i],
|
|
286
|
+
script=resolved[run_start],
|
|
287
|
+
start=run_start,
|
|
288
|
+
end=i,
|
|
289
|
+
))
|
|
290
|
+
run_start = i
|
|
291
|
+
return runs
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def describe_text(text: str) -> Dict[str, object]:
|
|
295
|
+
"""
|
|
296
|
+
Diagnostic summary of a string: dominant script, direction,
|
|
297
|
+
mixed-script flag, and the segmented runs. Useful for debugging
|
|
298
|
+
and for reporting in evaluation experiments.
|
|
299
|
+
"""
|
|
300
|
+
dominant = detect_script(text)
|
|
301
|
+
return {
|
|
302
|
+
"text": str(text),
|
|
303
|
+
"dominant_script": dominant,
|
|
304
|
+
"direction": script_direction(dominant),
|
|
305
|
+
"is_mixed": is_mixed_script(text),
|
|
306
|
+
"script_counts": detect_scripts(text),
|
|
307
|
+
"runs": [
|
|
308
|
+
{"text": r.text, "script": r.script, "start": r.start, "end": r.end}
|
|
309
|
+
for r in segment_runs(text)
|
|
310
|
+
],
|
|
311
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: universal-render
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal complex-script text rendering for Matplotlib and Seaborn using Qt/HarfBuzz shaping.
|
|
5
|
+
Author-email: Mrinal Basak Shuvo <mbs.jr.2009@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mbs57/universal-render
|
|
8
|
+
Project-URL: Repository, https://github.com/mbs57/universal-render
|
|
9
|
+
Project-URL: Issues, https://github.com/mbs57/universal-render/issues
|
|
10
|
+
Keywords: matplotlib,unicode,complex scripts,text shaping,multilingual,visualization
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
16
|
+
Classifier: Topic :: Text Processing :: Fonts
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Requires-Dist: matplotlib>=3.5
|
|
22
|
+
Requires-Dist: PySide6>=6.4
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/mbs57/universal-render/master/assets/universal_render.jpg" alt="universal-render" width="720">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<h1 align="center">universal-render</h1>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<b>Universal complex-script text rendering for Matplotlib and Seaborn, powered by Qt/HarfBuzz shaping.</b>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img alt="Python" src="https://img.shields.io/badge/python-3.9%2B-blue">
|
|
37
|
+
<img alt="License" src="https://img.shields.io/badge/license-MIT-green">
|
|
38
|
+
<img alt="Scripts" src="https://img.shields.io/badge/scripts%20verified-22-orange">
|
|
39
|
+
<img alt="Tests" src="https://img.shields.io/badge/tests-34%20passing-brightgreen">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
Matplotlib's text engine cannot shape complex scripts — Bengali conjuncts break apart, Arabic letters don't join, Tamil and Thai vowels land in the wrong place, and often all you get is tofu boxes (▯▯▯). `universal_render` routes text through Qt's shaping engine (HarfBuzz) and embeds the correctly shaped result into your figures, with **automatic script detection and per-script font fallback** — no font configuration needed.
|
|
45
|
+
|
|
46
|
+
## Before / After
|
|
47
|
+
|
|
48
|
+
Left: Matplotlib's own text engine. Right: the same text through `universal_render` — 22 scripts, same machine, zero configuration.
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="https://raw.githubusercontent.com/mbs57/universal-render/master/assets/comparison_before_after.png" alt="Matplotlib default vs universal_render across 22 scripts" width="900">
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
And it isn't just tofu: even with the *correct font installed*, Matplotlib (and, on Windows, mplcairo and Pillow — their official wheels ship without Raqm/HarfBuzz) silently renders complex scripts unshaped: Arabic unjoined, Bengali conjuncts split, with **zero warnings**. See [`evaluation/`](evaluation/) for the measured comparison.
|
|
55
|
+
|
|
56
|
+
## ✨ Features
|
|
57
|
+
|
|
58
|
+
- Automatic Unicode script detection (`detect_script`, `segment_runs`)
|
|
59
|
+
- Per-script font fallback with glyph-coverage validation (`auto_font_fallback`)
|
|
60
|
+
- Correct shaping for Indic (Bengali, Hindi, Tamil, Telugu, …), RTL (Arabic, Hebrew), Southeast Asian (Thai, Khmer, Lao, Myanmar), CJK, and more — **22 scripts verified** by the built-in `self_test()`
|
|
61
|
+
- Mixed-script text: Bengali + English + numbers in one string
|
|
62
|
+
- Native numerals for 16 scripts (`to_native_numerals(2026, "Bengali")` → `২০২৬`)
|
|
63
|
+
- Drop-in replacements for titles, axis labels, tick labels, legends, annotations, and heatmap cell text
|
|
64
|
+
- Matplotlib-style text options: `weight="bold"`, `italic=True`, `alpha=0.5`, `rotation=45` (any angle, including rotated tick labels), multiline `"\n"` strings, title `loc="left"/"center"/"right"`, and a proper figure `suptitle`
|
|
65
|
+
- DPI-safe layout: titles, labels, and ticks stay correctly placed at any `savefig(dpi=..., bbox_inches="tight")`
|
|
66
|
+
- Works headless (Colab/Kaggle/CI) via Qt offscreen mode
|
|
67
|
+
- Drop-in `bangla_render` compatibility: `import universal_render.compat as br`
|
|
68
|
+
|
|
69
|
+
## 🚀 Quick Example
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import matplotlib.pyplot as plt
|
|
73
|
+
import universal_render as ur
|
|
74
|
+
|
|
75
|
+
fig, ax = plt.subplots()
|
|
76
|
+
ax.plot([1, 2, 3], [3, 1, 4])
|
|
77
|
+
|
|
78
|
+
ur.set_multilingual_title(ax, "বাংলা শিরোনাম") # Bengali — font auto-selected
|
|
79
|
+
ur.set_multilingual_xlabel(ax, "أشهر السنة") # Arabic — shaped + RTL
|
|
80
|
+
ur.set_multilingual_ylabel(ax, "மாத விற்பனை") # Tamil
|
|
81
|
+
|
|
82
|
+
ur.set_multilingual_xticks(ax, [1, 2, 3], ["एक", "दो", "तीन"]) # Hindi
|
|
83
|
+
ur.apply_multilingual_layout(fig, auto=True)
|
|
84
|
+
|
|
85
|
+
plt.savefig("plot.png", dpi=300)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
<p align="center">
|
|
89
|
+
<img src="https://raw.githubusercontent.com/mbs57/universal-render/master/assets/line_plot_six_scripts.png" alt="Line plot with Bengali, Cyrillic, Tamil, Arabic, Hindi, Thai and English labels" width="640">
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
### One-call APIs
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# Label a whole plot at once — each string picks its own script's font
|
|
96
|
+
ur.localize_axes(
|
|
97
|
+
ax,
|
|
98
|
+
title="বিক্রয় প্রতিবেদন ২০২৬",
|
|
99
|
+
xlabel="महीना", ylabel="மதிப்பு",
|
|
100
|
+
xticklabels=["জানু", "फ़र", "மார்", "April"],
|
|
101
|
+
legend_labels=["পূর্বাভাস"],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Annotated heatmap with native Bengali digits and auto text contrast
|
|
105
|
+
ur.multilingual_heatmap(
|
|
106
|
+
ax, data, value_format=".2f", value_script="Bengali",
|
|
107
|
+
row_labels=["ঢাকা", "চট্টগ্রাম", "খুলনা"],
|
|
108
|
+
col_labels=["جودة", "คุณภาพ", "Quality"],
|
|
109
|
+
title="গুণমান ম্যাট্রিক্স",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Verify this machine renders all 22 supported scripts
|
|
113
|
+
ur.self_test()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
<p align="center">
|
|
117
|
+
<img src="https://raw.githubusercontent.com/mbs57/universal-render/master/assets/heatmap_six_scripts.png" alt="Heatmap with per-cell text in five scripts" width="480">
|
|
118
|
+
<img src="https://raw.githubusercontent.com/mbs57/universal-render/master/assets/styling_showcase.png" alt="Bold suptitle, left-aligned mixed-script title, rotated Bengali ticks" width="480">
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
## 📦 API Overview
|
|
122
|
+
|
|
123
|
+
| Area | Functions |
|
|
124
|
+
|---|---|
|
|
125
|
+
| High-level (one call) | `localize_axes`, `multilingual_heatmap`, `multilingual_bar_labels`, `language_to_script`, `set_language_font`, `font_for_language`, `localized_numerals`, `supported_languages` |
|
|
126
|
+
| Script detection | `detect_script`, `segment_runs`, `is_rtl_text`, `is_mixed_script`, `describe_text`, `to_native_numerals`, `script_direction` |
|
|
127
|
+
| Fonts | `auto_font_fallback`, `find_font_for_script`, `set_script_font`, `register_font`, `validate_font`, `font_covers_text`, `coverage_report` |
|
|
128
|
+
| Rendering | `render_text`, `render_text_array`, `render_mixed_text`, `render_paragraph`, `measure_text`, `set_render_defaults`, render cache controls |
|
|
129
|
+
| Matplotlib | `set_multilingual_title/xlabel/ylabel/suptitle`, `set_multilingual_xticks/yticks/numeric_ticks`, `set_multilingual_legend`, `multilingual_text`, `annotate_multilingual`, `add_multilingual_cell_text`, `multilingual_paragraph`, `apply_multilingual_layout` |
|
|
130
|
+
| Diagnostics / evaluation | `self_test` (per-script render check), `benchmark_render` (cold/warm latency), `save_comparison_figure` (before/after figure) |
|
|
131
|
+
| bangla-render compat | `import universal_render.compat as br` — every `set_bangla_*` name works unchanged |
|
|
132
|
+
|
|
133
|
+
## 🌍 Supported Script Families
|
|
134
|
+
|
|
135
|
+
| Family | Scripts (verified by `self_test()`) | Direction |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| Indic / Brahmic | Bengali, Devanagari (Hindi/Marathi/Nepali), Tamil, Telugu, Kannada, Malayalam, Gujarati, Gurmukhi (Punjabi), Odia, Sinhala | LTR |
|
|
138
|
+
| Right-to-left | Arabic (Arabic/Urdu/Persian), Hebrew | RTL |
|
|
139
|
+
| Southeast Asian | Thai, Lao, Khmer, Myanmar | LTR |
|
|
140
|
+
| East Asian | Han (Chinese), Hangul (Korean), Hiragana (Japanese) | LTR |
|
|
141
|
+
| European | Latin, Greek, Cyrillic | LTR |
|
|
142
|
+
|
|
143
|
+
Script *detection* additionally covers Katakana, Tibetan, Georgian, Armenian, and Ethiopic. One script serves many languages — `language_to_script()` maps ~60 language names.
|
|
144
|
+
|
|
145
|
+
## 📊 Evaluation
|
|
146
|
+
|
|
147
|
+
[`evaluation/compare_backends.py`](evaluation/compare_backends.py) renders the same text with the same font file through every backend, so the shaping engine is the only variable. Findings on Windows (Python 3.11):
|
|
148
|
+
|
|
149
|
+
| Backend | Bengali | Arabic | Notes |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `universal_render` | ✅ correct | ✅ correct | reference (Qt/HarfBuzz) |
|
|
152
|
+
| Matplotlib, default font | ▯▯▯ tofu (104 warnings) | unjoined | at least it warns |
|
|
153
|
+
| Matplotlib, correct font | broken conjuncts, **0 warnings** | unjoined, **0 warnings** | silent failure |
|
|
154
|
+
| mplcairo 0.6.1 (pip wheel) | broken conjuncts, **0 warnings** | unjoined, **0 warnings** | wheel ships without Raqm |
|
|
155
|
+
| Pillow (pip wheel) | — | — | `PIL.features.check("raqm")` → `False` |
|
|
156
|
+
|
|
157
|
+
## 🛠 Installation
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install PySide6 matplotlib numpy
|
|
161
|
+
pip install git+https://github.com/mbs57/universal-render.git
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Fonts: on Windows, everything works out of the box (Nirmala UI, Segoe UI, Leelawadee UI…). On Linux/Colab, install Noto fonts (`fonts-noto` / `fonts-noto-cjk`) — the fallback tables pick them up automatically. Verify any environment with:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import universal_render as ur
|
|
168
|
+
ur.self_test() # prints a per-script pass/fail table
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 🧪 Tests
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
py tests/test_universal_render.py # or: pytest tests/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Limitations
|
|
178
|
+
|
|
179
|
+
- Text is embedded as high-resolution raster images; SVG/PDF exports contain images rather than selectable vector glyphs.
|
|
180
|
+
- Arbitrary-angle rotated tick labels are center-anchored under their tick (Matplotlib anchors the label end).
|
|
181
|
+
- Scripts requiring vertical layout (e.g. traditional Mongolian) are not supported.
|
|
182
|
+
|
|
183
|
+
## Citation
|
|
184
|
+
|
|
185
|
+
If you use universal-render in academic work, please cite the bangla-render paper (SoftwareX) for now — a dedicated paper for the universal framework is in preparation.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
[MIT](LICENSE) © 2026 Mrinal Basak Shuvo
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
universal_render/__init__.py,sha256=JhWoQkYUhm7u9qAoCdt6MHUa7VAwg3EoajMu9MeS3wA,7295
|
|
2
|
+
universal_render/backend.py,sha256=B19MrexotD5jSmd8np0hP64QWM11v1W4u_g4p3iZmEQ,11629
|
|
3
|
+
universal_render/compat.py,sha256=35XJxwIMbLMnpFdtaBYGjgjx90myGK_JBefgnvh1vhA,3573
|
|
4
|
+
universal_render/diagnostics.py,sha256=5SD24aWpzyNUtI2ZKtrWC0K4nPtPBvB-rU85lZft3Os,8977
|
|
5
|
+
universal_render/fonts.py,sha256=0YFxjXRQAIDyQ68b_8TRjR3aHL0XwQ1dsH_CS91v-_k,16340
|
|
6
|
+
universal_render/highlevel.py,sha256=cRsBYvvmaKYFrKWmAiwAHCbqy06R9ywypY2T_PE6dcs,12997
|
|
7
|
+
universal_render/layout.py,sha256=UKHid0SOhmaizHtSZfKqAIJ6YgWIk5hbquj6KmnwCIw,39796
|
|
8
|
+
universal_render/mpl_support.py,sha256=NLOB0NVqo0GrL3B0Wr13rZZU83FjsTCohA4e-8qB_Z8,28830
|
|
9
|
+
universal_render/renderer.py,sha256=Gr3Wo7_1pUdXNCPBINSDPBKnhiiQz8GzG4TgTFjgKMU,22990
|
|
10
|
+
universal_render/scripts.py,sha256=A8z5TF3soJLIG7jcb_95h_05p7Kc-bQZKUFzmejvKuA,9897
|
|
11
|
+
universal_render-0.1.0.dist-info/licenses/LICENSE,sha256=qiPxk3IGKMqz6cM_DDargKeILWsO29OhB2pf6lou8Rw,1075
|
|
12
|
+
universal_render-0.1.0.dist-info/METADATA,sha256=213I8giVNnroK2-5bwTTZH3Jhl1fqs4ME3KCkENaOw0,10051
|
|
13
|
+
universal_render-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
14
|
+
universal_render-0.1.0.dist-info/top_level.txt,sha256=iuPHAOGPhOaeiS8O2MuoC_RqYgT34O3A6xoSEEBUsE4,17
|
|
15
|
+
universal_render-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mrinal Basak Shuvo
|
|
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 @@
|
|
|
1
|
+
universal_render
|