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/08/13 18:37:40 UTC

[sis] 01/03: Rename `samplesPerElement` as `pixelsPerElement` (this is not the same value in `SinglePixelPackedSampleModel` case) and consolidate the way that this value is computed from the `SampleModel`.

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 aa876ee1ba8eb24c224d6e6c0435b091712be585
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 13 14:52:27 2021 +0200

    Rename `samplesPerElement` as `pixelsPerElement` (this is not the same value in `SinglePixelPackedSampleModel` case)
    and consolidate the way that this value is computed from the `SampleModel`.
---
 .../apache/sis/internal/geotiff/CopyFromBytes.java |  42 ++++----
 .../org/apache/sis/internal/geotiff/Inflater.java  |  76 ++++++-------
 .../sis/storage/geotiff/CompressedSubset.java      |  29 ++---
 .../org/apache/sis/storage/geotiff/DataCube.java   |   4 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |  34 ++----
 .../sis/internal/storage/TiledGridCoverage.java    |  56 +++++++++-
 .../sis/internal/storage/TiledGridResource.java    | 119 +++++++++++++--------
 7 files changed, 209 insertions(+), 151 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
index e18d8b6..0bc6e11 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
@@ -60,30 +60,36 @@ abstract class CopyFromBytes extends Inflater {
     private final int bytesPerElement;
 
     /**
-     * Number of sample values per primitive element.
+     * Number of pixels per primitive element.
      * Always 1 except for multi-pixels packed images.
+     *
+     * <div class="note"><b>Note:</b>
+     * this is "pixels per element", not "samples per element", because the value of this field shall be 1
+     * in the {@link java.awt.image.SinglePixelPackedSampleModel} case (by contrast a "samples per element"
+     * would have a value greater than 1). But this field can nevertheless be understood as a "samples per
+     * element" value where only one band is considered at a time.</div>
      */
-    private final int samplesPerElement;
+    private final int pixelsPerElement;
 
     /**
      * For constructors in inner classes.
      *
-     * @param  input              the source of data to decompress.
-     * @param  start              position in the input stream of the first byte to read.
-     * @param  chunksPerRow       number of chunks (usually pixels) per row in target image. Must be strictly positive.
-     * @param  samplesPerChunk    number of sample values per chunk (sample, pixel or row). Must be strictly positive.
-     * @param  skipAfterChunks    number of sample values to skip between chunks. May be empty or null.
-     * @param  samplesPerElement  number of sample values per primitive element. Always 1 except for multi-pixels packed images.
-     * @param  bytesPerElement    number of bytes in a bank element (a bank element is usually a sample value).
+     * @param  input             the source of data to decompress.
+     * @param  start             position in the input stream of the first byte to read.
+     * @param  chunksPerRow      number of chunks (usually pixels) per row in target image. Must be strictly positive.
+     * @param  samplesPerChunk   number of sample values per chunk (sample, pixel or row). Must be strictly positive.
+     * @param  skipAfterChunks   number of sample values to skip between chunks. May be empty or null.
+     * @param  pixelsPerElement  number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+     * @param  bytesPerElement   number of bytes in a bank element (a bank element is usually a sample value).
      */
     private CopyFromBytes(final ChannelDataInput input, final long start, final int chunksPerRow,
                           final int samplesPerChunk, final int[] skipAfterChunks,
-                          final int samplesPerElement, final int bytesPerElement)
+                          final int pixelsPerElement, final int bytesPerElement)
     {
-        super(input, chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, input.buffer.capacity() / bytesPerElement);
-        this.streamPosition    = start;
-        this.bytesPerElement   = bytesPerElement;
-        this.samplesPerElement = samplesPerElement;
+        super(input, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, input.buffer.capacity() / bytesPerElement);
+        this.streamPosition   = start;
+        this.bytesPerElement  = bytesPerElement;
+        this.pixelsPerElement = pixelsPerElement;
     }
 
     /**
@@ -129,7 +135,7 @@ abstract class CopyFromBytes extends Inflater {
      * The given value is in units of sample values, not in bytes.
      *
      * <h4>Case of multi-pixels packed image</h4>
-     * It is caller's responsibility to ensure that <var>n</var> is a multiple of {@link #samplesPerElement}
+     * It is caller's responsibility to ensure that <var>n</var> is a multiple of {@link #pixelsPerElement}
      * if this method is not invoked for skipping all remaining values until end of row.
      * See {@link Inflater#skip(long)} for more information.
      *
@@ -145,12 +151,12 @@ abstract class CopyFromBytes extends Inflater {
             }
             /*
              * Convert number of sample values to number of elements, then to number of bytes.
-             * The number of sample values to skip shall be a multiple of `samplesPerElement`,
+             * The number of sample values to skip shall be a multiple of `pixelsPerElement`,
              * except when skipping all remaining values until end of row. We do not verify
              * because this method does not know when the row ends.
              */
-            final boolean r = (n % samplesPerElement) > 0;
-            n /= samplesPerElement; if (r) n++;             // Round after division to next element boundary.
+            final boolean r = (n % pixelsPerElement) > 0;
+            n /= pixelsPerElement; if (r) n++;              // Round after division to next element boundary.
             n *= bytesPerElement;                           // Must be multiplied only after above rounding.
             streamPosition = Math.addExact(streamPosition, n);
         }
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
index b158ed5..2c81c28 100644
--- 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
@@ -84,31 +84,31 @@ public abstract class Inflater {
      *
      * <h4>Note on sample definition</h4>
      * A "sample" is usually a primitive type such as a byte or a float, but may be a unit smaller than a byte
-     * (e.g. 1 bit) if {@code samplesPerElement} is greater than 1. In that case, the {@link #elementsPerChunk}
-     * and {@link #skipAfterChunks} values will be divided by {@code samplesPerElement}.
+     * (e.g. 1 bit) if {@code pixelsPerElement} is greater than 1. In that case, the {@link #elementsPerChunk}
+     * and {@link #skipAfterChunks} values will be divided by {@code pixelsPerElement}.
      *
-     * @param  input              the source of data to decompress.
-     * @param  chunksPerRow       number of chunks (usually pixels) per row in target image. Must be strictly positive.
-     * @param  samplesPerChunk    number of sample values per chunk (sample or pixel). Must be strictly positive.
-     * @param  skipAfterChunks    number of sample values to skip between chunks. May be empty or null.
-     * @param  samplesPerElement  number of sample values per primitive element. Always 1 except for multi-pixels packed images.
-     * @param  maxChunkSize       maximal value (in number of samples) for the {@link #elementsPerChunk} field.
+     * @param  input             the source of data to decompress.
+     * @param  chunksPerRow      number of chunks (usually pixels) per row in target image. Must be strictly positive.
+     * @param  samplesPerChunk   number of sample values per chunk (sample or pixel). Must be strictly positive.
+     * @param  skipAfterChunks   number of sample values to skip between chunks. May be empty or null.
+     * @param  pixelsPerElement  number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+     * @param  maxChunkSize      maximal value (in number of samples) for the {@link #elementsPerChunk} field.
      */
     protected Inflater(final ChannelDataInput input, int chunksPerRow, int samplesPerChunk,
-                       int[] skipAfterChunks, final int samplesPerElement, final int maxChunkSize)
+                       int[] skipAfterChunks, final int pixelsPerElement, final int maxChunkSize)
     {
         this.input = input;
-        ArgumentChecks.ensureStrictlyPositive("chunksPerRow",      chunksPerRow);
-        ArgumentChecks.ensureStrictlyPositive("samplesPerChunk",   samplesPerChunk);
-        ArgumentChecks.ensureStrictlyPositive("samplesPerElement", samplesPerElement);
+        ArgumentChecks.ensureStrictlyPositive("chunksPerRow",     chunksPerRow);
+        ArgumentChecks.ensureStrictlyPositive("samplesPerChunk",  samplesPerChunk);
+        ArgumentChecks.ensureStrictlyPositive("pixelsPerElement", pixelsPerElement);
         if (skipAfterChunks != null) {
-            if (samplesPerElement != 1) {
+            if (pixelsPerElement != 1) {
                 skipAfterChunks = skipAfterChunks.clone();
                 for (int i=0; i<skipAfterChunks.length; i++) {
                     final int s = skipAfterChunks[i];
                     ArgumentChecks.ensurePositive("skipAfterChunks", s);
-                    ArgumentChecks.ensureMultiple("skipAfterChunks", samplesPerElement, s);
-                    skipAfterChunks[i] /= samplesPerElement;
+                    ArgumentChecks.ensureMultiple("skipAfterChunks", pixelsPerElement, s);
+                    skipAfterChunks[i] /= pixelsPerElement;
                 }
             }
         } else {
@@ -124,8 +124,8 @@ public abstract class Inflater {
                 int i = Arrays.binarySearch(divisors, Numerics.ceilDiv(samplesPerChunk, maxChunkSize));
                 if (i < 0) i = ~i;      // No need for array bound check.
                 /*
-                 * Following loop iterates exactly once unless `samplesPerElement` > 1.
-                 * The intend is to ensure that `samplesPerChunk` is also a divisor of `samplesPerElement`.
+                 * Following loop iterates exactly once unless `pixelsPerElement` > 1.
+                 * The intend is to ensure that `samplesPerChunk` is also a divisor of `pixelsPerElement`.
                  * If we can not find such value, current implementation will fail at `ensureDivisor` call.
                  *
                  * TODO: to avoid this problem, one possible approach could be to force `maxChunkSize` to be
@@ -133,7 +133,7 @@ public abstract class Inflater {
                  */
                 do {
                     chunksPerRow = divisors[i];
-                    if ((samplesPerChunk % (chunksPerRow * samplesPerElement)) == 0) break;
+                    if ((samplesPerChunk % (chunksPerRow * pixelsPerElement)) == 0) break;
                 } while (++i < divisors.length);
                 samplesPerChunk /= chunksPerRow;
             }
@@ -144,9 +144,9 @@ public abstract class Inflater {
          * element boundary.
          */
         if (chunksPerRow != 1) {
-            ArgumentChecks.ensureDivisor("samplesPerElement", samplesPerChunk, samplesPerElement);
+            ArgumentChecks.ensureDivisor("pixelsPerElement", samplesPerChunk, pixelsPerElement);
         }
-        this.elementsPerChunk = ceilDiv(samplesPerChunk, samplesPerElement);
+        this.elementsPerChunk = ceilDiv(samplesPerChunk, pixelsPerElement);
         this.skipAfterChunks  = skipAfterChunks;
         this.chunksPerRow     = chunksPerRow;
     }
@@ -157,26 +157,26 @@ public abstract class Inflater {
      *
      * <h4>Note on sample definition</h4>
      * A "sample" is usually a primitive type such as a byte or a float, but may be a unit smaller than a byte
-     * (e.g. 1 bit) if {@code samplesPerElement} is greater than 1. If that case, the {@link #elementsPerChunk}
-     * and {@link #skipAfterChunks} values will be divided by {@code samplesPerElement}.
+     * (e.g. 1 bit) if {@code pixelsPerElement} is greater than 1. If that case, the {@link #elementsPerChunk}
+     * and {@link #skipAfterChunks} values will be divided by {@code pixelsPerElement}.
      *
-     * @param  compression        the compression method.
-     * @param  input              the source of data to decompress.
-     * @param  start              stream position where to start reading.
-     * @param  byteCount          number of bytes to read before decompression.
-     * @param  sourceWidth        number of pixels in a row of source image.
-     * @param  chunksPerRow       number of chunks (usually pixels) per row in target image. Must be strictly positive.
-     * @param  samplesPerChunk    number of sample values per chunk (sample or pixel). Must be strictly positive.
-     * @param  skipAfterChunks    number of sample values to skip between chunks. May be empty or null.
-     * @param  samplesPerElement  number of sample values per primitive element. Always 1 except for multi-pixels packed images.
-     * @param  banks              where to store sample values.
+     * @param  compression       the compression method.
+     * @param  input             the source of data to decompress.
+     * @param  start             stream position where to start reading.
+     * @param  byteCount         number of bytes to read before decompression.
+     * @param  sourceWidth       number of pixels in a row of source image.
+     * @param  chunksPerRow      number of chunks (usually pixels) per row in target image. Must be strictly positive.
+     * @param  samplesPerChunk   number of sample values per chunk (sample or pixel). Must be strictly positive.
+     * @param  skipAfterChunks   number of sample values to skip between chunks. May be empty or null.
+     * @param  pixelsPerElement  number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+     * @param  banks             where to store sample values.
      * @return the inflater for the given targe type, or {@code null} if the compression method is unknown.
      * @throws IOException if an I/O operation was required and failed.
      */
     public static Inflater create(final Compression compression,
             final ChannelDataInput input, final long start, final long byteCount, final int sourceWidth,
             final int chunksPerRow, final int samplesPerChunk, final int[] skipAfterChunks,
-            final int samplesPerElement, final Buffer banks)
+            final int pixelsPerElement, final Buffer banks)
             throws IOException
     {
         ArgumentChecks.ensureNonNull("input", input);
@@ -184,14 +184,14 @@ public abstract class Inflater {
         final InflaterChannel inflated;
         switch (compression) {
             case NONE: {
-                return CopyFromBytes.create(input, start, chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, banks);
+                return CopyFromBytes.create(input, start, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, banks);
             }
             case PACKBITS: inflated = new PackBits(input, start, byteCount); break;
             case CCITTRLE: inflated = new CCITTRLE(input, start, byteCount, sourceWidth); break;
             default: return null;
         }
         return CopyFromBytes.create(inflated.createDataInput(), 0,
-                chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, banks);
+                chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, banks);
     }
 
     /**
@@ -205,17 +205,17 @@ public abstract class Inflater {
      * one byte for the first 8 bits, then 2 bits rounded to the next byte boundary.
      *
      * <p>The exact rounding mode depends on the compression algorithm. To avoid erratic behavior, callers shall
-     * restrict <var>n</var> to multiples of {@code samplesPerElement} before to read the first pixel in a row.
+     * restrict <var>n</var> to multiples of {@code pixelsPerElement} before to read the first pixel in a row.
      * This restriction does not apply when skipping remaining data after the last pixels in a row, because the
      * next row will be realigned on an element boundary anyway. The way to perform this realignment depends on
      * the compression, which is the reason why the exact rounding mode may vary with compression algorithms.</p>
      *
-     * <p>Restricting <var>n</var> to multiples of {@code samplesPerElement} implies the following restrictions:</p>
+     * <p>Restricting <var>n</var> to multiples of {@code pixelsPerElement} implies the following restrictions:</p>
      * <ul>
      *   <li>No subsampling on the <var>x</var> axis (however subsampling on other axes is still allowed).</li>
      *   <li>No band subset if interleaved sample model, because the effect is similar to subsampling.</li>
      *   <li>If {@link org.apache.sis.coverage.grid.GridDerivation} is used for computing the sub-region to read,
-     *       it should have been configured with {@code chunkSize(samplesPerElement)}
+     *       it should have been configured with {@code chunkSize(pixelsPerElement)}
      *       (unless chunk size is already used for tile size).</li>
      * </ul>
      *
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
index bf62d2d..93be17b 100644
--- 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
@@ -19,7 +19,6 @@ package org.apache.sis.storage.geotiff;
 import java.io.IOException;
 import java.nio.Buffer;
 import java.awt.Point;
-import java.awt.image.RasterFormatException;
 import java.awt.image.WritableRaster;
 import org.apache.sis.internal.storage.TiledGridResource;
 import org.apache.sis.internal.coverage.j2d.RasterFactory;
@@ -28,7 +27,6 @@ import org.apache.sis.internal.geotiff.Inflater;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.image.DataType;
 
 import static java.lang.Math.toIntExact;
@@ -201,7 +199,6 @@ final class CompressedSubset extends DataSubset {
                              final int[] subsampling, final Point location) throws IOException, DataStoreException
     {
         final DataType type     = getDataType();
-        final int  typeSize     = type.size();
         final int  width        = pixelCount(lower, upper, subsampling, 0);
         final int  height       = pixelCount(lower, upper, subsampling, 1);
         final int  chunksPerRow = width * (targetPixelStride / samplesPerChunk);
@@ -213,30 +210,26 @@ final class CompressedSubset extends DataSubset {
          * `betweenPixels` is the number of sample values to skip between each pixel, but the actual skips
          * are more complicated if only a subset of the bands are read. The actual number of sample values
          * to skip between "chunks" is detailed by `skipAfterChunks`.
+         *
+         * `pixelsPerElement` below is a factor for converting a count of pixels to a count of primitive elements
+         * in the bank. The `pixelsPerElement` factor is usually 1, except when more than one pixel is packed in
+         * each single primitive type (e.g. 8 bits per byte in bilevel image). The `head` needs to be a multiple
+         * of `pixelsPerElement`; this restriction is documented in `Inflater.skip(long)` and should have been
+         * verified by `TiledGridResource`.
          */
+        final int pixelsPerElement = getPixelsPerElement();                 // Always ≥ 1 and usually = 1.
+        assert (head % pixelsPerElement) == 0 : head;
+        final int capacity = getBankCapacity(pixelsPerElement);
         final Buffer[] banks = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
             /*
-             * Factor for converting a count of sample values to a count of primitive elements in the bank.
-             * The `samplesPerElement` factor is usually 1, except when more than one pixel is packed
-             * in each single primitive type (e.g. 8 bits per byte in bilevel image).
-             */
-            final int sampleSize = getSampleSize(b);                    // Size in bits of sample values.
-            final int samplesPerElement = typeSize / sampleSize;        // Always ≥ 1 and usually = 1.
-            if (typeSize % sampleSize != 0) {
-                throw new RasterFormatException(reader().errors().getString(
-                        Errors.Keys.NotADivisorOrMultiple_4, "BitsPerSample", 0, typeSize, sampleSize));
-            }
-            // TiledGridResource shall ensure that following `Inflater.skip(long)` restriction is met.
-            assert (head % samplesPerElement) == 0 : head;
-            /*
              * Prepare the object which will perform the actual decompression row-by-row,
              * optionally skipping chunks if a subsampling is applied.
              */
-            final Buffer bank = RasterFactory.createBuffer(type, getBankCapacity(samplesPerElement));
+            final Buffer bank = RasterFactory.createBuffer(type, capacity);
             final Inflater algo = Inflater.create(compression, reader().input, offsets[b], byteCounts[b],
                                     getTileSize(0), chunksPerRow, samplesPerChunk, skipAfterChunks,
-                                    samplesPerElement, bank);
+                                    pixelsPerElement, bank);
             if (algo == null) {
                 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/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index c5b4a7c..79a2bc4 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
@@ -140,7 +140,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
         final SampleModel model = getSampleModel();
         int b = model.getNumBands();
         if (b != 1 && !(model instanceof BandedSampleModel)) {              // First condition (see Javadoc).
-            if (subset.hasBandSubset() || subset.hasSubsampling(0)) {       // Exception to first consition.
+            if (!subset.isXContiguous()) {                                  // Exception to first consition.
                 return false;
             }
         }
@@ -165,7 +165,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
         GridCoverage coverage;
         try {
             synchronized (getSynchronizationLock()) {
-                final Subset subset = new Subset(domain, range, false);
+                final Subset subset = new Subset(domain, range);
                 final Compression compression = getCompression();
                 if (compression == null) {
                     throw new DataStoreContentException(reader.resources().getString(
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 9657b38..fcbfcaf 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
@@ -22,7 +22,6 @@ import java.nio.Buffer;
 import java.io.IOException;
 import java.awt.Point;
 import java.awt.image.BandedSampleModel;
-import java.awt.image.ComponentSampleModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.WritableRaster;
 import org.apache.sis.image.DataType;
@@ -195,37 +194,16 @@ class DataSubset extends TiledGridCoverage implements Localized {
     }
 
     /**
-     * Returns the size in bits of samples for the specified <em>bank</em> (not band).
-     * If the sample model packs all pixel samples in a single bank element, then this
-     * method returns the sum of the size of sample sizes for all bands.
-     *
-     * @param  bank  the bank for which to get sample size.
-     * @return the size of the samples of the specified bank.
-     *
-     * @see java.awt.image.SampleModel#getSampleSize(int)
-     */
-    protected final int getSampleSize(final int bank) {
-        if (model instanceof ComponentSampleModel) {
-            return DataBuffer.getDataTypeSize(model.getDataType());
-        }
-        int size = 0;
-        for (int b = model.getNumBands(); --b >= 0;) {
-            size += model.getSampleSize(b);
-        }
-        return size;
-    }
-
-    /**
      * Returns the size of a bank (not necessarily a band) in number of primitive elements (bytes, integers, …).
      * This is tile width × height × {@link #targetPixelStride} divided by the number of sample values per element,
      * with each row starting on an element boundary.
      *
-     * @param  samplesPerElement  always 1 except when two or more pixels are packed in each element.
+     * @param  pixelsPerElement  always 1 except when two or more pixels are packed in each element.
      * @return expected number of primitive elements in the bank.
      */
-    protected final int getBankCapacity(final int samplesPerElement) {
+    protected final int getBankCapacity(final int pixelsPerElement) {
         // `ceilDiv(…)` must happen before multiplication by image height.
-        final int scanlineStride = ceilDiv(multiplyExact(model.getWidth(), targetPixelStride), samplesPerElement);
+        final int scanlineStride = ceilDiv(multiplyExact(model.getWidth(), targetPixelStride), pixelsPerElement);
         return multiplyExact(scanlineStride, model.getHeight());
     }
 
@@ -407,6 +385,8 @@ class DataSubset extends TiledGridCoverage implements Localized {
      * @throws DataStoreException if a logical error occurred.
      * @throws RuntimeException if the Java2D image can not be created for another reason
      *         (too many exception types to list them all).
+     *
+     * @see DataCube#canReadDirect(TiledGridResource.Subset)
      */
     WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
                              final int[] subsampling, final Point location) throws IOException, DataStoreException
@@ -432,7 +412,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
         lower[0] *= sourcePixelStride;
         upper[0] *= sourcePixelStride;
         /*
-         * Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
+         * Read each plane ("banks" in Java2D terminology). Note that a single bank contains all bands
          * in the interleaved sample model case. This block assumes that each bank element contains
          * exactly one sample value (verified by assertion), as documented in the Javadoc of this method.
          * If that assumption was not true, we would have to adjust `capacity`, `lower[0]` and `upper[0]`
@@ -447,7 +427,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
                         Resources.Keys.UnexpectedTileLength_2, length, byteCounts[b]));
             }
             hr.setOrigin(offsets[b]);
-            assert getSampleSize(b) == sampleSize;                              // See above comment.
+            assert model.getSampleSize(b) == sampleSize;                        // See above comment.
             final Buffer bank = hr.readAsBuffer(region, getBankCapacity(1));
             fillRemainingRows(bank);
             banks[b] = bank;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index 6ceb19a..8de9830 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -19,8 +19,11 @@ package org.apache.sis.internal.storage;
 import java.util.Map;
 import java.util.Locale;
 import java.io.IOException;
+import java.awt.Point;
+import java.awt.image.DataBuffer;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import org.opengis.coverage.CannotEvaluateException;
@@ -292,6 +295,50 @@ public abstract class TiledGridCoverage extends GridCoverage {
     }
 
     /**
+     * Returns the number of pixels in a single bank element. This is usually 1, except for
+     * {@link MultiPixelPackedSampleModel} which packs many pixels in a single bank element.
+     * This value is a power of 2 according {@code MultiPixelPackedSampleModel} specification.
+     *
+     * <div class="note"><b>Note:</b>
+     * this is "pixels per element", not "samples per element". It makes a difference in the
+     * {@link java.awt.image.SinglePixelPackedSampleModel} case, for which this method returns 1
+     * (by contrast a "samples per element" would give a value greater than 1).
+     * But this value can nevertheless be understood as a "samples per element" value
+     * where only one band is considered at a time.</div>
+     *
+     * @return number of pixels in a single bank element. Usually 1.
+     *
+     * @see SampleModel#getSampleSize(int)
+     * @see MultiPixelPackedSampleModel#getPixelBitStride()
+     */
+    protected final int getPixelsPerElement() {
+        return getPixelsPerElement(model);
+    }
+
+    /**
+     * Implementation of {@link #getPixelsPerElement()}.
+     *
+     * @param  model  the sample model from which to infer the number of pixels per bank element.
+     * @return number of pixels in a single bank element. Usually 1.
+     */
+    static int getPixelsPerElement(final SampleModel model) {
+        if (model instanceof MultiPixelPackedSampleModel) {
+            /*
+             * The following code performs the same computation than `MultiPixelPackedSampleModel`
+             * constructor when computing its package-private field `pixelsPerDataElement`.
+             * That constructor ensured that `sampleSize` is a divisor of `typeSize`.
+             */
+            final int typeSize   = DataBuffer.getDataTypeSize(model.getDataType());
+            final int sampleSize = ((MultiPixelPackedSampleModel) model).getPixelBitStride();
+            final int pixelsPerElement = typeSize / sampleSize;
+            if (pixelsPerElement > 0) {                         // Paranoiac check.
+                return pixelsPerElement;
+            }
+        }
+        return 1;
+    }
+
+    /**
      * Returns a two-dimensional slice of grid data as a rendered image.
      *
      * @param  sliceExtent  a subspace of this grid coverage extent, or {@code null} for the whole image.
@@ -478,10 +525,15 @@ public abstract class TiledGridCoverage extends GridCoverage {
         public WritableRaster getCachedTile() {
             WritableRaster tile = rasters.get(createCacheKey(indexInTileVector));
             if (tile != null) {
-                // Found a tile. Make sure that the raster starts at the expected coordinates.
+                /*
+                 * Found a tile, but the sample model may be different because band order may be different.
+                 * In both cases, we need to make sure that the raster starts at the expected coordinates.
+                 */
                 final int x = getTileOrigin(0);
                 final int y = getTileOrigin(1);
-                if (tile.getMinX() != x || tile.getMinY() != y) {
+                if (!model.equals(tile.getSampleModel())) {
+                    tile = WritableRaster.createWritableRaster(model, tile.getDataBuffer(), new Point(x, y));
+                } else if (tile.getMinX() != x || tile.getMinY() != y) {
                     tile = tile.createWritableTranslatedChild(x, y);
                 }
             }
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 45df226..e5d4c10 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
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.awt.image.DataBuffer;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
 import java.awt.image.ComponentSampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
@@ -148,26 +149,47 @@ public abstract class TiledGridResource extends AbstractGridResource {
      * By declaring an "atom" size of 8 sample values in dimension 0, the {@link Subset} constructor
      * will ensure than the sub-region to read starts at a byte boundary when reading a bilevel image.
      *
+     * <p>The default implementation returns the {@linkplain TiledGridCoverage#getPixelsPerElement()
+     * number of pixels per data element} for dimension 0 and returns 1 for all other dimensions.</p>
+     *
      * @param  dimension  the dimension for which to get the atom size.
      *         This is in units of sample values (may be bits, bytes, floats, <i>etc</i>).
      * @return indivisible amount of sample values to read in the specified dimension. Must be ≥ 1.
      * @throws DataStoreException if an error occurred while fetching the sample model.
      */
-    private int getAtomSize(final int dimension) throws DataStoreException {
-        if (dimension == 0) {
-            final SampleModel model = getSampleModel();
-            if (model != null && !(model instanceof ComponentSampleModel)) {
-                int size = 1;
-                for (int b = model.getNumBands(); --b >= 0;) {
-                    size = Math.max(size, model.getSampleSize(b));
-                }
-                final int samplesPerElement = DataBuffer.getDataTypeSize(model.getDataType()) / size;
-                if (samplesPerElement >= 1) {
-                    return samplesPerElement;
-                }
-            }
-        }
-        return 1;
+    protected int getAtomSize(final int dimension) throws DataStoreException {
+        return (dimension == 0) ? TiledGridCoverage.getPixelsPerElement(getSampleModel()) : 1;
+    }
+
+    /**
+     * Returns {@code true} if the reader can load only the requested bands and skip the other bands,
+     * or {@code false} if the reader must load all bands. This value controls the amount of data to
+     * be loaded by {@link #read(GridGeometry, int...)}:
+     *
+     * <ul class="verbose">
+     *   <li>If {@code false}, then {@link TiledGridCoverage#model} will expect the same {@link DataBuffer}
+     *       than the one expected by the {@linkplain #getSampleModel() sample model of this resource}.
+     *       All bands will be loaded but the coverage sample model will ignore the bands that were not
+     *       enumerated in the {@code range} argument. This strategy is convenient when skipping bands
+     *       at reading time is hard.</li>
+     *   <li>If {@code true}, then {@link TiledGridCoverage#model} will have its band indices and bit masks
+     *       "compressed" for making them consecutive. For example if the {@code range} argument specifies that
+     *       the bands to read are {1, 3, 4, 6}, then after "compression" band indices become {0, 1, 2, 3}.
+     *       This strategy is efficient when the reader is capable to skip bands at reading time.</li>
+     * </ul>
+     *
+     * <p>The default implementation returns {@code true} if the sample model is a {@link ComponentSampleModel}
+     * and {@code false} if all other cases, because skipping bands in a packed sample model is more difficult
+     * to implement.</p>
+     *
+     * @return {@code true} if the reader can load only the requested bands and skip other bands, or
+     *         {@code false} if the reader needs to load all bands regardless the {@code range} subset.
+     * @throws DataStoreException if an error occurred while fetching the sample model.
+     *
+     * @see AbstractGridResource.RangeArgument#select(SampleModel, boolean)
+     */
+    protected boolean getDissociableBands() throws DataStoreException {
+        return getSampleModel() instanceof ComponentSampleModel;
     }
 
     /**
@@ -178,6 +200,9 @@ public abstract class TiledGridResource extends AbstractGridResource {
      * If this resource has more than 2 dimensions, then this model is for the two first ones (usually horizontal).
      * The images for all levels in additional dimensions shall use the same sample model.
      *
+     * <h4>Performance note</h4>
+     * Implementation should return a cached value, because this method may be invoked many times.
+     *
      * @return the sample model for tiles at full resolution with all their bands.
      * @throws DataStoreException if an error occurred during sample model construction.
      */
@@ -228,8 +253,12 @@ public abstract class TiledGridResource extends AbstractGridResource {
         final GridGeometry domain;
 
         /**
-         * Sample dimensions for each image band.
-         * This is the range of the grid coverage to create.
+         * Sample dimensions for each image band. This is the range of the grid coverage to create.
+         * If {@link #includedBands} is non-null, then the the size of this list should be equal to
+         * {@link #includedBands} array length. However bands are not necessarily in the same order:
+         * the order of bands in this {@code ranges} list is the order specified by user, while the
+         * order of bands in {@link #includedBands} is always increasing index order for efficiency
+         * reasons.
          */
         final List<? extends SampleDimension> ranges;
 
@@ -294,10 +323,6 @@ public abstract class TiledGridResource extends AbstractGridResource {
          *
          * @param  domain  the domain argument specified by user in a call to {@code GridCoverageResource.read(…)}.
          * @param  range   the range argument specified by user in a call to {@code GridCoverageResource.read(…)}.
-         * @param  loadAllBands {@code true} if the reader will load all bands regardless the {@code range} subset,
-         *         or {@code false} if only requested bands will be loaded and other bands will be skipped. See
-         *         {@link AbstractGridResource.RangeArgument#select(SampleModel, boolean) RangeArgument.select(…)}
-         *         for more information.
          *
          * @throws ArithmeticException if pixel indices exceed 64 bits integer capacity.
          * @throws DataStoreException if a call to {@link TiledGridResource} method failed.
@@ -305,7 +330,7 @@ public abstract class TiledGridResource extends AbstractGridResource {
          * @throws IllegalArgumentException if an error occurred in an operation
          *         such as creating the {@code SampleModel} subset for selected bands.
          */
-        public Subset(GridGeometry domain, final int[] range, final boolean loadAllBands) throws DataStoreException {
+        public Subset(GridGeometry domain, final int[] range) throws DataStoreException {
             List<SampleDimension> bands        = getSampleDimensions();
             final RangeArgument   rangeIndices = validateRangeArgument(bands.size(), range);
             final GridGeometry    gridGeometry = getGridGeometry();
@@ -343,16 +368,20 @@ public abstract class TiledGridResource extends AbstractGridResource {
              * be handled by the `SampleModel`, not in `includedBands` array.
              */
             int[] includedBands = null;
-            if (!rangeIndices.isIdentity()) {
+            boolean loadAllBands = rangeIndices.isIdentity();
+            if (!loadAllBands) {
                 bands = Arrays.asList(rangeIndices.select(bands));
-                includedBands = new int[rangeIndices.getNumBands()];
-                for (int i=0; i<includedBands.length; i++) {
-                    includedBands[i] = rangeIndices.getSourceIndex(i);
-                }
-                assert ArraysExt.isSorted(includedBands, true);
-                if (rangeIndices.hasAllBands) {
-                    assert ArraysExt.isRange(0, includedBands);
-                    includedBands = null;
+                loadAllBands = !getDissociableBands();
+                if (!loadAllBands) {
+                    includedBands = new int[rangeIndices.getNumBands()];
+                    for (int i=0; i<includedBands.length; i++) {
+                        includedBands[i] = rangeIndices.getSourceIndex(i);
+                    }
+                    assert ArraysExt.isSorted(includedBands, true);
+                    if (rangeIndices.hasAllBands) {
+                        assert ArraysExt.isRange(0, includedBands);
+                        includedBands = null;
+                    }
                 }
             }
             this.domain              = domain;
@@ -369,24 +398,22 @@ public abstract class TiledGridResource extends AbstractGridResource {
         }
 
         /**
-         * Returns {@code true} if this subset contains a subsampling in the given dimension.
+         * Returns {@code true} if reading data in this subset will read contiguous values on the <var>x</var> axis.
+         * This method returns {@code true} if all following conditions are met:
          *
-         * @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;
-        }
-
-        /**
-         * Returns {@code true} if the user requested a subset of bands, or {@code false}
-         * if all bands shall be read in order. For this method, a change of band order
-         * is considered as a "subset".
+         * <ul>
+         *   <li>All bands will be read (ignoring change of band order
+         *       because this change is handled by the sample model).</li>
+         *   <li>There is no subsampling on the <var>x</var> axis.</li>
+         * </ul>
+         *
+         * Note that the first criterion can often be relaxed when the sample model is an instance
+         * of {@link BandedSampleModel}. This method does not check the sample model type.
          *
-         * @return {@code true} if only a subset of bands will be read or if bands will be read out of order.
+         * @return whether the values to read on a row are contiguous.
          */
-        public boolean hasBandSubset() {
-            return includedBands != null;
+        public boolean isXContiguous() {
+            return includedBands == null && subsampling[0] == 1;
         }
     }