This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 14e7273208 Add support for writing image with bands in different order 
than RGB.
14e7273208 is described below

commit 14e7273208df6e9888dcb82dd861921ede5c7767
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Nov 15 15:58:25 2023 +0100

    Add support for writing image with bands in different order than RGB.
---
 .../sis/storage/geotiff/writer/TileMatrix.java     |  21 +-
 .../apache/sis/io/stream/HyperRectangleWriter.java |  98 +++---
 .../sis/io/stream/SubsampledRectangleWriter.java   | 331 +++++++++++++++++++++
 .../io/stream/SubsampledRectangleWriterTest.java   | 192 ++++++++++++
 4 files changed, 590 insertions(+), 52 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
index 4e750053b7..f8d5314b24 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
@@ -192,12 +192,10 @@ public final class TileMatrix {
      */
     @SuppressWarnings("null")
     public void writeRasters(final ChannelDataOutput output) throws 
DataStoreException, IOException {
-        ChannelDataOutput    compOutput  = null;
-        PixelChannel         compressor  = null;
-        SampleModel          sampleModel = null;
-        int[]                bankIndices = null;
-        HyperRectangleWriter rect        = null;
-        boolean              direct      = false;
+        ChannelDataOutput compOutput  = null;
+        PixelChannel      compressor  = null;
+        SampleModel       sampleModel = null;
+        boolean           direct      = false;
         final int minTileX = image.getMinTileX();
         final int minTileY = image.getMinTileY();
         for (int tileIndex = 0; tileIndex < numTiles; tileIndex++) {
@@ -217,12 +215,14 @@ public final class TileMatrix {
              * so `compressor` is usually created only once and shared by all 
tiles.
              */
             final var builder = new HyperRectangleWriter.Builder();
-            rect = builder.create(tile);
+            final HyperRectangleWriter rect = builder.create(tile);
             if (rect == null) {
                 throw new UnsupportedOperationException();      // TODO: 
reformat using a recycled Raster.
             }
-            bankIndices = builder.bankIndices();
+            final int[] bankIndices = builder.bankIndices();
+            final int[] bankOffsets = builder.bankOffsets();
             if (!Objects.equals(sampleModel, sampleModel = 
tile.getSampleModel())) {
+                direct = type.equals(DataType.BYTE) && 
rect.suggestDirect(output);
                 if (compressor != null) {
                     compressor.close();
                     compressor = null;
@@ -243,7 +243,7 @@ public final class TileMatrix {
                     }
                     switch (predictor) {
                         default: throw 
unsupported(Resources.Keys.UnsupportedPredictor_1, predictor);
-                        case NONE: direct = type.equals(DataType.BYTE); break;
+                        case NONE: break;
                         case HORIZONTAL_DIFFERENCING: {
                             compressor = 
HorizontalPredictor.create(compressor, type, builder.pixelStride(), 
builder.scanlineStride());
                             direct = false;     // Because the predictor will 
write in the buffer, so it must be a copy of the data.
@@ -254,7 +254,6 @@ public final class TileMatrix {
                     compOutput = new ChannelDataOutput(output.filename, 
compressor, buffer.order(output.buffer.order()));
                 } else {
                     compOutput = output;
-                    direct = rect.suggestDirect(output);                // 
Will be ignored if data type is not byte.
                     assert predictor == Predictor.NONE : predictor;     // 
Assumption documented in `Compression` class.
                 }
             }
@@ -265,7 +264,7 @@ public final class TileMatrix {
             final int[] bufferOffsets = buffer.getOffsets();
             for (int j=0; j<numPlanes; j++) {
                 final int  b        = bankIndices[j];
-                final int  offset   = bufferOffsets[b];
+                final int  offset   = bankOffsets[j] + bufferOffsets[b];
                 final long position = output.getStreamPosition();
                 switch (type) {
                     default:     throw new AssertionError(type);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
index cec18bd0b2..f4ec6a0c1d 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
@@ -37,7 +37,7 @@ import org.apache.sis.util.ArraysExt;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class HyperRectangleWriter {
+public class HyperRectangleWriter {
     /**
      * Index of the first value to use in the array given to write methods.
      */
@@ -46,7 +46,7 @@ public final class HyperRectangleWriter {
     /**
      * Number of elements that can be written in a single I/O operation.
      */
-    private final int contiguousDataLength;
+    final int contiguousDataLength;
 
     /**
      * Number of elements to write in each dimension after the contiguous 
dimensions, in reverse order.
@@ -127,10 +127,17 @@ public final class HyperRectangleWriter {
          * A length greater than one means that the {@link 
HyperRectangleWriter} instance
          * created by this builder will need to be invoked repetitively for 
each bank.
          *
-         * @see bankIndices()
+         * @see #bankIndices()
          */
         private int[] bankIndices;
 
+        /**
+         * The offset to add to each bank. This is in addition of offsets 
declared in {@link DataBuffer#getOffsets()}.
+         *
+         * @see #bankOffsets()
+         */
+        private int[] bankOffsets;
+
         /**
          * Subregion to write, or {@code null} for writing the whole raster.
          *
@@ -171,10 +178,13 @@ public final class HyperRectangleWriter {
 
         /**
          * Creates a new writer for raster data described by the given sample 
model and strides.
-         * If the {@link #region} is non-null, it specifies a subset of the 
data to write.
+         * The {@link #pixelStride} and {@link #scanlineStride} fields must be 
set before this method is invoked.
+         *
+         * @param  sm           the sample model of the rasters to write.
+         * @param  bandOffsets  bands to read, or {@code null} for all of them 
in same order.
+         * @return writer for rasters using the specified sample model.
          */
-        private HyperRectangleWriter create(final SampleModel sm, final int 
subX) {
-            final int[]  subsampling = {subX, 1};
+        private HyperRectangleWriter create(final SampleModel sm, final int[] 
bandOffsets) {
             final long[] sourceSize  = {scanlineStride, sm.getHeight()};
             if (region == null) {
                 region = new Rectangle(sm.getWidth(), sm.getHeight());
@@ -189,55 +199,47 @@ public final class HyperRectangleWriter {
             };
             regionLower[0] = Math.multiplyExact(regionLower[0], pixelStride);
             regionUpper[0] = Math.multiplyExact(regionUpper[0], pixelStride);
-            var subset = new Region(sourceSize, regionLower, regionUpper, 
subsampling);
+            var subset = new Region(sourceSize, regionLower, regionUpper, new 
int[] {1,1});
             length = subset.length;
-            return new HyperRectangleWriter(subset);
+            if (bandOffsets == null || (bandOffsets.length == pixelStride && 
ArraysExt.isRange(0, bandOffsets))) {
+                return new HyperRectangleWriter(subset);
+            } else {
+                return new SubsampledRectangleWriter(subset, bandOffsets, 
pixelStride);
+            }
         }
 
         /**
          * Creates a new writer for raster data described by the given sample 
model.
-         * This method supports only the writing of either a single band, or 
all bands
-         * in the order they appear in the array.
-         *
-         * <p>The returned writer will need to be applied repetitively for 
each bank
-         * if {@link #bankIndices()} returns an array with a length greater 
than one.</p>
+         * The returned writer will need to be applied repetitively for each 
bank
+         * if {@link #bankIndices()} returns an array with a length greater 
than one.
          *
          * @param  sm  the sample model of the rasters to write.
          * @return writer, or {@code null} if the given sample model is not 
supported.
          */
         public HyperRectangleWriter create(final ComponentSampleModel sm) {
+            int[] bandOffsets;
             pixelStride    = sm.getPixelStride();
             scanlineStride = sm.getScanlineStride();
             bankIndices    = sm.getBankIndices();
-            final int[] d  = sm.getBandOffsets();
-            final int subX;
+            bandOffsets    = sm.getBandOffsets();
             if (ArraysExt.allEquals(bankIndices, bankIndices[0])) {
                 /*
                  * PixelInterleavedSampleModel (at least conceptually, no 
matter the actual type).
-                 * The returned `HyperRectangleWriter` instance will write all 
sample values in a
-                 * single call to a `write(…)` method, no matter the actual 
number of bands.
+                 * The returned `HyperRectangleWriter` instance may write all 
sample values in a
+                 * single call to a `write(…)` method, even if there is many 
bands.
                  */
                 bankIndices = ArraysExt.resize(bankIndices, 1);
-                if (d.length == pixelStride && ArraysExt.isRange(0, d)) {
-                    subX = 1;
-                } else if (d.length == 1) {
-                    subX = pixelStride;
-                } else {
-                    return null;
-                }
+                bankOffsets = new int[1];
             } else {
                 /*
                  * BandedSampleModel (at least conceptually, no matter the 
actual type).
                  * The returned `HyperRectangleWriter` instance will need to 
be used
                  * repetitively by the caller.
                  */
-                if (ArraysExt.allEquals(d, 0)) {
-                    subX = 1;
-                } else {
-                    return null;
-                }
+                bankOffsets = bandOffsets;
+                bandOffsets = null;
             }
-            return create(sm, subX);
+            return create(sm, bandOffsets);
         }
 
         /**
@@ -249,13 +251,14 @@ public final class HyperRectangleWriter {
          */
         public HyperRectangleWriter create(final SinglePixelPackedSampleModel 
sm) {
             bankIndices    = new int[1];   // Length is NOT the number of 
bands.
+            bankOffsets    = bankIndices;
             pixelStride    = 1;
             scanlineStride = sm.getScanlineStride();
             final int[] d  = sm.getBitMasks();
             if (d.length == 1) {
                 final long mask = (1L << 
DataBuffer.getDataTypeSize(sm.getDataType())) - 1;
                 if ((d[0] & mask) == mask) {
-                    return create(sm, 1);
+                    return create(sm, null);
                 }
             }
             return null;
@@ -270,13 +273,14 @@ public final class HyperRectangleWriter {
          */
         public HyperRectangleWriter create(final MultiPixelPackedSampleModel 
sm) {
             bankIndices    = new int[1];   // Length is NOT the number of 
bands.
+            bankOffsets    = bankIndices;
             pixelStride    = 1;
             scanlineStride = sm.getScanlineStride();
             final int[] d  = sm.getSampleSize();
             if (d.length == 1) {
                 final int size = DataBuffer.getDataTypeSize(sm.getDataType());
                 if (d[0] == size && sm.getPixelBitStride() == size) {
-                    return create(sm, 1);
+                    return create(sm, null);
                 }
             }
             return null;
@@ -350,6 +354,17 @@ public final class HyperRectangleWriter {
         public int[] bankIndices() {
             return bankIndices;
         }
+
+        /**
+         * Returns the offset to add to each bank to write with {@code 
HyperRectangleWriter}.
+         * This is in addition of offsets declared in {@link 
DataBuffer#getOffsets()}.
+         *
+         * @return offsets of all banks to write with {@code 
HyperRectangleWriter}.
+         */
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        public int[] bankOffsets() {
+            return bankOffsets;
+        }
     }
 
     /**
@@ -358,7 +373,7 @@ public final class HyperRectangleWriter {
      * @param  offset  offset supplied by the useR.
      * @return offset to use.
      */
-    private int startAt(final int offset) {
+    final int startAt(final int offset) {
         return Math.addExact(startAt, offset);
     }
 
@@ -367,7 +382,7 @@ public final class HyperRectangleWriter {
      *
      * @return number of I/O operations to apply.
      */
-    private int[] count() {
+    final int[] count() {
         return remaining.clone();
     }
 
@@ -377,7 +392,7 @@ public final class HyperRectangleWriter {
      * @param  count  array of counters to update in-place.
      * @return next offset, or -1 if the iteration is finished.
      */
-    private int next(int offset, final int[] count) {
+    final int next(int offset, final int[] count) {
         for (int i = count.length; --i >= 0;) {
             if (--count[i] >= 0) {
                 return offset + strides[i];
@@ -412,7 +427,8 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
+     * @param  direct  whether to write directly to the channel if possible.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final byte[] data, int 
offset, final boolean direct) throws IOException {
@@ -442,7 +458,7 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final short[] data, int 
offset) throws IOException {
@@ -457,7 +473,7 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final int[] data, int 
offset) throws IOException {
@@ -472,7 +488,7 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final long[] data, int 
offset) throws IOException {
@@ -487,7 +503,7 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final float[] data, int 
offset) throws IOException {
@@ -502,7 +518,7 @@ public final class HyperRectangleWriter {
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
-     * @param  offset  offset to add to array index.
+     * @param  offset  index of the first data element to write.
      * @throws IOException if an error occurred while writing the data.
      */
     public void write(final ChannelDataOutput output, final double[] data, int 
offset) throws IOException {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/SubsampledRectangleWriter.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/SubsampledRectangleWriter.java
new file mode 100644
index 0000000000..89c00f3712
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/SubsampledRectangleWriter.java
@@ -0,0 +1,331 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.io.stream;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+
+/**
+ * Helper methods for writing a rectangular area with subsampling applied 
on-the-fly.
+ * This class is thread-safe if writing in different output channels.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class SubsampledRectangleWriter extends HyperRectangleWriter {
+    /**
+     * The indices of the sample value to take in each pixel.
+     */
+    private final int[] bandOffsets;
+
+    /**
+     * Number of sample values (usually bands) between a pixel and the next 
pixel in the source arrays.
+     * By comparison, {@code super.strides[0]} is the scanline stride.
+     */
+    private final int pixelStride;
+
+    /**
+     * Creates a new writer for data of a shape specified by the given region.
+     * The region also specifies the subset to write.
+     *
+     * @param  output       where to write data.
+     * @param  region       size of the source hyper-rectangle and region to 
write.
+     * @param  bandOffsets  indices of bands to write. This array is not 
cloned.
+     * @param  pixelStride  number of bands in a pixel.
+     * @throws ArithmeticException if the region is too large.
+     */
+    public SubsampledRectangleWriter(final Region region, final int[] 
bandOffsets, final int pixelStride) {
+        super(region);
+        this.bandOffsets = bandOffsets;
+        this.pixelStride = pixelStride;
+    }
+
+    /**
+     * Returns {@code false} since direct mode is never supported when 
sub-sampling is applied.
+     */
+    @Override
+    public boolean suggestDirect(final ChannelDataOutput output) {
+        return false;
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output      where to write data.
+     * @param  offset      index of the first data element to write.
+     * @param  sampleSize  number of bytes in a sample value.
+     * @param  data        wrapper over the data of the hyper-rectangle to 
write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    private void write(final ChannelDataOutput output, int offset, final int 
sampleSize, final Data data) throws IOException {
+        final ByteBuffer target = output.buffer;
+        final int numBands  = bandOffsets.length;
+        final int pixelSize = numBands * sampleSize;        // Pixel stride in 
target buffer and in bytes.
+        final int[] count = count();
+        offset = startAt(offset);
+        do {
+            int index = offset;
+            final int end = index + contiguousDataLength;
+            do {
+                output.ensureBufferAccepts(pixelSize);      // At least one 
pixel, but will usually free more space.
+                final int numPixels = Math.min((end - index) / pixelStride,
+                        (target.capacity() - target.position()) / pixelSize);
+                target.limit(target.position() + numPixels * pixelSize);
+                if (numBands == 1) {
+                    index = data.fill(target, index + bandOffsets[0], 
pixelStride);
+                } else {
+                    index = data.fill(target, index, bandOffsets, pixelStride);
+                }
+            } while (index < end);
+        } while ((offset = next(offset, count)) >= 0);
+    }
+
+    /**
+     * A wrapper of an array of arbitrary primitive type to be sub-sampled in 
a {@link ByteBuffer}.
+     * An instance is created for each array to write. The subclass depends on 
the primitive type.
+     */
+    private static abstract class Data {
+        /**
+         * Creates a new adapter.
+         */
+        Data() {
+        }
+
+        /**
+         * Fills the given buffer with pixels of one sample value each.
+         * Caller must ensure that the remaining space in the buffer is an 
integer number of pixels.
+         *
+         * @param  target  the buffer to fill.
+         * @param  index   index of the first array element to put in the 
target buffer.
+         * @param  stride  value to add to the index for moving to the next 
pixel in the source array.
+         * @return value of {@code index} after the buffer has been filled.
+         */
+        abstract int fill(final ByteBuffer target, int index, int stride);
+
+        /**
+         * Fills the given buffer with pixels of made of multiple sample 
values each.
+         * Caller must ensure that the remaining space in the buffer is an 
integer number of pixels.
+         *
+         * @param  target  the buffer to fill.
+         * @param  index   index of the first pixel to put in the target 
buffer.
+         * @param  bands   indices of the bands to put in the buffer, in order.
+         * @param  stride  value to add to the index for moving to the next 
pixel in the source array.
+         * @return value of {@code index} after the buffer has been filled.
+         */
+        abstract int fill(final ByteBuffer target, int index, int[] bands, int 
stride);
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @param  direct  Must be {@code false}. The transfer will never be 
direct.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final byte[] data, int 
offset, final boolean direct) throws IOException {
+        if (direct) throw new UnsupportedOperationException();
+        write(output, offset, Byte.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.put(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.put(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final short[] data, int 
offset) throws IOException {
+        write(output, offset, Short.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.putShort(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.putShort(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final int[] data, int 
offset) throws IOException {
+        write(output, offset, Integer.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.putInt(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.putInt(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final long[] data, int 
offset) throws IOException {
+        write(output, offset, Long.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.putLong(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.putLong(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final float[] data, int 
offset) throws IOException {
+        write(output, offset, Float.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.putFloat(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.putFloat(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+
+    /**
+     * Writes an hyper-rectangle with the shape and subsampling described at 
construction time.
+     *
+     * @param  output  where to write data.
+     * @param  data    data of the hyper-rectangle.
+     * @param  offset  index of the first data element to write.
+     * @throws IOException if an error occurred while writing the data.
+     */
+    @Override
+    public void write(final ChannelDataOutput output, final double[] data, int 
offset) throws IOException {
+        write(output, offset, Double.BYTES, new Data() {
+            /** Fill the buffer with pixels made of a single sample value. */
+            @Override int fill(final ByteBuffer target, int index, final int 
stride) {
+                while (target.hasRemaining()) {
+                    target.putDouble(data[index]);
+                    index += stride;
+                }
+                return index;
+            }
+
+            /** Fill the buffer with pixels made of multiple sample values. */
+            @Override int fill(final ByteBuffer target, int index, final int[] 
bands, final int stride) {
+                while (target.hasRemaining()) {
+                    for (int b : bands) {
+                        target.putDouble(data[index + b]);
+                    }
+                    index += stride;
+                }
+                return index;
+            }
+        });
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/SubsampledRectangleWriterTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/SubsampledRectangleWriterTest.java
new file mode 100644
index 0000000000..db6ad83ae0
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/SubsampledRectangleWriterTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.io.stream;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.lang.reflect.Array;
+import java.util.Random;
+import java.util.function.IntFunction;
+import java.util.function.ToDoubleFunction;
+
+// Test dependencies
+import org.junit.Test;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.test.TestUtilities;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Tests {@link SubsampledRectangleWriter}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class SubsampledRectangleWriterTest extends TestCase {
+    /**
+     * The writer to test.
+     */
+    private SubsampledRectangleWriter writer;
+
+    /**
+     * The channel where the {@linkplain #writer} will write.
+     */
+    private ChannelDataOutput output;
+
+    /**
+     * The data actually written by the {@linkplain #writer}.
+     */
+    private ByteBuffer actual;
+
+    /**
+     * Value of the band offsets argument used for the test.
+     */
+    private int[] bandOffsets;
+
+    /**
+     * Lower value to store in the array.
+     */
+    private static final int BASE = 10;
+
+    /**
+     * Creates a new test case.
+     */
+    public SubsampledRectangleWriterTest() {
+    }
+
+    /**
+     * Allocates resources for a test of a primitive type.
+     *
+     * @param  <A>       type of the array of primitive type.
+     * @param  creator   function to invoke for creating an array of specified 
length.
+     * @param  dataSize  size in bytes of the primitive type.
+     * @return array of data which will be given to a {@code write(…)} method 
to test.
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    private <A> A allocate(final IntFunction<A> creator, final int dataSize) 
throws IOException {
+        bandOffsets = new int[] {2, 1, 3, 0};
+
+        final Random random = TestUtilities.createRandomNumberGenerator();
+        final int    width  = (random.nextInt(9) + 3) * bandOffsets.length;
+        final int    height = (random.nextInt(5) + 1);
+        final int    length = width * height;
+        final long[] lower  = new long[2];
+        final long[] upper  = new long[] {width, height};
+        final int[]  subsm  = new int[]  {1,1};
+        final A source = creator.apply(length);
+        for (int i=0; i<length; i++) {
+            Array.setByte(source, i, (byte) (BASE + i));
+        }
+        final byte[] target = new byte[length * dataSize];
+        final var buffer = ByteBuffer.allocate((random.nextInt(4) + 1) + 
bandOffsets.length * dataSize);
+        actual = ByteBuffer.wrap(target);
+        output = new ChannelDataOutput("Test", new ByteArrayChannel(target, 
false), buffer);
+        writer = new SubsampledRectangleWriter(new Region(upper, lower, upper, 
subsm), bandOffsets, bandOffsets.length);
+        return source;
+    }
+
+    /**
+     * Verifies that the bytes written by {@linkplain #writer} are equal to 
the expected value.
+     *
+     * @param  getter    the {@link ByteBuffer} getter method corresponding to 
the tested type.
+     * @param  dataSize  size in bytes of the primitive type.
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    private void verifyWrittenBytes(final ToDoubleFunction<ByteBuffer> getter) 
throws IOException {
+        output.flush();
+        int base = BASE;
+        while (actual.hasRemaining()) {
+            for (int offset : bandOffsets) {
+                final double value = getter.applyAsDouble(actual);
+                assertEquals((byte) (base + offset), (byte) value);
+            }
+            base += bandOffsets.length;
+        }
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, byte[], 
int, boolean)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteBytes() throws IOException {
+        final byte[] source = allocate(byte[]::new, Byte.BYTES);
+        writer.write(output, source, 0, false);
+        verifyWrittenBytes(ByteBuffer::get);
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, short[], 
int)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteShorts() throws IOException {
+        final short[] source = allocate(short[]::new, Short.BYTES);
+        writer.write(output, source, 0);
+        verifyWrittenBytes(ByteBuffer::getShort);
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, int[], 
int)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteInts() throws IOException {
+        final int[] source = allocate(int[]::new, Integer.BYTES);
+        writer.write(output, source, 0);
+        verifyWrittenBytes(ByteBuffer::getInt);
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, long[], 
int)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteLongs() throws IOException {
+        final long[] source = allocate(long[]::new, Long.BYTES);
+        writer.write(output, source, 0);
+        verifyWrittenBytes(ByteBuffer::getLong);
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, float[], 
int)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteFloats() throws IOException {
+        final float[] source = allocate(float[]::new, Float.BYTES);
+        writer.write(output, source, 0);
+        verifyWrittenBytes(ByteBuffer::getFloat);
+    }
+
+    /**
+     * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, 
double[], int)} method.
+     *
+     * @throws IOException should never happen since we are writing in memory.
+     */
+    @Test
+    public void testWriteDoubles() throws IOException {
+        final double[] source = allocate(double[]::new, Double.BYTES);
+        writer.write(output, source, 0);
+        verifyWrittenBytes(ByteBuffer::getDouble);
+    }
+}


Reply via email to