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 b925009bef9fedeac029dcee5a53ab022d150935
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Feb 26 15:29:00 2025 +0100

    Make color model and sample model builders more flexible in preparation for 
GeoHEIF reader.
---
 .../main/org/apache/sis/image/DataType.java        |   5 +-
 .../main/org/apache/sis/image/MaskImage.java       |   2 +-
 .../main/org/apache/sis/image/RecoloredImage.java  |   3 +-
 .../apache/sis/image/privy/ColorModelBuilder.java  | 309 ++++++++++++++-------
 .../apache/sis/image/privy/ColorModelFactory.java  |  35 ++-
 .../apache/sis/image/privy/SampleModelBuilder.java |  35 ++-
 .../org/apache/sis/image/BandSelectImageTest.java  |   2 +-
 .../sis/image/privy/SampleModelBuilderTest.java    |  22 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   9 +-
 .../org/apache/sis/storage/AbstractResource.java   |  10 +-
 .../apache/sis/storage/base/TiledGridCoverage.java |   6 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |   2 +-
 .../org/apache/sis/storage/gdal/TiledResource.java |   2 +-
 13 files changed, 302 insertions(+), 140 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java
index 9b4fa8d71a..4ea76572c5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java
@@ -338,14 +338,13 @@ public enum DataType {
 
     /**
      * Returns the size in bytes of this data type.
-     * If the {@linkplain #size() number of bits} is smaller than {@value 
Byte#SIZE}, then this method returns 1.
      *
-     * @return size in bytes of this data type, not smaller than 1.
+     * @return size in bytes of this data type, from 1 to 4 inclusive.
      *
      * @since 1.3
      */
     public final int bytes() {
-        return Math.max(size() >>> 3, 1);
+        return size() >>> 3;        // `size()` is never smaller than 8.
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java
index 18506461dd..ba6ea1262b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java
@@ -48,7 +48,7 @@ final class MaskImage extends SourceAlignedImage {
      * Creates a new instance for the given image.
      */
     MaskImage(final ResampledImage image) {
-        super(image, ColorModelFactory.createIndexColorModel(
+        super(image, ColorModelFactory.createIndexColorModel(null, 0,
                 1, Math.max(0, ImageUtilities.getVisibleBand(image)), new 
int[] {0, -1}, true, 0));
 
         MathTransform converter = null;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java
index ca0297e3c4..7d5b8742b2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java
@@ -342,7 +342,8 @@ final class RecoloredImage extends ImageAdapter {
                 ARGB[i] = icm.getRGB(Math.round((i - start) * scale) + 
validMin);
             }
             final SampleModel sm = source.getSampleModel();
-            cm = ColorModelFactory.createIndexColorModel(sm.getNumBands(), 
visibleBand, ARGB, icm.hasAlpha(), icm.getTransparentPixel());
+            cm = ColorModelFactory.createIndexColorModel(null, 0, 
sm.getNumBands(),
+                    visibleBand, ARGB, icm.hasAlpha(), 
icm.getTransparentPixel());
         } else {
             /*
              * Wraps the given image with its colors ramp scaled between the 
given bounds. If the given image is
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java
index 89988e5d4d..830b8a9bee 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java
@@ -16,12 +16,10 @@
  */
 package org.apache.sis.image.privy;
 
-import java.util.Arrays;
 import java.awt.Transparency;
 import java.awt.color.ColorSpace;
 import java.awt.image.ColorModel;
 import java.awt.image.DirectColorModel;
-import java.awt.image.BandedSampleModel;
 import java.awt.image.ComponentColorModel;
 import java.awt.image.SinglePixelPackedSampleModel;
 import java.awt.image.DataBuffer;
@@ -29,6 +27,7 @@ import java.awt.image.SampleModel;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.image.DataType;
+import org.apache.sis.measure.NumberRange;
 
 
 /**
@@ -48,21 +47,69 @@ import org.apache.sis.image.DataType;
  */
 public final class ColorModelBuilder {
     /**
-     * The usual position of the alpha channel.
-     * This channel is usually immediately after the <abbr>RGB</abbr> 
components.
+     * Sentinel value for saying that {@link #alphaBand} has not been set.
+     * This is different than setting {@code alphaBand} to -1, which means "no 
alpha".
      */
-    private static final int STANDARD_ALPHA_BAND = 3;
+    private static final int UNSPECIFIED_ALPHA = -2;
+
+    /**
+     * The color space of the color model to build.
+     * The default value is a <abbr>RGB</abbr> color space.
+     *
+     * <h4>Limitation</h4>
+     * Note that {@link #createPackedRGB()} is limited to color spaces of the 
<abbr>RGB</abbr> family.
+     * This is a restriction of {@link DirectColorModel}.
+     */
+    private final ColorSpace colorSpace;
+
+    /**
+     * The data type, or {@code null} for automatic.
+     *
+     * @see #dataType(DataType)
+     * @see #dataType(int)
+     */
+    private DataType dataType;
 
     /**
      * Number of bits per sample, or {@code null} for the default values in 
all bands.
      * The array length is the number of bands: 3 for <abbr>RGB</abbr> or 4 
for <abbr>ARGB</abbr>.
      * Each array element may be 0, which means to use the default for that 
specific band.
      * If no number of bits is specified, the default is {@value Byte#SIZE}.
+     *
+     * @see #bitsPerSample(int[])
+     * @see #getBitsPerSample(int, int)
      */
     private int[] bitsPerSample;
 
     /**
-     * Index of the alpha channel (usually the last band), or -1 if none.
+     * Alternative to {@link #bitsPerSample} when all samples use the same 
number of bits.
+     * Stored separately because the number of bands may not be known.
+     * The default value is {@value Byte#SIZE}.
+     *
+     * @see #bitsPerSample(int)
+     * @see #getBitsPerSample(int, int)
+     */
+    private int defaultBitsPerSample;
+
+    /**
+     * The red, green, blue and alpha masks, in that order. This is used only 
for {@link DirectColorModel},
+     * where all sample values are packed in a single integer value.
+     *
+     * @see #componentMasks(int...)
+     */
+    private int[] componentMasks;
+
+    /**
+     * The band to show in the case of gray scale or indexed color model.
+     * The default value is {@value ColorModelFactory#DEFAULT_VISIBLE_BAND}.
+     *
+     * @see #visibleBand(int)
+     * @see ColorModelFactory#DEFAULT_VISIBLE_BAND
+     */
+    private int visibleBand;
+
+    /**
+     * Index of the alpha channel (usually the last band), or negative if none.
      */
     private int alphaBand;
 
@@ -72,10 +119,39 @@ public final class ColorModelBuilder {
     private boolean isAlphaPremultiplied;
 
     /**
-     * Creates a new builder initialized with default values.
+     * Range of the sample values in the {@link #visibleBand}, or {@code null} 
if unknown.
+     * This is used for the gray scale fallback if the <abbr>ARGN</abbr> color 
model cannot be created.
+     */
+    private NumberRange<?> sampleValuesRange;
+
+    /**
+     * Creates a new builder initialized with default values for an 
<abbr>RGB</abbr> color space.
      */
     public ColorModelBuilder() {
-        alphaBand = -1;
+        this(true);
+    }
+
+    /**
+     * Creates a new builder initialized with default values.
+     *
+     * @param  isRGB  {@code true} for an <abbr>RGB</abbr> color space, or 
{@code false} for gray scale.
+     */
+    public ColorModelBuilder(final boolean isRGB) {
+        colorSpace = ColorSpace.getInstance(isRGB ? ColorSpace.CS_sRGB : 
ColorSpace.CS_GRAY);
+        defaultBitsPerSample = Byte.SIZE;
+        alphaBand = UNSPECIFIED_ALPHA;
+    }
+
+    /**
+     * Sets the type of data. If this method is not invoked,
+     * the default is to determine the type automatically from the number of 
bits.
+     *
+     * @param  type  the data type to use, or {@code null} for automatic.
+     * @return {@code this} for method calls chaining.
+     */
+    public ColorModelBuilder dataType(final DataType type) {
+        dataType = type;
+        return this;
     }
 
     /**
@@ -88,9 +164,15 @@ public final class ColorModelBuilder {
      * @return {@code this} for method calls chaining.
      */
     public ColorModelBuilder bitsPerSample(final int[] numBits) {
-        bitsPerSample = numBits;
-        if (hasDefaultBitsPerSample()) {
-            bitsPerSample = null;
+        defaultBitsPerSample = Byte.SIZE;
+        bitsPerSample = null;
+        if (numBits != null) {
+            for (int n : numBits) {
+                if (n != Byte.SIZE) {
+                    bitsPerSample = numBits;
+                    break;
+                }
+            }
         }
         return this;
     }
@@ -105,29 +187,11 @@ public final class ColorModelBuilder {
      */
     public ColorModelBuilder bitsPerSample(final int numBits) {
         ArgumentChecks.ensureBetween("bitsPerSample", 1, Integer.SIZE, 
numBits);
-        if (numBits != Byte.SIZE) {
-            bitsPerSample = new int[STANDARD_ALPHA_BAND + 1];
-            Arrays.fill(bitsPerSample, numBits);
-        } else {
-            bitsPerSample = null;
-        }
+        defaultBitsPerSample = numBits;
+        bitsPerSample = null;
         return this;
     }
 
-    /**
-     * Returns whether this builder currently uses the default number of bits 
per sample.
-     */
-    private boolean hasDefaultBitsPerSample() {
-        if (bitsPerSample != null) {
-            for (int numBits : bitsPerSample) {
-                if (numBits != Byte.SIZE) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
     /**
      * Returns the number of bits per sample in the given band.
      * If no number of bits per sample was specified, the default value is 
{@link Byte#SIZE}.
@@ -145,7 +209,48 @@ public final class ColorModelBuilder {
                 return numBits;
             }
         }
-        return Byte.SIZE;
+        return defaultBitsPerSample;
+    }
+
+    /**
+     * Sets the red, green, blue and alpha masks, in that order. Those masks 
are used only for
+     * {@link DirectColorModel}, where all sample values are packed in a 
single integer value.
+     * Trailing zeros are trimmed, as a convenience for setting the mask of 
the alpha band to
+     * zero when there is no alpha band.
+     *
+     * <p>The given array may be stored without copy on the assumption that is 
will not be modified
+     * (this is okay for internal API). If no mask is specified, the default 
masks will be computed
+     * from the {@code bitsPerSample} values.</p>
+     *
+     * @param  masks  the red, green, blue and alpha masks (in that order), or 
{@code null} for defaults.
+     * @return {@code this} for method calls chaining.
+     */
+    public ColorModelBuilder componentMasks(int... masks) {
+        if (masks != null) {
+            for (int i = masks.length; --i >= 0;) {
+                if (masks[i] != 0) {
+                    masks = ArraysExt.resize(masks, i+1);
+                    break;
+                }
+            }
+        }
+        componentMasks = masks;
+        return this;
+    }
+
+    /**
+     * Sets the band to show in the case of gray scale or indexed color model.
+     * The default value is {@value ColorModelFactory#DEFAULT_VISIBLE_BAND}.
+     *
+     * @param  index  of the alpha channel (usually the first band).
+     * @param  range  range of the sample values, or {@code null} if unknown.
+     * @return {@code this} for method calls chaining.
+     */
+    public ColorModelBuilder visibleBand(final int index, final NumberRange<?> 
range) {
+        ArgumentChecks.ensurePositive("visibleBand", index);
+        visibleBand = index;
+        sampleValuesRange = range;
+        return this;
     }
 
     /**
@@ -156,10 +261,7 @@ public final class ColorModelBuilder {
      * @return {@code this} for method calls chaining.
      */
     public ColorModelBuilder alphaBand(final int index) {
-        if (index >= 0) {
-            ArgumentChecks.ensureBetween("alphaBand", 0, STANDARD_ALPHA_BAND, 
index);
-        }
-        alphaBand = index;
+        alphaBand = Math.max(index, UNSPECIFIED_ALPHA + 1);
         return this;
     }
 
@@ -169,28 +271,44 @@ public final class ColorModelBuilder {
      * @param  p  whether the alpha value (if present) is premultiplied.
      * @return {@code this} for method calls chaining.
      */
-    public ColorModelBuilder isAlphaPremultiplied(final boolean p) {
+    public ColorModelBuilder alphaPremultiplied(final boolean p) {
         isAlphaPremultiplied = p;
         return this;
     }
 
     /**
-     * Returns the color space of the color model to build.
-     * The current implementation fixes the color space to <abbr>RGB</abbr>,
-     * but it may change in a future implementation if needed.
+     * Returns the given array, or an empty array if the given array is null.
+     */
+    private static int[] orEmpty(final int[] numBits) {
+        return (numBits != null) ? numBits : ArraysExt.EMPTY_INT;
+    }
+
+    /**
+     * Returns the number of bands. If the user provided no indication,
+     * a default value suitable for <abbr>RGB(A)</abbr> colors is returned.
      *
-     * <h4>Limitation</h4>
-     * Note that {@link #createPackedRGB()} is limited to color spaces of the 
<abbr>RGB</abbr> family.
-     * This is a restriction of {@link DirectColorModel}.
+     * @return the number of bands.
      */
-    private static ColorSpace colorSpace() {
-        return ColorSpace.getInstance(ColorSpace.CS_sRGB);
+    private int numBands() {
+        int numBands = Math.max(orEmpty(bitsPerSample).length, 
orEmpty(componentMasks).length);
+        if (numBands == 0) {
+            numBands = colorSpace.getNumComponents();
+            if (alphaBand >= 0) {
+                ArgumentChecks.ensureBetween("alphaBand", 0, numBands, 
alphaBand);
+                numBands++;
+            }
+        }
+        return numBands;
     }
 
     /**
      * Returns the data type to use for the given number of bits.
+     * The number of bits is ignored if the user explicitly specified a data 
type.
      */
-    private static int dataType(final int numBits) {
+    private int dataType(final int numBits) {
+        if (dataType != null) {
+            return dataType.toDataBufferType();
+        }
         return (numBits <=  Byte.SIZE) ? DataBuffer.TYPE_BYTE :
                (numBits <= Short.SIZE) ? DataBuffer.TYPE_USHORT : 
DataBuffer.TYPE_INT;
     }
@@ -207,98 +325,103 @@ public final class ColorModelBuilder {
      * @throws IllegalArgumentException if any argument specified to the 
builder is invalid.
      */
     public ColorModel createPackedRGB() {
-        if (!isAlphaPremultiplied && alphaBand == STANDARD_ALPHA_BAND && 
hasDefaultBitsPerSample()) {
-            return ColorModel.getRGBdefault();
-        }
-        // Red, Green, Blue, Alpha masks in that order.
-        final int[] masks = new int[STANDARD_ALPHA_BAND + 1];
         int numBits = 0;
-        for (int i=STANDARD_ALPHA_BAND - 1; i >= -1; i--) {
-            int band = i;
-            if (band < 0) {
-                band = alphaBand;
-                if (band < 0) break;
+        int[] masks = componentMasks;                   // Red, Green, Blue, 
Alpha masks in that order.
+        if (masks == null) {
+            final int numBands = numBands();
+            if (numBands == 4 && alphaBand == 3 && !isAlphaPremultiplied) {
+                return ColorModel.getRGBdefault();      // Shared instance.
+            }
+            masks = new int[numBands];
+            for (int i=numBands; --i >= -1;) {
+                if (i != alphaBand) {
+                    int band = i;
+                    if (band < 0) {
+                        band = alphaBand;
+                        if (band < 0) break;
+                    }
+                    int n = getBitsPerSample(band, Math.min(Byte.SIZE, 
Integer.SIZE - numBits));
+                    masks[i] = ((1 << n) - 1) << numBits;
+                    numBits += n;
+                }
             }
-            int n = getBitsPerSample(band, Math.min(Byte.SIZE, Integer.SIZE - 
numBits));
-            masks[i] = ((1 << n) - 1) << numBits;
-            numBits += n;
+        } else {
+            for (int i = masks.length; --i >= 0;) numBits |= masks[i];
+            numBits = Integer.SIZE - Integer.numberOfLeadingZeros(numBits);
         }
-        return ColorModelFactory.unique(new DirectColorModel(colorSpace(),
-                numBits, masks[0], masks[1], masks[2], masks[3], 
isAlphaPremultiplied, dataType(numBits)));
+        return ColorModelFactory.unique(new DirectColorModel(colorSpace, 
numBits,
+                masks[0], masks[1], masks[2], masks[3], isAlphaPremultiplied, 
dataType(numBits)));
     }
 
     /**
-     * Creates a RGB color model for use with {@link BandedSampleModel}.
+     * Creates a RGB color model for use with {@link ComponentSampleModel}.
      * Each color component (sample value) is stored in a separated data 
element.
+     * Note that "banded" is taken in a loose sense here, as the data do not 
need
+     * to be stored in separated arrays as long as the components are distinct 
elements.
      *
      * <h4>Limitations</h4>
      * The current version requires the alpha channel (if present) to be the 
last band.
      * If this condition is not met, this method returns {@code null}.
      *
-     * @return color model for use with {@link 
java.awt.image.BandedSampleModel}.
+     * @return color model for use with {@link ComponentSampleModel}.
      * @throws IllegalArgumentException if any argument specified to the 
builder is invalid.
      */
     public ColorModel createBandedRGB() {
-        final int numBands;
-        final int transparency;
+        final int numBands = numBands();
         final boolean hasAlpha = (alphaBand >= 0);
-        if (hasAlpha) {
-            if (alphaBand != STANDARD_ALPHA_BAND) {
-                throw new IllegalArgumentException("Alpha channel must be 
after the color components.");
-            }
-            numBands = 4;
-            transparency = Transparency.TRANSLUCENT;
-        } else {
-            numBands = 3;
-            transparency = Transparency.OPAQUE;
+        if (hasAlpha && alphaBand != numBands - 1) {
+            throw new IllegalArgumentException("Alpha channel must be after 
the color components.");
         }
-        int[] numBits = bitsPerSample;
-        numBits = ArraysExt.resize(numBits != null ? numBits : 
ArraysExt.EMPTY_INT, numBands);
+        int[] numBits = ArraysExt.resize(orEmpty(bitsPerSample), numBands);
         int maxSize = 0;
         for (int i=0; i<numBands; i++) {
             if (numBits[i] == 0) {
                 if (numBits == bitsPerSample) {
                     numBits = numBits.clone();
                 }
-                numBits[i] = Byte.SIZE;
-            } else {
-                maxSize = Math.max(maxSize, numBits[i]);
+                numBits[i] = defaultBitsPerSample;
             }
+            maxSize = Math.max(maxSize, numBits[i]);
         }
-        return ColorModelFactory.unique(new ComponentColorModel(colorSpace(),
-                numBits, hasAlpha, isAlphaPremultiplied, transparency, 
dataType(maxSize)));
+        return ColorModelFactory.unique(new ComponentColorModel(colorSpace,
+                numBits, hasAlpha, isAlphaPremultiplied,
+                hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE,
+                dataType(maxSize)));
     }
 
     /**
-     * Creates a <abbr>RGB</abbr> color model for the given sample model.
-     * The sample model shall use integer type and have 3 or 4 bands.
-     * If no <abbr>RGB</abbr> or <abbr>ARGB</abbr> color model can be created,
-     * this method default on a gray scale color model.
+     * Creates a <abbr>RGB</abbr> or gray scale color model for the given 
sample model.
+     * If this method does not know how to create the requested color model,
+     * it defaults on a gray scale color model.
      *
      * @param  targetModel  the sample model for which to create a color model.
      * @return the <abbr>RGB</abbr> color model, or a gray scale color model 
as a fallback.
      * @throws IllegalArgumentException if any argument specified to the 
builder is invalid.
      */
     public ColorModel createRGB(final SampleModel targetModel) {
-check:  if (DataType.isInteger(targetModel)) {
+check:  if (DataType.isInteger(targetModel) && colorSpace.getType() == 
ColorSpace.TYPE_RGB) {
             final int numBands = targetModel.getNumBands();
-            switch (numBands) {
-                case 3:  alphaBand = -1; break;
-                case 4:  alphaBand = STANDARD_ALPHA_BAND; break;
-                default: break check;
+            if (alphaBand == UNSPECIFIED_ALPHA) {
+                switch (numBands) {
+                    case 3:  alphaBand = -1; break;
+                    case 4:  alphaBand =  3; break;
+                    default: break check;
+                }
+            }
+            if (bitsPerSample == null) {
+                bitsPerSample(targetModel.getSampleSize());
             }
-            bitsPerSample = targetModel.getSampleSize();
             if (targetModel.getNumDataElements() != 1) {
                 return createBandedRGB();
             } else {
                 for (int i=0; i<numBands; i++) {
-                    if (bitsPerSample[i] > Byte.SIZE) {
+                    if (getBitsPerSample(i, Integer.SIZE) > Byte.SIZE) {
                         break check;
                     }
                 }
                 return createPackedRGB();
             }
         }
-        return ColorModelFactory.createGrayScale(targetModel, 
ColorModelFactory.DEFAULT_VISIBLE_BAND, null);
+        return ColorModelFactory.createGrayScale(targetModel, visibleBand, 
sampleValuesRange);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java
index 0e4322ef10..d5945ed4fc 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java
@@ -361,10 +361,10 @@ public final class ColorModelFactory {
         final int categoryCount = pieceStarts.length - 1;
         if (numBands == 1 && categoryCount <= 0) {
             final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
-            final int[] nBits = {
+            final int[] numBits = {
                 DataBuffer.getDataTypeSize(dataType)
             };
-            return unique(new ComponentColorModel(cs, nBits, false, true, 
Transparency.OPAQUE, dataType));
+            return unique(new ComponentColorModel(cs, numBits, false, true, 
Transparency.OPAQUE, dataType));
         }
         /*
          * Interpolates the colors in the color palette. Colors that do not 
fall
@@ -386,7 +386,7 @@ public final class ColorModelFactory {
                 expand(colors, colorMap, lower, upper);
             }
         }
-        return createIndexColorModel(numBands, visibleBand, colorMap, true, 
transparent);
+        return createIndexColorModel(null, 0, numBands, visibleBand, colorMap, 
true, transparent);
     }
 
     /**
@@ -421,28 +421,33 @@ public final class ColorModelFactory {
      * <p>This methods caches previously created instances using weak 
references,
      * because index color model may be big (up to 256 kb).</p>
      *
-     * @param  numBands     the number of bands.
-     * @param  visibleBand  the band to display.
-     * @param  ARGB         an array of ARGB values.
-     * @param  hasAlpha     indicates whether alpha values are contained in 
the {@code ARGB} array.
-     * @param  transparent  the transparent pixel, or -1 for auto-detection.
+     * @param  transferType  the transfer type, or {@code null} for automatic.
+     * @param  numBits       the number of bits in sample values, or 0 for 
automatic.
+     * @param  numBands      the number of bands (usually 1).
+     * @param  visibleBand   the band to display (usually {@value 
#DEFAULT_VISIBLE_BAND}).
+     * @param  ARGB          an array of ARGB values.
+     * @param  hasAlpha      indicates whether alpha values are contained in 
the {@code ARGB} array.
+     * @param  transparent   the transparent pixel, or -1 for auto-detection.
      * @return an index color model for the specified array of ARGB values.
      */
-    public static IndexColorModel createIndexColorModel(final int numBands, 
final int visibleBand, final int[] ARGB,
+    public static IndexColorModel createIndexColorModel(final DataType 
transferType, int numBits,
+            final int numBands, final int visibleBand, final int[] ARGB,
             final boolean hasAlpha, final int transparent)
     {
         /*
          * No need to scan the ARGB values in search of a transparent pixel;
          * the IndexColorModel constructor does that for us.
          */
-        final int length   = ARGB.length;
-        final int bits     = getBitCount(length);
-        final int dataType = getTransferType(length);
+        final int length = ARGB.length;
+        if (numBits == 0) {
+            numBits = getBitCount(length);
+        }
+        final int dataType = (transferType != null) ? 
transferType.toDataBufferType() : getTransferType(length);
         final IndexColorModel cm;
         if (numBands == 1) {
-            cm = new IndexColorModel(bits, length, ARGB, 0, hasAlpha, 
transparent, dataType);
+            cm = new IndexColorModel(numBits, length, ARGB, 0, hasAlpha, 
transparent, dataType);
         } else {
-            cm = new MultiBandsIndexColorModel(bits, length, ARGB, 0, 
hasAlpha, transparent,
+            cm = new MultiBandsIndexColorModel(numBits, length, ARGB, 0, 
hasAlpha, transparent,
                                                dataType, numBands, 
visibleBand);
         }
         return CACHE.unique(cm);
@@ -450,7 +455,7 @@ public final class ColorModelFactory {
 
     /**
      * Returns a unique instance of the given color model.
-     * This method is a shortcut used when the return type does not need to be 
a specialized type.
+     * The color models are kept by weak references.
      *
      * @param  cm  the color model.
      * @return a unique instance of the given color model.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java
index cb619181b9..46f395cdd9 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java
@@ -108,36 +108,49 @@ public final class SampleModelBuilder {
      *
      * @param  type           type of sample values.
      * @param  size           tile width and height in pixels.
-     * @param  numBands       number of bands.
-     * @param  bitsPerSample  number of bits per sample values.
+     * @param  bitsPerSample  number of bits per sample values. The array 
length is the number of bands.
      * @param  isBanded       {@code true} if each band is stored in a 
separated bank.
      * @throws RasterFormatException if the arguments imply a sample model of 
unsupported type.
      */
     public SampleModelBuilder(final DataType type, final Dimension size,
-            final int numBands, final int bitsPerSample, final boolean 
isBanded)
+            final int[] bitsPerSample, final boolean isBanded)
     {
         this.dataType  = type.toDataBufferType();
         this.width     = size.width;
         this.height    = size.height;
-        this.numBands  = numBands;
+        this.numBands  = bitsPerSample.length;
         scanlineStride = width;
         pixelStride    = 1;
-        if (bitsPerSample != type.size()) {
+        boolean packed = true;
+        final int elementSize = type.size();
+        for (int n : bitsPerSample) {
+            if (n >= elementSize) {
+                packed = false;
+                break;
+            }
+        }
+        if (packed) {
             if (numBands == 1) {
                 // MultiPixelPackedSampleModel
                 pixelStride    = 0;
-                numberOfBits   = bitsPerSample;
-                scanlineStride = JDK18.ceilDiv(Math.multiplyExact(width, 
numberOfBits), type.size());
+                numberOfBits   = bitsPerSample[0];
+                scanlineStride = JDK18.ceilDiv(Math.multiplyExact(width, 
numberOfBits), elementSize);
             } else if (!isBanded) {
                 // SinglePixelPackedSampleModel
+                int shift = 0;
                 bitMasks = new int[numBands];
-                bitMasks[0] = (1 << bitsPerSample) - 1;
-                for (int i=1; i < bitMasks.length; i++) {
-                    bitMasks[i] = bitMasks[i-1] << bitsPerSample;
+                for (int i=0; i<numBands; i++) {
+                    final int n = bitsPerSample[i];
+                    ArgumentChecks.ensureBetween("bitsPerSample", 1, 
elementSize, n);
+                    bitMasks[i] = ((1 << n) - 1) << shift;
+                    shift += n;
+                }
+                if (shift > elementSize) {
+                    throw new 
RasterFormatException(Errors.format(Errors.Keys.IntegerOverflow_1, 
elementSize));
                 }
             } else {
                 // TODO: we can support that with a little bit more work.
-                throw new 
RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, 
"bitsPerSample=" + type.size()));
+                throw new 
RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, 
"bitsPerSample=" + bitsPerSample[0]));
             }
         } else if (isBanded) {
             // BandedSampleModel
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
index 498d3ba9bb..ab7fa2fa9d 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
@@ -92,7 +92,7 @@ public final class BandSelectImageTest extends TestCase {
         if (icm) {
             final int[] ARGB = new int[256];
             ColorModelFactory.expand(new int[] {0xFF000000, 0xFFFFFFFF}, ARGB, 
0, ARGB.length);
-            cm = ColorModelFactory.createIndexColorModel(numBands, 
checkedBand, ARGB, true, -1);
+            cm = ColorModelFactory.createIndexColorModel(null, 0, numBands, 
checkedBand, ARGB, true, -1);
         } else {
             cm = ColorModelFactory.createGrayScale(DataBuffer.TYPE_BYTE, 
numBands, checkedBand, Byte.MIN_VALUE, Byte.MAX_VALUE);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java
index 671f47f0c1..dd719b0f18 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image.privy;
 
+import java.util.Arrays;
 import java.awt.Dimension;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
@@ -56,13 +57,26 @@ public final class SampleModelBuilderTest extends TestCase {
         return new Dimension(WIDTH, HEIGHT);
     }
 
+    /**
+     * Returns an array of bits per sample.
+     *
+     * @param  numBands  number of bands.
+     * @param  numBits   number of bits per sample in each band.
+     * @return the array to give to {@link SampleModelBuilder} constructor.
+     */
+    private static int[] bitsPerSample(final int numBands, final int numBits) {
+        final var bitsPerSample = new int[numBands];
+        Arrays.fill(bitsPerSample, numBits);
+        return bitsPerSample;
+    }
+
     /**
      * Tests the creation and modification of a {@link BandedSampleModel}.
      */
     @Test
     public void testBanded() {
         final BandedSampleModel model = test(BandedSampleModel.class,
-                new SampleModelBuilder(DataType.FLOAT, size(), NUM_BANDS, 
Float.SIZE, true));
+                new SampleModelBuilder(DataType.FLOAT, size(), 
bitsPerSample(NUM_BANDS, Float.SIZE), true));
 
         assertArrayEquals(new int[] {1, 0, 2}, model.getBankIndices());
         assertArrayEquals(new int[] {0, 0, 0}, model.getBandOffsets());
@@ -77,7 +91,7 @@ public final class SampleModelBuilderTest extends TestCase {
     @Test
     public void testPixelInterleaved() {
         final PixelInterleavedSampleModel model = 
test(PixelInterleavedSampleModel.class,
-                new SampleModelBuilder(DataType.BYTE, size(), NUM_BANDS, 
Byte.SIZE, false));
+                new SampleModelBuilder(DataType.BYTE, size(), 
bitsPerSample(NUM_BANDS, Byte.SIZE), false));
 
         assertArrayEquals(new int[] {0, 0, 0}, model.getBankIndices());
         assertArrayEquals(new int[] {1, 0, 2}, model.getBandOffsets());
@@ -94,7 +108,7 @@ public final class SampleModelBuilderTest extends TestCase {
     @Test
     public void testSinglePixelPacked() {
         final SinglePixelPackedSampleModel model = 
test(SinglePixelPackedSampleModel.class,
-                new SampleModelBuilder(DataType.INT, size(), NUM_BANDS, 5, 
false));
+                new SampleModelBuilder(DataType.INT, size(), 
bitsPerSample(NUM_BANDS, 5), false));
 
         final int[] expected = {
             0b1111100000,           // Band 2 specified, 1 after compression.
@@ -114,7 +128,7 @@ public final class SampleModelBuilderTest extends TestCase {
     @Test
     public void testPixelMultiPixelPacked() {
         final int bitsPerSample = 4;
-        var builder = new SampleModelBuilder(DataType.INT, size(), 1, 
bitsPerSample, false);
+        var builder = new SampleModelBuilder(DataType.INT, size(), 
bitsPerSample(1, bitsPerSample), false);
         final var model = (MultiPixelPackedSampleModel) builder.build();
 
         assertEquals(bitsPerSample, model.getPixelBitStride());
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index c6cc20ebec..78404db9c5 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -1610,7 +1610,9 @@ final class ImageFileDirectory extends DataCube {
             final DataType type = getDataType();
             if (type != null) try {
                 var size = new Dimension(tileWidth, tileHeight);
-                sampleModel = new SampleModelBuilder(type, size, 
samplesPerPixel, bitsPerSample, isPlanar).build();
+                var numBits = new int[samplesPerPixel];
+                Arrays.fill(numBits, bitsPerSample);
+                sampleModel = new SampleModelBuilder(type, size, numBits, 
isPlanar).build();
             } catch (IllegalArgumentException | RasterFormatException e) {
                 error = e;
             }
@@ -1750,7 +1752,7 @@ final class ImageFileDirectory extends DataCube {
                 case PHOTOMETRIC_INTERPRETATION_RGB: {
                     if (alphaBand >= 0) alphaBand += 3;     // Must add the 
number of color bands.
                     final var builder = new 
ColorModelBuilder().bitsPerSample(bitsPerSample)
-                            
.alphaBand(alphaBand).isAlphaPremultiplied(isAlphaPremultiplied);
+                            
.alphaBand(alphaBand).alphaPremultiplied(isAlphaPremultiplied);
                     if (getSampleModel(null) instanceof 
SinglePixelPackedSampleModel) {
                         colorModel = builder.createPackedRGB();
                     } else {
@@ -1773,7 +1775,8 @@ final class ImageFileDirectory extends DataCube {
                                 | ((colorMap.intValue(bi++) & 0xFF00) >>> 
Byte.SIZE);
                     }
                     int transparent = Double.isFinite(noData) ? (int) 
Math.round(noData) : -1;
-                    colorModel = 
ColorModelFactory.createIndexColorModel(samplesPerPixel, VISIBLE_BAND, ARGB, 
true, transparent);
+                    colorModel = ColorModelFactory.createIndexColorModel(null, 
0,
+                            samplesPerPixel, VISIBLE_BAND, ARGB, true, 
transparent);
                     break;
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
index 63a5fb41cb..49c9497819 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
@@ -66,10 +66,11 @@ public abstract class AbstractResource implements Resource {
 
     /**
      * A description of this resource as an unmodifiable metadata, or {@code 
null} if not yet computed.
-     * If non-null, this metadata should contain at least the resource 
{@linkplain #getIdentifier() identifier}.
-     * Those metadata are created by {@link #createMetadata()} when first 
needed.
+     * This metadata should contain at least the resource {@linkplain 
#getIdentifier() identifier}.
+     * This field is initialized by {@link #getMetadata()} when first needed.
      *
      * @see #getMetadata()
+     * @see #createMetadata()
      */
     private volatile Metadata metadata;
 
@@ -152,7 +153,8 @@ public abstract class AbstractResource implements Resource {
     }
 
     /**
-     * Returns a description of this resource. This method invokes {@link 
#createMetadata()}
+     * Returns a description of this resource.
+     * The implementation invokes {@link #createMetadata()}
      * in a synchronized block when first needed, then caches the result.
      *
      * @return information about this resource (never {@code null}).
@@ -189,7 +191,7 @@ public abstract class AbstractResource implements Resource {
      * @throws DataStoreException if an error occurred while reading metadata 
from this resource.
      */
     protected Metadata createMetadata() throws DataStoreException {
-        final MetadataBuilder builder = new MetadataBuilder();
+        final var builder = new MetadataBuilder();
         builder.addDefaultMetadata(this, listeners);
         return builder.build();
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
index f298f1a0e9..2a1ec30e29 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
@@ -68,7 +68,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * When there is a subsampling, cell coordinates in this coverage are divided 
by the subsampling factors.
  * Conversions are done by {@link #coverageToResourceCoordinate(long, int)}.
  *
- * <p><b>DEsign note:</b> {@code TiledGridCoverage} use the same cell 
coordinates as the originating
+ * <p><b>Design note:</b> {@code TiledGridCoverage} uses the same cell 
coordinates as the originating
  * {@link TiledGridResource} (when no subsampling) because those two classes 
use {@code long} integers.
  * There is no integer overflow to avoid.</p>
  *
@@ -240,7 +240,9 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
     private final boolean deferredTileReading;
 
     /**
-     * Creates a new tiled grid coverage.
+     * Creates a new tiled grid coverage. This constructor does not load any 
tile.
+     * Callers should invoke {@link TiledGridResource#preload(GridCoverage)} 
after
+     * construction for loading tiles when immediate loading was requested by 
user.
      *
      * @param  subset  description of the {@link TiledGridResource} subset to 
cover.
      * @throws ArithmeticException if the number of tiles overflows 32 bits 
integer arithmetic.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
index f3f1234c5c..6d7792ea5d 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
@@ -255,7 +255,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
                 break;
             }
         }
-        return ColorModelFactory.createIndexColorModel(numBands, VISIBLE_BAND, 
ARGB, true, -1);
+        return ColorModelFactory.createIndexColorModel(null, 0, numBands, 
VISIBLE_BAND, ARGB, true, -1);
     }
 
     /**
diff --git 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
index 3f72b176de..e88786fb79 100644
--- 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
+++ 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
@@ -486,7 +486,7 @@ final class TiledResource extends TiledGridResource {
             colorModel = new 
ColorModelBuilder().bitsPerSample(dataType.numBits).alphaBand(alpha).createBandedRGB();
             // TODO: needs custom color model if too many bands, or if order 
is not (A)RGB.
         } else if (palette != null) {
-            colorModel = 
ColorModelFactory.createIndexColorModel(selectedBands.length, paletteIndex, 
palette, true, -1);
+            colorModel = ColorModelFactory.createIndexColorModel(null, 0, 
selectedBands.length, paletteIndex, palette, true, -1);
         } else {
             gray = Math.max(gray, 0);
             final Band band = selectedBands[gray];


Reply via email to