scitex 2.15.1__py3-none-any.whl → 2.15.2__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.
- scitex/__init__.py +68 -61
- scitex/_mcp_tools/introspect.py +42 -23
- scitex/_mcp_tools/template.py +24 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/__init__.py +2 -2
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +1 -1
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/cli/introspect.py +112 -74
- scitex/cli/main.py +2 -0
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +15 -8
- scitex/cli/social.py +6 -6
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +15 -8
- scitex/cloud/__init__.py +41 -2
- scitex/config/_env_registry.py +84 -19
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +16 -9
- scitex/introspect/_core.py +7 -8
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
- scitex/introspect/_mcp/__init__.py +10 -6
- scitex/introspect/_mcp/handlers.py +37 -12
- scitex/introspect/_members.py +7 -3
- scitex/introspect/_signature.py +3 -3
- scitex/introspect/_source.py +2 -2
- scitex/io/_save.py +1 -2
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +1 -1
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +11 -14
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +10 -8
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/METADATA +1 -1
- {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/RECORD +105 -64
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
- {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.15.1.dist-info → scitex-2.15.2.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
|