Skip to content

Commit 53b10ac

Browse files
committed
feat: Added RFC-9 Zipped OME-Zarr
Signed-off-by: Sricharan Reddy Varra <sricharan.varra@biohub.org>
1 parent efff544 commit 53b10ac

6 files changed

Lines changed: 573 additions & 12 deletions

File tree

src/iohub/cli/cli.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
from iohub import __version__, open_ome_zarr
66
from iohub.cli.parsing import input_position_dirpaths
77
from iohub.convert import TIFFConverter
8+
from iohub.core.ozx import pack_ozx, summarize_ozx
89
from iohub.reader import print_info
910
from iohub.rename_wells import rename_wells
1011

1112
VERSION = __version__
1213

1314
_DATASET_PATH = click.Path(exists=True, file_okay=False, resolve_path=True, path_type=pathlib.Path)
15+
# Like _DATASET_PATH but also accepts files (e.g. ``.ozx`` archives).
16+
_OME_ZARR_PATH = click.Path(exists=True, resolve_path=True, path_type=pathlib.Path)
1417

1518

1619
@click.group()
@@ -26,7 +29,7 @@ def cli():
2629
"files",
2730
nargs=-1,
2831
required=True,
29-
type=_DATASET_PATH,
32+
type=_OME_ZARR_PATH,
3033
)
3134
@click.option(
3235
"--verbose",
@@ -35,11 +38,11 @@ def cli():
3538
help="Show usage guide to open dataset in Python and full tree for HCS Plates in OME-Zarr",
3639
)
3740
def info(files, verbose):
38-
"""View basic metadata of a list of FILES.
41+
"""View metadata for one or more FILES.
3942
40-
Supported formats are Micro-Manager-acquired TIFF datasets
41-
(single-page TIFF, multi-page OME-TIFF, NDTIFF)
42-
and OME-Zarr (v0.1 linear HCS layout and all v0.4 layouts).
43+
Supports Micro-Manager TIFF datasets (multi-page OME-TIFF, NDTIFF),
44+
OME-Zarr directory stores (v0.4 and v0.5), and RFC-9 zipped
45+
OME-Zarr archives (``.ozx``).
4346
"""
4447
for file in files:
4548
click.echo(f"Reading file:\t {file}")
@@ -195,3 +198,91 @@ def rename_wells_command(zarrfile, csvfile):
195198
```
196199
"""
197200
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}")

src/iohub/core/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
PathNormalizationError,
1616
StoreOpenError,
1717
)
18+
from iohub.core.ozx import (
19+
OZX_EXTENSION,
20+
OzxStore,
21+
OzxSummary,
22+
is_ozx_path,
23+
pack_ozx,
24+
read_ozx_comment,
25+
read_ozx_json_first,
26+
read_ozx_version,
27+
summarize_ozx,
28+
write_ozx_comment,
29+
)
1830
from iohub.core.protocol import (
1931
ArrayBackend,
2032
ArrayIO,
@@ -37,6 +49,7 @@
3749
from iohub.core.utils import normalize_path, pad_shape
3850

3951
__all__ = [
52+
"OZX_EXTENSION",
4053
# Types
4154
"AccessMode",
4255
"ArrayBackend",
@@ -55,6 +68,9 @@
5568
# Arrays
5669
"NGFFArray",
5770
"NGFFVersion",
71+
# OZX (RFC-9 zipped OME-Zarr)
72+
"OzxStore",
73+
"OzxSummary",
5874
"PathNormalizationError",
5975
"StoreOpenError",
6076
"StorePath",
@@ -65,12 +81,19 @@
6581
# Registry
6682
"available_implementations",
6783
"get_implementation",
84+
"is_ozx_path",
6885
"ngff_version_for_format",
6986
# Utils
7087
"normalize_path",
88+
"pack_ozx",
7189
"pad_shape",
90+
"read_ozx_comment",
91+
"read_ozx_json_first",
92+
"read_ozx_version",
7293
"register_implementation",
7394
"set_default_implementation",
95+
"summarize_ozx",
96+
"write_ozx_comment",
7497
# Compat
7598
"zarr_format_for_version",
7699
]

0 commit comments

Comments
 (0)