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,332 @@
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_export.py
4
+
5
+ """Export and download Flask routes for the editor."""
6
+
7
+ import io
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .._core import WebEditor
13
+
14
+ __all__ = [
15
+ "create_export_route",
16
+ "create_download_route",
17
+ "create_download_figz_route",
18
+ ]
19
+
20
+
21
+ def create_export_route(app, editor: "WebEditor"):
22
+ """Create the export route."""
23
+ from flask import jsonify, request
24
+
25
+ @app.route("/export", methods=["POST"])
26
+ def export_figure():
27
+ try:
28
+ data = request.get_json()
29
+ formats = data.get("formats", ["png", "svg"])
30
+
31
+ if not editor.panel_info:
32
+ return jsonify({"success": False, "error": "No panel info available"})
33
+
34
+ bundle_path = editor.panel_info.get("bundle_path")
35
+ if not bundle_path:
36
+ return jsonify({"success": False, "error": "Bundle path not available"})
37
+
38
+ import matplotlib
39
+
40
+ matplotlib.use("Agg")
41
+ import matplotlib.pyplot as plt
42
+
43
+ from scitex.io import ZipBundle
44
+
45
+ figure_name = Path(bundle_path).stem
46
+ dpi = data.get("dpi", 150)
47
+
48
+ with ZipBundle(bundle_path, mode="a") as bundle:
49
+ try:
50
+ spec = bundle.read_json("spec.json")
51
+ except:
52
+ spec = {}
53
+
54
+ fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
55
+ fig_width_in = fig_width_mm / 25.4
56
+ fig_height_in = fig_height_mm / 25.4
57
+
58
+ fig = plt.figure(
59
+ figsize=(fig_width_in, fig_height_in),
60
+ dpi=dpi,
61
+ facecolor="white",
62
+ )
63
+
64
+ _compose_panels_from_spec(
65
+ fig, spec, bundle, fig_width_mm, fig_height_mm
66
+ )
67
+
68
+ exported = {}
69
+ for fmt in formats:
70
+ buf = io.BytesIO()
71
+ if fmt in ["png", "jpeg", "jpg"]:
72
+ fig.savefig(
73
+ buf,
74
+ format="png" if fmt == "png" else "jpeg",
75
+ dpi=dpi,
76
+ bbox_inches="tight",
77
+ facecolor="white",
78
+ pad_inches=0.02,
79
+ )
80
+ elif fmt == "svg":
81
+ fig.savefig(
82
+ buf, format="svg", bbox_inches="tight", pad_inches=0.02
83
+ )
84
+ elif fmt == "pdf":
85
+ fig.savefig(
86
+ buf, format="pdf", bbox_inches="tight", pad_inches=0.02
87
+ )
88
+ else:
89
+ continue
90
+
91
+ buf.seek(0)
92
+ export_path = f"exports/{figure_name}.{fmt}"
93
+ bundle.write_bytes(export_path, buf.read())
94
+ exported[fmt] = export_path
95
+
96
+ plt.close(fig)
97
+
98
+ return jsonify(
99
+ {
100
+ "success": True,
101
+ "exported": exported,
102
+ "bundle_path": str(bundle_path),
103
+ }
104
+ )
105
+
106
+ except Exception as e:
107
+ import traceback
108
+
109
+ return jsonify(
110
+ {
111
+ "success": False,
112
+ "error": str(e),
113
+ "traceback": traceback.format_exc(),
114
+ }
115
+ )
116
+
117
+ return export_figure
118
+
119
+
120
+ def create_download_route(app, editor: "WebEditor"):
121
+ """Create the download route."""
122
+ from flask import send_file
123
+
124
+ @app.route("/download/<fmt>")
125
+ def download_figure(fmt):
126
+ try:
127
+ mime_types = {
128
+ "png": "image/png",
129
+ "jpeg": "image/jpeg",
130
+ "jpg": "image/jpeg",
131
+ "svg": "image/svg+xml",
132
+ "pdf": "application/pdf",
133
+ }
134
+
135
+ if fmt not in mime_types:
136
+ return f"Unsupported format: {fmt}", 400
137
+
138
+ # For figure bundles, download the composed figure
139
+ if editor.panel_info:
140
+ from ._export_helpers import compose_panels_to_figure
141
+
142
+ bundle_path = editor.panel_info.get("bundle_path")
143
+ figure_dir = editor.panel_info.get("figure_dir")
144
+ figure_name = (
145
+ Path(bundle_path).stem
146
+ if bundle_path
147
+ else (
148
+ Path(figure_dir).stem.replace(".figure", "")
149
+ if figure_dir
150
+ else "figure"
151
+ )
152
+ )
153
+
154
+ if bundle_path or figure_dir:
155
+ dpi = 150 if fmt in ["jpeg", "jpg"] else 300
156
+ buf = compose_panels_to_figure(editor, fmt=fmt, dpi=dpi)
157
+ return send_file(
158
+ buf,
159
+ mimetype=mime_types[fmt],
160
+ as_attachment=True,
161
+ download_name=f"{figure_name}.{fmt}",
162
+ )
163
+
164
+ # For single pltz files, render from csv_data
165
+ import matplotlib
166
+
167
+ matplotlib.use("Agg")
168
+ import matplotlib.pyplot as plt
169
+
170
+ from .._renderer import render_preview_with_bboxes
171
+
172
+ figure_name = "figure"
173
+ if editor.json_path:
174
+ figure_name = Path(editor.json_path).stem
175
+
176
+ img_data, _, _ = render_preview_with_bboxes(
177
+ editor.csv_data,
178
+ editor.current_overrides,
179
+ metadata=editor.metadata,
180
+ dark_mode=False,
181
+ )
182
+
183
+ if fmt == "png":
184
+ import base64
185
+
186
+ content = base64.b64decode(img_data)
187
+ buf = io.BytesIO(content)
188
+ return send_file(
189
+ buf,
190
+ mimetype=mime_types[fmt],
191
+ as_attachment=True,
192
+ download_name=f"{figure_name}.{fmt}",
193
+ )
194
+
195
+ # For other formats, re-render
196
+ from .._plotter import plot_from_csv
197
+
198
+ fig, ax = plt.subplots(figsize=(8, 6))
199
+ plot_from_csv(ax, editor.csv_data, editor.current_overrides)
200
+
201
+ buf = io.BytesIO()
202
+ dpi = 150 if fmt in ["jpeg", "jpg"] else 300
203
+ fig.savefig(
204
+ buf,
205
+ format=fmt if fmt != "jpg" else "jpeg",
206
+ dpi=dpi,
207
+ bbox_inches="tight",
208
+ facecolor="white" if fmt in ["jpeg", "jpg"] else None,
209
+ )
210
+ plt.close(fig)
211
+ buf.seek(0)
212
+
213
+ return send_file(
214
+ buf,
215
+ mimetype=mime_types[fmt],
216
+ as_attachment=True,
217
+ download_name=f"{figure_name}.{fmt}",
218
+ )
219
+
220
+ except Exception as e:
221
+ import traceback
222
+
223
+ return f"Error: {str(e)}\n{traceback.format_exc()}", 500
224
+
225
+ return download_figure
226
+
227
+
228
+ def create_download_figz_route(app, editor: "WebEditor"):
229
+ """Create the download_figz route."""
230
+ from flask import send_file
231
+
232
+ @app.route("/download_figz")
233
+ def download_figz():
234
+ try:
235
+ if not editor.panel_info:
236
+ return "No panel info available", 404
237
+
238
+ bundle_path = editor.panel_info.get("bundle_path")
239
+ if not bundle_path:
240
+ return "Bundle path not available", 404
241
+
242
+ return send_file(
243
+ bundle_path,
244
+ mimetype="application/zip",
245
+ as_attachment=True,
246
+ download_name=Path(bundle_path).name,
247
+ )
248
+
249
+ except Exception as e:
250
+ return str(e), 500
251
+
252
+ return download_figz
253
+
254
+
255
+ def _get_figure_dimensions(spec):
256
+ """Extract figure dimensions from spec."""
257
+ fig_width_mm = 180
258
+ fig_height_mm = 120
259
+
260
+ if "figure" in spec:
261
+ fig_info = spec.get("figure", {})
262
+ styles = fig_info.get("styles", {})
263
+ size = styles.get("size", {})
264
+ fig_width_mm = size.get("width_mm", 180)
265
+ fig_height_mm = size.get("height_mm", 120)
266
+
267
+ return fig_width_mm, fig_height_mm
268
+
269
+
270
+ def _compose_panels_from_spec(fig, spec, bundle, fig_width_mm, fig_height_mm):
271
+ """Compose panels onto figure from spec (used in export route)."""
272
+ import tempfile
273
+
274
+ import numpy as np
275
+ from PIL import Image
276
+
277
+ from scitex.io import ZipBundle
278
+
279
+ panels_spec = spec.get("panels", [])
280
+
281
+ for panel_spec in panels_spec:
282
+ panel_id = panel_spec.get("id", "")
283
+ plot_name = panel_spec.get("plot", "")
284
+ pos = panel_spec.get("position", {})
285
+ size = panel_spec.get("size", {})
286
+
287
+ x_mm = pos.get("x_mm", 0)
288
+ y_mm = pos.get("y_mm", 0)
289
+ w_mm = size.get("width_mm", 60)
290
+ h_mm = size.get("height_mm", 40)
291
+
292
+ x_frac = x_mm / fig_width_mm
293
+ y_frac = 1 - (y_mm + h_mm) / fig_height_mm
294
+ w_frac = w_mm / fig_width_mm
295
+ h_frac = h_mm / fig_height_mm
296
+
297
+ img_loaded = False
298
+ for plot_path in [f"{panel_id}.plot", plot_name.replace(".d", "")]:
299
+ if img_loaded:
300
+ break
301
+ try:
302
+ plot_bytes = bundle.read_bytes(plot_path)
303
+ with tempfile.NamedTemporaryFile(suffix=".plot", delete=False) as tmp:
304
+ tmp.write(plot_bytes)
305
+ tmp_path = tmp.name
306
+ try:
307
+ with ZipBundle(tmp_path, mode="r") as plot_bundle:
308
+ for preview_path in [
309
+ "exports/preview.png",
310
+ "preview.png",
311
+ f"exports/{panel_id}.png",
312
+ ]:
313
+ try:
314
+ img_data = plot_bundle.read_bytes(preview_path)
315
+ img = Image.open(io.BytesIO(img_data))
316
+ ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
317
+ ax.imshow(np.array(img))
318
+ ax.axis("off")
319
+ img_loaded = True
320
+ break
321
+ except:
322
+ continue
323
+ finally:
324
+ import os
325
+
326
+ os.unlink(tmp_path)
327
+ except Exception as e:
328
+ print(f"Could not load plot {plot_path}: {e}")
329
+ continue
330
+
331
+
332
+ # EOF
@@ -0,0 +1,252 @@
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_panels.py
4
+
5
+ """Panel-related Flask routes for the editor."""
6
+
7
+ import base64
8
+ import copy
9
+ import json as json_module
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from .._core import WebEditor
15
+
16
+ __all__ = [
17
+ "create_panels_route",
18
+ "create_switch_panel_route",
19
+ ]
20
+
21
+
22
+ def create_panels_route(app, editor: "WebEditor"):
23
+ """Create the panels route for multi-panel figure bundles."""
24
+ from flask import jsonify
25
+
26
+ from .._bbox import (
27
+ extract_bboxes_from_geometry_px,
28
+ extract_bboxes_from_metadata,
29
+ )
30
+ from ..edit import load_panel_data
31
+
32
+ @app.route("/panels")
33
+ def panels():
34
+ if not editor.panel_info:
35
+ return jsonify({"error": "Not a multi-panel figure bundle"}), 400
36
+
37
+ panel_names = editor.panel_info["panels"]
38
+ panel_paths = editor.panel_info.get("panel_paths", [])
39
+ panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panel_names))
40
+ figure_dir = Path(editor.panel_info["figure_dir"])
41
+
42
+ if not panel_paths:
43
+ panel_paths = [str(figure_dir / name) for name in panel_names]
44
+
45
+ # Load figz spec.json for panel layout
46
+ figure_layout = {}
47
+ spec_path = figure_dir / "spec.json"
48
+ if spec_path.exists():
49
+ with open(spec_path) as f:
50
+ figure_spec = json_module.load(f)
51
+ for panel_spec in figure_spec.get("panels", []):
52
+ panel_id = panel_spec.get("id", "")
53
+ figure_layout[panel_id] = {
54
+ "position": panel_spec.get("position", {}),
55
+ "size": panel_spec.get("size", {}),
56
+ }
57
+
58
+ panel_images = []
59
+
60
+ for idx, panel_name in enumerate(panel_names):
61
+ panel_path = panel_paths[idx]
62
+ is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else None
63
+ display_name = panel_name.replace(".plot", "").replace(".plot", "")
64
+
65
+ loaded = load_panel_data(panel_path, is_zip=is_zip)
66
+
67
+ panel_data = {
68
+ "name": display_name,
69
+ "image": None,
70
+ "bboxes": None,
71
+ "img_size": None,
72
+ }
73
+
74
+ if display_name in figure_layout:
75
+ panel_data["layout"] = figure_layout[display_name]
76
+
77
+ if loaded:
78
+ # Get image data
79
+ if loaded.get("is_zip"):
80
+ png_bytes = loaded.get("png_bytes")
81
+ if png_bytes:
82
+ panel_data["image"] = base64.b64encode(png_bytes).decode(
83
+ "utf-8"
84
+ )
85
+ else:
86
+ png_path = loaded.get("png_path")
87
+ if png_path and png_path.exists():
88
+ with open(png_path, "rb") as f:
89
+ panel_data["image"] = base64.b64encode(f.read()).decode(
90
+ "utf-8"
91
+ )
92
+
93
+ # Get image size
94
+ img_size = loaded.get("img_size")
95
+ if img_size:
96
+ panel_data["img_size"] = img_size
97
+ panel_data["width"] = img_size["width"]
98
+ panel_data["height"] = img_size["height"]
99
+ elif loaded.get("png_path"):
100
+ from PIL import Image
101
+
102
+ img = Image.open(loaded["png_path"])
103
+ panel_data["img_size"] = {
104
+ "width": img.size[0],
105
+ "height": img.size[1],
106
+ }
107
+ panel_data["width"], panel_data["height"] = img.size
108
+ img.close()
109
+
110
+ # Extract bboxes
111
+ if panel_data.get("img_size"):
112
+ geometry_data = loaded.get("geometry_data")
113
+ metadata = loaded.get("metadata", {})
114
+
115
+ if geometry_data:
116
+ panel_data["bboxes"] = extract_bboxes_from_geometry_px(
117
+ geometry_data,
118
+ panel_data["img_size"]["width"],
119
+ panel_data["img_size"]["height"],
120
+ )
121
+ elif metadata:
122
+ panel_data["bboxes"] = extract_bboxes_from_metadata(
123
+ metadata,
124
+ panel_data["img_size"]["width"],
125
+ panel_data["img_size"]["height"],
126
+ )
127
+
128
+ panel_images.append(panel_data)
129
+
130
+ return jsonify(
131
+ {
132
+ "panels": panel_images,
133
+ "count": len(panel_images),
134
+ "layout": figure_layout,
135
+ }
136
+ )
137
+
138
+ return panels
139
+
140
+
141
+ def create_switch_panel_route(app, editor: "WebEditor"):
142
+ """Create the switch_panel route."""
143
+ from flask import jsonify
144
+
145
+ from .._bbox import (
146
+ extract_bboxes_from_geometry_px,
147
+ extract_bboxes_from_metadata,
148
+ )
149
+ from ..edit import load_panel_data
150
+
151
+ @app.route("/switch_panel/<int:panel_index>")
152
+ def switch_panel(panel_index):
153
+ if not editor.panel_info:
154
+ return jsonify({"error": "Not a multi-panel figure bundle"}), 400
155
+
156
+ panels = editor.panel_info["panels"]
157
+ panel_paths = editor.panel_info.get("panel_paths", [])
158
+ panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panels))
159
+
160
+ if panel_index < 0 or panel_index >= len(panels):
161
+ return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
162
+
163
+ panel_name = panels[panel_index]
164
+ panel_path = (
165
+ panel_paths[panel_index]
166
+ if panel_paths
167
+ else str(Path(editor.panel_info["figure_dir"]) / panel_name)
168
+ )
169
+ is_zip = panel_is_zip[panel_index] if panel_index < len(panel_is_zip) else None
170
+
171
+ try:
172
+ loaded = load_panel_data(panel_path, is_zip=is_zip)
173
+
174
+ if not loaded:
175
+ return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
176
+
177
+ # Get image data
178
+ img_data = None
179
+ if loaded.get("is_zip"):
180
+ png_bytes = loaded.get("png_bytes")
181
+ if png_bytes:
182
+ img_data = base64.b64encode(png_bytes).decode("utf-8")
183
+ else:
184
+ png_path = loaded.get("png_path")
185
+ if png_path and png_path.exists():
186
+ with open(png_path, "rb") as f:
187
+ img_data = base64.b64encode(f.read()).decode("utf-8")
188
+
189
+ if not img_data:
190
+ return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
191
+
192
+ # Get image size
193
+ img_size = loaded.get("img_size", {"width": 0, "height": 0})
194
+ if not img_size and loaded.get("png_path"):
195
+ from PIL import Image
196
+
197
+ img = Image.open(loaded["png_path"])
198
+ img_size = {"width": img.size[0], "height": img.size[1]}
199
+ img.close()
200
+
201
+ # Extract bboxes
202
+ bboxes = {}
203
+ geometry_data = loaded.get("geometry_data")
204
+ metadata = loaded.get("metadata", {})
205
+
206
+ if geometry_data and img_size:
207
+ bboxes = extract_bboxes_from_geometry_px(
208
+ geometry_data, img_size["width"], img_size["height"]
209
+ )
210
+ elif metadata and img_size:
211
+ bboxes = extract_bboxes_from_metadata(
212
+ metadata, img_size["width"], img_size["height"]
213
+ )
214
+
215
+ # Update editor state
216
+ editor.metadata = metadata
217
+ editor.panel_info["current_index"] = panel_index
218
+
219
+ # Re-extract defaults
220
+ from ..._defaults import extract_defaults_from_metadata, get_scitex_defaults
221
+
222
+ editor.scitex_defaults = get_scitex_defaults()
223
+ editor.metadata_defaults = extract_defaults_from_metadata(metadata)
224
+ editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
225
+ editor.current_overrides.update(editor.metadata_defaults)
226
+ editor.current_overrides.update(editor.manual_overrides)
227
+
228
+ return jsonify(
229
+ {
230
+ "success": True,
231
+ "panel_name": panel_name,
232
+ "panel_index": panel_index,
233
+ "image": img_data,
234
+ "bboxes": bboxes,
235
+ "img_size": img_size,
236
+ "overrides": editor.current_overrides,
237
+ }
238
+ )
239
+ except Exception as e:
240
+ import traceback
241
+
242
+ return jsonify(
243
+ {
244
+ "error": f"Failed to switch panel: {str(e)}",
245
+ "traceback": traceback.format_exc(),
246
+ }
247
+ ), 500
248
+
249
+ return switch_panel
250
+
251
+
252
+ # EOF