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 f3614986106f0580aca978083eaf0ae0245f866c Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Nov 4 17:30:29 2023 +0100 When writing a TIFF file, make possible (in some circumstances) to send data directly from raster to deflater, bypassing the intermediate buffer. Switch `Compression.DEFLATE` to no predictor by default for improving the chances that the direct mode can be used, and also because experiences suggest that horizontal differentiating predictor can sometime be counter-productive. --- .../apache/sis/storage/geotiff/Compression.java | 49 ++++++--- .../sis/storage/geotiff/base/Compression.java | 9 +- .../storage/geotiff/writer/CompressionChannel.java | 37 ++++++- .../sis/storage/geotiff/writer/PixelChannel.java | 8 ++ .../storage/geotiff/writer/PredictorChannel.java | 9 ++ .../sis/storage/geotiff/writer/TileMatrix.java | 112 +++++++++++---------- .../org/apache/sis/storage/geotiff/writer/ZIP.java | 38 +++++-- .../main/org/apache/sis/io/stream/ChannelData.java | 4 + .../apache/sis/io/stream/HyperRectangleWriter.java | 64 +++++++++++- .../main/org/apache/sis/util/ArgumentChecks.java | 22 ++-- 10 files changed, 254 insertions(+), 98 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Compression.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Compression.java index 149cfed4b8..160198b070 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Compression.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Compression.java @@ -66,10 +66,30 @@ public final class Compression implements Serializable { /** * Deflate compression (like ZIP format) with a default compression level and a default predictor. * This is the compression used by default by the Apache SIS GeoTIFF writer. + * + * <h4>Predictors</h4> + * The compression ratio can <em>sometime</em> be improved by the use of a predictor. + * For example instead of specifying {@code DEFLATE} directly to the {@link StorageConnector} options, + * the following can be specified: + * + * {@snippet lang="java" : + * Compression.DEFLATE.withPreductor(BaselineTIFFTagSet.PREDICTOR_HORIZONTAL_DIFFERENCING); + * } + * + * Whether the use of predictor improves or not the compression ratio depends on the image content. + * Predictors can help a lot on "smooth" images, but can also be counter-productive on heterogeneous images. + * The current Apache SIS version uses no predictor by default, but a future SIS version may try to detect + * automatically whether a predictor should be used. If a deterministic predictor is desired, + * then {@link #withPredictor(int)} should be invoked explicitly. + * + * @todo Compute Shannon Entropy with and without predictor on a few sample rows + * for deciding automatically which predictor to use. + * + * @see #withPredictor(int) */ public static final Compression DEFLATE = new Compression( org.apache.sis.storage.geotiff.base.Compression.DEFLATE, - Deflater.DEFAULT_COMPRESSION, Predictor.HORIZONTAL_DIFFERENCING); + Deflater.DEFAULT_COMPRESSION, Predictor.NONE); /** * The key for declaring the compression at store creation time. @@ -110,8 +130,9 @@ public final class Compression implements Serializable { /** * Returns an instance with the specified compression level. * The value can range from {@value Deflater#BEST_SPEED} to {@value Deflater#BEST_COMPRESSION} inclusive. - * A value of {@value Deflater#NO_COMPRESSION} means no compression. + * A value of {@value Deflater#NO_COMPRESSION} returns {@link #NONE}. * A value of {@value Deflater#DEFAULT_COMPRESSION} resets the default compression. + * This method does nothing if this compression does not support compression levels. * * @param value the new compression level (0-9), or -1 for the default compression. * @return a compression of the specified level. @@ -122,7 +143,13 @@ public final class Compression implements Serializable { * @see Deflater#NO_COMPRESSION */ public Compression withLevel(final int value) { - if (value == level) return this; + if (value == Deflater.NO_COMPRESSION) { + // Required by `TileMatrix.writeRasters(…)` assumption that `level == 0` implies no predictor. + return NONE; + } + if (value == level || !method.supportLevels()) { + return this; + } ArgumentChecks.ensureBetween("level", Deflater.DEFAULT_COMPRESSION, Deflater.BEST_COMPRESSION, value); return new Compression(method, (byte) value, predictor); } @@ -140,14 +167,11 @@ public final class Compression implements Serializable { /** * Returns an instance with the specified predictor. A predictor is a mathematical * operator that is applied to the image data before an encoding scheme is applied. - * Predictors can improve the result of some compression algorithms. + * Predictors sometime improve the result of some compression algorithms such as {@link #DEFLATE}. * * <p>The given predictor may be ignored if it is unsupported by this compression. * For example invoking this method on {@link #NONE} has no effect.</p> * - * <p>The constants defined in this {@code Compression} class are already defined - * with suitable predictors. This method usually do not need to be invoked.</p> - * * @param value one of the {@code PREDICTOR_*} constants in {@link BaselineTIFFTagSet}. * @return a compression using the specified predictor. * @throws IllegalArgumentException if the given value is not valid. @@ -155,11 +179,10 @@ public final class Compression implements Serializable { * @see BaselineTIFFTagSet#PREDICTOR_NONE * @see BaselineTIFFTagSet#PREDICTOR_HORIZONTAL_DIFFERENCING */ - public Compression withPredictor(final int value) { - if (value == predictor.code || !usePredictor()) { - return this; - } - return new Compression(method, level, Predictor.supported(value)); + public Compression withPredictor(int value) { + // `NONE` is required by `TileMatrix.writeRasters(…)` assumption that `level == 0` implies no predictor. + final Predictor p = usePredictor() ? Predictor.supported(value) : Predictor.NONE; + return p.equals(predictor) ? this : new Compression(method, level, p); } /** @@ -176,7 +199,7 @@ public final class Compression implements Serializable { * {@return whether the compression method may use predictor}. */ final boolean usePredictor() { - return !org.apache.sis.storage.geotiff.base.Compression.NONE.equals(method); + return level != 0; } /** diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java index 21309732dc..22ed417e36 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java @@ -142,7 +142,7 @@ public enum Compression { */ public static Compression valueOf(final int code) { switch (code) { - case 32946: // Fall through + case COMPRESSION_DEFLATE: // Fall through case COMPRESSION_ZLIB: return DEFLATE; case COMPRESSION_OLD_JPEG: // "old-style" JPEG, later overriden in Technical Notes 2. case COMPRESSION_JPEG: return JPEG; @@ -170,4 +170,11 @@ public enum Compression { public final boolean useNativeLibrary() { return this == DEFLATE; } + + /** + * {@return whether the compression can be configured with different levels}. + */ + public final boolean supportLevels() { + return this == DEFLATE; + } } diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java index 2515b961bb..cd74d11574 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.geotiff.writer; import java.io.IOException; +import java.nio.ByteBuffer; import org.apache.sis.storage.StorageConnector; import org.apache.sis.io.stream.ChannelDataOutput; @@ -31,22 +32,48 @@ import org.apache.sis.io.stream.ChannelDataOutput; */ abstract class CompressionChannel extends PixelChannel { /** - * Desired size of the temporary buffer where to compress data. + * The destination where to write compressed data. */ - static final int BUFFER_SIZE = StorageConnector.DEFAULT_BUFFER_SIZE / 2; + protected final ChannelDataOutput output; /** - * The destination where to write compressed data. + * Number of bytes to be compressed. */ - protected final ChannelDataOutput output; + protected final long length; /** * Creates a new channel which will compress data to the given output. * * @param output the destination of compressed data. + * @param length number of bytes to be compressed. */ - protected CompressionChannel(final ChannelDataOutput output) { + protected CompressionChannel(final ChannelDataOutput output, final long length) { this.output = output; + this.length = length; + } + + /** + * {@return a proposed buffer capacity}. + * This is an helper method for {@link #createBuffer()} implementations. + */ + final int bufferCapacity() { + /* + * Size of compressed data should be less than `length`, but we do not try to do a better + * estimation because the length will usually be limited by the maximal value below anyway. + * Those minimal and maximal capacity values are arbitrary. + */ + return Math.max((int) Math.min(length, StorageConnector.DEFAULT_BUFFER_SIZE / 2), 1024); + } + + /** + * Creates a buffer to use with this compression channel. + * The default implementation creates a buffer on heap, + * which is suitable for decompression implemented in Java. + * Decompression implemented by native libraries may prefer direct buffer. + */ + @Override + ByteBuffer createBuffer() { + return ByteBuffer.allocate(bufferCapacity()); } /** diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PixelChannel.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PixelChannel.java index 773964ac49..2d4d725a28 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PixelChannel.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PixelChannel.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.geotiff.writer; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import org.apache.sis.io.stream.ChannelDataOutput; @@ -42,6 +43,13 @@ abstract class PixelChannel implements WritableByteChannel { protected PixelChannel() { } + /** + * Creates a buffer to use with this compression channel. + * The buffer size, and whether the buffer should be direct or not, + * depends on the decompression implementation. + */ + abstract ByteBuffer createBuffer(); + /** * Writes any pending data and reset the deflater for the next tile to compress. * diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PredictorChannel.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PredictorChannel.java index 46abceb43b..8940faf7e6 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PredictorChannel.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PredictorChannel.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.geotiff.writer; import java.io.IOException; +import java.nio.ByteBuffer; import org.apache.sis.io.stream.ChannelDataOutput; import org.apache.sis.storage.geotiff.base.Predictor; @@ -47,6 +48,14 @@ abstract class PredictorChannel extends PixelChannel { this.output = output; } + /** + * Creates a buffer to use with this compression channel. + */ + @Override + final ByteBuffer createBuffer() { + return output.createBuffer(); + } + /** * Writes any pending data and reset the deflater for the next tile to compress. * 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 1bf8b24f65..6d45646d22 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 @@ -17,6 +17,7 @@ package org.apache.sis.storage.geotiff.writer; import java.util.Arrays; +import java.util.Objects; import java.io.IOException; import java.nio.ByteBuffer; import java.awt.Rectangle; @@ -174,41 +175,6 @@ public final class TileMatrix { } } - /** - * Creates the data output stream to use for writing compressed data. - * - * @param output where to write compressed bytes. - * @throws DataStoreException if the compression method is unsupported. - * @throws IOException if an error occurred while creating the data channel. - * @return the data output for compressing data, or {@code output} if uncompressed. - */ - private ChannelDataOutput createCompressionChannel(final ChannelDataOutput output, - final int pixelStride, final int scanlineStride) - throws DataStoreException, IOException - { - if (compressionLevel == 0) { - return output; - } - PixelChannel channel; - boolean isDirect = false; // `true` if using a native library which accepts NIO buffers. - switch (compression) { - case NONE: return output; - case DEFLATE: channel = new ZIP(output, compressionLevel); isDirect = true; break; - default: throw unsupported(Resources.Keys.UnsupportedCompressionMethod_1, compression); - } - switch (predictor) { - case NONE: break; - case HORIZONTAL_DIFFERENCING: { - channel = HorizontalPredictor.create(channel, type, pixelStride, scanlineStride); - break; - } - default: throw unsupported(Resources.Keys.UnsupportedPredictor_1, predictor); - } - final int capacity = CompressionChannel.BUFFER_SIZE; - ByteBuffer buffer = isDirect ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); - return new ChannelDataOutput(output.filename, channel, buffer.order(output.buffer.order())); - } - /** * Creates an exception for an unsupported configuration. */ @@ -227,11 +193,12 @@ public final class TileMatrix { */ @SuppressWarnings("null") public void writeRasters(final ChannelDataOutput output) throws DataStoreException, IOException { - ChannelDataOutput compress = null; - PixelChannel cc = null; - SampleModel sm = null; - int[] bankIndices = null; - HyperRectangleWriter rect = null; + ChannelDataOutput compOutput = null; + PixelChannel compressor = null; + SampleModel sampleModel = null; + int[] bankIndices = null; + HyperRectangleWriter rect = null; + boolean direct = false; final int minTileX = image.getMinTileX(); final int minTileY = image.getMinTileY(); for (int tileIndex = 0; tileIndex < numTiles; tileIndex++) { @@ -244,16 +211,51 @@ public final class TileMatrix { tileX += minTileX; tileY += minTileY; final Raster tile = image.getTile(tileX, tileY); - if (sm != (sm = tile.getSampleModel())) { + if (!Objects.equals(sampleModel, sampleModel = tile.getSampleModel())) { + if (compressor != null) { + compressor.close(); + compressor = null; + } + /* + * Creates the `rect` object which will be used for writing a subset of the raster data. + * This object depends on the sample model, but is usually created only once because all + * tiles should share the same sample model. + */ final var builder = new HyperRectangleWriter.Builder().region(new Rectangle(tileWidth, tileHeight)); - rect = builder.create(sm); + rect = builder.create(sampleModel); if (rect == null) { throw new UnsupportedOperationException(); // TODO: reformat using a recycled Raster. } bankIndices = builder.bankIndices(); - if (compress == null) { - compress = createCompressionChannel(output, builder.pixelStride(), builder.scanlineStride()); - if (compress != output) cc = (PixelChannel) compress.channel; + /* + * 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 + * buffer and send data directly from the raster to the `compressor` channel. Such direct + * mode allows to send large blocks of data without being constrained by the buffer size. + * It is possible only for bytes (not integers of floats) and only if the compressor does + * not modify the data (i.e. no predictor is used). + */ + if (compressionLevel != 0) { + final long length = Math.multiplyExact(builder.length(), type.bytes()); + switch (compression) { + case DEFLATE: compressor = new ZIP(output, length, compressionLevel); break; + default: throw unsupported(Resources.Keys.UnsupportedCompressionMethod_1, compression); + } + switch (predictor) { + default: throw unsupported(Resources.Keys.UnsupportedPredictor_1, predictor); + case NONE: direct = type.equals(DataType.BYTE); 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. + break; + } + } + ByteBuffer buffer = direct ? ByteBuffer.allocate(0) : compressor.createBuffer(); + 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. } } final DataBuffer buffer = tile.getDataBuffer(); @@ -264,21 +266,23 @@ public final class TileMatrix { final long position = output.getStreamPosition(); switch (type) { default: throw new AssertionError(type); - case BYTE: rect.write(compress, ((DataBufferByte) buffer).getData(b), offset); break; - case USHORT: rect.write(compress, ((DataBufferUShort) buffer).getData(b), offset); break; - case SHORT: rect.write(compress, ((DataBufferShort) buffer).getData(b), offset); break; - case INT: rect.write(compress, ((DataBufferInt) buffer).getData(b), offset); break; - case FLOAT: rect.write(compress, ((DataBufferFloat) buffer).getData(b), offset); break; - case DOUBLE: rect.write(compress, ((DataBufferDouble) buffer).getData(b), offset); break; + 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 (cc != null) { - cc.finish(compress); + if (compressor != null) { + compressor.finish(compOutput); } final int planeIndex = tileIndex + j*numTiles; offsets[planeIndex] = position; lengths[planeIndex] = Math.toIntExact(Math.subtractExact(output.getStreamPosition(), position)); } } - if (cc != null) cc.close(); + if (compressor != null) { + compressor.close(); + } } } diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ZIP.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ZIP.java index abb85234fd..f06aef2ca9 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ZIP.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ZIP.java @@ -19,7 +19,6 @@ package org.apache.sis.storage.geotiff.writer; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.Deflater; -import org.apache.sis.util.ArraysExt; import org.apache.sis.io.stream.ChannelDataOutput; @@ -39,16 +38,24 @@ final class ZIP extends CompressionChannel { * Creates a new channel which will compress data to the given output. * * @param output the destination of compressed data. + * @param length number of bytes to be compressed. * @param level the compression level. */ - public ZIP(final ChannelDataOutput output, final int level) { - super(output); + public ZIP(final ChannelDataOutput output, final long length, final int level) { + super(output, length); deflater = new Deflater(level); - deflater.setStrategy(Deflater.FILTERED); } /** - * Compresses some bytes from the given buffer to the {@linkplain #output output}. + * Creates a buffer to use with this compression channel. + */ + @Override + final ByteBuffer createBuffer() { + return ByteBuffer.allocateDirect(bufferCapacity()); + } + + /** + * Compresses all remaining bytes from the given buffer to the {@linkplain #output output}. * * @param source the buffer from which bytes are to be transferred. * @return the number of uncompressed bytes written. @@ -58,20 +65,33 @@ final class ZIP extends CompressionChannel { public int write(final ByteBuffer source) throws IOException { final ByteBuffer target = output.buffer; final int start = source.position(); + int remaining = source.remaining(); deflater.setInput(source); - int remaining; - while ((remaining = source.remaining()) > 0) { + /* + * If the given buffer is the last input, notify the deflater about that. It is not strictly + * necessary to do this notification here because `finish(…)` will flush pending data anyway. + * But providing this information in advance may hopefully help the deflater. The condition + * can be true when raster data are sent directly to this compressor, without `this.buffer`. + */ + if (deflater.getBytesRead() >= length - remaining) { + deflater.finish(); + } + while (remaining > 0) { assert !deflater.needsInput(); output.ensureBufferAccepts(Math.min(remaining, target.capacity())); target.limit(target.capacity()); // Allow the use of all available space. deflater.deflate(target); target.limit(target.position()); // Bytes after the position are not valid. + remaining = source.remaining(); } return source.position() - start; // Number from caller's perspective (it doesn't know about compression). } /** * Writes any pending data and reset the deflater for the next tile to compress. + * Usually there is some remaining bytes in the {@code owner} buffer. + * The call to {@code super.finish(owner)} will indirectly performs one last call + * to {@link #write(ByteBuffer)} with the {@link Deflater#finish()} flag set. * * @param owner the data output which is writing in this channel. * @throws IOException if an error occurred while writing to the underlying output channel. @@ -82,10 +102,8 @@ final class ZIP extends CompressionChannel { super.finish(owner); final ByteBuffer target = output.buffer; while (!deflater.finished()) { - output.ensureBufferAccepts(Math.min(target.capacity(), BUFFER_SIZE)); + output.ensureBufferAccepts(Math.max(target.capacity() / 2, 1)); target.limit(target.capacity()); // Allow the use of all available space. - deflater.setInput(ArraysExt.EMPTY_BYTE); - deflater.finish(); deflater.deflate(target); target.limit(target.position()); // Bytes after the position are not valid. } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java index d9da89481f..e035857642 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java @@ -504,6 +504,10 @@ public abstract class ChannelData implements Markable { * @throws IOException if the implementation chooses to stop the process. */ protected void onEmptyTransfer() throws IOException { + if (buffer.capacity() == 0) { + // For avoiding never-ending loop. + throw new IOException(Errors.format(Errors.Keys.Uninitialized_1, "buffer")); + } try { Thread.sleep(200); } catch (InterruptedException e) { 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 2e737fe89e..4effdeeded 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 @@ -17,6 +17,7 @@ package org.apache.sis.io.stream; import java.io.IOException; +import java.nio.ByteBuffer; import java.awt.Rectangle; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; @@ -96,6 +97,14 @@ public final class HyperRectangleWriter { * @author Martin Desruisseaux (Geomatys) */ public static final class Builder { + /** + * Number of elements (not necessarily bytes) contained in this hyper-rectangle. + * The number of bytes to write will be this length multiplied by element size. + * + * @see #length() + */ + private long length; + /** * Number of elements (not necessarily bytes) between a pixel and the next pixel. * @@ -160,7 +169,9 @@ public final class HyperRectangleWriter { } regionLower[0] = Math.multiplyExact(regionLower[0], pixelStride); regionUpper[0] = Math.multiplyExact(regionUpper[0], pixelStride); - return new HyperRectangleWriter(new Region(sourceSize, regionLower, regionUpper, subsampling)); + var subset = new Region(sourceSize, regionLower, regionUpper, subsampling); + length = subset.length; + return new HyperRectangleWriter(subset); } /** @@ -266,6 +277,15 @@ public final class HyperRectangleWriter { return null; } + /** + * {@return the total number of elements contained in the hyper-rectangle}. + * The number of bytes to write will be this length multiplied by element size. + * This information is valid only after a {@code create(…)} method has been invoked. + */ + public long length() { + return length; + } + /** * {@return the number of elements (not necessarily bytes) between a pixel and the next pixel}. * This information is valid only after a {@code create(…)} method has been invoked. @@ -331,19 +351,55 @@ public final class HyperRectangleWriter { return -1; } + /** + * Returns a suggested value for the {@code direct} argument of {@code write(…, byte[], …)}. + * The suggestion is based on whether the output buffer is direct or not. + * + * @param output the output which will be given in argument to the {@code write(…)} methods. + * @return suggested value for the {@code direct} boolean argument. + */ + public boolean suggestDirect(final ChannelDataOutput output) { + // Really ! because we want to use the direct buffer if it exists. + return !output.buffer.isDirect() && output.buffer.capacity() <= contiguousDataLength; + } + /** * Writes an hyper-rectangle with the shape described at construction time. + * If the {@code direct} argument is {@code true}, then this method writes + * directly in the {@link ChannelDataOutput#channel} without copying bytes + * to the {@link ChannelDataOutput#buffer}. + * + * <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> * * @param output where to write data. * @param data data of the hyper-rectangle. * @param offset offset to add to array index. * @throws IOException if an error occurred while writing the data. */ - public void write(final ChannelDataOutput output, final byte[] data, int offset) throws IOException { + public void write(final ChannelDataOutput output, final byte[] data, int offset, final boolean direct) throws IOException { offset = startAt(offset); final int[] count = count(); - do output.write(data, offset, contiguousDataLength); - while ((offset = next(offset, count)) >= 0); + if (direct) { + final ByteBuffer buffer = ByteBuffer.wrap(data, offset, contiguousDataLength); + offset = 0; + output.flush(); + do { + buffer.limit(offset + contiguousDataLength).position(offset); + do { + final int n = output.channel.write(buffer); + output.moveBufferForward(n); + if (n == 0) { + output.onEmptyTransfer(); + } + } while (buffer.hasRemaining()); + } while ((offset = next(offset, count)) >= 0); + } else { + do output.write(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } } /** diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java index 7fde483705..853fb867a1 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java @@ -652,11 +652,11 @@ public final class ArgumentChecks extends Static { * This method performs the same check than {@link #ensureBetween(String, int, int, int) * ensureBetween(…)}, but the error message is different in case of failure. * - * @param name the name of the argument to be checked. Used only if an exception is thrown. - * @param named whether to use {@code name} as the name of a collection or array argument. - * @param min the minimal size (inclusive), or 0 if none. - * @param max the maximal size (inclusive), or {@link Integer#MAX_VALUE} if none. - * @param count the number of user-specified arguments, collection size or array length to be checked. + * @param name the name of the argument to be checked. Used only if an exception is thrown. + * @param collection {@code true} if {@code name} is a collection, or {@code false} for a variable argument list. + * @param min the minimal size (inclusive), or 0 if none. + * @param max the maximal size (inclusive), or {@link Integer#MAX_VALUE} if none. + * @param count the number of user-specified arguments, collection size or array length to be checked. * @throws IllegalArgumentException if the given value is not in the given range. * * @see #ensureBetween(String, int, int, int) @@ -664,20 +664,20 @@ public final class ArgumentChecks extends Static { * * @since 1.4 */ - public static void ensureCountBetween(final String name, final boolean named, final int min, final int max, final int count) - throws IllegalArgumentException + public static void ensureCountBetween(final String name, final boolean collection, + final int min, final int max, final int count) throws IllegalArgumentException { final String message; if (count < min) { if (count == 0) { message = Errors.format(Errors.Keys.EmptyArgument_1, name); } else { - message = named ? Errors.format(Errors.Keys.TooFewCollectionElements_3, name, min, count) - : Errors.format(Errors.Keys.TooFewArguments_2, min, count); + message = collection ? Errors.format(Errors.Keys.TooFewCollectionElements_3, name, min, count) + : Errors.format(Errors.Keys.TooFewArguments_2, min, count); } } else if (count > max) { - message = named ? Errors.format(Errors.Keys.TooManyCollectionElements_3, name, max, count) - : Errors.format(Errors.Keys.TooManyArguments_2, max, count); + message = collection ? Errors.format(Errors.Keys.TooManyCollectionElements_3, name, max, count) + : Errors.format(Errors.Keys.TooManyArguments_2, max, count); } else { return; }