This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 3e9a1deb2e05a01c5712c793a89771a5ec61347a Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Jun 12 19:17:58 2026 +0200 Move also the predictor base class to the shared package. Reuse the byte buffer in the GeoHEIF reader when possible. --- .../storage/geotiff/base/CompressionMethod.java | 10 ---- .../geotiff/inflater/HorizontalPredictor.java | 1 + .../sis/storage/geotiff/inflater/Inflater.java | 4 +- .../io/stream/inflater/ComputedByteChannel.java | 66 +++++++++++++++++++++- .../org/apache/sis/io/stream/inflater/Deflate.java | 10 ++++ .../sis/io/stream/inflater/InflaterChannel.java | 50 +++------------- .../sis/io/stream}/inflater/PredictorChannel.java | 25 ++++++-- .../apache/sis/storage/geoheif/ImageResource.java | 64 +++++++++++++-------- .../sis/storage/geoheif/UncompressedImage.java | 12 +++- 9 files changed, 154 insertions(+), 88 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/CompressionMethod.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/CompressionMethod.java index 61d29ba9c1..d0b69b31d0 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/CompressionMethod.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/CompressionMethod.java @@ -162,16 +162,6 @@ public enum CompressionMethod { return UNKNOWN; } - /** - * Whether the decompression uses native library. - * In such case, the use of direct buffer may be more efficient. - * - * @return whether the compression may use a native library. - */ - public final boolean useNativeLibrary() { - return this == DEFLATE; - } - /** * Returns whether the compression can be configured with different levels. * 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 14bb7c2c69..14854e44af 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,6 +20,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import org.apache.sis.image.DataType; import org.apache.sis.io.stream.inflater.InflaterChannel; +import org.apache.sis.io.stream.inflater.PredictorChannel; import org.apache.sis.pending.jdk.JDK13; 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 659b447b13..f8d8f9117d 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 @@ -238,8 +238,8 @@ public abstract class Inflater implements Closeable { } } final int scanlineStride = Math.multiplyExact(sourceWidth, sourcePixelStride * dataType.bytes()); - return CopyFromBytes.create(inflater.createDataInput(channel, scanlineStride, compression.useNativeLibrary()), - dataType, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement); + final ChannelDataInput inflated = channel.createDataInput(null, scanlineStride); + return CopyFromBytes.create(inflated, dataType, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/ComputedByteChannel.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/ComputedByteChannel.java index cddd8c803b..95c92da1da 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/ComputedByteChannel.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/ComputedByteChannel.java @@ -16,8 +16,14 @@ */ package org.apache.sis.io.stream.inflater; +import java.util.Arrays; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; +import org.apache.sis.io.stream.ChannelDataInput; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.math.MathFunctions; +import org.apache.sis.pending.jdk.JDK18; /** @@ -36,12 +42,26 @@ import java.nio.channels.ReadableByteChannel; * @author Martin Desruisseaux (Geomatys) */ public abstract class ComputedByteChannel implements ReadableByteChannel { + /** + * Desired size of the buffer where to temporarily copy decompressed data. + * The actual buffer size may become larger (but not smaller) + * because we try to use a multiple of scanline stride. + */ + private static final int BUFFER_SIZE = StorageConnector.DEFAULT_BUFFER_SIZE / 2; + /** * Creates a new channel. */ - protected ComputedByteChannel() { + ComputedByteChannel() { } + /** + * Returns the channel from which to read compressed data. + * + * @return the input channel, typically opened on a file. + */ + public abstract ChannelDataInput compressedInput(); + /** * Prepares this channel for reading a new block of data. * A block may be, for example, a tile or a band of a tile. @@ -51,4 +71,48 @@ public abstract class ComputedByteChannel implements ReadableByteChannel { * @throws IOException if the stream cannot be seek to the given start position. */ public abstract void setInputRegion(long start, long byteCount) throws IOException; + + /** + * Returns whether the inflater or predictor algorithm prefers native byte buffer. + * The default implementation returns {@code false}. + * Subclasses that depends on native library may return {@code true}. + * + * @return whether the inflater or predictor prefers native byte buffer. + */ + protected boolean preferNativeBuffer() { + return false; + } + + /** + * Creates the data input stream to use for getting uncompressed data. + * The source {@link ChannelDataInput} must be on the start position before to invoke this method. + * + * <p>This method tries to create a buffer of the size of scanline stride, or a multiple of that size, + * for performance reasons. A well adjusted buffer size reduces calls to {@link ByteBuffer#compact()}, + * which in turn reduces the number of copy operations between different regions of the buffer.</p> + * + * @param buffer buffer to reuse if {@code null}. The content of this buffer will be discarded. + * @param scanlineStride the scanline stride of the image to read. Used for choosing a buffer size. + * @throws IOException if an error occurred while filling the buffer with initial data. + * @return the data input for uncompressed data. + */ + public final ChannelDataInput createDataInput(ByteBuffer buffer, final int scanlineStride) throws IOException { + final int capacity; + if (scanlineStride > BUFFER_SIZE) { + final int[] divisors = MathFunctions.divisors(scanlineStride); + int i = Arrays.binarySearch(divisors, BUFFER_SIZE); + if (i < 0) i = ~i; // Really tild, not minus. + capacity = divisors[i]; // Smallest divisor ≥ BUFFER_SIZE + } else if (scanlineStride > Long.SIZE) { + capacity = JDK18.ceilDiv(BUFFER_SIZE, scanlineStride) * scanlineStride; // ≥ BUFFER_SIZE + } else { + capacity = BUFFER_SIZE; + } + final ChannelDataInput input = compressedInput(); + if (buffer == null) { + buffer = preferNativeBuffer() ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); + } + buffer = buffer.order(input.buffer.order()).limit(0); + return new ChannelDataInput(input.filename, this, buffer, true); + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/Deflate.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/Deflate.java index 49d7bd0eef..d364d64180 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/Deflate.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/Deflate.java @@ -51,6 +51,16 @@ public final class Deflate extends InflaterChannel { inflater = new Inflater(); } + /** + * Notifies that this inflater prefers native buffer. + * + * @return {@code true}. + */ + @Override + protected boolean preferNativeBuffer() { + return true; + } + /** * Prepares this channel for reading a new block of data. * diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/InflaterChannel.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/InflaterChannel.java index 16d81de18f..7fa5cb95ba 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/InflaterChannel.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/InflaterChannel.java @@ -16,15 +16,11 @@ */ package org.apache.sis.io.stream.inflater; -import java.util.Arrays; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.BufferOverflowException; -import org.apache.sis.math.MathFunctions; -import org.apache.sis.storage.StorageConnector; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.storage.event.StoreListeners; -import org.apache.sis.pending.jdk.JDK18; /** @@ -36,13 +32,6 @@ import org.apache.sis.pending.jdk.JDK18; * @author Martin Desruisseaux (Geomatys) */ public abstract class InflaterChannel extends ComputedByteChannel { - /** - * Desired size of the buffer where to temporarily copy decompressed data. - * The actual buffer size may become larger (but not smaller) - * because we try to use a multiple of scanline stride. - */ - private static final int BUFFER_SIZE = StorageConnector.DEFAULT_BUFFER_SIZE / 2; - /** * The source of data to decompress. */ @@ -71,6 +60,14 @@ public abstract class InflaterChannel extends ComputedByteChannel { this.listeners = listeners; } + /** + * Returns the channel from which to read compressed data. + */ + @Override + public final ChannelDataInput compressedInput() { + return input; + } + /** * Prepares this channel for reading a new block of data. * @@ -94,37 +91,6 @@ public abstract class InflaterChannel extends ComputedByteChannel { return input.getStreamPosition() >= endPosition; } - /** - * Creates the data input stream to use for getting uncompressed data. - * The {@linkplain #input} stream must be on the start position before to invoke this method. - * - * <p>This method tries to create a buffer of the size of scanline stride, or a multiple of that size, - * for performance reasons. A well adjusted buffer size reduces calls to {@link ByteBuffer#compact()}, - * which in turn reduces the number of copy operations between different regions of the buffer.</p> - * - * @param channel the channel to wrap. This is {@code this} unless a predictor is applied. - * @param scanlineStride the scanline stride of the image to read. Used for choosing a buffer size. - * @param directBuffer whether the use of direct buffer is preferred to heap buffer. - * @throws IOException if an error occurred while filling the buffer with initial data. - * @return the data input for uncompressed data. - */ - public final ChannelDataInput createDataInput(final ComputedByteChannel channel, final int scanlineStride, final boolean directBuffer) - throws IOException - { - final int capacity; - if (scanlineStride > BUFFER_SIZE) { - final int[] divisors = MathFunctions.divisors(scanlineStride); - int i = Arrays.binarySearch(divisors, BUFFER_SIZE); - if (i < 0) i = ~i; // Really tild, not minus. - capacity = divisors[i]; // Smallest divisor ≥ BUFFER_SIZE - } else { - capacity = JDK18.ceilDiv(BUFFER_SIZE, scanlineStride) * scanlineStride; // ≥ BUFFER_SIZE - } - ByteBuffer buffer = directBuffer ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); - buffer = buffer.order(input.buffer.order()).limit(0); - return new ChannelDataInput(input.filename, channel, buffer, true); - } - /** * Copies the given byte <var>n</var> times in the given buffer. * This is a convenience method for an operation frequently found in different compression algorithms. 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/main/org/apache/sis/io/stream/inflater/PredictorChannel.java similarity index 88% rename from endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java rename to endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/inflater/PredictorChannel.java index e80589f39e..65e40fe0a8 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/main/org/apache/sis/io/stream/inflater/PredictorChannel.java @@ -14,14 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.sis.storage.geotiff.inflater; +package org.apache.sis.io.stream.inflater; import java.io.IOException; import java.nio.ByteBuffer; import org.apache.sis.util.ArraysExt; import org.apache.sis.pending.jdk.JDK13; -import org.apache.sis.io.stream.inflater.ComputedByteChannel; -import org.apache.sis.io.stream.inflater.InflaterChannel; +import org.apache.sis.io.stream.ChannelDataInput; /** @@ -31,7 +30,7 @@ import org.apache.sis.io.stream.inflater.InflaterChannel; * * @author Martin Desruisseaux (Geomatys) */ -abstract class PredictorChannel extends ComputedByteChannel { +public abstract class PredictorChannel extends ComputedByteChannel { /** * The channel from which to read data. */ @@ -59,6 +58,14 @@ abstract class PredictorChannel extends ComputedByteChannel { deferred = ArraysExt.EMPTY_BYTE; } + /** + * Returns the channel from which to read compressed data. + */ + @Override + public final ChannelDataInput compressedInput() { + return input.compressedInput(); + } + /** * Prepares this predictor for reading a new tile or a new band of a planar image. * @@ -91,7 +98,7 @@ abstract class PredictorChannel extends ComputedByteChannel { * @throws IOException if some other I/O error occurs. */ @Override - public int read(final ByteBuffer target) throws IOException { + public final int read(final ByteBuffer target) throws IOException { final int start = target.position(); if (deferredCount != 0) { /* @@ -123,6 +130,14 @@ abstract class PredictorChannel extends ComputedByteChannel { return end - start; } + /** + * Returns whether the inflater or predictor algorithm prefers native byte buffer. + */ + @Override + protected final boolean preferNativeBuffer() { + return input.preferNativeBuffer(); + } + /** * Tells whether this channel is still open. */ diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java index c767e2a646..3686e778f6 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.nio.ByteBuffer; import java.io.IOException; import java.io.UncheckedIOException; import static java.lang.Math.addExact; @@ -299,13 +301,15 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso final var requests = new ReadContext[result.length]; int count = 0; synchronized (getSynchronizationLock()) { - try (final var context = new ReadContext(iterator)) { + final var readers = new HashMap<ImageReaderSpi, ImageReader>(); + final var buffer = new AtomicReference<ByteBuffer>(); + try { do { Raster raster = iterator.getCachedTile(); if (raster != null) { result[iterator.getTileIndexInResultArray()] = raster; } else { - requests[count++] = new ReadContext(context, ImageResource.this); + requests[count++] = new ReadContext(iterator, readers, buffer, ImageResource.this); } } while (iterator.next()); /* @@ -324,6 +328,8 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso for (int i=0; i<count; i++) { requests[i].readTile(input, result); // Implementation may create an `input` view. } + } finally { + readers.values().forEach(ImageReader::dispose); } } return result; @@ -333,7 +339,7 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso * Context about a {@code readTile(…)} operation. Contains the tile to create, or * the image reader to use in the case of read operations delegated to Image I/O. */ - static final class ReadContext extends ByteRanges implements AutoCloseable { + static final class ReadContext extends ByteRanges { /** * Iterator over the tiles to read. */ @@ -356,29 +362,28 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso final long subTileX, subTileY; /** - * Creates a new read context. - * - * @param iterator iterator over the tiles to read. + * Buffer to reuse for each tile. */ - private ReadContext(final AOI iterator) { - this.iterator = iterator; - this.readers = new HashMap<>(); - this.reader = null; - this.subTileX = 0; - this.subTileY = 0; - } + private final AtomicReference<ByteBuffer> buffer; /** - * Creates a context which is a snapshot of the given context at the current iterator position. + * Creates a new read context. * - * @param parent the parent from which to create a snapshot. - * @param tileX 0-based column index of the tile to read, starting from image left. - * @param tileY 0-based row index of the tile to read, starting from image top. + * @param iterator iterator over the tiles to read. + * @param readers an initially empty map where to store image readers for reuse. + * @param buffer an initially empty reference to a buffer. + * @param owner the resource for which to read a tile. */ @SuppressWarnings("LeakingThisInConstructor") - private ReadContext(final ReadContext parent, final ImageResource owner) throws DataStoreException { - this.iterator = new Snapshot(parent.iterator); - this.readers = parent.readers; + private ReadContext(final AOI iterator, + final Map<ImageReaderSpi, ImageReader> readers, + final AtomicReference<ByteBuffer> buffer, + final ImageResource owner) + throws DataStoreException + { + this.iterator = new Snapshot(iterator); + this.readers = readers; + this.buffer = buffer; final long[] tileCoord = iterator.getTileCoordinatesInResource(); final Image tile = owner.getTile(tileCoord[0], tileCoord[1]); subTileX = tileCoord[0] % tile.numXTiles; @@ -436,12 +441,21 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso } /** - * Invoked after a sequence of tiles have been read. - * This method disposes the image readers. + * If a buffer has already been used in a previous read operation, returns that buffer. + * + * @return buffer that can be reused, or {@code null} if none. + */ + public ByteBuffer reuseBuffer() { + return buffer.getAndSet(null); + } + + /** + * Saves the given buffer for reuse. + * + * @param done a buffer which is no longer needed. */ - @Override - public void close() { - readers.values().forEach(ImageReader::dispose); + public void saveForReuse(final ByteBuffer done) { + buffer.set(done); } } } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java index a9c77ede8c..b98ac9980f 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java @@ -28,11 +28,11 @@ import org.apache.sis.image.internal.shared.RasterFactory; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.HyperRectangleReader; import org.apache.sis.io.stream.Region; +import org.apache.sis.io.stream.inflater.ComputedByteChannel; import org.apache.sis.io.stream.inflater.Deflate; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.isobmff.ByteRanges; import org.apache.sis.storage.isobmff.mpeg.CompressedUnitsItemInfo; -import org.apache.sis.util.internal.shared.Numerics; /** @@ -135,12 +135,15 @@ final class UncompressedImage extends Image { locator.resolve(offset, tileSize, context); return (ChannelDataInput input) -> { long origin = context.offset(); + final ComputedByteChannel inflater; final CompressedUnitsItemInfo.Unit unit = compressedImageUnit; if (unit != null) { - final var inflater = new Deflate(input, listeners); + inflater = new Deflate(input, listeners); inflater.setInputRegion(addExact(origin, unit.offset), unit.size); - input = inflater.createDataInput(inflater, Numerics.clamp(sourceSize[0]), true); + input = inflater.createDataInput(context.reuseBuffer(), (int) sourceSize[0]); // (int) cast okay even if inexact. origin = 0; + } else { + inflater = null; } /* * Now read all banks and store the values in the image buffer. @@ -162,6 +165,9 @@ final class UncompressedImage extends Image { hr.setDestination(RasterFactory.wrapAsBuffer(data, b)); hr.readAsBuffer(rasterRegion, 0); } + if (inflater != null) { + context.saveForReuse(inflater.compressedInput().buffer); + } return raster; }; }
