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


Reply via email to