server, String path) {
+ if (!path.endsWith(FILE_EXTENSION)) {
+ throw new IllegalArgumentException(String.format("The provided path (%s) does not have the OME-Zarr extension (%s)", path, FILE_EXTENSION));
+ }
+
+ this.server = server;
+ this.path = path;
+ }
+
+ /**
+ * Set the compressor to use when writing tiles. By default, the blocs compression is used.
+ *
+ * @param compressor the compressor to use when writing tiles
+ * @return this builder
+ */
+ public Builder setCompressor(Compressor compressor) {
+ this.compressor = compressor;
+ return this;
+ }
+
+ /**
+ * Tiles will be written from a pool of thread. This function
+ * specifies the number of threads to use. By default, 12 threads are
+ * used.
+ *
+ * @param numberOfThreads the number of threads to use when writing tiles
+ * @return this builder
+ */
+ public Builder setNumberOfThreads(int numberOfThreads) {
+ this.numberOfThreads = numberOfThreads;
+ return this;
+ }
+
+ /**
+ *
+ * Enable the creation of a pyramidal image with the provided downsamples. The levels corresponding
+ * to the provided downsamples will be automatically generated.
+ *
+ *
+ * If this function is not called (or if it is called with no parameters), the downsamples of
+ * the provided image server will be used instead.
+ *
+ *
+ * @param downsamples the downsamples of the pyramid to generate
+ * @return this builder
+ */
+ public Builder setDownsamples(double... downsamples) {
+ this.downsamples = downsamples;
+ return this;
+ }
+
+ /**
+ *
+ * In Zarr files, data is stored in chunks. This parameter defines the maximum number
+ * of chunks on the x,y, and z dimensions. By default, this value is set to 50.
+ *
+ *
+ * Use a negative value to not define any maximum number of chunks.
+ *
+ *
+ * @param maxNumberOfChunks the maximum number of chunks on the x,y, and z dimensions
+ * @return this builder
+ */
+ public Builder setMaxNumberOfChunksOnEachSpatialDimension(int maxNumberOfChunks) {
+ this.maxNumberOfChunks = maxNumberOfChunks;
+ return this;
+ }
+
+ /**
+ *
+ * In Zarr files, data is stored in chunks. This parameter defines the size
+ * of chunks on the x dimension. By default, this value is set to 512.
+ *
+ *
+ * Use a negative value to use the tile width of the provided image server.
+ *
+ *
+ * The provided tile width may not be used if this implies creating more chunks
+ * than the value given in {@link #setMaxNumberOfChunksOnEachSpatialDimension(int)}.
+ *
+ *
+ * @param tileWidth the width each chunk should have
+ * @return this builder
+ */
+ public Builder setTileWidth(int tileWidth) {
+ this.tileWidth = tileWidth;
+ return this;
+ }
+
+ /**
+ *
+ * In Zarr files, data is stored in chunks. This parameter defines the size
+ * of chunks on the y dimension. By default, this value is set to 512.
+ *
+ *
+ * Use a negative value to use the tile height of the provided image server.
+ *
+ *
+ * The provided tile height may not be used if this implies creating more chunks
+ * than the value given in {@link #setMaxNumberOfChunksOnEachSpatialDimension(int)}.
+ *
+ *
+ * @param tileHeight the height each chunk should have
+ * @return this builder
+ */
+ public Builder setTileHeight(int tileHeight) {
+ this.tileHeight = tileHeight;
+ return this;
+ }
+
+ /**
+ * Create a new instance of {@link OMEZarrWriter}. This will also
+ * create an empty image on the provided path.
+ *
+ * @return the new {@link OMEZarrWriter}
+ * @throws IOException when the empty image cannot be created. This can happen
+ * if the provided path is incorrect or if the user doesn't have enough permissions
+ */
+ public OMEZarrWriter build() throws IOException {
+ return new OMEZarrWriter(this);
+ }
+ }
+
+ private static int getChunkSize(int tileSize, int maxNumberOfChunks, int imageSize) {
+ return maxNumberOfChunks > 0 ?
+ Math.max(tileSize, imageSize / maxNumberOfChunks) :
+ tileSize;
+ }
+
+ private static Map createLevelArrays(
+ ImageServer server,
+ ZarrGroup root,
+ Map levelAttributes,
+ Compressor compressor
+ ) throws IOException {
+ Map levelArrays = new HashMap<>();
+
+ for (int level=0; level DataType.u1;
+ case INT8 -> DataType.i1;
+ case UINT16 -> DataType.u2;
+ case INT16 -> DataType.i2;
+ case UINT32 -> DataType.u4;
+ case INT32 -> DataType.i4;
+ case FLOAT32 -> DataType.f4;
+ case FLOAT64 -> DataType.f8;
+ })
+ .dimensionSeparator(DimensionSeparator.SLASH),
+ levelAttributes
+ ));
+ }
+
+ return levelArrays;
+ }
+
+ private static int[] getDimensionsOfImage(ImageServer server, int level) {
+ List dimensions = new ArrayList<>();
+ if (server.nTimepoints() > 1) {
+ dimensions.add(server.nTimepoints());
+ }
+ if (server.nChannels() > 1) {
+ dimensions.add(server.nChannels());
+ }
+ if (server.nZSlices() > 1) {
+ dimensions.add(server.nZSlices());
+ }
+ dimensions.add((int) (server.getHeight() / server.getDownsampleForResolution(level)));
+ dimensions.add((int) (server.getWidth() / server.getDownsampleForResolution(level)));
+
+ return dimensions.stream().mapToInt(i -> i).toArray();
+ }
+
+ private static int[] getChunksOfImage(ImageServer server) {
+ List chunks = new ArrayList<>();
+ if (server.nTimepoints() > 1) {
+ chunks.add(1);
+ }
+ if (server.nChannels() > 1) {
+ chunks.add(1);
+ }
+ if (server.nZSlices() > 1) {
+ chunks.add(Math.max(server.getMetadata().getPreferredTileWidth(), server.getMetadata().getPreferredTileHeight()));
+ }
+ chunks.add(server.getMetadata().getPreferredTileHeight());
+ chunks.add(server.getMetadata().getPreferredTileWidth());
+
+ return chunks.stream().mapToInt(i -> i).toArray();
+ }
+
+ private Object getData(BufferedImage image) {
+ Object pixels = AWTImageTools.getPixels(image);
+
+ if (server.isRGB()) {
+ int[][] data = (int[][]) pixels;
+
+ int[] output = new int[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c {
+ byte[][] data = (byte[][]) pixels;
+
+ byte[] output = new byte[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c {
+ short[][] data = (short[][]) pixels;
+
+ short[] output = new short[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c {
+ int[][] data = (int[][]) pixels;
+
+ int[] output = new int[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c {
+ float[][] data = (float[][]) pixels;
+
+ float[] output = new float[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c {
+ double[][] data = (double[][]) pixels;
+
+ double[] output = new double[server.nChannels() * image.getWidth() * image.getHeight()];
+ int i = 0;
+ for (int c=0; c dimensions = new ArrayList<>();
+ if (server.nTimepoints() > 1) {
+ dimensions.add(1);
+ }
+ if (server.nChannels() > 1) {
+ dimensions.add(server.nChannels());
+ }
+ if (server.nZSlices() > 1) {
+ dimensions.add(1);
+ }
+ dimensions.add(tileRequest.getTileHeight());
+ dimensions.add(tileRequest.getTileWidth());
+
+ return dimensions.stream().mapToInt(i -> i).toArray();
+ }
+
+ private int[] getOffsetsOfTile(TileRequest tileRequest) {
+ List offset = new ArrayList<>();
+ if (server.nTimepoints() > 1) {
+ offset.add(tileRequest.getT());
+ }
+ if (server.nChannels() > 1) {
+ offset.add(0);
+ }
+ if (server.nZSlices() > 1) {
+ offset.add(tileRequest.getZ());
+ }
+ offset.add(tileRequest.getTileY());
+ offset.add(tileRequest.getTileX());
+
+ return offset.stream().mapToInt(i -> i).toArray();
+ }
+}