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