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); + } +}