scitex 2.4.3__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/io/_load.py +5 -0
  3. scitex/io/_load_modules/_canvas.py +171 -0
  4. scitex/io/_save.py +8 -0
  5. scitex/io/_save_modules/_canvas.py +356 -0
  6. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
  7. scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
  8. scitex/plt/utils/__init__.py +10 -0
  9. scitex/plt/utils/_collect_figure_metadata.py +14 -12
  10. scitex/plt/utils/_csv_column_naming.py +237 -0
  11. scitex/session/_decorator.py +13 -1
  12. scitex/vis/README.md +246 -615
  13. scitex/vis/__init__.py +138 -78
  14. scitex/vis/canvas.py +423 -0
  15. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  16. scitex/vis/editor/__init__.py +1 -1
  17. scitex/vis/editor/_dearpygui_editor.py +1830 -0
  18. scitex/vis/editor/_defaults.py +40 -1
  19. scitex/vis/editor/_edit.py +54 -18
  20. scitex/vis/editor/_flask_editor.py +37 -0
  21. scitex/vis/editor/_qt_editor.py +865 -0
  22. scitex/vis/editor/flask_editor/__init__.py +21 -0
  23. scitex/vis/editor/flask_editor/bbox.py +216 -0
  24. scitex/vis/editor/flask_editor/core.py +152 -0
  25. scitex/vis/editor/flask_editor/plotter.py +130 -0
  26. scitex/vis/editor/flask_editor/renderer.py +184 -0
  27. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  28. scitex/vis/editor/flask_editor/templates/html.py +295 -0
  29. scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
  30. scitex/vis/editor/flask_editor/templates/styles.py +549 -0
  31. scitex/vis/editor/flask_editor/utils.py +81 -0
  32. scitex/vis/io/__init__.py +84 -21
  33. scitex/vis/io/canvas.py +226 -0
  34. scitex/vis/io/data.py +204 -0
  35. scitex/vis/io/directory.py +202 -0
  36. scitex/vis/io/export.py +460 -0
  37. scitex/vis/io/panel.py +424 -0
  38. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
  39. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/RECORD +42 -21
  40. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  41. scitex/vis/editor/_web_editor.py +0 -1440
  42. scitex/vis/tmp.txt +0 -239
  43. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
  44. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
  45. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
scitex/__version__.py CHANGED
@@ -9,6 +9,6 @@ __FILE__ = "./src/scitex/__version__.py"
9
9
  __DIR__ = os.path.dirname(__FILE__)
10
10
  # ----------------------------------------
11
11
 
12
- __version__ = "2.4.3"
12
+ __version__ = "2.5.0"
13
13
 
14
14
  # EOF
scitex/io/_load.py CHANGED
@@ -44,6 +44,7 @@ from ._load_modules._txt import _load_txt
44
44
  from ._load_modules._xml import _load_xml
45
45
  from ._load_modules._yaml import _load_yaml
46
46
  from ._load_modules._zarr import _load_zarr
47
+ from ._load_modules._canvas import _load_canvas
47
48
 
48
49
 
49
50
  def load(
@@ -151,6 +152,10 @@ def load(
151
152
  if verbose:
152
153
  print(f"[DEBUG] After Path conversion: {lpath}")
153
154
 
155
+ # Handle .canvas directories (special case - directory not file)
156
+ if lpath.endswith(".canvas"):
157
+ return _load_canvas(lpath, verbose=verbose, **kwargs)
158
+
154
159
  # Check if it's a glob pattern
155
160
  if "*" in lpath or "?" in lpath or "[" in lpath:
156
161
  # Handle glob pattern
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_load_modules/_canvas.py
5
+ """
6
+ Load canvas directory (.canvas) for scitex.vis.
7
+
8
+ Canvas directories are portable figure bundles containing:
9
+ - canvas.json: Layout, panels, composition settings
10
+ - panels/: Panel directories (scitex or image type)
11
+ - exports/: Composed outputs (PNG, PDF, SVG)
12
+
13
+ Usage:
14
+ >>> import scitex as stx
15
+ >>> # Load canvas from directory
16
+ >>> canvas = stx.io.load("/path/to/fig1_results.canvas")
17
+ >>> # Access canvas properties
18
+ >>> print(canvas["canvas_name"])
19
+ >>> print(canvas["panels"])
20
+ """
21
+
22
+ import json
23
+ from pathlib import Path
24
+ from typing import Any, Dict, Union
25
+
26
+
27
+ def _load_canvas(
28
+ lpath: Union[str, Path],
29
+ verbose: bool = False,
30
+ load_panels: bool = False,
31
+ as_dict: bool = False,
32
+ **kwargs,
33
+ ) -> Any:
34
+ """
35
+ Load a canvas from a .canvas directory.
36
+
37
+ Parameters
38
+ ----------
39
+ lpath : str or Path
40
+ Path to the .canvas directory.
41
+ verbose : bool, optional
42
+ If True, print verbose output. Default is False.
43
+ load_panels : bool, optional
44
+ If True, also load panel images as numpy arrays.
45
+ If False (default), only load canvas.json metadata.
46
+ as_dict : bool, optional
47
+ If True, return raw dict instead of Canvas object.
48
+ Default is False (returns Canvas object).
49
+ **kwargs
50
+ Additional arguments (reserved for future use).
51
+
52
+ Returns
53
+ -------
54
+ Canvas or Dict[str, Any]
55
+ Canvas object (default) or dictionary if as_dict=True.
56
+ Contains:
57
+ - All fields from canvas.json
58
+ - '_canvas_dir': Path to the canvas directory
59
+ - If load_panels=True, panel images are loaded into memory
60
+
61
+ Raises
62
+ ------
63
+ FileNotFoundError
64
+ If the .canvas directory or canvas.json doesn't exist.
65
+ ValueError
66
+ If the path doesn't appear to be a valid canvas directory.
67
+ """
68
+ lpath = Path(lpath)
69
+
70
+ # Validate path
71
+ if not str(lpath).endswith(".canvas"):
72
+ raise ValueError(
73
+ f"Canvas path must end with .canvas extension: {lpath}"
74
+ )
75
+
76
+ if not lpath.exists():
77
+ raise FileNotFoundError(f"Canvas directory not found: {lpath}")
78
+
79
+ if not lpath.is_dir():
80
+ raise ValueError(
81
+ f"Canvas path must be a directory: {lpath}"
82
+ )
83
+
84
+ json_path = lpath / "canvas.json"
85
+ if not json_path.exists():
86
+ raise FileNotFoundError(
87
+ f"canvas.json not found in canvas directory: {lpath}"
88
+ )
89
+
90
+ # Load canvas.json
91
+ with open(json_path, "r") as f:
92
+ canvas_dict = json.load(f)
93
+
94
+ # Add reference to the canvas directory
95
+ canvas_dict["_canvas_dir"] = str(lpath)
96
+
97
+ if verbose:
98
+ print(f"Loaded canvas: {canvas_dict.get('canvas_name', 'unknown')}")
99
+ print(f" Schema version: {canvas_dict.get('schema_version', 'unknown')}")
100
+ print(f" Panels: {len(canvas_dict.get('panels', []))}")
101
+
102
+ # Optionally load panel images
103
+ if load_panels:
104
+ _load_panel_images(lpath, canvas_dict, verbose=verbose)
105
+
106
+ # Return Canvas object by default
107
+ if not as_dict:
108
+ try:
109
+ from scitex.vis.canvas import Canvas
110
+ canvas_obj = Canvas.from_dict(canvas_dict)
111
+ # Store reference to original directory for copying
112
+ canvas_obj._canvas_dir = str(lpath)
113
+ return canvas_obj
114
+ except ImportError:
115
+ # Fall back to dict if Canvas class unavailable
116
+ pass
117
+
118
+ return canvas_dict
119
+
120
+
121
+ def _load_panel_images(
122
+ canvas_dir: Path,
123
+ canvas_dict: Dict[str, Any],
124
+ verbose: bool = False,
125
+ ) -> None:
126
+ """
127
+ Load panel images into canvas_dict.
128
+
129
+ Modifies canvas_dict in place, adding '_image' key to each panel
130
+ containing the loaded numpy array.
131
+ """
132
+ try:
133
+ from PIL import Image
134
+ import numpy as np
135
+ except ImportError:
136
+ if verbose:
137
+ print("PIL/numpy not available, skipping panel image loading")
138
+ return
139
+
140
+ panels_dir = canvas_dir / "panels"
141
+
142
+ for panel in canvas_dict.get("panels", []):
143
+ panel_name = panel.get("name", "")
144
+ if not panel_name:
145
+ continue
146
+
147
+ panel_dir = panels_dir / panel_name
148
+ if not panel_dir.exists():
149
+ continue
150
+
151
+ # Try to find panel image
152
+ panel_type = panel.get("type", "image")
153
+ if panel_type == "scitex":
154
+ img_path = panel_dir / "panel.png"
155
+ else:
156
+ # For image type, use source filename
157
+ source = panel.get("source", "panel.png")
158
+ img_path = panel_dir / source
159
+
160
+ if img_path.exists():
161
+ try:
162
+ img = Image.open(img_path)
163
+ panel["_image"] = np.array(img)
164
+ if verbose:
165
+ print(f" Loaded panel image: {panel_name}")
166
+ except Exception as e:
167
+ if verbose:
168
+ print(f" Failed to load panel image {panel_name}: {e}")
169
+
170
+
171
+ # EOF
scitex/io/_save.py CHANGED
@@ -61,6 +61,7 @@ from ._save_modules import save_torch
61
61
  from ._save_modules import save_yaml
62
62
  from ._save_modules import save_zarr
63
63
  from ._save_modules._bibtex import save_bibtex
64
+ from ._save_modules._canvas import save_canvas
64
65
 
65
66
  logger = logging.getLogger()
66
67
 
@@ -510,6 +511,11 @@ def _save(
510
511
  # Get file extension
511
512
  ext = _os.path.splitext(spath)[1].lower()
512
513
 
514
+ # Handle .canvas directories (special case - path ends with .canvas)
515
+ if spath.endswith(".canvas"):
516
+ save_canvas(obj, spath, **kwargs)
517
+ return
518
+
513
519
  # Try dispatch dictionary first for O(1) lookup
514
520
  if ext in _FILE_HANDLERS:
515
521
  # Check if handler needs special parameters
@@ -1028,6 +1034,8 @@ def _handle_image_with_csv(
1028
1034
 
1029
1035
  # Dispatch dictionary for O(1) file format lookup
1030
1036
  _FILE_HANDLERS = {
1037
+ # Canvas directory format (scitex.vis)
1038
+ ".canvas": save_canvas,
1031
1039
  # Excel formats
1032
1040
  ".xlsx": save_excel,
1033
1041
  ".xls": save_excel,
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_save_modules/_canvas.py
5
+ """
6
+ Save canvas directory (.canvas) for scitex.vis.
7
+
8
+ Canvas directories are portable figure bundles containing:
9
+ - canvas.json: Layout, panels, composition settings
10
+ - panels/: Panel directories (scitex or image type)
11
+ - exports/: Composed outputs (PNG, PDF, SVG)
12
+
13
+ Usage:
14
+ >>> import scitex as stx
15
+ >>> # Create canvas object
16
+ >>> canvas = stx.vis.Canvas(name="fig1_results")
17
+ >>> canvas.add_panel("panel_a", "plot.png", ...)
18
+ >>> # Save canvas to directory
19
+ >>> stx.io.save(canvas, "/path/to/fig1_results.canvas")
20
+ >>>
21
+ >>> # Or save existing canvas directory to new location
22
+ >>> stx.io.save(canvas_json_dict, "/path/to/new_location.canvas")
23
+ """
24
+
25
+ import os
26
+ import shutil
27
+ from pathlib import Path
28
+ from typing import Any, Dict, Union
29
+
30
+
31
+ def save_canvas(
32
+ obj: Any,
33
+ spath: Union[str, Path],
34
+ **kwargs,
35
+ ) -> Path:
36
+ """
37
+ Save a canvas object or dictionary to a .canvas directory.
38
+
39
+ Parameters
40
+ ----------
41
+ obj : Any
42
+ Canvas object or dictionary containing canvas data.
43
+ Can be:
44
+ - Dict with canvas.json structure
45
+ - Canvas object with to_dict() method
46
+ - Path to existing .canvas directory (for copy/move)
47
+ spath : str or Path
48
+ Path where the .canvas directory should be created.
49
+ Must end with .canvas extension.
50
+ **kwargs
51
+ Additional arguments (reserved for future use).
52
+
53
+ Returns
54
+ -------
55
+ Path
56
+ Path to the created .canvas directory.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If path doesn't end with .canvas extension.
62
+ """
63
+ spath = Path(spath)
64
+
65
+ # Validate extension
66
+ if not str(spath).endswith(".canvas"):
67
+ raise ValueError(
68
+ f"Canvas path must end with .canvas extension: {spath}"
69
+ )
70
+
71
+ # Handle different object types
72
+ if isinstance(obj, (str, Path)):
73
+ # Source is an existing canvas directory - copy it
74
+ _copy_canvas_directory(Path(obj), spath)
75
+ elif isinstance(obj, dict):
76
+ # Object is a canvas JSON dictionary
77
+ _save_canvas_from_dict(obj, spath)
78
+ elif hasattr(obj, "to_dict"):
79
+ # Object has to_dict method (Canvas object)
80
+ canvas_dict = obj.to_dict()
81
+ # Check if this Canvas was loaded from disk (has _canvas_dir)
82
+ if hasattr(obj, "_canvas_dir") and obj._canvas_dir:
83
+ canvas_dict["_canvas_dir"] = obj._canvas_dir
84
+ # Pass bundle option
85
+ if "bundle" in kwargs:
86
+ canvas_dict["_bundle"] = kwargs.pop("bundle")
87
+ _save_canvas_from_dict(canvas_dict, spath)
88
+ elif hasattr(obj, "_canvas_json"):
89
+ # Object has internal canvas JSON (Canvas object variant)
90
+ _save_canvas_from_dict(obj._canvas_json, spath)
91
+ else:
92
+ raise TypeError(
93
+ f"Cannot save object of type {type(obj).__name__} as canvas. "
94
+ "Expected dict, Canvas object, or path to existing canvas."
95
+ )
96
+
97
+ # Export figures to exports/ directory
98
+ _export_canvas_figures(spath, **kwargs)
99
+
100
+ return spath
101
+
102
+
103
+ def _export_canvas_figures(
104
+ canvas_dir: Path,
105
+ formats: list = None,
106
+ dpi: int = 300,
107
+ **kwargs,
108
+ ) -> None:
109
+ """
110
+ Export canvas figures directly to canvas directory.
111
+
112
+ Automatically exports PNG, PDF, and SVG formats.
113
+ """
114
+ if formats is None:
115
+ formats = ["png", "pdf", "svg"]
116
+
117
+ try:
118
+ from scitex.vis.io.export import _compose_and_export
119
+ import json
120
+
121
+ # Load canvas.json
122
+ json_path = canvas_dir / "canvas.json"
123
+ if not json_path.exists():
124
+ return
125
+
126
+ with open(json_path, "r") as f:
127
+ canvas_json = json.load(f)
128
+
129
+ # Export directly to canvas directory (no exports/ subdirectory)
130
+ for fmt in formats:
131
+ output_path = canvas_dir / f"canvas.{fmt}"
132
+ _compose_and_export(
133
+ canvas_dir=canvas_dir,
134
+ canvas_json=canvas_json,
135
+ output_path=output_path,
136
+ output_format=fmt,
137
+ dpi=dpi,
138
+ transparent=False,
139
+ )
140
+ except ImportError:
141
+ # scitex.vis not available
142
+ pass
143
+ except Exception as e:
144
+ # Log but don't fail save if export fails
145
+ import sys
146
+ print(f"Warning: Canvas export failed: {e}", file=sys.stderr)
147
+
148
+
149
+ def _copy_canvas_directory(source: Path, dest: Path) -> None:
150
+ """Copy an existing canvas directory to a new location."""
151
+ if not source.exists():
152
+ raise FileNotFoundError(f"Source canvas directory not found: {source}")
153
+
154
+ if not (source / "canvas.json").exists():
155
+ raise ValueError(
156
+ f"Invalid canvas directory (missing canvas.json): {source}"
157
+ )
158
+
159
+ # Remove destination if exists
160
+ if dest.exists():
161
+ shutil.rmtree(dest)
162
+
163
+ # Copy entire directory tree
164
+ shutil.copytree(source, dest)
165
+
166
+
167
+ def _save_canvas_from_dict(canvas_dict: Dict[str, Any], dest: Path) -> None:
168
+ """Create a canvas directory from a dictionary."""
169
+ import json
170
+
171
+ # Create directory structure (no exports/ - files go directly in canvas dir)
172
+ dest.mkdir(parents=True, exist_ok=True)
173
+ (dest / "panels").mkdir(exist_ok=True)
174
+
175
+ # Check if this dict was loaded from an existing canvas (has _canvas_dir)
176
+ source_canvas_dir = canvas_dict.get("_canvas_dir")
177
+
178
+ # Check if this dict has source files (from Canvas object)
179
+ source_files = canvas_dict.get("_source_files", {})
180
+
181
+ # Get bundle option
182
+ bundle = canvas_dict.get("_bundle", False)
183
+
184
+ # Create a clean copy of canvas_dict without internal keys for saving
185
+ save_dict = {k: v for k, v in canvas_dict.items() if not k.startswith("_")}
186
+
187
+ # Save canvas.json
188
+ json_path = dest / "canvas.json"
189
+ with open(json_path, "w") as f:
190
+ json.dump(save_dict, f, indent=2, default=str)
191
+
192
+ # If canvas_dict was loaded from an existing canvas, copy panel files
193
+ if source_canvas_dir:
194
+ _copy_panels_from_source(Path(source_canvas_dir), dest, canvas_dict)
195
+
196
+ # If canvas_dict has source files (from Canvas object), create panel dirs
197
+ if source_files:
198
+ _create_panels_from_source_files(source_files, dest, canvas_dict, bundle=bundle)
199
+
200
+ # If canvas_dict contains embedded panel data, extract it
201
+ _extract_embedded_panels(canvas_dict, dest)
202
+
203
+
204
+ def _copy_panels_from_source(
205
+ source_canvas_dir: Path,
206
+ dest: Path,
207
+ canvas_dict: Dict[str, Any],
208
+ ) -> None:
209
+ """
210
+ Copy panel files from source canvas directory to destination.
211
+
212
+ When a canvas dict was loaded from an existing canvas directory,
213
+ this function copies the panel files to the new location.
214
+ Skips copying if source and destination are the same.
215
+ """
216
+ source_panels_dir = source_canvas_dir / "panels"
217
+ dest_panels_dir = dest / "panels"
218
+
219
+ if not source_panels_dir.exists():
220
+ return
221
+
222
+ # Skip if source and dest are the same (saving back to same location)
223
+ try:
224
+ if source_canvas_dir.resolve() == dest.resolve():
225
+ return
226
+ except (OSError, ValueError):
227
+ pass
228
+
229
+ for panel in canvas_dict.get("panels", []):
230
+ panel_name = panel.get("name", "")
231
+ if not panel_name:
232
+ continue
233
+
234
+ source_panel_dir = source_panels_dir / panel_name
235
+ dest_panel_dir = dest_panels_dir / panel_name
236
+
237
+ if source_panel_dir.exists() and source_panel_dir.is_dir():
238
+ # Copy entire panel directory (follow symlinks to get actual content)
239
+ if dest_panel_dir.exists():
240
+ shutil.rmtree(dest_panel_dir)
241
+ shutil.copytree(source_panel_dir, dest_panel_dir, symlinks=False)
242
+
243
+
244
+ def _create_panels_from_source_files(
245
+ source_files: Dict[str, str],
246
+ dest: Path,
247
+ canvas_dict: Dict[str, Any],
248
+ bundle: bool = False,
249
+ ) -> None:
250
+ """
251
+ Create panel directories from source files.
252
+
253
+ When a Canvas object is saved, this creates the panel directories
254
+ with symlinks (default) or copies of the source files.
255
+
256
+ Parameters
257
+ ----------
258
+ source_files : Dict[str, str]
259
+ Mapping of panel_name -> source_file_path
260
+ dest : Path
261
+ Destination canvas directory
262
+ canvas_dict : Dict[str, Any]
263
+ Canvas dictionary (to get panel types)
264
+ bundle : bool
265
+ If True, copy files. If False (default), create symlinks.
266
+ """
267
+ dest_panels_dir = dest / "panels"
268
+
269
+ for panel in canvas_dict.get("panels", []):
270
+ panel_name = panel.get("name", "")
271
+ if not panel_name or panel_name not in source_files:
272
+ continue
273
+
274
+ source_path = Path(source_files[panel_name])
275
+ if not source_path.exists():
276
+ continue
277
+
278
+ panel_type = panel.get("type", "image")
279
+ panel_dir = dest_panels_dir / panel_name
280
+ panel_dir.mkdir(parents=True, exist_ok=True)
281
+
282
+ # Symlink or copy panel files
283
+ if panel_type == "scitex":
284
+ # Scitex panel: PNG, JSON, CSV
285
+ _link_or_copy(source_path, panel_dir / "panel.png", bundle)
286
+ json_sibling = source_path.parent / f"{source_path.stem}.json"
287
+ if json_sibling.exists():
288
+ _link_or_copy(json_sibling, panel_dir / "panel.json", bundle)
289
+ csv_sibling = source_path.parent / f"{source_path.stem}.csv"
290
+ if csv_sibling.exists():
291
+ _link_or_copy(csv_sibling, panel_dir / "panel.csv", bundle)
292
+ else:
293
+ # Image panel: just the image
294
+ dest_name = f"panel{source_path.suffix}"
295
+ _link_or_copy(source_path, panel_dir / dest_name, bundle)
296
+
297
+
298
+ def _link_or_copy(source: Path, dest: Path, bundle: bool = False) -> None:
299
+ """Create relative symlink or copy file based on bundle flag."""
300
+ if dest.exists() or dest.is_symlink():
301
+ dest.unlink()
302
+
303
+ if bundle:
304
+ shutil.copy2(source, dest)
305
+ else:
306
+ try:
307
+ # Use relative symlink for portability
308
+ import os
309
+ rel_path = os.path.relpath(source.resolve(), dest.parent.resolve())
310
+ dest.symlink_to(rel_path)
311
+ except (OSError, ValueError):
312
+ # Fallback to copy if symlink fails
313
+ shutil.copy2(source, dest)
314
+
315
+
316
+ def _extract_embedded_panels(canvas_dict: Dict[str, Any], dest: Path) -> None:
317
+ """
318
+ Extract embedded panel data from canvas dictionary.
319
+
320
+ Some Canvas objects may embed panel image data (base64) in the dict.
321
+ This function extracts them to the panels/ directory.
322
+ """
323
+ import base64
324
+
325
+ for panel in canvas_dict.get("panels", []):
326
+ panel_name = panel.get("name", "")
327
+ if not panel_name:
328
+ continue
329
+
330
+ panel_dir = dest / "panels" / panel_name
331
+ panel_dir.mkdir(parents=True, exist_ok=True)
332
+
333
+ # Check for embedded image data
334
+ if "image_data" in panel:
335
+ # Decode base64 image data
336
+ img_data = base64.b64decode(panel["image_data"])
337
+ img_ext = panel.get("image_ext", "png")
338
+ img_path = panel_dir / f"panel.{img_ext}"
339
+ with open(img_path, "wb") as f:
340
+ f.write(img_data)
341
+
342
+ # Check for embedded JSON data (scitex type panels)
343
+ if "panel_json" in panel:
344
+ import json
345
+ json_path = panel_dir / "panel.json"
346
+ with open(json_path, "w") as f:
347
+ json.dump(panel["panel_json"], f, indent=2, default=str)
348
+
349
+ # Check for embedded CSV data (scitex type panels)
350
+ if "panel_csv" in panel:
351
+ csv_path = panel_dir / "panel.csv"
352
+ with open(csv_path, "w") as f:
353
+ f.write(panel["panel_csv"])
354
+
355
+
356
+ # EOF