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

commit bbe24a2fba4459500cbbe3448c076a22846ee97b
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Dec 13 11:26:10 2024 +0100

    Better checks of SampleModel properties before to write a GeoTIFF file.
---
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  15 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |  12 +-
 .../storage/geotiff/writer/ReformattedImage.java   |  23 ++-
 .../sis/storage/geotiff/writer/TileMatrix.java     |  72 +++++---
 .../apache/sis/io/stream/HyperRectangleWriter.java | 202 +++++++++++++++------
 .../main/org/apache/sis/io/stream/Region.java      |  30 +++
 .../sis/storage/base/URIDataStoreProvider.java     |  25 +++
 7 files changed, 283 insertions(+), 96 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 9ddabf81b3..0c5cd17ce6 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -30,6 +30,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.awt.image.RenderedImage;
+import java.awt.image.RasterFormatException;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.util.GenericName;
@@ -46,6 +47,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.WriteOnlyStorageException;
+import org.apache.sis.storage.IncompatibleResourceException;
 import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.StoreUtilities;
@@ -258,7 +260,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         path        = connector.getStorageAs(Path.class);
         try {
             if (URIDataStoreProvider.isWritable(connector, true)) {
-                ChannelDataOutput output = 
connector.commit(ChannelDataOutput.class, Constants.GEOTIFF);
+                ChannelDataOutput output = 
URIDataStoreProvider.openAndSetNativeByteOrder(connector, Constants.GEOTIFF);
                 writer = new Writer(this, output, 
connector.getOption(FormatModifier.OPTION_KEY));
             } else {
                 ChannelDataInput input = 
connector.commit(ChannelDataInput.class, Constants.GEOTIFF);
@@ -705,8 +707,10 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
                 reader.offsetOfWrittenIFD(offsetIFD);
             }
             index = writer.imageIndex++;
+        } catch (RasterFormatException | ArithmeticException e) {
+            throw new IncompatibleResourceException(cannotWrite(), e);
         } catch (IOException e) {
-            throw new 
DataStoreException(errors().getString(Errors.Keys.CanNotWriteFile_2, 
Constants.GEOTIFF, getDisplayName()), e);
+            throw new DataStoreException(cannotWrite(), e);
         }
         if (components != null) {
             components.incrementSize(1);
@@ -781,6 +785,13 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         return new 
DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, 
getDisplayName()), e);
     }
 
+    /**
+     * Returns the error message for a file that cannot be written.
+     */
+    private String cannotWrite() {
+        return errors().getString(Errors.Keys.CanNotWriteFile_2, 
Constants.GEOTIFF, getDisplayName());
+    }
+
     /**
      * Returns a localized error message saying that this data store has been 
opened in read-only or write-only mode.
      *
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 3de9db2926..4dd261c294 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -31,6 +31,7 @@ import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.IndexColorModel;
+import java.awt.image.RasterFormatException;
 import javax.imageio.plugins.tiff.TIFFTag;
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
@@ -45,6 +46,10 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.base.MetadataFetcher;
+import org.apache.sis.storage.geotiff.writer.TagValue;
+import org.apache.sis.storage.geotiff.writer.TileMatrix;
+import org.apache.sis.storage.geotiff.writer.GeoEncoder;
+import org.apache.sis.storage.geotiff.writer.ReformattedImage;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.UpdatableWrite;
 import org.apache.sis.util.CharSequences;
@@ -52,10 +57,6 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.privy.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Fraction;
-import org.apache.sis.storage.geotiff.writer.TagValue;
-import org.apache.sis.storage.geotiff.writer.TileMatrix;
-import org.apache.sis.storage.geotiff.writer.GeoEncoder;
-import org.apache.sis.storage.geotiff.writer.ReformattedImage;
 
 
 /**
@@ -168,6 +169,7 @@ final class Writer extends IOBase implements Flushable {
 
     /**
      * Creates a new GeoTIFF writer which will write data in the given output.
+     * The byte order of the given output determines the byte order of the 
GeoTIFF file to write.
      *
      * @param  store    the store writing data.
      * @param  output   where to write the bytes.
@@ -276,6 +278,8 @@ final class Writer extends IOBase implements Flushable {
      * @param  grid      mapping from pixel coordinates to "real world" 
coordinates, or {@code null} if none.
      * @param  metadata  title, author and other information, or {@code null} 
if none.
      * @return offset if {@link #output} where the Image File Directory (IFD) 
starts.
+     * @throws RasterFormatException if the raster uses an unsupported sample 
model.
+     * @throws ArithmeticException if an integer overflow occurs.
      * @throws IOException if an error occurred while writing to the output.
      * @throws DataStoreException if the given {@code image} has a property
      *         which is not supported by TIFF specification or by this writer.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
index 3d82659a1b..bb3e9f8452 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
@@ -125,7 +125,7 @@ found:  if (property instanceof Statistics[]) {
      * @throws IncompatibleResourceException if the color model is not 
supported.
      */
     public int getColorInterpretation() throws IncompatibleResourceException {
-        final ColorModel  cm = visibleBands.getColorModel();
+        final ColorModel cm = visibleBands.getColorModel();
         if (cm instanceof IndexColorModel) {
             final var   icm   = (IndexColorModel) cm;
             final int   last  = icm.getMapSize() - 1;
@@ -142,17 +142,20 @@ found:  if (property instanceof Statistics[]) {
             if (white) return PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
             return PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR;
         }
-        switch (cm.getColorSpace().getType()) {
-            case ColorSpace.TYPE_GRAY: {
-                return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
-            }
-            case ColorSpace.TYPE_RGB: {
-                return PHOTOMETRIC_INTERPRETATION_RGB;
-            }
-            default: {
+        if (cm != null) {
+            switch (cm.getColorSpace().getType()) {
+                case ColorSpace.TYPE_GRAY:  return 
PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
+                case ColorSpace.TYPE_RGB:   return 
PHOTOMETRIC_INTERPRETATION_RGB;
+                case ColorSpace.TYPE_CMY:   return 
PHOTOMETRIC_INTERPRETATION_CMYK;
+                case ColorSpace.TYPE_Lab:   return 
PHOTOMETRIC_INTERPRETATION_CIELAB;
+                case ColorSpace.TYPE_YCbCr: return 
PHOTOMETRIC_INTERPRETATION_Y_CB_CR;
                 // A future version may add support for more color models.
-                throw new IncompatibleResourceException("Unsupported color 
model");
             }
         }
+        if (ImageUtilities.getNumBands(visibleBands) >= 3) {
+            return PHOTOMETRIC_INTERPRETATION_RGB;
+        } else {
+            return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
+        }
     }
 }
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 15c332f334..52947d685b 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
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Objects;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.DataBuffer;
@@ -29,11 +30,13 @@ import java.awt.image.DataBufferUShort;
 import java.awt.image.DataBufferInt;
 import java.awt.image.DataBufferFloat;
 import java.awt.image.DataBufferDouble;
+import java.awt.image.RasterFormatException;
 import java.awt.image.SampleModel;
 import org.apache.sis.image.DataType;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.HyperRectangleWriter;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.storage.geotiff.base.Compression;
 import org.apache.sis.storage.geotiff.base.Predictor;
 import org.apache.sis.storage.geotiff.base.Resources;
@@ -182,19 +185,23 @@ public final class TileMatrix {
     }
 
     /**
-     * Writes all tiles of the image.
+     * Writes the (eventually compressed) sample values of all tiles of the 
image.
      * Caller shall invoke {@link #writeOffsetsAndLengths(ChannelDataOutput)} 
after this method.
      * This invocation is not done by this method for allowing the caller to 
control when to write data.
      *
      * @param  output  where to write the tiles data.
+     * @throws RasterFormatException if the raster uses an unsupported sample 
model.
+     * @throws ArithmeticException if an integer overflow occurs.
      * @throws DataStoreException if the compression method is unsupported.
      * @throws IOException if an error occurred while writing to the given 
output.
      */
     public void writeRasters(final ChannelDataOutput output) throws 
DataStoreException, IOException {
-        ChannelDataOutput compOutput  = null;
-        PixelChannel      compressor  = null;
-        SampleModel       sampleModel = null;
-        boolean           direct      = false;
+        ChannelDataOutput compOutput    = null;
+        PixelChannel      compressor    = null;
+        SampleModel       sampleModel   = null;
+        boolean           direct        = false;
+        ByteOrder         dataByteOrder = null;
+        final ByteOrder   fileByteOrder = output.buffer.order();
         final int minTileX = image.getMinTileX();
         final int minTileY = image.getMinTileY();
         for (int tileIndex = 0; tileIndex < numTiles; tileIndex++) {
@@ -210,22 +217,26 @@ public final class TileMatrix {
             /*
              * Creates the `rect` object which will be used for writing a 
subset of the raster data.
              * This object depends not only on the sample model, but also on 
the raster coordinates.
-             * However the compressor depends on properties that change only 
with the sample model,
-             * so `compressor` is usually created only once and shared by all 
tiles.
+             * Therefore, a new instance needs to be created for each tile.
              */
             final var builder = new HyperRectangleWriter.Builder();
-            final HyperRectangleWriter rect = builder.create(tile);
-            if (rect == null) {
-                throw new UnsupportedOperationException();      // TODO: 
reformat using a recycled Raster.
+            final HyperRectangleWriter rect = builder.create(tile, tileWidth, 
tileHeight);
+            if (builder.numBanks() != numPlanes) {
+                // This exception would be a bug in our analysis of the sample 
model.
+                throw new 
InternalDataStoreException(tile.getSampleModel().toString());
             }
-            final int[] bankIndices = builder.bankIndices();
-            final int[] bankOffsets = builder.bankOffsets();
+            /*
+             * The compressor depends on properties that change only with the 
sample model,
+             * so `compressor` is usually created only once and shared by all 
tiles.
+             */
             if (!Objects.equals(sampleModel, sampleModel = 
tile.getSampleModel())) {
                 direct = type.equals(DataType.BYTE) && 
rect.suggestDirect(output);
                 if (compressor != null) {
                     compressor.close();
                     compressor = null;
                 }
+                dataByteOrder = builder.byteOrder(fileByteOrder);
+                direct &= (dataByteOrder == null);      // Disable direct mode 
if a change of byte order is needed.
                 /*
                  * Creates the data output to use for writing compressed data. 
The compressor will need an
                  * intermediate buffer, unless the `direct` flag is true, in 
which case we will bypass the
@@ -250,7 +261,7 @@ public final class TileMatrix {
                         }
                     }
                     ByteBuffer buffer = direct ? ByteBuffer.allocate(0) : 
compressor.createBuffer();
-                    compOutput = new ChannelDataOutput(output.filename, 
compressor, buffer.order(output.buffer.order()));
+                    compOutput = new ChannelDataOutput(output.filename, 
compressor, buffer.order(fileByteOrder));
                 } else {
                     compOutput = output;
                     assert predictor == Predictor.NONE : predictor;     // 
Assumption documented in `Compression` class.
@@ -262,20 +273,29 @@ public final class TileMatrix {
             final DataBuffer buffer = tile.getDataBuffer();
             final int[] bufferOffsets = buffer.getOffsets();
             for (int j=0; j<numPlanes; j++) {
-                final int  b        = bankIndices[j];
-                final int  offset   = bankOffsets[j] + bufferOffsets[b];
+                final int  b        = builder.bankIndex(j);
+                final int  offset   = builder.bankOffset(j, bufferOffsets[b]);
                 final long position = output.getStreamPosition();
-                switch (type) {
-                    default:     throw new AssertionError(type);
-                    case BYTE:   rect.write(compOutput, ((DataBufferByte)   
buffer).getData(b), offset, direct); break;
-                    case USHORT: rect.write(compOutput, ((DataBufferUShort) 
buffer).getData(b), offset); break;
-                    case SHORT:  rect.write(compOutput, ((DataBufferShort)  
buffer).getData(b), offset); break;
-                    case INT:    rect.write(compOutput, ((DataBufferInt)    
buffer).getData(b), offset); break;
-                    case FLOAT:  rect.write(compOutput, ((DataBufferFloat)  
buffer).getData(b), offset); break;
-                    case DOUBLE: rect.write(compOutput, ((DataBufferDouble) 
buffer).getData(b), offset); break;
-                }
-                if (compressor != null) {
-                    compressor.finish(compOutput);
+                try {
+                    if (dataByteOrder != null) {
+                        compOutput.buffer.order(dataByteOrder);
+                    }
+                    switch (type) {
+                        default:     throw new AssertionError(type);
+                        case BYTE:   rect.write(compOutput, ((DataBufferByte)  
 buffer).getData(b), offset, direct); break;
+                        case USHORT: rect.write(compOutput, 
((DataBufferUShort) buffer).getData(b), offset); break;
+                        case SHORT:  rect.write(compOutput, ((DataBufferShort) 
 buffer).getData(b), offset); break;
+                        case INT:    rect.write(compOutput, ((DataBufferInt)   
 buffer).getData(b), offset); break;
+                        case FLOAT:  rect.write(compOutput, ((DataBufferFloat) 
 buffer).getData(b), offset); break;
+                        case DOUBLE: rect.write(compOutput, 
((DataBufferDouble) buffer).getData(b), offset); break;
+                    }
+                    if (compressor != null) {
+                        compressor.finish(compOutput);
+                    }
+                } finally {
+                    if (dataByteOrder != null) {    // Avoid touching byte 
order if it wasn't needed.
+                        compOutput.buffer.order(fileByteOrder);
+                    }
                 }
                 final int planeIndex = tileIndex + j*numTiles;
                 offsets[planeIndex] = position;
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 ae69c83767..5bdd13ea3f 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
@@ -18,6 +18,7 @@ package org.apache.sis.io.stream;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.DataBuffer;
@@ -25,6 +26,7 @@ import java.awt.image.SampleModel;
 import java.awt.image.ComponentSampleModel;
 import java.awt.image.MultiPixelPackedSampleModel;
 import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.RasterFormatException;
 import org.apache.sis.util.ArraysExt;
 
 
@@ -97,6 +99,9 @@ public class HyperRectangleWriter {
      * Each builder shall be used only once. For creating more {@code 
HyperRectangleWriter} instances,
      * new builders shall be created.
      *
+     * <p>All getter methods contain information that are valid
+     * only after a {@code create(…)} method has been invoked.</p>
+     *
      * @author  Martin Desruisseaux (Geomatys)
      */
     public static final class Builder {
@@ -124,24 +129,46 @@ public class HyperRectangleWriter {
 
         /**
          * The indices of all banks to write with {@code HyperRectangleWriter}.
+         * This is not necessarily the bank indices of all bands, because the 
writer may be
+         * able to write all bands contiguously in a single call to a {@code 
write(…)} method.
          * 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 #bankIndex(int)
          */
         private int[] bankIndices;
 
         /**
-         * The offset to add to each bank. This is in addition of offsets 
declared in {@link DataBuffer#getOffsets()}.
+         * The offset to add to each bank to write with {@code 
HyperRectangleWriter}.
+         * This is in addition of offsets declared in {@link 
DataBuffer#getOffsets()}.
          *
-         * @see #bankOffsets()
+         * @see #bankOffset(int, int)
          */
         private int[] bankOffsets;
 
+        /**
+         * Whether writing data requires the use of {@link 
ByteOrder#BIG_ENDIAN} in the destination buffer.
+         * This is sometime needed when {@code short} or {@code int} values 
are used for packing many bytes.
+         * This flag can be ignored if, for example, ARGB values are really 
stored as integers in native
+         * byte order rather than 4 bytes. If this flag is {@code false}, then 
byte order does not matter
+         * (i.e., {@code false} does mean that {@link ByteOrder#LITTLE_ENDIAN} 
is required).
+         *
+         * @see #byteOrder(ByteOrder)
+         */
+        private boolean requiresBigEndian;
+
         /**
          * Subregion to write, or {@code null} for writing the whole raster.
+         * This field serves two purposes:
          *
-         * @see #region(Rectangle)
+         * <h4>Before a call to a {@code create(…)}</h4>
+         * The rectangle is in the coordinate system of the object specified 
to the {@code create(…)} method:
+         * {@link Raster} coordinates if the {@link #create(Raster)} method is 
invoked, or
+         * {@link SampleModel} coordinates if the {@link #create(SampleModel)} 
method is invoked.
+         *
+         * <h4>After a call to a {@code create(…)}</h4>
+         * If the field was null, it is set to a non-null value containing 
{@link Raster} or {@link SampleModel}
+         * coordinates as described above. If the field was already non-null, 
the previous instance is unmodified.
          */
         private Rectangle region;
 
@@ -161,28 +188,13 @@ public class HyperRectangleWriter {
         public Builder() {
         }
 
-        /**
-         * Specifies the region to write.
-         * The rectangle is in the coordinate system of the object specified 
to the {@code create(…)} method:
-         * {@link Raster} coordinates if the {@link #create(Raster)} method is 
invoked, or
-         * {@link SampleModel} coordinates if the {@link #create(SampleModel)} 
method is invoked.
-         * This method retains the given rectangle by reference, it is not 
copied.
-         *
-         * @param  aoi  the region to write, or {@code null} for writing the 
whole raster.
-         * @return {@code this} for chained call.
-         */
-        public Builder region(final Rectangle aoi) {
-            region = aoi;
-            return this;
-        }
-
         /**
          * Creates a new writer for raster data described by the given sample 
model and strides.
          * 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.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
          */
         private HyperRectangleWriter create(final SampleModel sm, final int[] 
bandOffsets) {
             final long[] sourceSize  = {scanlineStride, sm.getHeight()};
@@ -214,7 +226,7 @@ public class HyperRectangleWriter {
          * 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.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
          */
         public HyperRectangleWriter create(final ComponentSampleModel sm) {
             int[] bandOffsets;
@@ -244,10 +256,21 @@ public class HyperRectangleWriter {
 
         /**
          * Creates a new writer for raster data described by the given sample 
model.
-         * This method supports only the writing of a single band using all 
bits.
+         * {@code SinglePixelPackedSampleModel} packs all sample values of a 
pixel
+         * in a single data array element, one element per pixel.
+         *
+         * <h4>Limitations</h4>
+         * This method currently supports only the writing of a single band 
using all bits.
+         * This constraint is a safety for the GeoTIFF writer in case it 
didn't detected correctly
+         * that a raster need to be reformatted before encoding. One reason 
for this constraint is
+         * because if (for example) an {@code int[]} array is interpreted as 
an {@code byte[]} array
+         * with 4 components per integer, we have a byte order issue.
+         *
+         * This constraint will probably be relaxed in a future Apache SIS 
version.
          *
          * @param  sm  the sample model of the rasters to write.
-         * @return writer, or {@code null} if the given sample model is not 
supported.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
+         * @throws RasterFormatException if the given sample model is not 
supported.
          */
         public HyperRectangleWriter create(final SinglePixelPackedSampleModel 
sm) {
             bankIndices    = new int[1];   // Length is NOT the number of 
bands.
@@ -255,21 +278,58 @@ public class HyperRectangleWriter {
             pixelStride    = 1;
             scanlineStride = sm.getScanlineStride();
             final int[] d  = sm.getBitMasks();
+            /*
+             * If there is only one band, it is okay to store the values using 
the current data type.
+             * We require that all bits are used because otherwise, there is a 
risk to write garbage
+             * and we are not sure that the destination format can encode the 
mask.
+             */
+            final int dataSize = DataBuffer.getDataTypeSize(sm.getDataType());
             if (d.length == 1) {
-                final long mask = (1L << 
DataBuffer.getDataTypeSize(sm.getDataType())) - 1;
+                int mask = (int) ((1L << dataSize) - 1);
                 if ((d[0] & mask) == mask) {
                     return create(sm, null);
                 }
             }
-            return null;
+            /*
+             * If the sample model is packing many sample values in a single 
element,
+             * many formats such as GeoTIFF will interpret those values as 
bytes.
+             */
+            if ((dataSize % (d.length * Byte.SIZE)) != 0) {
+                throw new RasterFormatException(sm.toString());
+            }
+            int mask = 0xFF;
+            for (int i = d.length; --i >= 0;) {
+                if (d[i] != mask) {
+                    throw new RasterFormatException(sm.toString());
+                }
+                mask <<= Byte.SIZE;
+            }
+            requiresBigEndian = true;
+            return create(sm, null);
         }
 
         /**
          * Creates a new writer for raster data described by the given sample 
model.
-         * This method supports only the writing of a single band using all 
bits.
+         * {@code MultiPixelPackedSampleModel} represents one-banded images 
and can
+         * pack multiple one-sample pixels into one data element.
+         *
+         * <h4>Limitations</h4>
+         * As a matter of principle, this method verifies that the following 
conditions are true.
+         * They are parts of {@link MultiPixelPackedSampleModel} definition 
(sometime indirectly),
+         * so they should always be true:
+         *
+         * <ul>
+         *   <li>The number of bands is 1.</li>
+         *   <li>The pixel bit stride is the number of bits per sample.</li>
+         *   <li>All bits are used. It implies that the number of bits is 1, 
2, 4, 8, 16 or 32.</li>
+         * </ul>
+         *
+         * This method sets {@link #requiresBigEndian} to {@code true} if a 
multi-bytes data type is
+         * used for storing sample values that could have been stored 
separately with a smaller type.
          *
          * @param  sm  the sample model of the rasters to write.
-         * @return writer, or {@code null} if the given sample model is not 
supported.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
+         * @throws RasterFormatException if one of above-cited condition is 
false (should be very rare).
          */
         public HyperRectangleWriter create(final MultiPixelPackedSampleModel 
sm) {
             bankIndices    = new int[1];   // Length is NOT the number of 
bands.
@@ -278,12 +338,16 @@ public class HyperRectangleWriter {
             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, null);
+                final int sampleSize = d[0];
+                if (sm.getPixelBitStride() == sampleSize) {
+                    final int dataSize = 
DataBuffer.getDataTypeSize(sm.getDataType());
+                    if (dataSize % sampleSize == 0) {   // Check that all bits 
are used.
+                        requiresBigEndian = Math.max(sampleSize, Byte.SIZE) != 
dataSize;
+                        return create(sm, null);
+                    }
                 }
             }
-            return null;
+            throw new RasterFormatException(sm.toString());
         }
 
         /**
@@ -292,13 +356,14 @@ public class HyperRectangleWriter {
          * 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.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
+         * @throws RasterFormatException if the given sample model is not 
supported.
          */
         public HyperRectangleWriter create(final SampleModel sm) {
             if (sm instanceof ComponentSampleModel)         return 
create((ComponentSampleModel)         sm);
             if (sm instanceof SinglePixelPackedSampleModel) return 
create((SinglePixelPackedSampleModel) sm);
             if (sm instanceof MultiPixelPackedSampleModel)  return 
create((MultiPixelPackedSampleModel)  sm);
-            return null;
+            throw new RasterFormatException(sm.toString());
         }
 
         /**
@@ -306,15 +371,34 @@ public class HyperRectangleWriter {
          * 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  raster  the rasters to write.
-         * @return writer, or {@code null} if the given raster uses an 
unsupported sample model.
+         * <h4>Tile size</h4>
+         * Many formats such as GeoTIFF require that all tiles have the same 
size,
+         * including the size in the last row and last column of the tile 
matrix.
+         * In such case, {@code width} and {@code height} should be set to the 
size of the tiles to write.
+         * This size shall not be greater than the {@link SampleModel} size.
+         *
+         * @param  tile    the raster to write.
+         * @param  width   the tile width,  or -1 for the raster width.
+         * @param  height  the tile height, or -1 for the raster height.
+         * @return writer for rasters using the specified sample model (never 
{@code null}).
+         * @throws RasterFormatException if the given sample model is not 
supported.
          */
-        public HyperRectangleWriter create(final Raster raster) {
-            final Rectangle bounds = raster.getBounds();
-            region = (region != null) ? bounds.intersection(region) : bounds;
-            sampleModelTranslateX = raster.getSampleModelTranslateX();
-            sampleModelTranslateY = raster.getSampleModelTranslateY();
-            return create(raster.getSampleModel());
+        public HyperRectangleWriter create(final Raster tile, final int width, 
final int height) {
+            region = tile.getBounds();
+            if (width  >= 0) region.width  = width;
+            if (height >= 0) region.height = height;
+            sampleModelTranslateX = tile.getSampleModelTranslateX();
+            sampleModelTranslateY = tile.getSampleModelTranslateY();
+            return create(tile.getSampleModel());
+        }
+
+        /**
+         * {@return the number of banks to write}.
+         * This is not necessarily the number of bands, because the writer may 
be able
+         * to write all bands contiguously in a single call to a {@code 
write(…)} method.
+         */
+        public int numBanks() {
+            return bankIndices.length;
         }
 
         /**
@@ -343,27 +427,36 @@ public class HyperRectangleWriter {
         }
 
         /**
-         * Returns the indices of all banks to write with {@code 
HyperRectangleWriter}.
-         * This is not necessarily the bank indices of all bands, because the 
writer may be
-         * able to write all bands contiguously in a single call to a {@code 
write(…)} method.
+         * Returns the index of a bank to write with {@code 
HyperRectangleWriter}.
          * This information is valid only after a {@code create(…)} method has 
been invoked.
          *
-         * @return indices of all banks to write with {@code 
HyperRectangleWriter}.
+         * @param  i  index from 0 inclusive to {@link #numBanks()} exclusive.
+         * @return the index of a bank to write with {@code 
HyperRectangleWriter}.
          */
-        @SuppressWarnings("ReturnOfCollectionOrArrayField")
-        public int[] bankIndices() {
-            return bankIndices;
+        public int bankIndex(int i) {
+            return bankIndices[i];
         }
 
         /**
-         * Returns the offset to add to each bank to write with {@code 
HyperRectangleWriter}.
+         * Returns the offset to add to a 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}.
+         * @param  i  index from 0 inclusive to {@link #numBanks()} exclusive.
+         * @param  bufferOffset  the value of <code>{@link 
DataBuffer#getOffsets()}[bankIndex(i)]</code>.
+         * @return offset of a banks to write with {@code 
HyperRectangleWriter}.
+         */
+        public int bankOffset(int i, int bufferOffset) {
+            return Math.addExact(bankOffsets[i], bufferOffset);
+        }
+
+        /**
+         * Returns the byte order to use for writing data, or {@code null} if 
no change is needed.
+         * A specific byte order is sometime needed when {@code short} or 
{@code int} values are
+         * used for packing many bytes. This order can be ignored if, for 
example, ARGB values
+         * are really stored as integers in native byte order rather than 4 
bytes.
          */
-        @SuppressWarnings("ReturnOfCollectionOrArrayField")
-        public int[] bankOffsets() {
-            return bankOffsets;
+        public ByteOrder byteOrder(ByteOrder current) {
+            return requiresBigEndian && (current != ByteOrder.BIG_ENDIAN) ? 
ByteOrder.BIG_ENDIAN : null;
         }
     }
 
@@ -423,7 +516,8 @@ public class HyperRectangleWriter {
      * <p>Note that the direct mode is not necessarily faster.
      * If the destination is a NIO channel, Java may perform internally a copy 
to a direct buffer.
      * If the {@code output} buffer is already {@linkplain 
ByteBuffer#isDirect() direct}, it may be
-     * more efficient to set the {@code direct} argument to {@code false} for 
using that buffer.</p>
+     * more efficient to set the {@code direct} argument to {@code false} for 
using that buffer.
+     * See {@link #suggestDirect(ChannelDataOutput)} for a hint about this 
argument value.</p>
      *
      * @param  output  where to write data.
      * @param  data    data of the hyper-rectangle.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
index b3109ae75b..4c87cfe96b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.io.stream;
 
+import java.util.Arrays;
 import static java.lang.Math.addExact;
 import static java.lang.Math.subtractExact;
 import static java.lang.Math.multiplyExact;
@@ -256,6 +257,35 @@ public final class Region {
         return targetSize[dimension];
     }
 
+    /**
+     * Compares this region with the given object for equality.
+     *
+     * @return the object to compare with this region.
+     * @return whether this region and the given object are equal.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj instanceof Region) {
+            final var r = (Region) obj;
+            return length == r.length && startAt == r.startAt
+                    && Arrays.equals(targetSize, r.targetSize)
+                    && Arrays.equals(skips,      r.skips)
+                    && Arrays.equals(skipBytes,  r.skipBytes);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this region.
+     *
+     * @return a hash code value.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(targetSize) + 7*Arrays.hashCode(skips) + 
37*Arrays.hashCode(skipBytes)
+                + Long.hashCode(startAt);   // Rarely different than zero.
+    }
+
     /**
      * Returns a string representation of this region for debugging purpose.
      *
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
index 565670359c..50a3eb334d 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
@@ -23,6 +23,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.nio.Buffer;
+import java.nio.ByteOrder;
 import java.nio.file.Path;
 import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
@@ -39,6 +41,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.IllegalOpenParameterException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.util.ArraysExt;
@@ -230,4 +233,26 @@ public abstract class URIDataStoreProvider extends 
DataStoreProvider {
         }
         return false;
     }
+
+    /**
+     * Creates a new output stream and set by the order to native order if is 
was not explicitly specified by the user.
+     * The byte order is considered explicitly specified if the storage type 
is one of the types were the user could
+     * have specified that order.
+     *
+     * @param  connector  the connector to use for opening a file.
+     * @param  format     short name or abbreviation of the data format (e.g. 
"CSV", "GML", "WKT", <i>etc</i>).
+     *                    Used for information purpose in error messages if 
needed.
+     * @return the output stream.
+     * @throws DataStoreException if an error occurred while creating the 
output stream.
+     */
+    public static ChannelDataOutput openAndSetNativeByteOrder(final 
StorageConnector connector, final String format)
+            throws DataStoreException
+    {
+        final Object storage = connector.getStorage();
+        final ChannelDataOutput output = 
connector.commit(ChannelDataOutput.class, format);
+        if (output != storage && !(storage instanceof Buffer)) {
+            output.buffer.order(ByteOrder.nativeOrder());
+        }
+        return output;
+    }
 }


Reply via email to