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

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

commit 9824159f78319d2103823c90b2b80ef4d0b75c79
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Dec 5 15:19:50 2021 +0100

    Handle reduced-resolution (overview) images as levels in a pyramid of 
images (Cloud Optimized GeoTIFF convention).
---
 .../org/apache/sis/coverage/grid/GridGeometry.java |   5 +-
 .../sis/internal/geotiff/SchemaModifier.java       |   5 +
 .../org/apache/sis/storage/geotiff/DataCube.java   |   5 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  13 +-
 .../sis/storage/geotiff/GridGeometryBuilder.java   |  42 ++---
 .../sis/storage/geotiff/ImageFileDirectory.java    | 171 +++++++++++++-----
 .../sis/storage/geotiff/MultiResolutionImage.java  | 190 ++++++++++++++++++++
 .../org/apache/sis/storage/geotiff/Reader.java     | 195 ++++++++++++++-------
 8 files changed, 485 insertions(+), 141 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 25eddfd..d635d13 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -337,8 +337,11 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * @throws TransformException if the math transform can not compute the 
geospatial envelope from the grid extent.
      *
      * @see GridDerivation#subgrid(Envelope, double...)
+     *
+     * @since 1.2
      */
-    GridGeometry(final GridGeometry other, final GridExtent extent, final 
MathTransform toOther) throws TransformException {
+    public GridGeometry(final GridGeometry other, final GridExtent extent, 
final MathTransform toOther) throws TransformException {
+        ArgumentChecks.ensureNonNull("other", other);
         final int dimension = other.getDimension();
         this.extent = extent;
         ensureDimensionMatches(dimension, extent);
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/SchemaModifier.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/SchemaModifier.java
index 11e1650..8efa55b 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/SchemaModifier.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/SchemaModifier.java
@@ -30,6 +30,11 @@ import org.opengis.util.GenericName;
 /**
  * Modifies the metadata and bands inferred from GeoTIFF tags.
  *
+ * <h2>Image indices</h2>
+ * All image {@code index} arguments in this interfaces starts with 0 for the 
first (potentially pyramided) image
+ * are are incremented by 1 after each <em>pyramid</em>, as defined by the 
cloud Optimized GeoTIFF specification.
+ * Consequently those indices may differ from TIFF <cite>Image File 
Directory</cite> (IFD) indices.
+ *
  * @todo May move to public API (in revised form) in a future version.
  *
  * @author  Martin Desruisseaux (Geomatys)
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index 8c946ec..bd10a69 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -31,7 +31,6 @@ import org.apache.sis.internal.geotiff.Compression;
 import org.apache.sis.internal.storage.TiledGridResource;
 import org.apache.sis.internal.storage.ResourceOnFileSystem;
 import org.apache.sis.internal.storage.StoreResource;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Vector;
 
 
@@ -42,7 +41,7 @@ import org.apache.sis.math.Vector;
  * or a pyramid of images with their overviews used when low resolution images 
is requested.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -206,7 +205,7 @@ abstract class DataCube extends TiledGridResource 
implements ResourceOnFileSyste
                 coverage = preload(coverage);
             }
         } catch (RuntimeException e) {
-            throw new 
DataStoreException(reader.errors().getString(Errors.Keys.CanNotRead_1, 
filename()), e);
+            throw reader.store.errorIO(e);
         }
         logReadOperation(reader.store.path, coverage.getGridGeometry(), 
startTime);
         return coverage;
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 9da2e6e..333560a 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -311,8 +311,8 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             setFormatInfo(builder);
             int n = 0;
             try {
-                ImageFileDirectory dir;
-                while ((dir = reader.getImageFileDirectory(n++)) != null) {
+                GridCoverageResource dir;
+                while ((dir = reader.getImage(n++)) != null) {
                     builder.addFromComponent(dir.getMetadata());
                 }
             } catch (IOException e) {
@@ -369,9 +369,10 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     }
 
     /**
-     * Returns the exception to throw when an I/O error occurred.
+     * Returns the exception to throw when an I/O or other kind of error 
occurred.
+     * This method wraps the exception with a {@literal "Can not read 
<filename>"} message.
      */
-    private DataStoreException errorIO(final IOException e) {
+    final DataStoreException errorIO(final Exception e) {
         return new 
DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, 
reader.input.filename), e);
     }
 
@@ -446,7 +447,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         /** Returns element at the given index or returns {@code null} if the 
index is invalid. */
         private GridCoverageResource getImageFileDirectory(final int index) {
             try {
-                return reader().getImageFileDirectory(index);
+                return reader().getImage(index);
             } catch (IOException e) {
                 throw new BackingStoreException(errorIO(e));
             } catch (DataStoreException e) {
@@ -474,7 +475,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             cause = e;
         }
         if (index > 0) try {
-            ImageFileDirectory image = reader().getImageFileDirectory(index - 
1);
+            GridCoverageResource image = reader().getImage(index - 1);
             if (image != null) return image;
         } catch (IOException e) {
             throw errorIO(e);
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
index ac30ee7..ed8c7ad 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
@@ -75,16 +75,11 @@ import org.apache.sis.math.Vector;
  * So compared to the {@code CELL_CORNER} case, the {@code CELL_CENTER} case 
has a translation of +0.5 × scale.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.2
  * @since   1.0
  * @module
  */
 final class GridGeometryBuilder extends GeoKeysLoader {
-    /**
-     * The reader for which we will create coordinate reference systems.
-     * This is used for reporting warnings.
-     */
-    private final Reader reader;
 
     
////////////////////////////////////////////////////////////////////////////////////////
     ////                                                                       
         ////
@@ -173,31 +168,22 @@ final class GridGeometryBuilder extends GeoKeysLoader {
     
////////////////////////////////////////////////////////////////////////////////////////
 
     /**
-     * The grid geometry to be created by {@link #build(long, long)}.
-     * It has 2 or 3 dimensions, depending on whether the CRS declares a 
vertical axis or not.
-     */
-    public GridGeometry gridGeometry;
-
-    /**
      * Suggested value for a general description of the transformation form 
grid coordinates to "real world" coordinates.
-     * This information is obtained as a side-effect of {@link #build(long, 
long)} call.
+     * This information is obtained as a side-effect of {@link #build(Reader, 
long, long)} call.
      */
     private String description;
 
     /**
      * {@code POINT} if {@link GeoKeys#RasterType} is {@link 
GeoCodes#RasterPixelIsPoint},
      * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if 
unspecified.
-     * This information is obtained as a side-effect of {@link #build(long, 
long)} call.
+     * This information is obtained as a side-effect of {@link #build(Reader, 
long, long)} call.
      */
     private CellGeometry cellGeometry;
 
     /**
      * Creates a new builder.
-     *
-     * @param reader  where to report warnings if any.
      */
-    GridGeometryBuilder(final Reader reader) {
-        this.reader = reader;
+    GridGeometryBuilder() {
     }
 
     /**
@@ -266,16 +252,16 @@ final class GridGeometryBuilder extends GeoKeysLoader {
     /**
      * Creates the grid geometry and collect related metadata.
      * This method shall be invoked exactly once after {@link 
#validateMandatoryTags()}.
-     * After this method call (if successful), {@link #gridGeometry} is 
guaranteed non-null
+     * After this method call (if successful), the returned value is 
guaranteed non-null
      * and can be used as a flag for determining that the build has been 
completed.
      *
      * @param  width   the image width in pixels.
      * @param  height  the image height in pixels.
-     * @return {@link #gridGeometry}, guaranteed non-null.
+     * @return the grid geometry, guaranteed non-null.
      * @throws FactoryException if an error occurred while creating a CRS or a 
transform.
      */
     @SuppressWarnings("fallthrough")
-    public GridGeometry build(final long width, final long height) throws 
FactoryException {
+    public GridGeometry build(final Reader reader, final long width, final 
long height) throws FactoryException {
         CoordinateReferenceSystem crs = null;
         if (keyDirectory != null) {
             final CRSBuilder helper = new CRSBuilder(reader);
@@ -291,7 +277,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
                 reader.store.warning(reader.resources().getString(key, 
reader.store.getDisplayName()), e);
             } catch (IllegalArgumentException | NoSuchElementException | 
ClassCastException e) {
                 if (!helper.alreadyReported) {
-                    canNotCreate(e);
+                    canNotCreate(reader, e);
                 }
             }
         }
@@ -312,6 +298,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
         final GridExtent extent = new GridExtent(axisTypes, null, high, true);
         boolean pixelIsPoint = CellGeometry.POINT.equals(cellGeometry);
         final MathTransformFactory factory = 
DefaultFactories.forBuildin(MathTransformFactory.class);
+        GridGeometry gridGeometry;
         try {
             MathTransform gridToCRS;
             if (affine != null) {
@@ -329,7 +316,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
                 envelope.setToNaN();
             }
             gridGeometry = new GridGeometry(extent, envelope, 
GridOrientation.HOMOTHETY);
-            canNotCreate(e);
+            canNotCreate(reader, e);
             /*
              * Note: we catch TransformExceptions because they may be caused 
by erroneous data in the GeoTIFF file,
              * but let FactoryExceptions propagate because they are more 
likely to be a SIS configuration problem.
@@ -348,7 +335,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
      *
      * <h4>Pre-requite</h4>
      * <ul>
-     *   <li>{@link #build(long, long)} must have been invoked successfully 
before this method.</li>
+     *   <li>{@link #build(Reader, long, long)} must have been invoked 
successfully before this method.</li>
      *   <li>{@link ImageFileDirectory} must have filled its part of metadata 
before to invoke this method.</li>
      * </ul>
      *
@@ -362,10 +349,11 @@ final class GridGeometryBuilder extends GeoKeysLoader {
      *   <li>{@code metadata/referenceSystemInfo}</li>
      * </ul>
      *
-     * @param  metadata  the helper class where to write metadata values.
+     * @param  gridGeometry  the grid geometry computed by {@link 
#build(Reader, long, long)}.
+     * @param  metadata      the helper class where to write metadata values.
      * @throws NumberFormatException if a numeric value was stored as a string 
and can not be parsed.
      */
-    public void completeMetadata(final MetadataBuilder metadata) {
+    public void completeMetadata(final GridGeometry gridGeometry, final 
MetadataBuilder metadata) {
         if (metadata.addSpatialRepresentation(description, gridGeometry, 
true)) {
             /*
              * Whether the pixel value is thought of as filling the cell area 
or is considered as point measurements at
@@ -391,7 +379,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
     /**
      * Logs a warning telling that we can not create a grid geometry for the 
given reason.
      */
-    private void canNotCreate(final Exception e) {
+    private static void canNotCreate(final Reader reader, final Exception e) {
         
reader.store.warning(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1,
 reader.input.filename), e);
     }
 }
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 4b55c6b..c536b49 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -36,6 +36,7 @@ import org.opengis.metadata.citation.DateType;
 import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.geotiff.Predictor;
 import org.apache.sis.internal.geotiff.Compression;
@@ -73,7 +74,7 @@ import org.apache.sis.image.DataType;
  * @author  Johann Sorel (Geomatys)
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags.html";>TIFF 
Tag Reference</a>
  *
@@ -95,7 +96,14 @@ final class ImageFileDirectory extends DataCube {
     private static final byte SIGNED = 1, UNSIGNED = 0, FLOAT = 3;
 
     /**
-     * Index of this Image File Directory.
+     * Index of the (potentially pyramided) image containing this Image File 
Directory (IFD).
+     * All reduced-resolution (overviews) images are ignored when computing 
this index value.
+     * If the TIFF file does not contain reduced-resolution (overview) images, 
then
+     * {@code index} value is the same as the index of this IFD in the TIFF 
file.
+     *
+     * <p>If this IFD is a reduced-resolution (overview) image, then this 
index is off by one.
+     * It has the value of the next pyramid. This is an artifact of the way 
index is computed
+     * but should be invisible to user because they should not handle 
overviews directly.</p>
      */
     private final int index;
 
@@ -124,11 +132,26 @@ final class ImageFileDirectory extends DataCube {
     private boolean isValidated;
 
     /**
+     * A general indication of the kind of data contained in this subfile, 
mainly useful when there
+     * are multiple subfiles in a single TIFF file. This field is made up of a 
set of 32 flag bits.
+     *
+     * Bit 0 is 1 if the image is a reduced-resolution version of another 
image in this TIFF file.
+     * Bit 1 is 1 if the image is a single page of a multi-page image (see 
PageNumber).
+     * Bit 2 is 1 if the image defines a transparency mask for another image 
in this TIFF file (see PhotometricInterpretation).
+     * Bit 4 indicates MRC imaging model as described in ITU-T recommendation 
T.44 [T.44] (See ImageLayer tag) - RFC 2301.
+     *
+     * @see #isReducedResolution()
+     */
+    private int subfileType;
+
+    /**
      * The size of the image described by this FID, or -1 if the information 
has not been found.
      * The image may be much bigger than the memory capacity, in which case 
the image shall be tiled.
      *
      * <p><b>Note:</b>
      * the {@link #imageHeight} attribute is named {@code ImageLength} in TIFF 
specification.</p>
+     *
+     * @see #getExtent()
      */
     private long imageWidth = -1, imageHeight = -1;
 
@@ -399,12 +422,20 @@ final class ImageFileDirectory extends DataCube {
      */
     private GridGeometryBuilder referencing() {
         if (referencing == null) {
-            referencing = new GridGeometryBuilder(reader);
+            referencing = new GridGeometryBuilder();
         }
         return referencing;
     }
 
     /**
+     * The grid geometry created by {@link GridGeometryBuilder#build(Reader, 
long, long)}.
+     * It has 2 or 3 dimensions, depending on whether the CRS declares a 
vertical axis or not.
+     *
+     * @see #getGridGeometry()
+     */
+    private GridGeometry gridGeometry;
+
+    /**
      * The sample dimensions, or {@code null} if not yet created.
      *
      * @see #getSampleDimensions()
@@ -428,9 +459,10 @@ final class ImageFileDirectory extends DataCube {
 
     /**
      * Creates a new image file directory.
+     * The index arguments is used for metadata identifier only.
      *
      * @param reader  information about the input stream to read, the metadata 
and the character encoding.
-     * @param index   the image index as a sequence number starting with 0 for 
the first image.
+     * @param index   the pyramided image index as a sequence number starting 
with 0 for the first pyramid.
      */
     ImageFileDirectory(final Reader reader, final int index) {
         super(reader);
@@ -453,28 +485,25 @@ final class ImageFileDirectory extends DataCube {
     }
 
     /**
-     * Returns the identifier, creating it when first needed.
-     * This method must be invoked in a synchronized block.
-     */
-    private GenericName identifier() throws DataStoreException {
-        if (identifier == null) {
-            final GenericName name = 
reader.nameFactory.createLocalName(reader.store.namespace(), 
String.valueOf(index + 1));
-            identifier = reader.store.customizer.customize(index, name);
-            if (identifier == null) identifier = name;
-        }
-        return identifier;
-    }
-
-    /**
-     * Returns the identifier as a sequence number in the namespace of the 
{@link GeoTiffStore}.
-     * The first image has the sequence number "1".
+     * Returns the identifier in the namespace of the {@link GeoTiffStore}.
+     * The first image has the sequence number "1", optionally customized.
+     * Reduced-resolution (overviews) images have no identifier.
      *
      * @see #getMetadata()
      */
     @Override
     public Optional<GenericName> getIdentifier() throws DataStoreException {
         synchronized (getSynchronizationLock()) {
-            return Optional.of(identifier());
+            if (identifier == null) {
+                if (isReducedResolution()) {
+                    return Optional.empty();
+                }
+                final String id = String.valueOf(index + 1);
+                final GenericName name = 
reader.nameFactory.createLocalName(reader.store.namespace(), id);
+                identifier = reader.store.customizer.customize(index, name);
+                if (identifier == null) identifier = name;
+            }
+            return Optional.of(identifier);
         }
     }
 
@@ -765,7 +794,7 @@ final class ImageFileDirectory extends DataCube {
              * Bit 4 indicates MRC imaging model as described in ITU-T 
recommendation T.44 [T.44] (See ImageLayer tag) - RFC 2301.
              */
             case Tags.NewSubfileType: {
-                // TODO
+                subfileType = type.readInt(input(), count);
                 break;
             }
             /*
@@ -775,7 +804,13 @@ final class ImageFileDirectory extends DataCube {
              * 3 = a single page of a multi-page image (see PageNumber).
              */
             case Tags.SubfileType: {
-                // TODO
+                final int value = type.readInt(input(), count);
+                switch (value) {
+                    default: return value;                          // Warning 
to be reported by the caller.
+                    case 1:  subfileType &= ~1; break;
+                    case 2:  subfileType |=  1; break;
+                    case 3:  subfileType |=  2; break;
+                }
                 break;
             }
 
@@ -1316,7 +1351,16 @@ final class ImageFileDirectory extends DataCube {
      */
     @Override
     protected Metadata createMetadata() throws DataStoreException {
-        metadata.addTitle(identifier().toString());
+        final MetadataBuilder metadata = this.metadata;
+        if (metadata == null) {
+            /*
+             * We enter in this block only if an exception occurred during the 
first attempt to build metadata.
+             * If the user insists for getting metadata, fallback on the 
default (less complete) implementation.
+             */
+            return super.createMetadata();
+        }
+        this.metadata = null;     // Clear now in case an exception happens.
+        getIdentifier().ifPresent((id) -> metadata.addTitle(id.toString()));
         /*
          * Add information about the file format.
          *
@@ -1333,7 +1377,8 @@ final class ImageFileDirectory extends DataCube {
          *
          * Destination: metadata/contentInfo/attributeGroup/attribute
          */
-        
metadata.newCoverage(reader.store.customizer.isElectromagneticMeasurement(index));
+        final boolean isIndexValid = !isReducedResolution();
+        metadata.newCoverage(isIndexValid && 
reader.store.customizer.isElectromagneticMeasurement(index));
         final List<SampleDimension> sampleDimensions = getSampleDimensions();
         for (int band = 0; band < samplesPerPixel; band++) {
             metadata.addNewBand(sampleDimensions.get(band));
@@ -1392,16 +1437,45 @@ final class ImageFileDirectory extends DataCube {
             } catch (TransformException e) {
                 warning(e);
             }
-            referencing.completeMetadata(metadata);         // Must be after 
`getGridGeometry()`.
+            referencing.completeMetadata(gridGeometry, metadata);
         }
         /*
          * End of metadata construction from TIFF tags.
          */
         final DefaultMetadata md = metadata.build(false);
-        final Metadata c = reader.store.customizer.customize(index, md);
-        md.transitionTo(DefaultMetadata.State.FINAL);
-        metadata = null;
-        return (c != null) ? c : md;
+        if (isIndexValid) {
+            final Metadata c = reader.store.customizer.customize(index, md);
+            md.transitionTo(DefaultMetadata.State.FINAL);
+            if (c != null) return c;
+        }
+        return md;
+    }
+
+    /**
+     * Returns {@code true} if this image is a reduced resolution (overview) 
version
+     * of another image in this TIFF file.
+     */
+    final boolean isReducedResolution() {
+        return (subfileType & 1) != 0;
+    }
+
+    /**
+     * If this IFD has no grid geometry information, derives a grid geometry 
by applying a scale factor
+     * on the grid geometry of another IFD. Information about bands are also 
copied if compatible.
+     * This method should be invoked only when {@link #isReducedResolution()} 
is {@code true}.
+     *
+     * @param  fullResolution  the full-resolution image.
+     * @param  scales  <var>size of full resolution image</var> / <var>size of 
this image</var> for each grid axis.
+     */
+    final void initReducedResolution(final ImageFileDirectory fullResolution, 
final double[] scales)
+            throws DataStoreException, TransformException
+    {
+        if (referencing == null) {
+            gridGeometry = new GridGeometry(fullResolution.getGridGeometry(), 
getExtent(), MathTransforms.scale(scales));
+        }
+        if (samplesPerPixel == fullResolution.samplesPerPixel) {
+            sampleDimensions = fullResolution.getSampleDimensions();
+        }
     }
 
     /**
@@ -1411,26 +1485,37 @@ final class ImageFileDirectory extends DataCube {
      * <h4>Thread-safety</h4>
      * This method is thread-safe because it can be invoked directly by user.
      *
+     * @see #getExtent()
      * @see #getTileSize()
      */
     @Override
     public GridGeometry getGridGeometry() throws DataStoreContentException {
         synchronized (getSynchronizationLock()) {
-            if (referencing != null) {
-                GridGeometry gridGeometry = referencing.gridGeometry;
-                if (gridGeometry == null) try {
-                    gridGeometry = referencing.build(imageWidth, imageHeight);
+            if (gridGeometry == null) {
+                if (referencing != null) try {
+                    gridGeometry = referencing.build(reader, imageWidth, 
imageHeight);
                 } catch (FactoryException e) {
                     throw new 
DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1,
 filename()), e);
+                } else {
+                    // Fallback if the TIFF file has no GeoKeys.
+                    gridGeometry = new GridGeometry(getExtent(), null, null);
                 }
-                return gridGeometry;
-            } else {
-                return new GridGeometry(new GridExtent(imageWidth, 
imageHeight), null, null);
             }
+            return gridGeometry;
         }
     }
 
     /**
+     * Returns the image width and height without building the full grid 
geometry.
+     *
+     * @see #getTileSize()
+     * @see #getGridGeometry()
+     */
+    final GridExtent getExtent() {
+        return new GridExtent(imageWidth, imageHeight);
+    }
+
+    /**
      * Returns the ranges of sample values together with the conversion from 
samples to real values.
      *
      * <h4>Thread-safety</h4>
@@ -1443,15 +1528,22 @@ final class ImageFileDirectory extends DataCube {
             if (sampleDimensions == null) {
                 final SampleDimension[] dimensions = new 
SampleDimension[samplesPerPixel];
                 final SampleDimension.Builder builder = new 
SampleDimension.Builder();
-                for (int band = 0; band < samplesPerPixel;) {
+                final boolean isIndexValid = !isReducedResolution();
+                for (int band = 0; band < dimensions.length; band++) {
                     NumberRange<?> sampleRange = null;
                     if (minValues != null && maxValues != null) {
                         sampleRange = NumberRange.createBestFit(sampleFormat 
== FLOAT,
                                 minValues.get(Math.min(band, 
minValues.size()-1)), true,
                                 maxValues.get(Math.min(band, 
maxValues.size()-1)), true);
                     }
-                    dimensions[band] = 
reader.store.customizer.customize(index, band,
-                                        sampleRange, getFillValue(true), 
builder.setName(++band));
+                    builder.setName(band + 1);
+                    final SampleDimension sd;
+                    if (isIndexValid) {
+                        sd = reader.store.customizer.customize(index, band, 
sampleRange, getFillValue(true), builder);
+                    } else {
+                        sd = builder.build();
+                    }
+                    dimensions[band] = sd;
                     builder.clear();
                 }
                 sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
@@ -1495,6 +1587,7 @@ final class ImageFileDirectory extends DataCube {
      * Returns the size of tiles. This is also the size of the image sample 
model.
      * The number of dimensions is always 2 for {@code ImageFileDirectory}.
      *
+     * @see #getExtent()
      * @see #getSampleModel()
      */
     @Override
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/MultiResolutionImage.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/MultiResolutionImage.java
new file mode 100644
index 0000000..5c1b205
--- /dev/null
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/MultiResolutionImage.java
@@ -0,0 +1,190 @@
+/*
+ * 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.geotiff;
+
+import java.util.List;
+import java.util.Arrays;
+import java.io.IOException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.internal.storage.GridResourceWrapper;
+import org.apache.sis.internal.referencing.DirectPositionView;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+
+
+/**
+ * A list of Image File Directory (FID) where the first entry is the image at 
finest resolution
+ * and following entries are images at finer resolutions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class MultiResolutionImage extends GridResourceWrapper {
+    /**
+     * Descriptions of each <cite>Image File Directory</cite> (IFD) in the 
GeoTIFF file.
+     */
+    private final ImageFileDirectory[] levels;
+
+    /**
+     * Resolutions (in units of CRS axes) of each level from finest to 
coarsest resolution.
+     * Array elements may be {@code null} if not yet computed.
+     *
+     * @see #resolution(int)
+     * @see #getResolutions()
+     */
+    private final double[][] resolutions;
+
+    /**
+     * Creates a multi-resolution images with all the given reduced-resolution 
(overview) images,
+     * from finest resolution to coarsest resolution. The full-resolution 
image shall be at index 0.
+     */
+    MultiResolutionImage(final List<ImageFileDirectory> overviews) {
+        levels = overviews.toArray(new ImageFileDirectory[overviews.size()]);
+        resolutions = new double[levels.length][];
+    }
+
+    /**
+     * Returns the object on which to perform all synchronizations for 
thread-safety.
+     */
+    @Override
+    protected final Object getSynchronizationLock() {
+        return levels[0].getSynchronizationLock();
+    }
+
+    /**
+     * Creates the resource on which to delegate operations.
+     * The source is the first image, the one having finest resolution.
+     * By Cloud Optimized GeoTIFF (COG) convention, this is the image 
containing metadata (CRS).
+     * This method is invoked in a synchronized block when first needed and 
the result is cached.
+     */
+    @Override
+    protected GridCoverageResource createSource() throws DataStoreException {
+        try {
+            return getImageFileDirectory(0);
+        } catch (IOException e) {
+            throw levels[0].reader.store.errorIO(e);
+        }
+    }
+
+    /**
+     * Completes and returns the image at the given pyramid level.
+     * Indices are in the same order as the images appear in the TIFF file,
+     * with 0 for the full resolution image.
+     *
+     * @param  index  image index (level) in the pyramid, with 0 for finest 
resolution.
+     * @return image at the given pyramid level.
+     */
+    private ImageFileDirectory getImageFileDirectory(final int index) throws 
IOException, DataStoreException {
+        final ImageFileDirectory dir = levels[index];
+        if (dir.hasDeferredEntries) {
+            dir.reader.resolveDeferredEntries(dir);
+        }
+        dir.validateMandatoryTags();
+        return dir;
+    }
+
+    /**
+     * Returns the resolution (in units of CRS axes) for the given level.
+     *
+     * @param  level  the desired resolution level, numbered from finest to 
coarsest resolution.
+     * @return resolution at the specified level, not cloned (caller shall not 
modify).
+     */
+    private double[] resolution(final int level) throws DataStoreException {
+        double[] resolution = resolutions[level];
+        if (resolution == null) try {
+            final ImageFileDirectory image      = getImageFileDirectory(level);
+            final ImageFileDirectory base       = getImageFileDirectory(0);
+            final GridGeometry       geometry   = base.getGridGeometry();
+            final GridExtent         fullExtent = geometry.getExtent();
+            final GridExtent         subExtent  = image.getExtent();
+            final MatrixSIS          gridToCRS  = 
MatrixSIS.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER)
+                    .derivative(new 
DirectPositionView.Double(fullExtent.getPointOfInterest())));
+            final double[] scales = new double[fullExtent.getDimension()];
+            for (int i=0; i<scales.length; i++) {
+                scales[i] = fullExtent.getSize(i, false) / 
subExtent.getSize(i, false);
+            }
+            image.initReducedResolution(base, scales);
+            resolution = gridToCRS.multiply(scales);
+            for (int i=0; i<resolution.length; i++) {
+                resolution[i] = Math.abs(resolution[i]);
+            }
+            resolutions[level] = resolution;
+        } catch (TransformException e) {
+            throw new DataStoreReferencingException(e);
+        } catch (IOException e) {
+            throw levels[level].reader.store.errorIO(e);
+        }
+        return resolution;
+    }
+
+    /**
+     * Returns the preferred resolutions (in units of CRS axes) for read 
operations in this data store.
+     */
+    @Override
+    public List<double[]> getResolutions() throws DataStoreException {
+        final double[][] copy = new double[resolutions.length][];
+        synchronized (getSynchronizationLock()) {
+            for (int i=0; i<copy.length; i++) {
+                copy[i] = resolution(i).clone();
+            }
+        }
+        ArraysExt.reverse(copy);
+        return Arrays.asList(copy);
+    }
+
+    /**
+     * Loads a subset of the grid coverage represented by this resource.
+     *
+     * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
+     * @param  range   0-based indices of sample dimensions to read, or {@code 
null} or an empty sequence for reading them all.
+     * @return the grid coverage for the specified domain and range.
+     * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     */
+    @Override
+    public GridCoverage read(final GridGeometry domain, final int... range) 
throws DataStoreException {
+        final double[] request = domain.getResolution(true);
+        int level = resolutions.length;
+        synchronized (getSynchronizationLock()) {
+finer:      while (--level > 0) {
+                final double[] resolution = resolution(level);
+                for (int i=0; i<request.length; i++) {
+                    if (!(request[i] >= resolution[i])) {            // Use 
`!` for catching NaN.
+                        continue finer;
+                    }
+                }
+                break;
+            }
+            final ImageFileDirectory image;
+            try {
+                image = getImageFileDirectory(level);
+            } catch (IOException e) {
+                throw levels[level].reader.store.errorIO(e);
+            }
+            image.setLoadingStrategy(getLoadingStrategy());
+            return image.read(domain, range);
+        }
+    }
+}
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
index ef6223a..770836f 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
@@ -26,6 +26,7 @@ import java.io.IOException;
 import java.nio.ByteOrder;
 import java.text.ParseException;
 import org.opengis.util.NameFactory;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
@@ -81,6 +82,12 @@ final class Reader extends GeoTIFF {
     final byte intSizeExpansion;
 
     /**
+     * The last <cite>Image File Directory</cite> (IFD) read, or {@code null} 
if none.
+     * This is used when we detected the end of a pyramid and the beginning of 
next one.
+     */
+    private ImageFileDirectory lastIFD;
+
+    /**
      * Offset (relative to the beginning of the TIFF file) of the next Image 
File Directory (IFD)
      * to read, or 0 if we have finished to read all of them.
      *
@@ -98,10 +105,12 @@ final class Reader extends GeoTIFF {
     private final Set<Long> doneIFD;
 
     /**
-     * Positions of each <cite>Image File Directory</cite> (IFD) in this file.
-     * Those positions are fetched when first needed.
+     * Information about each (potentially pyramided) image in this file.
+     * Those objects are created when first needed.
+     *
+     * @see #getImage(int)
      */
-    private final List<ImageFileDirectory> imageFileDirectories = new 
ArrayList<>();
+    private final List<GridCoverageResource> images = new ArrayList<>();
 
     /**
      * Entries having a value that can not be read immediately, but instead 
have a pointer
@@ -229,90 +238,94 @@ final class Reader extends GeoTIFF {
     }
 
     /**
-     * Returns the <cite>Image File Directory</cite> (IFD) at the given index.
-     * If the IFD has already been read, then it is returned.
-     * Otherwise this method reads the IFD now and returns it.
-     *
-     * <p>The IFD consists of a 2 (classical) or 8 (BigTiff)-bytes count of 
the number of directory entries,
-     * followed by a sequence of 12-byte field entries, followed by a pointer 
to the next IFD (or 0 if none).</p>
+     * Returns the next <cite>Image File Directory</cite> (IFD), or {@code 
null} if we reached the last IFD.
+     * The IFD consists of a 2 (classical) or 8 (BigTiff)-bytes count of the 
number of directory entries,
+     * followed by a sequence of 12-byte field entries, followed by a pointer 
to the next IFD (or 0 if none).
      *
-     * @return the IFD if we found it, or {@code null} if there is no more IFD 
at the given index.
+     * @param  index  index of the (potentially pyramided) image, not counting 
reduced-resolution (overview) images.
+     * @return the IFD if we found it, or {@code null} if there is no more IFD.
      * @throws ArithmeticException if the pointer to a next IFD is too far.
+     *
+     * @see #getImage(int)
      */
-    final ImageFileDirectory getImageFileDirectory(final int index) throws 
IOException, DataStoreException {
-        while (index >= imageFileDirectories.size()) {
-            if (nextIFD == 0) {
-                return null;
-            }
-            resolveDeferredEntries(null, nextIFD);
-            input.seek(Math.addExact(origin, nextIFD));
-            nextIFD = 0;               // Prevent trying other IFD if we fail 
to read this one.
+    private ImageFileDirectory getImageFileDirectory(final int index) throws 
IOException, DataStoreException {
+        if (nextIFD == 0) {
+            return null;
+        }
+        resolveDeferredEntries(null, nextIFD);
+        input.seek(Math.addExact(origin, nextIFD));
+        nextIFD = 0;               // Prevent trying other IFD if we fail to 
read this one.
+        /*
+         * Design note: we parse the Image File Directory entry now because 
even if we were
+         * not interrested in that IFD, we need to go anyway after its last 
record in order
+         * to get the pointer to the next IFD.
+         */
+        final int offsetSize = Integer.BYTES << intSizeExpansion;
+        final ImageFileDirectory dir = new ImageFileDirectory(this, index);
+        for (long remaining = readUnsignedShort(); --remaining >= 0;) {
             /*
-             * Design note: we parse the Image File Directory entry now 
because even if we were
-             * not interrested in that IFD, we need to go anyway after its 
last record in order
-             * to get the pointer to the next IFD.
+             * Each entry in the Image File Directory has the following format:
+             *   - The tag that identifies the field (see constants in the 
Tags class).
+             *   - The field type (see constants inherited from the GeoTIFF 
class).
+             *   - The number of values of the indicated type.
+             *   - The value, or the file offset to the value elswhere in the 
file.
              */
-            final int offsetSize = Integer.BYTES << intSizeExpansion;
-            final ImageFileDirectory dir = new ImageFileDirectory(this, index);
-            for (long remaining = readUnsignedShort(); --remaining >= 0;) {
+            final short tag  = (short) input.readUnsignedShort();
+            final Type type  = Type.valueOf(input.readShort());        // May 
be null.
+            final long count = readUnsignedInt();
+            final long size  = (type != null) ? Math.multiplyExact(type.size, 
count) : 0;
+            if (size <= offsetSize) {
                 /*
-                 * Each entry in the Image File Directory has the following 
format:
-                 *   - The tag that identifies the field (see constants in the 
Tags class).
-                 *   - The field type (see constants inherited from the 
GeoTIFF class).
-                 *   - The number of values of the indicated type.
-                 *   - The value, or the file offset to the value elswhere in 
the file.
+                 * If the value can fit inside the number of bytes given by 
`offsetSize`, then the value is
+                 * stored directly at that location. This is the most common 
way TIFF tag values are stored.
                  */
-                final short tag  = (short) input.readUnsignedShort();
-                final Type type  = Type.valueOf(input.readShort());        // 
May be null.
-                final long count = readUnsignedInt();
-                final long size  = (type != null) ? 
Math.multiplyExact(type.size, count) : 0;
-                if (size <= offsetSize) {
-                    /*
-                     * If the value can fit inside the number of bytes given 
by `offsetSize`, then the value is
-                     * stored directly at that location. This is the most 
common way TIFF tag values are stored.
-                     */
-                    final long position = input.getStreamPosition();
-                    if (size != 0) {
-                        Object error;
-                        try {
-                            /*
-                             * A size of zero means that we have an unknown 
type, in which case the TIFF specification
-                             * recommends to ignore it (for allowing them to 
add new types in the future), or an entry
-                             * without value (count = 0) - in principle 
illegal but we make this reader tolerant.
-                             */
-                            error = dir.addEntry(tag, type, count);
-                        } catch (ParseException | RuntimeException e) {
-                            error = e;
-                        }
-                        if (error != null) {
-                            warning(tag, error);
-                        }
+                final long position = input.getStreamPosition();
+                if (size != 0) {
+                    Object error;
+                    try {
+                        /*
+                         * A size of zero means that we have an unknown type, 
in which case the TIFF specification
+                         * recommends to ignore it (for allowing them to add 
new types in the future), or an entry
+                         * without value (count = 0) - in principle illegal 
but we make this reader tolerant.
+                         */
+                        error = dir.addEntry(tag, type, count);
+                    } catch (ParseException | RuntimeException e) {
+                        error = e;
+                    }
+                    if (error != null) {
+                        warning(tag, error);
                     }
-                    input.seek(position + offsetSize);      // Usually just 
move the buffer position by a few bytes.
-                } else {
-                    // Offset from beginning of TIFF file where the values are 
stored.
-                    deferredEntries.add(new DeferredEntry(dir, tag, type, 
count, readUnsignedInt()));
-                    dir.hasDeferredEntries = true;
-                    deferredNeedsSort = true;
                 }
+                input.seek(position + offsetSize);      // Usually just move 
the buffer position by a few bytes.
+            } else {
+                // Offset from beginning of TIFF file where the values are 
stored.
+                deferredEntries.add(new DeferredEntry(dir, tag, type, count, 
readUnsignedInt()));
+                dir.hasDeferredEntries = true;
+                deferredNeedsSort = true;
             }
-            imageFileDirectories.add(dir);
-            readNextImageOffset();                          // Zero if the IFD 
that we just read was the last one.
         }
         /*
          * At this point we got the requested IFD. But maybe some deferred 
entries need to be read.
          * The values of those entries may be anywhere in the TIFF file, in 
any order. Given that
          * seek operations in the input stream may be costly or even not 
possible, we try to read
          * all values in sequential order, including values of other IFD if 
there is some before
-         * our IFD of interest.
+         * our IFD of interest. This is the purpose of 
`resolveDeferredEntries(…)`.
          */
-        final ImageFileDirectory dir = imageFileDirectories.get(index);
+        readNextImageOffset();                          // Zero if the IFD 
that we just read was the last one.
+        return dir;
+    }
+
+    /**
+     * Reads all entries that were deferred.
+     *
+     * @param dir  the IFD for which to resolve deferred entries regardless 
stream position or {@code ignoreAfter} value.
+     */
+    final void resolveDeferredEntries(final ImageFileDirectory dir) throws 
IOException, DataStoreException {
         if (dir.hasDeferredEntries) {
             resolveDeferredEntries(dir, Long.MAX_VALUE);
             dir.hasDeferredEntries = false;
         }
         dir.validateMandatoryTags();
-        return dir;
     }
 
     /**
@@ -361,6 +374,58 @@ final class Reader extends GeoTIFF {
     }
 
     /**
+     * Returns the potentially pyramided <cite>Image File Directories</cite> 
(IFDs) at the given index.
+     * If the pyramid has already been initialized, then it is returned.
+     * Otherwise this method initializes the pyramid now and returns it.
+     *
+     * <p>This method assumes that the first IFD is the full resolution image 
and all following IFDs having
+     * {@link ImageFileDirectory#isReducedResolution()} flag set are the same 
image at lower resolutions.
+     * This is the <cite>cloud optimized GeoTIFF</cite> convention.</p>
+     *
+     * @return the pyramid if we found it, or {@code null} if there is no more 
pyramid at the given index.
+     * @throws ArithmeticException if the pointer to a next IFD is too far.
+     */
+    final GridCoverageResource getImage(final int index) throws IOException, 
DataStoreException {
+        while (index >= images.size()) {
+            int imageIndex = images.size();
+            ImageFileDirectory fullResolution = lastIFD;
+            if (fullResolution == null) {
+                fullResolution = getImageFileDirectory(imageIndex);
+                if (fullResolution == null) {
+                    return null;
+                }
+            }
+            lastIFD = null;     // Clear now in case of error.
+            imageIndex++;       // In case next image is full-resolution.
+            ImageFileDirectory image;
+            final List<ImageFileDirectory> overviews = new ArrayList<>();
+            while ((image = getImageFileDirectory(imageIndex)) != null) {
+                if (image.isReducedResolution()) {
+                    overviews.add(image);
+                } else {
+                    lastIFD = image;
+                    break;
+                }
+            }
+            /*
+             * All pyramid levels have been read. If there is only one level,
+             * use the image directly. Otherwise create the pyramid.
+             */
+            if (overviews.isEmpty()) {
+                images.add(fullResolution);
+            } else {
+                overviews.add(0, fullResolution);
+                images.add(new MultiResolutionImage(overviews));
+            }
+        }
+        final GridCoverageResource image = images.get(index);
+        if (image instanceof ImageFileDirectory) {
+            resolveDeferredEntries((ImageFileDirectory) image);
+        }
+        return image;
+    }
+
+    /**
      * Logs a warning about a tag that can not be read, but does not interrupt 
the TIFF reading.
      *
      * @param tag    the tag than can not be read.

Reply via email to