sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/07: Put in place the abstract classes needed for uncompression. Support uncompressed image of bytes for testing and for the case with subsampling along X axis (in which case we can not use `HyperRectangleReader`).
Date Thu, 29 Jul 2021 13:07:18 GMT
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 a061e01940ae3660a6110e0fbac19adc8f7c4411
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Jul 23 19:28:33 2021 +0200

    Put in place the abstract classes needed for uncompression.
    Support uncompressed image of bytes for testing and for the case with subsampling along
X axis (in which case we can not use `HyperRectangleReader`).
---
 .../sis/internal/coverage/j2d/RasterFactory.java   |  25 ++++
 .../org/apache/sis/internal/geotiff/Inflater.java  |  98 +++++++++++++
 .../apache/sis/internal/geotiff/Uncompressed.java  | 151 +++++++++++++++++++++
 .../apache/sis/internal/geotiff/package-info.java  |   6 +-
 .../sis/storage/geotiff/CompressedSubset.java      | 117 ++++++++++++++++
 .../org/apache/sis/storage/geotiff/DataCube.java   |  18 ++-
 .../org/apache/sis/storage/geotiff/DataSubset.java | 119 ++++++++++------
 .../sis/internal/storage/TiledGridResource.java    |  10 ++
 .../sis/internal/storage/io/ChannelData.java       |  18 ++-
 .../sis/test/storage/CoverageReadConsistency.java  |  37 ++++-
 10 files changed, 550 insertions(+), 49 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index e350521..0afb1a4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -33,6 +33,11 @@ import java.awt.image.RasterFormatException;
 import java.awt.image.WritableRaster;
 import java.awt.image.BufferedImage;
 import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.IntBuffer;
+import java.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
 import java.nio.ReadOnlyBufferException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.internal.feature.Resources;
@@ -200,6 +205,26 @@ public final class RasterFactory extends Static {
     }
 
     /**
+     * Creates a NIO buffer of the specified capacity.
+     * The buffer position will be 0 and its limit will be its capacity.
+     *
+     * @param  dataType  type of buffer to create.
+     * @param  capacity  the {@code Buffer} size.
+     * @return buffer of the specified type and size.
+     */
+    public static Buffer createBuffer(final DataType dataType, final int capacity) {
+        switch (dataType) {
+            case USHORT: // Fallthrough
+            case SHORT:  return ShortBuffer .allocate(capacity);
+            case BYTE:   return ByteBuffer  .allocate(capacity);
+            case INT:    return IntBuffer   .allocate(capacity);
+            case FLOAT:  return FloatBuffer .allocate(capacity);
+            case DOUBLE: return DoubleBuffer.allocate(capacity);
+            default: throw new AssertionError(dataType);
+        }
+    }
+
+    /**
      * Wraps the backing arrays of given NIO buffers into Java2D buffers.
      * This method wraps the underlying array of primitive types; data are not copied.
      * For each buffer, the data starts at {@linkplain Buffer#position() buffer position}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
new file mode 100644
index 0000000..e5fa40c
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
@@ -0,0 +1,98 @@
+/*
+ * 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.internal.geotiff;
+
+import java.io.IOException;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+
+import static org.apache.sis.util.ArgumentChecks.ensureStrictlyPositive;
+import static org.apache.sis.util.ArgumentChecks.ensurePositive;
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+
+/**
+ * Decompression algorithm.
+ * Decompression is applied row-by-row.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Inflater {
+    /**
+     * The source of data to decompress.
+     */
+    protected final ChannelDataInput input;
+
+    /**
+     * Number of chunk per row, as a strictly positive integer.
+     * A chunk is a pixel, except if we can optimize by reading the whole row as a single
chunk.
+     */
+    protected final int chunksPerRow;
+
+    /**
+     * Number of sample values per chunk, as a strictly positive integer.
+     * A chunk is a pixel, except if we can optimize by reading the whole row as a single
chunk.
+     */
+    protected final int samplesPerChunk;
+
+    /**
+     * Number of sample values to skip between pixels. Positive but often zero.
+     */
+    protected final int interpixels;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param  input            the source of data to decompress.
+     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
+     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
+     * @param  interpixels      number of sample values to skip between pixels. May be zero.
+     */
+    protected Inflater(final ChannelDataInput input, final int pixelsPerRow, final int samplesPerPixel,
final int interpixels) {
+        ensureStrictlyPositive("pixelsPerRow",    pixelsPerRow);
+        ensureStrictlyPositive("samplesPerPixel", samplesPerPixel);
+        ensurePositive        ("interpixels",     interpixels);
+        ensureNonNull         ("input",           input);
+        if (interpixels == 0) {
+            chunksPerRow    = 1;
+            samplesPerChunk = Math.multiplyExact(samplesPerPixel, pixelsPerRow);
+        } else {
+            chunksPerRow    = pixelsPerRow;
+            samplesPerChunk = samplesPerPixel;
+        }
+        this.interpixels = interpixels;
+        this.input       = input;
+    }
+
+    /**
+     * Reads a row of sample values and stores them in the target buffer.
+     *
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    public abstract void uncompressRow() throws IOException;
+
+    /**
+     * Reads the given amount of sample values without storing them.
+     * The given value is in units of sample values, not in bytes.
+     *
+     * @param  n  number of uncompressed sample values to ignore.
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    public abstract void skip(long n) throws IOException;
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
new file mode 100644
index 0000000..3cb3ce5
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
@@ -0,0 +1,151 @@
+/*
+ * 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.internal.geotiff;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Classes;
+
+
+/**
+ * A pseudo-inflater which copy values unchanged.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Uncompressed extends Inflater {
+    /**
+     * Stream position where to perform the next reading.
+     */
+    private long streamPosition;
+
+    /**
+     * Whether {@link #streamPosition} needs to be refreshed by
+     * a call to {@link ChannelDataInput#getStreamPosition()}.
+     */
+    private boolean positionNeedsRefresh;
+
+    /**
+     * Number of bytes in a sample value.
+     */
+    private final int sampleSize;
+
+    /**
+     * For constructors in inner classes.
+     */
+    private Uncompressed(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel,
int interpixels, final int sampleSize) {
+        super(input, pixelsPerRow, samplesPerPixel, interpixels);
+        this.streamPosition = start;
+        this.sampleSize = sampleSize;
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param  input            the source of data to decompress.
+     * @param  start            stream position where to start reading.
+     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
+     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
+     * @param  interpixels      number of sample values to skip between pixels. May be zero.
+     * @param  target           where to store sample values.
+     * @return the inflater for the given targe type.
+     * @throws IllegalArgumentException if the buffer type is not recognized.
+     */
+    public static Uncompressed create(final ChannelDataInput input, final long start,
+            final int pixelsPerRow, final int samplesPerPixel, final int interpixels, final
Buffer target)
+    {
+        if (target instanceof ByteBuffer) return new Bytes(input, start, pixelsPerRow, samplesPerPixel,
interpixels, (ByteBuffer) target);
+        throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(target)));
+    }
+
+    /**
+     * Reads a row of sample values and stores them in the target buffer.
+     * Subclasses must override and invoke {@code super.uncompress()} before to do the actual
reading.
+     */
+    @Override
+    public void uncompressRow() throws IOException {
+        if (!positionNeedsRefresh) {
+            positionNeedsRefresh = true;
+            input.seek(streamPosition);
+        }
+    }
+
+    /**
+     * Skips the given amount of sample values without storing them.
+     * The given value is in units of sample values, not in bytes.
+     *
+     * @param  n  number of uncompressed sample values to ignore.
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    @Override
+    public final void skip(final long n) throws IOException {
+        if (n != 0) {
+            if (positionNeedsRefresh) {
+                positionNeedsRefresh = false;
+                streamPosition = input.getStreamPosition();
+            }
+            streamPosition = Math.addExact(streamPosition, n * sampleSize);
+        }
+    }
+
+    /**
+     * Inflater when the values to read and store are bytes.
+     */
+    private static final class Bytes extends Uncompressed {
+        /** Where to copy the values that we will read. */
+        private final ByteBuffer target;
+
+        /** Creates a new inflater which will write in the given buffer. */
+        Bytes(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel,
int interpixels, ByteBuffer target) {
+            super(input, start, pixelsPerRow, samplesPerPixel, interpixels, Byte.BYTES);
+            this.target = target;
+        }
+
+        /**
+         * Reads and decompress a row of sample values.
+         */
+        @Override public void uncompressRow() throws IOException {
+            super.uncompressRow();
+            for (int i = chunksPerRow; --i > 0;) {      // (chunksPerRow - 1) iterations.
+                int n = samplesPerChunk;
+                do target.put(input.readByte());
+                while (--n != 0);
+                /*
+                 * Following loop is executed only if there is subsampling on the X axis.
+                 * We invoke `readByte()` in a loop instead of invoking `skip` because if
+                 * the number of bytes to skip is small, this is more efficient.
+                 */
+                for (n = interpixels; --n >= 0;) {
+                    input.readByte();
+                }
+            }
+            /*
+             * Read the last element that was not read in first above `for` loop, but without
+             * skipping `interpixels` values after it. This is necessary for avoiding EOF
if
+             * the last pixel to read is in the last column of the tile.
+             */
+            int n = samplesPerChunk;
+            do target.put(input.readByte());
+            while (--n != 0);
+        }
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
index 65e8c85..a802b5e 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
@@ -23,8 +23,12 @@
  * This package is for internal use by SIS only. Classes in this package
  * may change in incompatible ways in any future version without notice.
  *
+ * <p>Current version contains the classes for decompressing sample values.
+ * Those classes may move in another package in a future version if we want
+ * to share them with other image formats.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.8
  * @module
  */
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
new file mode 100644
index 0000000..bf1b2b2
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
@@ -0,0 +1,117 @@
+/*
+ * 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.awt.Point;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.nio.Buffer;
+import org.apache.sis.internal.storage.TiledGridResource;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.geotiff.Uncompressed;
+import org.apache.sis.internal.geotiff.Inflater;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.image.DataType;
+
+import static java.lang.Math.toIntExact;
+import static org.apache.sis.internal.jdk9.JDK9.multiplyFull;
+
+
+/**
+ * Raster data obtained from a compressed GeoTIFF file in the domain requested by user.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class CompressedSubset extends DataSubset {
+    /**
+     * Number of sample values to skip for moving to the next row of a tile.
+     */
+    private final long scanlineStride;
+
+    /**
+     * Creates a new data subset. All parameters should have been validated
+     * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
+     * This constructor should be invoked inside a synchronized block.
+     *
+     * @param  source   the resource which contain this {@code DataSubset}.
+     * @param  subset   description of the {@code owner} subset to cover.
+     * @param  rasters  potentially shared cache of rasters read by this {@code DataSubset}.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
+     */
+    CompressedSubset(final DataCube source, final TiledGridResource.Subset subset) throws
DataStoreException {
+        super(source, subset);
+        scanlineStride = multiplyFull(getTileSize(0), numInterleaved);
+    }
+
+    /**
+     * Computes the number of pixels to read in dimension <var>i</var>
+     * The arguments given to this method are the ones given to the {@code readSlice(…)}
method.
+     */
+    private static int pixelCount(final long[] lower, final long[] upper, final int[] subsampling,
final int i) {
+        final int n = toIntExact((upper[i] - lower[i] - 1) / subsampling[i] + 1);
+        assert (n > 0) : n;
+        return n;
+    }
+
+    /**
+     * Reads a two-dimensional slice of the data cube from the given input channel.
+     *
+     * @param  offsets      position in the channel where tile data begins, one value per
bank.
+     * @param  byteCounts   number of bytes for the compressed tile data, one value per bank.
+     * @param  lower        (<var>x</var>, <var>y</var>) coordinates
of the first pixel to read relative to the tile.
+     * @param  upper        (<var>x</var>, <var>y</var>) coordinates
after the last pixel to read relative to the tile.
+     * @param  subsampling  (<var>sx</var>, <var>sy</var>) subsampling
factors.
+     * @param  location     pixel coordinates in the upper-left corner of the tile to return.
+     * @return image decoded from the GeoTIFF file.
+     */
+    @Override
+    WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[]
lower, final long[] upper,
+                             final int[] subsampling, final Point location) throws IOException,
DataStoreException
+    {
+        final DataType type   = getDataType();
+        final int      width  = pixelCount(lower, upper, subsampling, 0);
+        final int      height = pixelCount(lower, upper, subsampling, 1);
+        final int      skipY  = subsampling[1] - 1;
+        final int      skipX  = numInterleaved * (subsampling[0] - 1);
+        final long     head   = numInterleaved * lower[0];
+        final long     tail   = numInterleaved * (getTileSize(0) - (width*subsampling[0]
+ lower[0])) + skipX;
+        final Buffer[] banks  = new Buffer[numBanks];
+        for (int b=0; b<numBanks; b++) {
+            final Buffer   bank = RasterFactory.createBuffer(type, capacity);
+            final Inflater algo = Uncompressed.create(reader().input, offsets[b], width,
numInterleaved, skipX, bank);
+            for (long y = lower[1]; --y >= 0;) {
+                algo.skip(scanlineStride);
+            }
+            for (int y = height; --y > 0;) {        // (height - 1) iterations.
+                algo.skip(head);
+                algo.uncompressRow();
+                algo.skip(tail);
+                for (int j=skipY; --j>=0;) {
+                    algo.skip(scanlineStride);
+                }
+            }
+            algo.skip(head);                        // Last iteration without last `skip(…)`
calls.
+            algo.uncompressRow();
+            fillRemainingRows(bank.flip());
+            banks[b] = bank;
+        }
+        return WritableRaster.createWritableRaster(model, RasterFactory.wrap(type, banks),
location);
+    }
+}
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 1cbc0c1..0c2794d 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
@@ -19,6 +19,7 @@ package org.apache.sis.storage.geotiff;
 import java.nio.file.Path;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -121,6 +122,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
     abstract Compression getCompression();
 
     /**
+     * Returns {@code true} if the sample model interleaves 2 or more sample values per pixel.
+     */
+    private boolean isInterleaved() throws DataStoreException {
+        final SampleModel model = getSampleModel();
+        return model.getNumBands() != 1 && !(model instanceof BandedSampleModel);
+    }
+
+    /**
      * Creates a {@link GridCoverage} which will load pixel data in the given domain.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for reading the
whole domain.
@@ -141,7 +150,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
                             Resources.Keys.MissingValue_2, Tags.name(Tags.Compression)));
                 }
                 switch (compression) {
-                    case NONE: coverage = new DataSubset(this, subset); break;
+                    case NONE: {
+                        if (subset.hasSubsampling(0) && isInterleaved()) {
+                            coverage = new CompressedSubset(this, subset);
+                        } else {
+                            coverage = new DataSubset(this, subset);
+                        }
+                        break;
+                    }
                     default: {
                         throw new DataStoreContentException(reader.resources().getString(
                                 Resources.Keys.UnsupportedCompressionMethod_1, compression));
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
index c23fc34..920ecb7 100644
--- 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
@@ -27,7 +27,6 @@ import java.awt.image.WritableRaster;
 import org.apache.sis.image.DataType;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.internal.storage.io.Region;
 import org.apache.sis.internal.storage.io.HyperRectangleReader;
 import org.apache.sis.internal.storage.TiledGridCoverage;
@@ -101,6 +100,25 @@ class DataSubset extends TiledGridCoverage implements Localized {
     private final int numTiles;
 
     /**
+     * Number of banks in the image data buffer.
+     * This is equal to the number of bands only for planar images, and 1 in all other cases.
+     */
+    protected final int numBanks;
+
+    /**
+     * Number of interleaved sample values in a pixel. For planar images, this is equal to
1.
+     * For interleaved sample model, this is equal to the number of bands. This value is
often
+     * equal to the {@linkplain java.awt.image.ComponentSampleModel#getPixelStride() pixel
stride}.
+     */
+    protected final int numInterleaved;
+
+    /**
+     * Number of sample values in a bank (not necessarily a band).
+     * This is tile width × height × {@link #numInterleaved}.
+     */
+    protected final int capacity;
+
+    /**
      * Creates a new data subset. All parameters should have been validated
      * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
      * This constructor should be invoked inside a synchronized block.
@@ -117,6 +135,23 @@ class DataSubset extends TiledGridCoverage implements Localized {
         final Vector[] tileArrayInfo = source.getTileArrayInfo();
         this.tileOffsets    = tileArrayInfo[0];
         this.tileByteCounts = tileArrayInfo[1];
+        /*
+         * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar
image only.
+         * Otherwise there is only one bank no matter the amount of bands. Each bank will
be read separately.
+         */
+        if (model instanceof BandedSampleModel) {
+            numBanks = model.getNumBands();
+            numInterleaved = 1;
+        } else {
+            numBanks = 1;
+            numInterleaved = model.getNumBands();
+        }
+        final int n = tileOffsets.size();
+        if (numBanks > n / numTiles) {
+            throw new DataStoreContentException(source.reader.errors().getString(
+                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles,
n));
+        }
+        capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
     }
 
     /**
@@ -136,6 +171,20 @@ class DataSubset extends TiledGridCoverage implements Localized {
     }
 
     /**
+     * Returns the type of data in all tiles.
+     */
+    protected final DataType getDataType() {
+        return DataType.forDataBufferType(model.getDataType());
+    }
+
+    /**
+     * Returns the GeoTIFF reader which contains this subset.
+     */
+    final Reader reader() {
+        return source.reader;
+    }
+
+    /**
      * Information about a tile to be read. A list of {@code Tile} is created and sorted
by increasing offsets
      * before the read operation begins, in order to read tiles in the order they are written
in the TIFF file.
      */
@@ -218,7 +267,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
         final WritableRaster[] result = new WritableRaster[iterator.tileCountInQuery];
         final Tile[] missings = new Tile[iterator.tileCountInQuery];
         int numMissings = 0;
-        synchronized (source.reader.store) {
+        synchronized (reader().store) {
             do {
                 final WritableRaster tile = iterator.getCachedTile();
                 if (tile != null) {
@@ -250,6 +299,9 @@ class DataSubset extends TiledGridCoverage implements Localized {
                 tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL);
                 tile.copyTileInfo(tileOffsets,    offsets,    numTiles);
                 tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
+                for (int b=0; b<offsets.length; b++) {
+                    offsets[b] = addExact(offsets[b], reader().origin);
+                }
                 result[tile.indexInResultArray] = tile.cache(
                         readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
             }
@@ -283,70 +335,61 @@ class DataSubset extends TiledGridCoverage implements Localized {
     WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[]
lower, final long[] upper,
                              final int[] subsampling, final Point location) throws IOException,
DataStoreException
     {
-        final DataType type = DataType.forDataBufferType(model.getDataType());
+        final DataType type = getDataType();
         final long width  = subtractExact(upper[0], lower[0]);
         final long height = subtractExact(upper[1], lower[1]);
         /*
-         * "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.
-         */
-        int numBanks = 1;
-        int numInterleaved = model.getNumBands();
-        if (model instanceof BandedSampleModel) {
-            numBanks = numInterleaved;
-            numInterleaved = 1;
-        }
-        if (numBanks > offsets.length) {
-            throw new DataStoreContentException(source.reader.errors().getString(
-                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles,
tileOffsets.size()));
-        }
-        /*
          * The number of bytes to read should not be greater than `byteCount`. It may be
smaller however if only
          * a subregion is read. Note that the `length` value may be different than `capacity`
if the tile to read
          * is smaller than the "standard" tile size of the image. It happens often when reading
the last strip.
          */
         final long length  = multiplyExact(type.size() / Byte.SIZE,
                              multiplyExact(multiplyExact(width, height), numInterleaved));
-        final int capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()),
numInterleaved);
         final long[] size = new long[] {multiplyFull(numInterleaved, getTileSize(0)), getTileSize(1)};
         /*
          * If we use an interleaved sample model, each "element" from `HyperRectangleReader`
perspective is actually
          * a group of `numInterleaved` values. Note that in such case, we can not handle
subsampling on the first axis.
+         * Such case should be handled by the `CompressedSubset` subclass instead, even if
there is no compression.
          */
-        if (numInterleaved != 1 && subsampling[0] != 1) {
-            throw new InternalDataStoreException();
-        }
+        assert numInterleaved == 1 || subsampling[0] == 1;
         lower[0] *= numInterleaved;
         upper[0] *= numInterleaved;
         /*
          * Read each plane ("banks" in Java2D terminology). Note that a single bank contain
all bands
          * in the interleaved sample model case.
          */
-        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()),
source.reader.input);
+        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()),
reader().input);
         final Region region = new Region(size, lower, upper, subsampling);
         final Buffer[] banks = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
             if (b < byteCounts.length && length > byteCounts[b]) {
-                throw new DataStoreContentException(source.reader.resources().getString(
+                throw new DataStoreContentException(reader().resources().getString(
                         Resources.Keys.UnexpectedTileLength_2, length, byteCounts[b]));
             }
-            hr.setOrigin(addExact(source.reader.origin, offsets[b]));
-            final Buffer buffer = hr.readAsBuffer(region, capacity);
-            /*
-             * The buffer returned by `readAsBuffer(…)` may have less data than the buffer
capacity
-             * if the current tile is smaller than the expected tile size (e.g. truncated
last tile).
-             * Following code applies the fill value if it is different than the default
value (zero).
-             */
-            if (fillValue != null) {
-                final int end = buffer.limit();
-                if (end != capacity) {
-                    Vector.create(buffer.limit(capacity), ImageUtilities.isUnsignedType(model))
-                          .fill(end, capacity, fillValue);
-                }
-            }
-            banks[b] = buffer.limit(capacity);
+            hr.setOrigin(offsets[b]);
+            final Buffer bank = hr.readAsBuffer(region, capacity);
+            fillRemainingRows(bank);
+            banks[b] = bank;
         }
         final DataBuffer buffer = RasterFactory.wrap(type, banks);
         return WritableRaster.createWritableRaster(model, buffer, location);
     }
+
+    /**
+     * Applies the fill value if it is different than the default value (zero) to all remaining
rows.
+     * This method is needed because the buffer filled by read methods may have less data
than the buffer
+     * capacity if the current tile is smaller than the expected tile size (e.g. last tile
is truncated).
+     *
+     * @param  bank  the buffer where to fill remaining rows.
+     */
+    final void fillRemainingRows(final Buffer bank) {
+        if (fillValue != null) {
+            final int end = bank.limit();
+            if (end != capacity) {
+                Vector.create(bank.limit(capacity), ImageUtilities.isUnsignedType(model))
+                      .fill(end, capacity, fillValue);
+                bank.limit(capacity);
+            }
+        }
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index fd0c6ee..8a13cbd 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -195,5 +195,15 @@ public abstract class TiledGridResource extends AbstractGridResource
{
              */
             cache = sharedCache ? caller.rasters : new WeakValueHashMap<>(Integer.class);
         }
+
+        /**
+         * Returns {@code true} if this subset contains a subsampling in the given dimension.
+         *
+         * @param  dimension  the dimension to test.
+         * @return {@code true} if there is a subsampling in the given dimension.
+         */
+        public boolean hasSubsampling(final int dimension) {
+            return subsampling[dimension] != 1;
+        }
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
index 49e5195..0a3059b 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
@@ -32,7 +32,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureBetween;
  * querying or modifying the stream position. This class does not define any read or write
operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.1
  * @since   0.5 (derived from 0.3)
  * @module
  */
@@ -135,7 +135,7 @@ public abstract class ChannelData implements Markable {
      * @return the bit offset of the stream.
      */
     public final int getBitOffset() {
-        final long position = bufferOffset + buffer.position();
+        final long position = position();
         if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
             bitPosition = position << BIT_OFFSET_SIZE;
         }
@@ -149,8 +149,7 @@ public abstract class ChannelData implements Markable {
      */
     public final void setBitOffset(final int bitOffset) {
         ensureBetween("bitOffset", 0, Byte.SIZE - 1, bitOffset);
-        final long position = bufferOffset + buffer.position();
-        bitPosition = (position << BIT_OFFSET_SIZE) | bitOffset;
+        bitPosition = (position() << BIT_OFFSET_SIZE) | bitOffset;
     }
 
     /**
@@ -167,7 +166,14 @@ public abstract class ChannelData implements Markable {
      */
     @Override
     public long getStreamPosition() {
-        return bufferOffset + buffer.position();
+        return position();
+    }
+
+    /**
+     * Returns the current byte position of the stream, ignoring overriding by subclasses.
+     */
+    private long position() {
+        return Math.addExact(bufferOffset, buffer.position());
     }
 
     /**
@@ -187,7 +193,7 @@ public abstract class ChannelData implements Markable {
      * @param position the new position of the stream.
      */
     public final void setStreamPosition(final long position) {
-        bufferOffset = position - buffer.position();
+        bufferOffset = Math.subtractExact(position, buffer.position());
         // Clearing the bit offset is needed if we don't want to handle the case of ChannelDataOutput,
         // which use a different stream position calculation when the bit offset is non-zero.
         clearBitOffset();
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 1d4424a..585db35 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -266,9 +266,11 @@ nextSlice:  for (;;) {
                             if (!failOnMismatch) break;
                             final Point pr = itr.getPosition();
                             final Point pc = itc.getPosition();
-                            assertArrayEquals("Mismatch at position (" + pr.x + ", " + pr.y
+ ") in full image " +
-                                              "and (" + pc.x + ", " + pc.y + ") in tested
sub-image",
-                                              expected, actual, STRICT);
+                            final StringBuilder message = new StringBuilder(100).append("Mismatch
at position (")
+                                    .append(pr.x).append(", ").append(pr.y).append(") in
full image and (")
+                                    .append(pc.x).append(", ").append(pc.y).append(") in
tested sub-image");
+                            findMatchPosition(itr, pr, expected, message);
+                            assertArrayEquals(message.toString(), expected, actual, STRICT);
                         }
                     }
                     assertFalse(itc.next());
@@ -363,4 +365,33 @@ nextSlice:  for (;;) {
         }
         return new PixelIterator.Builder().setRegionOfInterest(sliceAOI).create(image);
     }
+
+    /**
+     * Explores pixel values around the given position in search for a pixel having the expected
values.
+     * If a match is found, the error message is completed with information about the match
position.
+     */
+    private static void findMatchPosition(final PixelIterator ir, final Point pr, final double[]
expected, final StringBuilder message) {
+        double[] actual = null;
+        for (int dy=0; dy<10; dy++) {
+            for (int dx=0; dx<10; dx++) {
+                if ((dx | dy) != 0) {
+                    for (int c=0; c<4; c++) {
+                        final int x = (c & 1) == 0 ? -dx : dx;
+                        final int y = (c & 2) == 0 ? -dy : dy;
+                        try {
+                            ir.moveTo(pr.x + x, pr.y + y);
+                        } catch (IndexOutOfBoundsException e) {
+                            continue;
+                        }
+                        actual = ir.getPixel(actual);
+                        if (Arrays.equals(expected, actual)) {
+                            message.append(" (note: found a match at offset (").append(x).append(",
").append(y)
+                                   .append(") in full image)");
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
 }

Mime
View raw message