scitex 2.7.0__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/__init__.py +6 -2
- scitex/__version__.py +1 -1
- scitex/audio/README.md +52 -0
- scitex/audio/__init__.py +384 -0
- scitex/audio/__main__.py +129 -0
- scitex/audio/_tts.py +334 -0
- scitex/audio/engines/__init__.py +44 -0
- scitex/audio/engines/base.py +275 -0
- scitex/audio/engines/elevenlabs_engine.py +143 -0
- scitex/audio/engines/gtts_engine.py +162 -0
- scitex/audio/engines/pyttsx3_engine.py +131 -0
- scitex/audio/mcp_server.py +757 -0
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_vis.py +1 -1
- scitex/dev/plt/__init__.py +272 -0
- scitex/dev/plt/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/plot_mpl_bar.py +29 -0
- scitex/dev/plt/plot_mpl_barh.py +29 -0
- scitex/dev/plt/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/plot_mpl_contour.py +31 -0
- scitex/dev/plt/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/plot_mpl_fill.py +30 -0
- scitex/dev/plt/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/plot_mpl_hist.py +28 -0
- scitex/dev/plt/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/plot_mpl_pie.py +29 -0
- scitex/dev/plt/plot_mpl_plot.py +29 -0
- scitex/dev/plt/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/plot_mpl_stem.py +29 -0
- scitex/dev/plt/plot_mpl_step.py +29 -0
- scitex/dev/plt/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/plot_sns_barplot.py +29 -0
- scitex/dev/plt/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/plot_sns_histplot.py +29 -0
- scitex/dev/plt/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/plot_stx_bar.py +29 -0
- scitex/dev/plt/plot_stx_barh.py +29 -0
- scitex/dev/plt/plot_stx_box.py +28 -0
- scitex/dev/plt/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/plot_stx_contour.py +31 -0
- scitex/dev/plt/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/plot_stx_fillv.py +28 -0
- scitex/dev/plt/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/plot_stx_image.py +28 -0
- scitex/dev/plt/plot_stx_imshow.py +28 -0
- scitex/dev/plt/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/plot_stx_kde.py +28 -0
- scitex/dev/plt/plot_stx_line.py +28 -0
- scitex/dev/plt/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/plot_stx_raster.py +28 -0
- scitex/dev/plt/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/plot_stx_scatter.py +29 -0
- scitex/dev/plt/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/plot_stx_violin.py +28 -0
- scitex/dev/plt/plot_stx_violinplot.py +28 -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/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/__init__.py +5 -2
- scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
- scitex/{vis → fig}/editor/_qt_editor.py +182 -2
- scitex/{vis → 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 +1299 -0
- scitex/fig/editor/flask_editor/_core.py +1429 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +813 -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 +123 -0
- scitex/fig/editor/flask_editor/templates/_html.py +852 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
- scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +1058 -0
- scitex/{vis → fig}/io/_canvas.py +1 -1
- scitex/{vis → fig}/io/_data.py +1 -1
- scitex/{vis → fig}/io/_export.py +1 -1
- scitex/{vis → fig}/io/_load.py +1 -1
- scitex/{vis → fig}/io/_panel.py +1 -1
- scitex/{vis → fig}/io/_save.py +1 -1
- scitex/{vis → fig}/model/__init__.py +1 -1
- scitex/{vis → fig}/model/_annotations.py +1 -1
- scitex/{vis → fig}/model/_axes.py +1 -1
- scitex/{vis → fig}/model/_figure.py +1 -1
- scitex/{vis → fig}/model/_guides.py +1 -1
- scitex/{vis → fig}/model/_plot.py +1 -1
- scitex/{vis → fig}/model/_styles.py +1 -1
- scitex/{vis → fig}/utils/__init__.py +1 -1
- scitex/io/__init__.py +22 -26
- scitex/io/_bundle.py +493 -0
- scitex/io/_flush.py +5 -2
- scitex/io/_load.py +98 -0
- scitex/io/_load_modules/_H5Explorer.py +5 -2
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_image.py +3 -4
- scitex/io/_load_modules/_txt.py +4 -2
- scitex/io/_metadata.py +34 -324
- scitex/io/_metadata_modules/__init__.py +46 -0
- scitex/io/_metadata_modules/_embed.py +70 -0
- scitex/io/_metadata_modules/_read.py +64 -0
- scitex/io/_metadata_modules/_utils.py +79 -0
- scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
- scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
- scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
- scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
- scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
- scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
- scitex/io/_metadata_modules/read_metadata_png.py +39 -0
- scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
- scitex/io/_qr_utils.py +5 -3
- scitex/io/_save.py +548 -30
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_image.py +5 -9
- scitex/io/_save_modules/_tex.py +7 -4
- scitex/io/_zip_bundle.py +439 -0
- scitex/io/utils/h5_to_zarr.py +11 -9
- scitex/msword/__init__.py +255 -0
- scitex/msword/profiles.py +357 -0
- scitex/msword/reader.py +753 -0
- scitex/msword/utils.py +289 -0
- scitex/msword/writer.py +362 -0
- scitex/plt/__init__.py +5 -2
- scitex/plt/_subplots/_AxesWrapper.py +6 -6
- scitex/plt/_subplots/_AxisWrapper.py +15 -9
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
- scitex/plt/_subplots/_FigWrapper.py +6 -6
- scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
- scitex/plt/_subplots/_export_as_csv.py +35 -5
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
- scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
- scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
- scitex/plt/gallery/_generate.py +421 -14
- scitex/plt/io/__init__.py +53 -0
- scitex/plt/io/_bundle.py +490 -0
- scitex/plt/io/_layered_bundle.py +1343 -0
- scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
- scitex/plt/styles/__init__.py +14 -0
- scitex/plt/styles/presets.py +78 -0
- scitex/plt/utils/__init__.py +13 -1
- scitex/plt/utils/_collect_figure_metadata.py +10 -14
- scitex/plt/utils/_configure_mpl.py +6 -18
- scitex/plt/utils/_crop.py +32 -14
- scitex/plt/utils/_csv_column_naming.py +54 -0
- scitex/plt/utils/_figure_mm.py +116 -1
- scitex/plt/utils/_hitmap.py +1643 -0
- scitex/plt/utils/metadata/__init__.py +25 -0
- scitex/plt/utils/metadata/_core.py +9 -10
- scitex/plt/utils/metadata/_dimensions.py +6 -3
- scitex/plt/utils/metadata/_editable_export.py +405 -0
- scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
- scitex/schema/__init__.py +109 -16
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_plot.py +1015 -0
- scitex/schema/_stats.py +2 -2
- scitex/stats/__init__.py +117 -0
- scitex/stats/io/__init__.py +29 -0
- scitex/stats/io/_bundle.py +156 -0
- scitex/tex/__init__.py +4 -0
- scitex/tex/_export.py +890 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
- scitex/io/memo.md +0 -2827
- scitex/plt/REQUESTS.md +0 -191
- scitex/plt/_subplots/TODO.md +0 -53
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
- scitex/vis/__init__.py +0 -177
- scitex/vis/editor/_edit.py +0 -390
- scitex/vis/editor/flask_editor/_bbox.py +0 -529
- scitex/vis/editor/flask_editor/_core.py +0 -168
- scitex/vis/editor/flask_editor/_renderer.py +0 -393
- scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
- scitex/vis/editor/flask_editor/templates/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
- /scitex/{vis → fig}/README.md +0 -0
- /scitex/{vis → fig}/backend/__init__.py +0 -0
- /scitex/{vis → fig}/backend/_export.py +0 -0
- /scitex/{vis → fig}/backend/_render.py +0 -0
- /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
- /scitex/{vis → fig}/io/_directory.py +0 -0
- /scitex/{vis → fig}/model/_plot_types.py +0 -0
- /scitex/{vis → fig}/utils/_defaults.py +0 -0
- /scitex/{vis → fig}/utils/_validate.py +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
scitex/fig/io/_bundle.py
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-14 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fig/io/_bundle.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
SciTeX .figz Bundle I/O - Figure-specific bundle operations.
|
|
8
|
+
|
|
9
|
+
Handles:
|
|
10
|
+
- Figure specification validation
|
|
11
|
+
- Panel composition and layout
|
|
12
|
+
- Nested .pltz bundle management
|
|
13
|
+
- Export file handling
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import shutil
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"validate_figz_spec",
|
|
23
|
+
"load_figz_bundle",
|
|
24
|
+
"save_figz_bundle",
|
|
25
|
+
"FIGZ_SCHEMA_SPEC",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Schema specification for .figz bundles
|
|
29
|
+
FIGZ_SCHEMA_SPEC = {
|
|
30
|
+
"name": "scitex.fig.figure",
|
|
31
|
+
"version": "1.0.0",
|
|
32
|
+
"required_fields": ["schema"],
|
|
33
|
+
"optional_fields": ["figure", "panels", "notations"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_figz_spec(spec: Dict[str, Any]) -> List[str]:
|
|
38
|
+
"""Validate .figz-specific fields.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
spec: The specification dictionary to validate.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of validation error messages (empty if valid).
|
|
45
|
+
"""
|
|
46
|
+
errors = []
|
|
47
|
+
|
|
48
|
+
if "panels" in spec:
|
|
49
|
+
panels = spec["panels"]
|
|
50
|
+
if not isinstance(panels, list):
|
|
51
|
+
errors.append("'panels' must be a list")
|
|
52
|
+
else:
|
|
53
|
+
for i, panel in enumerate(panels):
|
|
54
|
+
if not isinstance(panel, dict):
|
|
55
|
+
errors.append(f"panels[{i}] must be a dictionary")
|
|
56
|
+
continue
|
|
57
|
+
if "id" not in panel:
|
|
58
|
+
errors.append(f"panels[{i}].id is required")
|
|
59
|
+
|
|
60
|
+
if "figure" in spec:
|
|
61
|
+
figure = spec["figure"]
|
|
62
|
+
if not isinstance(figure, dict):
|
|
63
|
+
errors.append("'figure' must be a dictionary")
|
|
64
|
+
|
|
65
|
+
return errors
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_figz_bundle(bundle_dir: Path) -> Dict[str, Any]:
|
|
69
|
+
"""Load .figz bundle contents from directory.
|
|
70
|
+
|
|
71
|
+
Supports both:
|
|
72
|
+
- New format: spec.json + style.json (separate semantic/appearance)
|
|
73
|
+
- Legacy format: {basename}.json (embedded styles)
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
bundle_dir: Path to the bundle directory.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with loaded bundle contents:
|
|
80
|
+
- spec: Figure specification (semantic)
|
|
81
|
+
- style: Figure style (appearance)
|
|
82
|
+
- plots: Dict of nested pltz bundles
|
|
83
|
+
- basename: Base filename
|
|
84
|
+
"""
|
|
85
|
+
result = {}
|
|
86
|
+
bundle_dir = Path(bundle_dir)
|
|
87
|
+
|
|
88
|
+
# Determine basename from directory name
|
|
89
|
+
basename = bundle_dir.stem.replace(".figz", "")
|
|
90
|
+
result["basename"] = basename
|
|
91
|
+
|
|
92
|
+
# Try to load spec.json (new format) first
|
|
93
|
+
spec_file = bundle_dir / "spec.json"
|
|
94
|
+
if spec_file.exists():
|
|
95
|
+
with open(spec_file, "r") as f:
|
|
96
|
+
result["spec"] = json.load(f)
|
|
97
|
+
else:
|
|
98
|
+
# Fallback to {basename}.json (legacy format)
|
|
99
|
+
legacy_file = bundle_dir / f"{basename}.json"
|
|
100
|
+
if legacy_file.exists():
|
|
101
|
+
with open(legacy_file, "r") as f:
|
|
102
|
+
result["spec"] = json.load(f)
|
|
103
|
+
else:
|
|
104
|
+
# Try any .json file
|
|
105
|
+
for f in bundle_dir.glob("*.json"):
|
|
106
|
+
if not f.name.startswith('.') and f.name != "style.json":
|
|
107
|
+
with open(f, "r") as fp:
|
|
108
|
+
result["spec"] = json.load(fp)
|
|
109
|
+
break
|
|
110
|
+
else:
|
|
111
|
+
result["spec"] = None
|
|
112
|
+
|
|
113
|
+
# Load style.json if exists
|
|
114
|
+
style_file = bundle_dir / "style.json"
|
|
115
|
+
if style_file.exists():
|
|
116
|
+
with open(style_file, "r") as f:
|
|
117
|
+
result["style"] = json.load(f)
|
|
118
|
+
else:
|
|
119
|
+
# Extract from embedded styles in spec (legacy)
|
|
120
|
+
if result.get("spec"):
|
|
121
|
+
figure = result["spec"].get("figure", {})
|
|
122
|
+
if "styles" in figure:
|
|
123
|
+
result["style"] = figure["styles"]
|
|
124
|
+
else:
|
|
125
|
+
result["style"] = {}
|
|
126
|
+
else:
|
|
127
|
+
result["style"] = {}
|
|
128
|
+
|
|
129
|
+
# Load nested .pltz bundles
|
|
130
|
+
result["plots"] = {}
|
|
131
|
+
|
|
132
|
+
# Load from .pltz.d directories
|
|
133
|
+
for pltz_dir in bundle_dir.glob("*.pltz.d"):
|
|
134
|
+
plot_name = pltz_dir.stem.replace(".pltz", "")
|
|
135
|
+
from scitex.io._bundle import load_bundle
|
|
136
|
+
result["plots"][plot_name] = load_bundle(pltz_dir)
|
|
137
|
+
|
|
138
|
+
# Load from .pltz ZIP files
|
|
139
|
+
for pltz_zip in bundle_dir.glob("*.pltz"):
|
|
140
|
+
if pltz_zip.is_file():
|
|
141
|
+
plot_name = pltz_zip.stem
|
|
142
|
+
from scitex.io._bundle import load_bundle
|
|
143
|
+
result["plots"][plot_name] = load_bundle(pltz_zip)
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def save_figz_bundle(data: Dict[str, Any], dir_path: Path) -> None:
|
|
149
|
+
"""Save .figz bundle contents to directory.
|
|
150
|
+
|
|
151
|
+
Structure:
|
|
152
|
+
figure.figz.d/
|
|
153
|
+
spec.json # Figure-level specification
|
|
154
|
+
style.json # Figure-level style (optional)
|
|
155
|
+
exports/ # Figure-level exports
|
|
156
|
+
figure.png
|
|
157
|
+
figure.svg
|
|
158
|
+
figure_hitmap.png
|
|
159
|
+
figure_overview.png
|
|
160
|
+
cache/ # Figure-level cache
|
|
161
|
+
geometry_px.json # Combined geometry for all panels
|
|
162
|
+
render_manifest.json
|
|
163
|
+
panels/ # Nested panel bundles (or *.pltz.d at root)
|
|
164
|
+
A.pltz.d/
|
|
165
|
+
B.pltz.d/
|
|
166
|
+
README.md
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
data: Bundle data dictionary.
|
|
170
|
+
dir_path: Path to the bundle directory.
|
|
171
|
+
"""
|
|
172
|
+
import logging
|
|
173
|
+
logger = logging.getLogger("scitex")
|
|
174
|
+
|
|
175
|
+
# Get basename from directory name (e.g., "Figure1" from "Figure1.figz.d")
|
|
176
|
+
basename = dir_path.stem.replace(".figz", "")
|
|
177
|
+
|
|
178
|
+
# Create directories
|
|
179
|
+
exports_dir = dir_path / "exports"
|
|
180
|
+
cache_dir = dir_path / "cache"
|
|
181
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
# Split spec into spec.json (semantic) and style.json (appearance)
|
|
185
|
+
spec = data.get("spec", {})
|
|
186
|
+
style = data.get("style", {})
|
|
187
|
+
|
|
188
|
+
# Extract style from spec.figure.styles if not provided separately
|
|
189
|
+
figure_data = spec.get("figure", {})
|
|
190
|
+
if not style and "styles" in figure_data:
|
|
191
|
+
style = figure_data.get("styles", {})
|
|
192
|
+
|
|
193
|
+
# Build clean spec (semantic data only)
|
|
194
|
+
clean_spec = {
|
|
195
|
+
"schema": spec.get("schema", {"name": "scitex.fig.figure", "version": "1.0.0"}),
|
|
196
|
+
"figure": {
|
|
197
|
+
"id": figure_data.get("id", "figure"),
|
|
198
|
+
"title": figure_data.get("title", ""),
|
|
199
|
+
"caption": figure_data.get("caption", ""),
|
|
200
|
+
},
|
|
201
|
+
"panels": spec.get("panels", []),
|
|
202
|
+
}
|
|
203
|
+
if "notations" in spec:
|
|
204
|
+
clean_spec["notations"] = spec["notations"]
|
|
205
|
+
|
|
206
|
+
# Build style (appearance data)
|
|
207
|
+
figz_style = {
|
|
208
|
+
"schema": {"name": "scitex.fig.style", "version": "1.0.0"},
|
|
209
|
+
"size": style.get("size", {"width_mm": 180, "height_mm": 120}),
|
|
210
|
+
"background": style.get("background", "#ffffff"),
|
|
211
|
+
"theme": style.get("theme", {"mode": "light"}),
|
|
212
|
+
"panel_labels": style.get("panel_labels", {
|
|
213
|
+
"visible": True,
|
|
214
|
+
"fontsize": 12,
|
|
215
|
+
"fontweight": "bold",
|
|
216
|
+
"position": "top-left",
|
|
217
|
+
}),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Save spec.json (semantic)
|
|
221
|
+
spec_file = dir_path / "spec.json"
|
|
222
|
+
with open(spec_file, "w") as f:
|
|
223
|
+
json.dump(clean_spec, f, indent=2)
|
|
224
|
+
|
|
225
|
+
# Save style.json (appearance)
|
|
226
|
+
style_file = dir_path / "style.json"
|
|
227
|
+
with open(style_file, "w") as f:
|
|
228
|
+
json.dump(figz_style, f, indent=2)
|
|
229
|
+
|
|
230
|
+
# Also save as {basename}.json for backward compatibility (full spec with embedded style)
|
|
231
|
+
compat_spec = dict(clean_spec)
|
|
232
|
+
compat_spec["figure"]["styles"] = {
|
|
233
|
+
"size": figz_style["size"],
|
|
234
|
+
"background": figz_style["background"],
|
|
235
|
+
}
|
|
236
|
+
compat_spec_file = dir_path / f"{basename}.json"
|
|
237
|
+
with open(compat_spec_file, "w") as f:
|
|
238
|
+
json.dump(compat_spec, f, indent=2)
|
|
239
|
+
|
|
240
|
+
# Save exports to exports/ directory
|
|
241
|
+
_save_figz_exports(data, exports_dir, spec, basename)
|
|
242
|
+
|
|
243
|
+
# Copy nested .pltz bundles directly (preserving all files)
|
|
244
|
+
if "plots" in data:
|
|
245
|
+
_copy_nested_pltz_bundles(data["plots"], dir_path)
|
|
246
|
+
|
|
247
|
+
# Generate composed figure in exports/ (Figure1.png, Figure1.svg)
|
|
248
|
+
try:
|
|
249
|
+
_generate_composed_figure(dir_path, spec, basename)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.debug(f"Could not generate composed figure: {e}")
|
|
252
|
+
|
|
253
|
+
# Generate figz overview in exports/
|
|
254
|
+
try:
|
|
255
|
+
_generate_figz_overview(dir_path, spec, data, basename)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.debug(f"Could not generate figz overview: {e}")
|
|
258
|
+
|
|
259
|
+
# Generate figure-level geometry cache
|
|
260
|
+
try:
|
|
261
|
+
_generate_figz_geometry_cache(dir_path, spec, basename)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.debug(f"Could not generate figz geometry cache: {e}")
|
|
264
|
+
|
|
265
|
+
# Generate README.md
|
|
266
|
+
try:
|
|
267
|
+
_generate_figz_readme(dir_path, spec, data, basename)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.debug(f"Could not generate figz README: {e}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _save_figz_exports(data: Dict[str, Any], exports_dir: Path, spec: Dict, basename: str) -> None:
|
|
273
|
+
"""Save figure-level export files to exports/ directory.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
data: Bundle data containing PNG/SVG/PDF bytes or paths.
|
|
277
|
+
exports_dir: Path to exports/ directory.
|
|
278
|
+
spec: Figure specification.
|
|
279
|
+
basename: Base filename for exports.
|
|
280
|
+
"""
|
|
281
|
+
for fmt in ["png", "svg", "pdf"]:
|
|
282
|
+
if fmt not in data:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
out_file = exports_dir / f"{basename}.{fmt}"
|
|
286
|
+
export_data = data[fmt]
|
|
287
|
+
|
|
288
|
+
if isinstance(export_data, bytes):
|
|
289
|
+
with open(out_file, "wb") as f:
|
|
290
|
+
f.write(export_data)
|
|
291
|
+
elif isinstance(export_data, (str, Path)) and Path(export_data).exists():
|
|
292
|
+
shutil.copy(export_data, out_file)
|
|
293
|
+
|
|
294
|
+
# Embed metadata into PNG and PDF files
|
|
295
|
+
if out_file.exists() and spec:
|
|
296
|
+
try:
|
|
297
|
+
_embed_metadata_in_export(out_file, spec, fmt)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
import logging
|
|
300
|
+
logging.getLogger("scitex").debug(
|
|
301
|
+
f"Could not embed metadata in {out_file}: {e}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _save_exports(data: Dict[str, Any], dir_path: Path, spec: Dict, basename: str = "figure") -> None:
|
|
306
|
+
"""Save export files (PNG, SVG, PDF) with embedded metadata. (Legacy - root level)"""
|
|
307
|
+
for fmt in ["png", "svg", "pdf"]:
|
|
308
|
+
if fmt not in data:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
out_file = dir_path / f"{basename}.{fmt}"
|
|
312
|
+
export_data = data[fmt]
|
|
313
|
+
|
|
314
|
+
if isinstance(export_data, bytes):
|
|
315
|
+
with open(out_file, "wb") as f:
|
|
316
|
+
f.write(export_data)
|
|
317
|
+
elif isinstance(export_data, (str, Path)) and Path(export_data).exists():
|
|
318
|
+
shutil.copy(export_data, out_file)
|
|
319
|
+
|
|
320
|
+
# Embed metadata into PNG and PDF files
|
|
321
|
+
if out_file.exists() and spec:
|
|
322
|
+
try:
|
|
323
|
+
_embed_metadata_in_export(out_file, spec, fmt)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
import logging
|
|
326
|
+
logging.getLogger("scitex").debug(
|
|
327
|
+
f"Could not embed metadata in {out_file}: {e}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _copy_nested_pltz_bundles(plots: Dict[str, Any], dir_path: Path) -> None:
|
|
332
|
+
"""Copy nested .pltz bundles directly, preserving all files.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
plots: Dict mapping panel IDs to either:
|
|
336
|
+
- source_path: Path to existing .pltz.d directory or .pltz zip
|
|
337
|
+
- bundle_data: Dict with spec/data (will use save_bundle)
|
|
338
|
+
dir_path: Target figz directory.
|
|
339
|
+
"""
|
|
340
|
+
for panel_id, plot_source in plots.items():
|
|
341
|
+
if isinstance(plot_source, (str, Path)):
|
|
342
|
+
# Direct copy from source path
|
|
343
|
+
source_path = Path(plot_source)
|
|
344
|
+
|
|
345
|
+
if source_path.is_dir() and str(source_path).endswith('.pltz.d'):
|
|
346
|
+
# Source is .pltz.d directory - copy as directory
|
|
347
|
+
target_path = dir_path / f"{panel_id}.pltz.d"
|
|
348
|
+
if target_path.exists():
|
|
349
|
+
shutil.rmtree(target_path)
|
|
350
|
+
shutil.copytree(source_path, target_path)
|
|
351
|
+
|
|
352
|
+
elif source_path.is_file() and str(source_path).endswith('.pltz'):
|
|
353
|
+
# Source is .pltz zip file - copy as zip file (preserving zip format)
|
|
354
|
+
target_path = dir_path / f"{panel_id}.pltz"
|
|
355
|
+
if target_path.exists():
|
|
356
|
+
target_path.unlink()
|
|
357
|
+
shutil.copy2(source_path, target_path)
|
|
358
|
+
|
|
359
|
+
elif source_path.exists():
|
|
360
|
+
# Unknown format - try to copy as directory
|
|
361
|
+
target_path = dir_path / f"{panel_id}.pltz.d"
|
|
362
|
+
if source_path.is_dir():
|
|
363
|
+
if target_path.exists():
|
|
364
|
+
shutil.rmtree(target_path)
|
|
365
|
+
shutil.copytree(source_path, target_path)
|
|
366
|
+
|
|
367
|
+
elif isinstance(plot_source, dict):
|
|
368
|
+
# Check if it has source_path for direct copy
|
|
369
|
+
if "source_path" in plot_source:
|
|
370
|
+
source_path = Path(plot_source["source_path"])
|
|
371
|
+
if source_path.is_file() and str(source_path).endswith('.pltz'):
|
|
372
|
+
# .pltz zip file
|
|
373
|
+
target_path = dir_path / f"{panel_id}.pltz"
|
|
374
|
+
if target_path.exists():
|
|
375
|
+
target_path.unlink()
|
|
376
|
+
shutil.copy2(source_path, target_path)
|
|
377
|
+
elif source_path.exists() and source_path.is_dir():
|
|
378
|
+
# .pltz.d directory
|
|
379
|
+
target_path = dir_path / f"{panel_id}.pltz.d"
|
|
380
|
+
if target_path.exists():
|
|
381
|
+
shutil.rmtree(target_path)
|
|
382
|
+
shutil.copytree(source_path, target_path)
|
|
383
|
+
else:
|
|
384
|
+
# Fallback to save_bundle (will lose images)
|
|
385
|
+
from scitex.io._bundle import save_bundle, BundleType
|
|
386
|
+
target_path = dir_path / f"{panel_id}.pltz.d"
|
|
387
|
+
save_bundle(plot_source, target_path, bundle_type=BundleType.PLTZ)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _generate_figz_overview(dir_path: Path, spec: Dict, data: Dict, basename: str) -> None:
|
|
391
|
+
"""Generate overview image for figz bundle showing panels with hitmaps, overlays, and bboxes.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
dir_path: Bundle directory path.
|
|
395
|
+
spec: Bundle specification.
|
|
396
|
+
data: Bundle data dictionary.
|
|
397
|
+
basename: Base filename for bundle files.
|
|
398
|
+
"""
|
|
399
|
+
import matplotlib.pyplot as plt
|
|
400
|
+
import matplotlib.gridspec as gridspec
|
|
401
|
+
import matplotlib.patches as patches
|
|
402
|
+
from PIL import Image
|
|
403
|
+
import numpy as np
|
|
404
|
+
import warnings
|
|
405
|
+
import tempfile
|
|
406
|
+
import zipfile
|
|
407
|
+
|
|
408
|
+
# Find all panel bundles (both .pltz.d directories and .pltz zip files)
|
|
409
|
+
panel_dirs = []
|
|
410
|
+
temp_dirs_to_cleanup = []
|
|
411
|
+
|
|
412
|
+
for item in dir_path.iterdir():
|
|
413
|
+
if item.is_dir() and str(item).endswith('.pltz.d'):
|
|
414
|
+
panel_dirs.append(item)
|
|
415
|
+
elif item.is_file() and str(item).endswith('.pltz'):
|
|
416
|
+
# Extract .pltz zip to temp directory for overview generation
|
|
417
|
+
temp_dir = tempfile.mkdtemp(prefix=f'scitex_overview_{item.stem}_')
|
|
418
|
+
temp_dirs_to_cleanup.append(temp_dir)
|
|
419
|
+
with zipfile.ZipFile(item, 'r') as zf:
|
|
420
|
+
zf.extractall(temp_dir)
|
|
421
|
+
# Find the extracted .pltz.d directory
|
|
422
|
+
extracted = Path(temp_dir)
|
|
423
|
+
for subitem in extracted.iterdir():
|
|
424
|
+
if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
|
|
425
|
+
panel_dirs.append(subitem)
|
|
426
|
+
break
|
|
427
|
+
else:
|
|
428
|
+
# Use temp dir directly if no .pltz.d subfolder
|
|
429
|
+
panel_dirs.append(extracted)
|
|
430
|
+
|
|
431
|
+
panel_dirs = sorted(panel_dirs, key=lambda x: x.name)
|
|
432
|
+
n_panels = len(panel_dirs)
|
|
433
|
+
|
|
434
|
+
if n_panels == 0:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
# Create figure with 2 rows per panel:
|
|
438
|
+
# Row 1: Plot | Hitmap | Overlay
|
|
439
|
+
# Row 2: Bboxes | (empty) | (empty)
|
|
440
|
+
fig_width = 15
|
|
441
|
+
fig_height = 6 * n_panels + 1
|
|
442
|
+
fig = plt.figure(figsize=(fig_width, fig_height), facecolor="white")
|
|
443
|
+
|
|
444
|
+
# Title
|
|
445
|
+
title = spec.get("figure", {}).get("title", basename)
|
|
446
|
+
fig.suptitle(f"Figure Overview: {title}", fontsize=14, fontweight="bold", y=0.99)
|
|
447
|
+
|
|
448
|
+
# Create gridspec - 2 rows per panel, 3 columns
|
|
449
|
+
gs = gridspec.GridSpec(n_panels * 2, 3, figure=fig, hspace=0.3, wspace=0.15,
|
|
450
|
+
height_ratios=[1, 1] * n_panels)
|
|
451
|
+
|
|
452
|
+
# Add each panel
|
|
453
|
+
for idx, panel_dir in enumerate(panel_dirs):
|
|
454
|
+
panel_id = panel_dir.stem.replace(".pltz", "")
|
|
455
|
+
row_base = idx * 2 # Two rows per panel
|
|
456
|
+
|
|
457
|
+
# Find PNG in panel directory (check exports/ first for layered format, then root)
|
|
458
|
+
png_files = list(panel_dir.glob("exports/*.png"))
|
|
459
|
+
if not png_files:
|
|
460
|
+
png_files = list(panel_dir.glob("*.png"))
|
|
461
|
+
main_pngs = [f for f in png_files if "_hitmap" not in f.name and "_overview" not in f.name]
|
|
462
|
+
|
|
463
|
+
# Find hitmap PNG
|
|
464
|
+
hitmap_files = list(panel_dir.glob("exports/*_hitmap.png"))
|
|
465
|
+
if not hitmap_files:
|
|
466
|
+
hitmap_files = list(panel_dir.glob("*_hitmap.png"))
|
|
467
|
+
|
|
468
|
+
# Load geometry for bboxes
|
|
469
|
+
geometry_data = {}
|
|
470
|
+
geometry_path = panel_dir / "cache" / "geometry_px.json"
|
|
471
|
+
if geometry_path.exists():
|
|
472
|
+
with open(geometry_path, "r") as f:
|
|
473
|
+
geometry_data = json.load(f)
|
|
474
|
+
|
|
475
|
+
# === Row 1: Plot | Hitmap | Overlay ===
|
|
476
|
+
# Left subplot: main image
|
|
477
|
+
ax_main = fig.add_subplot(gs[row_base, 0])
|
|
478
|
+
ax_main.set_title(f"Panel {panel_id}", fontweight="bold", fontsize=11)
|
|
479
|
+
|
|
480
|
+
main_img = None
|
|
481
|
+
if main_pngs:
|
|
482
|
+
main_img = Image.open(main_pngs[0])
|
|
483
|
+
ax_main.imshow(main_img)
|
|
484
|
+
else:
|
|
485
|
+
ax_main.text(0.5, 0.5, "No image", ha="center", va="center", transform=ax_main.transAxes)
|
|
486
|
+
ax_main.axis("off")
|
|
487
|
+
|
|
488
|
+
# Middle subplot: hitmap
|
|
489
|
+
ax_hitmap = fig.add_subplot(gs[row_base, 1])
|
|
490
|
+
ax_hitmap.set_title(f"Hitmap {panel_id}", fontweight="bold", fontsize=11)
|
|
491
|
+
|
|
492
|
+
hitmap_img = None
|
|
493
|
+
if hitmap_files:
|
|
494
|
+
hitmap_img = Image.open(hitmap_files[0])
|
|
495
|
+
ax_hitmap.imshow(hitmap_img)
|
|
496
|
+
else:
|
|
497
|
+
ax_hitmap.text(0.5, 0.5, "No hitmap", ha="center", va="center", transform=ax_hitmap.transAxes)
|
|
498
|
+
ax_hitmap.axis("off")
|
|
499
|
+
|
|
500
|
+
# Right subplot: overlay
|
|
501
|
+
ax_overlay = fig.add_subplot(gs[row_base, 2])
|
|
502
|
+
ax_overlay.set_title(f"Overlay {panel_id}", fontweight="bold", fontsize=11)
|
|
503
|
+
|
|
504
|
+
if main_img is not None:
|
|
505
|
+
ax_overlay.imshow(main_img)
|
|
506
|
+
if hitmap_img is not None:
|
|
507
|
+
hitmap_rgba = hitmap_img.convert("RGBA")
|
|
508
|
+
hitmap_array = np.array(hitmap_rgba)
|
|
509
|
+
# Create semi-transparent overlay
|
|
510
|
+
hitmap_array[:, :, 3] = (hitmap_array[:, :, 3] * 0.5).astype(np.uint8)
|
|
511
|
+
ax_overlay.imshow(hitmap_array, alpha=0.5)
|
|
512
|
+
else:
|
|
513
|
+
ax_overlay.text(0.5, 0.5, "No overlay", ha="center", va="center", transform=ax_overlay.transAxes)
|
|
514
|
+
ax_overlay.axis("off")
|
|
515
|
+
|
|
516
|
+
# === Row 2: Bboxes ===
|
|
517
|
+
ax_bboxes = fig.add_subplot(gs[row_base + 1, 0])
|
|
518
|
+
ax_bboxes.set_title(f"Bboxes {panel_id}", fontweight="bold", fontsize=11)
|
|
519
|
+
|
|
520
|
+
if main_img is not None:
|
|
521
|
+
ax_bboxes.imshow(main_img)
|
|
522
|
+
# Draw bboxes from geometry
|
|
523
|
+
_draw_bboxes_from_geometry(ax_bboxes, geometry_data)
|
|
524
|
+
else:
|
|
525
|
+
ax_bboxes.text(0.5, 0.5, "No image", ha="center", va="center", transform=ax_bboxes.transAxes)
|
|
526
|
+
ax_bboxes.axis("off")
|
|
527
|
+
|
|
528
|
+
# Info panel
|
|
529
|
+
ax_info = fig.add_subplot(gs[row_base + 1, 1:])
|
|
530
|
+
ax_info.set_title(f"Info {panel_id}", fontweight="bold", fontsize=11)
|
|
531
|
+
ax_info.axis("off")
|
|
532
|
+
|
|
533
|
+
# Show spec/style summary
|
|
534
|
+
spec_path = panel_dir / "spec.json"
|
|
535
|
+
style_path = panel_dir / "style.json"
|
|
536
|
+
info_text = ""
|
|
537
|
+
|
|
538
|
+
if spec_path.exists():
|
|
539
|
+
with open(spec_path, "r") as f:
|
|
540
|
+
spec_data = json.load(f)
|
|
541
|
+
info_text += f"Axes: {len(spec_data.get('axes', []))}\n"
|
|
542
|
+
info_text += f"Traces: {len(spec_data.get('traces', []))}\n"
|
|
543
|
+
|
|
544
|
+
if style_path.exists():
|
|
545
|
+
with open(style_path, "r") as f:
|
|
546
|
+
style_data = json.load(f)
|
|
547
|
+
size = style_data.get("size", {})
|
|
548
|
+
info_text += f"Size: {size.get('width_mm', 0):.1f} × {size.get('height_mm', 0):.1f} mm\n"
|
|
549
|
+
info_text += f"Theme: {style_data.get('theme', {}).get('mode', 'light')}\n"
|
|
550
|
+
|
|
551
|
+
manifest_path = panel_dir / "cache" / "render_manifest.json"
|
|
552
|
+
if manifest_path.exists():
|
|
553
|
+
with open(manifest_path, "r") as f:
|
|
554
|
+
manifest_data = json.load(f)
|
|
555
|
+
info_text += f"DPI: {manifest_data.get('dpi', 300)}\n"
|
|
556
|
+
render_px = manifest_data.get("render_px", [0, 0])
|
|
557
|
+
info_text += f"Pixels: {render_px[0]} × {render_px[1]}\n"
|
|
558
|
+
|
|
559
|
+
ax_info.text(0.02, 0.98, info_text, transform=ax_info.transAxes,
|
|
560
|
+
fontsize=10, fontfamily="monospace", verticalalignment="top",
|
|
561
|
+
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5))
|
|
562
|
+
|
|
563
|
+
# Save overview to exports/ directory
|
|
564
|
+
exports_dir = dir_path / "exports"
|
|
565
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
566
|
+
overview_path = exports_dir / f"{basename}_overview.png"
|
|
567
|
+
with warnings.catch_warnings():
|
|
568
|
+
warnings.filterwarnings("ignore", message=".*tight_layout.*")
|
|
569
|
+
fig.savefig(overview_path, dpi=150, bbox_inches="tight", facecolor="white")
|
|
570
|
+
plt.close(fig)
|
|
571
|
+
|
|
572
|
+
# Cleanup temp directories
|
|
573
|
+
for temp_dir in temp_dirs_to_cleanup:
|
|
574
|
+
try:
|
|
575
|
+
shutil.rmtree(temp_dir)
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _draw_bboxes_from_geometry(ax, geometry_data: Dict) -> None:
|
|
581
|
+
"""Draw bboxes from geometry data on an axes.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
ax: Matplotlib axes.
|
|
585
|
+
geometry_data: Geometry data dictionary.
|
|
586
|
+
"""
|
|
587
|
+
import matplotlib.patches as patches
|
|
588
|
+
|
|
589
|
+
colors = ["red", "blue", "green", "orange", "purple", "cyan"]
|
|
590
|
+
selectable = geometry_data.get("selectable_regions", {})
|
|
591
|
+
|
|
592
|
+
for ax_idx, ax_region in enumerate(selectable.get("axes", [])):
|
|
593
|
+
color = colors[ax_idx % len(colors)]
|
|
594
|
+
|
|
595
|
+
# Title bbox
|
|
596
|
+
if "title" in ax_region:
|
|
597
|
+
bbox = ax_region["title"].get("bbox_px", [])
|
|
598
|
+
if len(bbox) == 4:
|
|
599
|
+
_draw_single_bbox(ax, bbox, color, "title")
|
|
600
|
+
|
|
601
|
+
# xlabel bbox
|
|
602
|
+
if "xlabel" in ax_region:
|
|
603
|
+
bbox = ax_region["xlabel"].get("bbox_px", [])
|
|
604
|
+
if len(bbox) == 4:
|
|
605
|
+
_draw_single_bbox(ax, bbox, color, "xlabel")
|
|
606
|
+
|
|
607
|
+
# ylabel bbox
|
|
608
|
+
if "ylabel" in ax_region:
|
|
609
|
+
bbox = ax_region["ylabel"].get("bbox_px", [])
|
|
610
|
+
if len(bbox) == 4:
|
|
611
|
+
_draw_single_bbox(ax, bbox, color, "ylabel")
|
|
612
|
+
|
|
613
|
+
# xaxis spine
|
|
614
|
+
if "xaxis" in ax_region and "spine" in ax_region["xaxis"]:
|
|
615
|
+
bbox = ax_region["xaxis"]["spine"].get("bbox_px", [])
|
|
616
|
+
if len(bbox) == 4:
|
|
617
|
+
_draw_single_bbox(ax, bbox, "gray", "xaxis", lw=1)
|
|
618
|
+
|
|
619
|
+
# yaxis spine
|
|
620
|
+
if "yaxis" in ax_region and "spine" in ax_region["yaxis"]:
|
|
621
|
+
bbox = ax_region["yaxis"]["spine"].get("bbox_px", [])
|
|
622
|
+
if len(bbox) == 4:
|
|
623
|
+
_draw_single_bbox(ax, bbox, "gray", "yaxis", lw=1)
|
|
624
|
+
|
|
625
|
+
# legend bbox
|
|
626
|
+
if "legend" in ax_region:
|
|
627
|
+
bbox = ax_region["legend"].get("bbox_px", [])
|
|
628
|
+
if len(bbox) == 4:
|
|
629
|
+
_draw_single_bbox(ax, bbox, "magenta", "legend")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _draw_single_bbox(ax, bbox: List, color: str, label: str, lw: int = 2) -> None:
|
|
633
|
+
"""Draw a single bbox rectangle on axes.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
ax: Matplotlib axes.
|
|
637
|
+
bbox: [x0, y0, x1, y1] bounding box (corner coordinates).
|
|
638
|
+
color: Rectangle color.
|
|
639
|
+
label: Label text.
|
|
640
|
+
lw: Line width.
|
|
641
|
+
"""
|
|
642
|
+
import matplotlib.patches as patches
|
|
643
|
+
|
|
644
|
+
# bbox is [x0, y0, x1, y1] format
|
|
645
|
+
x0, y0, x1, y1 = bbox
|
|
646
|
+
width = x1 - x0
|
|
647
|
+
height = y1 - y0
|
|
648
|
+
rect = patches.Rectangle((x0, y0), width, height,
|
|
649
|
+
linewidth=lw, edgecolor=color, facecolor='none')
|
|
650
|
+
ax.add_patch(rect)
|
|
651
|
+
# Add label
|
|
652
|
+
ax.text(x0 + 2, y0 + height / 2, label, fontsize=6, color=color, fontweight="bold")
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _generate_composed_figure(dir_path: Path, spec: Dict, basename: str) -> None:
|
|
656
|
+
"""Generate composed figure from panel images.
|
|
657
|
+
|
|
658
|
+
Composes all panel PNG images into a single figure based on the layout
|
|
659
|
+
specified in the figz spec.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
dir_path: Bundle directory path.
|
|
663
|
+
spec: Bundle specification with panel layout.
|
|
664
|
+
basename: Base filename for exports.
|
|
665
|
+
"""
|
|
666
|
+
from PIL import Image
|
|
667
|
+
import warnings
|
|
668
|
+
|
|
669
|
+
exports_dir = dir_path / "exports"
|
|
670
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
671
|
+
|
|
672
|
+
# Load style from style.json if exists, else from spec
|
|
673
|
+
style_file = dir_path / "style.json"
|
|
674
|
+
if style_file.exists():
|
|
675
|
+
with open(style_file, "r") as f:
|
|
676
|
+
style = json.load(f)
|
|
677
|
+
size = style.get("size", {})
|
|
678
|
+
background = style.get("background", "#ffffff")
|
|
679
|
+
else:
|
|
680
|
+
# Fallback to embedded styles in spec
|
|
681
|
+
figure = spec.get("figure", {})
|
|
682
|
+
styles = figure.get("styles", {})
|
|
683
|
+
size = styles.get("size", {})
|
|
684
|
+
background = styles.get("background", "#ffffff")
|
|
685
|
+
|
|
686
|
+
fig_width_mm = size.get("width_mm", 180)
|
|
687
|
+
fig_height_mm = size.get("height_mm", 120)
|
|
688
|
+
|
|
689
|
+
# Use 300 DPI for composition
|
|
690
|
+
dpi = 300
|
|
691
|
+
mm_to_inch = 1 / 25.4
|
|
692
|
+
fig_width_px = int(fig_width_mm * mm_to_inch * dpi)
|
|
693
|
+
fig_height_px = int(fig_height_mm * mm_to_inch * dpi)
|
|
694
|
+
|
|
695
|
+
# Create canvas
|
|
696
|
+
canvas = Image.new("RGB", (fig_width_px, fig_height_px), background)
|
|
697
|
+
|
|
698
|
+
# Get panels from spec
|
|
699
|
+
panels = spec.get("panels", [])
|
|
700
|
+
|
|
701
|
+
for panel in panels:
|
|
702
|
+
panel_id = panel.get("id", "")
|
|
703
|
+
plot_ref = panel.get("plot", "")
|
|
704
|
+
|
|
705
|
+
# Find the panel's pltz bundle
|
|
706
|
+
if plot_ref.endswith(".pltz.d"):
|
|
707
|
+
panel_dir = dir_path / plot_ref
|
|
708
|
+
else:
|
|
709
|
+
panel_dir = dir_path / f"{panel_id}.pltz.d"
|
|
710
|
+
|
|
711
|
+
if not panel_dir.exists():
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
# Find panel PNG in exports/
|
|
715
|
+
panel_png = None
|
|
716
|
+
exports_subdir = panel_dir / "exports"
|
|
717
|
+
if exports_subdir.exists():
|
|
718
|
+
for png_file in exports_subdir.glob("*.png"):
|
|
719
|
+
if "_hitmap" not in png_file.name and "_overview" not in png_file.name:
|
|
720
|
+
panel_png = png_file
|
|
721
|
+
break
|
|
722
|
+
|
|
723
|
+
# Fallback: look in panel root
|
|
724
|
+
if not panel_png:
|
|
725
|
+
for png_file in panel_dir.glob("*.png"):
|
|
726
|
+
if "_hitmap" not in png_file.name and "_overview" not in png_file.name:
|
|
727
|
+
panel_png = png_file
|
|
728
|
+
break
|
|
729
|
+
|
|
730
|
+
if not panel_png or not panel_png.exists():
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
# Load panel image
|
|
734
|
+
panel_img = Image.open(panel_png)
|
|
735
|
+
|
|
736
|
+
# Get panel position and size from spec
|
|
737
|
+
pos = panel.get("position", {})
|
|
738
|
+
panel_size = panel.get("size", {})
|
|
739
|
+
|
|
740
|
+
x_mm = pos.get("x_mm", 0)
|
|
741
|
+
y_mm = pos.get("y_mm", 0)
|
|
742
|
+
width_mm = panel_size.get("width_mm", 80)
|
|
743
|
+
height_mm = panel_size.get("height_mm", 68)
|
|
744
|
+
|
|
745
|
+
# Convert to pixels
|
|
746
|
+
x_px = int(x_mm * mm_to_inch * dpi)
|
|
747
|
+
y_px = int(y_mm * mm_to_inch * dpi)
|
|
748
|
+
target_width = int(width_mm * mm_to_inch * dpi)
|
|
749
|
+
target_height = int(height_mm * mm_to_inch * dpi)
|
|
750
|
+
|
|
751
|
+
# Resize panel to fit
|
|
752
|
+
panel_img = panel_img.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
753
|
+
|
|
754
|
+
# Convert to RGB if necessary (for transparent PNGs)
|
|
755
|
+
if panel_img.mode == "RGBA":
|
|
756
|
+
# Create white background
|
|
757
|
+
bg = Image.new("RGB", panel_img.size, background)
|
|
758
|
+
bg.paste(panel_img, mask=panel_img.split()[3])
|
|
759
|
+
panel_img = bg
|
|
760
|
+
elif panel_img.mode != "RGB":
|
|
761
|
+
panel_img = panel_img.convert("RGB")
|
|
762
|
+
|
|
763
|
+
# Paste onto canvas
|
|
764
|
+
canvas.paste(panel_img, (x_px, y_px))
|
|
765
|
+
|
|
766
|
+
# Save composed figure
|
|
767
|
+
png_path = exports_dir / f"{basename}.png"
|
|
768
|
+
canvas.save(png_path, "PNG", dpi=(dpi, dpi))
|
|
769
|
+
|
|
770
|
+
# Also save as SVG (embed PNG in SVG for now)
|
|
771
|
+
svg_path = exports_dir / f"{basename}.svg"
|
|
772
|
+
svg_width_in = fig_width_mm * mm_to_inch
|
|
773
|
+
svg_height_in = fig_height_mm * mm_to_inch
|
|
774
|
+
|
|
775
|
+
# Create simple SVG wrapper with embedded image
|
|
776
|
+
import base64
|
|
777
|
+
with open(png_path, "rb") as f:
|
|
778
|
+
png_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
779
|
+
|
|
780
|
+
svg_content = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
781
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
782
|
+
width="{fig_width_px}" height="{fig_height_px}"
|
|
783
|
+
viewBox="0 0 {fig_width_px} {fig_height_px}">
|
|
784
|
+
<image width="{fig_width_px}" height="{fig_height_px}"
|
|
785
|
+
xlink:href="data:image/png;base64,{png_b64}"/>
|
|
786
|
+
</svg>'''
|
|
787
|
+
|
|
788
|
+
with open(svg_path, "w") as f:
|
|
789
|
+
f.write(svg_content)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) -> None:
|
|
793
|
+
"""Generate figure-level geometry cache combining all panel geometries.
|
|
794
|
+
|
|
795
|
+
Creates:
|
|
796
|
+
cache/geometry_px.json - Combined geometry for all panels
|
|
797
|
+
cache/render_manifest.json - Figure-level render metadata
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
dir_path: Bundle directory path.
|
|
801
|
+
spec: Bundle specification.
|
|
802
|
+
basename: Base filename for bundle files.
|
|
803
|
+
"""
|
|
804
|
+
from datetime import datetime
|
|
805
|
+
import tempfile
|
|
806
|
+
import zipfile
|
|
807
|
+
|
|
808
|
+
cache_dir = dir_path / "cache"
|
|
809
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
810
|
+
|
|
811
|
+
# Collect geometry from all panel bundles
|
|
812
|
+
combined_geometry = {
|
|
813
|
+
"figure_id": basename,
|
|
814
|
+
"panels": {},
|
|
815
|
+
"generated_at": datetime.now().isoformat(),
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
# Find all panel bundles (both .pltz.d directories and .pltz zip files)
|
|
819
|
+
panel_sources = []
|
|
820
|
+
temp_dirs_to_cleanup = []
|
|
821
|
+
|
|
822
|
+
for item in dir_path.iterdir():
|
|
823
|
+
if item.is_dir() and str(item).endswith('.pltz.d'):
|
|
824
|
+
panel_sources.append((item.stem.replace('.pltz', ''), item))
|
|
825
|
+
elif item.is_file() and str(item).endswith('.pltz'):
|
|
826
|
+
# Extract .pltz zip to temp directory
|
|
827
|
+
temp_dir = tempfile.mkdtemp(prefix=f'scitex_geom_{item.stem}_')
|
|
828
|
+
temp_dirs_to_cleanup.append(temp_dir)
|
|
829
|
+
with zipfile.ZipFile(item, 'r') as zf:
|
|
830
|
+
zf.extractall(temp_dir)
|
|
831
|
+
extracted = Path(temp_dir)
|
|
832
|
+
for subitem in extracted.iterdir():
|
|
833
|
+
if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
|
|
834
|
+
panel_sources.append((item.stem, subitem))
|
|
835
|
+
break
|
|
836
|
+
else:
|
|
837
|
+
panel_sources.append((item.stem, extracted))
|
|
838
|
+
|
|
839
|
+
panel_sources = sorted(panel_sources, key=lambda x: x[0])
|
|
840
|
+
|
|
841
|
+
for panel_id, panel_dir in panel_sources:
|
|
842
|
+
|
|
843
|
+
# Load panel geometry
|
|
844
|
+
panel_geometry_path = panel_dir / "cache" / "geometry_px.json"
|
|
845
|
+
if panel_geometry_path.exists():
|
|
846
|
+
with open(panel_geometry_path, "r") as f:
|
|
847
|
+
panel_geometry = json.load(f)
|
|
848
|
+
combined_geometry["panels"][panel_id] = panel_geometry
|
|
849
|
+
|
|
850
|
+
# Add panel positions from spec
|
|
851
|
+
panels_spec = spec.get("panels", [])
|
|
852
|
+
for panel in panels_spec:
|
|
853
|
+
panel_id = panel.get("id")
|
|
854
|
+
if panel_id and panel_id in combined_geometry["panels"]:
|
|
855
|
+
combined_geometry["panels"][panel_id]["position_mm"] = panel.get("position", {})
|
|
856
|
+
combined_geometry["panels"][panel_id]["size_mm"] = panel.get("size", {})
|
|
857
|
+
|
|
858
|
+
# Save combined geometry
|
|
859
|
+
geometry_path = cache_dir / "geometry_px.json"
|
|
860
|
+
with open(geometry_path, "w") as f:
|
|
861
|
+
json.dump(combined_geometry, f, indent=2)
|
|
862
|
+
|
|
863
|
+
# Generate render manifest
|
|
864
|
+
figure_styles = spec.get("figure", {}).get("styles", {})
|
|
865
|
+
size = figure_styles.get("size", {})
|
|
866
|
+
|
|
867
|
+
manifest = {
|
|
868
|
+
"figure_id": basename,
|
|
869
|
+
"generated_at": datetime.now().isoformat(),
|
|
870
|
+
"size_mm": [size.get("width_mm", 0), size.get("height_mm", 0)],
|
|
871
|
+
"panels_count": len(panel_sources),
|
|
872
|
+
"schema": spec.get("schema", {}),
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
manifest_path = cache_dir / "render_manifest.json"
|
|
876
|
+
with open(manifest_path, "w") as f:
|
|
877
|
+
json.dump(manifest, f, indent=2)
|
|
878
|
+
|
|
879
|
+
# Cleanup temp directories
|
|
880
|
+
for temp_dir in temp_dirs_to_cleanup:
|
|
881
|
+
try:
|
|
882
|
+
shutil.rmtree(temp_dir)
|
|
883
|
+
except Exception:
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _embed_metadata_in_export(
|
|
888
|
+
file_path: Path, spec: Dict[str, Any], fmt: str
|
|
889
|
+
) -> None:
|
|
890
|
+
"""Embed bundle spec metadata into exported image files."""
|
|
891
|
+
from scitex.io._metadata import embed_metadata
|
|
892
|
+
|
|
893
|
+
embed_data = {
|
|
894
|
+
"scitex_bundle": True,
|
|
895
|
+
"schema": spec.get("schema", {}),
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
for key in ["figure", "panels", "notations"]:
|
|
899
|
+
if key in spec:
|
|
900
|
+
embed_data[key] = spec[key]
|
|
901
|
+
|
|
902
|
+
if fmt in ("png", "pdf"):
|
|
903
|
+
embed_metadata(str(file_path), embed_data)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _generate_figz_readme(
|
|
907
|
+
dir_path: Path, spec: Dict, data: Dict, basename: str
|
|
908
|
+
) -> None:
|
|
909
|
+
"""Generate a dynamic README.md for figz bundle.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
dir_path: Bundle directory path.
|
|
913
|
+
spec: Bundle specification.
|
|
914
|
+
data: Bundle data dictionary.
|
|
915
|
+
basename: Base filename for bundle files.
|
|
916
|
+
"""
|
|
917
|
+
from datetime import datetime
|
|
918
|
+
|
|
919
|
+
# Extract figure info
|
|
920
|
+
figure = spec.get("figure", {})
|
|
921
|
+
title = figure.get("title", basename)
|
|
922
|
+
caption = figure.get("caption", "")
|
|
923
|
+
|
|
924
|
+
# Load style from style.json if exists, else from spec.figure.styles
|
|
925
|
+
style_file = dir_path / "style.json"
|
|
926
|
+
if style_file.exists():
|
|
927
|
+
with open(style_file, "r") as f:
|
|
928
|
+
style = json.load(f)
|
|
929
|
+
size = style.get("size", {})
|
|
930
|
+
background = style.get("background", "#ffffff")
|
|
931
|
+
else:
|
|
932
|
+
styles = figure.get("styles", {})
|
|
933
|
+
size = styles.get("size", {})
|
|
934
|
+
background = styles.get("background", "#ffffff")
|
|
935
|
+
|
|
936
|
+
width_mm = size.get("width_mm", 0)
|
|
937
|
+
height_mm = size.get("height_mm", 0)
|
|
938
|
+
|
|
939
|
+
# Count panels
|
|
940
|
+
panels = spec.get("panels", [])
|
|
941
|
+
n_panels = len(panels)
|
|
942
|
+
|
|
943
|
+
# Find panel directories
|
|
944
|
+
panel_dirs = sorted(dir_path.glob("*.pltz.d"))
|
|
945
|
+
|
|
946
|
+
# Build panel table
|
|
947
|
+
panel_rows = ""
|
|
948
|
+
for panel in panels:
|
|
949
|
+
panel_id = panel.get("id", "?")
|
|
950
|
+
label = panel.get("label", panel_id)
|
|
951
|
+
plot_ref = panel.get("plot", "")
|
|
952
|
+
pos = panel.get("position", {})
|
|
953
|
+
panel_size = panel.get("size", {})
|
|
954
|
+
panel_rows += f"| {label} | {plot_ref} | ({pos.get('x_mm', 0)}, {pos.get('y_mm', 0)}) | {panel_size.get('width_mm', 0)} × {panel_size.get('height_mm', 0)} mm |\n"
|
|
955
|
+
|
|
956
|
+
# Build panel directory list
|
|
957
|
+
panel_dir_list = ""
|
|
958
|
+
for pd in panel_dirs:
|
|
959
|
+
panel_dir_list += f"│ ├── {pd.name}/\n"
|
|
960
|
+
|
|
961
|
+
readme_content = f"""# {basename}.figz.d
|
|
962
|
+
|
|
963
|
+
> SciTeX Figure Bundle - Auto-generated README
|
|
964
|
+
|
|
965
|
+
## Overview
|
|
966
|
+
|
|
967
|
+

|
|
968
|
+
|
|
969
|
+
## Bundle Structure
|
|
970
|
+
|
|
971
|
+
```
|
|
972
|
+
{basename}.figz.d/
|
|
973
|
+
├── spec.json # Figure specification (semantic: what to draw)
|
|
974
|
+
├── style.json # Figure style (appearance: how it looks)
|
|
975
|
+
├── {basename}.json # Combined spec+style (legacy compatibility)
|
|
976
|
+
├── exports/ # Figure-level exports
|
|
977
|
+
│ ├── {basename}.png # Rendered figure (raster)
|
|
978
|
+
│ ├── {basename}.svg # Rendered figure (vector)
|
|
979
|
+
│ └── {basename}_overview.png # Visual summary with hitmaps
|
|
980
|
+
├── cache/ # Figure-level cache (regenerable)
|
|
981
|
+
│ ├── geometry_px.json # Combined geometry for all panels
|
|
982
|
+
│ └── render_manifest.json # Render metadata
|
|
983
|
+
{panel_dir_list}└── README.md # This file
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
## Figure Information
|
|
987
|
+
|
|
988
|
+
| Property | Value |
|
|
989
|
+
|----------|-------|
|
|
990
|
+
| Title | {title or '(none)'} |
|
|
991
|
+
| Panels | {n_panels} |
|
|
992
|
+
| Size | {width_mm:.1f} × {height_mm:.1f} mm |
|
|
993
|
+
| Background | `{background}` |
|
|
994
|
+
|
|
995
|
+
{f"**Caption**: {caption}" if caption else ""}
|
|
996
|
+
|
|
997
|
+
## Panel Layout
|
|
998
|
+
|
|
999
|
+
| Label | Plot Bundle | Position (x, y) | Size |
|
|
1000
|
+
|-------|-------------|-----------------|------|
|
|
1001
|
+
{panel_rows}
|
|
1002
|
+
|
|
1003
|
+
## Nested Bundles
|
|
1004
|
+
|
|
1005
|
+
Each panel is stored as a separate `.pltz.d` bundle containing:
|
|
1006
|
+
- `spec.json` - What to plot (data, axes, traces)
|
|
1007
|
+
- `style.json` - How it looks (colors, fonts, theme)
|
|
1008
|
+
- `exports/` - Rendered images (PNG, SVG, hitmap)
|
|
1009
|
+
- `cache/` - Computed geometry (regenerable)
|
|
1010
|
+
|
|
1011
|
+
## Usage
|
|
1012
|
+
|
|
1013
|
+
### Python
|
|
1014
|
+
|
|
1015
|
+
```python
|
|
1016
|
+
import scitex as stx
|
|
1017
|
+
|
|
1018
|
+
# Load the figure bundle
|
|
1019
|
+
bundle = stx.load("{dir_path}")
|
|
1020
|
+
|
|
1021
|
+
# Access components
|
|
1022
|
+
spec = bundle["spec"] # Figure layout
|
|
1023
|
+
plots = bundle["plots"] # Dict of panel bundles
|
|
1024
|
+
|
|
1025
|
+
# Access specific panel
|
|
1026
|
+
panel_a = plots["A"] # Get panel A's pltz bundle
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### Editing
|
|
1030
|
+
|
|
1031
|
+
Edit `spec.json` to change semantic content:
|
|
1032
|
+
- Panel positions and sizes
|
|
1033
|
+
- Figure title and caption
|
|
1034
|
+
- Panel layout
|
|
1035
|
+
|
|
1036
|
+
Edit `style.json` to change appearance:
|
|
1037
|
+
- Figure size (width_mm, height_mm)
|
|
1038
|
+
- Background color
|
|
1039
|
+
- Panel label styling
|
|
1040
|
+
- Theme (light/dark)
|
|
1041
|
+
|
|
1042
|
+
Edit individual `*.pltz.d/spec.json` and `*.pltz.d/style.json` to change:
|
|
1043
|
+
- Plot data and axes (spec.json)
|
|
1044
|
+
- Trace specifications (spec.json)
|
|
1045
|
+
- Colors, fonts, theme (style.json)
|
|
1046
|
+
|
|
1047
|
+
---
|
|
1048
|
+
|
|
1049
|
+
*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
|
1050
|
+
*Schema: {spec.get("schema", {}).get("name", "scitex.fig.figure")} v{spec.get("schema", {}).get("version", "1.0.0")}*
|
|
1051
|
+
"""
|
|
1052
|
+
|
|
1053
|
+
readme_path = dir_path / "README.md"
|
|
1054
|
+
with open(readme_path, "w") as f:
|
|
1055
|
+
f.write(readme_content)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
# EOF
|