You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/05/04 22:29:54 UTC

[sis] branch GeoTIFF updated: Put the foundation for reading GeoTIFF sample values. This code anticipates a support of N-dimensional data cube, to be explored later. This commit is not yet functional and will be squashed before merge.

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

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


The following commit(s) were added to refs/heads/GeoTIFF by this push:
     new a296271  Put the foundation for reading GeoTIFF sample values. This code anticipates a support of N-dimensional data cube, to be explored later. This commit is not yet functional and will be squashed before merge.
a296271 is described below

commit a296271e646ad4bacf040f543e0b47bb38db3fea
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue May 4 20:55:49 2021 +0200

    Put the foundation for reading GeoTIFF sample values.
    This code anticipates a support of N-dimensional data cube, to be explored later.
    This commit is not yet functional and will be squashed before merge.
---
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |   8 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   | 235 +++++++++++++++++++++
 .../org/apache/sis/storage/geotiff/DataSubset.java | 146 +++++++++++++
 .../org/apache/sis/storage/geotiff/GeoTIFF.java    |  10 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  11 +
 .../sis/storage/geotiff/GridGeometryBuilder.java   |   7 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    | 170 +++++++++------
 .../apache/sis/storage/geotiff/PixelDecoder.java   | 139 ++++++++++++
 .../org/apache/sis/storage/geotiff/Reader.java     |  16 +-
 9 files changed, 663 insertions(+), 79 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 82d1dd6..abd0d11 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -244,7 +244,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      */
     private void warning(final short key, final Object... args) {
         final LogRecord r = reader.resources().getLogRecord(Level.WARNING, key, args);
-        reader.owner.warning(r);
+        reader.store.warning(r);
     }
 
     /**
@@ -463,7 +463,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 try {
                     expected = Integer.parseInt(id.getCode());
                 } catch (NumberFormatException e) {
-                    reader.owner.warning(null, e);                  // Should not happen.
+                    reader.store.warning(null, e);                  // Should not happen.
                     return;
                 }
                 if (code != expected) {
@@ -720,7 +720,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
         if (epsg != null) try {
             return getCSAuthorityFactory().createCartesianCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
-            reader.owner.warning(null, e);
+            reader.store.warning(null, e);
         }
         return (CartesianCS) CoordinateSystems.replaceLinearUnit(cs, unit);
     }
@@ -738,7 +738,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
         if (epsg != null) try {
             return getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
-            reader.owner.warning(null, e);
+            reader.store.warning(null, e);
         }
         return (EllipsoidalCS) CoordinateSystems.replaceAngularUnit(cs, unit);
     }
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
new file mode 100644
index 0000000..417686e
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -0,0 +1,235 @@
+/*
+ * 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.nio.file.Path;
+import org.apache.sis.image.DataType;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridDerivation;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.internal.storage.AbstractGridResource;
+import org.apache.sis.internal.storage.ResourceOnFileSystem;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.math.Vector;
+
+
+/**
+ * One or many GeoTIFF images packaged as a single resource.
+ * This is typically a single two-dimensional image represented as a {@link ImageFileDirectory}.
+ * But it can also be a stack of images organized in a <var>n</var>-dimensional data cube,
+ * or a pyramid of images with their overviews used when low resolution images is requested.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+abstract class DataCube extends AbstractGridResource implements ResourceOnFileSystem {
+    /**
+     * The GeoTIFF reader which contain this {@code DataCube}.
+     * Used for fetching information like the input channel and where to report warnings.
+     */
+    final Reader reader;
+
+    /**
+     * Creates a new data cube.
+     *
+     * @param reader  information about the input stream to read, the metadata and the character encoding.
+     */
+    DataCube(final Reader reader) {
+        super(reader.store.listeners());
+        this.reader = reader;
+    }
+
+    /**
+     * Shortcut for a frequently requested information.
+     */
+    final String filename() {
+        return reader.input.filename;
+    }
+
+    /**
+     * Information about pixel layout (data type, number of pixels, …) in the TIFF file.
+     */
+    static final class Layout {
+        /** The type to use for storing raster data. */
+        final DataType type;
+
+        /** Number of bits per component. */
+        final int bitsPerSample;
+
+        /** Whether the components are stored in separate “component planes”. */
+        final boolean isPlanar;
+
+        /** Creates new layout information. */
+        Layout(final DataType type, final int bitsPerSample, final boolean isPlanar) {
+            this.type          = type;
+            this.bitsPerSample = bitsPerSample;
+            this.isPlanar      = isPlanar;
+        }
+    }
+
+    /**
+     * Returns the type to use for storing raster data, together with the number of bits per sample.
+     * The type should be the one used in the GeoTIFF file as much as possible, but may sometime be wider.
+     *
+     * @throws DataStoreContentException if the type is not recognized.
+     */
+    protected abstract Layout getLayout() throws DataStoreContentException;
+
+    /**
+     * Returns the size in pixels of all tiles in this data cube. The array length shall be equal to
+     * {@link GridGeometry#getDimension()}, unless the grid geometry has one extra dimension for the
+     * vertical axis.
+     *
+     * @see #getGridGeometry()
+     */
+    protected abstract int[] getTileSize();
+
+    /**
+     * Gets the stream position or the length in bytes of compressed tile arrays in the GeoTIFF file.
+     * Values in the returned vector are {@code long} primitive type.
+     *
+     * @param  length  {@code false} for {@code tileOffsets} or {@code true} for {@code tileByteCounts}.
+     * @return stream position (relative to file beginning) or length of compressed tile arrays, in bytes.
+     */
+    abstract Vector getTileArrayInfo(boolean length);
+
+    /**
+     * 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 index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @return the grid coverage for the specified domain and range.
+     * @throws DataStoreException if an error occurred while reading the grid coverage data.
+     */
+    @Override
+    public final GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException {
+        final long startTime = System.nanoTime();
+        final GridCoverage coverage;
+        try {
+            synchronized (reader.store) {
+                coverage = subset(domain, range);
+            }
+        } catch (RuntimeException e) {
+            throw new DataStoreException(reader.errors().getString(Errors.Keys.CanNotRead_1, filename()), e);
+        }
+        logReadOperation(reader.store.path, coverage.getGridGeometry(), startTime);
+        return coverage;
+    }
+
+    /**
+     * Creates a {@link GridCoverage} which will load pixel data in the given domain,
+     * but without loading data immediately. This method does not perform I/O operations.
+     *
+     * @throws ArithmeticException if pixel indices exceed 64 bits integer capacity.
+     */
+    private DataSubset subset(GridGeometry domain, final int... range) throws DataStoreException {
+        final List<SampleDimension> bands = getSampleDimensions();
+        final RangeArgument  rangeIndices = validateRangeArgument(bands.size(), range);
+        final GridGeometry   gridGeometry = getGridGeometry();
+        final GridExtent     fullSize     = gridGeometry.getExtent();
+        final int[]          tileSize     = getTileSize();
+        final int            dimension    = tileSize.length;            // May be less than `fullSize.getDimension()`.
+        final int[]          subsamplings;
+        GridExtent           areaOfInterest;
+        if (domain == null) {
+            domain         = gridGeometry;
+            areaOfInterest = fullSize;
+            subsamplings   = new int[gridGeometry.getDimension()];
+            Arrays.fill(subsamplings, 1);
+        } else {
+            final GridDerivation target = gridGeometry.derive()
+                    .rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
+            areaOfInterest = target.getIntersection();
+            subsamplings   = target.getSubsamplings();
+            domain         = target.build();
+        }
+        Vector tileOffsets    = getTileArrayInfo(false);
+        Vector tileByteCounts = getTileArrayInfo(true);
+        /*
+         * Compute an initial number of tiles from the image size and tile size.
+         * Those numbers will be modified later if a sub-domain has been requested.
+         */
+        final long[] numTiles = new long[dimension];
+        for (int i=0; i<dimension; i++) {
+            numTiles[i] = Numerics.ceilDiv(fullSize.getSize(i), tileSize[i]);
+        }
+        /*
+         * Compute the values by which to multiply each tile coordinate for obtaining
+         * the index in `tileOffsets` and `tileByteCounts` vectors. This computation
+         * must be done before to clip `numTiles` to user-specified domain.
+         */
+        final int[] tileStrides = new int[dimension];
+        tileStrides[0] = 1;
+        for (int i=1; i<dimension; i++) {
+            tileStrides[i] = Math.toIntExact(Math.multiplyExact(numTiles[i], tileStrides[i-1]));
+        }
+        /*
+         * If a sub-domain is requested, modify the tile indices in order to include only
+         * the tiles that intersect the specified domain. Since we will still use (0,0,…)
+         * as the indices of the first tile, we need to have that tile at index 0 in the
+         * `tileOffsets` and `tileByteCounts` vectors.
+         */
+        if (!fullSize.equals(areaOfInterest)) {
+            long firstTileIndex = 0;
+            final long[] lowerDiff = new long[dimension];
+            final long[] upperDiff = new long[dimension];
+            for (int i=0; i<dimension; i++) {
+                final long low       = areaOfInterest.getLow(i);                            // Always ≥ 0.
+                final long high      = areaOfInterest.getHigh(i);                           // Inclusive
+                final int  size      = tileSize[i];
+                final long tileLower = low / size;                                          // Always ≥ 0.
+                final long tileUpper = Math.incrementExact(high / size);                    // Exclusive.
+                upperDiff[i]         = Math.multiplyExact(tileUpper, size) - high - 1;      // Always ≥ 0.
+                lowerDiff[i]         = low % size;                                          // low - tileLower*size
+                numTiles[i]          = tileUpper - tileLower;
+                firstTileIndex      += tileLower * tileStrides[i];
+            }
+            final int start = Math.toIntExact(firstTileIndex);
+            tileOffsets     = tileOffsets   .subList(start, tileOffsets.size());
+            tileByteCounts  = tileByteCounts.subList(start, tileByteCounts.size());
+            areaOfInterest  = areaOfInterest.expand(lowerDiff, upperDiff);                  // TODO: need to clip.
+            /*
+             * TODO: need to recompute `domain`. We should put above calculation directly in `GridDerivation`.
+             * See `GridDerivation.margin`; we may be able to apply tiling in the same code than margins.
+             */
+        }
+        /*
+         * TODO: take selected bands in account.
+         */
+        final PixelDecoder decoder = new PixelDecoder(tileSize, subsamplings, bands.size(), getLayout());
+        return new DataSubset(this, domain, bands, tileOffsets, tileByteCounts, numTiles, tileStrides, decoder);
+    }
+
+    /**
+     * Gets the paths to files used by this resource, or an empty array if unknown.
+     */
+    @Override
+    public Path[] getComponentFiles() {
+        final Path location = reader.store.path;
+        return (location != null) ? new Path[] {location} : new Path[0];
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
new file mode 100644
index 0000000..c7bc03b
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -0,0 +1,146 @@
+/*
+ * 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.io.IOException;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.math.Vector;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Raster data obtained from a GeoTIFF file in the domain requested by user. The number of dimensions is 2
+ * for standard TIFF files, but this class accepts higher number of dimensions if 3- or 4-dimensional data
+ * are stored in a GeoTIFF file using some convention.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class DataSubset extends GridCoverage {
+    /**
+     * The GeoTIFF reader which contain this {@code DataSubset}.
+     * Used for fetching information like the input channel and where to report warnings.
+     */
+    private final Reader reader;
+
+    /**
+     * For each tile, the byte offset of that tile as compressed and stored on disk.
+     * Tile X index varies fastest, followed by tile Y index, then tile Z index if any.
+     *
+     * @see ImageFileDirectory#tileOffsets
+     * @see #tileStrides
+     */
+    private final Vector tileOffsets;
+
+    /**
+     * For each tile, the number of (compressed) bytes in that tile.
+     * Elements are in the same order than {@link #tileOffsets}.
+     *
+     * @see ImageFileDirectory#tileByteCounts
+     * @see #tileStrides
+     */
+    private final Vector tileByteCounts;
+
+    /**
+     * Number of tiles along each dimension, inside the domain specified by the user.
+     * The array length is 2 for a standard TIFF file, but this class accepts a higher number of
+     * dimensions if 3- or 4-dimensional data are stored in a GeoTIFF file using some convention.
+     * This is the number of tiles in the requested domain, not necessarily in the whole image on file.
+     * The first tile is located at indices  (0, 0, …).
+     */
+    private final int[] numTiles;
+
+    /**
+     * Values by which to multiply each tile coordinates for obtaining the index in
+     * {@link #tileOffsets} and {@link #tileByteCounts} vectors. The length of this
+     * array is same as {@link #numTiles}.
+     */
+    private final int[] tileStrides;
+
+    /**
+     * The method for transferring pixel values from GeoTIFF file to this {@code Raster}.
+     * There is different subtypes depending on decompression methods.
+     */
+    private final PixelDecoder decoder;
+
+    /**
+     * Creates a new data subset. All parameters should have been validated
+     * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
+     *
+     * @param  parent          the resource which is creating this {@code DataSubset}.
+     * @param  domain          the sub-region extent, CRS and conversion from cell indices to CRS.
+     * @param  ranges          sample dimensions for each image band.
+     * @param  tileOffsets     the byte offset of each tile relative to the beginning of the TIFF file.
+     * @param  tileByteCounts  number of byte for each tile stored in the TIFF file.
+     * @param  numTiles        number of tiles along each dimension, inside the domain specified by the user.
+     * @param  tileStrides     values by which to multiply each tile coordinates for obtaining index in vectors.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
+     */
+    DataSubset(final DataCube parent, final GridGeometry domain, final List<? extends SampleDimension> ranges,
+               final Vector tileOffsets, final Vector tileByteCounts, final long[] numTiles, final int[] tileStrides,
+               final PixelDecoder decoder)
+    {
+        super(domain, ranges);
+        this.reader         = parent.reader;
+        this.tileOffsets    = tileOffsets;
+        this.tileByteCounts = tileByteCounts;
+        this.tileStrides    = tileStrides;
+        this.decoder        = decoder;
+        this.numTiles       = new int[numTiles.length];
+        for (int i=0; i<numTiles.length; i++) {
+            this.numTiles[i] = Math.toIntExact(numTiles[i]);
+        }
+    }
+
+    /**
+     * Reads the tile at the given tile coordinates.
+     * This method is thread-safe.
+     *
+     * @param  tile  indices of the tile to read.
+     * @return image decoded from the GeoTIFF file.
+     * @throws IOException if an I/O error occurred.
+     * @throws DataStoreContentException if the sample model is not supported.
+     * @throws RuntimeException if the Java2D image can not be created for another reason
+     *         (too many exception types to list them all).
+     */
+    private WritableRaster readTile(final int[] tile) throws IOException, DataStoreContentException {
+        synchronized (reader.store) {
+            int index = 0;
+            for (int i=0; i<tileStrides.length; i++) {
+                final int t = tile[i];
+                ArgumentChecks.ensureBetween("tile", 0, numTiles[i] - 1, t);
+                index = Math.addExact(index, Math.multiplyExact(tileStrides[i], t));
+            }
+            final long offset = Math.addExact(reader.origin, tileOffsets.longValue(index));
+            return decoder.readSlice(reader.input, offset);
+        }
+    }
+
+    @Override
+    public RenderedImage render(final GridExtent sliceExtent) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
index 1ff3a40..3f9d7da 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
@@ -64,7 +64,7 @@ abstract class GeoTIFF implements Closeable {
     /**
      * The store which created this reader or writer.
      */
-    final GeoTiffStore owner;
+    final GeoTiffStore store;
 
     /**
      * The object to use for parsing and formatting dates. Created when first needed.
@@ -74,22 +74,22 @@ abstract class GeoTIFF implements Closeable {
     /**
      * For subclass constructors.
      */
-    GeoTIFF(final GeoTiffStore owner) {
-        this.owner = owner;
+    GeoTIFF(final GeoTiffStore store) {
+        this.store = store;
     }
 
     /**
      * Returns the resources to use for formatting error messages.
      */
     final Errors errors() {
-        return Errors.getResources(owner.getLocale());
+        return Errors.getResources(store.getLocale());
     }
 
     /**
      * Returns the GeoTIFF-specific resource for error messages and warnings.
      */
     final Resources resources() {
-        return Resources.forLocale(owner.getLocale());
+        return Resources.forLocale(store.getLocale());
     }
 
     /**
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 d8175b6..aba07c6 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
@@ -24,6 +24,7 @@ import java.net.URI;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
@@ -90,6 +91,15 @@ public class GeoTiffStore extends DataStore implements Aggregate {
     private final URI location;
 
     /**
+     * Same value than {@link #location} but as a path, or {@code null} if none.
+     * Stored separately because conversion from path to URI back to path is not
+     * looseness (relative paths become absolutes).
+     *
+     * @todo May become an array later if we want to handle TFW and PRJ file heres.
+     */
+    final Path path;
+
+    /**
      * The data store identifier created from the filename, or {@code null} if none.
      * Defined as a namespace for use as the scope of children resources (the images).
      */
@@ -128,6 +138,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
                     connector.getStorage(), connector.getOption(OptionKey.OPEN_OPTIONS));
         }
         location = connector.getStorageAs(URI.class);
+        path = connector.getStorageAs(Path.class);
         connector.closeAllExcept(input);
         try {
             reader = new Reader(this, input);
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 3a6973f..3ca072d 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
@@ -182,6 +182,7 @@ final class GridGeometryBuilder {
 
     /**
      * 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;
 
@@ -295,7 +296,7 @@ final class GridGeometryBuilder {
                 if (e instanceof NoSuchAuthorityCodeException) {
                     key = Resources.Keys.UnknownCRS_1;
                 }
-                reader.owner.warning(reader.resources().getString(key, reader.owner.getDisplayName()), e);
+                reader.store.warning(reader.resources().getString(key, reader.store.getDisplayName()), e);
             } catch (IllegalArgumentException | NoSuchElementException | ClassCastException e) {
                 if (!helper.alreadyReported) {
                     canNotCreate(e);
@@ -304,7 +305,7 @@ final class GridGeometryBuilder {
         }
         /*
          * If the CRS is non-null, then it is either two- or three-dimensional.
-         * The 'affine' matrix may be for a greater number of dimensions, so it
+         * The `affine` matrix may be for a greater number of dimensions, so it
          * may need to be reduced.
          */
         int n = (crs != null) ? crs.getCoordinateSystem().getDimension() : 2;
@@ -392,6 +393,6 @@ final class GridGeometryBuilder {
      * Logs a warning telling that we can not create a grid geometry for the given reason.
      */
     private void canNotCreate(final Exception e) {
-        reader.owner.warning(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, reader.input.filename), 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 5456d32..7c7f838 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
@@ -33,18 +33,18 @@ import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.AbstractGridResource;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
+import org.apache.sis.image.DataType;
 
 
 /**
@@ -55,14 +55,14 @@ import org.apache.sis.measure.Units;
  * @author  Johann Sorel (Geomatys)
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags.html">TIFF Tag Reference</a>
  *
  * @since 0.8
  * @module
  */
-final class ImageFileDirectory extends AbstractGridResource {
+final class ImageFileDirectory extends DataCube {
     /**
      * Possible value for the {@link #tileTagFamily} field. That field tells whether image tiling
      * was specified using the {@code Tile*} family of TIFF tags or the {@code Strip*} family.
@@ -77,12 +77,6 @@ final class ImageFileDirectory extends AbstractGridResource {
     private static final byte SIGNED = 1, UNSIGNED = 0, FLOAT = 3;
 
     /**
-     * The GeoTIFF reader which contain this {@code ImageFileDirectory}.
-     * Used for fetching information like the input channel and where to report warnings.
-     */
-    private final Reader reader;
-
-    /**
      * The identifier as a sequence number in the namespace of the {@link GeoTiffStore}.
      * The first image has the sequence number "1".
      *
@@ -140,7 +134,7 @@ final class ImageFileDirectory extends AbstractGridResource {
      * The offset is specified with respect to the beginning of the TIFF file.
      * Each tile has a location independent of the locations of other tiles
      *
-     * <p>Offsets are ordered left-to-right and top-to-bottom. if {@link #isPlanar} is {@code true}
+     * <p>Offsets are ordered left-to-right and top-to-bottom. If {@link #isPlanar} is {@code true}
      * (i.e. components are stored in separate “component planes”), then the offsets for the first
      * component plane are stored first, followed by all the offsets for the second component plane,
      * and so on.</p>
@@ -364,9 +358,8 @@ final class ImageFileDirectory extends AbstractGridResource {
      * @param index   the image index as a sequence number starting with 0 for the first image.
      */
     ImageFileDirectory(final Reader reader, final int index) {
-        super(reader.owner.listeners());
-        this.reader = reader;
-        identifier = reader.nameFactory.createLocalName(reader.owner.identifier, String.valueOf(index + 1));
+        super(reader);
+        identifier = reader.nameFactory.createLocalName(reader.store.identifier, String.valueOf(index + 1));
     }
 
     /**
@@ -379,15 +372,8 @@ final class ImageFileDirectory extends AbstractGridResource {
     /**
      * Shortcut for a frequently requested information.
      */
-    private String filename() {
-        return input().filename;
-    }
-
-    /**
-     * Shortcut for a frequently requested information.
-     */
     private Charset encoding() {
-        return reader.owner.encoding;
+        return reader.store.encoding;
     }
 
     /**
@@ -573,9 +559,9 @@ final class ImageFileDirectory extends AbstractGridResource {
             case Tags.BitsPerSample: {
                 final Vector values = type.readVector(input(), count);
                 /*
-                 * The current implementation requires that all 'bitsPerSample' elements have the same value.
+                 * The current implementation requires that all `bitsPerSample` elements have the same value.
                  * This restriction may be revisited in future Apache SIS versions.
-                 * Note: 'count' is never zero when this method is invoked, so we do not need to check bounds.
+                 * Note: `count` is never zero when this method is invoked, so we do not need to check bounds.
                  */
                 bitsPerSample = values.shortValue(0);
                 final int length = values.size();
@@ -706,7 +692,7 @@ final class ImageFileDirectory extends AbstractGridResource {
                 break;
             }
             /*
-             * Stores all of the 'double' valued GeoKeys, referenced by the GeoKeyDirectory.
+             * Stores all of the `double` valued GeoKeys, referenced by the GeoKeyDirectory.
              */
             case Tags.GeoDoubleParams: {
                 referencing().numericParameters = type.readVector(input(), count);
@@ -777,7 +763,7 @@ final class ImageFileDirectory extends AbstractGridResource {
             ////                                                                                        ////
             ////    Metadata for discovery purposes, conditions of use, etc.                            ////
             ////    Those metadata are not "critical" information for reading the image.                ////
-            ////    Should not write anything under 'metadata/contentInfo' node.                        ////
+            ////    Should not write anything under `metadata/contentInfo` node.                        ////
             ////                                                                                        ////
             ////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -973,7 +959,7 @@ final class ImageFileDirectory extends AbstractGridResource {
                 i = s;
                 final Vector t = a; a = b; b = t;
             }
-            while (--i >= 0) {                      // At this point, 'b' shall be the longest vector.
+            while (--i >= 0) {                      // At this point, `b` shall be the longest vector.
                 final double va = a.doubleValue(i);
                 final double vb = b.doubleValue(i);
                 if (Double.isNaN(vb) || (max ? va > vb : va < vb)) {
@@ -1091,7 +1077,7 @@ final class ImageFileDirectory extends AbstractGridResource {
         /*
          * All of tile width, height and length information should be provided. But if only one of them is missing,
          * we can compute it provided that the file does not use any compression method. If there is a compression,
-         * then we set a bit for preventing the 'switch' block to perform a calculation but we let the code performs
+         * then we set a bit for preventing the `switch` block to perform a calculation but we let the code performs
          * the other checks in order to get an exception to be thrown with a good message.
          */
         int missing = !isPlanar && compression.equals(Compression.NONE) ? 0 : 0b1000;
@@ -1113,7 +1099,7 @@ final class ImageFileDirectory extends AbstractGridResource {
                 missingTag(Tags.TileLength, tileHeight, true);
                 break;
             }
-            case 0b0100: {          // Compute missing tile byte count.
+            case 0b0100: {          // Compute missing tile byte count in uncompressed case.
                 final long tileByteCount = pixelToByteCount(Math.multiplyExact(tileWidth, tileHeight));
                 final long[] tileByteCountArray = new long[tileOffsets.size()];
                 Arrays.fill(tileByteCountArray, tileByteCount);
@@ -1242,19 +1228,24 @@ final class ImageFileDirectory extends AbstractGridResource {
 
     /**
      * Returns an object containing the image size, the CRS and the conversion from pixel indices to CRS coordinates.
+     * The grid geometry has 2 or 3 dimensions, depending on whether the CRS declares a vertical axis or not.
+     *
+     * @see #getTileSize()
      */
     @Override
     public GridGeometry getGridGeometry() throws DataStoreContentException {
-        if (referencing != null) {
-            GridGeometry gridGeometry = referencing.gridGeometry;
-            if (gridGeometry == null) try {
-                gridGeometry = referencing.build(imageWidth, imageHeight);
-            } catch (FactoryException e) {
-                throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename()), e);
-            }
-            return gridGeometry;
-        } else {
-            return new GridGeometry(new GridExtent(imageWidth, imageHeight), null, null);
+        synchronized (reader.store) {
+            if (referencing != null) {
+                GridGeometry gridGeometry = referencing.gridGeometry;
+                if (gridGeometry == null) try {
+                    gridGeometry = referencing.build(imageWidth, imageHeight);
+                } catch (FactoryException e) {
+                    throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename()), e);
+                }
+                return gridGeometry;
+            } else {
+                return new GridGeometry(new GridExtent(imageWidth, imageHeight), null, null);
+            }
         }
     }
 
@@ -1264,32 +1255,93 @@ final class ImageFileDirectory extends AbstractGridResource {
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<SampleDimension> getSampleDimensions() throws DataStoreContentException {
-        if (sampleDimensions == null) {
-            final SampleDimension[] dimensions = new SampleDimension[samplesPerPixel];
-            final SampleDimension.Builder builder = new SampleDimension.Builder();
-            final InternationalString name = Vocabulary.formatInternational(Vocabulary.Keys.Value);
-            for (int band = 0; band < samplesPerPixel;) {
-                builder.addQualitative(name, minValues.get(Math.min(band, minValues.size()-1)),
-                                             maxValues.get(Math.min(band, maxValues.size()-1)));
-                dimensions[band] = builder.setName(++band).build();
-                builder.clear();
-            }
-            sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
+        synchronized (reader.store) {
+            if (sampleDimensions == null) {
+                final SampleDimension[] dimensions = new SampleDimension[samplesPerPixel];
+                final SampleDimension.Builder builder = new SampleDimension.Builder();
+                final InternationalString name = Vocabulary.formatInternational(Vocabulary.Keys.Value);
+                for (int band = 0; band < samplesPerPixel;) {
+                    builder.addQualitative(name, minValues.get(Math.min(band, minValues.size()-1)),
+                                                 maxValues.get(Math.min(band, maxValues.size()-1)));
+                    dimensions[band] = builder.setName(++band).build();
+                    builder.clear();
+                }
+                sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
+            }
+            return sampleDimensions;        // Safe because unmodifiable.
         }
-        return sampleDimensions;        // Safe because unmodifiable.
     }
 
     /**
-     * Loads a subset of the grid coverage represented by this resource.
+     * Returns the type of raster data. The enumeration values are restricted to types compatible with Java2D,
+     * at the cost of using more bits than {@link #bitsPerSample} if there is no exact match.
+     *
+     * @throws DataStoreContentException if the type is not recognized.
+     */
+    private DataType getDataType() throws DataStoreContentException {
+        final String format;
+        switch (sampleFormat) {
+            case SIGNED: {
+                if (bitsPerSample <  Byte   .SIZE) return DataType.BYTE;
+                if (bitsPerSample <= Short  .SIZE) return DataType.SHORT;
+                if (bitsPerSample <= Integer.SIZE) return DataType.INT;
+                format = "int";
+                break;
+            }
+            case UNSIGNED: {
+                if (bitsPerSample <= Byte   .SIZE) return DataType.BYTE;
+                if (bitsPerSample <= Short  .SIZE) return DataType.USHORT;
+                if (bitsPerSample <= Integer.SIZE) return DataType.INT;
+                format = "unsigned";
+                break;
+            }
+            case FLOAT: {
+                if (bitsPerSample == Float  .SIZE) return DataType.FLOAT;
+                if (bitsPerSample == Double .SIZE) return DataType.DOUBLE;
+                format = "float";
+                break;
+            }
+            default: {
+                format = "?";
+                break;
+            }
+        }
+        throw new DataStoreContentException(Errors.format(
+                Errors.Keys.UnsupportedFormat_1, format + ' ' + bitsPerSample + " bits"));
+    }
+
+    /**
+     * Returns the type to use for storing raster data, together with the number of bits per sample.
+     * The type should be the one used in the GeoTIFF file as much as possible, but may sometime be wider.
+     *
+     * @throws DataStoreContentException if the type is not recognized.
+     */
+    @Override
+    protected Layout getLayout() throws DataStoreContentException {
+        return new Layout(getDataType(), bitsPerSample, isPlanar);
+    }
+
+    /**
+     * Returns the size in pixels of all tiles in this data cube.
+     * The array length is exactly 2 for this class.
+     *
+     * @see #getGridGeometry()
+     */
+    @Override
+    protected int[] getTileSize() {
+        return new int[] {tileWidth, tileHeight};
+    }
+
+    /**
+     * Gets the stream position or the length in bytes of compressed tile arrays in the GeoTIFF file.
+     * Values in the returned vector are {@code long} primitive type.
      *
-     * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
-     * @param  range   0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
-     * @return the grid coverage for the specified domain and range.
-     * @throws DataStoreException if an error occurred while reading the grid coverage data.
+     * @param  length  {@code false} for {@code tileOffsets} or {@code true} for {@code tileByteCounts}.
+     * @return stream position (relative to file beginning) or length of compressed tile arrays, in bytes.
      */
     @Override
-    public GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException {
-        throw new DataStoreException("Not yet implemented.");   // TODO
+    Vector getTileArrayInfo(boolean length) {
+        return length ? tileByteCounts : tileOffsets;
     }
 
     /**
@@ -1301,7 +1353,7 @@ final class ImageFileDirectory extends AbstractGridResource {
      */
     private void warning(final Level level, final short key, final Object... parameters) {
         final LogRecord r = reader.resources().getLogRecord(level, key, parameters);
-        reader.owner.warning(r);
+        reader.store.warning(r);
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
new file mode 100644
index 0000000..9f743dd
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
@@ -0,0 +1,139 @@
+/*
+ * 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.nio.Buffer;
+import java.io.IOException;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.WritableRaster;
+import org.apache.sis.image.DataType;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.storage.io.Region;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.HyperRectangleReader;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Method for transferring pixel values from GeoTIFF tile to a {@link Raster}.
+ * The base class transfers uncompressed data. Compressed data are handled by
+ * specialized subclasses.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+class PixelDecoder {
+    /**
+     * Size of all tiles in the GeoTIFF image (without clipping and subsampling).
+     */
+    private final int[] tileSize;
+
+    /**
+     * Subsampling along each dimension. All values shall be greater than zero.
+     */
+    private final int[] subsamplings;
+
+    /**
+     * The sample model for all rasters. The size of this sample model is the values of
+     * the two first elements of {@link #tileSize} divided by subsampling after clipping.
+     */
+    protected final SampleModel model;
+
+    /**
+     * Creates a new pixel reader for the given type.
+     *
+     * @param  tileSize      size of all tiles before clipping and subsampling.
+     * @param  subsamplings  subsampling along each dimension. All values shall be greater than zero.
+     * @param  numBands      the number of bands in the raster to read.
+     * @param  layout        the Java2D type to use in destination image and other information.
+     * @throws IllegalArgumentException if a {@link SampleModel} can not be created from the given parameters.
+     */
+    PixelDecoder(final int[] tileSize, final int[] subsamplings, final int numBands, final DataCube.Layout layout) {
+        this.subsamplings = subsamplings;
+        this.tileSize     = tileSize;
+        final int width   = tileSize[0] / subsamplings[0];
+        final int height  = tileSize[1] / subsamplings[1];
+        final int type    = layout.type.ordinal();
+        if (layout.bitsPerSample < DataBuffer.getDataTypeSize(type)) {
+            /*
+             * Supported types for both sample models are TYPE_BYTE, TYPE_USHORT, and TYPE_INT.
+             * Note that if the TIFF data are signed bytes, we have TYPE_SHORT which will cause
+             * an exception below.
+             */
+            if (numBands == 1) {
+                model = new MultiPixelPackedSampleModel(type, width, height, layout.bitsPerSample);
+            } else if (!layout.isPlanar) {
+                final int[] bitMasks = new int[numBands];
+                model = new SinglePixelPackedSampleModel(type, width, height, bitMasks);
+            } else {
+                // TODO: we can support that with a little bit more work.
+                throw new IllegalArgumentException("Unsupported data format.");
+            }
+        } else if (layout.isPlanar) {
+            tileSize[2] = numBands;
+            model = new BandedSampleModel(type, width, height, numBands);
+        } else {
+            model = new PixelInterleavedSampleModel(type, width, height, numBands,
+                    Math.multiplyExact(numBands, width), ArraysExt.range(0, numBands));
+        }
+    }
+
+    /**
+     * Reads a two-dimensional slice of the data cube. Implementation in base class assumes uncompressed data.
+     * Subclasses must override for handling decompression.
+     *
+     * @param  input   the channel to read.
+     * @param  offset  position in the channel where tile data begins.
+     * @return image decoded from the GeoTIFF file.
+     * @throws IOException if an I/O error occurred.
+     * @throws DataStoreContentException if the sample model is not supported.
+     * @throws RuntimeException if the Java2D image can not be created for another reason
+     *         (too many exception types to list them all).
+     */
+    WritableRaster readSlice(final ChannelDataInput input, final long offset)
+            throws IOException, DataStoreContentException
+    {
+        final HyperRectangleReader reader = new HyperRectangleReader(
+                ImageUtilities.toNumberEnum(model.getDataType()), input, offset);
+        /*
+         * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
+         * Otherwise there is only one bank not matter the amount of bands. Each bank is read separately.
+         */
+        final Buffer[] banks = new Buffer[model instanceof BandedSampleModel ? model.getNumBands() : 1];
+        final long[] size        = new long[] {tileSize[0], tileSize[1], banks.length};
+        final int[]  subsample2D = new int[]  {subsamplings[0], subsamplings[1], 1};
+        final long[] regionLower = new long[3];     // TODO: allow subregion if only one tile along x or y.
+        final long[] regionUpper = new long[] {regionLower[0] + tileSize[0],
+                                               regionLower[1] + tileSize[1], 0};
+        for (int b=0; b < banks.length; b++) {
+            regionLower[2] = b;
+            regionUpper[2] = b + 1;
+            banks[b] = reader.readAsBuffer(new Region(size, regionLower, regionUpper, subsample2D));
+        }
+        final DataBuffer buffer = RasterFactory.wrap(DataType.forDataBufferType(model.getDataType()), banks);
+        return WritableRaster.createWritableRaster(model, buffer, null);
+    }
+}
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 8302217..e6f90be 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
@@ -50,7 +50,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Alexis Manin (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -63,7 +63,7 @@ final class Reader extends GeoTIFF {
     /**
      * Stream position of the first byte of the GeoTIFF file. This is usually zero.
      */
-    private final long origin;
+    final long origin;
 
     /**
      * A multiplication factor for the size of pointers, expressed as a power of 2.
@@ -137,8 +137,8 @@ final class Reader extends GeoTIFF {
      * @throws IOException if an error occurred while reading first bytes from the stream.
      * @throws DataStoreException if the file is not encoded in the TIFF or BigTIFF format.
      */
-    Reader(final GeoTiffStore owner, final ChannelDataInput input) throws IOException, DataStoreException {
-        super(owner);
+    Reader(final GeoTiffStore store, final ChannelDataInput input) throws IOException, DataStoreException {
+        super(store);
         this.input       = input;
         this.origin      = input.getStreamPosition();
         this.metadata    = new MetadataBuilder();
@@ -186,7 +186,7 @@ final class Reader extends GeoTIFF {
             }
         }
         // Do not invoke this.errors() yet because GeoTiffStore construction may not be finished. Owner.error() is okay.
-        throw new DataStoreContentException(owner.errors().getString(Errors.Keys.UnexpectedFileFormat_2, "TIFF", input.filename));
+        throw new DataStoreContentException(store.errors().getString(Errors.Keys.UnexpectedFileFormat_2, "TIFF", input.filename));
     }
 
     /**
@@ -215,7 +215,7 @@ final class Reader extends GeoTIFF {
         if (pointer >= 0) {
             return pointer;
         }
-        throw new DataStoreContentException(owner.getLocale(), "TIFF", input.filename, null);
+        throw new DataStoreContentException(store.getLocale(), "TIFF", input.filename, null);
     }
 
     /**
@@ -232,7 +232,7 @@ final class Reader extends GeoTIFF {
         if (entry >= 0) {
             return entry;
         }
-        throw new DataStoreContentException(owner.getLocale(), "TIFF", input.filename, null);
+        throw new DataStoreContentException(store.getLocale(), "TIFF", input.filename, null);
     }
 
     /**
@@ -388,7 +388,7 @@ final class Reader extends GeoTIFF {
             exception = null;
         }
         args[0] = Tags.name(tag);
-        owner.warning(errors().getString(key, args), exception);
+        store.warning(errors().getString(key, args), exception);
     }
 
     /**