3using ZarrNET.Core.OmeZarr.Metadata;
10 internal static class ZarrExporter
12 internal static async Task SaveAsync(BioImage image,
string destination)
14 if (File.Exists(destination) || Directory.Exists(destination))
15 throw new IOException(
"The destination already exists. Choose a new folder name.");
16 if (image.isPyramidal || image.IsZarrSource)
17 throw new NotSupportedException(
"Saving pyramidal or Zarr-backed images is not yet supported. Save a fully loaded image instead.");
18 if (image.Resolutions.Count == 0 || image.SizeZ <= 0 || image.SizeC <= 0 || image.SizeT <= 0)
19 throw new InvalidOperationException(
"The image dimensions are incomplete.");
21 var resolution = image.Resolutions[0];
22 var format = resolution.PixelFormat;
23 int bytesPerSample = format
switch
25 PixelFormat.Format8bppIndexed or PixelFormat.Format24bppRgb => 1,
26 PixelFormat.Format16bppGrayScale or PixelFormat.Format48bppRgb => 2,
27 _ =>
throw new NotSupportedException($
"The pixel format {format} is not supported for OME-Zarr export.")
29 bool rgb = format == PixelFormat.Format24bppRgb || format ==
PixelFormat.Format48bppRgb;
30 if (rgb && image.SizeC != 1)
31 throw new NotSupportedException(
"Interleaved RGB images with multiple logical channels are not supported.");
32 int width = resolution.SizeX, height = resolution.SizeY;
33 if (width <= 0 || height <= 0)
34 throw new InvalidOperationException(
"The image has invalid dimensions.");
35 int outputChannels = rgb ? 3 : image.SizeC;
36 const int chunk = 512;
37 var descriptor =
new BioImageDescriptor(width, height,
new ZarrNET.Core.ZCT(image.SizeZ, outputChannels, image.SizeT))
39 Name = Path.GetFileNameWithoutExtension(image.filename),
40 DataType = bytesPerSample == 1 ?
"uint8" :
"uint16",
41 PhysicalSizeX = image.PhysicalSizeX,
42 PhysicalSizeY = image.PhysicalSizeY,
43 PhysicalSizeZ = image.PhysicalSizeZ,
48 string fullPath = Path.GetFullPath(destination);
49 string temporary = fullPath +
".partial-" + Guid.NewGuid().ToString(
"N");
52 var levels =
new List<ResolutionLevelDescriptor> {
new(width, height, 1.0) };
53 await
using (var writer = await OmeZarrWriter.CreateAsync(temporary, descriptor, levels).ConfigureAwait(
false))
55 for (
int t = 0; t < image.SizeT; t++)
56 for (
int z = 0; z < image.SizeZ; z++)
57 for (
int c = 0; c < outputChannels; c++)
59 int index = image.GetFrameIndex(z, rgb ? 0 : c, t);
60 if (index < 0 || index >= image.Buffers.Count || image.Buffers[index] ==
null)
61 throw new InvalidOperationException($
"Image plane Z{z} C{c} T{t} is missing.");
62 var buffer = image.Buffers[index];
63 if (buffer.SizeX != width || buffer.SizeY != height || buffer.PixelFormat != format || buffer.Bytes ==
null)
64 throw new InvalidOperationException(
"The image does not contain complete, matching pixel planes.");
65 int components = rgb ? 3 : 1;
66 int rowBytes = checked(width * components * bytesPerSample);
67 if (buffer.Stride < rowBytes || buffer.Bytes.Length < checked(buffer.Stride * height))
68 throw new InvalidOperationException(
"An image plane has an invalid pixel buffer.");
69 for (
int y = 0; y < height; y += chunk)
70 for (
int x = 0; x < width; x += chunk)
72 int w = Math.Min(chunk, width - x), h = Math.Min(chunk, height - y);
73 var tile =
new byte[checked(w * h * bytesPerSample)];
74 for (
int row = 0; row < h; row++)
75 for (
int col = 0; col < w; col++)
78 int component = rgb ? 2 - c : 0;
79 int src = (y + row) * buffer.Stride + ((x + col) * components + component) * bytesPerSample;
80 int dst = (row * w + col) * bytesPerSample;
81 if (bytesPerSample == 2 && !buffer.LittleEndian)
83 tile[dst] = buffer.Bytes[src + 1];
84 tile[dst + 1] = buffer.Bytes[src];
87 Buffer.BlockCopy(buffer.Bytes, src, tile, dst, bytesPerSample);
89 await writer.WriteRegionAsync(t, c, z, y, x, h, w, tile, levelIndex: 0).ConfigureAwait(
false);
93 if (image.Annotations.Count > 0)
94 Zarr.SaveROIs(image, temporary);
97 for (
int attempt = 0; ; attempt++)
101 Directory.Move(temporary, fullPath);
104 catch (IOException) when (attempt < 3 && !File.Exists(fullPath) && !Directory.Exists(fullPath))
106 await Task.Delay(100 * (attempt + 1)).ConfigureAwait(
false);
112 if (Directory.Exists(temporary))
113 Directory.Delete(temporary,
true);
OpenTK.Graphics.OpenGL.PixelFormat PixelFormat