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 <[email protected]>
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);
+ }
+}