napari-ome-arrow 0.0.4__tar.gz → 0.0.5__tar.gz

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 (36) hide show
  1. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/run-tests.yml +7 -0
  2. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.pre-commit-config.yaml +4 -4
  3. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CITATION.cff +26 -0
  4. {napari_ome_arrow-0.0.4/src/napari_ome_arrow.egg-info → napari_ome_arrow-0.0.5}/PKG-INFO +24 -5
  5. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/README.md +20 -3
  6. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/pyproject.toml +5 -4
  7. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader.py +340 -0
  8. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_infer.py +233 -0
  9. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_napari.py +107 -0
  10. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_omearrow.py +474 -0
  11. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_stack.py +711 -0
  12. napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_types.py +11 -0
  13. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/_version.py +3 -3
  14. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/napari.yaml +3 -0
  15. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5/src/napari_ome_arrow.egg-info}/PKG-INFO +24 -5
  16. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/SOURCES.txt +5 -0
  17. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/requires.txt +4 -1
  18. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/uv.lock +205 -318
  19. napari_ome_arrow-0.0.4/src/napari_ome_arrow/_reader.py +0 -456
  20. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/ISSUE_TEMPLATE/issue.yml +0 -0
  21. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/dependabot.yml +0 -0
  23. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/release-drafter.yml +0 -0
  24. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/draft-release.yml +0 -0
  25. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/publish-pypi.yml +0 -0
  26. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.gitignore +0 -0
  27. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.napari-hub/config.yml +0 -0
  28. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CODE_OF_CONDUCT.md +0 -0
  29. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CONTRIBUTING.md +0 -0
  30. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/LICENSE +0 -0
  31. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/MANIFEST.in +0 -0
  32. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/setup.cfg +0 -0
  33. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/__init__.py +0 -0
  34. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/dependency_links.txt +0 -0
  35. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/entry_points.txt +0 -0
  36. {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/top_level.txt +0 -0
@@ -52,5 +52,12 @@ jobs:
52
52
  with:
53
53
  qt: true
54
54
  wm: herbstluftwm
55
+ - name: Install test dependencies
56
+ run: >
57
+ uv sync --frozen
58
+ --python ${{ matrix.python_version }}
59
+ --extra all
60
+ --extra pyside6
61
+ --extra vortex
55
62
  - name: Run pytest
56
63
  run: uv run --python ${{ matrix.python_version }} --frozen pytest
@@ -9,7 +9,7 @@ repos:
9
9
  - id: check-yaml
10
10
  - id: detect-private-key
11
11
  - repo: https://github.com/tox-dev/pyproject-fmt
12
- rev: "v2.11.1"
12
+ rev: "v2.12.1"
13
13
  hooks:
14
14
  - id: pyproject-fmt
15
15
  - repo: https://github.com/citation-file-format/cffconvert
@@ -34,17 +34,17 @@ repos:
34
34
  additional_dependencies:
35
35
  - mdformat-gfm
36
36
  - repo: https://github.com/adrienverge/yamllint
37
- rev: v1.37.1
37
+ rev: v1.38.0
38
38
  hooks:
39
39
  - id: yamllint
40
40
  exclude: pre-commit-config.yaml
41
41
  - repo: https://github.com/astral-sh/ruff-pre-commit
42
- rev: "v0.14.7"
42
+ rev: "v0.15.0"
43
43
  hooks:
44
44
  - id: ruff-format
45
45
  - id: ruff-check
46
46
  - repo: https://github.com/rhysd/actionlint
47
- rev: v1.7.9
47
+ rev: v1.7.10
48
48
  hooks:
49
49
  - id: actionlint
50
50
  - repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update
@@ -129,3 +129,29 @@ references:
129
129
  JUMP (cpg0000-jump-pilot) was used to help demonstrate napari-ome-arrow's
130
130
  ability to parse various image data (specifically, plate BR00117006).
131
131
  https://github.com/broadinstitute/cellpainting-gallery
132
+ - type: article
133
+ authors:
134
+ - family-names: Blin
135
+ given-names: Guillaume
136
+ - family-names: Sadurska
137
+ given-names: Dominika
138
+ - family-names: Portero Migueles
139
+ given-names: Rafael
140
+ - family-names: Chen
141
+ given-names: Ni
142
+ - family-names: Watson
143
+ given-names: James A.
144
+ - family-names: Lowell
145
+ given-names: Sally
146
+ title: "Nessys: A new set of tools for the automated detection of nuclei within intact tissues and dense 3D cultures"
147
+ journal: PLoS Biology
148
+ volume: "17"
149
+ issue: "8"
150
+ pages: e3000388
151
+ year: 2019
152
+ doi: "10.1371/journal.pbio.3000388"
153
+ url: "https://doi.org/10.1371/journal.pbio.3000388"
154
+ notes: >
155
+ This work used the file "6001240_labels.zarr" from the DISCEPTS imaging
156
+ dataset, available through the Image Data Resource (IDR) under accession
157
+ number idr0062.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-ome-arrow
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: A Napari plugin for OME-Arrow and OME-Parquet bioimage data
5
5
  License:
6
6
  Copyright (c) 2025, Dave Bunten
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
51
51
  License-File: LICENSE
52
52
  Requires-Dist: magicgui
53
53
  Requires-Dist: numpy
54
- Requires-Dist: ome-arrow>=0.0.2
54
+ Requires-Dist: ome-arrow>=0.0.3
55
55
  Requires-Dist: qtpy>=2.4
56
56
  Requires-Dist: scikit-image
57
57
  Provides-Extra: all
@@ -65,6 +65,8 @@ Provides-Extra: pyside6
65
65
  Requires-Dist: napari[pyside6]; extra == "pyside6"
66
66
  Requires-Dist: pyside6>=6.6; extra == "pyside6"
67
67
  Requires-Dist: qtpy>=2.4; extra == "pyside6"
68
+ Provides-Extra: vortex
69
+ Requires-Dist: vortex-data; extra == "vortex"
68
70
  Dynamic: license-file
69
71
 
70
72
  # napari-ome-arrow
@@ -83,6 +85,7 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
83
85
  - **OME-TIFF** (`.ome.tif`, `.ome.tiff`, `.tif`, `.tiff`)
84
86
  - **OME-Zarr** (`.ome.zarr`, `.zarr` stores and URLs)
85
87
  - **OME-Parquet** (`.ome.parquet`, `.parquet`, `.pq`)
88
+ - **OME-Vortex** (`.ome.vortex`, `.vortex`)
86
89
  - **Bio-Formats–style stack patterns** (paths containing `<`, `>`, or `*`)
87
90
  - A simple **`.npy` fallback** for quick testing / ad-hoc arrays
88
91
 
@@ -127,8 +130,11 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
127
130
  - respects `NAPARI_OME_ARROW_LAYER_TYPE`, and
128
131
  - defaults to `"image"` if the variable is not set.
129
132
 
130
- - ✅ **Grid view for multi-row OME-Parquet**
131
- When a Parquet file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` to pick which image column to visualize.
133
+ - ✅ **Grid view for multi-row OME-Parquet / OME-Vortex**
134
+ When a Parquet or Vortex file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` or `NAPARI_OME_ARROW_VORTEX_COLUMN` to pick which image column to visualize.
135
+
136
+ - ✅ **Stack scale prompt + override**
137
+ When loading image stacks (stack patterns), napari prompts for voxel spacing only if no scale metadata or `NAPARI_OME_ARROW_STACK_SCALE` override is present and a Qt UI is available (headless runs skip the prompt). To avoid the prompt, set `NAPARI_OME_ARROW_STACK_SCALE` using `Z,Y,X` or `T,C,Z,Y,X`.
132
138
 
133
139
  ______________________________________________________________________
134
140
 
@@ -154,13 +160,19 @@ To install latest development version :
154
160
  pip install git+https://github.com/wayscience/napari-ome-arrow.git
155
161
  ```
156
162
 
163
+ To enable OME-Vortex support via OME-Arrow, install the optional extra:
164
+
165
+ ```bash
166
+ pip install "napari-ome-arrow[vortex]"
167
+ ```
168
+
157
169
  ## Usage
158
170
 
159
171
  ### From the napari GUI
160
172
 
161
173
  1. Install the plugin (see above).
162
174
  1. Start napari.
163
- 1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet file, or stack pattern into the viewer.
175
+ 1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet, OME-Vortex file, a stack pattern, or a multi-select stack (e.g. `img_000.tif` ... `img_123.tif`) into the viewer.
164
176
  1. When prompted, choose **Image** or **Labels**.
165
177
 
166
178
  The plugin will:
@@ -168,6 +180,7 @@ The plugin will:
168
180
  - load the data through `OMEArrow`,
169
181
  - map channels and axes appropriately, and
170
182
  - automatically switch to 3D if there is a Z-stack.
183
+ - When multiple files look like a numbered stack, treat them as a single stack rather than independent layers.
171
184
 
172
185
  ### From the command line
173
186
 
@@ -184,6 +197,12 @@ NAPARI_OME_ARROW_LAYER_TYPE=labels napari my_labels.ome.parquet
184
197
  NAPARI_OME_ARROW_LAYER_TYPE=image \\
185
198
  NAPARI_OME_ARROW_PARQUET_COLUMN=Image_FileName_OrigDNA_OMEArrow_ORIG \\
186
199
  napari tests/data/cytodataframe/BR00117006.ome.parquet
200
+
201
+ # Prefill stack voxel spacing for stack patterns (Z,Y,X or T,C,Z,Y,X)
202
+ NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/z<000-120>.tif"
203
+
204
+ # Prefill stack voxel spacing for multi-file stacks (use a pattern or glob on CLI)
205
+ NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/img_<000-120>.tif"
187
206
  ```
188
207
 
189
208
  ## Contributing
@@ -14,6 +14,7 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
14
14
  - **OME-TIFF** (`.ome.tif`, `.ome.tiff`, `.tif`, `.tiff`)
15
15
  - **OME-Zarr** (`.ome.zarr`, `.zarr` stores and URLs)
16
16
  - **OME-Parquet** (`.ome.parquet`, `.parquet`, `.pq`)
17
+ - **OME-Vortex** (`.ome.vortex`, `.vortex`)
17
18
  - **Bio-Formats–style stack patterns** (paths containing `<`, `>`, or `*`)
18
19
  - A simple **`.npy` fallback** for quick testing / ad-hoc arrays
19
20
 
@@ -58,8 +59,11 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
58
59
  - respects `NAPARI_OME_ARROW_LAYER_TYPE`, and
59
60
  - defaults to `"image"` if the variable is not set.
60
61
 
61
- - ✅ **Grid view for multi-row OME-Parquet**
62
- When a Parquet file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` to pick which image column to visualize.
62
+ - ✅ **Grid view for multi-row OME-Parquet / OME-Vortex**
63
+ When a Parquet or Vortex file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` or `NAPARI_OME_ARROW_VORTEX_COLUMN` to pick which image column to visualize.
64
+
65
+ - ✅ **Stack scale prompt + override**
66
+ When loading image stacks (stack patterns), napari prompts for voxel spacing only if no scale metadata or `NAPARI_OME_ARROW_STACK_SCALE` override is present and a Qt UI is available (headless runs skip the prompt). To avoid the prompt, set `NAPARI_OME_ARROW_STACK_SCALE` using `Z,Y,X` or `T,C,Z,Y,X`.
63
67
 
64
68
  ______________________________________________________________________
65
69
 
@@ -85,13 +89,19 @@ To install latest development version :
85
89
  pip install git+https://github.com/wayscience/napari-ome-arrow.git
86
90
  ```
87
91
 
92
+ To enable OME-Vortex support via OME-Arrow, install the optional extra:
93
+
94
+ ```bash
95
+ pip install "napari-ome-arrow[vortex]"
96
+ ```
97
+
88
98
  ## Usage
89
99
 
90
100
  ### From the napari GUI
91
101
 
92
102
  1. Install the plugin (see above).
93
103
  1. Start napari.
94
- 1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet file, or stack pattern into the viewer.
104
+ 1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet, OME-Vortex file, a stack pattern, or a multi-select stack (e.g. `img_000.tif` ... `img_123.tif`) into the viewer.
95
105
  1. When prompted, choose **Image** or **Labels**.
96
106
 
97
107
  The plugin will:
@@ -99,6 +109,7 @@ The plugin will:
99
109
  - load the data through `OMEArrow`,
100
110
  - map channels and axes appropriately, and
101
111
  - automatically switch to 3D if there is a Z-stack.
112
+ - When multiple files look like a numbered stack, treat them as a single stack rather than independent layers.
102
113
 
103
114
  ### From the command line
104
115
 
@@ -115,6 +126,12 @@ NAPARI_OME_ARROW_LAYER_TYPE=labels napari my_labels.ome.parquet
115
126
  NAPARI_OME_ARROW_LAYER_TYPE=image \\
116
127
  NAPARI_OME_ARROW_PARQUET_COLUMN=Image_FileName_OrigDNA_OMEArrow_ORIG \\
117
128
  napari tests/data/cytodataframe/BR00117006.ome.parquet
129
+
130
+ # Prefill stack voxel spacing for stack patterns (Z,Y,X or T,C,Z,Y,X)
131
+ NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/z<000-120>.tif"
132
+
133
+ # Prefill stack voxel spacing for multi-file stacks (use a pattern or glob on CLI)
134
+ NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/img_<000-120>.tif"
118
135
  ```
119
136
 
120
137
  ## Contributing
@@ -29,7 +29,7 @@ dynamic = [ "version" ]
29
29
  dependencies = [
30
30
  "magicgui",
31
31
  "numpy",
32
- "ome-arrow>=0.0.2",
32
+ "ome-arrow>=0.0.3",
33
33
  "qtpy>=2.4",
34
34
  "scikit-image",
35
35
  ]
@@ -39,6 +39,7 @@ optional-dependencies.all = [ "napari", "qtpy>=2.4" ]
39
39
  optional-dependencies.pyqt6 = [ "napari[pyqt6]", "pyqt6>=6.6", "qtpy>=2.4" ]
40
40
  # Let users choose their Qt backend explicitly:
41
41
  optional-dependencies.pyside6 = [ "napari[pyside6]", "pyside6>=6.6", "qtpy>=2.4" ]
42
+ optional-dependencies.vortex = [ "vortex-data" ]
42
43
  urls."Bug Tracker" = "https://github.com/wayscience/napari-ome-arrow/issues"
43
44
  urls."Documentation" = "https://github.com/wayscience/napari-ome-arrow#README.md"
44
45
  urls."Source Code" = "https://github.com/wayscience/napari-ome-arrow"
@@ -58,13 +59,13 @@ dev = [
58
59
  [tool.setuptools]
59
60
  include-package-data = true
60
61
 
62
+ [tool.setuptools.package-data]
63
+ "*" = [ "*.yaml" ]
64
+
61
65
  [tool.setuptools.packages.find]
62
66
  where = [ "src" ]
63
67
  exclude = [ "*.tests", "*.tests.*", "tests*" ]
64
68
 
65
- [tool.setuptools.package-data]
66
- "*" = [ "*.yaml" ]
67
-
68
69
  [tool.setuptools_scm]
69
70
  write_to = "src/napari_ome_arrow/_version.py"
70
71
  fallback_version = "0.0.1+nogit"
@@ -0,0 +1,340 @@
1
+ """
2
+ Minimal napari reader for OME-Arrow sources (stack patterns, OME-Zarr, OME-Parquet,
3
+ OME-Vortex, OME-TIFF) plus a fallback .npy example.
4
+
5
+ Behavior:
6
+ * If NAPARI_OME_ARROW_LAYER_TYPE is set to "image" or "labels",
7
+ that choice is used.
8
+ * Otherwise, in a GUI/Qt context, the user is prompted with a modal
9
+ dialog asking whether to load as image or labels.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import warnings
16
+ from collections.abc import Callable, Sequence
17
+ from pathlib import Path
18
+
19
+ from ._reader_infer import ( # noqa: F401
20
+ _infer_layer_mode_from_record,
21
+ _infer_layer_mode_from_source,
22
+ _looks_like_ome_source,
23
+ _normalize_image_type,
24
+ )
25
+ from ._reader_napari import ( # noqa: F401
26
+ _maybe_set_viewer_3d,
27
+ _strip_channel_axis,
28
+ )
29
+ from ._reader_omearrow import ( # noqa: F401
30
+ _read_one,
31
+ _read_parquet_rows,
32
+ _read_vortex_rows,
33
+ )
34
+ from ._reader_stack import ( # noqa: F401
35
+ _channel_names_from_pattern,
36
+ _collect_stack_files,
37
+ _infer_stack_scale_from_pattern,
38
+ _parse_stack_scale,
39
+ _prompt_stack_pattern,
40
+ _prompt_stack_scale,
41
+ _read_rgb_stack_pattern,
42
+ _replace_channel_placeholder,
43
+ _stack_default_dim_for_pattern,
44
+ _suggest_stack_pattern,
45
+ )
46
+ from ._reader_types import LayerData, PathLike
47
+
48
+ __all__ = [
49
+ "napari_get_reader",
50
+ "reader_function",
51
+ "_infer_layer_mode_from_record",
52
+ "_infer_layer_mode_from_source",
53
+ "_looks_like_ome_source",
54
+ "_normalize_image_type",
55
+ "_maybe_set_viewer_3d",
56
+ "_strip_channel_axis",
57
+ "_read_one",
58
+ "_read_parquet_rows",
59
+ "_read_vortex_rows",
60
+ "_channel_names_from_pattern",
61
+ "_collect_stack_files",
62
+ "_infer_stack_scale_from_pattern",
63
+ "_parse_stack_scale",
64
+ "_prompt_stack_pattern",
65
+ "_prompt_stack_scale",
66
+ "_read_rgb_stack_pattern",
67
+ "_replace_channel_placeholder",
68
+ "_stack_default_dim_for_pattern",
69
+ "_suggest_stack_pattern",
70
+ ]
71
+
72
+ # --------------------------------------------------------------------- #
73
+ # Mode selection (env var + GUI prompt)
74
+ # --------------------------------------------------------------------- #
75
+
76
+
77
+ def _get_layer_mode(
78
+ sample_path: str, *, image_type_hint: str | None = None
79
+ ) -> str:
80
+ """Decide whether to load as "image" or "labels".
81
+
82
+ Priority:
83
+ 1. `NAPARI_OME_ARROW_LAYER_TYPE` env var (image/labels).
84
+ 2. If in a Qt GUI context, show a modal dialog asking the user.
85
+ 3. Otherwise, default to "image".
86
+
87
+ Args:
88
+ sample_path: Path used for prompt labeling.
89
+ image_type_hint: Optional inferred mode from metadata.
90
+
91
+ Returns:
92
+ The selected mode, "image" or "labels".
93
+
94
+ Raises:
95
+ RuntimeError: If the environment variable is invalid or the user
96
+ cancels the dialog.
97
+ """
98
+ # Env var override (used in headless/batch workflows).
99
+ mode = os.environ.get("NAPARI_OME_ARROW_LAYER_TYPE")
100
+ if mode is not None:
101
+ mode = mode.lower()
102
+ if mode in {"image", "labels"}:
103
+ return mode
104
+ raise RuntimeError(
105
+ f"Invalid NAPARI_OME_ARROW_LAYER_TYPE={mode!r}; expected 'image' or 'labels'."
106
+ )
107
+
108
+ # Metadata hint (best effort).
109
+ if image_type_hint in {"image", "labels"}:
110
+ return image_type_hint
111
+
112
+ # No env var → try to prompt in GUI context
113
+ try:
114
+ from qtpy import QtWidgets
115
+ except Exception:
116
+ # no Qt, probably headless: default to image
117
+ warnings.warn(
118
+ "NAPARI_OME_ARROW_LAYER_TYPE not set and Qt not available; "
119
+ "defaulting to 'image'.",
120
+ stacklevel=2,
121
+ )
122
+ return "image"
123
+
124
+ app = QtWidgets.QApplication.instance()
125
+ if app is None:
126
+ # Again, likely headless or non-Qt usage
127
+ warnings.warn(
128
+ "NAPARI_OME_ARROW_LAYER_TYPE not set and no QApplication instance; "
129
+ "defaulting to 'image'.",
130
+ stacklevel=2,
131
+ )
132
+ return "image"
133
+
134
+ # Build a simple modal choice dialog
135
+ msg = QtWidgets.QMessageBox()
136
+ msg.setWindowTitle("napari-ome-arrow: choose layer type")
137
+ msg.setText(
138
+ f"<p align='left'>How should '{Path(sample_path).name}' be loaded?<br><br>"
139
+ "You can also set NAPARI_OME_ARROW_LAYER_TYPE=image or labels to skip this prompt.</p>"
140
+ )
141
+
142
+ # Use ActionRole for ALL buttons so Qt does NOT reorder them
143
+ image_button = msg.addButton("Image", QtWidgets.QMessageBox.ActionRole)
144
+ labels_button = msg.addButton("Labels", QtWidgets.QMessageBox.ActionRole)
145
+ cancel_button = msg.addButton("Cancel", QtWidgets.QMessageBox.ActionRole)
146
+
147
+ # If you want Esc to behave as Cancel:
148
+ msg.setEscapeButton(cancel_button)
149
+
150
+ msg.exec_()
151
+ clicked = msg.clickedButton()
152
+
153
+ if clicked is labels_button:
154
+ return "labels"
155
+ if clicked is image_button:
156
+ return "image"
157
+
158
+ raise RuntimeError("User cancelled napari-ome-arrow load dialog.")
159
+
160
+
161
+ # --------------------------------------------------------------------- #
162
+ # napari entry point: napari_get_reader
163
+ # --------------------------------------------------------------------- #
164
+
165
+
166
+ def napari_get_reader(
167
+ path: PathLike | Sequence[PathLike],
168
+ ) -> Callable[[PathLike | Sequence[PathLike]], list[LayerData]] | None:
169
+ """Return a reader callable for napari if the path is supported.
170
+
171
+ Args:
172
+ path: A single path or a sequence of paths provided by napari.
173
+
174
+ Returns:
175
+ Reader callable if the path is supported, otherwise None.
176
+ """
177
+ # napari may pass a list/tuple or a single path
178
+ first = str(path[0] if isinstance(path, (list, tuple)) else path).strip()
179
+
180
+ if _looks_like_ome_source(first):
181
+ return reader_function
182
+ return None
183
+
184
+
185
+ # --------------------------------------------------------------------- #
186
+ # Reader implementation: reader_function
187
+ # --------------------------------------------------------------------- #
188
+
189
+
190
+ def reader_function(
191
+ path: PathLike | Sequence[PathLike],
192
+ ) -> list[LayerData]:
193
+ """Read one or more paths into napari layer data.
194
+
195
+ The user may be prompted (or an env var used) to decide "image" vs
196
+ "labels".
197
+
198
+ Args:
199
+ path: A single path or a sequence of paths provided by napari.
200
+
201
+ Returns:
202
+ List of layer tuples for napari.
203
+
204
+ Raises:
205
+ ValueError: If no readable inputs are found.
206
+ """
207
+ paths: list[str] = [
208
+ str(p) for p in (path if isinstance(path, (list, tuple)) else [path])
209
+ ]
210
+ layers: list[LayerData] = []
211
+
212
+ # Offer a stack pattern prompt when multiple compatible files are present.
213
+ stack_selection = _collect_stack_files(paths)
214
+ stack_pattern = None
215
+ if stack_selection is not None:
216
+ stack_pattern = _prompt_stack_pattern(*stack_selection)
217
+
218
+ # Use the original first path as context for the dialog label
219
+ mode_hint = None
220
+ if stack_pattern is not None:
221
+ stack_default_dim = _stack_default_dim_for_pattern(stack_pattern)
222
+ mode_hint = _infer_layer_mode_from_source(
223
+ stack_pattern, stack_default_dim=stack_default_dim
224
+ )
225
+ elif paths:
226
+ mode_hint = _infer_layer_mode_from_source(paths[0])
227
+ try:
228
+ mode = _get_layer_mode(
229
+ sample_path=paths[0], image_type_hint=mode_hint
230
+ ) # 'image' or 'labels'
231
+ except RuntimeError as e:
232
+ # If user canceled the dialog, propagate a clean error for napari
233
+ raise ValueError(str(e)) from e
234
+
235
+ if stack_pattern is not None:
236
+ try:
237
+ channel_names = _channel_names_from_pattern(
238
+ stack_pattern, stack_default_dim
239
+ )
240
+ resolved_stack_scale = None
241
+ if mode == "image" and channel_names and len(channel_names) > 1:
242
+ # Split each channel into separate layers for stack patterns.
243
+ inferred_stack_scale = _infer_stack_scale_from_pattern(
244
+ stack_pattern, stack_default_dim
245
+ )
246
+ env_scale = os.environ.get("NAPARI_OME_ARROW_STACK_SCALE")
247
+ if env_scale:
248
+ try:
249
+ resolved_stack_scale = _parse_stack_scale(env_scale)
250
+ except ValueError as exc:
251
+ warnings.warn(
252
+ f"Invalid NAPARI_OME_ARROW_STACK_SCALE '{env_scale}': {exc}.",
253
+ stacklevel=2,
254
+ )
255
+ resolved_stack_scale = None
256
+ if (
257
+ resolved_stack_scale is None
258
+ and inferred_stack_scale is None
259
+ ):
260
+ resolved_stack_scale = _prompt_stack_scale(
261
+ sample_path=stack_pattern, default_scale=None
262
+ )
263
+
264
+ for label in channel_names:
265
+ channel_pattern = _replace_channel_placeholder(
266
+ stack_pattern, label, stack_default_dim
267
+ )
268
+ channel_default_dim = _stack_default_dim_for_pattern(
269
+ channel_pattern
270
+ )
271
+ try:
272
+ arr, add_kwargs, layer_type = _read_one(
273
+ channel_pattern,
274
+ mode=mode,
275
+ stack_default_dim=channel_default_dim,
276
+ stack_scale_override=resolved_stack_scale,
277
+ )
278
+ arr, add_kwargs = _strip_channel_axis(arr, add_kwargs)
279
+ except Exception as exc:
280
+ try:
281
+ arr, is_rgb = _read_rgb_stack_pattern(
282
+ channel_pattern
283
+ )
284
+ except Exception:
285
+ warnings.warn(
286
+ f"Failed to read channel '{label}' from stack "
287
+ f"pattern '{stack_pattern}': {exc}",
288
+ stacklevel=2,
289
+ )
290
+ continue
291
+ add_kwargs = {"name": Path(channel_pattern).name}
292
+ if is_rgb:
293
+ add_kwargs["rgb"] = True
294
+ layer_type = "image"
295
+
296
+ add_kwargs["name"] = (
297
+ f"{add_kwargs.get('name', label)}[{label}]"
298
+ )
299
+ if not add_kwargs.get("rgb"):
300
+ _maybe_set_viewer_3d(arr)
301
+ layers.append((arr, add_kwargs, layer_type))
302
+ else:
303
+ # Single-layer stack read.
304
+ arr, add_kwargs, layer_type = _read_one(
305
+ stack_pattern,
306
+ mode=mode,
307
+ stack_default_dim=stack_default_dim,
308
+ )
309
+ layers.append((arr, add_kwargs, layer_type))
310
+ except Exception as e:
311
+ warnings.warn(
312
+ f"Failed to read stack pattern '{stack_pattern}': {e}. "
313
+ "Loading files individually instead.",
314
+ stacklevel=2,
315
+ )
316
+ else:
317
+ return layers
318
+ elif stack_selection is not None and len(paths) == 1:
319
+ paths = [str(p) for p in stack_selection[0]]
320
+
321
+ for src in paths:
322
+ try:
323
+ parquet_layers = _read_parquet_rows(src, mode)
324
+ if parquet_layers is not None:
325
+ layers.extend(parquet_layers)
326
+ continue
327
+ vortex_layers = _read_vortex_rows(src, mode)
328
+ if vortex_layers is not None:
329
+ layers.extend(vortex_layers)
330
+ continue
331
+ layers.append(_read_one(src, mode=mode))
332
+ except Exception as e:
333
+ warnings.warn(
334
+ f"Failed to read '{src}' with napari-ome-arrow: {e}",
335
+ stacklevel=2,
336
+ )
337
+
338
+ if not layers:
339
+ raise ValueError("No readable inputs found for given path(s).")
340
+ return layers