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 <[email protected]>
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;
}
/**