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)); } } }