22
33from collections .abc import Sequence
44from pathlib import Path
5- from typing import Any , Literal , TypeGuard
5+ from typing import Any , Literal , TypeGuard , cast
66
77import dask .array as da
88import numpy as np
@@ -265,6 +265,7 @@ def _write_raster(
265265 name : str ,
266266 raster_format : RasterFormatType ,
267267 storage_options : JSONDict | list [JSONDict ] | None = None ,
268+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None = None ,
268269 label_metadata : JSONDict | None = None ,
269270 ** metadata : str | JSONDict | list [JSONDict ],
270271) -> None :
@@ -284,6 +285,8 @@ def _write_raster(
284285 The format used to write the raster data.
285286 storage_options
286287 Additional options for writing the raster data, like chunks and compression.
288+ raster_compressor
289+ Compression settings as a len-1 dictionary with a single key-value {compression: compression level} pair
287290 label_metadata
288291 Label metadata which can only be defined when writing 'labels'.
289292 metadata
@@ -313,6 +316,7 @@ def _write_raster(
313316 raster_data ,
314317 raster_format ,
315318 storage_options ,
319+ raster_compressor = raster_compressor ,
316320 ** metadata ,
317321 )
318322 elif isinstance (raster_data , DataTree ):
@@ -323,6 +327,7 @@ def _write_raster(
323327 raster_data ,
324328 raster_format ,
325329 storage_options ,
330+ raster_compressor = raster_compressor ,
326331 ** metadata ,
327332 )
328333 else :
@@ -337,13 +342,93 @@ def _write_raster(
337342 group .attrs [ATTRS_KEY ] = attrs
338343
339344
345+ def _build_v3_codec (
346+ compression : Literal ["lz4" , "zstd" ],
347+ compression_level : int ,
348+ ) -> Any :
349+ """Return the appropriate zarr v3 codec for the given compression type and level."""
350+ if compression == "zstd" :
351+ from zarr .codecs import ZstdCodec
352+
353+ return ZstdCodec (level = compression_level )
354+ # lz4: use the native zarr v3 BloscCodec
355+ from zarr .codecs import BloscCodec
356+
357+ return BloscCodec (cname = "lz4" , clevel = compression_level )
358+
359+
360+ def _apply_compression (
361+ storage_options : JSONDict | list [JSONDict ],
362+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None ,
363+ zarr_format : Literal [2 , 3 ] = 3 ,
364+ ) -> JSONDict | list [JSONDict ]:
365+ """Apply compression settings to storage options.
366+
367+ Parameters
368+ ----------
369+ storage_options
370+ Storage options for zarr arrays
371+ raster_compressor
372+ Compression settings as a dictionary with a single key-value pair
373+ zarr_format
374+ The zarr format version (2 or 3)
375+
376+ Returns
377+ -------
378+ Updated storage options with compression settings
379+ """
380+ if not raster_compressor :
381+ return storage_options
382+
383+ ((compression , compression_level ),) = raster_compressor .items ()
384+
385+ if zarr_format == 2 :
386+ from numcodecs import Blosc as BloscV2
387+
388+ codec_v2 = BloscV2 (cname = compression , clevel = compression_level , shuffle = 1 )
389+
390+ def _update_dict (d : dict [str , Any ]) -> None :
391+ d ["compressor" ] = codec_v2
392+
393+ if isinstance (storage_options , dict ):
394+ _update_dict (d = storage_options )
395+ elif isinstance (storage_options , list ):
396+ for option in storage_options :
397+ _update_dict (d = option )
398+ elif storage_options is None :
399+ return {"compressor" : codec_v2 }
400+ else :
401+ raise ValueError (f"storage_options must be a dict or list, not { type (storage_options )} " )
402+ else :
403+ # zarr v3: use native codec objects via the "compressors" (plural) key.
404+ # see https://github.com/ome/ome-zarr-py/blob/v0.16.0/ome_zarr/writer.py#L754
405+ # ome-zarr-py ≥ 0.16.0 with dask ≥ 2026.3.0 forwards this key to zarr_array_kwargs.
406+ codec_v3 = _build_v3_codec (compression , compression_level )
407+
408+ def _update_dict_v3 (d : dict [str , Any ]) -> None :
409+ d ["compressors" ] = [codec_v3 ]
410+
411+ if isinstance (storage_options , dict ):
412+ _update_dict_v3 (d = storage_options )
413+ elif isinstance (storage_options , list ):
414+ for option in storage_options :
415+ _update_dict_v3 (d = option )
416+ elif storage_options is None :
417+ return {"compressors" : [codec_v3 ]}
418+ else :
419+ raise ValueError (f"storage_options must be a dict or list, not { type (storage_options )} " )
420+
421+ return storage_options
422+
423+
340424def _write_raster_dataarray (
341425 raster_type : Literal ["image" , "labels" ],
342426 group : zarr .Group ,
343427 element_name : str ,
344428 raster_data : DataArray ,
345429 raster_format : RasterFormatType ,
346- storage_options : JSONDict | list [JSONDict ] | None = None ,
430+ storage_options : JSONDict | list [JSONDict ] | None ,
431+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None ,
347432 ** metadata : str | JSONDict | list [JSONDict ],
348433) -> None :
349434 """Write raster data of type DataArray to disk.
@@ -362,6 +447,8 @@ def _write_raster_dataarray(
362447 The format used to write the raster data.
363448 storage_options
364449 Additional options for writing the raster data, like chunks and compression.
450+ raster_compressor
451+ Compression settings as a len-1 dictionary with a single key-value {compression: compression level} pair
365452 metadata
366453 Additional metadata for the raster element
367454 """
@@ -373,6 +460,11 @@ def _write_raster_dataarray(
373460 input_axes : tuple [str , ...] = tuple (raster_data .dims )
374461 parsed_axes = _get_valid_axes (axes = list (input_axes ), fmt = raster_format )
375462 storage_options = _prepare_storage_options (storage_options )
463+ # Apply compression if specified
464+ storage_options = _apply_compression (
465+ storage_options , raster_compressor , zarr_format = cast (Literal [2 , 3 ], raster_format .zarr_format )
466+ )
467+
376468 # Explicitly disable pyramid generation for single-scale rasters. Recent ome-zarr versions default
377469 # write_image()/write_labels() to scale_factors=(2, 4, 8, 16), which would otherwise write s0, s1, ...
378470 # even when the input is a plain DataArray.
@@ -406,7 +498,8 @@ def _write_raster_datatree(
406498 element_name : str ,
407499 raster_data : DataTree ,
408500 raster_format : RasterFormatType ,
409- storage_options : JSONDict | list [JSONDict ] | None = None ,
501+ storage_options : JSONDict | list [JSONDict ] | None ,
502+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None ,
410503 ** metadata : str | JSONDict | list [JSONDict ],
411504) -> zarr .Group :
412505 """Write raster data of type DataTree to disk.
@@ -425,6 +518,8 @@ def _write_raster_datatree(
425518 The format used to write the raster data.
426519 storage_options
427520 Additional options for writing the raster data, like chunks and compression.
521+ raster_compressor
522+ Compression settings as a len-1 dictionary with a single key-value {compression: compression level} pair
428523 metadata
429524 Additional metadata for the raster element
430525 """
@@ -442,6 +537,10 @@ def _write_raster_datatree(
442537
443538 parsed_axes = _get_valid_axes (axes = list (input_axes ), fmt = raster_format )
444539 storage_options = _prepare_storage_options (storage_options )
540+
541+ # Apply compression if specified
542+ storage_options = _apply_compression (storage_options , raster_compressor , zarr_format = raster_format .zarr_format )
543+
445544 ome_zarr_format = get_ome_zarr_format (raster_format )
446545 dask_delayed = write_multi_scale_ngff (
447546 pyramid = data ,
@@ -483,6 +582,7 @@ def write_image(
483582 name : str ,
484583 element_format : RasterFormatType = CurrentRasterFormat (),
485584 storage_options : JSONDict | list [JSONDict ] | None = None ,
585+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None = None ,
486586 ** metadata : str | JSONDict | list [JSONDict ],
487587) -> None :
488588 _write_raster (
@@ -492,6 +592,7 @@ def write_image(
492592 name = name ,
493593 raster_format = element_format ,
494594 storage_options = storage_options ,
595+ raster_compressor = raster_compressor ,
495596 ** metadata ,
496597 )
497598
@@ -503,6 +604,7 @@ def write_labels(
503604 element_format : RasterFormatType = CurrentRasterFormat (),
504605 storage_options : JSONDict | list [JSONDict ] | None = None ,
505606 label_metadata : JSONDict | None = None ,
607+ raster_compressor : dict [Literal ["lz4" , "zstd" ], int ] | None = None ,
506608 ** metadata : JSONDict ,
507609) -> None :
508610 _write_raster (
@@ -512,6 +614,7 @@ def write_labels(
512614 name = name ,
513615 raster_format = element_format ,
514616 storage_options = storage_options ,
617+ raster_compressor = raster_compressor ,
515618 label_metadata = label_metadata ,
516619 ** metadata ,
517620 )
0 commit comments