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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 374367cabf Add support for predictor before writing images in a 
GeoTIFF file.
374367cabf is described below

commit 374367cabffa857d46dffcd20360582b56fff1a6
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Oct 31 12:22:31 2023 +0100

    Add support for predictor before writing images in a GeoTIFF file.
---
 .../apache/sis/storage/geotiff/Compression.java    |  98 +++++-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  43 ++-
 .../org/apache/sis/storage/geotiff/Writer.java     |  30 +-
 .../sis/storage/geotiff/base/Compression.java      |   4 +
 .../apache/sis/storage/geotiff/base/Predictor.java |  41 ++-
 .../geotiff/inflater/HorizontalPredictor.java      |   4 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |   2 +-
 .../storage/geotiff/inflater/PredictorChannel.java |   2 +
 .../storage/geotiff/writer/CompressionChannel.java |   9 +-
 .../geotiff/writer/HorizontalPredictor.java        | 390 +++++++++++++++++++++
 .../sis/storage/geotiff/writer/PixelChannel.java   |  52 +++
 ...mpressionChannel.java => PredictorChannel.java} |  61 ++--
 .../sis/storage/geotiff/writer/TileMatrix.java     |  62 +++-
 .../apache/sis/io/stream/HyperRectangleWriter.java | 202 +++++++----
 14 files changed, 817 insertions(+), 183 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 24470c5d13..149cfed4b8 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
@@ -20,10 +20,11 @@ import java.io.Serializable;
 import java.util.OptionalInt;
 import java.util.zip.Deflater;
 import org.apache.sis.setup.OptionKey;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.internal.Strings;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.geotiff.base.Predictor;
 import org.apache.sis.io.stream.InternalOptionKey;
-import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -43,6 +44,8 @@ import org.apache.sis.util.ArgumentChecks;
  *     }
  *     }
  *
+ * If no compression is explicitly specified, Apache SIS uses by default the 
{@link #DEFLATE} compression.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  * @since   1.5
@@ -56,13 +59,17 @@ public final class Compression implements Serializable {
     /**
      * 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);
+    public static final Compression NONE = new Compression(
+            org.apache.sis.storage.geotiff.base.Compression.NONE,
+            0, Predictor.NONE);
 
     /**
-     * Deflate compression, like ZIP format.
-     * This is the default compression method.
+     * 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.
      */
-    public static final Compression DEFLATE = new 
Compression(org.apache.sis.storage.geotiff.base.Compression.DEFLATE, 
Deflater.DEFAULT_COMPRESSION);
+    public static final Compression DEFLATE = new Compression(
+            org.apache.sis.storage.geotiff.base.Compression.DEFLATE,
+            Deflater.DEFAULT_COMPRESSION, Predictor.HORIZONTAL_DIFFERENCING);
 
     /**
      * The key for declaring the compression at store creation time.
@@ -78,35 +85,51 @@ public final class Compression implements Serializable {
     final org.apache.sis.storage.geotiff.base.Compression method;
 
     /**
-     * The compression level, or -1 for default.
+     * The compression level from 0 to 9 inclusive, or -1 for default.
      */
     final int level;
 
+    /**
+     * The predictor to apply before compression.
+     */
+    final Predictor predictor;
+
     /**
      * Creates a new instance.
      *
-     * @param  method  the compression method.
+     * @param  method     the compression method.
+     * @param  level      the compression level, or -1 for default.
+     * @param  predictor  the predictor to apply before compression.
      */
-    private Compression(final org.apache.sis.storage.geotiff.base.Compression 
method, final int level) {
-        this.method = method;
-        this.level  = level;
+    private Compression(final org.apache.sis.storage.geotiff.base.Compression 
method, final int level, final Predictor predictor) {
+        this.method    = method;
+        this.level     = level;
+        this.predictor = predictor;
     }
 
     /**
      * Returns an instance with the specified compression level.
-     * Value 0 means no compression. A value of -1 resets the default 
compression.
+     * 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#DEFAULT_COMPRESSION} resets the default 
compression.
      *
-     * @param  value  the new compression level (0-9).
+     * @param  value  the new compression level (0-9), or -1 for the default 
compression.
      * @return a compression of the specified level.
+     * @throws IllegalArgumentException if the given value is not in the 
expected range.
+     *
+     * @see Deflater#BEST_SPEED
+     * @see Deflater#BEST_COMPRESSION
+     * @see Deflater#NO_COMPRESSION
      */
     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);
+        return new Compression(method, (byte) value, predictor);
     }
 
     /**
      * Returns the current compression level.
+     * The returned value is between 0 and 9 inclusive.
      *
      * @return the current compression level, or an empty value for the 
default level.
      */
@@ -114,9 +137,47 @@ public final class Compression implements Serializable {
         return (level >= 0) ? OptionalInt.of(level) : OptionalInt.empty();
     }
 
-    /*
-     * TODO: add `withPredictor(Predictor)` method.
+    /**
+     * 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.
+     *
+     * <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.
+     *
+     * @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));
+    }
+
+    /**
+     * Returns the current predictor.
+     * The returned value is one of the {@code PREDICTOR_*} constants defined 
in {@link BaselineTIFFTagSet}.
+     *
+     * @return one of the {@code PREDICTOR_*} constants, or empty if predictor 
does not apply to this compression.
+     */
+    public OptionalInt predictor() {
+        return usePredictor() ? OptionalInt.of(predictor.code) : 
OptionalInt.empty();
+    }
+
+    /**
+     * {@return whether the compression method may use predictor}.
+     */
+    final boolean usePredictor() {
+        return 
!org.apache.sis.storage.geotiff.base.Compression.NONE.equals(method);
+    }
 
     /**
      * Compares this compression with the given object for equality.
@@ -128,7 +189,7 @@ public final class Compression implements Serializable {
     public boolean equals(final Object other) {
         if (other instanceof Compression) {
             final var c = (Compression) other;
-            return method.equals(c.method) && level == c.level;
+            return (level == c.level) && method.equals(c.method) && 
predictor.equals(c.predictor);
         }
         return false;
     }
@@ -140,7 +201,7 @@ public final class Compression implements Serializable {
      */
     @Override
     public int hashCode() {
-        return method.hashCode() + level;
+        return method.hashCode() + predictor.hashCode() + level;
     }
 
     /**
@@ -151,6 +212,7 @@ public final class Compression implements Serializable {
     @Override
     public String toString() {
         return Strings.toString(Compression.class, "method", method,
-                "level", (level != 0) ? Integer.valueOf(level) : null);
+                "level", (level != 0) ? Integer.valueOf(level) : null,
+                "predictor", usePredictor() ? predictor : 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 e075cc7100..55befa758f 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
@@ -105,8 +105,10 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
 
     /**
      * The compression to apply when writing tiles, or {@code null} if 
unspecified.
+     *
+     * @see #getCompression()
      */
-    final Compression compression;
+    private final Compression compression;
 
     /**
      * The locale to use for formatting metadata. This is not necessarily the 
same as {@link #getLocale()},
@@ -263,19 +265,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         }
     }
 
-    /**
-     * Returns the modifiers (BigTIFF, COG…) of this data store.
-     *
-     * @return format modifiers of this data store.
-     *
-     * @since 1.5
-     */
-    public Set<FormatModifier> getModifiers() {
-        final Writer w = writer; if (w != null) return w.getModifiers();
-        final Reader r = reader; if (r != null) return r.getModifiers();
-        return Set.of();
-    }
-
     /**
      * Returns the namespace to use in identifier of components, or {@code 
null} if none.
      * This method must be invoked inside a block synchronized on {@code this}.
@@ -337,6 +326,32 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         return Optional.ofNullable(param);
     }
 
+    /**
+     * Returns the modifiers (BigTIFF, COG…) of this data store.
+     *
+     * @return format modifiers of this data store.
+     *
+     * @since 1.5
+     */
+    public Set<FormatModifier> getModifiers() {
+        final Writer w = writer; if (w != null) return w.getModifiers();
+        final Reader r = reader; if (r != null) return r.getModifiers();
+        return Set.of();
+    }
+
+    /**
+     * Returns the compression used when writing tiles.
+     * This is not necessarily the compression of images to be read.
+     * For the compression of existing images, see {@linkplain #getMetadata() 
the metadata}.
+     *
+     * @return the compression to use for writing new images, or empty if 
unspecified.
+     *
+     * @since 1.5
+     */
+    public Optional<Compression> getCompression() {
+        return Optional.ofNullable(compression);
+    }
+
     /**
      * Returns an identifier constructed from the name of the TIFF file.
      * An identifier is available only if the storage input specified at 
construction time was something convertible to
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 09caad5b16..b0ec5408cc 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,7 +27,6 @@ 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;
@@ -38,6 +37,7 @@ import org.opengis.util.FactoryException;
 import org.opengis.metadata.Metadata;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.j2d.ImageUtilities;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.ReadOnlyStorageException;
@@ -49,7 +49,6 @@ 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;
@@ -318,6 +317,11 @@ final class Writer extends IOBase implements Flushable {
     private TileMatrix writeImageFileDirectory(final ReformattedImage image, 
final GridGeometry grid, final Metadata metadata,
             final boolean overview) throws IOException, DataStoreException
     {
+        final SampleModel sm = image.visibleBands.getSampleModel();
+        Compression compression = 
store.getCompression().orElse(Compression.DEFLATE);
+        if (!ImageUtilities.isIntegerType(sm)) {
+            compression = compression.withPredictor(PREDICTOR_NONE);
+        }
         /*
          * Extract all image properties and metadata that we will need to 
encode in the Image File Directory.
          * It allows us to know if we will be able to encode the image before 
we start writing in the stream,
@@ -326,11 +330,11 @@ final class Writer extends IOBase implements Flushable {
          * (for example) to be interleaved with other aspects.
          */
         numberOfTags = MINIMAL_NUMBER_OF_TAGS;      // Only a guess at this 
stage. Real number computed later.
+        if (compression.usePredictor()) numberOfTags++;
         final int colorInterpretation = image.getColorInterpretation();
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
             numberOfTags++;
         }
-        final SampleModel sm      = image.visibleBands.getSampleModel();
         final int   sampleFormat  = image.getSampleFormat();
         final int[] bitsPerSample = sm.getSampleSize();
         final int   numBands      = sm.getNumBands();
@@ -367,18 +371,6 @@ 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.
@@ -394,7 +386,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.code);
+        writeTag((short) TAG_COMPRESSION,                (short) 
TIFFTag.TIFF_SHORT, compression.method.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);
@@ -410,10 +402,14 @@ final class Writer extends IOBase implements Flushable {
         writeTag((short) TAG_DATE_TIME,                  /* TIFF_ASCII */      
      mf.creationDate);
         writeTag((short) TAG_ARTIST,                     /* TIFF_ASCII */      
      mf.party);
         writeTag((short) TAG_HOST_COMPUTER,              /* TIFF_ASCII */      
      mf.procedure);
+        if (compression.usePredictor()) {
+            writeTag((short) TAG_PREDICTOR, (short) TIFFTag.TIFF_SHORT, 
compression.predictor.code);
+        }
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
             writeColorPalette((IndexColorModel) 
image.visibleBands.getColorModel(), 1L << bitsPerSample[0]);
         }
-        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD, compression, compressionLevel);
+        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD,
+                                          compression.method, 
compression.level, compression.predictor);
         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 629b895585..21309732dc 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
@@ -30,6 +30,10 @@ import static 
javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
  *
  * The main exception is {@code CCITT}, which has different name in WCS query 
and response.
  *
+ * <p>This enumeration contains a relatively large number of compressions in 
order to put a name
+ * on the numerical codes that the reader may find. However the Apache SIS 
reader and writer do
+ * not support all those compressions. This enumeration is not put in public 
API for that reason.</p>
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
index 834169505e..c0be9ed3fd 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
@@ -17,6 +17,7 @@
 package org.apache.sis.storage.geotiff.base;
 
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -24,28 +25,43 @@ import static 
javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
  * A predictor is a mathematical operator that is applied to the image data
  * before an encoding scheme is applied.
  *
+ * <p>This enumeration contains more values than what the Apache SIS reader 
and writer can support.
+ * This enumeration is not put in public API for that reason.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  */
 public enum Predictor {
     /**
      * No prediction scheme used before coding.
      */
-    NONE,
+    NONE(PREDICTOR_NONE),
 
     /**
      * Horizontal differencing.
      */
-    HORIZONTAL,
+    HORIZONTAL_DIFFERENCING(PREDICTOR_HORIZONTAL_DIFFERENCING),
 
     /**
      * Floating point prediction.
      */
-    FLOAT,
+    FLOAT(3),
 
     /**
      * Predictor code is not recognized.
      */
-    UNKNOWN;
+    UNKNOWN(0);
+
+    /**
+     * The TIFF code for this predictor.
+     */
+    public final int code;
+
+    /**
+     * Creates a new predictor enumeration.
+     */
+    private Predictor(final int code) {
+        this.code = code;
+    }
 
     /**
      * Returns the predictor for the given code.
@@ -56,9 +72,24 @@ public enum Predictor {
     public static Predictor valueOf(final int code) {
         switch (code) {
             case PREDICTOR_NONE: return NONE;
-            case PREDICTOR_HORIZONTAL_DIFFERENCING: return HORIZONTAL;
+            case PREDICTOR_HORIZONTAL_DIFFERENCING: return 
HORIZONTAL_DIFFERENCING;
             case 3:  return FLOAT;
             default: return UNKNOWN;
         }
     }
+
+    /**
+     * Returns the predictor for the given code if supported.
+     *
+     * @param  code  value associated to TIFF "predictor" tag.
+     * @return predictor for the given code.
+     * @throws IllegalArgumentException if the given code is unsupported.
+     */
+    public static Predictor supported(final int code) {
+        final Predictor value = valueOf(code);
+        if (value.ordinal() <= HORIZONTAL_DIFFERENCING.ordinal()) {
+            return value;
+        }
+        throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1, 
code));
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/HorizontalPredictor.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/HorizontalPredictor.java
index 650b0fc944..a1fbb9a447 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/HorizontalPredictor.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/HorizontalPredictor.java
@@ -20,10 +20,11 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import org.apache.sis.image.DataType;
 import org.apache.sis.pending.jdk.JDK17;
+import org.apache.sis.storage.geotiff.base.Predictor;
 
 
 /**
- * Implementation of {@link 
org.apache.sis.storage.geotiff.internal.Predictor#HORIZONTAL}.
+ * Implementation of {@link Predictor#HORIZONTAL_DIFFERENCING}.
  * Current implementation works only on 8, 16, 32 or 64-bits samples.
  * Values packed on 4, 2 or 1 bits are not yet supported.
  *
@@ -98,7 +99,6 @@ abstract class HorizontalPredictor extends PredictorChannel {
      * @param  dataType     primitive type used for storing data elements in 
the bank.
      * @param  pixelStride  number of sample values per pixel in the source 
image.
      * @param  width        number of pixels in the source image.
-     * @param  sampleSize   number of bytes in a sample value.
      * @return the predictor, or {@code null} if the given type is unsupported.
      */
     static HorizontalPredictor create(final CompressionChannel input, final 
DataType dataType,
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
index 25dfb9f396..a530d07a24 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
@@ -221,7 +221,7 @@ public abstract class Inflater implements Closeable {
                 channel = inflater;
                 break;
             }
-            case HORIZONTAL: {
+            case HORIZONTAL_DIFFERENCING: {
                 if (sourceWidth == 1) {
                     channel = inflater;     // Horizontal predictor is no-op 
if image width is 1 pixel.
                     break;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
index 7624f5e269..08148517bf 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
@@ -25,6 +25,8 @@ import org.apache.sis.pending.jdk.JDK17;
 
 /**
  * Implementation of a {@link Predictor} to be executed after decompression.
+ * A predictor is a mathematical operator that is applied to the image data
+ * before an encoding scheme is applied, in order to improve compression.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
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 46bcb56b4a..2515b961bb 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,7 +17,6 @@
 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;
 
@@ -30,7 +29,7 @@ import org.apache.sis.io.stream.ChannelDataOutput;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-abstract class CompressionChannel implements WritableByteChannel {
+abstract class CompressionChannel extends PixelChannel {
     /**
      * Desired size of the temporary buffer where to compress data.
      */
@@ -64,8 +63,8 @@ abstract class CompressionChannel implements 
WritableByteChannel {
      * @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 {
-        assert owner.channel == this;
         owner.flush();
         owner.clear();
     }
@@ -74,11 +73,9 @@ abstract class CompressionChannel implements 
WritableByteChannel {
      * 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 {
+    public void close() {
         // Do NOT close `output`.
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/HorizontalPredictor.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/HorizontalPredictor.java
new file mode 100644
index 0000000000..b6c174ec3b
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/HorizontalPredictor.java
@@ -0,0 +1,390 @@
+/*
+ * 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.util.Arrays;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.IntBuffer;
+import java.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
+import org.apache.sis.image.DataType;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.apache.sis.storage.geotiff.base.Predictor;
+
+
+/**
+ * Implementation of {@link Predictor#HORIZONTAL_DIFFERENCING}.
+ * Current implementation works only on 8, 16, 32 or 64-bits samples.
+ * Values packed on 4, 2 or 1 bits are not yet supported.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+abstract class HorizontalPredictor extends PredictorChannel {
+    /**
+     * Number of elements (not necessarily bytes) between a row and the next 
row.
+     * This is usually the tile scanlineStride.
+     */
+    protected final int scanlineStride;
+
+    /**
+     * The column index of the next sample values to write.
+     * This is reset to 0 for each new row, and increased by 1 for each sample 
value.
+     */
+    private int column;
+
+    /**
+     * Creates a new predictor which will write uncompressed data to the given 
channel.
+     *
+     * @param  output          the channel that compress data.
+     * @param  scanlineStride  number of elements (not necessarily bytes) 
between a row and the next row.
+     */
+    HorizontalPredictor(final PixelChannel output, final int scanlineStride) {
+        super(output);
+        this.scanlineStride = scanlineStride;
+    }
+
+    /**
+     * Creates a new predictor.
+     *
+     * @param  output          the channel that decompress data.
+     * @param  dataType        primitive type used for storing data elements 
in the bank.
+     * @param  pixelStride     number of elements (not necessarily bytes) 
between a pixel and the next pixel.
+     * @param  scanlineStride  number of elements (not necessarily bytes) 
between a row and the next row.
+     * @return the predictor, or {@code null} if the given type is unsupported.
+     */
+    static HorizontalPredictor create(final PixelChannel output, final 
DataType dataType,
+            final int pixelStride, final int scanlineStride)
+    {
+        switch (dataType) {
+            case USHORT:
+            case SHORT:  return new Shorts  (output, pixelStride, 
scanlineStride);
+            case BYTE:   return new Bytes   (output, pixelStride, 
scanlineStride);
+            case INT:    return new Integers(output, pixelStride, 
scanlineStride);
+            case FLOAT:  return new Floats  (output, pixelStride, 
scanlineStride);
+            case DOUBLE: return new Doubles (output, pixelStride, 
scanlineStride);
+            default:     return null;
+        }
+    }
+
+    /**
+     * {@return the size of sample values in number of bytes}.
+     */
+    abstract int sampleSize();
+
+    /**
+     * Applies the predictor on data in the given buffer,
+     * from the buffer position until the buffer limit.
+     * This method modifies in-place the content of the given buffer.
+     * That buffer should contain only temporary data, typically copied from a 
raster data buffer.
+     *
+     * @param  buffer  the buffer on which to apply the predictor. Content 
will be modified in-place.
+     * @return number of bytes written.
+     * @throws IOException if an error occurred while writing the data to the 
channel.
+     */
+    @Override
+    public final int write(final ByteBuffer buffer) throws IOException {
+        final int start = buffer.position();
+        final int count = apply(buffer, column);
+        column = (column + count) % scanlineStride;
+        final int limit = buffer.limit();
+        buffer.limit(buffer.position() + count * sampleSize());
+        while (buffer.hasRemaining()) {
+            output.write(buffer);
+        }
+        buffer.limit(limit);
+        return buffer.position() - start;
+    }
+
+    /**
+     * Applies the differential predictor on the given buffer, from current 
position to limit.
+     * Implementation shall not modify the buffer position or limit.
+     *
+     * @param  buffer  the buffer on which to apply the predictor.
+     * @param  start   index of the column of the first value in the buffer.
+     */
+    abstract int apply(ByteBuffer output, int start);
+
+
+    /**
+     * A horizontal predictor working on byte values.
+     */
+    private static final class Bytes extends HorizontalPredictor {
+        /** Sample values of the previous pixel. */
+        private final byte[] previous;
+
+        /** Creates a new predictor. */
+        Bytes(final PixelChannel output, final int pixelStride, final int 
scanlineStride) {
+            super(output, scanlineStride);
+            previous = new byte[pixelStride];
+        }
+
+        /** The number of bytes in each sample value. */
+        @Override int sampleSize() {
+            return Byte.BYTES;
+        }
+
+        /** Applies the differential predictor. */
+        @Override int apply(final ByteBuffer buffer, final int start) {
+            final ByteBuffer view = buffer.slice();
+            final int pixelStride = previous.length;
+            final int bankShift   = start % pixelStride;
+            for (int bank=0; bank < pixelStride; bank++) {
+                final int pi = (bank + bankShift) % pixelStride;
+                byte p = previous[pi];
+                int endOfRow = scanlineStride - start;
+                for (int i=bank;;) {
+                    final int endOfPass = Math.min(endOfRow, view.limit());
+                    while (i < endOfPass) {
+                        final byte v = view.get(i);
+                        view.put(i, (byte) (v - p));
+                        p = v;
+                        i += pixelStride;
+                    }
+                    if (i < endOfRow) break;
+                    endOfRow += scanlineStride;
+                    p = 0;
+                }
+                previous[pi] = p;
+            }
+            return view.limit();
+        }
+
+        /** Writes pending data and resets the predictor for the next tile to 
write. */
+        @Override public void finish(final ChannelDataOutput owner) throws 
IOException {
+            super.finish(owner);
+            Arrays.fill(previous, (byte) 0);
+        }
+    }
+
+
+
+    /**
+     * A horizontal predictor working on short integer values.
+     * The code of this class is a copy of {@link Bytes} adapted for short 
integers.
+     */
+    private static final class Shorts extends HorizontalPredictor {
+        /** Sample values of the previous pixel. */
+        private final short[] previous;
+
+        /** Creates a new predictor. */
+        Shorts(final PixelChannel output, final int pixelStride, final int 
scanlineStride) {
+            super(output, scanlineStride);
+            previous = new short[pixelStride];
+        }
+
+        /** The number of bytes in each sample value. */
+        @Override int sampleSize() {
+            return Short.BYTES;
+        }
+
+        /** Applies the differential predictor. */
+        @Override int apply(final ByteBuffer buffer, final int start) {
+            final ShortBuffer view = buffer.asShortBuffer();
+            final int pixelStride = previous.length;
+            final int bankShift   = start % pixelStride;
+            for (int bank=0; bank < pixelStride; bank++) {
+                final int pi = (bank + bankShift) % pixelStride;
+                short p = previous[pi];
+                int endOfRow = scanlineStride - start;
+                for (int i=bank;;) {
+                    final int endOfPass = Math.min(endOfRow, view.limit());
+                    while (i < endOfPass) {
+                        final short v = view.get(i);
+                        view.put(i, (short) (v - p));
+                        p = v;
+                        i += pixelStride;
+                    }
+                    if (i < endOfRow) break;
+                    endOfRow += scanlineStride;
+                    p = 0;
+                }
+                previous[pi] = p;
+            }
+            return view.limit();
+        }
+
+        /** Writes pending data and resets the predictor for the next tile to 
write. */
+        @Override public void finish(final ChannelDataOutput owner) throws 
IOException {
+            super.finish(owner);
+            Arrays.fill(previous, (short) 0);
+        }
+    }
+
+
+
+    /**
+     * A horizontal predictor working on 32 bits integer values.
+     * The code of this class is a copy of {@link Bytes} adapted for integers.
+     */
+    private static final class Integers extends HorizontalPredictor {
+        /** Sample values of the previous pixel. */
+        private final int[] previous;
+
+        /** Creates a new predictor. */
+        Integers(final PixelChannel output, final int pixelStride, final int 
scanlineStride) {
+            super(output, scanlineStride);
+            previous = new int[pixelStride];
+        }
+
+        /** The number of bytes in each sample value. */
+        @Override int sampleSize() {
+            return Integer.BYTES;
+        }
+
+        /** Applies the differential predictor. */
+        @Override int apply(final ByteBuffer buffer, final int start) {
+            final IntBuffer view = buffer.asIntBuffer();
+            final int pixelStride = previous.length;
+            final int bankShift   = start % pixelStride;
+            for (int bank=0; bank < pixelStride; bank++) {
+                final int pi = (bank + bankShift) % pixelStride;
+                int p = previous[pi];
+                int endOfRow = scanlineStride - start;
+                for (int i=bank;;) {
+                    final int endOfPass = Math.min(endOfRow, view.limit());
+                    while (i < endOfPass) {
+                        final int v = view.get(i);
+                        view.put(i, v - p);
+                        p = v;
+                        i += pixelStride;
+                    }
+                    if (i < endOfRow) break;
+                    endOfRow += scanlineStride;
+                    p = 0;
+                }
+                previous[pi] = p;
+            }
+            return view.limit();
+        }
+
+        /** Writes pending data and resets the predictor for the next tile to 
write. */
+        @Override public void finish(final ChannelDataOutput owner) throws 
IOException {
+            super.finish(owner);
+            Arrays.fill(previous, 0);
+        }
+    }
+
+
+
+    /**
+     * A horizontal predictor working on single-precision floating point 
values.
+     * The code of this class is a copy of {@link Bytes} adapted for floating 
point values.
+     */
+    private static final class Floats extends HorizontalPredictor {
+        /** Sample values of the previous pixel. */
+        private final float[] previous;
+
+        /** Creates a new predictor. */
+        Floats(final PixelChannel output, final int pixelStride, final int 
scanlineStride) {
+            super(output, scanlineStride);
+            previous = new float[pixelStride];
+        }
+
+        /** The number of bytes in each sample value. */
+        @Override int sampleSize() {
+            return Float.BYTES;
+        }
+
+        /** Applies the differential predictor. */
+        @Override int apply(final ByteBuffer buffer, final int start) {
+            final FloatBuffer view = buffer.asFloatBuffer();
+            final int pixelStride = previous.length;
+            final int bankShift   = start % pixelStride;
+            for (int bank=0; bank < pixelStride; bank++) {
+                final int pi = (bank + bankShift) % pixelStride;
+                float p = previous[pi];
+                int endOfRow = scanlineStride - start;
+                for (int i=bank;;) {
+                    final int endOfPass = Math.min(endOfRow, view.limit());
+                    while (i < endOfPass) {
+                        final float v = view.get(i);
+                        view.put(i, v - p);
+                        p = v;
+                        i += pixelStride;
+                    }
+                    if (i < endOfRow) break;
+                    endOfRow += scanlineStride;
+                    p = 0;
+                }
+                previous[pi] = p;
+            }
+            return view.limit();
+        }
+
+        /** Writes pending data and resets the predictor for the next tile to 
write. */
+        @Override public void finish(final ChannelDataOutput owner) throws 
IOException {
+            super.finish(owner);
+            Arrays.fill(previous, 0);
+        }
+    }
+
+
+
+    /**
+     * A horizontal predictor working on double-precision floating point 
values.
+     * The code of this class is a copy of {@link Bytes} adapted for floating 
point values.
+     */
+    private static final class Doubles extends HorizontalPredictor {
+        /** Sample values of the previous pixel. */
+        private final double[] previous;
+
+        /** Creates a new predictor. */
+        Doubles(final PixelChannel output, final int pixelStride, final int 
scanlineStride) {
+            super(output, scanlineStride);
+            previous = new double[pixelStride];
+        }
+
+        /** The number of bytes in each sample value. */
+        @Override int sampleSize() {
+            return Double.BYTES;
+        }
+
+        /** Applies the differential predictor. */
+        @Override int apply(final ByteBuffer buffer, final int start) {
+            final DoubleBuffer view = buffer.asDoubleBuffer();
+            final int pixelStride = previous.length;
+            final int bankShift   = start % pixelStride;
+            for (int bank=0; bank < pixelStride; bank++) {
+                final int pi = (bank + bankShift) % pixelStride;
+                double p = previous[pi];
+                int endOfRow = scanlineStride - start;
+                for (int i=bank;;) {
+                    final int endOfPass = Math.min(endOfRow, view.limit());
+                    while (i < endOfPass) {
+                        final double v = view.get(i);
+                        view.put(i, v - p);
+                        p = v;
+                        i += pixelStride;
+                    }
+                    if (i < endOfRow) break;
+                    endOfRow += scanlineStride;
+                    p = 0;
+                }
+                previous[pi] = p;
+            }
+            return view.limit();
+        }
+
+        /** Writes pending data and resets the predictor for the next tile to 
write. */
+        @Override public void finish(final ChannelDataOutput owner) throws 
IOException {
+            super.finish(owner);
+            Arrays.fill(previous, 0);
+        }
+    }
+}
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
new file mode 100644
index 0000000000..773964ac49
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PixelChannel.java
@@ -0,0 +1,52 @@
+/*
+ * 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.io.stream.ChannelDataOutput;
+
+
+/**
+ * A channel of pixel values after all steps have been completed.
+ * The steps may be:
+ *
+ * <ul>
+ *   <li>Compression alone, in which case this class is a subtype of {@link 
CompressionChannel}.</li>
+ *   <li>Compression after some mathematical operation applied on the data 
before compression.
+ *       In that case this class is a subtype of {@link PredictorChannel}.</li>
+ * </ul>
+ *
+ * The {@link #close()} method shall be invoked when this channel is no longer 
used.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+abstract class PixelChannel implements WritableByteChannel {
+    /**
+     * Creates a new channel.
+     */
+    protected PixelChannel() {
+    }
+
+    /**
+     * 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 abstract void finish(ChannelDataOutput owner) throws IOException;
+}
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/PredictorChannel.java
similarity index 52%
copy from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/CompressionChannel.java
copy to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/PredictorChannel.java
index 46bcb56b4a..46abceb43b 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/PredictorChannel.java
@@ -17,68 +17,61 @@
 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;
+import org.apache.sis.storage.geotiff.base.Predictor;
 
 
 /**
- * Deflater using a temporary buffer where to compress data before writing to 
the channel.
- * This class does not need to care about subsampling.
+ * Implementation of a {@link Predictor} to be executed before compression.
+ * A predictor is a mathematical operator that is applied to the image data
+ * before an encoding scheme is applied, in order to improve compression.
  *
- * <p>The {@link #close()} method shall be invoked when this channel is no 
longer used.</p>
+ * <p>Note that this channel may modify in-place the content of the buffer
+ * given in calls to {@link #write(ByteBuffer)}. That buffer should contain
+ * only temporary data, typically copied from a raster data buffer.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-abstract class CompressionChannel implements WritableByteChannel {
+abstract class PredictorChannel extends PixelChannel {
     /**
-     * Desired size of the temporary buffer where to compress data.
+     * The channel where to write data.
      */
-    static final int BUFFER_SIZE = StorageConnector.DEFAULT_BUFFER_SIZE / 2;
+    protected final PixelChannel output;
 
     /**
-     * The destination where to write compressed data.
-     */
-    protected final ChannelDataOutput output;
-
-    /**
-     * Creates a new channel which will compress data to the given output.
+     * Creates a predictor.
      *
-     * @param  output  the destination of compressed data.
+     * @param  output  the channel that compress data.
      */
-    protected CompressionChannel(final ChannelDataOutput output) {
+    protected PredictorChannel(final PixelChannel 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.
      */
+    @Override
     public void finish(final ChannelDataOutput owner) throws IOException {
-        assert owner.channel == this;
-        owner.flush();
-        owner.clear();
+        output.finish(owner);
     }
 
     /**
-     * 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.
+     * Tells whether this channel is still open.
+     */
+    @Override
+    public final boolean isOpen() {
+        return output.isOpen();
+    }
+
+    /**
+     * Closes {@link #output}. Note that it will <strong>not</strong> closes 
the channel wrapped by {@link #output}
+     * because that channel will typically be needed again for compressing 
other tiles.
      */
     @Override
-    public void close() throws IOException {
-        // Do NOT close `output`.
+    public final void close() throws IOException {
+        output.close();
     }
 }
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 f3cda439a8..b4ee813e6a 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
@@ -39,6 +39,7 @@ 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.Predictor;
 import org.apache.sis.storage.geotiff.base.Resources;
 
 
@@ -109,6 +110,11 @@ public final class TileMatrix {
      */
     private final int compressionLevel;
 
+    /**
+     * The predictor to apply before to compress data.
+     */
+    private final Predictor predictor;
+
     /**
      * Creates a new set of information about tiles to write.
      *
@@ -118,9 +124,11 @@ public final class TileMatrix {
      * @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.
+     * @param predictor         the predictor to apply before to compress data.
      */
     public TileMatrix(final RenderedImage image, final int numPlanes, final 
int[] bitsPerSample,
-                      final long offsetIFD, final Compression compression, 
final int compressionLevel)
+                      final long offsetIFD, final Compression compression, 
final int compressionLevel,
+                      final Predictor predictor)
     {
         final int pixelSize, numArrays;
         this.offsetIFD        = offsetIFD;
@@ -128,6 +136,8 @@ public final class TileMatrix {
         this.image            = image;
         this.compression      = compression;
         this.compressionLevel = compressionLevel;
+        this.predictor        = predictor;
+
         type       = DataType.forBands(image);
         tileWidth  = image.getTileWidth();
         tileHeight = image.getTileHeight();
@@ -166,22 +176,40 @@ public final class TileMatrix {
      * @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)
+    private ChannelDataOutput createCompressionChannel(final ChannelDataOutput 
output,
+            final int pixelStride, final int scanlineStride)
             throws DataStoreException, IOException
     {
-        final CompressionChannel channel;
+        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 new DataStoreException(Resources.forLocale(null)
-                    .getString(Resources.Keys.UnsupportedCompressionMethod_1, 
compression));
+            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.
+     */
+    private static DataStoreException unsupported(final short key, final 
Enum<?> value) {
+        return new DataStoreException(Resources.forLocale(null).getString(key, 
value));
+    }
+
     /**
      * Writes all tiles of the image.
      * Caller shall invoke {@link #writeOffsetsAndLengths(ChannelDataOutput)} 
after this method.
@@ -192,12 +220,11 @@ public final class TileMatrix {
      * @throws IOException if an error occurred while writing to the given 
output.
      */
     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;
+        ChannelDataOutput compress = null;
+        PixelChannel      cc       = null;
+        SampleModel       sm       = null;
+        int[] bankIndices          = null;
+        HyperRectangleWriter rect  = null;
         final int minTileX = image.getMinTileX();
         final int minTileY = image.getMinTileY();
         int planeIndex = 0;
@@ -214,20 +241,24 @@ public final class TileMatrix {
             final Raster tile = image.getTile(tileX, tileY);
             if (sm != (sm = tile.getSampleModel())) {
                 rect = null;
-                final var region = new Rectangle(tileWidth, tileHeight);
+                final var builder = new 
HyperRectangleWriter.Builder().region(new Rectangle(tileWidth, tileHeight));
                 if (sm instanceof ComponentSampleModel) {
                     final var csm = (ComponentSampleModel) sm;
-                    rect = HyperRectangleWriter.of(csm, region);
+                    rect = builder.create(csm);
                     bankIndices = csm.getBankIndices();
                 } else if (sm instanceof SinglePixelPackedSampleModel) {
                     final var csm = (SinglePixelPackedSampleModel) sm;
-                    rect = HyperRectangleWriter.of(csm, region);
+                    rect = builder.create(csm);
                     bankIndices = new int[1];
                 } else if (sm instanceof MultiPixelPackedSampleModel) {
                     final var csm = (MultiPixelPackedSampleModel) sm;
-                    rect = HyperRectangleWriter.of(csm, region);
+                    rect = builder.create(csm);
                     bankIndices = new int[1];
                 }
+                if (compress == null) {
+                    compress = createCompressionChannel(output, 
builder.pixelStride(), builder.scanlineStride());
+                    if (compress != output) cc = (PixelChannel) 
compress.channel;
+                }
             }
             if (rect == null) {
                 throw new UnsupportedOperationException();      // TODO: 
reformat using a recycled Raster.
@@ -239,6 +270,7 @@ public final class TileMatrix {
                 final int  offset   = bufferOffsets[b];
                 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;
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 961007a995..792b90e8ab 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
@@ -91,87 +91,147 @@ public final class HyperRectangleWriter {
     }
 
     /**
-     * Creates a new writer for raster data described by the given sample 
model and strides.
-     * If the given {@code region} is non-null, it specifies a subset of the 
data to write.
+     * A builder for {@code HyperRectangleWriter} created from a {@code 
SampleModel}.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
      */
-    private static HyperRectangleWriter of(final SampleModel sm, final 
Rectangle region,
-            final int subX, final int pixelStride, final int scanlineStride)
-    {
-        final int[]  subsampling = {subX, 1};
-        final long[] sourceSize  = {scanlineStride, sm.getHeight()};
-        final long[] regionLower = new long[2];
-        final long[] regionUpper = new long[2];
-        if (region != null) {
-            regionUpper[0] = (regionLower[0] = region.x) + region.width;
-            regionUpper[1] = (regionLower[1] = region.y) + region.height;
-        } else {
-            regionUpper[0] = sm.getWidth();
-            regionUpper[1] = sm.getHeight();
+    public static final class Builder {
+        /**
+         * Number of elements (not necessarily bytes) between a pixel and the 
next pixel.
+         *
+         * @see #pixelStride()
+         */
+        private int pixelStride;
+
+        /**
+         * Number of elements (not necessarily bytes) between a row and the 
next row.
+         *
+         * @see #scanlineStride()
+         */
+        private int scanlineStride;
+
+        /**
+         * Subregion to write, or {@code null} for writing the whole raster.
+         */
+        private Rectangle region;
+
+        /**
+         * Creates a new builder.
+         */
+        public Builder() {
         }
-        regionLower[0] = Math.multiplyExact(regionLower[0], pixelStride);
-        regionUpper[0] = Math.multiplyExact(regionUpper[0], pixelStride);
-        return new HyperRectangleWriter(new Region(sourceSize, regionLower, 
regionUpper, subsampling));
-    }
 
-    /**
-     * Creates a new writer for raster data described by the given sample 
model.
-     * This method supports only the writing of either a single band, or all 
bands
-     * in the order they appear in the array.
-     *
-     * @param  sm      the sample model of the rasters to write.
-     * @param  region  subset to write, or {@code null} if none.
-     * @return writer, or {@code null} if the given sample model is not 
supported.
-     */
-    public static HyperRectangleWriter of(final ComponentSampleModel sm, final 
Rectangle region) {
-        final int pixelStride = sm.getPixelStride();
-        final int[] d = sm.getBandOffsets();
-        final int subX;
-        if (d.length == pixelStride && ArraysExt.isRange(0, d)) {
-            subX = 1;
-        } else if (d.length == 1) {
-            subX = pixelStride;
-        } else {
-            return null;
+        /**
+         * Specifies the region to write.
+         * This method retains the given rectangle by reference, it is not 
copied.
+         *
+         * @param  aoi  the region to write, or {@code null} for writing the 
whole raster.
+         * @return {@code this} for chained call.
+         */
+        public Builder region(final Rectangle aoi) {
+            region = aoi;
+            return this;
         }
-        return of(sm, region, subX, pixelStride, sm.getScanlineStride());
-    }
 
-    /**
-     * Creates a new writer for raster data described by the given sample 
model.
-     * This method supports only the writing of a single band using all bits.
-     *
-     * @param  sm      the sample model of the rasters to write.
-     * @param  region  subset to write, or {@code null} if none.
-     * @return writer, or {@code null} if the given sample model is not 
supported.
-     */
-    public static HyperRectangleWriter of(final SinglePixelPackedSampleModel 
sm, final Rectangle region) {
-        final int[] d = sm.getBitMasks();
-        if (d.length == 1) {
-            final long mask = (1L << 
DataBuffer.getDataTypeSize(sm.getDataType())) - 1;
-            if ((d[0] & mask) == mask) {
-                return of(sm, region, 1, 1, sm.getScanlineStride());
+        /**
+         * Creates a new writer for raster data described by the given sample 
model and strides.
+         * If the {@link #region} is non-null, it specifies a subset of the 
data to write.
+         */
+        private HyperRectangleWriter create(final SampleModel sm, final int 
subX) {
+            final int[]  subsampling = {subX, 1};
+            final long[] sourceSize  = {scanlineStride, sm.getHeight()};
+            final long[] regionLower = new long[2];
+            final long[] regionUpper = new long[2];
+            if (region != null) {
+                regionUpper[0] = (regionLower[0] = region.x) + region.width;
+                regionUpper[1] = (regionLower[1] = region.y) + region.height;
+            } else {
+                regionUpper[0] = sm.getWidth();
+                regionUpper[1] = sm.getHeight();
             }
+            regionLower[0] = Math.multiplyExact(regionLower[0], pixelStride);
+            regionUpper[0] = Math.multiplyExact(regionUpper[0], pixelStride);
+            return new HyperRectangleWriter(new Region(sourceSize, 
regionLower, regionUpper, subsampling));
         }
-        return null;
-    }
 
-    /**
-     * Creates a new writer for raster data described by the given sample 
model.
-     * This method supports only the writing of a single band using all bits.
-     *
-     * @param  sm      the sample model of the rasters to write.
-     * @param  region  subset to write, or {@code null} if none.
-     * @return writer, or {@code null} if the given sample model is not 
supported.
-     */
-    public static HyperRectangleWriter of(final MultiPixelPackedSampleModel 
sm, final Rectangle region) {
-        final int[] d = sm.getSampleSize();
-        if (d.length == 1) {
-            final int size = DataBuffer.getDataTypeSize(sm.getDataType());
-            if (d[0] == size && sm.getPixelBitStride() == size) {
-                return of(sm, region, 1, 1, sm.getScanlineStride());
+        /**
+         * Creates a new writer for raster data described by the given sample 
model.
+         * This method supports only the writing of either a single band, or 
all bands
+         * in the order they appear in the array.
+         *
+         * @param  sm  the sample model of the rasters to write.
+         * @return writer, or {@code null} if the given sample model is not 
supported.
+         */
+        public HyperRectangleWriter create(final ComponentSampleModel sm) {
+            pixelStride    = sm.getPixelStride();
+            scanlineStride = sm.getScanlineStride();
+            final int[] d  = sm.getBandOffsets();
+            final int subX;
+            if (d.length == pixelStride && ArraysExt.isRange(0, d)) {
+                subX = 1;
+            } else if (d.length == 1) {
+                subX = pixelStride;
+            } else {
+                return null;
+            }
+            return create(sm, subX);
+        }
+
+        /**
+         * Creates a new writer for raster data described by the given sample 
model.
+         * This method supports only the writing of a single band using all 
bits.
+         *
+         * @param  sm  the sample model of the rasters to write.
+         * @return writer, or {@code null} if the given sample model is not 
supported.
+         */
+        public HyperRectangleWriter create(final SinglePixelPackedSampleModel 
sm) {
+            pixelStride    = 1;
+            scanlineStride = sm.getScanlineStride();
+            final int[] d  = sm.getBitMasks();
+            if (d.length == 1) {
+                final long mask = (1L << 
DataBuffer.getDataTypeSize(sm.getDataType())) - 1;
+                if ((d[0] & mask) == mask) {
+                    return create(sm, 1);
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Creates a new writer for raster data described by the given sample 
model.
+         * This method supports only the writing of a single band using all 
bits.
+         *
+         * @param  sm  the sample model of the rasters to write.
+         * @return writer, or {@code null} if the given sample model is not 
supported.
+         */
+        public HyperRectangleWriter create(final MultiPixelPackedSampleModel 
sm) {
+            pixelStride    = 1;
+            scanlineStride = sm.getScanlineStride();
+            final int[] d  = sm.getSampleSize();
+            if (d.length == 1) {
+                final int size = DataBuffer.getDataTypeSize(sm.getDataType());
+                if (d[0] == size && sm.getPixelBitStride() == size) {
+                    return create(sm, 1);
+                }
             }
+            return null;
+        }
+
+        /**
+         * {@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.
+         */
+        public int pixelStride() {
+            return pixelStride;
+        }
+
+        /**
+         * {@return the number of elements (not necessarily bytes) between a 
row and the next row}.
+         * This information is valid only after a {@code create(…)} method has 
been invoked.
+         */
+        public int scanlineStride() {
+            return scanlineStride;
         }
-        return null;
     }
 
     /**

Reply via email to