diff --git a/docs/io.md b/docs/io.md
index fdb27c4fbf1eb43f9c2c7eccfa8b88eab812a4c5..67625c30eb0489cb1ceb1be6924b23f6b6254699 100644
--- a/docs/io.md
+++ b/docs/io.md
@@ -8,4 +8,6 @@ Currently, it is possible to directly load `tiff`, `h5`, `nii`,`txm`, `vol` and
         members:
             - load
             - save
-            - Downloader
\ No newline at end of file
+            - Downloader
+            - export_ome_zarr
+            - import_ome_zarr
\ No newline at end of file
diff --git a/qim3d/io/__init__.py b/qim3d/io/__init__.py
index b9c0e64c9bc055c30f87d779bf40ea8cdd570465..5bdde84ce0830ec05f1dce89ccf3b94cef8e8c7b 100644
--- a/qim3d/io/__init__.py
+++ b/qim3d/io/__init__.py
@@ -4,3 +4,4 @@ from .saving import DataSaver, save
 from .sync import Sync
 from .convert import convert
 from ..utils import logger
+from .ome_zarr import export_ome_zarr, import_ome_zarr
diff --git a/qim3d/io/ome_zarr.py b/qim3d/io/ome_zarr.py
new file mode 100644
index 0000000000000000000000000000000000000000..484cb4920e57903457d7abb93e798e75622f3e5b
--- /dev/null
+++ b/qim3d/io/ome_zarr.py
@@ -0,0 +1,172 @@
+"""
+Exporting data to different formats.
+"""
+
+import os
+import numpy as np
+import zarr
+from ome_zarr.io import parse_url
+from ome_zarr.writer import write_image
+from ome_zarr.reader import Reader
+from ome_zarr import scale
+import math
+import shutil
+from qim3d.utils.logger import log
+from scipy.ndimage import zoom
+
+
+class OMEScaler(
+    scale.Scaler,
+):
+    """Scaler in the style of OME-Zarr.
+    This is needed because their current zoom implementation is broken."""
+
+    def __init__(self, order=0, downscale=2, max_layer=5, method="scaleZYX"):
+        self.order = order
+        self.downscale = downscale
+        self.max_layer = max_layer
+        self.method = method
+
+    def scaleZYX(self, base):
+        """Downsample using :func:`scipy.ndimage.zoom`."""
+        rv = [base]
+        log.info(f"- Scale 0: {rv[-1].shape}")
+
+        for i in range(self.max_layer):
+            rv.append(zoom(base, zoom=1 / (self.downscale * (i + 1)), order=self.order))
+            log.info(f"- Scale {i+1}: {rv[-1].shape}")
+        return list(rv)
+
+
+def export_ome_zarr(
+    path, data, chunk_size=100, downsample_rate=2, order=0, replace=False
+):
+    """
+    Export image data to OME-Zarr format with pyramidal downsampling.
+
+    Automatically calculates the number of downsampled scales such that the smallest scale fits within the specified `chunk_size`. 
+    
+    Args:
+        path (str): The directory where the OME-Zarr data will be stored.
+        data (np.ndarray): The image data to be exported.
+        chunk_size (int, optional): The size of the chunks for storing data. Defaults to 100.
+        downsample_rate (int, optional): Factor by which to downsample the data for each scale. Must be greater than 1. Defaults to 2.
+        order (int, optional): Interpolation order to use when downsampling. Defaults to 0 (nearest-neighbor).
+        replace (bool, optional): Whether to replace the existing directory if it already exists. Defaults to False.
+
+    Raises:
+        ValueError: If the directory already exists and `replace` is False.
+        ValueError: If `downsample_rate` is less than or equal to 1.
+
+    Example:
+        ```python
+        import qim3d
+
+        downloader = qim3d.io.Downloader()
+        data = downloader.Snail.Escargot(load_file=True)
+
+        qim3d.io.export_ome_zarr("Escargot.zarr", data, chunk_size=100, downsample_rate=2)
+
+        ```
+
+    """
+
+    # Check if directory exists
+    if os.path.exists(path):
+        if replace:
+            shutil.rmtree(path)
+        else:
+            raise ValueError(
+                f"Directory {path} already exists. Use replace=True to overwrite."
+            )
+
+    # Check if downsample_rate is valid
+    if downsample_rate <= 1:
+        raise ValueError("Downsample rate must be greater than 1.")
+
+    log.info(f"Exporting data to OME-Zarr format at {path}")
+
+    # Get the number of scales
+    min_dim = np.max(np.shape(data))
+    nscales = math.ceil(math.log(min_dim / chunk_size) / math.log(downsample_rate)) + 1
+    log.info(f"Number of scales: {nscales + 1}")
+
+    # Create scaler
+    scaler = OMEScaler(
+        downscale=downsample_rate, max_layer=nscales, method="scaleZYX", order=order
+    )
+
+    # write the image data
+    os.mkdir(path)
+    store = parse_url(path, mode="w").store
+    root = zarr.group(store=store)
+    write_image(
+        image=data,
+        group=root,
+        axes="zyx",
+        storage_options=dict(chunks=(chunk_size, chunk_size, chunk_size)),
+        scaler=scaler,
+    )
+
+
+def import_ome_zarr(path, scale=0, load=True):
+    """
+    Import image data from an OME-Zarr file.
+
+    This function reads OME-Zarr formatted volumetric image data and returns the specified scale. 
+    The image data can be lazily loaded (as Dask arrays) or fully computed into memory.
+
+    Args:
+        path (str): The file path to the OME-Zarr data.
+        scale (int or str, optional): The scale level to load. 
+            If 'highest', loads the finest scale (scale 0). 
+            If 'lowest', loads the coarsest scale (last available scale). Defaults to 0.
+        load (bool, optional): Whether to compute the selected scale into memory. 
+            If False, returns a lazy Dask array. Defaults to True.
+
+    Returns:
+        np.ndarray or dask.array.Array: The requested image data, either as a NumPy array if `load=True`, 
+        or a Dask array if `load=False`.
+
+    Raises:
+        ValueError: If the requested `scale` does not exist in the data.
+
+    Example:
+        ```python
+        import qim3d
+
+        data = qim3d.io.import_ome_zarr("Escargot.zarr", scale=0, load=True)
+
+        ```
+
+    """
+
+    # read the image data
+    #store = parse_url(path, mode="r").store
+
+    reader = Reader(parse_url(path))
+    nodes = list(reader())
+    image_node = nodes[0]
+    dask_data = image_node.data
+
+    log.info(f"Data contains {len(dask_data)} scales:")
+    for i in np.arange(len(dask_data)):
+        log.info(f"- Scale {i}: {dask_data[i].shape}")
+
+    if scale == "highest":
+        scale = 0
+
+    if scale == "lowest":
+        scale = len(dask_data) - 1
+
+    if scale >= len(dask_data):
+        raise ValueError(f"Scale {scale} does not exist in the data. Please choose a scale between 0 and {len(dask_data)-1}.")
+
+    log.info(f"\nLoading scale {scale} with shape {dask_data[scale].shape}")
+
+    if load:
+        vol = dask_data[scale].compute()
+    else:
+        vol = dask_data[scale]
+
+    return vol
diff --git a/qim3d/utils/misc.py b/qim3d/utils/misc.py
index 1c542a3ed47b8aa157afaa283fae0bf841a70bf9..a754ecb8840b889eed2b3132a98c1c8ea1e9f1f5 100644
--- a/qim3d/utils/misc.py
+++ b/qim3d/utils/misc.py
@@ -223,7 +223,7 @@ def downscale_img(img, max_voxels=512**3):
     zoom_factor = (max_voxels / total_voxels) ** (1 / 3)
 
     # Downscale image
-    return zoom(img, zoom_factor)
+    return zoom(img, zoom_factor, order=0)
 
 
 def scale_to_float16(arr: np.ndarray):
diff --git a/qim3d/viz/k3d.py b/qim3d/viz/k3d.py
index 767d84dfeed699a2d75f9fd3058fd00fab9ca46a..8f82c18bd9e57cee7a9a7b505213210934400ccc 100644
--- a/qim3d/viz/k3d.py
+++ b/qim3d/viz/k3d.py
@@ -22,7 +22,7 @@ def vol(
     grid_visible=False,
     cmap=None,
     samples="auto",
-    max_voxels=412**3,
+    max_voxels=512**3,
     data_type="scaled_float16",
     **kwargs,
 ):
@@ -97,7 +97,7 @@ def vol(
 
     if original_shape != new_shape:
         log.warning(
-            f"Downsampled image for visualization. From {original_shape} to {new_shape}"
+            f"Downsampled image for visualization, from {original_shape} to {new_shape}"
         )
 
     # Scale the image to float16 if needed