scitex 2.7.3__py3-none-any.whl → 2.8.1__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/__version__.py +1 -1
- scitex/dev/plt/__init__.py +0 -0
- scitex/dev/plt/plot_mpl_axhline.py +0 -0
- scitex/dev/plt/plot_mpl_axhspan.py +0 -0
- scitex/dev/plt/plot_mpl_axvline.py +0 -0
- scitex/dev/plt/plot_mpl_axvspan.py +0 -0
- scitex/dev/plt/plot_mpl_bar.py +0 -0
- scitex/dev/plt/plot_mpl_barh.py +0 -0
- scitex/dev/plt/plot_mpl_boxplot.py +0 -0
- scitex/dev/plt/plot_mpl_contour.py +0 -0
- scitex/dev/plt/plot_mpl_contourf.py +0 -0
- scitex/dev/plt/plot_mpl_errorbar.py +0 -0
- scitex/dev/plt/plot_mpl_eventplot.py +0 -0
- scitex/dev/plt/plot_mpl_fill.py +0 -0
- scitex/dev/plt/plot_mpl_fill_between.py +0 -0
- scitex/dev/plt/plot_mpl_hexbin.py +0 -0
- scitex/dev/plt/plot_mpl_hist.py +0 -0
- scitex/dev/plt/plot_mpl_hist2d.py +0 -0
- scitex/dev/plt/plot_mpl_imshow.py +0 -0
- scitex/dev/plt/plot_mpl_pcolormesh.py +0 -0
- scitex/dev/plt/plot_mpl_pie.py +0 -0
- scitex/dev/plt/plot_mpl_plot.py +0 -0
- scitex/dev/plt/plot_mpl_quiver.py +0 -0
- scitex/dev/plt/plot_mpl_scatter.py +0 -0
- scitex/dev/plt/plot_mpl_stackplot.py +0 -0
- scitex/dev/plt/plot_mpl_stem.py +0 -0
- scitex/dev/plt/plot_mpl_step.py +0 -0
- scitex/dev/plt/plot_mpl_violinplot.py +0 -0
- scitex/dev/plt/plot_sns_barplot.py +0 -0
- scitex/dev/plt/plot_sns_boxplot.py +0 -0
- scitex/dev/plt/plot_sns_heatmap.py +0 -0
- scitex/dev/plt/plot_sns_histplot.py +0 -0
- scitex/dev/plt/plot_sns_kdeplot.py +0 -0
- scitex/dev/plt/plot_sns_lineplot.py +0 -0
- scitex/dev/plt/plot_sns_scatterplot.py +0 -0
- scitex/dev/plt/plot_sns_stripplot.py +0 -0
- scitex/dev/plt/plot_sns_swarmplot.py +0 -0
- scitex/dev/plt/plot_sns_violinplot.py +0 -0
- scitex/dev/plt/plot_stx_bar.py +0 -0
- scitex/dev/plt/plot_stx_barh.py +0 -0
- scitex/dev/plt/plot_stx_box.py +0 -0
- scitex/dev/plt/plot_stx_boxplot.py +0 -0
- scitex/dev/plt/plot_stx_conf_mat.py +0 -0
- scitex/dev/plt/plot_stx_contour.py +0 -0
- scitex/dev/plt/plot_stx_ecdf.py +0 -0
- scitex/dev/plt/plot_stx_errorbar.py +0 -0
- scitex/dev/plt/plot_stx_fill_between.py +0 -0
- scitex/dev/plt/plot_stx_fillv.py +0 -0
- scitex/dev/plt/plot_stx_heatmap.py +0 -0
- scitex/dev/plt/plot_stx_image.py +0 -0
- scitex/dev/plt/plot_stx_imshow.py +0 -0
- scitex/dev/plt/plot_stx_joyplot.py +0 -0
- scitex/dev/plt/plot_stx_kde.py +0 -0
- scitex/dev/plt/plot_stx_line.py +0 -0
- scitex/dev/plt/plot_stx_mean_ci.py +0 -0
- scitex/dev/plt/plot_stx_mean_std.py +0 -0
- scitex/dev/plt/plot_stx_median_iqr.py +0 -0
- scitex/dev/plt/plot_stx_raster.py +0 -0
- scitex/dev/plt/plot_stx_rectangle.py +0 -0
- scitex/dev/plt/plot_stx_scatter.py +0 -0
- scitex/dev/plt/plot_stx_shaded_line.py +0 -0
- scitex/dev/plt/plot_stx_violin.py +0 -0
- scitex/dev/plt/plot_stx_violinplot.py +0 -0
- scitex/diagram/README.md +197 -0
- scitex/diagram/__init__.py +48 -0
- scitex/diagram/_compile.py +312 -0
- scitex/diagram/_diagram.py +355 -0
- scitex/diagram/_presets.py +173 -0
- scitex/diagram/_schema.py +182 -0
- scitex/diagram/_split.py +278 -0
- scitex/fig/editor/__init__.py +5 -2
- scitex/fig/editor/_dearpygui_editor.py +1 -1
- scitex/fig/editor/_mpl_editor.py +1 -1
- scitex/fig/editor/_qt_editor.py +1 -1
- scitex/fig/editor/_tkinter_editor.py +1 -1
- scitex/fig/editor/edit/__init__.py +50 -0
- scitex/fig/editor/edit/backend_detector.py +109 -0
- scitex/fig/editor/edit/bundle_resolver.py +240 -0
- scitex/fig/editor/edit/editor_launcher.py +239 -0
- scitex/fig/editor/edit/manual_handler.py +53 -0
- scitex/fig/editor/edit/panel_loader.py +232 -0
- scitex/fig/editor/edit/path_resolver.py +67 -0
- scitex/fig/editor/flask_editor/_bbox.py +23 -0
- scitex/fig/editor/flask_editor/_core.py +908 -103
- scitex/fig/editor/flask_editor/_renderer.py +74 -0
- scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
- scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
- scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
- scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
- scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
- scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
- scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
- scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
- scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
- scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
- scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
- scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
- scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
- scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
- scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
- scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
- scitex/fig/editor/flask_editor/static/css/index.css +31 -0
- scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
- scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
- scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
- scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
- scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
- scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
- scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
- scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
- scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
- scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
- scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
- scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
- scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
- scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
- scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
- scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
- scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
- scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
- scitex/fig/editor/flask_editor/static/js/main.js +426 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
- scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
- scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
- scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
- scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
- scitex/fig/editor/flask_editor/templates/__init__.py +95 -5
- scitex/fig/editor/flask_editor/templates/_html.py +27 -9
- scitex/fig/editor/flask_editor/templates/_scripts.py +1928 -131
- scitex/fig/editor/flask_editor/templates/_styles.py +363 -51
- scitex/fig/io/_bundle.py +97 -12
- scitex/io/__init__.py +12 -0
- scitex/io/_bundle.py +69 -10
- scitex/io/_zip_bundle.py +439 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +0 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +0 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +0 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +0 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +0 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +0 -0
- scitex/plt/io/_layered_bundle.py +0 -0
- scitex/schema/_plot.py +0 -0
- {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/METADATA +1 -1
- {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/RECORD +78 -22
- scitex/fig/editor/_edit.py +0 -751
- {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
- {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -92,9 +92,193 @@ class WebEditor:
|
|
|
92
92
|
print(f"Warning: Port {self._requested_port} may still be in use")
|
|
93
93
|
self.port = self._requested_port
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
# Configure Flask with static folder path
|
|
96
|
+
import os
|
|
97
|
+
static_folder = os.path.join(os.path.dirname(__file__), 'static')
|
|
98
|
+
app = Flask(__name__, static_folder=static_folder, static_url_path='/static')
|
|
96
99
|
editor = self
|
|
97
100
|
|
|
101
|
+
def _export_composed_figure(editor, formats=["png", "svg"], dpi=150):
|
|
102
|
+
"""Helper to compose and export figure to bundle."""
|
|
103
|
+
from scitex.io import ZipBundle
|
|
104
|
+
from PIL import Image
|
|
105
|
+
import numpy as np
|
|
106
|
+
import matplotlib
|
|
107
|
+
matplotlib.use('Agg')
|
|
108
|
+
import matplotlib.pyplot as plt
|
|
109
|
+
import json as json_module
|
|
110
|
+
import io
|
|
111
|
+
import zipfile
|
|
112
|
+
|
|
113
|
+
if not editor.panel_info:
|
|
114
|
+
return {"success": False, "error": "No panel info"}
|
|
115
|
+
|
|
116
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
117
|
+
figz_dir = editor.panel_info.get("figz_dir")
|
|
118
|
+
|
|
119
|
+
if not bundle_path and not figz_dir:
|
|
120
|
+
return {"success": False, "error": "No bundle path"}
|
|
121
|
+
|
|
122
|
+
figure_name = Path(bundle_path).stem if bundle_path else (
|
|
123
|
+
Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Read spec.json for layout and layout.json for position overrides
|
|
127
|
+
spec = {}
|
|
128
|
+
layout_overrides = {}
|
|
129
|
+
if bundle_path:
|
|
130
|
+
try:
|
|
131
|
+
with ZipBundle(bundle_path, mode="r") as bundle:
|
|
132
|
+
spec = bundle.read_json("spec.json")
|
|
133
|
+
try:
|
|
134
|
+
layout_overrides = bundle.read_json("layout.json")
|
|
135
|
+
except:
|
|
136
|
+
pass
|
|
137
|
+
except:
|
|
138
|
+
pass
|
|
139
|
+
elif figz_dir:
|
|
140
|
+
spec_path = Path(figz_dir) / "spec.json"
|
|
141
|
+
if spec_path.exists():
|
|
142
|
+
with open(spec_path) as f:
|
|
143
|
+
spec = json_module.load(f)
|
|
144
|
+
layout_path = Path(figz_dir) / "layout.json"
|
|
145
|
+
if layout_path.exists():
|
|
146
|
+
with open(layout_path) as f:
|
|
147
|
+
layout_overrides = json_module.load(f)
|
|
148
|
+
|
|
149
|
+
# Also check in-memory layout overrides
|
|
150
|
+
if editor.panel_info and editor.panel_info.get("layout"):
|
|
151
|
+
layout_overrides = editor.panel_info.get("layout", {})
|
|
152
|
+
|
|
153
|
+
# Get figure dimensions
|
|
154
|
+
fig_width_mm = 180
|
|
155
|
+
fig_height_mm = 120
|
|
156
|
+
if "figure" in spec:
|
|
157
|
+
fig_info = spec.get("figure", {})
|
|
158
|
+
styles = fig_info.get("styles", {})
|
|
159
|
+
size = styles.get("size", {})
|
|
160
|
+
fig_width_mm = size.get("width_mm", 180)
|
|
161
|
+
fig_height_mm = size.get("height_mm", 120)
|
|
162
|
+
|
|
163
|
+
fig_width_in = fig_width_mm / 25.4
|
|
164
|
+
fig_height_in = fig_height_mm / 25.4
|
|
165
|
+
|
|
166
|
+
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
167
|
+
|
|
168
|
+
# Compose panels
|
|
169
|
+
panels_spec = spec.get("panels", [])
|
|
170
|
+
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
171
|
+
panel_is_zip = editor.panel_info.get("panel_is_zip", [])
|
|
172
|
+
|
|
173
|
+
for panel_spec in panels_spec:
|
|
174
|
+
panel_id = panel_spec.get("id", "")
|
|
175
|
+
pos = panel_spec.get("position", {})
|
|
176
|
+
size = panel_spec.get("size", {})
|
|
177
|
+
|
|
178
|
+
# Skip overview/auxiliary panels (only compose main panels A-Z)
|
|
179
|
+
panel_id_lower = panel_id.lower()
|
|
180
|
+
if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Find panel path first (needed to check layout_overrides)
|
|
184
|
+
panel_path = None
|
|
185
|
+
is_zip = False
|
|
186
|
+
panel_name = None
|
|
187
|
+
for idx, pp in enumerate(panel_paths):
|
|
188
|
+
pp_name = Path(pp).stem.replace(".pltz", "")
|
|
189
|
+
if (pp_name == panel_id or
|
|
190
|
+
pp_name.startswith(f"panel_{panel_id}_") or
|
|
191
|
+
pp_name == f"panel_{panel_id}" or
|
|
192
|
+
f"_{panel_id}_" in pp_name):
|
|
193
|
+
panel_path = pp
|
|
194
|
+
panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
|
|
195
|
+
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
if not panel_path:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Check for layout overrides (from layout.json or in-memory)
|
|
202
|
+
override = layout_overrides.get(panel_name, {})
|
|
203
|
+
override_pos = override.get("position", {})
|
|
204
|
+
override_size = override.get("size", {})
|
|
205
|
+
|
|
206
|
+
# Use override positions if available, otherwise use spec
|
|
207
|
+
x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
|
|
208
|
+
y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
|
|
209
|
+
w_mm = override_size.get("width_mm", size.get("width_mm", 60))
|
|
210
|
+
h_mm = override_size.get("height_mm", size.get("height_mm", 40))
|
|
211
|
+
|
|
212
|
+
x_frac = x_mm / fig_width_mm
|
|
213
|
+
y_frac = 1 - (y_mm + h_mm) / fig_height_mm
|
|
214
|
+
w_frac = w_mm / fig_width_mm
|
|
215
|
+
h_frac = h_mm / fig_height_mm
|
|
216
|
+
|
|
217
|
+
# Load panel preview
|
|
218
|
+
try:
|
|
219
|
+
# Exclusion patterns for preview selection
|
|
220
|
+
exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
|
|
221
|
+
|
|
222
|
+
if is_zip:
|
|
223
|
+
with ZipBundle(panel_path, mode="r") as pltz_bundle:
|
|
224
|
+
with zipfile.ZipFile(panel_path, 'r') as zf:
|
|
225
|
+
png_files = [n for n in zf.namelist()
|
|
226
|
+
if n.endswith('.png')
|
|
227
|
+
and 'exports/' in n
|
|
228
|
+
and not any(p in n.lower() for p in exclude_patterns)]
|
|
229
|
+
if png_files:
|
|
230
|
+
preview_path = png_files[0]
|
|
231
|
+
if '.pltz.d/' in preview_path:
|
|
232
|
+
preview_path = preview_path.split('.pltz.d/')[-1]
|
|
233
|
+
img_data = pltz_bundle.read_bytes(preview_path)
|
|
234
|
+
img = Image.open(io.BytesIO(img_data))
|
|
235
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
236
|
+
ax.imshow(np.array(img))
|
|
237
|
+
ax.axis('off')
|
|
238
|
+
else:
|
|
239
|
+
pltz_dir = Path(panel_path)
|
|
240
|
+
exports_dir = pltz_dir / "exports"
|
|
241
|
+
if exports_dir.exists():
|
|
242
|
+
for png_file in exports_dir.glob("*.png"):
|
|
243
|
+
name_lower = png_file.name.lower()
|
|
244
|
+
if not any(p in name_lower for p in exclude_patterns):
|
|
245
|
+
img = Image.open(png_file)
|
|
246
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
247
|
+
ax.imshow(np.array(img))
|
|
248
|
+
ax.axis('off')
|
|
249
|
+
break
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"Could not load panel {panel_id}: {e}")
|
|
252
|
+
|
|
253
|
+
# Draw panel letter
|
|
254
|
+
if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
|
|
255
|
+
# Position letter at top-left corner of panel
|
|
256
|
+
letter_x = x_frac + 0.01
|
|
257
|
+
letter_y = y_frac + h_frac - 0.02
|
|
258
|
+
fig.text(letter_x, letter_y, panel_id,
|
|
259
|
+
fontsize=14, fontweight='bold', color='black',
|
|
260
|
+
ha='left', va='top',
|
|
261
|
+
transform=fig.transFigure,
|
|
262
|
+
bbox=dict(boxstyle='square,pad=0.1',
|
|
263
|
+
facecolor='white', edgecolor='none', alpha=0.8))
|
|
264
|
+
|
|
265
|
+
exported = {}
|
|
266
|
+
|
|
267
|
+
# Save to bundle
|
|
268
|
+
if bundle_path:
|
|
269
|
+
with ZipBundle(bundle_path, mode="a") as bundle:
|
|
270
|
+
for fmt in formats:
|
|
271
|
+
buf = io.BytesIO()
|
|
272
|
+
fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight",
|
|
273
|
+
facecolor="white", pad_inches=0.02)
|
|
274
|
+
buf.seek(0)
|
|
275
|
+
export_path = f"exports/{figure_name}.{fmt}"
|
|
276
|
+
bundle.write_bytes(export_path, buf.read())
|
|
277
|
+
exported[fmt] = export_path
|
|
278
|
+
|
|
279
|
+
plt.close(fig)
|
|
280
|
+
return {"success": True, "exported": exported}
|
|
281
|
+
|
|
98
282
|
@app.route("/")
|
|
99
283
|
def index():
|
|
100
284
|
# Rebuild template each time for hot reload support
|
|
@@ -147,63 +331,94 @@ class WebEditor:
|
|
|
147
331
|
|
|
148
332
|
@app.route("/panels")
|
|
149
333
|
def panels():
|
|
150
|
-
"""Return all panel images with bboxes for interactive grid view (figz bundles only).
|
|
151
|
-
|
|
334
|
+
"""Return all panel images with bboxes for interactive grid view (figz bundles only).
|
|
335
|
+
|
|
336
|
+
Uses smart load_panel_data helper for transparent zip/directory handling.
|
|
337
|
+
Returns layout info from figz spec.json for unified canvas positioning.
|
|
338
|
+
"""
|
|
152
339
|
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
340
|
+
from ..edit import load_panel_data
|
|
341
|
+
import json as json_module
|
|
153
342
|
|
|
154
343
|
if not editor.panel_info:
|
|
155
344
|
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
156
345
|
|
|
157
|
-
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
158
346
|
panel_names = editor.panel_info["panels"]
|
|
347
|
+
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
348
|
+
panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panel_names))
|
|
349
|
+
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
350
|
+
|
|
351
|
+
if not panel_paths:
|
|
352
|
+
panel_paths = [str(figz_dir / name) for name in panel_names]
|
|
353
|
+
|
|
354
|
+
# Load figz spec.json to get panel layout
|
|
355
|
+
figz_layout = {}
|
|
356
|
+
spec_path = figz_dir / "spec.json"
|
|
357
|
+
if spec_path.exists():
|
|
358
|
+
with open(spec_path) as f:
|
|
359
|
+
figz_spec = json_module.load(f)
|
|
360
|
+
for panel_spec in figz_spec.get("panels", []):
|
|
361
|
+
panel_id = panel_spec.get("id", "")
|
|
362
|
+
figz_layout[panel_id] = {
|
|
363
|
+
"position": panel_spec.get("position", {}),
|
|
364
|
+
"size": panel_spec.get("size", {}),
|
|
365
|
+
}
|
|
366
|
+
|
|
159
367
|
panel_images = []
|
|
160
368
|
|
|
161
|
-
for panel_name in panel_names:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# Find PNG in exports/ or root
|
|
166
|
-
png_path = None
|
|
167
|
-
exports_dir = panel_dir / "exports"
|
|
168
|
-
if exports_dir.exists():
|
|
169
|
-
for f in exports_dir.glob("*.png"):
|
|
170
|
-
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
171
|
-
png_path = f
|
|
172
|
-
break
|
|
173
|
-
if not png_path:
|
|
174
|
-
for f in panel_dir.glob("*.png"):
|
|
175
|
-
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
176
|
-
png_path = f
|
|
177
|
-
break
|
|
178
|
-
|
|
179
|
-
if png_path and png_path.exists():
|
|
180
|
-
with open(png_path, "rb") as f:
|
|
181
|
-
panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
|
|
182
|
-
img = Image.open(png_path)
|
|
183
|
-
panel_data["width"], panel_data["height"] = img.size
|
|
184
|
-
panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
|
|
185
|
-
img.close()
|
|
369
|
+
for idx, panel_name in enumerate(panel_names):
|
|
370
|
+
panel_path = panel_paths[idx]
|
|
371
|
+
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else None
|
|
372
|
+
display_name = panel_name.replace(".pltz.d", "").replace(".pltz", "")
|
|
186
373
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
374
|
+
# Use smart helper to load panel data
|
|
375
|
+
loaded = load_panel_data(panel_path, is_zip=is_zip)
|
|
376
|
+
|
|
377
|
+
panel_data = {"name": display_name, "image": None, "bboxes": None, "img_size": None}
|
|
378
|
+
|
|
379
|
+
# Add layout info from figz spec
|
|
380
|
+
if display_name in figz_layout:
|
|
381
|
+
panel_data["layout"] = figz_layout[display_name]
|
|
382
|
+
|
|
383
|
+
if loaded:
|
|
384
|
+
# Get image data
|
|
385
|
+
if loaded.get("is_zip"):
|
|
386
|
+
png_bytes = loaded.get("png_bytes")
|
|
387
|
+
if png_bytes:
|
|
388
|
+
panel_data["image"] = base64.b64encode(png_bytes).decode("utf-8")
|
|
198
389
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
390
|
+
png_path = loaded.get("png_path")
|
|
391
|
+
if png_path and png_path.exists():
|
|
392
|
+
with open(png_path, "rb") as f:
|
|
393
|
+
panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
|
|
394
|
+
|
|
395
|
+
# Get image size
|
|
396
|
+
img_size = loaded.get("img_size")
|
|
397
|
+
if img_size:
|
|
398
|
+
panel_data["img_size"] = img_size
|
|
399
|
+
panel_data["width"] = img_size["width"]
|
|
400
|
+
panel_data["height"] = img_size["height"]
|
|
401
|
+
elif loaded.get("png_path"):
|
|
402
|
+
from PIL import Image
|
|
403
|
+
img = Image.open(loaded["png_path"])
|
|
404
|
+
panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
|
|
405
|
+
panel_data["width"], panel_data["height"] = img.size
|
|
406
|
+
img.close()
|
|
407
|
+
|
|
408
|
+
# Extract bboxes - prefer geometry_px.json
|
|
409
|
+
if panel_data.get("img_size"):
|
|
410
|
+
geometry_data = loaded.get("geometry_data")
|
|
411
|
+
metadata = loaded.get("metadata", {})
|
|
412
|
+
|
|
413
|
+
if geometry_data:
|
|
414
|
+
panel_data["bboxes"] = extract_bboxes_from_geometry_px(
|
|
415
|
+
geometry_data,
|
|
416
|
+
panel_data["img_size"]["width"],
|
|
417
|
+
panel_data["img_size"]["height"]
|
|
418
|
+
)
|
|
419
|
+
elif metadata:
|
|
205
420
|
panel_data["bboxes"] = extract_bboxes_from_metadata(
|
|
206
|
-
|
|
421
|
+
metadata,
|
|
207
422
|
panel_data["img_size"]["width"],
|
|
208
423
|
panel_data["img_size"]["height"]
|
|
209
424
|
)
|
|
@@ -213,97 +428,92 @@ class WebEditor:
|
|
|
213
428
|
return jsonify({
|
|
214
429
|
"panels": panel_images,
|
|
215
430
|
"count": len(panel_images),
|
|
431
|
+
"layout": figz_layout,
|
|
216
432
|
})
|
|
217
433
|
|
|
218
434
|
@app.route("/switch_panel/<int:panel_index>")
|
|
219
435
|
def switch_panel(panel_index):
|
|
220
436
|
"""Switch to a different panel in the figz bundle.
|
|
221
437
|
|
|
222
|
-
|
|
438
|
+
Uses smart load_panel_data helper for transparent zip/directory handling.
|
|
223
439
|
"""
|
|
224
|
-
from PIL import Image
|
|
225
|
-
from .._edit import _load_panel_data
|
|
226
440
|
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
441
|
+
from ..edit import load_panel_data
|
|
227
442
|
|
|
228
443
|
if not editor.panel_info:
|
|
229
444
|
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
230
445
|
|
|
231
446
|
panels = editor.panel_info["panels"]
|
|
447
|
+
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
448
|
+
panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panels))
|
|
449
|
+
|
|
232
450
|
if panel_index < 0 or panel_index >= len(panels):
|
|
233
451
|
return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
|
|
234
452
|
|
|
235
|
-
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
236
453
|
panel_name = panels[panel_index]
|
|
237
|
-
|
|
454
|
+
panel_path = panel_paths[panel_index] if panel_paths else str(Path(editor.panel_info["figz_dir"]) / panel_name)
|
|
455
|
+
is_zip = panel_is_zip[panel_index] if panel_index < len(panel_is_zip) else None
|
|
238
456
|
|
|
239
|
-
# Load the panel's data
|
|
240
457
|
try:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
|
|
458
|
+
# Use smart helper to load panel data
|
|
459
|
+
loaded = load_panel_data(panel_path, is_zip=is_zip)
|
|
244
460
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
editor.metadata = panel_data["metadata"]
|
|
248
|
-
editor.csv_data = panel_data.get("csv_data")
|
|
249
|
-
editor.png_path = panel_data.get("png_path")
|
|
250
|
-
editor.hitmap_path = panel_data.get("hitmap_path")
|
|
251
|
-
editor.panel_info["current_index"] = panel_index
|
|
252
|
-
|
|
253
|
-
# Re-extract defaults from new metadata
|
|
254
|
-
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
255
|
-
editor.scitex_defaults = get_scitex_defaults()
|
|
256
|
-
editor.metadata_defaults = extract_defaults_from_metadata(editor.metadata)
|
|
257
|
-
editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
|
|
258
|
-
editor.current_overrides.update(editor.metadata_defaults)
|
|
259
|
-
editor.current_overrides.update(editor.manual_overrides)
|
|
461
|
+
if not loaded:
|
|
462
|
+
return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
|
|
260
463
|
|
|
261
|
-
#
|
|
464
|
+
# Get image data
|
|
262
465
|
img_data = None
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
with open(png_path, "rb") as f:
|
|
268
|
-
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
269
|
-
img = Image.open(png_path)
|
|
270
|
-
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
271
|
-
img.close()
|
|
466
|
+
if loaded.get("is_zip"):
|
|
467
|
+
png_bytes = loaded.get("png_bytes")
|
|
468
|
+
if png_bytes:
|
|
469
|
+
img_data = base64.b64encode(png_bytes).decode("utf-8")
|
|
272
470
|
else:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
278
|
-
with open(f, "rb") as pf:
|
|
279
|
-
img_data = base64.b64encode(pf.read()).decode("utf-8")
|
|
280
|
-
img = Image.open(f)
|
|
281
|
-
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
282
|
-
img.close()
|
|
283
|
-
break
|
|
471
|
+
png_path = loaded.get("png_path")
|
|
472
|
+
if png_path and png_path.exists():
|
|
473
|
+
with open(png_path, "rb") as f:
|
|
474
|
+
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
284
475
|
|
|
285
476
|
if not img_data:
|
|
286
477
|
return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
|
|
287
478
|
|
|
288
|
-
#
|
|
479
|
+
# Get image size
|
|
480
|
+
img_size = loaded.get("img_size", {"width": 0, "height": 0})
|
|
481
|
+
if not img_size and loaded.get("png_path"):
|
|
482
|
+
from PIL import Image
|
|
483
|
+
img = Image.open(loaded["png_path"])
|
|
484
|
+
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
485
|
+
img.close()
|
|
486
|
+
|
|
487
|
+
# Extract bboxes - prefer geometry_px.json
|
|
289
488
|
bboxes = {}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
489
|
+
geometry_data = loaded.get("geometry_data")
|
|
490
|
+
metadata = loaded.get("metadata", {})
|
|
491
|
+
|
|
492
|
+
if geometry_data and img_size:
|
|
294
493
|
bboxes = extract_bboxes_from_geometry_px(
|
|
295
494
|
geometry_data,
|
|
296
495
|
img_size["width"],
|
|
297
|
-
img_size["height"]
|
|
496
|
+
img_size["height"]
|
|
298
497
|
)
|
|
299
|
-
|
|
300
|
-
# Fall back to metadata extraction
|
|
498
|
+
elif metadata and img_size:
|
|
301
499
|
bboxes = extract_bboxes_from_metadata(
|
|
302
|
-
|
|
500
|
+
metadata,
|
|
303
501
|
img_size["width"],
|
|
304
|
-
img_size["height"]
|
|
502
|
+
img_size["height"]
|
|
305
503
|
)
|
|
306
504
|
|
|
505
|
+
# Update editor state
|
|
506
|
+
editor.metadata = metadata
|
|
507
|
+
editor.panel_info["current_index"] = panel_index
|
|
508
|
+
|
|
509
|
+
# Re-extract defaults from new metadata
|
|
510
|
+
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
511
|
+
editor.scitex_defaults = get_scitex_defaults()
|
|
512
|
+
editor.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
513
|
+
editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
|
|
514
|
+
editor.current_overrides.update(editor.metadata_defaults)
|
|
515
|
+
editor.current_overrides.update(editor.manual_overrides)
|
|
516
|
+
|
|
307
517
|
return jsonify({
|
|
308
518
|
"success": True,
|
|
309
519
|
"panel_name": panel_name,
|
|
@@ -368,7 +578,7 @@ class WebEditor:
|
|
|
368
578
|
@app.route("/save", methods=["POST"])
|
|
369
579
|
def save():
|
|
370
580
|
"""Save to .manual.json."""
|
|
371
|
-
from ..
|
|
581
|
+
from ..edit import save_manual_overrides
|
|
372
582
|
|
|
373
583
|
try:
|
|
374
584
|
manual_path = save_manual_overrides(
|
|
@@ -378,6 +588,601 @@ class WebEditor:
|
|
|
378
588
|
except Exception as e:
|
|
379
589
|
return jsonify({"status": "error", "message": str(e)}), 500
|
|
380
590
|
|
|
591
|
+
@app.route("/save_layout", methods=["POST"])
|
|
592
|
+
def save_layout():
|
|
593
|
+
"""Save panel layout positions to figz bundle."""
|
|
594
|
+
try:
|
|
595
|
+
data = request.get_json()
|
|
596
|
+
layout = data.get("layout", {})
|
|
597
|
+
|
|
598
|
+
if not layout:
|
|
599
|
+
return jsonify({"success": False, "error": "No layout data provided"})
|
|
600
|
+
|
|
601
|
+
# Check if we have panel_info (figz bundle)
|
|
602
|
+
if not editor.panel_info:
|
|
603
|
+
return jsonify({"success": False, "error": "No panel info available (not a figz bundle)"})
|
|
604
|
+
|
|
605
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
606
|
+
if not bundle_path:
|
|
607
|
+
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
608
|
+
|
|
609
|
+
# Update layout in the figz bundle
|
|
610
|
+
from scitex.fig.io import ZipBundle
|
|
611
|
+
|
|
612
|
+
bundle = ZipBundle(bundle_path)
|
|
613
|
+
|
|
614
|
+
# Read existing layout or create new one
|
|
615
|
+
try:
|
|
616
|
+
existing_layout = bundle.read_json("layout.json")
|
|
617
|
+
except:
|
|
618
|
+
existing_layout = {}
|
|
619
|
+
|
|
620
|
+
# Update layout with new positions
|
|
621
|
+
for panel_name, pos in layout.items():
|
|
622
|
+
if panel_name not in existing_layout:
|
|
623
|
+
existing_layout[panel_name] = {}
|
|
624
|
+
if "position" not in existing_layout[panel_name]:
|
|
625
|
+
existing_layout[panel_name]["position"] = {}
|
|
626
|
+
if "size" not in existing_layout[panel_name]:
|
|
627
|
+
existing_layout[panel_name]["size"] = {}
|
|
628
|
+
|
|
629
|
+
# Update position
|
|
630
|
+
existing_layout[panel_name]["position"]["x_mm"] = pos.get("x_mm", 0)
|
|
631
|
+
existing_layout[panel_name]["position"]["y_mm"] = pos.get("y_mm", 0)
|
|
632
|
+
|
|
633
|
+
# Update size if provided
|
|
634
|
+
if "width_mm" in pos:
|
|
635
|
+
existing_layout[panel_name]["size"]["width_mm"] = pos["width_mm"]
|
|
636
|
+
if "height_mm" in pos:
|
|
637
|
+
existing_layout[panel_name]["size"]["height_mm"] = pos["height_mm"]
|
|
638
|
+
|
|
639
|
+
# Save updated layout
|
|
640
|
+
bundle.write_json("layout.json", existing_layout)
|
|
641
|
+
|
|
642
|
+
# Update in-memory panel_info
|
|
643
|
+
editor.panel_info["layout"] = existing_layout
|
|
644
|
+
|
|
645
|
+
# Auto-export composed figure to bundle
|
|
646
|
+
export_result = _export_composed_figure(editor, formats=["png", "svg"])
|
|
647
|
+
|
|
648
|
+
return jsonify({
|
|
649
|
+
"success": True,
|
|
650
|
+
"layout": existing_layout,
|
|
651
|
+
"exported": export_result.get("exported", {})
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
except Exception as e:
|
|
655
|
+
import traceback
|
|
656
|
+
return jsonify({
|
|
657
|
+
"success": False,
|
|
658
|
+
"error": str(e),
|
|
659
|
+
"traceback": traceback.format_exc()
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
@app.route("/save_element_position", methods=["POST"])
|
|
663
|
+
def save_element_position():
|
|
664
|
+
"""Save element position (legend/panel_letter) to figz bundle.
|
|
665
|
+
|
|
666
|
+
ONLY legends and panel letters can be repositioned to maintain
|
|
667
|
+
scientific rigor. Data elements are never moved.
|
|
668
|
+
"""
|
|
669
|
+
try:
|
|
670
|
+
data = request.get_json()
|
|
671
|
+
element = data.get("element", "")
|
|
672
|
+
panel = data.get("panel", "")
|
|
673
|
+
element_type = data.get("element_type", "")
|
|
674
|
+
position = data.get("position", {})
|
|
675
|
+
snap_name = data.get("snap_name")
|
|
676
|
+
|
|
677
|
+
# Validate element type (whitelist for scientific rigor)
|
|
678
|
+
ALLOWED_TYPES = ["legend", "panel_letter"]
|
|
679
|
+
if element_type not in ALLOWED_TYPES:
|
|
680
|
+
return jsonify({
|
|
681
|
+
"success": False,
|
|
682
|
+
"error": f"Element type '{element_type}' cannot be repositioned (scientific rigor)"
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
if not editor.panel_info:
|
|
686
|
+
return jsonify({"success": False, "error": "No panel info available"})
|
|
687
|
+
|
|
688
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
689
|
+
if not bundle_path:
|
|
690
|
+
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
691
|
+
|
|
692
|
+
from scitex.fig.io import ZipBundle
|
|
693
|
+
bundle = ZipBundle(bundle_path)
|
|
694
|
+
|
|
695
|
+
# Read or create style.json for element positions
|
|
696
|
+
try:
|
|
697
|
+
style = bundle.read_json("style.json")
|
|
698
|
+
except:
|
|
699
|
+
style = {}
|
|
700
|
+
|
|
701
|
+
# Initialize structure
|
|
702
|
+
if "elements" not in style:
|
|
703
|
+
style["elements"] = {}
|
|
704
|
+
if panel not in style["elements"]:
|
|
705
|
+
style["elements"][panel] = {}
|
|
706
|
+
|
|
707
|
+
# Save element position
|
|
708
|
+
style["elements"][panel][element] = {
|
|
709
|
+
"type": element_type,
|
|
710
|
+
"position": position,
|
|
711
|
+
"snap_name": snap_name,
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# For legends, also update legend_location for matplotlib compatibility
|
|
715
|
+
if element_type == "legend" and snap_name:
|
|
716
|
+
# Convert snap name to matplotlib loc format
|
|
717
|
+
loc_map = {
|
|
718
|
+
"upper left": "upper left",
|
|
719
|
+
"upper center": "upper center",
|
|
720
|
+
"upper right": "upper right",
|
|
721
|
+
"center left": "center left",
|
|
722
|
+
"center": "center",
|
|
723
|
+
"center right": "center right",
|
|
724
|
+
"lower left": "lower left",
|
|
725
|
+
"lower center": "lower center",
|
|
726
|
+
"lower right": "lower right",
|
|
727
|
+
}
|
|
728
|
+
if snap_name in loc_map:
|
|
729
|
+
if "legend" not in style:
|
|
730
|
+
style["legend"] = {}
|
|
731
|
+
style["legend"]["location"] = loc_map[snap_name]
|
|
732
|
+
|
|
733
|
+
bundle.write_json("style.json", style)
|
|
734
|
+
|
|
735
|
+
return jsonify({
|
|
736
|
+
"success": True,
|
|
737
|
+
"element": element,
|
|
738
|
+
"position": position,
|
|
739
|
+
"snap_name": snap_name
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
except Exception as e:
|
|
743
|
+
import traceback
|
|
744
|
+
return jsonify({
|
|
745
|
+
"success": False,
|
|
746
|
+
"error": str(e),
|
|
747
|
+
"traceback": traceback.format_exc()
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
@app.route("/export", methods=["POST"])
|
|
751
|
+
def export_figure():
|
|
752
|
+
"""Export composed figure to various formats and update figz bundle."""
|
|
753
|
+
try:
|
|
754
|
+
data = request.get_json()
|
|
755
|
+
formats = data.get("formats", ["png", "svg"])
|
|
756
|
+
|
|
757
|
+
if not editor.panel_info:
|
|
758
|
+
return jsonify({"success": False, "error": "No panel info available"})
|
|
759
|
+
|
|
760
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
761
|
+
if not bundle_path:
|
|
762
|
+
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
763
|
+
|
|
764
|
+
from scitex.io import ZipBundle
|
|
765
|
+
from pathlib import Path
|
|
766
|
+
import io
|
|
767
|
+
import matplotlib
|
|
768
|
+
matplotlib.use('Agg')
|
|
769
|
+
import matplotlib.pyplot as plt
|
|
770
|
+
from PIL import Image
|
|
771
|
+
import numpy as np
|
|
772
|
+
|
|
773
|
+
figure_name = Path(bundle_path).stem
|
|
774
|
+
dpi = data.get("dpi", 150)
|
|
775
|
+
|
|
776
|
+
with ZipBundle(bundle_path, mode="a") as bundle:
|
|
777
|
+
# Read spec for figure size and panel positions
|
|
778
|
+
try:
|
|
779
|
+
spec = bundle.read_json("spec.json")
|
|
780
|
+
except:
|
|
781
|
+
spec = {}
|
|
782
|
+
|
|
783
|
+
# Get figure dimensions
|
|
784
|
+
fig_width_mm = 180
|
|
785
|
+
fig_height_mm = 120
|
|
786
|
+
if "figure" in spec:
|
|
787
|
+
fig_info = spec.get("figure", {})
|
|
788
|
+
styles = fig_info.get("styles", {})
|
|
789
|
+
size = styles.get("size", {})
|
|
790
|
+
fig_width_mm = size.get("width_mm", 180)
|
|
791
|
+
fig_height_mm = size.get("height_mm", 120)
|
|
792
|
+
|
|
793
|
+
# Convert mm to inches
|
|
794
|
+
fig_width_in = fig_width_mm / 25.4
|
|
795
|
+
fig_height_in = fig_height_mm / 25.4
|
|
796
|
+
|
|
797
|
+
# Create figure with white background
|
|
798
|
+
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
799
|
+
|
|
800
|
+
# Get panels from spec or editor.panel_info
|
|
801
|
+
panels_spec = spec.get("panels", [])
|
|
802
|
+
|
|
803
|
+
# Compose panels onto figure
|
|
804
|
+
for panel_spec in panels_spec:
|
|
805
|
+
panel_id = panel_spec.get("id", "")
|
|
806
|
+
pltz_name = panel_spec.get("plot", "")
|
|
807
|
+
|
|
808
|
+
# Get position and size from spec
|
|
809
|
+
pos = panel_spec.get("position", {})
|
|
810
|
+
size = panel_spec.get("size", {})
|
|
811
|
+
|
|
812
|
+
x_mm = pos.get("x_mm", 0)
|
|
813
|
+
y_mm = pos.get("y_mm", 0)
|
|
814
|
+
w_mm = size.get("width_mm", 60)
|
|
815
|
+
h_mm = size.get("height_mm", 40)
|
|
816
|
+
|
|
817
|
+
# Convert to figure coordinates (0-1)
|
|
818
|
+
x_frac = x_mm / fig_width_mm
|
|
819
|
+
y_frac = 1 - (y_mm + h_mm) / fig_height_mm # Flip Y
|
|
820
|
+
w_frac = w_mm / fig_width_mm
|
|
821
|
+
h_frac = h_mm / fig_height_mm
|
|
822
|
+
|
|
823
|
+
# Try to read panel image from pltz exports
|
|
824
|
+
img_loaded = False
|
|
825
|
+
for pltz_path in [f"{panel_id}.pltz", pltz_name.replace(".d", "")]:
|
|
826
|
+
if img_loaded:
|
|
827
|
+
break
|
|
828
|
+
try:
|
|
829
|
+
# Read pltz as nested bundle
|
|
830
|
+
pltz_bytes = bundle.read_bytes(pltz_path)
|
|
831
|
+
import tempfile
|
|
832
|
+
with tempfile.NamedTemporaryFile(suffix=".pltz", delete=False) as tmp:
|
|
833
|
+
tmp.write(pltz_bytes)
|
|
834
|
+
tmp_path = tmp.name
|
|
835
|
+
try:
|
|
836
|
+
with ZipBundle(tmp_path, mode="r") as pltz_bundle:
|
|
837
|
+
# Try various preview paths
|
|
838
|
+
for preview_path in ["exports/preview.png", "preview.png", f"exports/{panel_id}.png"]:
|
|
839
|
+
try:
|
|
840
|
+
img_data = pltz_bundle.read_bytes(preview_path)
|
|
841
|
+
img = Image.open(io.BytesIO(img_data))
|
|
842
|
+
img_array = np.array(img)
|
|
843
|
+
|
|
844
|
+
# Create axes and add image
|
|
845
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
846
|
+
ax.imshow(img_array)
|
|
847
|
+
ax.axis('off')
|
|
848
|
+
img_loaded = True
|
|
849
|
+
break
|
|
850
|
+
except:
|
|
851
|
+
continue
|
|
852
|
+
finally:
|
|
853
|
+
import os
|
|
854
|
+
os.unlink(tmp_path)
|
|
855
|
+
except Exception as e:
|
|
856
|
+
print(f"Could not load pltz {pltz_path}: {e}")
|
|
857
|
+
continue
|
|
858
|
+
|
|
859
|
+
exported = {}
|
|
860
|
+
|
|
861
|
+
for fmt in formats:
|
|
862
|
+
buf = io.BytesIO()
|
|
863
|
+
if fmt in ["png", "jpeg", "jpg"]:
|
|
864
|
+
fig.savefig(buf, format="png" if fmt == "png" else "jpeg",
|
|
865
|
+
dpi=dpi, bbox_inches="tight", facecolor="white",
|
|
866
|
+
pad_inches=0.02)
|
|
867
|
+
elif fmt == "svg":
|
|
868
|
+
fig.savefig(buf, format="svg", bbox_inches="tight", pad_inches=0.02)
|
|
869
|
+
elif fmt == "pdf":
|
|
870
|
+
fig.savefig(buf, format="pdf", bbox_inches="tight", pad_inches=0.02)
|
|
871
|
+
else:
|
|
872
|
+
continue
|
|
873
|
+
|
|
874
|
+
buf.seek(0)
|
|
875
|
+
content = buf.read()
|
|
876
|
+
|
|
877
|
+
# Save to exports/ directory in bundle
|
|
878
|
+
export_path = f"exports/{figure_name}.{fmt}"
|
|
879
|
+
bundle.write_bytes(export_path, content)
|
|
880
|
+
exported[fmt] = export_path
|
|
881
|
+
|
|
882
|
+
plt.close(fig)
|
|
883
|
+
|
|
884
|
+
return jsonify({
|
|
885
|
+
"success": True,
|
|
886
|
+
"exported": exported,
|
|
887
|
+
"bundle_path": str(bundle_path)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
except Exception as e:
|
|
891
|
+
import traceback
|
|
892
|
+
return jsonify({
|
|
893
|
+
"success": False,
|
|
894
|
+
"error": str(e),
|
|
895
|
+
"traceback": traceback.format_exc()
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
@app.route("/download/<fmt>")
|
|
899
|
+
def download_figure(fmt):
|
|
900
|
+
"""Download figure in specified format."""
|
|
901
|
+
try:
|
|
902
|
+
from flask import send_file
|
|
903
|
+
import io
|
|
904
|
+
from pathlib import Path
|
|
905
|
+
|
|
906
|
+
mime_types = {
|
|
907
|
+
"png": "image/png",
|
|
908
|
+
"jpeg": "image/jpeg",
|
|
909
|
+
"jpg": "image/jpeg",
|
|
910
|
+
"svg": "image/svg+xml",
|
|
911
|
+
"pdf": "application/pdf",
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if fmt not in mime_types:
|
|
915
|
+
return f"Unsupported format: {fmt}", 400
|
|
916
|
+
|
|
917
|
+
# For figz bundles, download the composed figure
|
|
918
|
+
if editor.panel_info:
|
|
919
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
920
|
+
figz_dir = editor.panel_info.get("figz_dir")
|
|
921
|
+
figure_name = Path(bundle_path).stem if bundle_path else (
|
|
922
|
+
Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if bundle_path or figz_dir:
|
|
926
|
+
from scitex.io import ZipBundle
|
|
927
|
+
from PIL import Image
|
|
928
|
+
import numpy as np
|
|
929
|
+
import matplotlib
|
|
930
|
+
matplotlib.use('Agg')
|
|
931
|
+
import matplotlib.pyplot as plt
|
|
932
|
+
import json as json_module
|
|
933
|
+
|
|
934
|
+
# Always compose on-demand to ensure current panel state
|
|
935
|
+
# (existing exports in bundle may be stale or blank)
|
|
936
|
+
|
|
937
|
+
# Read spec.json and layout.json for position overrides
|
|
938
|
+
spec = {}
|
|
939
|
+
layout_overrides = {}
|
|
940
|
+
if bundle_path:
|
|
941
|
+
try:
|
|
942
|
+
with ZipBundle(bundle_path, mode="r") as bundle:
|
|
943
|
+
spec = bundle.read_json("spec.json")
|
|
944
|
+
try:
|
|
945
|
+
layout_overrides = bundle.read_json("layout.json")
|
|
946
|
+
except:
|
|
947
|
+
pass
|
|
948
|
+
except:
|
|
949
|
+
pass
|
|
950
|
+
elif figz_dir:
|
|
951
|
+
spec_path = Path(figz_dir) / "spec.json"
|
|
952
|
+
if spec_path.exists():
|
|
953
|
+
with open(spec_path) as f:
|
|
954
|
+
spec = json_module.load(f)
|
|
955
|
+
layout_path = Path(figz_dir) / "layout.json"
|
|
956
|
+
if layout_path.exists():
|
|
957
|
+
with open(layout_path) as f:
|
|
958
|
+
layout_overrides = json_module.load(f)
|
|
959
|
+
|
|
960
|
+
# Also check in-memory layout overrides (most current)
|
|
961
|
+
if editor.panel_info and editor.panel_info.get("layout"):
|
|
962
|
+
layout_overrides = editor.panel_info.get("layout", {})
|
|
963
|
+
|
|
964
|
+
# Get figure dimensions
|
|
965
|
+
fig_width_mm = 180
|
|
966
|
+
fig_height_mm = 120
|
|
967
|
+
if "figure" in spec:
|
|
968
|
+
fig_info = spec.get("figure", {})
|
|
969
|
+
styles = fig_info.get("styles", {})
|
|
970
|
+
size = styles.get("size", {})
|
|
971
|
+
fig_width_mm = size.get("width_mm", 180)
|
|
972
|
+
fig_height_mm = size.get("height_mm", 120)
|
|
973
|
+
|
|
974
|
+
fig_width_in = fig_width_mm / 25.4
|
|
975
|
+
fig_height_in = fig_height_mm / 25.4
|
|
976
|
+
|
|
977
|
+
dpi = 150 if fmt in ["jpeg", "jpg"] else 300
|
|
978
|
+
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
979
|
+
|
|
980
|
+
# Compose panels
|
|
981
|
+
panels_spec = spec.get("panels", [])
|
|
982
|
+
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
983
|
+
panel_is_zip = editor.panel_info.get("panel_is_zip", [])
|
|
984
|
+
|
|
985
|
+
for panel_spec in panels_spec:
|
|
986
|
+
panel_id = panel_spec.get("id", "")
|
|
987
|
+
pos = panel_spec.get("position", {})
|
|
988
|
+
size = panel_spec.get("size", {})
|
|
989
|
+
|
|
990
|
+
# Skip overview/auxiliary panels (only compose main panels A-Z)
|
|
991
|
+
panel_id_lower = panel_id.lower()
|
|
992
|
+
if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
|
|
993
|
+
continue
|
|
994
|
+
|
|
995
|
+
# Find panel path first (needed to check layout_overrides)
|
|
996
|
+
panel_path = None
|
|
997
|
+
is_zip = False
|
|
998
|
+
panel_name = None
|
|
999
|
+
for idx, pp in enumerate(panel_paths):
|
|
1000
|
+
pp_name = Path(pp).stem.replace(".pltz", "")
|
|
1001
|
+
# Match exact name, or name contains panel_id pattern
|
|
1002
|
+
# e.g., "panel_A_twinx" matches panel_id "A"
|
|
1003
|
+
if (pp_name == panel_id or
|
|
1004
|
+
pp_name.startswith(f"panel_{panel_id}_") or
|
|
1005
|
+
pp_name.startswith(f"panel_{panel_id}.") or
|
|
1006
|
+
pp_name == f"panel_{panel_id}" or
|
|
1007
|
+
pp_name == panel_id or
|
|
1008
|
+
f"_{panel_id}_" in pp_name or
|
|
1009
|
+
pp_name.endswith(f"_{panel_id}")):
|
|
1010
|
+
panel_path = pp
|
|
1011
|
+
panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
|
|
1012
|
+
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
|
|
1013
|
+
break
|
|
1014
|
+
|
|
1015
|
+
if not panel_path:
|
|
1016
|
+
print(f"Could not find panel path for id={panel_id}, available: {[Path(p).stem for p in panel_paths]}")
|
|
1017
|
+
continue
|
|
1018
|
+
|
|
1019
|
+
# Check for layout overrides (from layout.json or in-memory)
|
|
1020
|
+
override = layout_overrides.get(panel_name, {})
|
|
1021
|
+
override_pos = override.get("position", {})
|
|
1022
|
+
override_size = override.get("size", {})
|
|
1023
|
+
|
|
1024
|
+
# Use override positions if available, otherwise use spec
|
|
1025
|
+
x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
|
|
1026
|
+
y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
|
|
1027
|
+
w_mm = override_size.get("width_mm", size.get("width_mm", 60))
|
|
1028
|
+
h_mm = override_size.get("height_mm", size.get("height_mm", 40))
|
|
1029
|
+
|
|
1030
|
+
x_frac = x_mm / fig_width_mm
|
|
1031
|
+
y_frac = 1 - (y_mm + h_mm) / fig_height_mm
|
|
1032
|
+
w_frac = w_mm / fig_width_mm
|
|
1033
|
+
h_frac = h_mm / fig_height_mm
|
|
1034
|
+
|
|
1035
|
+
# Load panel preview image
|
|
1036
|
+
try:
|
|
1037
|
+
img_loaded = False
|
|
1038
|
+
# Exclusion patterns for preview selection
|
|
1039
|
+
exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
|
|
1040
|
+
|
|
1041
|
+
if is_zip:
|
|
1042
|
+
with ZipBundle(panel_path, mode="r") as pltz_bundle:
|
|
1043
|
+
# Find PNG in exports (exclude hitmap, overview, thumbnails)
|
|
1044
|
+
import zipfile
|
|
1045
|
+
with zipfile.ZipFile(panel_path, 'r') as zf:
|
|
1046
|
+
png_files = [n for n in zf.namelist()
|
|
1047
|
+
if n.endswith('.png')
|
|
1048
|
+
and 'exports/' in n
|
|
1049
|
+
and not any(p in n.lower() for p in exclude_patterns)]
|
|
1050
|
+
if png_files:
|
|
1051
|
+
# Use first matching PNG
|
|
1052
|
+
preview_path = png_files[0]
|
|
1053
|
+
# Extract the path relative to .d directory
|
|
1054
|
+
if '.pltz.d/' in preview_path:
|
|
1055
|
+
preview_path = preview_path.split('.pltz.d/')[-1]
|
|
1056
|
+
try:
|
|
1057
|
+
img_data = pltz_bundle.read_bytes(preview_path)
|
|
1058
|
+
img = Image.open(io.BytesIO(img_data))
|
|
1059
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
1060
|
+
ax.imshow(np.array(img))
|
|
1061
|
+
ax.axis('off')
|
|
1062
|
+
img_loaded = True
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
print(f"Could not read {preview_path}: {e}")
|
|
1065
|
+
else:
|
|
1066
|
+
# Directory-based pltz
|
|
1067
|
+
pltz_dir = Path(panel_path)
|
|
1068
|
+
exports_dir = pltz_dir / "exports"
|
|
1069
|
+
if exports_dir.exists():
|
|
1070
|
+
for png_file in exports_dir.glob("*.png"):
|
|
1071
|
+
name_lower = png_file.name.lower()
|
|
1072
|
+
if not any(p in name_lower for p in exclude_patterns):
|
|
1073
|
+
img = Image.open(png_file)
|
|
1074
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
1075
|
+
ax.imshow(np.array(img))
|
|
1076
|
+
ax.axis('off')
|
|
1077
|
+
img_loaded = True
|
|
1078
|
+
break
|
|
1079
|
+
if not img_loaded:
|
|
1080
|
+
print(f"No preview found for panel {panel_id}")
|
|
1081
|
+
except Exception as e:
|
|
1082
|
+
print(f"Could not load panel {panel_id}: {e}")
|
|
1083
|
+
|
|
1084
|
+
# Draw panel letter
|
|
1085
|
+
if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
|
|
1086
|
+
# Position letter at top-left corner of panel
|
|
1087
|
+
letter_x = x_frac + 0.01
|
|
1088
|
+
letter_y = y_frac + h_frac - 0.02
|
|
1089
|
+
fig.text(letter_x, letter_y, panel_id,
|
|
1090
|
+
fontsize=14, fontweight='bold', color='black',
|
|
1091
|
+
ha='left', va='top',
|
|
1092
|
+
transform=fig.transFigure,
|
|
1093
|
+
bbox=dict(boxstyle='square,pad=0.1',
|
|
1094
|
+
facecolor='white', edgecolor='none', alpha=0.8))
|
|
1095
|
+
|
|
1096
|
+
buf = io.BytesIO()
|
|
1097
|
+
fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
|
|
1098
|
+
dpi=dpi, bbox_inches="tight", facecolor="white",
|
|
1099
|
+
pad_inches=0.02)
|
|
1100
|
+
plt.close(fig)
|
|
1101
|
+
buf.seek(0)
|
|
1102
|
+
|
|
1103
|
+
return send_file(
|
|
1104
|
+
buf,
|
|
1105
|
+
mimetype=mime_types[fmt],
|
|
1106
|
+
as_attachment=True,
|
|
1107
|
+
download_name=f"{figure_name}.{fmt}"
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
# For single pltz files, render from csv_data
|
|
1111
|
+
from ._renderer import render_preview_with_bboxes
|
|
1112
|
+
import matplotlib
|
|
1113
|
+
matplotlib.use('Agg')
|
|
1114
|
+
import matplotlib.pyplot as plt
|
|
1115
|
+
|
|
1116
|
+
figure_name = "figure"
|
|
1117
|
+
if editor.json_path:
|
|
1118
|
+
figure_name = Path(editor.json_path).stem
|
|
1119
|
+
|
|
1120
|
+
img_data, _, _ = render_preview_with_bboxes(
|
|
1121
|
+
editor.csv_data, editor.current_overrides,
|
|
1122
|
+
metadata=editor.metadata,
|
|
1123
|
+
dark_mode=False,
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
if fmt == "png":
|
|
1127
|
+
import base64
|
|
1128
|
+
content = base64.b64decode(img_data)
|
|
1129
|
+
buf = io.BytesIO(content)
|
|
1130
|
+
return send_file(
|
|
1131
|
+
buf,
|
|
1132
|
+
mimetype=mime_types[fmt],
|
|
1133
|
+
as_attachment=True,
|
|
1134
|
+
download_name=f"{figure_name}.{fmt}"
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
# For other formats, re-render
|
|
1138
|
+
from ._plotter import plot_from_csv
|
|
1139
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
1140
|
+
plot_from_csv(ax, editor.csv_data, editor.current_overrides)
|
|
1141
|
+
|
|
1142
|
+
buf = io.BytesIO()
|
|
1143
|
+
dpi = 150 if fmt in ["jpeg", "jpg"] else 300
|
|
1144
|
+
fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
|
|
1145
|
+
dpi=dpi, bbox_inches="tight",
|
|
1146
|
+
facecolor="white" if fmt in ["jpeg", "jpg"] else None)
|
|
1147
|
+
plt.close(fig)
|
|
1148
|
+
buf.seek(0)
|
|
1149
|
+
|
|
1150
|
+
return send_file(
|
|
1151
|
+
buf,
|
|
1152
|
+
mimetype=mime_types[fmt],
|
|
1153
|
+
as_attachment=True,
|
|
1154
|
+
download_name=f"{figure_name}.{fmt}"
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
import traceback
|
|
1159
|
+
return f"Error: {str(e)}\n{traceback.format_exc()}", 500
|
|
1160
|
+
|
|
1161
|
+
@app.route("/download_figz")
|
|
1162
|
+
def download_figz():
|
|
1163
|
+
"""Download as figz bundle (re-editable format)."""
|
|
1164
|
+
try:
|
|
1165
|
+
if not editor.panel_info:
|
|
1166
|
+
return "No panel info available", 404
|
|
1167
|
+
|
|
1168
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
1169
|
+
if not bundle_path:
|
|
1170
|
+
return "Bundle path not available", 404
|
|
1171
|
+
|
|
1172
|
+
from flask import send_file
|
|
1173
|
+
from pathlib import Path
|
|
1174
|
+
|
|
1175
|
+
# Send the figz file directly (it's already a pltz-compatible format)
|
|
1176
|
+
return send_file(
|
|
1177
|
+
bundle_path,
|
|
1178
|
+
mimetype="application/zip",
|
|
1179
|
+
as_attachment=True,
|
|
1180
|
+
download_name=Path(bundle_path).name
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
except Exception as e:
|
|
1184
|
+
return str(e), 500
|
|
1185
|
+
|
|
381
1186
|
@app.route("/shutdown", methods=["POST"])
|
|
382
1187
|
def shutdown():
|
|
383
1188
|
"""Shutdown the server."""
|