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 ec8d89adde13533ad3dd197a7cd7244a7dab48fc
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Apr 10 16:40:32 2025 +0200

    Allow to read tiles through HTTP using HTTP range requests.
    It requires some reorganization for allowing to know the ranges in advance.
---
 .../org/apache/sis/io/stream/ChannelDataInput.java |  15 ++
 .../sis/storage/base/TiledDeferredImage.java       |   1 +
 .../apache/sis/storage/base/TiledGridCoverage.java |  19 ++-
 .../apache/sis/storage/geoheif/FromImageIO.java    |  65 ++++----
 .../main/org/apache/sis/storage/geoheif/Image.java |  43 ++++--
 .../apache/sis/storage/geoheif/ImageResource.java  | 152 +++++++++++++------
 .../sis/storage/geoheif/ResourceBuilder.java       |   6 +-
 .../sis/storage/geoheif/UncompressedImage.java     |  69 ++++-----
 .../org/apache/sis/storage/isobmff/ByteRanges.java | 163 +++++++++++++++++++++
 .../org/apache/sis/storage/isobmff/ByteReader.java |  92 ------------
 .../org/apache/sis/storage/isobmff/Reader.java     |  14 +-
 .../apache/sis/storage/isobmff/base/ItemData.java  |  32 ++--
 .../sis/storage/isobmff/base/ItemLocation.java     |  96 +++++++-----
 13 files changed, 481 insertions(+), 286 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
index 8616626267..2abd181a59 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
@@ -1221,6 +1221,8 @@ loop:   while (hasRemaining()) {
      *
      * @param  lower  position (inclusive) of the first byte to be requested.
      * @param  upper  position (exclusive) of the last byte to be requested.
+     *
+     * @see #useRangeOfInterest()
      */
     public final void rangeOfInterest(long lower, long upper) {
         if (channel instanceof ByteRangeChannel) {
@@ -1230,6 +1232,19 @@ loop:   while (hasRemaining()) {
         }
     }
 
+    /**
+     * Returns whether calls to {@code rangeOfInterest(…)} has any effect.
+     * This method is useful when {@code rangeOfInterest(…)} is invoked in a 
loop.
+     * If this method returns {@code false}, then the loop can be omitted 
completly.
+     *
+     * @return whether calls to {@code rangeOfInterest(…)} has any effect.
+     *
+     * @see #rangeOfInterest(long, long)
+     */
+    public final boolean useRangeOfInterest() {
+        return (channel instanceof ByteRangeChannel);
+    }
+
     /**
      * Forgets the given number of bytes in the buffer.
      * This is invoked for making room for more bytes.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java
index 1e6a7f10b9..eb4b50d2c3 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java
@@ -97,6 +97,7 @@ final class TiledDeferredImage extends BatchComputedImage {
      *
      * @param  tiles  range of tile indices for which to load tiles.
      * @return loaded tiles for the given indices, in row-major fashion.
+     * @throws Exception if an error occurred while loading a tile.
      */
     @Override
     protected Raster[] computeTiles(final Rectangle tiles) throws Exception {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
index f5604cfb34..cada7ef2ac 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
@@ -19,7 +19,6 @@ package org.apache.sis.storage.base;
 import java.util.Map;
 import java.util.Locale;
 import java.util.Optional;
-import java.io.IOException;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.DataBuffer;
@@ -43,7 +42,6 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.DisjointExtentException;
 import org.apache.sis.image.privy.DeferredProperty;
 import org.apache.sis.image.privy.TiledImage;
-import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.tiling.TileMatrixSet;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.util.collection.WeakValueHashMap;
@@ -1329,17 +1327,22 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
      * corner of the image in the {@link TiledGridResource}).
      *
      * The {@link Raster#getMinX()} and {@code getMinY()} coordinates of 
returned rasters
-     * shall start at the given {@code iterator.offsetAOI} values.
+     * shall start at the values given by {@link 
TileIterator#getTileOrigin(int)}.
+     * Each tile in the returned array shall be stored at the index given by
+     * {@link TileIterator#getTileIndexInResultArray()}.
      *
      * <p>This method must be thread-safe. It is implementer responsibility to 
ensure synchronization,
      * for example using {@link 
TiledGridResource#getSynchronizationLock()}.</p>
      *
      * @param  iterator  an iterator over the tiles that intersect the Area Of 
Interest specified by user.
      * @return tiles decoded from the {@link TiledGridResource}.
-     * @throws IOException if an I/O error occurred.
-     * @throws DataStoreException if a logical error occurred.
-     * @throws RuntimeException if the Java2D image cannot be created for 
another reason
-     *         (too many exception types to list them all).
+     * @throws Exception if the tile cannot be created. There is too many 
possible exceptions for listing all types,
+     *         but the main ones are {@link java.io.IOException} for I/O 
errors and various {@link RuntimeException}
+     *         subtypes for Java2D errors.
+     *
+     * @see TileIterator#createRaster()
+     * @see TileIterator#getTileOrigin(int)
+     * @see TileIterator#getTileIndexInResultArray()
      */
-    protected abstract Raster[] readTiles(TileIterator iterator) throws 
IOException, DataStoreException;
+    protected abstract Raster[] readTiles(TileIterator iterator) throws 
Exception;
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
index d8e55e8e93..2db0d56ebb 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
@@ -17,7 +17,6 @@
 package org.apache.sis.storage.geoheif;
 
 import java.io.IOException;
-import java.awt.image.Raster;
 import java.awt.image.SampleModel;
 import java.awt.image.BufferedImage;
 import java.awt.image.RasterFormatException;
@@ -25,11 +24,12 @@ import javax.imageio.ImageReader;
 import javax.imageio.ImageTypeSpecifier;
 import javax.imageio.spi.IIORegistry;
 import javax.imageio.spi.ImageReaderSpi;
+import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.IllegalOpenParameterException;
 import org.apache.sis.storage.UnsupportedEncodingException;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.util.ArraysExt;
 
 
@@ -60,7 +60,7 @@ final class FromImageIO extends Image {
      * @param  name      a name that identifies this image, for debugging 
purpose.
      * @throws RasterFormatException if the sample model cannot be created.
      */
-    FromImageIO(final CoverageBuilder builder, final ByteReader locator, final 
ImageReaderSpi provider, final String name) {
+    FromImageIO(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final ImageReaderSpi provider, final String name) {
         super(builder, locator, name);
         this.provider = provider;
     }
@@ -86,22 +86,21 @@ final class FromImageIO extends Image {
     }
 
     /**
-     * Sets the input of the given reader to an input stream positioned to the 
beginning of the image.
+     * Sets the input of the given reader to an input stream positioned at the 
beginning of the image.
      *
-     * @param  store   the store that opened the <abbr>HEIF</abbr> file.
      * @param  reader  the image reader for which to set the input.
+     * @param  input   the input from which to read bytes.
+     * @param  ranges  the ranges of bytes to read.
      * @throws DataStoreException if the input cannot be set because of its 
type.
      * @throws IOException if an I/O error occurred while setting the input.
      */
-    private void setReaderInput(final GeoHeifStore store, final ImageReader 
reader) throws DataStoreException, IOException {
-        final var request = new ByteReader.FileRegion();
-        request.input  = store.ensureOpen();
-        request.length = -1;
-        locator.resolve(request);
-        request.input.seek(request.offset);
-        request.input.buffer.order(byteOrder);
+    private void setReaderInput(final ImageReader reader, final 
ChannelDataInput input, final ByteRanges ranges)
+            throws DataStoreException, IOException
+    {
+        input.seek(ranges.offset());
+        input.buffer.order(byteOrder);
         try {
-            reader.setInput(request.input, true, true);
+            reader.setInput(input, true, true);
         } catch (IllegalArgumentException e) {
             throw new IllegalOpenParameterException("Not an image input 
stream.", e);
         }
@@ -119,7 +118,9 @@ final class FromImageIO extends Image {
     @Override
     protected ImageTypeSpecifier getImageType(final GeoHeifStore store) throws 
DataStoreException, IOException {
         final ImageReader reader = provider.createReaderInstance();
-        setReaderInput(store, reader);
+        final var ranges = new ByteRanges();
+        locator.resolve(0, -1, ranges);
+        setReaderInput(reader, 
ranges.viewAsConsecutiveBytes(store.ensureOpen()), ranges);
         final var it = reader.getImageTypes(IMAGE_INDEX);
         ImageTypeSpecifier specifier;
         if (it.hasNext()) {
@@ -139,26 +140,26 @@ final class FromImageIO extends Image {
     }
 
     /**
-     * Reads a single tile.
+     * Computes the range of bytes that will be needed for reading a single 
tile of this image.
      *
-     * @param  store    the data store reading a tile.
-     * @param  tileX    0-based column index of the tile to read, starting 
from image left.
-     * @param  tileY    0-based column index of the tile to read, starting 
from image top.
-     * @param  context  contains the target raster or the image reader to use.
-     * @return tile filled with the pixel values read by this method.
+     * @param  context  where to store the ranges of bytes.
+     * @throws DataStoreException if an error occurred while computing the 
range of bytes.
      */
     @Override
-    protected Raster readTile(final GeoHeifStore store, final long tileX, 
final long tileY,
-            final ImageResource.Coverage.ReadContext context) throws 
IOException, DataStoreException
-    {
-        final ImageReader reader = context.getReader(provider);
-        setReaderInput(store, reader);
-        final BufferedImage image;
-        try {
-            image = reader.readTile(IMAGE_INDEX, Math.toIntExact(tileX), 
Math.toIntExact(tileY));
-        } finally {
-            reader.setInput(null);
-        }
-        return image.getRaster();
+    protected Reader computeByteRanges(final 
ImageResource.Coverage.ReadContext context) throws DataStoreException {
+        locator.resolve(0, -1, context);
+        return (final ChannelDataInput input) -> {
+            final ImageReader reader = context.getReader(provider);
+            setReaderInput(reader, input, context);
+            final BufferedImage image;
+            try {
+                image = reader.readTile(IMAGE_INDEX,
+                        Math.toIntExact(context.subTileX),
+                        Math.toIntExact(context.subTileY));
+            } finally {
+                reader.setInput(null);
+            }
+            return image.getRaster();
+        };
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
index 75742b29f3..b04b221e54 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
@@ -24,7 +24,7 @@ import javax.imageio.ImageTypeSpecifier;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.isobmff.ByteRanges;
 
 
 /**
@@ -51,7 +51,7 @@ abstract class Image {
      * The provider of bytes to read from the <abbr>ISOBMFF</abbr> box.
      * The bytes are read from the {@link ChannelDataInput} at a position 
specified by the box.
      */
-    protected final ByteReader locator;
+    protected final ByteRanges.Reader locator;
 
     /**
      * The byte order to use for reading the sample values of the image.
@@ -66,7 +66,7 @@ abstract class Image {
      * @param  name     a name that identifies this image, for debugging 
purpose.
      * @throws RasterFormatException if the sample model cannot be created.
      */
-    protected Image(final CoverageBuilder builder, final ByteReader locator, 
final String name) {
+    protected Image(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final String name) {
         this.locator = locator;
         this.name    = name;
         byteOrder    = builder.byteOrder();
@@ -99,16 +99,37 @@ abstract class Image {
     }
 
     /**
-     * Reads a single tile.
+     * Computes the range of bytes that will be needed for reading a single 
tile of this image.
+     * There will be usually a single range of bytes per tile, but this method 
allows the bytes
+     * to be spread over more than one extent.
      *
-     * @param  store    the data store reading a tile.
-     * @param  tileX    0-based column index of the tile to read, starting 
from image left.
-     * @param  tileY    0-based column index of the tile to read, starting 
from image top.
-     * @param  context  contains the target raster or the image reader to use.
-     * @return tile filled with the pixel values read by this method.
+     * <p>This method does not read the bytes immediately.
+     * Instead, it returns a function which will be executed later for 
finishing the read operation.</p>
+     *
+     * @param  context  where to store the ranges of bytes.
+     * @return the function to invoke later for reading the tile.
+     * @throws DataStoreException if an error occurred while computing the 
range of bytes.
+     */
+    protected abstract Reader 
computeByteRanges(ImageResource.Coverage.ReadContext context) throws 
DataStoreException;
+
+    /**
+     * Reads a single tile from a sequence of bytes.
+     * Instances are prepared and returned by {@link #computeByteRanges 
computeByteRanges(…)}.
      */
-    protected abstract Raster readTile(final GeoHeifStore store, final long 
tileX, final long tileY,
-            final ImageResource.Coverage.ReadContext context) throws 
IOException, DataStoreException;
+    @FunctionalInterface
+    protected interface Reader {
+        /**
+         * Reads a single tile from a sequence of bytes in the given input.
+         * The implementation is responsible for setting the stream position 
before to start reading bytes.
+         * The given {@code input} will view all bytes after the initial 
position as if they were stored in
+         * one single large extent.
+         * Implementations should ignore the fact that the sequence of bytes 
may be spread in many extents.
+         *
+         * @param  input  a view of the byte sequences as if they were stored 
in one single large extent.
+         * @return tile filled with the pixel values read by this method.
+         */
+        Raster readTile(ChannelDataInput input) throws Exception;
+    }
 
     /**
      * Returns the name of this image, for debugging purposes.
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 d5f35f50a0..32439c4d07 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
@@ -16,11 +16,13 @@
  */
 package org.apache.sis.storage.geoheif;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.HashMap;
 import java.util.Optional;
-import java.util.IdentityHashMap;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import static java.lang.Math.addExact;
 import static java.lang.Math.multiplyExact;
 import java.awt.image.ColorModel;
@@ -41,6 +43,8 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.storage.base.TiledGridCoverage;
 import org.apache.sis.storage.base.TiledGridResource;
+import org.apache.sis.storage.isobmff.ByteRanges;
+import org.apache.sis.io.stream.ChannelDataInput;
 
 
 /**
@@ -109,18 +113,19 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
     private final ColorModel colorModel;
 
     /**
-     * The images that constitute the tiles of this grid. Shall contain at 
least 1 element.
+     * Information about the images that constitute the tiles of this grid. 
Shall contain at least 1 element.
+     * The {@link Image} elements contain only metadata such as position in 
the file, not actual pixel values.
      * All tiles shall have the same size.
      */
     private final Image[] tiles;
 
     /**
      * Creates a new grid coverage resource for an image.
-     * Exactly one of {@code tiles} and {@code reader} argument shall be 
non-null.
+     * Exactly one of {@code tiles} and {@code image} arguments shall be 
non-null.
      *
      * @param  builder  helper class for building the grid geometry and sample 
dimensions.
      * @param  tiles    the images that constitute the tiles, or {@code null} 
if {@code reader} is provided.
-     * @param  image    the single tile for the wole image, or {@code null} if 
{@code tiles} is provided.
+     * @param  image    the single tile for the whole image, or {@code null} 
if {@code tiles} is provided.
      * @throws RasterFormatException if the sample dimensions or sample model 
cannot be created.
      * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform 
cannot be created.
      */
@@ -299,19 +304,36 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
          * @return tiles decoded from the enclosing resource.
          */
         @Override
-        protected Raster[] readTiles(final TileIterator iterator) throws 
IOException, DataStoreException {
-            final Raster[] result = new Raster[iterator.tileCountInQuery];
-            try (final var context = new ReadContext(iterator)) {
-                synchronized (getSynchronizationLock()) {
+        protected Raster[] readTiles(final TileIterator iterator) throws 
Exception {
+            final var result = new Raster[iterator.tileCountInQuery];
+            final var requests = new ReadContext[result.length];
+            int count = 0;
+            synchronized (getSynchronizationLock()) {
+                try (final var context = new ReadContext(iterator)) {
                     do {
-                        Raster tile = iterator.getCachedTile();
-                        if (tile == null) {
-                            long[] tileCoord = 
iterator.getTileCoordinatesInResource();
-                            tile = readTile(tileCoord[0], tileCoord[1], 
context);
-                            tile = iterator.cache(iterator.moveRaster(tile));
+                        Raster raster = iterator.getCachedTile();
+                        if (raster != null) {
+                            result[iterator.getTileIndexInResultArray()] = 
raster;
+                        } else {
+                            requests[count++] = new ReadContext(context, 
ImageResource.this);
                         }
-                        result[iterator.getTileIndexInResultArray()] = tile;
                     } while (iterator.next());
+                    /*
+                     * For any tile that was not in the cache, read them in 
the order they appear in the file.
+                     * The ranges of bytes of all tiles should be declared 
before to start the reading process,
+                     * for giving a chance to emit a single "HTTP range" 
request. Then, for each tile, `input`
+                     * may be replaced by a view hiding the fact that byte may 
be spread in more than one extent.
+                     */
+                    Arrays.sort(requests, 0, count);
+                    final ChannelDataInput input = store.ensureOpen();
+                    if (input.useRangeOfInterest()) {
+                        for (int i=0; i<count; i++) {
+                            requests[i].notify(input);  // Implementation 
should take care of merging the ranges.
+                        }
+                    }
+                    for (int i=0; i<count; i++) {
+                        requests[i].readTile(input, result);        // 
Implementation may create an `input` view.
+                    }
                 }
             }
             return result;
@@ -321,17 +343,27 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
          * 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 implements AutoCloseable {
+        static final class ReadContext extends ByteRanges implements 
AutoCloseable {
             /**
              * Iterator over the tiles to read.
              */
             private final AOI iterator;
 
             /**
-             * The image reader created for reading the tiles,
-             * or {@code null} if image readers are not used.
+             * The image readers created for reading the tiles.
+             */
+            private final Map<ImageReaderSpi, ImageReader> readers;
+
+            /**
+             * The function to execute for reading the image as a tile.
              */
-            private Map<ImageReaderSpi, ImageReader> readers;
+            private final Image.Reader reader;
+
+            /**
+             * 0-based index (column and row) of the tile to read inside the 
image to read.
+             * This is a tile inside the {@linkplain #tile}, i.e. a second 
level of tiling.
+             */
+            final long subTileX, subTileY;
 
             /**
              * Creates a new read context.
@@ -340,6 +372,42 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
              */
             private ReadContext(final AOI iterator) {
                 this.iterator = iterator;
+                this.readers  = new HashMap<>();
+                this.reader   = null;
+                this.subTileX = 0;
+                this.subTileY = 0;
+            }
+
+            /**
+             * Creates a context which is a snapshot of the given context at 
the current iterator position.
+             *
+             * @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.
+             */
+            private ReadContext(final ReadContext parent, final ImageResource 
owner) throws DataStoreException {
+                this.iterator = new Snapshot(parent.iterator);
+                this.readers  = parent.readers;
+                final long[] tileCoord = 
iterator.getTileCoordinatesInResource();
+                final Image tile = owner.getTile(tileCoord[0], tileCoord[1]);
+                subTileX = tileCoord[0] % tile.numXTiles;
+                subTileY = tileCoord[1] % tile.numYTiles;
+                reader   = tile.computeByteRanges(this);
+            }
+
+            /**
+             * Reads the tile and store the result in the given array.
+             * The given input is the stream over the <abbr>HEIF</abbr> file
+             * (not yet the view showing all bytes as if they were stored in a 
single extent).
+             *
+             * @param  input   the input from which to read bytes.
+             * @param  result  where to store the result.
+             */
+            final void readTile(ChannelDataInput input, final Raster[] result) 
throws Exception {
+                input = viewAsConsecutiveBytes(input);
+                Raster raster = reader.readTile(input);
+                raster = iterator.cache(iterator.moveRaster(raster));
+                result[iterator.getTileIndexInResultArray()] = raster;
             }
 
             /**
@@ -363,15 +431,17 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
              * @throws IOException if an error occurred while creating the 
image reader.
              */
             public ImageReader getReader(final ImageReaderSpi spi) throws 
IOException {
-                if (readers == null) {
-                    readers = new IdentityHashMap<>();
-                }
-                ImageReader reader = readers.get(spi);
-                if (reader == null) {
-                    reader = spi.createReaderInstance();
-                    readers.put(spi, reader);
+                try {
+                    return readers.computeIfAbsent(spi, (factory) -> {
+                        try {
+                            return factory.createReaderInstance();
+                        } catch (IOException e) {
+                            throw new UncheckedIOException(e);
+                        }
+                    });
+                } catch (UncheckedIOException e) {
+                    throw e.getCause();
                 }
-                return reader;
             }
 
             /**
@@ -380,30 +450,24 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
              */
             @Override
             public void close() {
-                if (readers != null) {
-                    readers.values().forEach(ImageReader::dispose);
-                }
+                readers.values().forEach(ImageReader::dispose);
             }
         }
     }
 
     /**
-     * Reads a single tile.
+     * Returns information (not pixel values) about the tile at the given tile 
coordinates.
+     * The tile coordinates are in the tile matrix including the sub-divisions 
in each tile.
+     * This method returns the image which contains the requested tile.
      *
-     * @param  tileX    0-based column index of the tile to read, starting 
from image left.
-     * @param  tileY    0-based column index of the tile to read, starting 
from image top.
-     * @param  context  contains the target raster or the image reader to use.
-     * @return tile filled with the pixel values read by this method.
+     * @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.
+     * @return information about the tile at the specified tile coordinates.
      */
-    private Raster readTile(long tileX, long tileY, final Coverage.ReadContext 
context)
-            throws IOException, DataStoreException
-    {
-        Image tile = tiles[0];
-        final long tx = tileX / tile.numXTiles;
-        final long ty = tileY / tile.numYTiles;
-        tile = tiles[Math.toIntExact(addExact(tx, multiplyExact(ty, 
tileMatrixRowStride)))];
-        tileX %= tile.numXTiles;
-        tileY %= tile.numYTiles;
-        return tile.readTile(store, tileX, tileY, context);
+    final Image getTile(long tileX, long tileY) {
+        Image template = tiles[0];      // First tile taken as a template for 
all tiles.
+        tileX /= template.numXTiles;
+        tileY /= template.numYTiles;
+        return tiles[Math.toIntExact(addExact(tileX, multiplyExact(tileY, 
tileMatrixRowStride)))];
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
index e3bc44b1d1..087cabb903 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
@@ -36,7 +36,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.UnsupportedEncodingException;
 import org.apache.sis.storage.isobmff.Box;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.storage.isobmff.Root;
 import org.apache.sis.storage.isobmff.base.EntityToGroup;
 import org.apache.sis.storage.isobmff.base.GroupList;
@@ -257,7 +257,7 @@ final class ResourceBuilder {
      * @return the item for locating the identified data, or {@code null} if 
none.
      * @throws DataStoreContentException if there is two ore more items for 
the same identifier.
      */
-    private ByteReader getLocationByIdentifier(final int itemID) throws 
DataStoreContentException {
+    private ByteRanges.Reader getLocationByIdentifier(final int itemID) throws 
DataStoreContentException {
         final Object item = itemLocations.get(itemID);
         if (item == null) {
             return data;    // May be null.
@@ -335,7 +335,7 @@ final class ResourceBuilder {
             if (firstBuilder == null) {
                 firstBuilder = coverage;
             }
-            final ByteReader locator;
+            final ByteRanges.Reader locator;
             final Image.Supplier image;
             switch (entry.itemType) {
                 default: {
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 945520ff7e..2e9a05c964 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
@@ -16,10 +16,8 @@
  */
 package org.apache.sis.storage.geoheif;
 
-import java.io.IOException;
 import static java.lang.Math.addExact;
 import static java.lang.Math.multiplyExact;
-import java.awt.image.Raster;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
@@ -27,10 +25,11 @@ import java.awt.image.RasterFormatException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.image.privy.ImageUtilities;
 import org.apache.sis.image.privy.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.storage.DataStoreException;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.isobmff.ByteRanges;
 
 
 /**
@@ -66,25 +65,22 @@ final class UncompressedImage extends Image {
      * @param  name     a name that identifies this image, for debugging 
purpose.
      * @throws RasterFormatException if the sample model cannot be created.
      */
-    UncompressedImage(final CoverageBuilder builder, final ByteReader locator, 
final String name) throws DataStoreException {
+    UncompressedImage(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final String name)
+            throws DataStoreException
+    {
         super(builder, locator, name);
         sampleModel = builder.sampleModel();
         dataType    = builder.dataType();     // Shall be after 
`sampleModel()`.
     }
 
     /**
-     * Reads a single tile. The default implementation assumes an uncompressed 
tile.
+     * Computes the range of bytes that will be needed for reading a single 
tile of this image.
      *
-     * @param  store    the data store reading a tile.
-     * @param  tileX    0-based column index of the tile to read, starting 
from image left.
-     * @param  tileY    0-based column index of the tile to read, starting 
from image top.
-     * @param  context  contains the target raster or the image reader to use.
-     * @return tile filled with the pixel values read by this method.
+     * @param  context  where to store the ranges of bytes.
+     * @throws DataStoreException if an error occurred while computing the 
range of bytes.
      */
     @Override
-    protected Raster readTile(final GeoHeifStore store, final long tileX, 
final long tileY,
-            final ImageResource.Coverage.ReadContext context) throws 
IOException, DataStoreException
-    {
+    protected Reader computeByteRanges(final 
ImageResource.Coverage.ReadContext context) throws DataStoreException {
         final long[] sourceSize = {
             // Note: the following ignores `MultiPixelPackedSampleModel`, but 
that model does not seem used by HEIF.
             sampleModel.getWidth() * (long) sampleModel.getNumDataElements(),
@@ -99,30 +95,29 @@ final class UncompressedImage extends Image {
         final int  dataSize  = dataType.bytes();
         final long tileSize  = multiplyExact(region.length, dataSize);
         final long skipBytes = region.getStartByteOffset(dataSize);
-        final var  request   = new ByteReader.FileRegion();
-        request.input  = store.ensureOpen();
-        request.offset = multiplyExact(addExact(multiplyExact(tileY, 
numXTiles), tileX), tileSize);
-        request.length = tileSize;
-        request.skip(skipBytes);
-        locator.resolve(request);
-        /*
-         * Now read all banks and store the values in the image buffer.
-         * If there is many banks (`InterleavingMode.COMPONENT`), these
-         * banks are assumed consecutive.
-         */
-        request.input.buffer.order(byteOrder);
-        final var hr = new 
HyperRectangleReader(ImageUtilities.toNumberEnum(dataType), request.input);
-        hr.setOrigin(request.offset - skipBytes);
-        final WritableRaster raster = context.createRaster();
-        final DataBuffer data = raster.getDataBuffer();
-        final int numBanks = data.getNumBanks();
-        for (int b=0; b<numBanks; b++) {
-            if (b != 0) {
-                hr.setOrigin(addExact(hr.getOrigin(), tileSize));
+        final long tileIndex = addExact(multiplyExact(context.subTileY, 
numXTiles), context.subTileX);
+        final long offset    = addExact(multiplyExact(tileIndex, tileSize), 
skipBytes);
+        locator.resolve(offset, tileSize - skipBytes, context);
+        return (final ChannelDataInput input) -> {
+            /*
+             * Now read all banks and store the values in the image buffer.
+             * If there is many banks (`InterleavingMode.COMPONENT`), these
+             * banks are assumed consecutive.
+             */
+            input.buffer.order(byteOrder);
+            final var hr = new 
HyperRectangleReader(ImageUtilities.toNumberEnum(dataType), input);
+            hr.setOrigin(context.offset() - skipBytes);
+            final WritableRaster raster = context.createRaster();
+            final DataBuffer data = raster.getDataBuffer();
+            final int numBanks = data.getNumBanks();
+            for (int b=0; b<numBanks; b++) {
+                if (b != 0) {
+                    hr.setOrigin(addExact(hr.getOrigin(), tileSize));
+                }
+                hr.setDestination(RasterFactory.wrapAsBuffer(data, b));
+                hr.readAsBuffer(region, 0);
             }
-            hr.setDestination(RasterFactory.wrapAsBuffer(data, b));
-            hr.readAsBuffer(region, 0);
-        }
-        return raster;
+            return raster;
+        };
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteRanges.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteRanges.java
new file mode 100644
index 0000000000..2d2df96047
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteRanges.java
@@ -0,0 +1,163 @@
+/*
+ * 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.isobmff;
+
+import java.util.Arrays;
+import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.UnsupportedEncodingException;
+
+
+/**
+ * Ranges of bytes to read from a <abbr>HEIF</abbr> file.
+ * Instances are given to {@link Reader#resolve(long, long, ByteRanges)} for
+ * converting values relative to a box item to values relative to the stream.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public class ByteRanges implements Comparable<ByteRanges> {
+    /**
+     * Interface implemented by the {@link TreeNode} subclasses which read 
bytes only when requested.
+     * Contrarily to the constructors of {@link Box} subclasses which read all 
the payload immediately,
+     * the method provided by this interface allow access to a subset of a 
potentially large sequence of bytes.
+     *
+     * @author Martin Desruisseaux (Geomatys)
+     */
+    public interface Reader {
+        /**
+         * Converts an offset relative to the data of this item to offsets 
relative to the origin of the input stream.
+         * The subset is specified by the {@code offset} and {@code length} 
arguments, where an {@code offset} of zero
+         * is for the first byte stored by this item. Implementation will 
typically add to {@code offset} the position
+         * in the file of the first byte.
+         *
+         * @param  offset  offset of the first byte to read relative to the 
data stored is this item.
+         * @param  length  maximum number of bytes to read, starting at the 
offset, or a negative value for reading all.
+         * @param  addTo   where to add the ranges of bytes to read as offsets 
relatives to the beginning of the file.
+         * @throws UnsupportedEncodingException if this item uses an 
unsupported construction method.
+         * @throws DataStoreContentException if the <abbr>HEIF</abbr> file is 
malformed.
+         * @throws DataStoreException if another logical error occurred.
+         * @throws ArithmeticException if an integer overflow occurred.
+         *
+         * @see #addRange(long, long)
+         */
+        void resolve(long offset, long length, ByteRanges addTo) throws 
DataStoreException;
+    }
+
+    /**
+     * Ranges of bytes to read, relative to the file and in the order they 
were added.
+     * Consecutive ranges are merged in a single range, but no other 
simplification is performed.
+     * Offsets at even indexes are starting point (inclusive) and offsets at 
odd indexes are end point (exclusive).
+     */
+    private long[] offsets;
+
+    /**
+     * Creates an initially empty set of byte ranges.
+     */
+    public ByteRanges() {
+    }
+
+    /**
+     * Adds a range of byte offsets relative to the beginning of the 
<abbr>HEIF</abbr> file.
+     * this method is invoked by {@link Reader#resolve(long, long, 
ByteRanges)} implementations
+     * for specifying all range of bytes to read. This method is usually 
invoked exactly once.
+     * It may be invoked more often if the bytes are spread over many regions 
of the file.
+     *
+     * @param start  offset of the first byte to read, inclusive.
+     * @param end    offset after the last byte to read.
+     */
+    public void addRange(final long start, final long end) {
+        if (start < end) {
+            if (offsets == null) {
+                offsets = new long[] {start, end};
+            } else {
+                int i = offsets.length - 1;
+                if (offsets[i] == start) {
+                    offsets[i] = end;
+                } else {
+                    offsets = Arrays.copyOf(offsets, ++i + 2);
+                    offsets[i] = start;
+                    offsets[i+1] = end;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the offset of the first byte to read, or 0 if unknown.
+     * Note that this is not necessarily the smallest offset, because nothing 
prevent the
+     * ranges to be declared out-of-order. However, out-of-order ranges are 
assumed rare.
+     */
+    public final long offset() {
+        return (offsets != null) ? offsets[0] : 0;      // A non-null array 
should be non-empty.
+    }
+
+    /**
+     * Compares this set of ranges with the given ranges for the order in 
which they appear in the file.
+     * In the current implementation, the ordering is based on the offset of 
the first byte to read.
+     * It may be revised in a future version if useful.
+     *
+     * @param  other  the other range of bytes to compare with this instance.
+     * @return negative is this range of bytes is before {@code other}, 
positive if after, 0 if same offset.
+     */
+    @Override
+    public final int compareTo(final ByteRanges other) {
+        return Long.compare(offset(), other.offset());
+    }
+
+    /**
+     * Notifies the given input about the range of bytes which will be 
requested. This method should be
+     * invoked for the {@code ByteRanges} associated to all tiles to read 
before the actual reading starts.
+     * This notification is only a hint. It can be used for preparing a 
<abbr>HTTP</abbr> range request.
+     *
+     * @param  input  the input stream to notify about the ranges of bytes 
that will be requested.
+     */
+    public final void notify(final ChannelDataInput input) {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        final long[] offsets = this.offsets;
+        if (offsets != null) {
+            for (int i=0; i < offsets.length;) {
+                // Note: FileCacheByteChannel uses a RangeSet for merging 
efficiently those ranges.
+                input.rangeOfInterest(offsets[i++], offsets[i++]);
+            }
+        }
+    }
+
+    /**
+     * Returns a stream of bytes for the given request as if all bytes were 
stored in one sequence.
+     * If all data are stored in the same extent, then this method returns 
{@code input} directly.
+     * Otherwise, this method returns a view over the given input as if all 
extents were consecutive.
+     *
+     * @todo The view is not yet implemented.
+     *
+     * @param  input    the channel opened on the <abbr>HEIF</abbr> file.
+     * @param  request  the region to read, as adjusted by {@link 
#resolve(long, long, ByteRanges)}.
+     * @return the input to use. By default, this is {@code input} directly.
+     * @throws DataStoreException if the input cannot be created.
+     */
+    public final ChannelDataInput viewAsConsecutiveBytes(ChannelDataInput 
input) throws DataStoreException {
+        if (offsets != null && offsets.length == 2) {
+            return input;
+        }
+        /*
+         * There is at least two extents to read. Create a channel
+         * which will read each extent as if they were consecutive.
+         * TODO: not yet implemented.
+         */
+        throw new UnsupportedEncodingException("Not supported yet");
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteReader.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteReader.java
deleted file mode 100644
index d32e68297f..0000000000
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/ByteReader.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.isobmff;
-
-import org.apache.sis.io.stream.ChannelDataInput;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.UnsupportedEncodingException;
-
-
-/**
- * Interface implemented by {@link TreeNode} which read bytes only when 
requested.
- * Contrarily to {@link Box} constructors which are read all the payload 
immediately,
- * the method provided by this interface reads only a subset of a potentially 
large
- * array of bytes.
- *
- * @author Martin Desruisseaux (Geomatys)
- */
-public interface ByteReader {
-    /**
-     * Converts an offset relative to the data of this item to an offset 
relative to the origin of the input stream.
-     * This method updates the {@link FileRegion#offset} value in-place, 
typically by adding the stream position of
-     * the first byte of the data stored in this item. The {@link 
FileRegion#input} is usually unmodified, but this
-     * method may also replace it by a temporary instance if the bytes to read 
are spread in different regions of the file.
-     *
-     * @param  request  the input stream, offset and length of the region to 
read. Modified in-place by this method.
-     * @throws UnsupportedEncodingException if this item uses an unsupported 
construction method.
-     * @throws DataStoreContentException if the <abbr>HEIF</abbr> file is 
malformed.
-     * @throws DataStoreException if another logical error occurred.
-     * @throws ArithmeticException if an integer overflow occurred.
-     */
-    void resolve(FileRegion request) throws DataStoreException;
-
-    /**
-     * A sub-region of the item to read. Instances of this class are given to 
{@link #resolve(FileRegion)}
-     * and modified in-place for converting values relative to the item to 
values relative to the stream.
-     */
-    static final class FileRegion {
-        /**
-         * The input stream from which to read the bytes. After the call to 
{@link #resolve(FileRegion)},
-         * it may be a temporary instance providing a view of distinct extents 
as if they were a single
-         * stream of consecutive bytes.
-         */
-        public ChannelDataInput input;
-
-        /**
-         * Offset of the first byte to read. Before the call to {@link 
#resolve(FileRegion)},
-         * this offset is relative to the data stored by the item (i.e., 
offset zero is the
-         * first byte stored by the item). After the call to {@code 
resolve(FileRegion)},
-         * the offset is relative to the origin of {@link #input}.
-         */
-        public long offset;
-
-        /**
-         * Maximum number of bytes to read, starting at the offset, or a 
negative value for reading all.
-         * After the call to {@link #resolve(FileRegion)}, should be updated 
to a value not greater than
-         * the number of bytes actually available.
-         */
-        public long length;
-
-        /**
-         * Creates an initially empty file region.
-         */
-        public FileRegion() {
-        }
-
-        /**
-         * Skips the given number of bytes at the beginning.
-         * This method increments {@link #offset} and decrements {@link 
#length} by the given amount.
-         *
-         * @param  bytes  the number of bytes to skip at the beginning.
-         */
-        public void skip(final long bytes) {
-            offset  = Math.addExact(offset, bytes);
-            length -= bytes;
-        }
-    }
-}
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/Reader.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/Reader.java
index ee7389b883..b37bf0b435 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/Reader.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/Reader.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.ArrayList;
+import java.util.OptionalLong;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.net.URI;
@@ -253,7 +254,10 @@ public final class Reader implements Cloneable {
      * @throws IOException if an error occurred while reading the integers.
      */
     public final int[] readRemainingInts() throws IOException {
-        int n = Math.toIntExact((endOfCurrentBox() - 
input.getStreamPosition()) / Integer.BYTES);
+        if (endOfCurrentBox < 0) {
+            throw new IOException("Stream of unknown length.");
+        }
+        int n = Math.toIntExact((endOfCurrentBox - input.getStreamPosition()) 
/ Integer.BYTES);
         return (n != 0) ? input.readInts(n) : ArraysExt.EMPTY_INT;
     }
 
@@ -309,13 +313,9 @@ public final class Reader implements Cloneable {
      * Returns the stream position after the last byte of the current box.
      *
      * @return stream position after the last byte of the current box.
-     * @throws IOException if the box size is unknown.
      */
-    public final long endOfCurrentBox() throws IOException {
-        if (endOfCurrentBox >= 0) {
-           return endOfCurrentBox;
-        }
-        throw new IOException("Stream of unknown length.");
+    public final OptionalLong endOfCurrentBox() {
+        return (endOfCurrentBox >= 0) ? OptionalLong.of(endOfCurrentBox) : 
OptionalLong.empty();
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemData.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemData.java
index bd2a2c4433..c044b827f8 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemData.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemData.java
@@ -18,7 +18,8 @@ package org.apache.sis.storage.isobmff.base;
 
 import java.io.IOException;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.storage.isobmff.Reader;
 import org.apache.sis.storage.isobmff.Box;
 
@@ -34,7 +35,7 @@ import org.apache.sis.storage.isobmff.Box;
  * @author Johann Sorel (Geomatys)
  * @author Martin Desruisseaux (Geomatys)
  */
-public class ItemData extends Box implements ByteReader {
+public class ItemData extends Box implements ByteRanges.Reader {
     /**
      * Numerical representation of the {@code "idat"} box type.
      */
@@ -56,7 +57,8 @@ public class ItemData extends Box implements ByteReader {
     public final long payloadOffset;
 
     /**
-     * Number of bytes to read.
+     * Number of bytes to read, or -1 for reading until the end of file.
+     * Note that -1 is also the maximal unsigned value.
      */
     @Interpretation(Type.UNSIGNED)
     public final long size;
@@ -70,23 +72,29 @@ public class ItemData extends Box implements ByteReader {
      */
     public ItemData(final Reader reader) throws IOException {
         payloadOffset = reader.input.getStreamPosition();
-        size = reader.endOfCurrentBox() - payloadOffset;
+        size = reader.endOfCurrentBox().orElse(payloadOffset - 1) - 
payloadOffset;
     }
 
     /**
      * Converts an offset relative to the data of this item to an offset 
relative to the origin of the input stream.
-     * This method updates the {@link FileRegion#offset} value in-place by 
adding the stream position of the first
-     * byte of the data stored in this item.
+     * This method adds the stream position of the first byte of the data 
stored in this item.
      *
-     * @param  request  the input stream, offset and length of the region to 
read. Modified in-place by this method.
+     * @param  offset  offset of the first byte to read relative to the data 
stored in this item.
+     * @param  length  maximum number of bytes to read, starting at the 
offset, or a negative value for reading all.
+     * @param  addTo   where to add the range of bytes to read as offsets 
relatives to the beginning of the file.
      * @throws ArithmeticException if an integer overflow occurred.
      */
     @Override
-    public void resolve(final FileRegion request) {
-        ArgumentChecks.ensureBetween("offset", 0, size - Math.max(0, 
request.length), request.offset);
-        if (request.length > size || request.length < 0) {
-            request.length = size;
+    public void resolve(long offset, long length, ByteRanges addTo) throws 
DataStoreException {
+        if (size >= 0) {
+            ArgumentChecks.ensureBetween("offset", 0, size - Math.max(0, 
length), offset);
+            if (length > size || length < 0) {
+                length = size;
+            }
+        } else if (length < 0) {
+            throw new DataStoreException("Stream of unknown length.");
         }
-        request.offset = Math.addExact(payloadOffset, request.offset);
+        offset = Math.addExact(payloadOffset, offset);
+        addTo.addRange(offset, Math.addExact(offset, length));
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemLocation.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemLocation.java
index 879bb652a8..e2fd25011b 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemLocation.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/base/ItemLocation.java
@@ -24,7 +24,7 @@ import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.UnsupportedEncodingException;
 import org.apache.sis.storage.isobmff.UnsupportedVersionException;
 import org.apache.sis.storage.isobmff.VectorReader;
-import org.apache.sis.storage.isobmff.ByteReader;
+import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.storage.isobmff.TreeNode;
 import org.apache.sis.storage.isobmff.FullBox;
 import org.apache.sis.storage.isobmff.Reader;
@@ -89,7 +89,7 @@ public final class ItemLocation extends FullBox {
         }
         items = new Item[count];
         for (int i=0; i<count; i++) {
-            items[i] = new Item(input, version, sizes);
+            items[i] = new Item(reader, input, version, sizes);
         }
     }
 
@@ -98,7 +98,7 @@ public final class ItemLocation extends FullBox {
      * An extent is a continuous subset of the bytes of the resource. The full 
resource is formed by the
      * concatenation of the extents in the order specified in this {@code 
Item} object.
      */
-    public static final class Item extends TreeNode implements ByteReader {
+    public static final class Item extends TreeNode implements 
ByteRanges.Reader {
         /**
          * Identifier of this item, as an unsigned integer.
          */
@@ -180,13 +180,16 @@ public final class ItemLocation extends FullBox {
         /**
          * Decodes a single item.
          *
+         * @param  reader   the reader from which to read the payload.
          * @param  input    the input stream from which to read the item.
          * @param  version  version of the enclosing {@link ItemLocation}.
          * @param  sizes    sizes of (offset, length, base, index) in that 
order on 4 bits each.
          * @throws IOException if an error occurred while reading the payload.
          * @throws DataStoreContentException if the <abbr>HEIF</abbr> file is 
malformed.
          */
-        Item(final ChannelDataInput input, final int version, int sizes) 
throws IOException, DataStoreContentException {
+        Item(final Reader reader, final ChannelDataInput input, final int 
version, int sizes)
+                throws IOException, DataStoreContentException
+        {
             itemID = (version < 2) ? input.readUnsignedShort() : 
input.readInt();
             constructionMethod = (version == 0) ? 0 : (byte) 
(input.readShort() & 0x0F);
             dataReferenceIndex = input.readShort();
@@ -200,9 +203,18 @@ public final class ItemLocation extends FullBox {
                 if (offsetReader != null) offsetReader.read(input, i);
                 if (lengthReader != null) lengthReader.read(input, i);
             }
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            Vector extentLength;
             itemReferenceIndex = result( indexReader);
             extentOffset       = result(offsetReader);
             extentLength       = result(lengthReader);
+            if (extentLength == null) {
+                final var endOfCurrentBox = reader.endOfCurrentBox();
+                if (endOfCurrentBox.isPresent()) {
+                    extentLength = Vector.create(new long[] 
{endOfCurrentBox.getAsLong() - baseOffset}, true);
+                }
+            }
+            this.extentLength = extentLength;
         }
 
         /**
@@ -224,57 +236,61 @@ public final class ItemLocation extends FullBox {
         }
 
         /**
-         * Converts an offset relative to the data of this item to an offset 
relative to the origin of the input stream.
-         * This method updates the {@link FileRegion#offset} value in-place. 
It may also replace {@link FileRegion#input}
-         * if the bytes to read are spread in different regions of the item.
+         * Converts an offset relative to the data of this item to offsets 
relative to the origin of the input stream.
          *
-         * @param  request  the input stream, offset and length of the region 
to read. Modified in-place by this method.
+         * @param  offset  offset of the first byte to read relative to the 
data stored in this item.
+         * @param  length  maximum number of bytes to read, starting at the 
offset, or a negative value for reading all.
+         * @param  addTo   where to add the range of bytes to read as offsets 
relatives to the beginning of the file.
          * @throws UnsupportedEncodingException if this item uses an 
unsupported construction method.
          * @throws DataStoreContentException if the <abbr>HEIF</abbr> file is 
malformed.
          * @throws ArithmeticException if an integer overflow occurred.
          */
         @Override
-        public void resolve(final FileRegion request) throws 
DataStoreException {
+        public void resolve(long offset, long length, ByteRanges addTo) throws 
DataStoreException {
             switch (constructionMethod) {
-                default: throw new DataStoreContentException("Unexpected 
construction method.");
+                default: {
+                    throw new DataStoreContentException("Unexpected 
construction method.");
+                }
                 case IDAT_OFFSET:
-                case ITEM_OFFSET: throw new UnsupportedEncodingException("Not 
supported yet");
+                case ITEM_OFFSET: {
+                    throw new UnsupportedEncodingException("Not supported 
yet");
+                }
                 case FILE_OFFSET: {
                     if (dataReferenceIndex != 0) {
                         throw new UnsupportedEncodingException("Not supported 
yet");
                     }
-                    long start = 0;
-                    if (extentLength != null) {
-                        final int n = extentLength.size();
-                        for (int i=0; i<n; i++) {
-                            long available = extentLength.longValue(i);
-                            if (request.offset >= available) {
-                                request.offset -= available;
-                                continue;           // Extent is before 
requested data.
-                            }
-                            available -= request.offset;
-                            if (request.length > available && i < n-1) {
-                                /*
-                                 * There is at least two extents to read. 
Create a channel
-                                 * which will read each extent as if they were 
consecutive.
-                                 * TODO: assign a temporary instance to 
`request.input`.
-                                 */
-                                throw new UnsupportedEncodingException("Not 
supported yet");
-                            }
-                            if (extentOffset != null && i < 
extentOffset.size()) {
-                                start = extentOffset.longValue(i);
-                            }
-                            if (request.length > available || request.length < 
0) {
-                                request.length = available;
+                    final int offsetCount = (extentOffset != null) ? 
extentOffset.size() : 0;
+                    final int lengthCount = (extentLength != null) ? 
extentLength.size() : 0;
+                    final int n = Math.max(1, Math.min(offsetCount, 
lengthCount));
+                    for (int i=0; i<n; i++) {
+                        long available;
+                        if (i < lengthCount) {
+                            available = extentLength.longValue(i);
+                        } else if (length >= 0) {
+                            available = length;
+                        } else {
+                            throw new DataStoreException("Stream of unknown 
length.");
+                        }
+                        if (available < offset) {
+                            offset -= available;    // Make offset relative to 
the next extent.
+                            continue;               // Skip all extents before 
the first byte.
+                        }
+                        long position = Math.addExact(baseOffset, offset);
+                        if (i < offsetCount) {
+                            position = Math.addExact(position, 
extentOffset.longValue(i));
+                        }
+                        offset = 0;                 // Next extents will start 
from the first byte.
+                        if (length >= 0) {
+                            if (length < available) {
+                                available = length;
+                                length = 0;
+                            } else {
+                                length -= available;
                             }
-                            break;
                         }
+                        addTo.addRange(position, Math.addExact(position, 
available));
+                        if (length == 0) break;     // Short circuit if not 
reading everything.
                     }
-                    /*
-                     * All the bytes to read are in a single extent. We can
-                     * use the existing channel directly, without wrapping.
-                     */
-                    request.offset = Math.addExact(request.offset, 
Math.addExact(baseOffset, start));
                 }
             }
         }

Reply via email to