|
5 | 5 | from iohub import __version__, open_ome_zarr |
6 | 6 | from iohub.cli.parsing import input_position_dirpaths |
7 | 7 | from iohub.convert import TIFFConverter |
8 | | -from iohub.core.ozx import pack_ozx, summarize_ozx |
| 8 | +from iohub.core.ozx import is_ozx_path, pack_ozx, unpack_ozx |
9 | 9 | from iohub.reader import print_info |
10 | 10 | from iohub.rename_wells import rename_wells |
11 | 11 |
|
@@ -55,49 +55,65 @@ def info(files, verbose): |
55 | 55 | "--input", |
56 | 56 | "-i", |
57 | 57 | required=True, |
58 | | - type=_DATASET_PATH, |
59 | | - help="Input Micro-Manager TIFF dataset directory", |
| 58 | + type=_OME_ZARR_PATH, |
| 59 | + help="Input dataset: Micro-Manager TIFF dir, OME-Zarr dir, or RFC-9 .ozx archive.", |
60 | 60 | ) |
61 | 61 | @click.option( |
62 | 62 | "--output", |
63 | 63 | "-o", |
64 | 64 | required=True, |
65 | | - type=click.Path(exists=False, resolve_path=True), |
66 | | - help="Output zarr store (/**/converted.zarr)", |
| 65 | + type=click.Path(exists=False, resolve_path=True, path_type=pathlib.Path), |
| 66 | + help="Output path. Suffix selects the operation: .zarr (Zarr dir) or .ozx (zipped).", |
67 | 67 | ) |
68 | 68 | @click.option( |
69 | 69 | "--grid-layout", |
70 | 70 | "-g", |
71 | 71 | required=False, |
72 | 72 | is_flag=True, |
73 | | - help="Arrange FOVs in a row/column grid layout for tiled acquisition", |
| 73 | + help="(TIFF → Zarr only) Arrange FOVs in a row/column grid layout.", |
74 | 74 | ) |
75 | 75 | @click.option( |
76 | 76 | "--chunks", |
77 | 77 | "-c", |
78 | 78 | required=False, |
79 | 79 | default="XYZ", |
80 | | - help="Zarr chunk size given as 'XY', 'XYZ', or a tuple of chunk " |
81 | | - "dimensions. If 'XYZ', chunk size will be limited to 500 MB.", |
| 80 | + help="(TIFF → Zarr only) Zarr chunk size: 'XY', 'XYZ', or a tuple. 'XYZ' caps at 500 MB.", |
82 | 81 | ) |
83 | 82 | @click.option( |
84 | 83 | "--ome-zarr-version", |
85 | 84 | "-v", |
86 | 85 | required=False, |
87 | | - default="0.4", |
| 86 | + default=None, |
88 | 87 | type=click.Choice(["0.4", "0.5"]), |
89 | | - help="OME-NGFF version for the output Zarr store. '0.4' uses Zarr v2 format; '0.5' uses Zarr v3 format.", |
| 88 | + help="OME-NGFF version. TIFF default: 0.4. Pack: sniffed from source if omitted.", |
90 | 89 | ) |
91 | 90 | def convert(input, output, grid_layout, chunks, ome_zarr_version): |
92 | | - """Converts Micro-Manager TIFF datasets to OME-Zarr""" |
93 | | - converter = TIFFConverter( |
94 | | - input_dir=input, |
95 | | - output_dir=output, |
| 91 | + """Convert datasets between supported formats. |
| 92 | +
|
| 93 | + Routes by suffix: a TIFF directory plus a ``.zarr`` output runs the |
| 94 | + Micro-Manager TIFF → OME-Zarr converter; a ``.zarr`` source plus a |
| 95 | + ``.ozx`` output packs an RFC-9 zip archive; a ``.ozx`` source plus |
| 96 | + a ``.zarr`` output unpacks back to a directory store. |
| 97 | + """ |
| 98 | + src = pathlib.Path(input) |
| 99 | + dst = pathlib.Path(output) |
| 100 | + |
| 101 | + if is_ozx_path(dst): |
| 102 | + out = pack_ozx(src, dst, version=ome_zarr_version) |
| 103 | + click.echo(f"packed: {out}") |
| 104 | + return |
| 105 | + if is_ozx_path(src): |
| 106 | + out = unpack_ozx(src, dst) |
| 107 | + click.echo(f"unpacked: {out}") |
| 108 | + return |
| 109 | + # Default: TIFF → OME-Zarr (TIFFConverter sniffs the input format). |
| 110 | + TIFFConverter( |
| 111 | + input_dir=src, |
| 112 | + output_dir=dst, |
96 | 113 | grid_layout=grid_layout, |
97 | 114 | chunks=chunks, |
98 | | - version=ome_zarr_version, |
99 | | - ) |
100 | | - converter() |
| 115 | + version=ome_zarr_version or "0.4", |
| 116 | + )() |
101 | 117 |
|
102 | 118 |
|
103 | 119 | @cli.command() |
@@ -198,91 +214,3 @@ def rename_wells_command(zarrfile, csvfile): |
198 | 214 | ``` |
199 | 215 | """ |
200 | 216 | rename_wells(zarrfile, csvfile) |
201 | | - |
202 | | - |
203 | | -def _echo_ozx_summary(verb: str, path: pathlib.Path) -> None: |
204 | | - """Print the one-liner ``ozx pack`` shows on success.""" |
205 | | - s = summarize_ozx(path) |
206 | | - click.echo(f"{verb}: {path} (ome version={s.version}, jsonFirst={s.json_first})") |
207 | | - |
208 | | - |
209 | | -@cli.group(name="ozx") |
210 | | -@click.help_option("-h", "--help") |
211 | | -def ozx_group(): |
212 | | - """RFC-9 zipped OME-Zarr (``.ozx``) operations. |
213 | | -
|
214 | | - A ``.ozx`` is a single-file OME-Zarr archive (ZIP_STORED + ZIP64) |
215 | | - designed for distribution - S3, AWS Open Data, HPC↔compute-cluster |
216 | | - transfers - not as a live mutable store. One file to upload, |
217 | | - download, and serve. |
218 | | -
|
219 | | - Use ``ozx pack`` to bundle a directory store for export and |
220 | | - ``ozx info`` to inspect an archive's RFC-9 properties. ``iohub |
221 | | - info`` works on ``.ozx`` paths directly for the usual FOV summary. |
222 | | - """ |
223 | | - |
224 | | - |
225 | | -@ozx_group.command(name="pack") |
226 | | -@click.help_option("-h", "--help") |
227 | | -@click.option( |
228 | | - "-i", |
229 | | - "--input", |
230 | | - "src", |
231 | | - required=True, |
232 | | - type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True, path_type=pathlib.Path), |
233 | | - help="Input OME-Zarr directory store.", |
234 | | -) |
235 | | -@click.option( |
236 | | - "-o", |
237 | | - "--output", |
238 | | - "dst", |
239 | | - required=True, |
240 | | - type=click.Path(exists=False, resolve_path=True, path_type=pathlib.Path), |
241 | | - help="Output .ozx path (must not exist).", |
242 | | -) |
243 | | -@click.option( |
244 | | - "--version", |
245 | | - "ome_version", |
246 | | - default=None, |
247 | | - help="OME-NGFF version to record. Sniffed from source if omitted.", |
248 | | -) |
249 | | -def ozx_pack(src, dst, ome_version): |
250 | | - """Pack an OME-Zarr directory into an RFC-9 ``.ozx`` archive. |
251 | | -
|
252 | | - Writes a ZIP_STORED + ZIP64 archive in one pass with entries in BFS |
253 | | - order - root ``zarr.json`` first, other ``zarr.json`` next, chunks |
254 | | - last - and ``jsonFirst:true`` in the archive comment. HTTP-range |
255 | | - readers can then fetch all metadata in one contiguous Range request |
256 | | - on cold opens, the win that matters for S3 and AWS Open Data. |
257 | | - """ |
258 | | - out = pack_ozx(src, dst, version=ome_version) |
259 | | - _echo_ozx_summary("packed", out) |
260 | | - |
261 | | - |
262 | | -@ozx_group.command(name="info") |
263 | | -@click.help_option("-h", "--help") |
264 | | -@click.argument( |
265 | | - "path", |
266 | | - type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=pathlib.Path), |
267 | | -) |
268 | | -def ozx_info(path): |
269 | | - """Print RFC-9 archive metadata: version, ``jsonFirst``, entry count, size. |
270 | | -
|
271 | | - Use it to sanity-check an archive before publishing (``jsonFirst`` |
272 | | - should be ``True`` after ``iohub ozx pack``) or to inspect archives |
273 | | - received from collaborators or downloaded from public datasets. |
274 | | - """ |
275 | | - import zipfile |
276 | | - |
277 | | - # One zip open for the namelist, one for the comment. |
278 | | - with zipfile.ZipFile(path) as zf: |
279 | | - names = zf.namelist() |
280 | | - summary = summarize_ozx(path) |
281 | | - n_entries = len(names) |
282 | | - n_meta = sum(1 for n in names if n.rsplit("/", 1)[-1] == "zarr.json") |
283 | | - n_dupes = n_entries - len(set(names)) |
284 | | - click.echo(f"path: {path}") |
285 | | - click.echo(f"size: {path.stat().st_size:,} bytes") |
286 | | - click.echo(f"entries: {n_entries:,} ({n_meta} zarr.json, {n_dupes} duplicate names)") |
287 | | - click.echo(f"ome version: {summary.version}") |
288 | | - click.echo(f"jsonFirst: {summary.json_first}") |
0 commit comments