scitex 2.14.0__py3-none-any.whl → 2.15.3__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.
Files changed (264) hide show
  1. scitex/__init__.py +71 -17
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_editor.py
4
+
5
+ """Core WebEditor class for Flask-based figure editing."""
6
+
7
+ import copy
8
+ import threading
9
+ import time
10
+ import webbrowser
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+ from .._utils import check_port_available, kill_process_on_port
15
+
16
+ __all__ = ["WebEditor"]
17
+
18
+
19
+ class WebEditor:
20
+ """Browser-based figure editor using Flask.
21
+
22
+ Features:
23
+ - Displays existing PNG from plot bundle (no re-rendering)
24
+ - Hitmap-based element selection for precise clicking
25
+ - Property editors with sliders and color pickers
26
+ - Save to .manual.json
27
+ - SciTeX style defaults pre-filled
28
+ - Auto-finds available port if default is in use
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ json_path: Path,
34
+ metadata: Dict[str, Any],
35
+ csv_data: Optional[Any] = None,
36
+ png_path: Optional[Path] = None,
37
+ hitmap_path: Optional[Path] = None,
38
+ manual_overrides: Optional[Dict[str, Any]] = None,
39
+ port: int = 5050,
40
+ panel_info: Optional[Dict[str, Any]] = None,
41
+ ):
42
+ self.json_path = Path(json_path)
43
+ self.metadata = metadata
44
+ self.csv_data = csv_data
45
+ self.png_path = Path(png_path) if png_path else None
46
+ self.hitmap_path = Path(hitmap_path) if hitmap_path else None
47
+ self.manual_overrides = manual_overrides or {}
48
+ self._requested_port = port
49
+ self.port = port
50
+ self.panel_info = panel_info
51
+
52
+ # Extract hit_regions from metadata
53
+ self.hit_regions = metadata.get("hit_regions", {})
54
+ self.color_map = self.hit_regions.get("color_map", {})
55
+
56
+ # Get SciTeX defaults and merge with metadata
57
+ from ..._defaults import extract_defaults_from_metadata, get_scitex_defaults
58
+
59
+ self.scitex_defaults = get_scitex_defaults()
60
+ self.metadata_defaults = extract_defaults_from_metadata(metadata)
61
+
62
+ # Start with defaults, then overlay manual overrides
63
+ self.current_overrides = copy.deepcopy(self.scitex_defaults)
64
+ self.current_overrides.update(self.metadata_defaults)
65
+ self.current_overrides.update(self.manual_overrides)
66
+
67
+ # Track initial state to detect modifications
68
+ self._initial_overrides = copy.deepcopy(self.current_overrides)
69
+ self._user_modified = False
70
+
71
+ def run(self):
72
+ """Launch the web editor."""
73
+ try:
74
+ from flask import Flask
75
+ except ImportError:
76
+ raise ImportError(
77
+ "Flask is required for web editor. Install: pip install flask"
78
+ )
79
+
80
+ # Handle port conflicts
81
+ self._setup_port()
82
+
83
+ # Configure Flask
84
+ import os
85
+
86
+ static_folder = os.path.join(
87
+ os.path.dirname(os.path.dirname(__file__)), "static"
88
+ )
89
+ app = Flask(__name__, static_folder=static_folder, static_url_path="/static")
90
+
91
+ # Register all routes
92
+ self._register_routes(app)
93
+
94
+ # Open browser after short delay
95
+ def open_browser():
96
+ time.sleep(0.5)
97
+ webbrowser.open(f"http://127.0.0.1:{self.port}")
98
+
99
+ threading.Thread(target=open_browser, daemon=True).start()
100
+
101
+ print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
102
+ print("Press Ctrl+C to stop")
103
+
104
+ app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
105
+
106
+ def _setup_port(self):
107
+ """Handle port conflicts."""
108
+ max_retries = 3
109
+ for attempt in range(max_retries):
110
+ if check_port_available(self._requested_port):
111
+ self.port = self._requested_port
112
+ break
113
+ print(
114
+ f"Port {self._requested_port} in use. Freeing... "
115
+ f"(attempt {attempt + 1}/{max_retries})"
116
+ )
117
+ kill_process_on_port(self._requested_port)
118
+ time.sleep(1.0)
119
+ else:
120
+ print(f"Warning: Port {self._requested_port} may still be in use")
121
+ self.port = self._requested_port
122
+
123
+ def _register_routes(self, app):
124
+ """Register all Flask routes."""
125
+ from ._routes_basic import (
126
+ create_colormap_route,
127
+ create_hitmap_route,
128
+ create_index_route,
129
+ create_preview_route,
130
+ create_shutdown_route,
131
+ create_stats_route,
132
+ create_update_route,
133
+ )
134
+ from ._routes_export import (
135
+ create_download_figz_route,
136
+ create_download_route,
137
+ create_export_route,
138
+ )
139
+ from ._routes_panels import (
140
+ create_panels_route,
141
+ create_switch_panel_route,
142
+ )
143
+ from ._routes_save import (
144
+ create_save_element_position_route,
145
+ create_save_layout_route,
146
+ create_save_route,
147
+ )
148
+
149
+ # Basic routes
150
+ create_index_route(app, self)
151
+ create_preview_route(app, self)
152
+ create_hitmap_route(app, self)
153
+ create_colormap_route(app, self)
154
+ create_update_route(app, self)
155
+ create_stats_route(app, self)
156
+ create_shutdown_route(app, self)
157
+
158
+ # Panel routes
159
+ create_panels_route(app, self)
160
+ create_switch_panel_route(app, self)
161
+
162
+ # Save routes
163
+ create_save_route(app, self)
164
+ create_save_layout_route(app, self)
165
+ create_save_element_position_route(app, self)
166
+
167
+ # Export routes
168
+ create_export_route(app, self)
169
+ create_download_route(app, self)
170
+ create_download_figz_route(app, self)
171
+
172
+
173
+ # EOF
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_export_helpers.py
4
+
5
+ """Export and compose helpers for figure bundles."""
6
+
7
+ import io
8
+ import json as json_module
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, Dict, List
11
+
12
+ if TYPE_CHECKING:
13
+ from .._core import WebEditor
14
+
15
+ __all__ = ["export_composed_figure", "compose_panels_to_figure"]
16
+
17
+
18
+ def export_composed_figure(
19
+ editor: "WebEditor",
20
+ formats: List[str] = None,
21
+ dpi: int = 150,
22
+ ) -> Dict[str, Any]:
23
+ """Compose and export figure to bundle.
24
+
25
+ Parameters
26
+ ----------
27
+ editor : WebEditor
28
+ The editor instance with panel_info.
29
+ formats : list of str
30
+ Output formats (default: ["png", "svg"]).
31
+ dpi : int
32
+ Resolution for raster output.
33
+
34
+ Returns
35
+ -------
36
+ dict
37
+ Result with 'success' and 'exported' keys.
38
+ """
39
+ if formats is None:
40
+ formats = ["png", "svg"]
41
+
42
+ import matplotlib
43
+
44
+ matplotlib.use("Agg")
45
+
46
+ import matplotlib.pyplot as plt
47
+
48
+ from scitex.io import ZipBundle
49
+
50
+ if not editor.panel_info:
51
+ return {"success": False, "error": "No panel info"}
52
+
53
+ bundle_path = editor.panel_info.get("bundle_path")
54
+ figure_dir = editor.panel_info.get("figure_dir")
55
+
56
+ if not bundle_path and not figure_dir:
57
+ return {"success": False, "error": "No bundle path"}
58
+
59
+ figure_name = (
60
+ Path(bundle_path).stem
61
+ if bundle_path
62
+ else (Path(figure_dir).stem.replace(".figure", "") if figure_dir else "figure")
63
+ )
64
+
65
+ # Read spec.json and layout.json
66
+ spec, layout_overrides = _read_spec_and_layout(
67
+ bundle_path, figure_dir, editor.panel_info
68
+ )
69
+
70
+ # Get figure dimensions
71
+ fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
72
+ fig_width_in = fig_width_mm / 25.4
73
+ fig_height_in = fig_height_mm / 25.4
74
+
75
+ fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor="white")
76
+
77
+ # Compose panels
78
+ _compose_panels(
79
+ fig,
80
+ spec,
81
+ editor.panel_info,
82
+ layout_overrides,
83
+ fig_width_mm,
84
+ fig_height_mm,
85
+ )
86
+
87
+ exported = {}
88
+
89
+ # Save to bundle
90
+ if bundle_path:
91
+ with ZipBundle(bundle_path, mode="a") as bundle:
92
+ for fmt in formats:
93
+ buf = io.BytesIO()
94
+ fig.savefig(
95
+ buf,
96
+ format=fmt,
97
+ dpi=dpi,
98
+ bbox_inches="tight",
99
+ facecolor="white",
100
+ pad_inches=0.02,
101
+ )
102
+ buf.seek(0)
103
+ export_path = f"exports/{figure_name}.{fmt}"
104
+ bundle.write_bytes(export_path, buf.read())
105
+ exported[fmt] = export_path
106
+
107
+ plt.close(fig)
108
+ return {"success": True, "exported": exported}
109
+
110
+
111
+ def compose_panels_to_figure(
112
+ editor: "WebEditor",
113
+ fmt: str = "png",
114
+ dpi: int = 150,
115
+ ) -> io.BytesIO:
116
+ """Compose panels into a figure and return as BytesIO.
117
+
118
+ Parameters
119
+ ----------
120
+ editor : WebEditor
121
+ The editor instance.
122
+ fmt : str
123
+ Output format.
124
+ dpi : int
125
+ Resolution.
126
+
127
+ Returns
128
+ -------
129
+ io.BytesIO
130
+ The composed figure as bytes.
131
+ """
132
+ import matplotlib
133
+
134
+ matplotlib.use("Agg")
135
+ import matplotlib.pyplot as plt
136
+
137
+ bundle_path = editor.panel_info.get("bundle_path")
138
+ figure_dir = editor.panel_info.get("figure_dir")
139
+
140
+ spec, layout_overrides = _read_spec_and_layout(
141
+ bundle_path, figure_dir, editor.panel_info
142
+ )
143
+
144
+ fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
145
+ fig_width_in = fig_width_mm / 25.4
146
+ fig_height_in = fig_height_mm / 25.4
147
+
148
+ fig = plt.figure(
149
+ figsize=(fig_width_in, fig_height_in),
150
+ dpi=dpi,
151
+ facecolor="white",
152
+ )
153
+
154
+ _compose_panels(
155
+ fig,
156
+ spec,
157
+ editor.panel_info,
158
+ layout_overrides,
159
+ fig_width_mm,
160
+ fig_height_mm,
161
+ )
162
+
163
+ buf = io.BytesIO()
164
+ fig.savefig(
165
+ buf,
166
+ format=fmt if fmt != "jpg" else "jpeg",
167
+ dpi=dpi,
168
+ bbox_inches="tight",
169
+ facecolor="white",
170
+ pad_inches=0.02,
171
+ )
172
+ plt.close(fig)
173
+ buf.seek(0)
174
+ return buf
175
+
176
+
177
+ def _read_spec_and_layout(bundle_path, figure_dir, panel_info):
178
+ """Read spec.json and layout.json from bundle or directory."""
179
+ from scitex.io import ZipBundle
180
+
181
+ spec = {}
182
+ layout_overrides = {}
183
+
184
+ if bundle_path:
185
+ try:
186
+ with ZipBundle(bundle_path, mode="r") as bundle:
187
+ spec = bundle.read_json("spec.json")
188
+ try:
189
+ layout_overrides = bundle.read_json("layout.json")
190
+ except:
191
+ pass
192
+ except:
193
+ pass
194
+ elif figure_dir:
195
+ spec_path = Path(figure_dir) / "spec.json"
196
+ if spec_path.exists():
197
+ with open(spec_path) as f:
198
+ spec = json_module.load(f)
199
+ layout_path = Path(figure_dir) / "layout.json"
200
+ if layout_path.exists():
201
+ with open(layout_path) as f:
202
+ layout_overrides = json_module.load(f)
203
+
204
+ # In-memory layout overrides take precedence
205
+ if panel_info and panel_info.get("layout"):
206
+ layout_overrides = panel_info.get("layout", {})
207
+
208
+ return spec, layout_overrides
209
+
210
+
211
+ def _get_figure_dimensions(spec):
212
+ """Extract figure dimensions from spec."""
213
+ fig_width_mm = 180
214
+ fig_height_mm = 120
215
+
216
+ if "figure" in spec:
217
+ fig_info = spec.get("figure", {})
218
+ styles = fig_info.get("styles", {})
219
+ size = styles.get("size", {})
220
+ fig_width_mm = size.get("width_mm", 180)
221
+ fig_height_mm = size.get("height_mm", 120)
222
+
223
+ return fig_width_mm, fig_height_mm
224
+
225
+
226
+ def _compose_panels(
227
+ fig, spec, panel_info, layout_overrides, fig_width_mm, fig_height_mm
228
+ ):
229
+ """Compose panels onto the figure."""
230
+ import zipfile
231
+
232
+ import numpy as np
233
+ from PIL import Image
234
+
235
+ from scitex.io import ZipBundle
236
+
237
+ panels_spec = spec.get("panels", [])
238
+ panel_paths = panel_info.get("panel_paths", [])
239
+ panel_is_zip = panel_info.get("panel_is_zip", [])
240
+
241
+ exclude_patterns = ["hitmap", "overview", "thumb", "preview"]
242
+
243
+ for panel_spec in panels_spec:
244
+ panel_id = panel_spec.get("id", "")
245
+ pos = panel_spec.get("position", {})
246
+ size = panel_spec.get("size", {})
247
+
248
+ # Skip auxiliary panels
249
+ panel_id_lower = panel_id.lower()
250
+ if any(
251
+ skip in panel_id_lower for skip in ["overview", "thumb", "preview", "aux"]
252
+ ):
253
+ continue
254
+
255
+ # Find panel path
256
+ panel_path, panel_name, is_zip = _find_panel_path(
257
+ panel_id, panel_paths, panel_is_zip
258
+ )
259
+ if not panel_path:
260
+ continue
261
+
262
+ # Get layout override
263
+ override = layout_overrides.get(panel_name, {})
264
+ override_pos = override.get("position", {})
265
+ override_size = override.get("size", {})
266
+
267
+ x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
268
+ y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
269
+ w_mm = override_size.get("width_mm", size.get("width_mm", 60))
270
+ h_mm = override_size.get("height_mm", size.get("height_mm", 40))
271
+
272
+ x_frac = x_mm / fig_width_mm
273
+ y_frac = 1 - (y_mm + h_mm) / fig_height_mm
274
+ w_frac = w_mm / fig_width_mm
275
+ h_frac = h_mm / fig_height_mm
276
+
277
+ # Load and place panel image
278
+ try:
279
+ if is_zip:
280
+ with ZipBundle(panel_path, mode="r") as plot_bundle:
281
+ with zipfile.ZipFile(panel_path, "r") as zf:
282
+ png_files = [
283
+ n
284
+ for n in zf.namelist()
285
+ if n.endswith(".png")
286
+ and "exports/" in n
287
+ and not any(p in n.lower() for p in exclude_patterns)
288
+ ]
289
+ if png_files:
290
+ preview_path = png_files[0]
291
+ if ".plot/" in preview_path:
292
+ preview_path = preview_path.split(".plot/")[-1]
293
+ img_data = plot_bundle.read_bytes(preview_path)
294
+ img = Image.open(io.BytesIO(img_data))
295
+ ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
296
+ ax.imshow(np.array(img))
297
+ ax.axis("off")
298
+ else:
299
+ plot_dir = Path(panel_path)
300
+ exports_dir = plot_dir / "exports"
301
+ if exports_dir.exists():
302
+ for png_file in exports_dir.glob("*.png"):
303
+ if not any(
304
+ p in png_file.name.lower() for p in exclude_patterns
305
+ ):
306
+ img = Image.open(png_file)
307
+ ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
308
+ ax.imshow(np.array(img))
309
+ ax.axis("off")
310
+ break
311
+ except Exception as e:
312
+ print(f"Could not load panel {panel_id}: {e}")
313
+
314
+ # Draw panel letter
315
+ if panel_id and len(panel_id) <= 2:
316
+ letter_x = x_frac + 0.01
317
+ letter_y = y_frac + h_frac - 0.02
318
+ fig.text(
319
+ letter_x,
320
+ letter_y,
321
+ panel_id,
322
+ fontsize=14,
323
+ fontweight="bold",
324
+ color="black",
325
+ ha="left",
326
+ va="top",
327
+ transform=fig.transFigure,
328
+ bbox=dict(
329
+ boxstyle="square,pad=0.1",
330
+ facecolor="white",
331
+ edgecolor="none",
332
+ alpha=0.8,
333
+ ),
334
+ )
335
+
336
+
337
+ def _find_panel_path(panel_id, panel_paths, panel_is_zip):
338
+ """Find panel path matching the panel ID."""
339
+ for idx, pp in enumerate(panel_paths):
340
+ pp_name = Path(pp).stem.replace(".plot", "")
341
+ if (
342
+ pp_name == panel_id
343
+ or pp_name.startswith(f"panel_{panel_id}_")
344
+ or pp_name == f"panel_{panel_id}"
345
+ or f"_{panel_id}_" in pp_name
346
+ ):
347
+ panel_name = Path(pp).name
348
+ is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
349
+ return pp, panel_name, is_zip
350
+ return None, None, False
351
+
352
+
353
+ # EOF
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_routes_basic.py
4
+
5
+ """Basic Flask routes for the editor."""
6
+
7
+ import base64
8
+ import json
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .._core import WebEditor
13
+
14
+ __all__ = [
15
+ "create_index_route",
16
+ "create_preview_route",
17
+ "create_hitmap_route",
18
+ "create_colormap_route",
19
+ "create_update_route",
20
+ "create_stats_route",
21
+ "create_shutdown_route",
22
+ ]
23
+
24
+
25
+ def create_index_route(app, editor: "WebEditor"):
26
+ """Create the index route."""
27
+ from flask import render_template_string
28
+
29
+ from ..templates import build_html_template
30
+
31
+ @app.route("/")
32
+ def index():
33
+ html_template = build_html_template()
34
+ json_path_str = str(editor.json_path.resolve())
35
+ figure_path = ""
36
+ panel_path = ""
37
+
38
+ if ".figure/" in json_path_str:
39
+ parts = json_path_str.split(".figure/")
40
+ figure_path = parts[0] + ".figure"
41
+ panel_path = parts[1] if len(parts) > 1 else ""
42
+ elif ".plot/" in json_path_str:
43
+ parts = json_path_str.split(".plot/")
44
+ figure_path = parts[0] + ".plot"
45
+ panel_path = parts[1] if len(parts) > 1 else ""
46
+ else:
47
+ figure_path = json_path_str
48
+
49
+ return render_template_string(
50
+ html_template,
51
+ filename=figure_path,
52
+ panel_path=panel_path,
53
+ overrides=json.dumps(editor.current_overrides),
54
+ )
55
+
56
+ return index
57
+
58
+
59
+ def create_preview_route(app, editor: "WebEditor"):
60
+ """Create the preview route."""
61
+ from flask import jsonify, request
62
+
63
+ from .._renderer import render_preview_with_bboxes
64
+
65
+ @app.route("/preview")
66
+ def preview():
67
+ dark_mode = request.args.get("dark_mode", "false").lower() == "true"
68
+ img_data, bboxes, img_size = render_preview_with_bboxes(
69
+ editor.csv_data,
70
+ editor.current_overrides,
71
+ metadata=editor.metadata,
72
+ dark_mode=dark_mode,
73
+ )
74
+ return jsonify(
75
+ {
76
+ "image": img_data,
77
+ "bboxes": bboxes,
78
+ "img_size": img_size,
79
+ "has_hitmap": editor.hitmap_path is not None
80
+ and editor.hitmap_path.exists(),
81
+ "format": "png",
82
+ "panel_info": editor.panel_info,
83
+ }
84
+ )
85
+
86
+ return preview
87
+
88
+
89
+ def create_hitmap_route(app, editor: "WebEditor"):
90
+ """Create the hitmap route."""
91
+ from flask import jsonify
92
+
93
+ @app.route("/hitmap")
94
+ def hitmap():
95
+ if editor.hitmap_path and editor.hitmap_path.exists():
96
+ with open(editor.hitmap_path, "rb") as f:
97
+ img_data = base64.b64encode(f.read()).decode("utf-8")
98
+ return jsonify(
99
+ {
100
+ "image": img_data,
101
+ "color_map": editor.color_map,
102
+ }
103
+ )
104
+ return jsonify({"error": "No hitmap available"}), 404
105
+
106
+ return hitmap
107
+
108
+
109
+ def create_colormap_route(app, editor: "WebEditor"):
110
+ """Create the color_map route."""
111
+ from flask import jsonify
112
+
113
+ @app.route("/color_map")
114
+ def color_map():
115
+ return jsonify(
116
+ {
117
+ "color_map": editor.color_map,
118
+ "hit_regions": editor.hit_regions,
119
+ }
120
+ )
121
+
122
+ return color_map
123
+
124
+
125
+ def create_update_route(app, editor: "WebEditor"):
126
+ """Create the update route."""
127
+ from flask import jsonify, request
128
+
129
+ from .._renderer import render_preview_with_bboxes
130
+
131
+ @app.route("/update", methods=["POST"])
132
+ def update():
133
+ data = request.json
134
+ editor.current_overrides.update(data.get("overrides", {}))
135
+ editor._user_modified = True
136
+ dark_mode = data.get("dark_mode", False)
137
+
138
+ img_data, bboxes, img_size = render_preview_with_bboxes(
139
+ editor.csv_data,
140
+ editor.current_overrides,
141
+ metadata=editor.metadata,
142
+ dark_mode=dark_mode,
143
+ )
144
+ return jsonify(
145
+ {
146
+ "image": img_data,
147
+ "bboxes": bboxes,
148
+ "img_size": img_size,
149
+ "status": "updated",
150
+ }
151
+ )
152
+
153
+ return update
154
+
155
+
156
+ def create_stats_route(app, editor: "WebEditor"):
157
+ """Create the stats route."""
158
+ from flask import jsonify
159
+
160
+ @app.route("/stats")
161
+ def stats():
162
+ stats_data = editor.metadata.get("stats", [])
163
+ stats_summary = editor.metadata.get("stats_summary", None)
164
+ return jsonify(
165
+ {
166
+ "stats": stats_data,
167
+ "stats_summary": stats_summary,
168
+ "has_stats": len(stats_data) > 0,
169
+ }
170
+ )
171
+
172
+ return stats
173
+
174
+
175
+ def create_shutdown_route(app, editor: "WebEditor"):
176
+ """Create the shutdown route."""
177
+ from flask import jsonify, request
178
+
179
+ @app.route("/shutdown", methods=["POST"])
180
+ def shutdown():
181
+ func = request.environ.get("werkzeug.server.shutdown")
182
+ if func is None:
183
+ raise RuntimeError("Not running with Werkzeug Server")
184
+ func()
185
+ return jsonify({"status": "shutdown"})
186
+
187
+ return shutdown
188
+
189
+
190
+ # EOF