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

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

commit 83e5974f24c5d5de365994526ca97fc126d34cea
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jun 12 17:58:51 2026 +0200

    Refactor the GeoHEIF reader with more separation of concern.
    The code that creates color and sample models moves to a new class.
    The code for uncompressing tiles with ZLIB uses the code shared with 
GeoTIFF.
---
 .../sis/storage/geoheif/CoverageBuilder.java       | 309 ++++-----------------
 .../apache/sis/storage/geoheif/FromImageIO.java    |  66 +++--
 .../main/org/apache/sis/storage/geoheif/Image.java |  22 +-
 .../org/apache/sis/storage/geoheif/ImageModel.java | 289 +++++++++++++++++++
 .../apache/sis/storage/geoheif/ImageResource.java  |  25 +-
 .../sis/storage/geoheif/ResourceBuilder.java       |  41 ++-
 .../sis/storage/geoheif/UncompressedImage.java     |  24 +-
 .../apache/sis/storage/isobmff/mpeg/Component.java |  16 +-
 8 files changed, 445 insertions(+), 347 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java
index c16745d0e4..4f2c8033e6 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java
@@ -28,19 +28,10 @@ import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.io.IOException;
 import java.nio.ByteOrder;
-import java.awt.Dimension;
-import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
-import java.awt.image.RasterFormatException;
-import javax.imageio.ImageTypeSpecifier;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.MathTransform;
-import org.apache.sis.image.DataType;
-import org.apache.sis.image.internal.shared.ColorModelBuilder;
-import org.apache.sis.image.internal.shared.ColorModelFactory;
-import org.apache.sis.image.internal.shared.SampleModelBuilder;
-import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.PixelInCell;
@@ -54,7 +45,6 @@ import org.apache.sis.storage.isobmff.base.ItemInfoEntry;
 import org.apache.sis.storage.isobmff.base.ItemProperties;
 import org.apache.sis.storage.isobmff.geo.ModelTransformation;
 import org.apache.sis.storage.isobmff.geo.ModelCRS;
-import org.apache.sis.storage.isobmff.mpeg.Component;
 import org.apache.sis.storage.isobmff.mpeg.ComponentType;
 import org.apache.sis.storage.isobmff.mpeg.ComponentPalette;
 import org.apache.sis.storage.isobmff.mpeg.ComponentDefinition;
@@ -62,13 +52,10 @@ import 
org.apache.sis.storage.isobmff.mpeg.CompressedUnitsItemInfo;
 import org.apache.sis.storage.isobmff.mpeg.CompressionConfiguration;
 import org.apache.sis.storage.isobmff.mpeg.UncompressedFrameConfig;
 import org.apache.sis.storage.isobmff.mpeg.UnitType;
-import org.apache.sis.storage.isobmff.mpeg.InterleavingMode;
 import org.apache.sis.storage.isobmff.image.ImageSpatialExtents;
 import org.apache.sis.storage.isobmff.image.PixelInformation;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Emptiable;
-import org.apache.sis.util.collection.Containers;
-import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.pending.jdk.JDK18;
 
@@ -90,7 +77,7 @@ final class CoverageBuilder implements Emptiable {
      * An index of the image to build, for information purpose only.
      * This is given to {@link CoverageModifier.Source} constructor.
      */
-    private final int imageIndex;
+    final int imageIndex;
 
     /**
      * Name or identifier of the resource. This is taken from {@link 
ItemInfoEntry#itemName}
@@ -105,7 +92,7 @@ final class CoverageBuilder implements Emptiable {
     private String name;
 
     /**
-     * The size of the reconstructed image in pixels.
+     * The size (in pixels) of the reconstructed image.
      */
     private int width, height;
 
@@ -169,35 +156,12 @@ final class CoverageBuilder implements Emptiable {
     private final Map<Object, Boolean> unknownBoxes;
 
     /**
-     * The type of sample values stored in the raster.
-     * Determined as a side effect of {@link #sampleDimensions()}.
+     * The color model, sample model and sample dimensions.
+     * This is created when first needed. The same instance may be shared by 
many tiles.
      *
-     * @see #dataType()
+     * @see #imageModel()
      */
-    private DataType dataType;
-
-    /**
-     * The color model, created as a side effect of {@link 
#sampleDimensions()}.
-     * May stay null if this builder does not have enough information.
-     *
-     * @see #colorModel()
-     */
-    private ColorModel colorModel;
-
-    /**
-     * The sample model, created as a side effect of {@link 
#sampleDimensions()}.
-     * May stay null if this builder does not have enough information.
-     *
-     * @see #sampleModel()
-     */
-    private SampleModel sampleModel;
-
-    /**
-     * The sample dimensions, created when first requested.
-     *
-     * @see #sampleDimensions()
-     */
-    private SampleDimension[] sampleDimensions;
+    private ImageModel imageModel;
 
     /**
      * The builder of metadata, created when first needed.
@@ -208,6 +172,8 @@ final class CoverageBuilder implements Emptiable {
 
     /**
      * The builder used for building tiles, or {@code null} if none.
+     *
+     * @see #setTileBuilder(CoverageBuilder)
      */
     private CoverageBuilder tileBuilder;
 
@@ -219,7 +185,11 @@ final class CoverageBuilder implements Emptiable {
      * @param  properties       source of coverage properties for this 
coverage item.
      * @param  duplicatedBoxes  names of boxes that were duplicated. Used for 
logging a warning only once per type of box.
      */
-    CoverageBuilder(final GeoHeifStore store, final int imageIndex, final 
ItemProperties.ForID properties, final Set<String> duplicatedBoxes) {
+    CoverageBuilder(final GeoHeifStore store,
+                    final int imageIndex,
+                    final ItemProperties.ForID properties,
+                    final Set<String> duplicatedBoxes)
+    {
         this.store = store;
         this.imageIndex = imageIndex;
         unknownBoxes = new LinkedHashMap<>();
@@ -415,10 +385,10 @@ final class CoverageBuilder implements Emptiable {
      * @param  name   name of the resource.
      * @param  image  the single tile of the image.
      * @return the resource.
-     * @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.
+     * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" 
transform or the sample dimensions cannot be created.
+     * @throws DataStoreException if the construction failed for another 
reason.
      */
-    final ImageResource build(final String name, final Image.Supplier image) 
throws DataStoreException {
+    final ImageResource build(final String name, final Image image) throws 
DataStoreException {
         this.name = name;
         return new ImageResource(this, null, image);
     }
@@ -430,8 +400,8 @@ final class CoverageBuilder implements Emptiable {
      * @param  name   name of the resource.
      * @param  tiles  all tiles of the image.
      * @return the resource.
-     * @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.
+     * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" 
transform or the sample dimensions cannot be created.
+     * @throws DataStoreException if the construction failed for another 
reason.
      */
     final ImageResource build(final String name, final List<Image> tiles) 
throws DataStoreException {
         this.name = name;
@@ -473,17 +443,22 @@ final class CoverageBuilder implements Emptiable {
      *
      * @param  dimension  the dimension: 0 or 1.
      * @return number of tiles in the requested dimension.
+     * @throws DataStoreContentException if the sample model cannot be created.
+     * @throws DataStoreException if the reading failed for another reason.
      */
-    public final int numTiles(final int dimension) {
+    public final int numTiles(final int dimension) throws DataStoreException {
         if (model != null) {
             switch (dimension) {
                 case 0: return model.numTileCols;
                 case 1: return model.numTileRows;
             }
-        } else if ((width | height) != 0 && sampleModel != null) {
-            switch (dimension) {
-                case 0: return JDK18.ceilDiv(width,  sampleModel.getWidth());
-                case 1: return JDK18.ceilDiv(height, sampleModel.getHeight());
+        } else if (!isEmpty()) {
+            final SampleModel sampleModel = imageModel().sampleModel;
+            if (sampleModel != null) {
+                switch (dimension) {
+                    case 0: return JDK18.ceilDiv(width,  
sampleModel.getWidth());
+                    case 1: return JDK18.ceilDiv(height, 
sampleModel.getHeight());
+                }
             }
         }
         return 1;
@@ -498,8 +473,9 @@ final class CoverageBuilder implements Emptiable {
 
     /**
      * Fetches the color model and sample model from a sample tile (usually 
the first).
-     * This case happens when each tile is a JPEG image. In such case, the 
sample model
-     * is not declared in {@code UncompressedFrameConfig} and we have to get 
it from a tile.
+     * This case happens when each tile is a <abbr>JPEG</abbr> image.
+     * In such case, the sample model is not declared in {@code 
UncompressedFrameConfig}
+     * and we have to get it from a tile.
      *
      * <p>If this sample model has already been initialized, then this method 
does nothing.
      * This is desirable because this method is invoked in a loop for every 
tiles, while
@@ -510,222 +486,45 @@ final class CoverageBuilder implements Emptiable {
      * @throws IOException if an I/O error occurred.
      */
     final void setImageLayout(final Image image) throws DataStoreException, 
IOException {
-        if (sampleModel == null) {
-            ImageTypeSpecifier type = image.getImageType(store);
-            colorModel  = type.getColorModel();
-            sampleModel = type.getSampleModel();
-            dataType    = 
DataType.forDataBufferType(sampleModel.getDataType());
+        if (imageModel == null) {
+            imageModel = new ImageModel(image.getImageType(store));
         }
     }
 
     /**
-     * Returns the type of sample values stored in the raster.
-     * One of {@link #sampleModel()} or {@link #sampleDimensions()}
-     * methods must have been invoked before this method.
-     */
-    public final DataType dataType() {
-        return dataType;
-    }
-
-    /**
-     * Returns the color model, or {@code null} if unknown.
-     * The {@link #sampleDimensions()} method must have been invoked before 
this method.
-     */
-    public final ColorModel colorModel() {
-        return colorModel;
-    }
-
-    /**
-     * Returns the sample model with a size equals to the tile size.
+     * Returns color model, sample model and sample dimensions computed from 
<abbr>HEIF</abbr> boxes.
      *
-     * @return the sample model (never {@code null}).
-     * @throws RasterFormatException if the sample dimensions or sample model 
cannot be created.
-     * @throws DataStoreException if an error occurred in {@link 
CoverageModifier}.
-     */
-    public final SampleModel sampleModel() throws DataStoreException {
-        if (sampleModel == null) {
-            sampleDimensions(false);
-            if (sampleModel == null) {
-                throw new RasterFormatException("Unspecified sample model.");
+     * @throws DataStoreContentException if the sample model or sample 
dimensions cannot be created.
+     * @throws DataStoreException if the reading failed for another reason.
+     */
+    final ImageModel imageModel() throws DataStoreException {
+        if (imageModel == null) {
+            imageModel = new ImageModel(width, height, model, componentTypes, 
bitsPerChannel, palette, this);
+            if (imageModel.sampleModel == null && tileBuilder != null) {
+                imageModel = tileBuilder.imageModel();
             }
         }
-        return sampleModel;
+        return imageModel;
     }
 
     /**
-     * Implementation of {@link #sampleDimensions()} and {@link 
#sampleModel()}.
-     *
-     * @todo Need to add information from the {@code CellPropertyTypeProperty} 
box.
-     *       These information include sample dimension name and unit of 
measurement.
-     *
-     * @param  full  {@code true} for creating all objects, or {@code false} 
for creating only the sample model.
-     * @throws RasterFormatException if the sample dimensions or sample model 
cannot be created.
-     * @throws DataStoreException if an error occurred in {@link 
CoverageModifier}.
-     */
-    private void sampleDimensions(final boolean full) throws 
DataStoreException {
-        final int nc = (model          == null) ? 0 : model.components.length;
-        final int nt = (componentTypes == null) ? 0 : componentTypes.length;
-        final int ns = (bitsPerChannel == null) ? 0 : bitsPerChannel.length;
-        final int numBands = Math.max(Math.max(nc, nt), ns);
-        if (numBands == 0) {
-            if (tileBuilder != null) {
-                if (tileBuilder.sampleDimensions == null) {
-                    tileBuilder.sampleDimensions(full);
-                }
-                sampleDimensions = tileBuilder.sampleDimensions;
-                sampleModel      = tileBuilder.sampleModel;
-                colorModel       = tileBuilder.colorModel;
-                dataType         = tileBuilder.dataType;
-            }
-            return;
-        }
-        final var bitsPerSample = new int[numBands];
-        final var sd = full ? new SampleDimension[numBands] : null;
-        final var sb = full ? new SampleDimension.Builder() : null;
-        int numBits=0, redMask=0, greenMask=0, blueMask=0, alphaMask=0;
-        int grayBand = -1, indexBand = -1, alphaBand = -1;    // Negative 
value means none.
-        for (int band=0; band<numBands; band++) {
-            /*
-             * `colorType` can be an enumeration value such as `RED`, `GREEN`, 
`BLUE`,
-             * or an URI, or an Integer value. The same value can appear in 
two boxes.
-             * `bitDepth` can also appear in two boxes. The ISO 23001-17:2024 
standard
-             * said that when the component type information appears in the 
two boxes,
-             * `ComponentDefinitionBox` shall precede 
`UncompressedFrameConfigBox`.
-             */
-            var colorType = (band < nt) ? componentTypes[band] : null;  // 
From `ComponentDefinition`.
-            int bitDepth  = (band < ns) ? 
Byte.toUnsignedInt(bitsPerChannel[band]) : 0;
-            if (band < nc) {
-                final Component c = model.components[band];
-                if (colorType == null) colorType = c.type;              // 
From `UncompressedFrameConfig`
-                if (dataType == null) {
-                    dataType = c.getDataType();
-                } else if (dataType != c.getDataType()) {
-                    throw new RasterFormatException("All bands shall be of the 
same data type.");
-                }
-                bitDepth = Short.toUnsignedInt(c.bitDepth);
-                /*
-                 * Example: 10-bit unaligned RGB components followed by a 
1-byte aligned 7-bit alpha component.
-                 * Stored as 30 consecutive bits containing R, G and B, 
followed by 2 pre-alignment padding bits
-                 * for byte alignment, followed by one alignment padding bit 
then followed by the 7-bit alpha value.
-                 */
-                final int alignSize = Byte.toUnsignedInt(c.alignSize) * 
Byte.SIZE;
-                if (alignSize != 0) {
-                    numBits = Numerics.snapToCeil(numBits,  alignSize)
-                            + Numerics.snapToCeil(bitDepth, alignSize) - 
bitDepth;
-                }
-            }
-            /*
-             * Create the sample dimension and derive metadata from it.
-             * TODO: parse CellPropertyTypeProperty and 
CellPropertyCategoriesProperty boxes.
-             */
-            if (full) {
-                if (colorType != null) {
-                    sb.setName(colorType.toString());
-                } else {
-                    sb.setName(band);
-                }
-                var source = new CoverageModifier.BandSource(store, 
imageIndex, band, numBands, dataType);
-                metadata().addNewBand(sd[band] = 
store.customizer.customize(source, sb));
-                sb.clear();
-            }
-            if (bitDepth == 0) {
-                bitDepth = Component.DEFAULT_BIT_DEPTH;
-            } else if (full) {
-                metadata().setBitPerSample(bitDepth);
-            }
-            /*
-             * Identify the bands that we can map to RGBA.
-             * Will be used for building the color model.
-             */
-            bitsPerSample[band] = bitDepth;
-            numBits += bitDepth;
-            if (full && colorType instanceof ComponentType ct) {
-                int mask = 0;
-                if (numBits < Integer.SIZE) {
-                    mask = ((1 << bitDepth) - 1) << (Integer.SIZE - numBits);
-                }
-                switch (ct) {
-                    case RED:        redMask   |= mask; break;
-                    case GREEN:      greenMask |= mask; break;
-                    case BLUE:       blueMask  |= mask; break;
-                    case ALPHA:      alphaMask |= mask; alphaBand = band; 
break;
-                    case MONOCHROME: grayBand   = band; break;
-                    case PALETTE:    indexBand  = band; break;
-                }
-            }
-        }
-        final boolean isRGB = (redMask | greenMask | blueMask) != 0;
-        if (isRGB && numBits < Integer.SIZE) {
-            final int n = Integer.SIZE - numBits;   // Number of unused bits.
-            redMask   >>>= n;
-            greenMask >>>= n;
-            blueMask  >>>= n;
-            alphaMask >>>= n;
-        }
-        /*
-         * Build a sample model. The `InterleavingMode.COMPONENT` default 
value is arbitrary,
-         * as the `UncompressedFrameConfig` box is mandatory according ISO/IEC 
23001-17:2024.
-         */
-        if (sampleModel == null && dataType != null) {
-            InterleavingMode interleaveType = InterleavingMode.COMPONENT;
-            final var tileSize = new Dimension(width, height);
-            if (model != null) {
-                tileSize.width  /= model.numTileCols;
-                tileSize.height /= model.numTileRows;
-                interleaveType = model.interleaveType;
-            }
-            final boolean isBanded;
-            switch (interleaveType) {
-                case COMPONENT: isBanded = true;  break;    // Java2D: 
BandedSampleModel
-                case PIXEL:     isBanded = false; break;    // Java2D: 
PixelInterleavedSampleModel
-                default: throw new RasterFormatException("Unsupported 
interleave type: " + interleaveType);
-            }
-            sampleModel = new SampleModelBuilder(dataType, tileSize, 
bitsPerSample, isBanded).build();
-        }
-        /*
-         * Build a RGB(A) or indexed color model compatible with the sample 
model.
-         * The gray scale is used as a fallback for all unrecognized color 
models.
-         */
-        if (full) {
-            if (indexBand >= 0 && palette != null) {
-                colorModel = palette.toARGB(dataType, 
bitsPerSample[indexBand], numBands, indexBand);   // May be null.
-            }
-            if (colorModel == null && sampleModel != null) {
-                if (grayBand < 0 && (grayBand = indexBand) < 0) {
-                    grayBand = ColorModelFactory.DEFAULT_VISIBLE_BAND;
-                }
-                final var cb = new ColorModelBuilder(isRGB)
-                                .dataType(dataType)
-                                .bitsPerSample(bitsPerSample)
-                                .alphaBand(alphaBand)
-                                .visibleBand(grayBand, 
sd[grayBand].getSampleRange().orElse(null));
-                if (isRGB) {
-                    cb.componentMasks(redMask, greenMask, blueMask, alphaMask);
-                    // TODO: use another color space if not RGB.
-                }
-                colorModel = cb.createRGB(sampleModel);
-            }
-        }
-        sampleDimensions = sd;
-    }
-
-    /**
-     * Returns the sample dimensions for the bands and prepares metadata 
related to them.
+     * Returns the sample model with a size equals to the tile size.
      *
-     * @return the sample dimensions for all bands.
-     * @throws RasterFormatException if the sample dimensions or sample model 
cannot be created.
-     * @throws DataStoreException if an error occurred in {@link 
CoverageModifier}.
+     * @return the sample model (never {@code null}).
+     * @throws DataStoreContentException if the sample model or sample 
dimensions cannot be created.
+     * @throws DataStoreException if the reading failed for another reason.
      */
-    public final List<SampleDimension> sampleDimensions() throws 
DataStoreException {
-        if (sampleDimensions == null) {
-            sampleDimensions(true);
+    public final SampleModel sampleModel() throws DataStoreException {
+        final SampleModel sampleModel = imageModel().sampleModel;
+        if (sampleModel != null) {
+            return sampleModel;
         }
-        return Containers.viewAsUnmodifiableList(sampleDimensions);
+        throw new DataStoreContentException("Unspecified sample model.");
     }
 
     /**
      * Creates the grid geometry and opportunistically prepares metadata 
related to it.
-     * This method should be invoked exactly once, preferably after {@link 
#sampleDimensions()}.
+     * This method should be invoked at most once.
      * It may be invoked not at all when this object is used for building a 
tile instead of an image.
      *
      * @todo Need to add information from the {@code ExtraDimensionProperty} 
box.
@@ -749,7 +548,7 @@ final class CoverageBuilder implements Emptiable {
         if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) {
             metadata.addExtent(gridGeometry.getEnvelope(), store.listeners());
         }
-        var source = new CoverageModifier.Source(store, imageIndex, dataType);
+        var source = new CoverageModifier.Source(store, imageIndex, 
imageModel().dataType);
         return store.customizer.customize(source, gridGeometry);
     }
 }
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 b708a0a402..3b59410b62 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
@@ -16,12 +16,13 @@
  */
 package org.apache.sis.storage.geoheif;
 
+import java.util.Iterator;
 import java.io.IOException;
 import java.awt.image.SampleModel;
 import java.awt.image.BufferedImage;
-import java.awt.image.RasterFormatException;
 import javax.imageio.ImageReader;
 import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.event.IIOReadWarningListener;
 import javax.imageio.spi.IIORegistry;
 import javax.imageio.spi.ImageReaderSpi;
 import org.apache.sis.io.stream.ChannelDataInput;
@@ -40,7 +41,7 @@ import org.apache.sis.util.ArraysExt;
  * @author Johann Sorel (Geomatys)
  * @author Martin Desruisseaux (Geomatys)
  */
-final class FromImageIO extends Image {
+final class FromImageIO extends Image implements IIOReadWarningListener {
     /**
      * Index of the image to read.
      */
@@ -58,31 +59,34 @@ final class FromImageIO extends Image {
      * @param  locator   the provider of bytes to read from the 
<abbr>ISOBMFF</abbr> box.
      * @param  provider  provider of image readers for decoding the payload.
      * @param  name      a name that identifies this image, for debugging 
purpose.
-     * @throws RasterFormatException if the sample model cannot be created.
+     * @throws DataStoreException if an error occurred while decoding 
<abbr>HEIF</abbr> boxes.
      */
-    FromImageIO(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final ImageReaderSpi provider, final String name) {
+    FromImageIO(CoverageBuilder builder, ByteRanges.Reader locator, final 
ImageReaderSpi provider, String name)
+            throws DataStoreException
+    {
         super(builder, locator, name);
         this.provider = provider;
     }
 
     /**
-     * First an image reader provider by format name.
+     * Finds an image reader provider by format name.
+     * The search is case-insensitive.
      *
      * @param  format  name of the desired format.
      * @return image provider for the given format.
      * @throws UnsupportedEncodingException if no provider was found for the 
specified format.
+     *
+     * @see javax.imageio.ImageIO#getImageReadersByFormatName(String)
      */
     static ImageReaderSpi byFormatName(final String format) throws 
UnsupportedEncodingException {
-        var rg = IIORegistry.getDefaultInstance();
-        var it = rg.getServiceProviders(
-                ImageReaderSpi.class,
-                (spi) -> ArraysExt.containsIgnoreCase(((ImageReaderSpi) 
spi).getFormatNames(), format),
-                true);
-        if (it.hasNext()) {
-            return it.next();
-        } else {
-            throw new UnsupportedEncodingException("Could not find a JPEG 
reader.");
+        final Iterator<ImageReaderSpi> it = 
IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, 
true);
+        while (it.hasNext()) {
+            final ImageReaderSpi provider = it.next();
+            if (ArraysExt.containsIgnoreCase(provider.getFormatNames(), 
format)) {
+                return provider;
+            }
         }
+        throw new UnsupportedEncodingException("Could not find a " + format + 
" reader.");
     }
 
     /**
@@ -110,6 +114,8 @@ final class FromImageIO extends Image {
      * Returns the sample model and color model of this image.
      * The size of the sample model is the tile size.
      *
+     * @todo Try to fetch the image reader from {@code 
ImageResource.Coverage.ReadContext.getReader(provider))}.
+     *
      * @param  store   the store that opened the <abbr>HEIF</abbr> file.
      * @throws DataStoreContentException if this image does not include 
information about the sample/color models.
      * @throws DataStoreException if the input cannot be set because of its 
type.
@@ -118,6 +124,7 @@ final class FromImageIO extends Image {
     @Override
     protected ImageTypeSpecifier getImageType(final GeoHeifStore store) throws 
DataStoreException, IOException {
         final ImageReader reader = provider.createReaderInstance();
+        reader.addIIOReadWarningListener(this);
         final var ranges = new ByteRanges();
         locator.resolve(0, -1, ranges);
         setReaderInput(reader, 
ranges.viewAsConsecutiveBytes(store.ensureOpen()), ranges);
@@ -125,15 +132,15 @@ final class FromImageIO extends Image {
         ImageTypeSpecifier specifier;
         if (it.hasNext()) {
             specifier = it.next();
+            final int width  = reader.getTileWidth (IMAGE_INDEX);
+            final int height = reader.getTileHeight(IMAGE_INDEX);
+            SampleModel sampleModel = specifier.getSampleModel();
+            if (sampleModel.getWidth() != width || sampleModel.getHeight() != 
height) {
+                sampleModel = specifier.getSampleModel(width, height);
+                specifier = new ImageTypeSpecifier(specifier.getColorModel(), 
sampleModel);
+            }
         } else {
-            return super.getImageType(store);
-        }
-        final int width  = reader.getTileWidth (IMAGE_INDEX);
-        final int height = reader.getTileHeight(IMAGE_INDEX);
-        SampleModel sampleModel = specifier.getSampleModel();
-        if (sampleModel.getWidth() != width || sampleModel.getHeight() != 
height) {
-            sampleModel = specifier.getSampleModel(width, height);
-            specifier = new ImageTypeSpecifier(specifier.getColorModel(), 
sampleModel);
+            specifier = super.getImageType(store);
         }
         reader.dispose();
         return specifier;
@@ -152,17 +159,30 @@ final class FromImageIO extends Image {
             final int tx = Math.toIntExact(context.subTileX);
             final int ty = Math.toIntExact(context.subTileY);
             final ImageReader reader = context.getReader(provider);
-            setReaderInput(reader, input, context);
             final BufferedImage image;
             try {
+                reader.addIIOReadWarningListener(this);
+                setReaderInput(reader, input, context);
                 if (reader.canReadRaster()) {
                     return reader.readTileRaster(IMAGE_INDEX, tx, ty);
                 }
                 image = reader.readTile(IMAGE_INDEX, tx, ty);
             } finally {
                 reader.setInput(null);
+                reader.removeIIOReadWarningListener(this);
             }
             return image.getRaster();
         };
     }
+
+    /**
+     * Reports a non-fatal error during decoding.
+     *
+     * @param source   the reader calling this method.
+     * @param message  the warning.
+     */
+    @Override
+    public void warningOccurred(final ImageReader source, final String 
message) {
+        listeners.warning(message);
+    }
 }
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 39813392f9..f662062f3c 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
@@ -19,11 +19,11 @@ package org.apache.sis.storage.geoheif;
 import java.nio.ByteOrder;
 import java.io.IOException;
 import java.awt.image.Raster;
-import java.awt.image.RasterFormatException;
 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.event.StoreListeners;
 import org.apache.sis.storage.isobmff.ByteRanges;
 
 
@@ -58,31 +58,31 @@ abstract class Image {
      */
     protected final ByteOrder byteOrder;
 
+    /**
+     * Where to send the warning.
+     */
+    protected final StoreListeners listeners;
+
     /**
      * Creates a new tile.
      *
      * @param  builder  helper class for building the grid geometry and sample 
dimensions.
      * @param  locator  the provider of bytes to read from the 
<abbr>ISOBMFF</abbr> box.
      * @param  name     a name that identifies this image, for debugging 
purpose.
-     * @throws RasterFormatException if the sample model cannot be created.
+     * @throws DataStoreException if an error occurred while decoding 
<abbr>HEIF</abbr> boxes.
      */
-    protected Image(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final String name) {
+    protected Image(final CoverageBuilder builder, final ByteRanges.Reader 
locator, final String name)
+            throws DataStoreException
+    {
         this.locator = locator;
         this.name    = name;
         byteOrder    = builder.byteOrder();
         numXTiles    = builder.numTiles(0);
         numYTiles    = builder.numTiles(1);
+        listeners    = builder.store.listeners();
         // Do NOT invoke `builder.sampleModel()`, because that information is 
not available for all types.
     }
 
-    /**
-     * A supplier of image, used for deferred computation.
-     */
-    @FunctionalInterface
-    interface Supplier {
-        Image get() throws DataStoreException;
-    }
-
     /**
      * Returns the sample model and color model of this image.
      * For uncompressed image, this information is not available and this 
method always throws an exception.
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java
new file mode 100644
index 0000000000..c7fa5c9c51
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java
@@ -0,0 +1,289 @@
+/*
+ * 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.geoheif;
+
+import java.net.URI;
+import java.util.List;
+import java.awt.Dimension;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import javax.imageio.ImageTypeSpecifier;
+import org.opengis.util.GenericName;
+import org.apache.sis.image.DataType;
+import org.apache.sis.image.internal.shared.ColorModelBuilder;
+import org.apache.sis.image.internal.shared.ColorModelFactory;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.image.internal.shared.SampleModelBuilder;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.UnsupportedEncodingException;
+import org.apache.sis.storage.modifier.CoverageModifier;
+import org.apache.sis.storage.isobmff.mpeg.Component;
+import org.apache.sis.storage.isobmff.mpeg.ComponentType;
+import org.apache.sis.storage.isobmff.mpeg.ComponentPalette;
+import org.apache.sis.storage.isobmff.mpeg.InterleavingMode;
+import org.apache.sis.storage.isobmff.mpeg.UncompressedFrameConfig;
+import org.apache.sis.util.internal.shared.Numerics;
+
+
+/**
+ * Color model, sample model and sample dimensions computed from 
<abbr>HEIF</abbr> boxes.
+ *
+ * @todo The same {@code ImageModel} may be duplicated in many tiles of the 
same image.
+ * This class is intended to be used for avoiding to recompute the same 
information for every tiles.
+ * This sharing has not yet been implemented.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class ImageModel {
+    /**
+     * The type of sample values stored in the raster.
+     */
+    final DataType dataType;
+
+    /**
+     * The color model, or {@code null} if this builder did not have enough 
information.
+     */
+    final ColorModel colorModel;
+
+    /**
+     * The sample model, {@code null} if this builder did not have enough 
information.
+     */
+    final SampleModel sampleModel;
+
+    /**
+     * Default name of each band.
+     */
+    private final GenericName[] defaultBandNames;
+
+    /**
+     * The sample dimensions for each band.
+     */
+    private final SampleDimension[] sampleDimensions;
+
+    /**
+     * Creates a new image model from the given Image I/O specifier.
+     *
+     * @param  type  the image specifier from the Image I/O <abbr>API</abbr>.
+     */
+    ImageModel(final ImageTypeSpecifier type) {
+        colorModel  = type.getColorModel();
+        sampleModel = type.getSampleModel();
+        dataType    = DataType.forDataBufferType(sampleModel.getDataType());
+        sampleDimensions = null;
+        defaultBandNames = null;
+    }
+
+    /**
+     * Computes date type, color model, sample model, and sample dimensions.
+     *
+     * @todo Need to add information from the {@code CellPropertyTypeProperty} 
box.
+     *       These information include sample dimension name and unit of 
measurement.
+     *
+     * @param  width           the width  (in pixels) of the reconstructed 
image.
+     * @param  height          the height (in pixels) of the reconstructed 
image.
+     * @param  model           information about the sample model (data type, 
<i>etc.</i>), or {@code null}.
+     * @param  componentTypes  pixel data displaying as {@link ComponentType}, 
{@link URI} or {@link Integer}.
+     * @param  bitsPerChannel  number of bits per channel in the reconstructed 
image, or {@code null}.
+     * @param  palette         the color palette of an indexed color model, or 
{@code null}.
+     * @param  builder         the builder which is creating a grid coverage.
+     * @throws DataStoreContentException if the sample model or sample 
dimensions cannot be created.
+     * @throws DataStoreException if the reading failed for another reason.
+     */
+    ImageModel(final int width,
+               final int height,
+               final UncompressedFrameConfig model,
+               final Object[] componentTypes,
+               final byte[] bitsPerChannel,
+               final ComponentPalette palette,
+               final CoverageBuilder builder)
+            throws DataStoreException
+    {
+        final int nc = (model          == null) ? 0 : model.components.length;
+        final int nt = (componentTypes == null) ? 0 : componentTypes.length;
+        final int ns = (bitsPerChannel == null) ? 0 : bitsPerChannel.length;
+        final int numBands = Math.max(Math.max(nc, nt), ns);
+        final var bitsPerSample = new int[numBands];
+        final var sb = new SampleDimension.Builder();
+        sampleDimensions = new SampleDimension[numBands];
+        defaultBandNames = new GenericName[numBands];
+
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        DataType dataType = null;
+        int numBits=0, redMask=0, greenMask=0, blueMask=0, alphaMask=0;
+        int grayBand = -1, indexBand = -1, alphaBand = -1;    // Negative 
value means none.
+        for (int band = 0; band < numBands; band++) {
+            /*
+             * `colorType` can be an enumeration value such as `RED`, `GREEN`, 
`BLUE`,
+             * or an URI, or an Integer value. The same value can appear in 
two boxes.
+             * `bitDepth` can also appear in two boxes. The ISO 23001-17:2024 
standard
+             * said that when the component type information appears in the 
two boxes,
+             * `ComponentDefinitionBox` shall precede 
`UncompressedFrameConfigBox`.
+             */
+            var colorType = (band < nt) ? componentTypes[band] : null;  // 
From `ComponentDefinition`.
+            int bitDepth  = (band < ns) ? 
Byte.toUnsignedInt(bitsPerChannel[band]) : 0;
+            if (band < nc) {
+                final Component c = model.components[band];
+                if (colorType == null) {
+                    colorType = c.type;         // From 
`UncompressedFrameConfig`
+                }
+                if (dataType == null) {
+                    dataType = c.getDataType();
+                } else if (dataType != c.getDataType()) {
+                    throw new DataStoreContentException("All bands shall be of 
the same data type.");
+                }
+                bitDepth = Short.toUnsignedInt(c.bitDepth);
+                /*
+                 * Example: 10-bit unaligned RGB components followed by a 
1-byte aligned 7-bit alpha component.
+                 * Stored as 30 consecutive bits containing R, G and B, 
followed by 2 pre-alignment padding bits
+                 * for byte alignment, followed by one alignment padding bit 
then followed by the 7-bit alpha value.
+                 */
+                final int alignSize = Byte.toUnsignedInt(c.alignSize) * 
Byte.SIZE;
+                if (alignSize != 0) {
+                    numBits = Numerics.snapToCeil(numBits,  alignSize)
+                            + Numerics.snapToCeil(bitDepth, alignSize) - 
bitDepth;
+                }
+            }
+            /*
+             * Create the sample dimension and derive metadata from it.
+             * TODO: parse CellPropertyTypeProperty and 
CellPropertyCategoriesProperty boxes.
+             */
+            if (colorType != null) {
+                sb.setName(colorType.toString());
+            } else {
+                sb.setName(band);
+            }
+            defaultBandNames[band] = sb.getName();
+            var source = new CoverageModifier.BandSource(builder.store, 
builder.imageIndex, band, numBands, dataType);
+            builder.metadata().addNewBand(sampleDimensions[band] = 
builder.store.customizer.customize(source, sb));
+            sb.clear();
+            if (bitDepth == 0) {
+                bitDepth = Component.DEFAULT_BIT_DEPTH;
+            } else {
+                builder.metadata().setBitPerSample(bitDepth);
+            }
+            /*
+             * Identify the bands that we can map to RGBA.
+             * Will be used for building the color model.
+             */
+            bitsPerSample[band] = bitDepth;
+            numBits += bitDepth;
+            if (colorType instanceof ComponentType ct) {
+                int mask = 0;
+                if (numBits < Integer.SIZE) {
+                    mask = ((1 << bitDepth) - 1) << (Integer.SIZE - numBits);
+                }
+                switch (ct) {
+                    case RED:        redMask   |= mask; break;
+                    case GREEN:      greenMask |= mask; break;
+                    case BLUE:       blueMask  |= mask; break;
+                    case ALPHA:      alphaMask |= mask; alphaBand = band; 
break;
+                    case MONOCHROME: grayBand   = band; break;
+                    case PALETTE:    indexBand  = band; break;
+                }
+            }
+        }
+        final boolean isRGB = (redMask | greenMask | blueMask) != 0;
+        if (isRGB && numBits < Integer.SIZE) {
+            final int n = Integer.SIZE - numBits;   // Number of unused bits.
+            redMask   >>>= n;
+            greenMask >>>= n;
+            blueMask  >>>= n;
+            alphaMask >>>= n;
+        }
+        /*
+         * Build a sample model. The `InterleavingMode.COMPONENT` default 
value is arbitrary,
+         * as the `UncompressedFrameConfig` box is mandatory according ISO/IEC 
23001-17:2024.
+         */
+        this.dataType = dataType;
+        if (dataType != null) {
+            InterleavingMode interleaveType = InterleavingMode.COMPONENT;
+            final var tileSize = new Dimension(width, height);
+            if (model != null) {
+                tileSize.width  /= model.numTileCols;
+                tileSize.height /= model.numTileRows;
+                interleaveType = model.interleaveType;
+            }
+            final boolean isBanded;
+            switch (interleaveType) {
+                case COMPONENT: isBanded = true;  break;    // Java2D: 
BandedSampleModel
+                case PIXEL:     isBanded = false; break;    // Java2D: 
PixelInterleavedSampleModel
+                default: throw new UnsupportedEncodingException("Unsupported 
interleave type: " + interleaveType);
+            }
+            sampleModel = new SampleModelBuilder(dataType, tileSize, 
bitsPerSample, isBanded).build();
+        } else {
+            sampleModel = null;
+        }
+        /*
+         * Build a RGB(A) or indexed color model compatible with the sample 
model.
+         * The gray scale is used as a fallback for all unrecognized color 
models.
+         */
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        ColorModel colorModel = null;
+        if (indexBand >= 0 && palette != null) {
+            colorModel = palette.toARGB(dataType, bitsPerSample[indexBand], 
numBands, indexBand);   // May be null.
+        }
+        if (colorModel == null && sampleModel != null) {
+            if (grayBand < 0 && (grayBand = indexBand) < 0) {
+                grayBand = ColorModelFactory.DEFAULT_VISIBLE_BAND;
+            }
+            final var cb = new ColorModelBuilder(isRGB)
+                            .dataType(dataType)
+                            .bitsPerSample(bitsPerSample)
+                            .alphaBand(alphaBand)
+                            .visibleBand(grayBand, 
sampleDimensions[grayBand].getSampleRange().orElse(null));
+            if (isRGB) {
+                cb.componentMasks(redMask, greenMask, blueMask, alphaMask);
+                // TODO: use another color space if not RGB.
+            }
+            colorModel = cb.createRGB(sampleModel);
+        }
+        this.colorModel = colorModel;
+    }
+
+    /**
+     * Returns the sample dimensions.
+     * If the {@code coverage} argument is null, it is assumed the same as at 
construction time.
+     *
+     * @param  builder  builder of the coverage for which to create sample 
dimensions, or {@code null}.
+     * @return the sample dimensions.
+     * @throws DataStoreContentException if the sample dimensions cannot be 
created.
+     */
+    final List<SampleDimension> sampleDimensions(final CoverageBuilder 
builder) throws DataStoreException {
+        SampleDimension[] bands = sampleDimensions;
+        boolean share = true;
+        if (builder != null) {
+            bands = bands.clone();
+            final int numBands = bands.length;
+            final var sb = new SampleDimension.Builder();
+            for (int band = 0; band < numBands; band++) {
+                sb.setName(defaultBandNames[band]);
+                var source = new CoverageModifier.BandSource(builder.store, 
builder.imageIndex, band, numBands, dataType);
+                final SampleDimension sd = 
builder.store.customizer.customize(source, sb);
+                if (!sd.equals(bands[band])) {
+                    bands[band] = sd;
+                    share = false;
+                }
+                sb.clear();
+            }
+            if (share) {
+                bands = sampleDimensions;
+            }
+        }
+        return List.of(bands);
+    }
+}
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 d624101ad3..c767e2a646 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
@@ -27,7 +27,6 @@ import static java.lang.Math.addExact;
 import static java.lang.Math.multiplyExact;
 import java.awt.image.ColorModel;
 import java.awt.image.Raster;
-import java.awt.image.RasterFormatException;
 import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
 import javax.imageio.ImageReader;
@@ -125,24 +124,21 @@ final class ImageResource extends 
TiledGridCoverageResource implements StoreReso
      * @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 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.
+     * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform 
or the sample dimensions cannot be created.
      */
-    ImageResource(final CoverageBuilder builder, Image[] tiles, final 
Image.Supplier image) throws DataStoreException {
+    ImageResource(final CoverageBuilder builder, Image[] tiles, final Image 
image) throws DataStoreException {
         super(builder.store);
         this.store       = builder.store;
         identifier       = builder.name();
-        sampleDimensions = builder.sampleDimensions();
-        gridGeometry     = builder.gridGeometry();      // Should be after 
`sampleDimensions()`.
+        sampleDimensions = builder.imageModel().sampleDimensions(null);
+        gridGeometry     = builder.gridGeometry();
         if (tiles == null) {
             // Shall be after the call to `sampleDimensions()`.
-            tiles = new Image[] {
-                image.get()         // Actual I/O operations are deferred (not 
in this call).
-            };
+            tiles = new Image[] {image};
         }
         this.tiles  = tiles;
         sampleModel = builder.sampleModel();
-        colorModel  = builder.colorModel();
+        colorModel  = builder.imageModel().colorModel;
         metadata    = builder.metadata().build();     // Not 
`buildAndFreeze()` as the metadata may still be modified.
         tileMatrixRowStride = builder.numTiles(0);
     }
@@ -379,6 +375,7 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
              * @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.
              */
+            @SuppressWarnings("LeakingThisInConstructor")
             private ReadContext(final ReadContext parent, final ImageResource 
owner) throws DataStoreException {
                 this.iterator = new Snapshot(parent.iterator);
                 this.readers  = parent.readers;
@@ -420,15 +417,15 @@ final class ImageResource extends 
TiledGridCoverageResource implements StoreReso
              * This method caches the first reader created by this method,
              * then returns the cached value in subsequent calls.
              *
-             * @param  spi  the provider of image reader.
+             * @param  provider  the provider of image reader.
              * @return an image reader instance created by the given provider.
              * @throws IOException if an error occurred while creating the 
image reader.
              */
-            public ImageReader getReader(final ImageReaderSpi spi) throws 
IOException {
+            public ImageReader getReader(final ImageReaderSpi provider) throws 
IOException {
                 try {
-                    return readers.computeIfAbsent(spi, (factory) -> {
+                    return readers.computeIfAbsent(provider, (spi) -> {
                         try {
-                            return factory.createReaderInstance();
+                            return spi.createReaderInstance();
                         } catch (IOException e) {
                             throw new UncheckedIOException(e);
                         }
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 85b1e8c759..53cc82e09a 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
@@ -25,7 +25,6 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.awt.image.RasterFormatException;
 import java.io.IOException;
 import javax.imageio.spi.ImageReaderSpi;
 import org.opengis.util.GenericName;
@@ -130,7 +129,8 @@ final class ResourceBuilder {
     private MediaData data;
 
     /**
-     * Provider of Image I/O readers for the JPEG format, or {@code null} if 
not yet fetched.
+     * Provider of Image I/O readers for the <abbr>JPEG</abbr> format,
+     * or {@code null} if not yet fetched.
      *
      * @see #readerJPEG()
      */
@@ -222,7 +222,7 @@ final class ResourceBuilder {
     }
 
     /**
-     * Returns the provider of JPEG readers.
+     * Returns the provider of <abbr>JPEG</abbr> readers.
      */
     private ImageReaderSpi readerJPEG() throws UnsupportedEncodingException {
         if (readerJPEG == null) {
@@ -307,7 +307,7 @@ final class ResourceBuilder {
      * @param  info    information about the item, normally as a singleton but 
other size are nevertheless accepted.
      * @param  addTo   a list where to add the image, or {@code null} for 
adding to {@link #resources} instead.
      * @return the builder used for building the image. If many images were 
created, returns the first builder.
-     * @throws RasterFormatException if the sample dimensions or sample model 
cannot be created.
+     * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" 
transform or the sample dimensions cannot be created.
      * @throws DataStoreException if another error occurred while building the 
image or resource.
      */
     private CoverageBuilder createImage(final Integer itemID, final 
List<ItemInfoEntry> info, final List<Image> addTo)
@@ -335,8 +335,7 @@ final class ResourceBuilder {
             if (firstBuilder == null) {
                 firstBuilder = coverage;
             }
-            final ByteRanges.Reader locator;
-            final Image.Supplier image;
+            Image image = null;
             switch (entry.itemType) {
                 default: {
                     warning("Unsupported type " + 
Box.formatFourCC(entry.itemType) + " for the \"{0}\" resource.", name);
@@ -373,11 +372,12 @@ final class ResourceBuilder {
                  * the constructor will not ask for the sample model.
                  */
                 case ItemInfoEntry.JPEG: {
-                    final ImageReaderSpi reader = readerJPEG();
-                    locator = getLocationByIdentifier(itemID);
-                    final var prepared = new FromImageIO(coverage, locator, 
reader, name);
-                    if (locator != null) coverage.setImageLayout(prepared);
-                    image = () -> prepared;
+                    final ImageReaderSpi provider = readerJPEG();
+                    final ByteRanges.Reader locator = 
getLocationByIdentifier(itemID);
+                    if (locator != null) {
+                        image = new FromImageIO(coverage, locator, provider, 
name);
+                        coverage.setImageLayout(image);
+                    }
                     break;
                 }
                 /*
@@ -386,16 +386,18 @@ final class ResourceBuilder {
                  * actual reading is deferred.
                  */
                 case ItemInfoEntry.UNCI: {
-                    locator = getLocationByIdentifier(itemID);
-                    image = () -> new UncompressedImage(coverage, locator, 
name);
+                    final ByteRanges.Reader locator = 
getLocationByIdentifier(itemID);
+                    if (locator != null) {
+                        image = new UncompressedImage(coverage, locator, name);
+                    }
                     break;
                 }
             }
-            if (locator == null) {
+            if (image == null) {
                 warning("No data found for the \"{0}\" resource.", name);
             } else {
                 if (addTo != null) {
-                    addTo.add(image.get());
+                    addTo.add(image);
                 } else {
                     builders.remove(itemProperties);    // Builder cannot be 
reused after resource creation.
                     resources.add(coverage.build(name, image));
@@ -419,19 +421,14 @@ final class ResourceBuilder {
                 info = itemInfos.remove(0);     // `itemInfoEntry.itemID` = 0 
means the primary item.
                 if (info == null) continue;
             }
-            try {
-                createImage(primary.itemID, info, null);
-            } catch (RasterFormatException e) {
-                // Considered fatal because it was the primary resource.
-                throw new DataStoreContentException("Unsupported image 
layout.", e);
-            }
+            createImage(primary.itemID, info, null);
         }
         // Iterate over a snapshot because elements may be removed by 
`createImage(…)`.
         for (final Integer itemID : 
itemInfos.keySet().toArray(Integer[]::new)) {
             final List<ItemInfoEntry> info = itemInfos.remove(itemID);
             if (info != null) try {
                 createImage(itemID, info, null);
-            } catch (RasterFormatException e) {
+            } catch (UnsupportedEncodingException e) {
                 store.listeners().warning("A resource uses an unsupported 
sample model.", e);
             }
         }
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 df4d47afc4..a9c77ede8c 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
@@ -17,24 +17,22 @@
 package org.apache.sis.storage.geoheif;
 
 import java.util.Arrays;
-import java.util.zip.InflaterInputStream;
-import java.io.ByteArrayInputStream;
 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.WritableRaster;
-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;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.HyperRectangleReader;
 import org.apache.sis.io.stream.Region;
+import org.apache.sis.io.stream.inflater.Deflate;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.storage.isobmff.mpeg.CompressedUnitsItemInfo;
+import org.apache.sis.util.internal.shared.Numerics;
 
 
 /**
@@ -77,14 +75,14 @@ final class UncompressedImage extends Image {
      * @param  builder  helper class for building the grid geometry and sample 
dimensions.
      * @param  locator  the provider of bytes to read from the 
<abbr>ISOBMFF</abbr> box.
      * @param  name     a name that identifies this image, for debugging 
purpose.
-     * @throws RasterFormatException if the sample model cannot be created.
+     * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" 
transform or the sample dimensions cannot be created.
      */
     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()`.
+        dataType    = builder.imageModel().dataType;
         compressedImageUnit = builder.getCompressedImageUnit();
     }
 
@@ -132,18 +130,16 @@ final class UncompressedImage extends Image {
         final var  region    = region(sourceSize, sourceSize);
         final int  dataSize  = dataType.bytes();
         final long tileSize  = multiplyExact(region.length, dataSize);
-        final long skipBytes = region.getStartByteOffset(dataSize);
         final long tileIndex = addExact(multiplyExact(context.subTileY, 
numXTiles), context.subTileX);
-        final long offset    = addExact(multiplyExact(tileIndex, tileSize), 
skipBytes);
-        locator.resolve(offset, tileSize - skipBytes, context);
+        final long offset    = multiplyExact(tileIndex, tileSize);
+        locator.resolve(offset, tileSize, context);
         return (ChannelDataInput input) -> {
-            long origin = context.offset() - skipBytes;
+            long origin = context.offset();
             final CompressedUnitsItemInfo.Unit unit = compressedImageUnit;
             if (unit != null) {
-                input.skipNBytes(unit.offset);
-                final byte[] compressedChunk = input.readBytes((int) 
unit.size);
-                InflaterInputStream iis = new InflaterInputStream(new 
ByteArrayInputStream(compressedChunk));
-                input = new 
StorageConnector(iis).getStorageAs(ChannelDataInput.class);
+                final var inflater = new Deflate(input, listeners);
+                inflater.setInputRegion(addExact(origin, unit.offset), 
unit.size);
+                input = inflater.createDataInput(inflater, 
Numerics.clamp(sourceSize[0]), true);
                 origin = 0;
             }
             /*
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java
index 70146b2c57..312dddb9d0 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java
@@ -18,9 +18,9 @@ package org.apache.sis.storage.isobmff.mpeg;
 
 import java.net.URI;
 import java.io.IOException;
-import java.awt.image.RasterFormatException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.storage.UnsupportedEncodingException;
 import org.apache.sis.storage.isobmff.TreeNode;
 import org.apache.sis.util.resources.Errors;
 
@@ -126,16 +126,16 @@ public final class Component extends TreeNode {
     /**
      * Returns the Java2D data type for this component.
      *
-     * @throws RasterFormatException if the {@link #format} value is 
unsupported.
+     * @throws UnsupportedEncodingException if the {@link #format} value is 
unsupported.
      */
-    public DataType getDataType() {
+    public DataType getDataType() throws UnsupportedEncodingException {
+        boolean real   = false;
         boolean signed = false;
-        boolean real = false;
         switch (format) {
-            case 0: break;
-            case 1: real = true; break;
-            case 3: signed = true; break;
-            default: throw new 
RasterFormatException(Errors.format(Errors.Keys.UnsupportedFormat_1, format));
+            case 0:  break;
+            case 1:  real   = true; break;
+            case 3:  signed = true; break;
+            default: throw new 
UnsupportedEncodingException(Errors.format(Errors.Keys.UnsupportedFormat_1, 
format));
         }
         return DataType.forNumberOfBits(Short.toUnsignedInt(bitDepth), real, 
signed);
     }

Reply via email to