This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 5b82429ee8 When computing the sub-region to read from a large untiled 
raster, pixel coordinates need to be converted to sample value coordinates. 
This conversion was done in TIFF reader but missing in HEIF reader. Since the 
conversion is not obvious, add a helper `Builder` class.
5b82429ee8 is described below

commit 5b82429ee8d6caa5f464655153931f15a40b8704
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jul 6 11:51:54 2026 +0200

    When computing the sub-region to read from a large untiled raster,
    pixel coordinates need to be converted to sample value coordinates.
    This conversion was done in TIFF reader but missing in HEIF reader.
    Since the conversion is not obvious, add a helper `Builder` class.
---
 .../main/org/apache/sis/image/ComputedImage.java   |   1 +
 .../image/internal/shared/BatchComputedImage.java  |   5 +-
 .../image/internal/shared/SampleModelBuilder.java  |   5 +-
 .../apache/sis/io/stream/HyperRectangleWriter.java |  14 +-
 .../main/org/apache/sis/io/stream/Region.java      | 171 ++++++++++++++++++++-
 .../org/apache/sis/storage/internal/Resources.java |   5 +
 .../sis/storage/internal/Resources.properties      |   1 +
 .../sis/storage/internal/Resources_fr.properties   |   1 +
 .../sis/storage/tiling/TiledGridCoverage.java      |  42 ++++-
 .../main/org/apache/sis/util/ArraysExt.java        |  20 ++-
 .../main/org/apache/sis/storage/geoheif/Image.java |   2 +-
 .../apache/sis/storage/geoheif/ImageResource.java  |  29 ++++
 .../sis/storage/geoheif/UncompressedImage.java     |  53 +++----
 13 files changed, 296 insertions(+), 53 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
index 934b8d0c32..e1c90d0203 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
@@ -600,6 +600,7 @@ public abstract class ComputedImage extends PlanarImage 
implements Disposable {
      * @param  tiles  indices of the tiles which will be prefetched.
      * @return handler on which to invoke {@code dispose()} after the prefetch 
operation
      *         completed (successfully or not), or {@code null} if none.
+     * @throws ImagingOpException if an error occurred while preparing tile 
computation.
      *
      * @since 1.2
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/BatchComputedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/BatchComputedImage.java
index 9aa46cc72d..5495346bce 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/BatchComputedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/BatchComputedImage.java
@@ -172,14 +172,17 @@ public abstract class BatchComputedImage extends 
ComputedImage {
      *
      * @param  region  indices of the tiles which will be prefetched.
      * @return handler on which to invoke {@code dispose()} after the prefetch 
operation.
+     * @throws ImagingOpException if an error occurred while preparing tile 
computation.
      */
     @Override
     protected Disposable prefetch(final Rectangle region) {
         final Raster[] tiles;
         try {
             tiles = computeTiles(region);
+        } catch (ImagingOpException e) {
+            throw e;
         } catch (Exception e) {
-            throw (ImagingOpException) new 
ImagingOpException(null).initCause(e);
+            throw (ImagingOpException) new 
ImagingOpException(e.getMessage()).initCause(e);
         }
         final Rasters r = new Rasters(region, tiles);
         synchronized (this) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/SampleModelBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/SampleModelBuilder.java
index d5b753a486..4f0e421c5b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/SampleModelBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/SampleModelBuilder.java
@@ -181,10 +181,9 @@ public final class SampleModelBuilder {
             bandOffsets    = cm.getBandOffsets();
             scanlineStride = cm.getScanlineStride();
             pixelStride    = cm.getPixelStride();
-            for (int i=0; i<bankIndices.length; i++) {
-                if (bankIndices[i] != 0) return;
+            if (ArraysExt.allEquals(bankIndices, 0)) {
+                bankIndices = null;     // PixelInterleavedSampleModel
             }
-            bankIndices = null;     // PixelInterleavedSampleModel
         } else if (model instanceof SinglePixelPackedSampleModel) {
             final var cm = (SinglePixelPackedSampleModel) model;
             bitMasks       = cm.getBitMasks();
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
index 9b949a64de..65f7b6512c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
@@ -74,7 +74,6 @@ public class HyperRectangleWriter {
      * Creates a new writer for data of a shape specified by the given region.
      * The region also specifies the subset to write.
      *
-     * @param  output  where to write data.
      * @param  region  size of the source hyper-rectangle and region to write.
      * @throws ArithmeticException if the region is too large.
      */
@@ -351,7 +350,7 @@ public class HyperRectangleWriter {
             if (isSupported(this, sm)) {
                 return create(sm, null);
             } else {
-                throw new RasterFormatException(sm.toString());
+                throw Region.Builder.unsupportedSampleModel(sm);
             }
         }
 
@@ -428,7 +427,7 @@ public class HyperRectangleWriter {
             if (isSupported(this, sm)) {
                 return create(sm, null);
             } else {
-                throw new RasterFormatException(sm.toString());
+                throw Region.Builder.unsupportedSampleModel(sm);
             }
         }
 
@@ -471,7 +470,7 @@ public class HyperRectangleWriter {
             if (sm instanceof ComponentSampleModel)         return 
create((ComponentSampleModel)         sm);
             if (sm instanceof SinglePixelPackedSampleModel) return 
create((SinglePixelPackedSampleModel) sm);
             if (sm instanceof MultiPixelPackedSampleModel)  return 
create((MultiPixelPackedSampleModel)  sm);
-            throw new RasterFormatException(sm.toString());
+            throw Region.Builder.unsupportedSampleModel(sm);
         }
 
         /**
@@ -568,8 +567,11 @@ public class HyperRectangleWriter {
         /**
          * Returns the byte order to use for writing data, or {@code null} if 
no change is needed.
          * A specific byte order is sometime needed when {@code short} or 
{@code int} values are
-         * used for packing many bytes. This order can be ignored if, for 
example, ARGB values
-         * are really stored as integers in native byte order rather than 4 
bytes.
+         * used for packing many bytes. This order can be ignored if, for 
example, <abbr>ARGB</abbr>
+         * values are really stored as integers in native byte order rather 
than 4 bytes.
+         *
+         * @param  current  the byte order used by the output file.
+         * @return the byte order to use for writing the array of numbers.
          */
         public ByteOrder byteOrder(ByteOrder current) {
             return requiresBigEndian && (current != ByteOrder.BIG_ENDIAN) ? 
ByteOrder.BIG_ENDIAN : null;
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
index 848b83ea5f..690b21a813 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Region.java
@@ -17,13 +17,21 @@
 package org.apache.sis.io.stream;
 
 import java.util.Arrays;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.RasterFormatException;
 import static java.lang.Math.addExact;
 import static java.lang.Math.subtractExact;
 import static java.lang.Math.multiplyExact;
 import static java.lang.Math.incrementExact;
 import static java.lang.Math.toIntExact;
-import org.apache.sis.io.TableAppender;
 import static org.apache.sis.pending.jdk.JDK18.ceilDiv;
+import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.io.TableAppender;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.Classes;
 
 
 /**
@@ -314,4 +322,165 @@ public final class Region {
         }
         return table.toString();
     }
+
+    /**
+     * Helper class for building a region.
+     * All arrays are initialized to zero.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     */
+    public static final class Builder {
+        /**
+         * The number of elements along each dimension.
+         */
+        public final long[] sourceSize;
+
+        /**
+         * Indices of the first value to read or write along each dimension.
+         * May be {@code null} if all lower coordinates are zero.
+         */
+        public final long[] regionLower;
+
+        /**
+         * Indices after the last value to read or write along each dimension.
+         * May be {@code null} if the upper coordinates are {@link 
#sourceSize}.
+         */
+        public final long[] regionUpper;
+
+        /**
+         * Subsampling along each dimension. Shall be greater than zero.
+         * May be {@code null} if all subsampling factor are 1 (no 
subsampling).
+         */
+        public final long[] subsampling;
+
+        /**
+         * Creates a builder for a region of the given number of dimensions.
+         * If {@code subRegion} is {@code false}, then only {@link 
#sourceSize} is non-null.
+         * All non-null arrays are initialized to 0.
+         *
+         * @param  dimension  number of dimensions.
+         * @param  subRegion  whether the caller may need to specify a 
sub-region.
+         */
+        public Builder(final int dimension, final boolean subRegion) {
+            sourceSize  = new long[dimension];
+            if (subRegion) {
+                regionLower = new long[dimension];
+                regionUpper = new long[dimension];
+                subsampling = new long[dimension];
+            } else {
+                regionLower = null;
+                regionUpper = null;
+                subsampling = null;
+            }
+        }
+
+        /**
+         * Index of the bank in the arrays updated by {@link 
#pixelsToSampleValues(SampleModel)}.
+         */
+        private static final int BANK_INDEX = 2;
+
+        /**
+         * Computes the subregion to read for filling a raster which uses the 
given sample model.
+         * Before to invoke this method, the caller shall set the {@link 
#regionLower}, {@link #regionUpper}
+         * and {@link #subsampling} arrays (if non-null) to the pixel 
coordinates of the subregion to read.
+         * Subclasses of {@link 
org.apache.sis.storage.tiling.TiledGridCoverageResource} can do as below:
+         *
+         * {@snippet lang="java" :
+         *     final TileIterator t = ...;
+         *     final var b = new Region.Builder(3);
+         *     t.getRegionInsideTile(b.regionLower, b.regionUpper, 
b.subsampling, true);
+         *     b.pixelsToSampleValues(sampleModel);
+         *     }
+         *
+         * This method converts the pixel coordinates to sample values 
coordinates,
+         * assuming that the sample values will be stored in the raster in the 
same order as they are read.
+         * This method also overwrites the {@link #sourceSize} with the 
following values, in order:
+         *
+         * <blockquote>[(width × number of samples per pixel), (height), 
(number of banks)]</blockquote>
+         *
+         * The arrays can have any length. If too long, extraneous dimensions 
will be ignored.
+         * If too short, there is no attempt to update the missing values.
+         *
+         * @param  sm  the sample model for which to get the size and update 
the subregion.
+         * @throws RasterFormatException if the sample model is not recognized.
+         */
+        @SuppressWarnings("fallthrough")
+        public void pixelsToSampleValues(final SampleModel sm) {
+            switch (sourceSize.length) {
+                default: sourceSize[BANK_INDEX] = 1;        // Fallthrough in 
all cases.
+                case 2:  sourceSize[1] = sm.getHeight();
+                case 1:  sourceSize[0] = sm.getWidth();
+                case 0:  break;
+            }
+            final long n = sm.getNumDataElements();
+            if (n != 1) {
+                final int dimension;
+                if (sm instanceof PixelInterleavedSampleModel) {
+                    dimension = 0;      // Tile width
+                    if (subsampling != null && subsampling[dimension] != 1) {
+                        throw new RasterFormatException("Subsampling of pixel 
interleave not yet supported.");
+                    }
+                } else if (sm instanceof BandedSampleModel) {
+                    dimension = BANK_INDEX;
+                } else {
+                    throw unsupportedSampleModel(sm);
+                }
+                if (dimension < sourceSize.length) {
+                    sourceSize[dimension] *= n;
+                    if (regionLower != null) regionLower[dimension] *= n;
+                    if (regionUpper != null) regionUpper[dimension] *= n;
+                }
+            }
+        }
+
+        /**
+         * Returns the exception to throw for an unsupported sample model.
+         *
+         * @param  sm  the unsupported sample model.
+         * @return the exception to throw.
+         */
+        static RasterFormatException unsupportedSampleModel(final SampleModel 
sm) {
+            return new RasterFormatException(Resources.format(
+                    Resources.Keys.UnsupportedSampleModel_1,
+                    
CharSequences.camelCaseToSentence(Classes.getShortClassName(sm))));
+        }
+
+        /**
+         * Sets the index of the <em>bank</em> (not band) to read.
+         * This is useful when each bank is read separately and stored in its 
own array.
+         *
+         * @param  bank  index of the bank to read.
+         */
+        public void setBankToRead(final int bank) {
+            regionLower[BANK_INDEX] = bank;
+            regionUpper[BANK_INDEX] = bank + 1;
+            subsampling[BANK_INDEX] = 1;
+        }
+
+        /**
+         * Returns {@code true} if the region would contain all data of an 
hyper-cube of the given size.
+         * This is true if {@link #regionLower} is equivalent to (0, 0, …, 0), 
if {@link #regionUpper}
+         * and {@link #sourceSize} are both equivalent to the given {@code 
size} argument,
+         * and there is no subsampling.
+         *
+         * @param  size  the expected size of data.
+         * @return whether the region would cover all data of the given size.
+         */
+        public boolean containsAll(final long[] size) {
+            return Arrays.equals(sourceSize,  size) &&
+                   (regionUpper == null || Arrays.equals(regionUpper, 
sourceSize)) &&
+                   (regionLower == null || ArraysExt.allEquals(regionLower, 
0)) &&
+                   (subsampling == null || ArraysExt.allEquals(subsampling, 
1));
+        }
+
+        /**
+         * Creates a new region with the fields of this builder as arguments.
+         *
+         * @return the region created from the values of the builder.
+         * @throws ArithmeticException if a size is too large.
+         */
+        public Region build() {
+            return new Region(sourceSize, regionLower, regionUpper, 
subsampling);
+        }
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
index 7f2a153b78..d1c74eaa07 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
@@ -502,6 +502,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short UnknownFormatFor_1 = 14;
 
+        /**
+         * Interleave “{0}” is not supported.
+         */
+        public static final short UnsupportedSampleModel_1 = 89;
+
         /**
          * Using {0} JDBC driver version {1}.{2}.
          */
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
index 19613a17b4..0f69cd797a 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
@@ -105,5 +105,6 @@ UnexpectedNumberOfCoordinates_4   = The \u201c{0}\u201d 
feature at {1} has a {3}
 UnexpectedSliceResolution_3       = Expected a resolution of {0} but got {1} 
for the slice or tile \u201c{2}\u201d.
 UnfilteredData                    = Unfiltered data.
 UnknownFormatFor_1                = Format of \u201c{0}\u201d is not 
recognized.
+UnsupportedSampleModel_1          = Interleave \u201c{0}\u201d is not 
supported.
 UsedOnlyIfNotEncoded              = Used only if this information is not 
encoded with the data.
 UseJdbcDriverVersion_3            = Using {0} JDBC driver version {1}.{2}.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
index ad8399b738..dd0e6dd1ab 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
@@ -110,5 +110,6 @@ UnexpectedNumberOfCoordinates_4   = L\u2019entit\u00e9 
nomm\u00e9e \u00ab\u202f{
 UnexpectedSliceResolution_3       = Une r\u00e9solution de {0} \u00e9tait 
attendue mais une r\u00e9solution de {1} a \u00e9t\u00e9 trouv\u00e9e dans la 
tranche ou tuile \u00ab\u202f{2}\u202f\u00bb.
 UnfilteredData                    = Donn\u00e9es non-filtr\u00e9es.
 UnknownFormatFor_1                = Le format de \u00ab\u202f{0}\u202f\u00bb 
n\u2019est pas reconnu.
+UnsupportedSampleModel_1          = L\u2019entrelacement \u201c{0}\u201d 
n\u2019est pas support\u00e9.
 UsedOnlyIfNotEncoded              = Utilis\u00e9 seulement si cette 
information n\u2019est pas encod\u00e9e avec les donn\u00e9es.
 UseJdbcDriverVersion_3            = Utilise le pilote JDBC 
\u00ab\u202f{0}\u202f\u00bb version {1}.{2}.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
index 3421c2a561..3f0347ec64 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
@@ -37,6 +37,7 @@ import static java.lang.Math.decrementExact;
 import static java.lang.Math.toIntExact;
 import static java.lang.Math.floorDiv;
 import org.opengis.util.GenericName;
+import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.DisjointExtentException;
@@ -557,6 +558,10 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
      *
      * @param  sliceExtent  a subspace of this grid coverage, or {@code null} 
for the whole image.
      * @return the grid slice as a rendered image. Image location is relative 
to {@code sliceExtent}.
+     * @throws MismatchedDimensionException if the given extent does not have 
the same number of dimensions as this coverage.
+     * @throws SubspaceNotSpecifiedException if the given argument is not 
sufficient for reducing the grid to a two-dimensional slice.
+     * @throws DisjointExtentException if the given extent does not intersect 
this grid coverage.
+     * @throws CannotEvaluateException if this method cannot produce the 
rendered image for another reason.
      */
     @Override
     public RenderedImage render(GridExtent sliceExtent) {
@@ -871,11 +876,11 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
          * This policy is based on the fact that this method is typically 
invoked before a tile is read.</p>
          *
          * @return a newly created, initially empty raster.
+         *
+         * @see #getTileLocation()
          */
         public WritableRaster createRaster() {
-            final int x = getTileOrigin(xDimension);
-            final int y = getTileOrigin(yDimension);
-            final WritableRaster tile = 
Raster.createWritableRaster(getCoverage().model, new Point(x, y));
+            final WritableRaster tile = 
Raster.createWritableRaster(getCoverage().model, getTileLocation());
             if (fireTileReadEvent) {
                 fireTileReadEvent(tile.getBounds());
             }
@@ -907,6 +912,19 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
             return tile;
         }
 
+        /**
+         * Creates the coordinates of the upper-left corner of the tile at the 
current <abbr>AOI</abbr> position.
+         * This is the ({@code minX}, {@code minY}) values of the tile created 
by {@link #createRaster()}.
+         *
+         * @return coordinates of the upper-left pixel of the tile at the 
current iterator position.
+         *
+         * @see #createRaster()
+         * @see #getTileCoordinatesInResource()
+         */
+        public Point getTileLocation() {
+            return new Point(getTileOrigin(xDimension), 
getTileOrigin(yDimension));
+        }
+
         /**
          * Returns the location (relative to the cropped tile) of the 
upper-left corner of the uncropped tile.
          * This value is present under the following conditions:
@@ -966,17 +984,21 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
          *         Rectangle sourceArea = iterator.imageToResource(validArea, 
false);
          *
          *         // Conversion from uncropped coordinates to cropped 
coordinates.
-         *         getUncroppedTileLocation().ifPresent((p) -> 
sourceArea.translate(p.x, p.y));
+         *         iterator.getUncroppedTileLocation().ifPresent((p) -> 
sourceArea.translate(p.x, p.y));
          *     }
          *     }
          *
          * @param  subsampled  whether to return coordinates with subsampling 
applied.
          * @return pixel to read inside the tile, or {@code null} if the 
region is empty.
          * @throws ArithmeticException if the tile coordinates overflow 
32-bits integer capacity.
+         *
+         * @see #getUncroppedTileLocation()
+         * @see TileIterator#imageToResource(Rectangle, boolean)
          */
         public Rectangle getRegionInsideTile(final boolean subsampled) {
-            final long[] lower = new long[BIDIMENSIONAL];
-            final long[] upper = new long[BIDIMENSIONAL];
+            final int dimension = Math.max(xDimension, yDimension) + 1;
+            final long[] lower = new long[dimension];
+            final long[] upper = new long[dimension];
             if (getRegionInsideTile(lower, upper, null, subsampled)) {
                 return new Rectangle(
                         toIntExact(lower[xDimension]),
@@ -1000,21 +1022,25 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
          * The same values can be obtained by {@link #getSubsampling(int)}.</p>
          *
          * <p>This method is a generalization of {@link 
#getRegionInsideTile(boolean)} to any number of dimensions.
-         * See that method for a discussion about the {@code subsampled} 
argument and a code snippet.</p>
+         * See that method for a discussion about the {@code subsampled} 
argument and a code snippet.
+         * The given arrays can be of any length, but that length must be the 
same for all arguments.
+         * If the given arrays are longer than necessary, the result is 
truncated to the arrays length.
+         * If the given arrays are longer than necessary, the extraneous array 
elements are unchanged.</p>
          *
          * @param  lower        a pre-allocated array where to store relative 
coordinates of the first pixel.
          * @param  upper        a pre-allocated array where to store relative 
coordinates after the last pixel.
          * @param  subsampling  a pre-allocated array where to store 
subsampling, or {@code null} if not needed.
          * @param  subsampled   whether to return coordinates with subsampling 
applied.
          * @return {@code true} on success, or {@code false} if the tile is 
empty.
+         * @throws ArrayIndexOutOfBoundsException if the given arrays are not 
of the same length.
          */
         public boolean getRegionInsideTile(final long[] lower,
                                            final long[] upper,
                                            final long[] subsampling,
                                            final boolean subsampled)
         {
-            int dimension = Math.min(lower.length, upper.length);
             final TiledGridCoverage coverage = getCoverage();
+            int dimension = Math.min(coverage.readExtent.getDimension(), 
Math.max(lower.length, upper.length));
             if (subsampling != null) {
                 System.arraycopy(coverage.subsampling, 0, subsampling, 0, 
dimension);
             }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
index a68f265556..beb326bd17 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
@@ -67,7 +67,7 @@ import java.lang.reflect.Array;
  * objects.
  *
  * @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.7
  *
  * @see Arrays
  *
@@ -2230,6 +2230,24 @@ public final class ArraysExt {
         return true;
     }
 
+    /**
+     * Returns {@code true} if all values in the specified array are equal to 
the specified value.
+     *
+     * @param  array  the array to check.
+     * @param  value  the expected value.
+     * @return {@code true} if all elements in the given array are equal to 
the given value.
+     *
+     * @since 1.7
+     */
+    public static boolean allEquals(final long[] array, final long value) {
+        for (int i=0; i<array.length; i++) {
+            if (array[i] != value) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns {@code true} if all values in the specified array are equal to 
the specified value.
      *
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 cea5719bb0..bf91a4b2d5 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
@@ -127,7 +127,7 @@ abstract class Image {
          *
          * @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.
-         * @throws Exception any I/O error, arithmetic error or other kinds of 
error.
+         * @throws Exception any I/O error, arithmetic error, raster format 
error or other kinds of error.
          */
         Raster readTile(ChannelDataInput input) throws Exception;
     }
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 76f9879804..9d7d56d382 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
@@ -30,6 +30,7 @@ import java.awt.image.ColorModel;
 import java.awt.image.Raster;
 import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
+import java.awt.image.RasterFormatException;
 import javax.imageio.ImageReader;
 import javax.imageio.spi.ImageReaderSpi;
 import org.opengis.metadata.Metadata;
@@ -39,6 +40,7 @@ import 
org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.image.internal.shared.TilePlaceholder;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
@@ -46,6 +48,7 @@ import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.storage.tiling.TiledGridCoverage;
 import org.apache.sis.storage.tiling.TiledGridCoverageResource;
 import org.apache.sis.storage.isobmff.ByteRanges;
+import org.apache.sis.io.stream.Region;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.inflater.ComputedByteChannel;
 import org.apache.sis.util.internal.shared.Numerics;
@@ -317,6 +320,7 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
          *
          * @param  iterator  an iterator over the tiles that intersect the 
Area Of Interest specified by user.
          * @return tiles decoded from the enclosing resource.
+         * @throws Exception any I/O error, arithmetic error, raster format 
error or other kinds of error.
          */
         @Override
         protected Raster[] readTiles(final TileIterator iterator) throws 
Exception {
@@ -363,6 +367,9 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
          * the image reader to use in the case of read operations delegated to 
Image I/O.
          * An instance of this class exist for each tile and is discarded 
after the tile
          * has been read.
+         *
+         * <p><b>Note:</b> this is defined as an inner class of {@link 
TiledGridCoverage} subclass
+         * because, at the time of writing this class, {@link AOI} is a 
protected class.</p>
          */
         static final class ReadContext extends ByteRanges {
             /**
@@ -399,6 +406,8 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
              * @param  inflater  an initially empty reference to an inflater 
to recycle.
              * @param  owner     the resource for which to read a tile.
              * @throws DataStoreException if an error occurred while computing 
the range of bytes.
+             * @throws ArithmeticException if an overflow occurred during 
integer arithmetic.
+             * @throws RasterFormatException if the sample model is not 
recognized.
              */
             @SuppressWarnings("LeakingThisInConstructor")
             private ReadContext(final AOI iterator,
@@ -424,6 +433,7 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
              *
              * @param  input   the input from which to read bytes.
              * @param  result  where to store the result.
+             * @throws Exception any I/O error, arithmetic error, raster 
format error or other kinds of error.
              */
             final void readTile(ChannelDataInput input, final Raster[] result) 
throws Exception {
                 input = viewAsConsecutiveBytes(input);
@@ -432,6 +442,25 @@ final class ImageResource extends 
TiledGridCoverageResource implements StoreReso
                 result[iterator.getTileIndexInResultArray()] = raster;
             }
 
+            /**
+             * Gets offsets of the <em>sample values</em> to read, relative to 
the beginning of the tiles.
+             * If that region is empty, returns an empty raster that the 
caller should return immediately.
+             *
+             * @param  builder  the builder to update.
+             * @return an empty raster if the region to read is empty, or 
{@code null} otherwise.
+             * @throws RasterFormatException if the given sample model is 
unsupported.
+             * @throws ArithmeticException if an overflow occurred during 
integer arithmetic.
+             */
+            final Raster getRegionInsideTile(final Region.Builder builder,
+                                             final SampleModel sampleModel)
+            {
+                if (iterator.getRegionInsideTile(builder.regionLower, 
builder.regionUpper, builder.subsampling, true)) {
+                    builder.pixelsToSampleValues(sampleModel);
+                    return null;
+                }
+                return 
TilePlaceholder.empty(sampleModel).create(iterator.getTileLocation());
+            }
+
             /**
              * Returns the destination raster. This method shall be invoked at 
most once per tile.
              * It may be invoked not at all if the read operation is delegated 
to Image I/O,
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 070f6161a5..d1d77f30c5 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,13 +16,13 @@
  */
 package org.apache.sis.storage.geoheif;
 
-import java.util.Arrays;
 import static java.lang.Math.addExact;
 import static java.lang.Math.multiplyExact;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
+import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
-import java.awt.image.BandedSampleModel;
+import java.awt.image.RasterFormatException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.image.internal.shared.ImageUtilities;
 import org.apache.sis.image.internal.shared.RasterFactory;
@@ -118,26 +118,6 @@ class UncompressedImage extends Image {
         }
     }
 
-    /**
-     * Returns [(width × number of samples per pixel), (height), (number of 
banks)], in that order.
-     * The banks are assumed to be consecutive.
-     *
-     * <p><b>Limitation:</b> current implementation ignores {@link 
java.awt.image.MultiPixelPackedSampleModel},
-     * but that model does not seem to be used by <abbr>HEIF</abbr>.</p>
-     *
-     * @param  sampleModel  the sample model for which to get the size.
-     * @return the scanline stride, raster height and number of banks, in that 
order.
-     */
-    private static long[] size(final SampleModel sampleModel) {
-        long width    = sampleModel.getWidth();
-        long numBanks = sampleModel.getNumDataElements();
-        if (!(sampleModel instanceof BandedSampleModel)) {
-            width *= numBanks;
-            numBanks = 1;
-        }
-        return new long[] {width, sampleModel.getHeight(), numBanks};
-    }
-
     /**
      * Computes the range of bytes that will be needed for reading the tile at 
the specified index.
      * The given region is converted to offsets relatives to the beginning of 
the <abbr>HEIF</abbr>
@@ -179,15 +159,27 @@ class UncompressedImage extends Image {
      * @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.
-     * @throws ArithmeticException if an offset or index overflows the 
capacity of 32-bits integers.
+     * @throws ArithmeticException if an overflow occurred during integer 
arithmetic.
+     * @throws RasterFormatException if the sample model is unsupported.
      */
     @Override
     protected final Reader computeByteRanges(final 
ImageResource.Coverage.ReadContext context) throws DataStoreException {
-        final long[] sourceSize = size(sampleModel);
-        final var    region     = new Region(sourceSize, null, null, null);
-        final long   tileIndex  = addExact(multiplyExact(context.subTileY, 
numXTiles), context.subTileX);
+        final long[] sourceSize;
+        final Region region;
+        {   // For local scope.
+            final var builder = new Region.Builder(3, false);
+            builder.pixelsToSampleValues(sampleModel);
+            sourceSize = builder.sourceSize;
+            region = builder.build();
+        }
+        final long tileIndex = addExact(multiplyExact(context.subTileY, 
numXTiles), context.subTileX);
         computeByteRanges(tileIndex, region, context);
         return (ChannelDataInput input) -> {
+            final var builder = new Region.Builder(3, true);
+            final Raster empty = context.getRegionInsideTile(builder, 
sampleModel);
+            if (empty != null) {
+                return empty;
+            }
             long origin = context.offset();
             ComputedByteChannel inflater = context.reuseInflater();
             if (inflater == null || inflater.compressedInput() != input) {
@@ -210,15 +202,12 @@ class UncompressedImage extends Image {
             hr.setOrigin(origin);
             Region bank = region;
             final WritableRaster raster   = context.createRaster();
-            final long[]         upper    = size(raster.getSampleModel());
             final DataBuffer     target   = raster.getDataBuffer();
             final int            numBanks = target.getNumBanks();
             for (int b=0; b<numBanks; b++) {
-                upper[2] = b + 1;
-                if (b != 0 || !Arrays.equals(sourceSize, upper)) {
-                    final long[] lower = new long[upper.length];
-                    lower[2] = b;
-                    bank = new Region(sourceSize, lower, upper, null);
+                builder.setBankToRead(b);
+                if (b != 0 || !builder.containsAll(sourceSize)) {
+                    bank = builder.build();
                 }
                 hr.setDestination(RasterFactory.wrapAsBuffer(target, b));
                 hr.readAsBuffer(bank, 0);


Reply via email to