1313import glob
1414import logging
1515import os
16+ from pathlib import Path
1617from typing import List , Optional , Union
1718
1819import h5py
@@ -35,11 +36,9 @@ def _detect_format(filename: str) -> str:
3536 Returns canonical format string:
3637 'h5', 'tiff', 'png', 'nifti', 'zarr'.
3738 """
38- if ".zarr" in filename :
39- return "zarr"
4039 if filename .endswith (".nii.gz" ):
4140 return "nifti"
42- suffix = filename . rsplit ( "." , 1 )[ - 1 ]. lower () if "." in filename else ""
41+ suffix = Path ( filename ). suffix . lower (). lstrip ( "." )
4342 _SUFFIX_MAP = {
4443 "h5" : "h5" ,
4544 "hdf5" : "h5" ,
@@ -49,12 +48,22 @@ def _detect_format(filename: str) -> str:
4948 "nii" : "nifti" ,
5049 }
5150 fmt = _SUFFIX_MAP .get (suffix )
52- if fmt is None :
53- raise ValueError (
54- f"Unrecognizable file format for { filename } . "
55- f"Expected: h5, hdf5, tif, tiff, png, nii, nii.gz"
56- )
57- return fmt
51+ if fmt is not None :
52+ return fmt
53+ if ".zarr" in filename :
54+ return "zarr"
55+ raise ValueError (
56+ f"Unrecognizable file format for { filename } . "
57+ f"Expected: h5, hdf5, tif, tiff, png, nii, nii.gz, zarr"
58+ )
59+
60+
61+ def _split_zarr_path (filename : str ) -> tuple [str , Optional [str ]]:
62+ """Split a zarr path into store path and optional subkey."""
63+ zarr_idx = filename .index (".zarr" )
64+ zarr_path = filename [: zarr_idx + 5 ]
65+ sub_key = filename [zarr_idx + 5 :].strip ("/" ) or None
66+ return zarr_path , sub_key
5867
5968
6069# =============================================================================
@@ -346,10 +355,7 @@ def read_volume(
346355 elif fmt == "zarr" :
347356 import zarr
348357
349- # Path may be "dir.zarr/subkey" — split at .zarr boundary.
350- zarr_idx = filename .index (".zarr" )
351- zarr_path = filename [: zarr_idx + 5 ]
352- sub_key = filename [zarr_idx + 5 :].strip ("/" ) or None
358+ zarr_path , sub_key = _split_zarr_path (filename )
353359 store = zarr .open (zarr_path , mode = "r" )
354360 arr = store [sub_key ] if sub_key else store
355361 data = np .asarray (arr )
@@ -374,7 +380,7 @@ def save_volume(
374380 filename : str ,
375381 volume : np .ndarray ,
376382 dataset : str = "main" ,
377- file_format : str = "h5" ,
383+ file_format : Optional [ str ] = None ,
378384) -> None :
379385 """Save volumetric data in specified format.
380386
@@ -384,9 +390,27 @@ def save_volume(
384390 dataset: Dataset name for HDF5 format.
385391 file_format: 'h5', 'tiff', 'png', 'nii', 'nii.gz'.
386392 """
393+ file_format = file_format or _detect_format (filename )
394+
387395 if file_format == "h5" :
388396 write_hdf5 (filename , volume , dataset = dataset )
389397
398+ elif file_format == "zarr" :
399+ import zarr
400+
401+ zarr_path , sub_key = _split_zarr_path (filename )
402+ if sub_key :
403+ group = zarr .open_group (zarr_path , mode = "a" )
404+ group .create_dataset (sub_key , data = volume , overwrite = True )
405+ else :
406+ array = zarr .open (
407+ zarr_path ,
408+ mode = "w" ,
409+ shape = volume .shape ,
410+ dtype = volume .dtype ,
411+ )
412+ array [...] = volume
413+
390414 elif file_format in ("tif" , "tiff" ):
391415 import tifffile
392416
@@ -410,7 +434,7 @@ def save_volume(
410434
411435 else :
412436 raise ValueError (
413- f"Unsupported format: { file_format } . " f"Expected: h5, tiff, png, nii, nii.gz"
437+ f"Unsupported format: { file_format } . " f"Expected: h5, zarr, tiff, png, nii, nii.gz"
414438 )
415439
416440
@@ -436,17 +460,19 @@ def get_vol_shape(
436460 Returns shape consistent with what read_volume would
437461 produce: (D, H, W) or (C, D, H, W).
438462 """
439- if not os .path .exists (filename ):
440- raise FileNotFoundError (f"File not found: { filename } " )
441-
442463 fmt = _detect_format (filename )
443464
444465 if fmt == "zarr" :
445466 try :
446467 import zarr
447468 except ModuleNotFoundError as exc :
448469 raise ModuleNotFoundError ("zarr required. pip install zarr" ) from exc
449- obj = zarr .open (filename , mode = "r" )
470+ zarr_path , sub_key = _split_zarr_path (filename )
471+ if not os .path .exists (zarr_path ):
472+ raise FileNotFoundError (f"File not found: { zarr_path } " )
473+ obj = zarr .open (zarr_path , mode = "r" )
474+ if sub_key :
475+ return tuple (obj [sub_key ].shape )
450476 if hasattr (obj , "shape" ):
451477 return tuple (obj .shape )
452478 if dataset is not None :
@@ -456,6 +482,9 @@ def get_vol_shape(
456482 raise ValueError (f"No arrays in zarr group: { filename } " )
457483 return tuple (obj [keys [0 ]].shape )
458484
485+ if not os .path .exists (filename ):
486+ raise FileNotFoundError (f"File not found: { filename } " )
487+
459488 if fmt == "h5" :
460489 with h5py .File (filename , "r" ) as f :
461490 if dataset is None :
@@ -483,3 +512,15 @@ def get_vol_shape(
483512 return _get_nifti_shape (filename )
484513
485514 raise ValueError (f"Unsupported format: { fmt } " )
515+
516+
517+ def volume_exists (
518+ filename : str ,
519+ dataset : Optional [str ] = None ,
520+ ) -> bool :
521+ """Return True when a volume path can be opened by this IO layer."""
522+ try :
523+ get_vol_shape (filename , dataset = dataset )
524+ except (FileNotFoundError , KeyError , ValueError , OSError ):
525+ return False
526+ return True
0 commit comments