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