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 313bfa0ac1a207092290919a9b60600c60f8bbc6
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Oct 30 22:44:25 2023 +0100

    Add compression support for GeoTIFF. Only "Deflate" is supported for now.
---
 .../main/module-info.java                          |   2 +-
 .../apache/sis/storage/geotiff/Compression.java    | 155 +++++++++++++++++++++
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  17 ++-
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |  12 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |  18 ++-
 .../sis/storage/geotiff/base/Compression.java      |   2 +-
 .../storage/geotiff/writer/CompressionChannel.java |  84 +++++++++++
 .../sis/storage/geotiff/writer/TileMatrix.java     | 102 ++++++++++----
 .../org/apache/sis/storage/geotiff/writer/ZIP.java | 102 ++++++++++++++
 .../org/apache/sis/storage/geotiff/WriterTest.java |   1 +
 .../apache/sis/io/stream/ChannelDataOutput.java    |  10 ++
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 14 files changed, 475 insertions(+), 37 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java 
b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
index a2725a5357..105625c8c8 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
@@ -22,7 +22,7 @@
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Minh Chinh Vu (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.8
  */
 module org.apache.sis.storage.geotiff {
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
new file mode 100644
index 0000000000..51532f6ffa
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Compression.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import java.io.Serializable;
+import java.util.OptionalInt;
+import java.util.zip.Deflater;
+import org.apache.sis.setup.OptionKey;
+import org.apache.sis.util.internal.Strings;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.io.stream.InternalOptionKey;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * The compression method used for writing GeoTIFF files.
+ * This class specifies only the compressions supported by the Apache SIS 
writer.
+ * The Apache SIS reader supports more compression methods, but they are not 
listed in this class.
+ *
+ * <p>The compression to use can be specified as an option when opening the 
data store.
+ * For example for writing a TIFF file without compression, the following code 
can be used:</p>
+ *
+ * {@snippet lang="java" :
+ *     var file = Path.of("my_output_file.tiff");
+ *     var connector = new StorageConnector(file);
+ *     connector.setOption(Compression.OPTION_KEY, Compression.NONE);
+ *     try (GeoTiffStore ds = new GeoTiffStore(null, connector)) {
+ *         // Write data here.
+ *     }
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+public final class Compression implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 3916905136793784898L;
+
+    /**
+     * No compression, but pack data into bytes as tightly as possible.
+     */
+    public static final Compression NONE = new 
Compression(org.apache.sis.storage.geotiff.base.Compression.NONE, 0);
+
+    /**
+     * Deflate compression, like ZIP format.
+     * This is the default compression method.
+     */
+    public static final Compression DEFLATE = new 
Compression(org.apache.sis.storage.geotiff.base.Compression.DEFLATE, 
Deflater.DEFAULT_COMPRESSION);
+
+    /**
+     * The key for declaring the compression at store creation time.
+     * See class Javadoc for usage example.
+     *
+     * @see StorageConnector#setOption(OptionKey, Object)
+     */
+    public static final OptionKey<Compression> OPTION_KEY = new 
InternalOptionKey<>("TIFF_COMPRESSION", Compression.class);
+
+    /**
+     * The compression method.
+     */
+    final org.apache.sis.storage.geotiff.base.Compression method;
+
+    /**
+     * The compression level, or -1 for default.
+     */
+    final int level;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param  method  the compression method.
+     */
+    private Compression(final org.apache.sis.storage.geotiff.base.Compression 
method, final int level) {
+        this.method = method;
+        this.level  = level;
+    }
+
+    /**
+     * Returns an instance with the specified compression level.
+     * Value 0 means no compression. A value of -1 resets the default 
compression.
+     *
+     * @param  value  the new compression level (0-9).
+     * @return a compression of the specified level.
+     */
+    public Compression withLevel(final int value) {
+        if (value == level) return this;
+        ArgumentChecks.ensureBetween("level", Deflater.DEFAULT_COMPRESSION, 
Deflater.BEST_COMPRESSION, value);
+        return new Compression(method, value);
+    }
+
+    /**
+     * Returns the current compression level.
+     *
+     * @return the current compression level, or an empty value for the 
default level.
+     */
+    public OptionalInt level() {
+        return (level >= 0) ? OptionalInt.of(level) : OptionalInt.empty();
+    }
+
+    /*
+     * TODO: add `withPredictor(Predictor)` method.
+     */
+
+    /**
+     * Compares this compression with the given object for equality.
+     *
+     * @param  other  the object to compare with this compression.
+     * @return whether the two objects are equal.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other instanceof Compression) {
+            final var c = (Compression) other;
+            return method.equals(c.method) && level == c.level;
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this compression.
+     *
+     * @return a hash code value.
+     */
+    @Override
+    public int hashCode() {
+        return method.hashCode() + level;
+    }
+
+    /**
+     * Returns a string representation of this compression.
+     *
+     * @return a string representation for debugging purposes.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(Compression.class, "method", method,
+                "level", (level != 0) ? Integer.valueOf(level) : null);
+    }
+}
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 005a7855cd..5385f1406f 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
@@ -103,6 +103,11 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      */
     private volatile Writer writer;
 
+    /**
+     * The compression to apply when writing tiles, or {@code null} if 
unspecified.
+     */
+    final Compression compression;
+
     /**
      * The locale to use for formatting metadata. This is not necessarily the 
same as {@link #getLocale()},
      * which is about formatting error messages. A null value means 
"unlocalized", which is usually English.
@@ -240,10 +245,11 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         final Charset encoding = connector.getOption(OptionKey.ENCODING);
         this.encoding = (encoding != null) ? encoding : 
StandardCharsets.US_ASCII;
 
-        dataLocale = connector.getOption(OptionKey.LOCALE);
-        timezone   = connector.getOption(OptionKey.TIMEZONE);
-        location   = connector.getStorageAs(URI.class);
-        path       = connector.getStorageAs(Path.class);
+        compression = connector.getOption(Compression.OPTION_KEY);
+        dataLocale  = connector.getOption(OptionKey.LOCALE);
+        timezone    = connector.getOption(OptionKey.TIMEZONE);
+        location    = connector.getStorageAs(URI.class);
+        path        = connector.getStorageAs(Path.class);
         try {
             if (URIDataStore.Provider.isWritable(connector, true)) {
                 ChannelDataOutput output = 
connector.commit(ChannelDataOutput.class, Constants.GEOTIFF);
@@ -323,6 +329,9 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
                 if (!options.isEmpty()) {
                     
param.parameter(GeoTiffStoreProvider.OPTIONS).setValue(options.toArray(GeoTiffOption[]::new));
                 }
+                if (compression != null) {
+                    
param.parameter(GeoTiffStoreProvider.COMPRESSION).setValue(compression);
+                }
             }
         }
         return Optional.ofNullable(param);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
index b7a5c5d723..409faf5e23 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
@@ -78,14 +78,20 @@ public class GeoTiffStoreProvider extends DataStoreProvider 
{
      */
     static final String OPTIONS = "options";
 
+    /**
+     * Name of the parameter for specifying the compression.
+     */
+    static final String COMPRESSION = "compression";
+
     /**
      * The parameter descriptor to be returned by {@link #getOpenParameters()}.
      */
     private static final ParameterDescriptorGroup OPEN_DESCRIPTOR;
     static {
-        final var builder = new ParameterBuilder();
-        final var options = 
builder.addName(OPTIONS).setDescription(Vocabulary.formatInternational(Vocabulary.Keys.Options)).create(GeoTiffOption[].class,
 null);
-        OPEN_DESCRIPTOR = 
builder.addName(Constants.GEOTIFF).createGroup(URIDataStore.Provider.LOCATION_PARAM,
 options);
+        final var builder     = new ParameterBuilder();
+        final var options     = 
builder.addName(OPTIONS).setDescription(Vocabulary.formatInternational(Vocabulary.Keys.Options)).create(GeoTiffOption[].class,
 null);
+        final var compression = 
builder.addName(COMPRESSION).setDescription(Vocabulary.formatInternational(Vocabulary.Keys.Compression)).create(Compression.class,
 null);
+        OPEN_DESCRIPTOR = 
builder.addName(Constants.GEOTIFF).createGroup(URIDataStore.Provider.LOCATION_PARAM,
 options, compression);
     }
 
     /**
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 f9644ed632..f760802872 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
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Deque;
 import java.util.Queue;
 import java.util.Set;
+import java.util.zip.Deflater;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
@@ -48,6 +49,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Fraction;
+import org.apache.sis.storage.geotiff.base.Compression;
 import org.apache.sis.storage.geotiff.writer.TagValue;
 import org.apache.sis.storage.geotiff.writer.TileMatrix;
 import org.apache.sis.storage.geotiff.writer.GeoEncoder;
@@ -365,6 +367,18 @@ final class Writer extends IOBase implements Flushable {
          */
         final Fraction xres = new Fraction(1, 1);       // TODO
         final Fraction yres = xres;
+        /*
+         * Compression.
+         */
+        final Compression compression;
+        final int compressionLevel;
+        if (store.compression != null) {
+            compressionLevel = store.compression.level;
+            compression = (compressionLevel != 0) ? store.compression.method : 
Compression.NONE;
+        } else {
+            compression = Compression.DEFLATE;              // Default value 
documented in `Compression` Javadoc.
+            compressionLevel = Deflater.DEFAULT_COMPRESSION;
+        }
         /*
          * If the image has any unsupported feature, the exception should have 
been thrown before this point.
          * Now start writing the entries. The entries in an IFD must be sorted 
in ascending order by tag code.
@@ -380,7 +394,7 @@ final class Writer extends IOBase implements Flushable {
         writeTag((short) TAG_IMAGE_WIDTH,                (short) 
TIFFTag.TIFF_LONG,  image.visibleBands.getWidth());
         writeTag((short) TAG_IMAGE_LENGTH,               (short) 
TIFFTag.TIFF_LONG,  image.visibleBands.getHeight());
         writeTag((short) TAG_BITS_PER_SAMPLE,            (short) 
TIFFTag.TIFF_SHORT, bitsPerSample);
-        writeTag((short) TAG_COMPRESSION,                (short) 
TIFFTag.TIFF_SHORT, COMPRESSION_NONE);
+        writeTag((short) TAG_COMPRESSION,                (short) 
TIFFTag.TIFF_SHORT, compression.code);
         writeTag((short) TAG_PHOTOMETRIC_INTERPRETATION, (short) 
TIFFTag.TIFF_SHORT, colorInterpretation);
         writeTag((short) TAG_DOCUMENT_NAME,              /* TIFF_ASCII */      
      mf.series);
         writeTag((short) TAG_IMAGE_DESCRIPTION,          /* TIFF_ASCII */      
      mf.title);
@@ -399,7 +413,7 @@ final class Writer extends IOBase implements Flushable {
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
             writeColorPalette((IndexColorModel) 
image.visibleBands.getColorModel(), 1L << bitsPerSample[0]);
         }
-        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD);
+        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD, compression, compressionLevel);
         writeTag((short) TAG_TILE_WIDTH,  (short) TIFFTag.TIFF_LONG, 
tiling.tileWidth);
         writeTag((short) TAG_TILE_LENGTH, (short) TIFFTag.TIFF_LONG, 
tiling.tileHeight);
         tiling.offsetsTag = writeTag((short) TAG_TILE_OFFSETS, tiling.offsets);
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 17f0b0ab49..629b895585 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
@@ -121,7 +121,7 @@ public enum Compression {
     /**
      * The TIFF code for this compression.
      */
-    final int code;
+    public final int code;
 
     /**
      * Creates a new compression enumeration.
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
new file mode 100644
index 0000000000..46bcb56b4a
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff.writer;
+
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.io.stream.ChannelDataOutput;
+
+
+/**
+ * Deflater using a temporary buffer where to compress data before writing to 
the channel.
+ * This class does not need to care about subsampling.
+ *
+ * <p>The {@link #close()} method shall be invoked when this channel is no 
longer used.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+abstract class CompressionChannel implements WritableByteChannel {
+    /**
+     * Desired size of the temporary buffer where to compress data.
+     */
+    static final int BUFFER_SIZE = StorageConnector.DEFAULT_BUFFER_SIZE / 2;
+
+    /**
+     * The destination where to write compressed data.
+     */
+    protected final ChannelDataOutput output;
+
+    /**
+     * Creates a new channel which will compress data to the given output.
+     *
+     * @param  output  the destination of compressed data.
+     */
+    protected CompressionChannel(final ChannelDataOutput output) {
+        this.output = output;
+    }
+
+    /**
+     * Tells whether this channel is still open.
+     */
+    @Override
+    public final boolean isOpen() {
+        return output.channel.isOpen();
+    }
+
+    /**
+     * Writes any pending data and reset the deflater for the next tile to 
compress.
+     *
+     * @param  owner  the data output which is writing in this channel.
+     * @throws IOException if an error occurred while writing to the 
underlying output channel.
+     */
+    public void finish(final ChannelDataOutput owner) throws IOException {
+        assert owner.channel == this;
+        owner.flush();
+        owner.clear();
+    }
+
+    /**
+     * Releases resources used by this channel, but <strong>without</strong> 
closing the {@linkplain #output} channel.
+     * The {@linkplain #output} channel is not closed by this operation 
because it will typically be needed again for
+     * compressing other tiles.
+     *
+     * @throws IOException if an error occurred while flushing last data to 
the channel.
+     */
+    @Override
+    public void close() throws IOException {
+        // Do NOT close `output`.
+    }
+}
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 ed080fb0ef..f3cda439a8 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
@@ -18,6 +18,7 @@ package org.apache.sis.storage.geotiff.writer;
 
 import java.util.Arrays;
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -36,6 +37,9 @@ import org.apache.sis.image.DataType;
 import org.apache.sis.util.internal.Numerics;
 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.geotiff.base.Compression;
+import org.apache.sis.storage.geotiff.base.Resources;
 
 
 /**
@@ -95,22 +99,35 @@ public final class TileMatrix {
      */
     public TagValue offsetsTag, lengthsTag;
 
+    /**
+     * The compression to use for writing tiles.
+     */
+    private final Compression compression;
+
+    /**
+     * The compression level, or -1 for default.
+     */
+    private final int compressionLevel;
+
     /**
      * Creates a new set of information about tiles to write.
      *
-     * @param image          the image to write.
-     * @param numBands       the number of bands.
-     * @param bitsPerSample  number of bits per sample to write.
-     * @param isPlanar       whether the planar configuration is to store 
bands in separated planes.
-     * @param offsetIFD      offset in {@link ChannelDataOutput} where the IFD 
starts.
+     * @param image             the image to write.
+     * @param numPlanes         the number of banks (plane in TIFF 
terminology).
+     * @param bitsPerSample     number of bits per sample to write.
+     * @param offsetIFD         offset in {@link ChannelDataOutput} where the 
IFD starts.
+     * @param compression       the compression method to apply.
+     * @param compressionLevel  compression level (0-9), or -1 for the default.
      */
     public TileMatrix(final RenderedImage image, final int numPlanes, final 
int[] bitsPerSample,
-                      final long offsetIFD)
+                      final long offsetIFD, final Compression compression, 
final int compressionLevel)
     {
         final int pixelSize, numArrays;
-        this.offsetIFD = offsetIFD;
-        this.numPlanes = numPlanes;
-        this.image     = image;
+        this.offsetIFD        = offsetIFD;
+        this.numPlanes        = numPlanes;
+        this.image            = image;
+        this.compression      = compression;
+        this.compressionLevel = compressionLevel;
         type       = DataType.forBands(image);
         tileWidth  = image.getTileWidth();
         tileHeight = image.getTileHeight();
@@ -141,28 +158,57 @@ 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)
+            throws DataStoreException, IOException
+    {
+        final CompressionChannel 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 new DataStoreException(Resources.forLocale(null)
+                    .getString(Resources.Keys.UnsupportedCompressionMethod_1, 
compression));
+        }
+        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()));
+    }
+
     /**
      * Writes 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 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 
IOException {
+    public void writeRasters(final ChannelDataOutput output) throws 
DataStoreException, IOException {
+        final ChannelDataOutput compress = createCompressionChannel(output);
+        final CompressionChannel cc = (compress != output) ? 
(CompressionChannel) compress.channel : null;
+
         SampleModel sm = null;
         int[] bankIndices = null;
         HyperRectangleWriter rect = null;
         final int minTileX = image.getMinTileX();
         final int minTileY = image.getMinTileY();
         int planeIndex = 0;
-        for (int i=0; i < offsets.length; i++) {
+        while (planeIndex < offsets.length) {
             /*
              * In current implementation, we iterate from left to right then 
top to bottom.
              * But a future version could use Hilbert iterator (for example).
              */
-            int tileX = i % numXTiles;
-            int tileY = i / numXTiles;
+            final int tileIndex = planeIndex / numPlanes;
+            int tileX = tileIndex % numXTiles;
+            int tileY = tileIndex / numXTiles;
             tileX += minTileX;
             tileY += minTileY;
             final Raster tile = image.getTile(tileX, tileY);
@@ -186,25 +232,29 @@ public final class TileMatrix {
             if (rect == null) {
                 throw new UnsupportedOperationException();      // TODO: 
reformat using a recycled Raster.
             }
-            final long position = output.getStreamPosition();
             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 = bufferOffsets[b];
+            for (int j=0; j<numPlanes; j++) {
+                final int  b        = bankIndices[j];
+                final int  offset   = bufferOffsets[b];
+                final long position = output.getStreamPosition();
                 switch (type) {
-                    case BYTE:   rect.write(output, ((DataBufferByte)   
buffer).getData(b), offset); break;
-                    case USHORT: rect.write(output, ((DataBufferUShort) 
buffer).getData(b), offset); break;
-                    case SHORT:  rect.write(output, ((DataBufferShort)  
buffer).getData(b), offset); break;
-                    case INT:    rect.write(output, ((DataBufferInt)    
buffer).getData(b), offset); break;
-                    case FLOAT:  rect.write(output, ((DataBufferFloat)  
buffer).getData(b), offset); break;
-                    case DOUBLE: rect.write(output, ((DataBufferDouble) 
buffer).getData(b), offset); break;
+                    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;
+                }
+                if (cc != null) {
+                    cc.finish(compress);
                 }
+                offsets[planeIndex] = position;
+                lengths[planeIndex] = 
Math.toIntExact(Math.subtractExact(output.getStreamPosition(), position));
+                planeIndex++;
             }
-            offsets[planeIndex] = position;
-            lengths[planeIndex] = 
Math.toIntExact(Math.subtractExact(output.getStreamPosition(), position));
-            planeIndex++;
         }
+        if (cc != null) cc.close();
         if (planeIndex != offsets.length) {
             throw new AssertionError();                 // Should never happen.
         }
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
new file mode 100644
index 0000000000..abb85234fd
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ZIP.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.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;
+
+
+/**
+ * Deflater for values encoded with the "Deflate" compression.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class ZIP extends CompressionChannel {
+    /**
+     * Access to the ZLIB compression library.
+     * Must be released by call to {@link Deflater#end()} after decompression 
is completed.
+     */
+    private final Deflater deflater;
+
+    /**
+     * Creates a new channel which will compress data to the given output.
+     *
+     * @param  output  the destination of compressed data.
+     * @param  level   the compression level.
+     */
+    public ZIP(final ChannelDataOutput output, final int level) {
+        super(output);
+        deflater = new Deflater(level);
+        deflater.setStrategy(Deflater.FILTERED);
+    }
+
+    /**
+     * Compresses some 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.
+     * @throws IOException if an error occurred while writing to the 
underlying output channel.
+     */
+    @Override
+    public int write(final ByteBuffer source) throws IOException {
+        final ByteBuffer target = output.buffer;
+        final int start = source.position();
+        deflater.setInput(source);
+        int remaining;
+        while ((remaining = source.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.
+        }
+        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.
+     *
+     * @param  owner  the data output which is writing in this channel.
+     * @throws IOException if an error occurred while writing to the 
underlying output channel.
+     */
+    @Override
+    public void finish(final ChannelDataOutput owner) throws IOException {
+        deflater.finish();
+        super.finish(owner);
+        final ByteBuffer target = output.buffer;
+        while (!deflater.finished()) {
+            output.ensureBufferAccepts(Math.min(target.capacity(), 
BUFFER_SIZE));
+            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.
+        }
+        deflater.reset();
+    }
+
+    /**
+     * Releases resources used by the deflater.
+     */
+    @Override
+    public void close() {
+        deflater.end();
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
index 6995521cc0..e0d6c2ec74 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
@@ -146,6 +146,7 @@ public final class WriterTest extends TestCase {
         var d = new ChannelDataOutput("TIFF", output, 
ByteBuffer.allocate(random.nextInt(128) + 20).order(order));
         var c = new StorageConnector(d);
         c.setOption(GeoTiffOption.OPTION_KEY, options);
+        c.setOption(Compression.OPTION_KEY, Compression.NONE);
         store = new GeoTiffStore(null, c);
         data  = output.toBuffer().order(order);
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
index 08300cdf1b..c88a74e8f0 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
@@ -916,4 +916,14 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
          * (see ChannelDataInput.yield(…) for code example). For now it is not 
needed.
          */
     }
+
+    /**
+     * Clears the buffer and set the position to 0.
+     * This method does not read or write any byte.
+     */
+    public final void clear() {
+        buffer.clear().limit(0);
+        bufferOffset = 0;
+        bitPosition  = 0;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
index 1ec1a11bb2..c56c3ff649 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
@@ -234,6 +234,11 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short Commands = 31;
 
+        /**
+         * Compression
+         */
+        public static final short Compression = 273;
+
         /**
          * Configuration
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
index 471578feb2..3c760931aa 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
@@ -50,6 +50,7 @@ Color                   = Color
 Colors                  = Colors
 ColorIndex              = Color index
 Commands                = Commands
+Compression             = Compression
 Controls                = Controls
 Configuration           = Configuration
 Constants               = Constants
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
index a8ec92bc4b..0061fa151a 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -57,6 +57,7 @@ Color                   = Couleur
 Colors                  = Couleurs
 ColorIndex              = Indice de couleur
 Commands                = Commandes
+Compression             = Compression
 Controls                = Contr\u00f4les
 Configuration           = Configuration
 Constants               = Constantes


Reply via email to