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,277 @@
|
|
|
1
|
+
# universal_render/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
universal_render: universal complex-script text rendering for Matplotlib.
|
|
5
|
+
|
|
6
|
+
Text is shaped through Qt's text stack (HarfBuzz), so scripts that
|
|
7
|
+
Matplotlib's own text engine cannot shape — Bengali, Hindi, Tamil,
|
|
8
|
+
Arabic, Thai, and other complex scripts — render correctly in titles,
|
|
9
|
+
axis labels, tick labels, annotations, legends, and heatmap cells.
|
|
10
|
+
Fonts are chosen automatically from each string's dominant Unicode
|
|
11
|
+
script, with per-script fallback tables and glyph-coverage validation.
|
|
12
|
+
|
|
13
|
+
Typical usage:
|
|
14
|
+
|
|
15
|
+
import universal_render as ur
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
ur.init_renderer()
|
|
19
|
+
|
|
20
|
+
fig, ax = plt.subplots()
|
|
21
|
+
ax.plot([1, 2, 3], [3, 1, 4])
|
|
22
|
+
|
|
23
|
+
ur.set_multilingual_title(ax, "বাংলা শিরোনাম") # Bengali
|
|
24
|
+
ur.set_multilingual_xlabel(ax, "أشهر السنة") # Arabic (RTL)
|
|
25
|
+
ur.set_multilingual_ylabel(ax, "மாத விற்பனை") # Tamil
|
|
26
|
+
|
|
27
|
+
ur.set_multilingual_xticks(ax, [1, 2, 3], ["एक", "दो", "तीन"]) # Hindi
|
|
28
|
+
ur.text(ax, 0.5, 0.5, "ข้อความ Mixed 2026", coord="axes") # Thai + Latin
|
|
29
|
+
|
|
30
|
+
ur.apply_multilingual_layout(fig, auto=True)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------
|
|
36
|
+
# Backend / environment
|
|
37
|
+
# ---------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
from .backend import (
|
|
40
|
+
init_renderer,
|
|
41
|
+
ensure_qt_application,
|
|
42
|
+
get_renderer_status,
|
|
43
|
+
check_environment,
|
|
44
|
+
is_headless_environment,
|
|
45
|
+
is_notebook_environment,
|
|
46
|
+
is_colab_environment,
|
|
47
|
+
is_kaggle_environment,
|
|
48
|
+
reset_renderer_state,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------
|
|
52
|
+
# Script detection
|
|
53
|
+
# ---------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
from .scripts import (
|
|
56
|
+
KNOWN_SCRIPTS,
|
|
57
|
+
RTL_SCRIPTS,
|
|
58
|
+
TextRun,
|
|
59
|
+
detect_char_script,
|
|
60
|
+
detect_script,
|
|
61
|
+
detect_scripts,
|
|
62
|
+
describe_text,
|
|
63
|
+
is_mixed_script,
|
|
64
|
+
is_rtl_text,
|
|
65
|
+
script_direction,
|
|
66
|
+
scripts_with_native_digits,
|
|
67
|
+
segment_runs,
|
|
68
|
+
to_native_numerals,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Roadmap-compatible alias
|
|
72
|
+
script_detection_auto = detect_script
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------
|
|
75
|
+
# Font management
|
|
76
|
+
# ---------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
from .fonts import (
|
|
79
|
+
SCRIPT_FONT_CANDIDATES,
|
|
80
|
+
auto_font_fallback,
|
|
81
|
+
coverage_report,
|
|
82
|
+
family_exists,
|
|
83
|
+
find_font_for_script,
|
|
84
|
+
font_covers_text,
|
|
85
|
+
font_info,
|
|
86
|
+
get_default_font,
|
|
87
|
+
get_script_font,
|
|
88
|
+
list_available_fonts,
|
|
89
|
+
list_registered_fonts,
|
|
90
|
+
register_font,
|
|
91
|
+
register_fonts,
|
|
92
|
+
resolve_font,
|
|
93
|
+
resolve_fonts_for_runs,
|
|
94
|
+
set_default_font,
|
|
95
|
+
set_script_font,
|
|
96
|
+
validate_font,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------
|
|
100
|
+
# Rendering
|
|
101
|
+
# ---------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
from .renderer import (
|
|
104
|
+
render_text,
|
|
105
|
+
render_text_array,
|
|
106
|
+
render_mixed_text,
|
|
107
|
+
render_paragraph,
|
|
108
|
+
render_text_qimage,
|
|
109
|
+
render_paragraph_qimage,
|
|
110
|
+
measure_text,
|
|
111
|
+
clear_render_cache,
|
|
112
|
+
get_render_cache_info,
|
|
113
|
+
set_render_cache_maxsize,
|
|
114
|
+
set_render_defaults,
|
|
115
|
+
get_render_defaults,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------
|
|
119
|
+
# Layout
|
|
120
|
+
# ---------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
from .layout import (
|
|
123
|
+
get_layout_manager,
|
|
124
|
+
clear_layout_manager,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------
|
|
128
|
+
# Matplotlib-facing APIs
|
|
129
|
+
# ---------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
from .mpl_support import (
|
|
132
|
+
set_multilingual_legend,
|
|
133
|
+
set_multilingual_numeric_ticks,
|
|
134
|
+
set_multilingual_title,
|
|
135
|
+
set_multilingual_xlabel,
|
|
136
|
+
set_multilingual_ylabel,
|
|
137
|
+
set_multilingual_suptitle,
|
|
138
|
+
set_multilingual_xticks,
|
|
139
|
+
set_multilingual_yticks,
|
|
140
|
+
add_multilingual_cell_text,
|
|
141
|
+
multilingual_text,
|
|
142
|
+
annotate_multilingual,
|
|
143
|
+
multilingual_paragraph,
|
|
144
|
+
apply_multilingual_layout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------
|
|
148
|
+
# High-level API
|
|
149
|
+
# ---------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
from .highlevel import (
|
|
152
|
+
LANGUAGE_TO_SCRIPT,
|
|
153
|
+
font_for_language,
|
|
154
|
+
language_to_script,
|
|
155
|
+
localize_axes,
|
|
156
|
+
localized_numerals,
|
|
157
|
+
multilingual_bar_labels,
|
|
158
|
+
multilingual_heatmap,
|
|
159
|
+
set_language_font,
|
|
160
|
+
supported_languages,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------
|
|
164
|
+
# Diagnostics / evaluation
|
|
165
|
+
# ---------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
from .diagnostics import (
|
|
168
|
+
SCRIPT_TEST_SAMPLES,
|
|
169
|
+
benchmark_render,
|
|
170
|
+
save_comparison_figure,
|
|
171
|
+
self_test,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def text(ax, *args, **kwargs):
|
|
176
|
+
"""
|
|
177
|
+
Short alias for multilingual_text(), so you can write:
|
|
178
|
+
|
|
179
|
+
import universal_render as ur
|
|
180
|
+
ur.text(ax, 0.5, 0.5, "বাংলা / العربية / ไทย", coord="axes")
|
|
181
|
+
"""
|
|
182
|
+
return multilingual_text(ax, *args, **kwargs)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__all__ = [
|
|
186
|
+
"__version__",
|
|
187
|
+
# backend
|
|
188
|
+
"init_renderer",
|
|
189
|
+
"ensure_qt_application",
|
|
190
|
+
"get_renderer_status",
|
|
191
|
+
"check_environment",
|
|
192
|
+
"is_headless_environment",
|
|
193
|
+
"is_notebook_environment",
|
|
194
|
+
"is_colab_environment",
|
|
195
|
+
"is_kaggle_environment",
|
|
196
|
+
"reset_renderer_state",
|
|
197
|
+
# scripts
|
|
198
|
+
"KNOWN_SCRIPTS",
|
|
199
|
+
"RTL_SCRIPTS",
|
|
200
|
+
"TextRun",
|
|
201
|
+
"detect_char_script",
|
|
202
|
+
"detect_script",
|
|
203
|
+
"detect_scripts",
|
|
204
|
+
"describe_text",
|
|
205
|
+
"is_mixed_script",
|
|
206
|
+
"is_rtl_text",
|
|
207
|
+
"script_direction",
|
|
208
|
+
"script_detection_auto",
|
|
209
|
+
"scripts_with_native_digits",
|
|
210
|
+
"segment_runs",
|
|
211
|
+
"to_native_numerals",
|
|
212
|
+
# fonts
|
|
213
|
+
"SCRIPT_FONT_CANDIDATES",
|
|
214
|
+
"auto_font_fallback",
|
|
215
|
+
"coverage_report",
|
|
216
|
+
"family_exists",
|
|
217
|
+
"find_font_for_script",
|
|
218
|
+
"font_covers_text",
|
|
219
|
+
"font_info",
|
|
220
|
+
"get_default_font",
|
|
221
|
+
"get_script_font",
|
|
222
|
+
"list_available_fonts",
|
|
223
|
+
"list_registered_fonts",
|
|
224
|
+
"register_font",
|
|
225
|
+
"register_fonts",
|
|
226
|
+
"resolve_font",
|
|
227
|
+
"resolve_fonts_for_runs",
|
|
228
|
+
"set_default_font",
|
|
229
|
+
"set_script_font",
|
|
230
|
+
"validate_font",
|
|
231
|
+
# renderer
|
|
232
|
+
"render_text",
|
|
233
|
+
"render_text_array",
|
|
234
|
+
"render_mixed_text",
|
|
235
|
+
"render_paragraph",
|
|
236
|
+
"render_text_qimage",
|
|
237
|
+
"render_paragraph_qimage",
|
|
238
|
+
"measure_text",
|
|
239
|
+
"clear_render_cache",
|
|
240
|
+
"get_render_cache_info",
|
|
241
|
+
"set_render_cache_maxsize",
|
|
242
|
+
"set_render_defaults",
|
|
243
|
+
"get_render_defaults",
|
|
244
|
+
# layout
|
|
245
|
+
"get_layout_manager",
|
|
246
|
+
"clear_layout_manager",
|
|
247
|
+
# mpl support
|
|
248
|
+
"set_multilingual_legend",
|
|
249
|
+
"set_multilingual_numeric_ticks",
|
|
250
|
+
"set_multilingual_title",
|
|
251
|
+
"set_multilingual_xlabel",
|
|
252
|
+
"set_multilingual_ylabel",
|
|
253
|
+
"set_multilingual_suptitle",
|
|
254
|
+
"set_multilingual_xticks",
|
|
255
|
+
"set_multilingual_yticks",
|
|
256
|
+
"add_multilingual_cell_text",
|
|
257
|
+
"multilingual_text",
|
|
258
|
+
"annotate_multilingual",
|
|
259
|
+
"multilingual_paragraph",
|
|
260
|
+
"apply_multilingual_layout",
|
|
261
|
+
"text",
|
|
262
|
+
# high-level
|
|
263
|
+
"LANGUAGE_TO_SCRIPT",
|
|
264
|
+
"font_for_language",
|
|
265
|
+
"language_to_script",
|
|
266
|
+
"localize_axes",
|
|
267
|
+
"localized_numerals",
|
|
268
|
+
"multilingual_bar_labels",
|
|
269
|
+
"multilingual_heatmap",
|
|
270
|
+
"set_language_font",
|
|
271
|
+
"supported_languages",
|
|
272
|
+
# diagnostics
|
|
273
|
+
"SCRIPT_TEST_SAMPLES",
|
|
274
|
+
"benchmark_render",
|
|
275
|
+
"save_comparison_figure",
|
|
276
|
+
"self_test",
|
|
277
|
+
]
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# universal_render/backend.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import platform
|
|
7
|
+
from dataclasses import dataclass, asdict, field
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from PySide6.QtGui import QGuiApplication
|
|
12
|
+
from PySide6.QtWidgets import QApplication
|
|
13
|
+
QT_AVAILABLE = True
|
|
14
|
+
QT_IMPORT_ERROR = None
|
|
15
|
+
except Exception as e: # pragma: no cover
|
|
16
|
+
QGuiApplication = None
|
|
17
|
+
QApplication = None
|
|
18
|
+
QT_AVAILABLE = False
|
|
19
|
+
QT_IMPORT_ERROR = e
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------
|
|
23
|
+
# Internal state
|
|
24
|
+
# ---------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_APP = None
|
|
27
|
+
_INITIALIZED = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RendererStatus:
|
|
32
|
+
qt_available: bool = QT_AVAILABLE
|
|
33
|
+
qt_import_error: Optional[str] = None
|
|
34
|
+
initialized: bool = False
|
|
35
|
+
app_created: bool = False
|
|
36
|
+
app_class: Optional[str] = None
|
|
37
|
+
platform_name: str = field(default_factory=lambda: platform.system())
|
|
38
|
+
python_version: str = field(default_factory=lambda: platform.python_version())
|
|
39
|
+
headless: bool = False
|
|
40
|
+
offscreen_requested: bool = False
|
|
41
|
+
offscreen_enabled: bool = False
|
|
42
|
+
qt_qpa_platform: Optional[str] = None
|
|
43
|
+
warnings: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
return asdict(self)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_STATUS = RendererStatus(
|
|
50
|
+
qt_import_error=str(QT_IMPORT_ERROR) if QT_IMPORT_ERROR else None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------
|
|
55
|
+
# Environment helpers
|
|
56
|
+
# ---------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def is_notebook_environment() -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Best-effort detection for Jupyter/Colab/Kaggle-like notebook environments.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
from IPython import get_ipython # type: ignore
|
|
64
|
+
shell = get_ipython()
|
|
65
|
+
if shell is None:
|
|
66
|
+
return False
|
|
67
|
+
return True
|
|
68
|
+
except Exception:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_colab_environment() -> bool:
|
|
73
|
+
return "google.colab" in sys.modules
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_kaggle_environment() -> bool:
|
|
77
|
+
return (
|
|
78
|
+
"KAGGLE_KERNEL_RUN_TYPE" in os.environ
|
|
79
|
+
or "KAGGLE_URL_BASE" in os.environ
|
|
80
|
+
or "/kaggle/" in os.getcwd().replace("\\", "/")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_headless_environment() -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Heuristic headless detection.
|
|
87
|
+
|
|
88
|
+
On Linux, the absence of DISPLAY/WAYLAND_DISPLAY usually means headless.
|
|
89
|
+
On Colab/Kaggle, assume headless.
|
|
90
|
+
On Windows/macOS, default to False unless explicitly offscreen.
|
|
91
|
+
"""
|
|
92
|
+
if is_colab_environment() or is_kaggle_environment():
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
system_name = platform.system().lower()
|
|
96
|
+
|
|
97
|
+
if system_name == "linux":
|
|
98
|
+
display = os.environ.get("DISPLAY")
|
|
99
|
+
wayland = os.environ.get("WAYLAND_DISPLAY")
|
|
100
|
+
return not (display or wayland)
|
|
101
|
+
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize_bool(value: Optional[bool], default: bool) -> bool:
|
|
106
|
+
if value is None:
|
|
107
|
+
return default
|
|
108
|
+
return bool(value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ensure_offscreen_env(force: bool = False) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Enable Qt offscreen mode by setting QT_QPA_PLATFORM=offscreen.
|
|
114
|
+
|
|
115
|
+
Returns True if the environment variable is set to 'offscreen' after this call.
|
|
116
|
+
"""
|
|
117
|
+
current = os.environ.get("QT_QPA_PLATFORM")
|
|
118
|
+
|
|
119
|
+
if current:
|
|
120
|
+
# Respect explicit user setting unless force=True
|
|
121
|
+
if force:
|
|
122
|
+
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
|
123
|
+
return True
|
|
124
|
+
return current.lower() == "offscreen"
|
|
125
|
+
|
|
126
|
+
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _existing_app():
|
|
131
|
+
"""
|
|
132
|
+
Return an existing Qt application instance if one exists.
|
|
133
|
+
"""
|
|
134
|
+
if not QT_AVAILABLE:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
app = QApplication.instance()
|
|
139
|
+
if app is not None:
|
|
140
|
+
return app
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
app = QGuiApplication.instance()
|
|
146
|
+
if app is not None:
|
|
147
|
+
return app
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------
|
|
155
|
+
# Public API
|
|
156
|
+
# ---------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def init_renderer(
|
|
159
|
+
headless: Optional[bool] = None,
|
|
160
|
+
offscreen: bool = True,
|
|
161
|
+
force: bool = False,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Initialize a Qt application safely for multilingual text rendering.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
headless:
|
|
169
|
+
Whether to treat the current environment as headless.
|
|
170
|
+
If None, a best-effort detection is used.
|
|
171
|
+
offscreen:
|
|
172
|
+
Whether to request Qt offscreen mode in headless environments.
|
|
173
|
+
force:
|
|
174
|
+
If True, re-run initialization logic even if already initialized.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
app:
|
|
179
|
+
The active QApplication/QGuiApplication instance.
|
|
180
|
+
|
|
181
|
+
Notes
|
|
182
|
+
-----
|
|
183
|
+
- In Colab/Kaggle/headless Linux, offscreen mode is usually required.
|
|
184
|
+
- This function tries to reuse an existing Qt app if one already exists.
|
|
185
|
+
"""
|
|
186
|
+
global _APP, _INITIALIZED, _STATUS
|
|
187
|
+
|
|
188
|
+
if not QT_AVAILABLE:
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
"PySide6 is not available. Install PySide6 to use universal_render.\n"
|
|
191
|
+
f"Original import error: {QT_IMPORT_ERROR}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
inferred_headless = is_headless_environment()
|
|
195
|
+
use_headless = _normalize_bool(headless, inferred_headless)
|
|
196
|
+
|
|
197
|
+
warnings: List[str] = []
|
|
198
|
+
|
|
199
|
+
if _INITIALIZED and not force:
|
|
200
|
+
_STATUS.headless = use_headless
|
|
201
|
+
_STATUS.offscreen_requested = bool(offscreen)
|
|
202
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
203
|
+
_STATUS.offscreen_enabled = (
|
|
204
|
+
(_STATUS.qt_qpa_platform or "").lower() == "offscreen"
|
|
205
|
+
)
|
|
206
|
+
if warnings:
|
|
207
|
+
_STATUS.warnings.extend(warnings)
|
|
208
|
+
return _APP
|
|
209
|
+
|
|
210
|
+
if use_headless and offscreen:
|
|
211
|
+
enabled = _ensure_offscreen_env(force=force)
|
|
212
|
+
if enabled:
|
|
213
|
+
_STATUS.offscreen_enabled = True
|
|
214
|
+
else:
|
|
215
|
+
warnings.append(
|
|
216
|
+
"Headless mode detected, but QT_QPA_PLATFORM is not set to 'offscreen'."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
existing = _existing_app()
|
|
220
|
+
if existing is not None:
|
|
221
|
+
_APP = existing
|
|
222
|
+
_INITIALIZED = True
|
|
223
|
+
|
|
224
|
+
_STATUS.initialized = True
|
|
225
|
+
_STATUS.app_created = True
|
|
226
|
+
_STATUS.app_class = type(existing).__name__
|
|
227
|
+
_STATUS.headless = use_headless
|
|
228
|
+
_STATUS.offscreen_requested = bool(offscreen)
|
|
229
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
230
|
+
_STATUS.offscreen_enabled = (
|
|
231
|
+
(_STATUS.qt_qpa_platform or "").lower() == "offscreen"
|
|
232
|
+
)
|
|
233
|
+
_STATUS.warnings = warnings
|
|
234
|
+
return _APP
|
|
235
|
+
|
|
236
|
+
argv = [sys.argv[0] if sys.argv else "universal_render"]
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
_APP = QApplication(argv)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
_STATUS.initialized = False
|
|
242
|
+
_STATUS.app_created = False
|
|
243
|
+
_STATUS.app_class = None
|
|
244
|
+
_STATUS.headless = use_headless
|
|
245
|
+
_STATUS.offscreen_requested = bool(offscreen)
|
|
246
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
247
|
+
_STATUS.offscreen_enabled = (
|
|
248
|
+
(_STATUS.qt_qpa_platform or "").lower() == "offscreen"
|
|
249
|
+
)
|
|
250
|
+
_STATUS.warnings = warnings
|
|
251
|
+
|
|
252
|
+
extra = []
|
|
253
|
+
if use_headless and offscreen:
|
|
254
|
+
extra.append(
|
|
255
|
+
"This looks like a headless environment. Qt offscreen rendering was requested."
|
|
256
|
+
)
|
|
257
|
+
if is_colab_environment():
|
|
258
|
+
extra.append(
|
|
259
|
+
"Detected Google Colab. You may also need to install fonts and keep QT_QPA_PLATFORM=offscreen."
|
|
260
|
+
)
|
|
261
|
+
if is_kaggle_environment():
|
|
262
|
+
extra.append(
|
|
263
|
+
"Detected Kaggle. You may also need to install fonts and keep QT_QPA_PLATFORM=offscreen."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
extra_text = "\n".join(extra)
|
|
267
|
+
raise RuntimeError(
|
|
268
|
+
"Failed to initialize Qt for universal_render.\n"
|
|
269
|
+
f"Qt error: {e}\n"
|
|
270
|
+
f"{extra_text}".strip()
|
|
271
|
+
) from e
|
|
272
|
+
|
|
273
|
+
_INITIALIZED = True
|
|
274
|
+
|
|
275
|
+
_STATUS.initialized = True
|
|
276
|
+
_STATUS.app_created = True
|
|
277
|
+
_STATUS.app_class = type(_APP).__name__
|
|
278
|
+
_STATUS.headless = use_headless
|
|
279
|
+
_STATUS.offscreen_requested = bool(offscreen)
|
|
280
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
281
|
+
_STATUS.offscreen_enabled = (
|
|
282
|
+
(_STATUS.qt_qpa_platform or "").lower() == "offscreen"
|
|
283
|
+
)
|
|
284
|
+
_STATUS.warnings = warnings
|
|
285
|
+
|
|
286
|
+
if use_headless and not _STATUS.offscreen_enabled:
|
|
287
|
+
_STATUS.warnings.append(
|
|
288
|
+
"Headless environment detected, but offscreen Qt platform is not active."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return _APP
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def ensure_qt_application() -> Any:
|
|
295
|
+
"""
|
|
296
|
+
Convenience wrapper for lazy initialization with safe defaults.
|
|
297
|
+
"""
|
|
298
|
+
return init_renderer(headless=None, offscreen=True, force=False)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_renderer_status() -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
Return backend/Qt initialization status as a plain dictionary.
|
|
304
|
+
"""
|
|
305
|
+
_STATUS.qt_available = QT_AVAILABLE
|
|
306
|
+
_STATUS.qt_import_error = str(QT_IMPORT_ERROR) if QT_IMPORT_ERROR else None
|
|
307
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
308
|
+
_STATUS.offscreen_enabled = ((_STATUS.qt_qpa_platform or "").lower() == "offscreen")
|
|
309
|
+
_STATUS.headless = is_headless_environment()
|
|
310
|
+
return _STATUS.to_dict()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def check_environment() -> Dict[str, Any]:
|
|
314
|
+
"""
|
|
315
|
+
Lightweight diagnostic snapshot of the current runtime.
|
|
316
|
+
"""
|
|
317
|
+
warnings: List[str] = []
|
|
318
|
+
|
|
319
|
+
qt_qpa = os.environ.get("QT_QPA_PLATFORM")
|
|
320
|
+
headless = is_headless_environment()
|
|
321
|
+
|
|
322
|
+
if not QT_AVAILABLE:
|
|
323
|
+
warnings.append("PySide6 is not installed or failed to import.")
|
|
324
|
+
|
|
325
|
+
if headless and (qt_qpa or "").lower() != "offscreen":
|
|
326
|
+
warnings.append(
|
|
327
|
+
"Headless environment detected but QT_QPA_PLATFORM is not 'offscreen'."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if is_colab_environment():
|
|
331
|
+
warnings.append(
|
|
332
|
+
"Running in Google Colab. Script fonts may need manual installation or registration."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if is_kaggle_environment():
|
|
336
|
+
warnings.append(
|
|
337
|
+
"Running in Kaggle. Script fonts may need manual installation or registration."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"qt_available": QT_AVAILABLE,
|
|
342
|
+
"qt_import_error": str(QT_IMPORT_ERROR) if QT_IMPORT_ERROR else None,
|
|
343
|
+
"headless": headless,
|
|
344
|
+
"notebook": is_notebook_environment(),
|
|
345
|
+
"colab": is_colab_environment(),
|
|
346
|
+
"kaggle": is_kaggle_environment(),
|
|
347
|
+
"qt_qpa_platform": qt_qpa,
|
|
348
|
+
"warnings": warnings,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def reset_renderer_state() -> None:
|
|
353
|
+
"""
|
|
354
|
+
Reset only the Python-side tracking state.
|
|
355
|
+
|
|
356
|
+
This does not destroy an already-running Qt application, because Qt apps are
|
|
357
|
+
generally process-global and should not be forcefully torn down here.
|
|
358
|
+
"""
|
|
359
|
+
global _APP, _INITIALIZED, _STATUS
|
|
360
|
+
|
|
361
|
+
existing = _existing_app()
|
|
362
|
+
_APP = existing
|
|
363
|
+
_INITIALIZED = existing is not None
|
|
364
|
+
|
|
365
|
+
_STATUS = RendererStatus(
|
|
366
|
+
qt_import_error=str(QT_IMPORT_ERROR) if QT_IMPORT_ERROR else None
|
|
367
|
+
)
|
|
368
|
+
_STATUS.initialized = _INITIALIZED
|
|
369
|
+
_STATUS.app_created = existing is not None
|
|
370
|
+
_STATUS.app_class = type(existing).__name__ if existing is not None else None
|
|
371
|
+
_STATUS.qt_qpa_platform = os.environ.get("QT_QPA_PLATFORM")
|
|
372
|
+
_STATUS.offscreen_enabled = (
|
|
373
|
+
((_STATUS.qt_qpa_platform or "").lower() == "offscreen")
|
|
374
|
+
)
|
|
375
|
+
_STATUS.headless = is_headless_environment()
|