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