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.
- scitex/__version__.py +1 -1
- scitex/io/_load.py +5 -0
- scitex/io/_load_modules/_canvas.py +171 -0
- scitex/io/_save.py +8 -0
- scitex/io/_save_modules/_canvas.py +356 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
- scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
- scitex/plt/utils/__init__.py +10 -0
- scitex/plt/utils/_collect_figure_metadata.py +14 -12
- scitex/plt/utils/_csv_column_naming.py +237 -0
- scitex/session/_decorator.py +13 -1
- scitex/vis/README.md +246 -615
- scitex/vis/__init__.py +138 -78
- scitex/vis/canvas.py +423 -0
- scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
- scitex/vis/editor/__init__.py +1 -1
- scitex/vis/editor/_dearpygui_editor.py +1830 -0
- scitex/vis/editor/_defaults.py +40 -1
- scitex/vis/editor/_edit.py +54 -18
- scitex/vis/editor/_flask_editor.py +37 -0
- scitex/vis/editor/_qt_editor.py +865 -0
- scitex/vis/editor/flask_editor/__init__.py +21 -0
- scitex/vis/editor/flask_editor/bbox.py +216 -0
- scitex/vis/editor/flask_editor/core.py +152 -0
- scitex/vis/editor/flask_editor/plotter.py +130 -0
- scitex/vis/editor/flask_editor/renderer.py +184 -0
- scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
- scitex/vis/editor/flask_editor/templates/html.py +295 -0
- scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
- scitex/vis/editor/flask_editor/templates/styles.py +549 -0
- scitex/vis/editor/flask_editor/utils.py +81 -0
- scitex/vis/io/__init__.py +84 -21
- scitex/vis/io/canvas.py +226 -0
- scitex/vis/io/data.py +204 -0
- scitex/vis/io/directory.py +202 -0
- scitex/vis/io/export.py +460 -0
- scitex/vis/io/panel.py +424 -0
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/RECORD +42 -21
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
scitex/__version__.py
CHANGED
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
|