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 08e4257586fcb0736792d2b72dfbbf0e3746b8a2
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Dec 13 19:17:34 2024 +0100

    Better checks of ColorModel properties before to write a GeoTIFF file.
---
 .../sis/coverage/privy/ColorModelFactory.java      | 106 +++++++++----
 .../apache/sis/coverage/privy/ImageUtilities.java  |  16 --
 .../main/org/apache/sis/image/Colorizer.java       |   5 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  91 +++++++----
 .../org/apache/sis/storage/geotiff/Writer.java     |  17 ++-
 .../storage/geotiff/writer/ReformattedImage.java   | 170 ++++++++++++++-------
 .../org/apache/sis/storage/geotiff/WriterTest.java |   2 +-
 .../sis/storage/sql/postgis/RasterReader.java      |   2 +-
 .../apache/sis/io/stream/HyperRectangleWriter.java |  70 +++++++--
 .../org/apache/sis/storage/esri/RasterStore.java   |   8 +-
 .../org/apache/sis/storage/gdal/TiledResource.java |   2 +-
 11 files changed, 337 insertions(+), 152 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ColorModelFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ColorModelFactory.java
index 2f9750f88b..a17bb04d1d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ColorModelFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ColorModelFactory.java
@@ -163,6 +163,7 @@ public final class ColorModelFactory {
      *
      * @see #createPiecewise(int, int, int, ColorsForRange[])
      */
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     private ColorModelFactory(final int dataType, final int numBands, final 
int visibleBand, final ColorsForRange[] colors) {
         this.dataType    = dataType;
         this.numBands    = numBands;
@@ -595,52 +596,99 @@ public final class ColorModelFactory {
      * The sample model shall use integer type and have 3 or 4 bands.
      * This method may return {@code null} if the color model cannot be 
created.
      *
-     * @param  model  the sample model for which to create a color model.
+     * @param  targetModel           the sample model for which to create a 
color model.
+     * @param  isAlphaPremultiplied  whether the alpha value (if present) is 
premultiplied.
      * @return the color model, or null if a precondition does not hold.
      */
-    public static ColorModel createRGB(final SampleModel model) {
-        final int numBands = model.getNumBands();
-        if (numBands >= 3 && numBands <= 4 && 
ImageUtilities.isIntegerType(model)) {
+    public static ColorModel createRGB(final SampleModel targetModel, final 
boolean isAlphaPremultiplied) {
+check:  if (ImageUtilities.isIntegerType(targetModel)) {
+            final int numBands = targetModel.getNumBands();
+            final int alphaBand;
+            switch (numBands) {
+                case 3:  alphaBand = -1; break;
+                case 4:  alphaBand =  3; break;
+                default: break check;
+            }
             int bitsPerSample = 0;
             for (int i=0; i<numBands; i++) {
-                bitsPerSample = Math.max(bitsPerSample, 
model.getSampleSize(i));
+                bitsPerSample = Math.max(bitsPerSample, 
targetModel.getSampleSize(i));
             }
-            if (bitsPerSample <= Byte.SIZE) {
-                return createRGB(bitsPerSample, model.getNumDataElements() == 
1, numBands > 3);
+            if (targetModel.getNumDataElements() != 1) {
+                return createBandedRGB(bitsPerSample, alphaBand, 
isAlphaPremultiplied);
+            } else if (bitsPerSample <= Byte.SIZE) {
+                return createPackedRGB(bitsPerSample, alphaBand, 
isAlphaPremultiplied);
             }
         }
         return null;
     }
 
     /**
-     * Creates a RGB color model. The {@code packed} argument should be
-     * {@code true}  for color model used with {@link 
java.awt.image.SinglePixelPackedSampleModel}, and
-     * {@code false} for color model used with {@link 
java.awt.image.BandedSampleModel}.
+     * Creates a RGB color model for use with {@code 
SinglePixelPackedSampleModel}.
+     * Pixel values are packed in a single integer (usually 32-bits) per pixel.
+     * Color components are separated using a bitmask for each 
<abbr>RGBA</abbr> value.
      *
-     * @param  bitsPerSample  number of bits per sample, between 1 and 8 
(packed) or 32 (banded) inclusive.
-     * @param  packed         whether sample values are packed in a single 
element.
-     * @param  hasAlpha       whether the color model should have an alpha 
channel.
-     * @return the color model.
+     * @param  bitsPerSample         number of bits per sample, between 1 and 
8 inclusive.
+     * @param  alphaBand             index of the alpha channel (usually the 
last band), or -1 if none.
+     * @param  isAlphaPremultiplied  whether the alpha value (if present) is 
premultiplied.
+     * @return color model for use with {@link 
java.awt.image.SinglePixelPackedSampleModel}.
      */
-    public static ColorModel createRGB(final int bitsPerSample, final boolean 
packed, final boolean hasAlpha) {
-        if ((hasAlpha & packed) && bitsPerSample == Byte.SIZE) {
+    public static ColorModel createPackedRGB(final int bitsPerSample, final 
int alphaBand, final boolean isAlphaPremultiplied) {
+        if (bitsPerSample == Byte.SIZE && alphaBand == 3 && 
!isAlphaPremultiplied) {
             return ColorModel.getRGBdefault();
         }
-        ArgumentChecks.ensureBetween("bitsPerSample", 1, packed ? Byte.SIZE : 
Integer.SIZE, bitsPerSample);
-        final ColorModel cm;
-        if (packed) {
-            final int mask = (1 << bitsPerSample) - 1;
-            cm = new DirectColorModel((hasAlpha ? 4 : 3) * bitsPerSample,
-                    mask << (bitsPerSample * 2),        // Red
-                    mask <<  bitsPerSample,             // Green
-                    mask,                               // Blue
-                    hasAlpha ? mask << (bitsPerSample * 3) : 0);
+        ArgumentChecks.ensureBetween("bitsPerSample", 1, Byte.SIZE, 
bitsPerSample);
+        final int[] masks = new int[4];         // Red, Green, Blue, Alpha 
masks in that order.
+        int mask = (1 << bitsPerSample) - 1;    // Start with blue = 0xFF 
(usually).
+        for (int i=2; i >= 0; i--) {
+            masks[i] = mask;
+            mask <<= bitsPerSample;
+        }
+        int numBands = 3;
+        if (alphaBand >= 0) {
+            System.arraycopy(masks, alphaBand, masks, alphaBand+1, 3 - 
alphaBand);
+            masks[alphaBand] = mask;
+            numBands = 4;
+        }
+        int numBits = numBands * bitsPerSample;
+        int dataType = (numBits <=  Byte.SIZE) ? DataBuffer.TYPE_BYTE :
+                       (numBits <= Short.SIZE) ? DataBuffer.TYPE_USHORT : 
DataBuffer.TYPE_INT;
+        var cm = new 
DirectColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
+                        numBits, masks[0], masks[1], masks[2], masks[3],
+                        isAlphaPremultiplied, dataType);
+        return unique(cm);
+    }
+
+    /**
+     * Creates a RGB color model for use with {@code BandedSampleModel}.
+     * Each color component (sample value) is stored in a separated data 
element.
+     *
+     * <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}.
+     *
+     * @param  bitsPerSample         number of bits per sample, between 1 and 
32 inclusive.
+     * @param  alphaBand             index of the alpha channel (usually the 
last band), or -1 if none.
+     * @param  isAlphaPremultiplied  whether the alpha value (if present) is 
premultiplied.
+     * @return color model for use with {@link 
java.awt.image.BandedSampleModel}, or {@code null}.
+     */
+    public static ColorModel createBandedRGB(final int bitsPerSample, final 
int alphaBand, final boolean isAlphaPremultiplied) {
+        ArgumentChecks.ensureBetween("bitsPerSample", 1, Integer.SIZE, 
bitsPerSample);
+        final int[] numBits;
+        final int transparency;
+        final boolean hasAlpha = (alphaBand >= 0);
+        if (hasAlpha) {
+            if (alphaBand != 3) {
+                return null;        // Limitation documented in method Javadoc.
+            }
+            numBits = new int[4];
+            transparency = Transparency.TRANSLUCENT;
         } else {
-            final int[] numBits = new int[hasAlpha ? 4 : 3];
-            Arrays.fill(numBits, bitsPerSample);
-            cm = new 
ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), numBits, 
hasAlpha, false,
-                            hasAlpha ? Transparency.TRANSLUCENT : 
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
+            numBits = new int[3];
+            transparency = Transparency.OPAQUE;
         }
+        Arrays.fill(numBits, bitsPerSample);
+        var cm = new 
ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
+                        numBits, hasAlpha, isAlphaPremultiplied, transparency, 
DataBuffer.TYPE_BYTE);
         return unique(cm);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
index 362490293a..3595c187f7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
@@ -99,22 +99,6 @@ public final class ImageUtilities extends Static {
                 ((long) low) + aoi.height) - aoi.y);
     }
 
-    /**
-     * Returns whether the given image has an alpha channel.
-     *
-     * @param  image  the image or {@code null}.
-     * @return whether the image has an alpha channel.
-     *
-     * @see #getTransparencyDescription(ColorModel)
-     */
-    public static boolean hasAlpha(final RenderedImage image) {
-        if (image != null) {
-            final ColorModel cm = image.getColorModel();
-            if (cm != null) return cm.hasAlpha();
-        }
-        return false;
-    }
-
     /**
      * Returns the number of bands in the given image, or 0 if the image or 
its sample model is null.
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
index 5762390134..e8e2c005bb 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
@@ -157,9 +157,10 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
 
     /**
      * RGB(A) color model for images storing 8 bits integer on 3 or 4 bands.
-     * The color model is RGB for image having 3 bands, or ARGB for images 
having 4 bands.
+     * The color model is <abbr>RGB</abbr> for image having 3 bands, or 
<abbr>ARGB</abbr> for images having 4 bands.
+     * In the latter case, the color components are considered <em>not</em> 
premultiplied by the alpha value.
      */
-    Colorizer ARGB = (target) -> 
Optional.ofNullable(ColorModelFactory.createRGB(target.getSampleModel()));
+    Colorizer ARGB = (target) -> 
Optional.ofNullable(ColorModelFactory.createRGB(target.getSampleModel(), 
false));
 
     /**
      * Creates a colorizer which will interpolate the given colors in the 
given range of values.
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 181e8c3aa8..abe78d10dc 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
@@ -54,7 +54,6 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.privy.ColorModelFactory;
 import org.apache.sis.coverage.privy.SampleModelFactory;
-import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Vocabulary;
@@ -99,6 +98,11 @@ final class ImageFileDirectory extends DataCube {
      */
     private static final byte SIGNED = 1, UNSIGNED = 0, FLOAT = 3;
 
+    /**
+     * The band to make visible. May become configurable in a future version.
+     */
+    private static final int VISIBLE_BAND = 
ColorModelFactory.DEFAULT_VISIBLE_BAND;
+
     /**
      * Index of the (potentially pyramided) image containing this Image File 
Directory (IFD).
      * All reduced-resolution (overviews) images are ignored when computing 
this index value.
@@ -724,7 +728,7 @@ final class ImageFileDirectory extends DataCube {
             }
             /*
              * The number of components per pixel. Usually 1 for bilevel, 
grayscale, and palette-color images,
-             * and 3 for RGB images. Default value is 1.
+             * and 3 for RGB images. Default value is 1. May be greater than 3 
if there is extra samples.
              */
             case TAG_SAMPLES_PER_PIXEL: {
                 samplesPerPixel = type.readAsShort(input(), count);
@@ -1599,6 +1603,7 @@ final class ImageFileDirectory extends DataCube {
      * A sample model of different size or number of bands can be derived 
after construction
      * by call to one of {@code SampleModel.create…} methods.
      *
+     * @param  bands  should always be {@code null} if this implementation.
      * @throws DataStoreContentException if the data type is not supported.
      *
      * @see SampleModel#createCompatibleSampleModel(int, int)
@@ -1713,9 +1718,28 @@ final class ImageFileDirectory extends DataCube {
             return null;    // Let `TileGridResource` derive a model itself.
         }
         if (colorModel == null) {
-            final SampleModel sm  = getSampleModel(null);
-            final int dataType    = sm.getDataType();
-            final int visibleBand = 0;      // May be configurable in a future 
version.
+            /*
+             * The index of the alpha band is relative to extra samples.
+             * Before to be used, the number of color bands must be added.
+             * That number depends on the color interpretation.
+             *
+             * The alpha channel information should be used for all color 
models.
+             * However, this is only partially honored in current 
implementation.
+             */
+            int alphaBand = -1;
+            boolean isAlphaPremultiplied = false;
+            if (extraSamples != null) {
+                final int n = extraSamples.size();
+                for (int i=0; i<n; i++) {
+                    switch (extraSamples.intValue(i)) {
+                        case EXTRA_SAMPLES_ASSOCIATED_ALPHA: 
isAlphaPremultiplied = true; break;
+                        case EXTRA_SAMPLES_UNASSOCIATED_ALPHA: break;
+                        default: continue;
+                    }
+                    alphaBand = i;
+                    break;
+                }
+            }
             short missing = 0;              // Non-zero if there is a warning 
about missing information.
             switch (photometricInterpretation) {
                 default: {                  // For any unrecognized code, 
fallback on grayscale with 0 as black.
@@ -1726,31 +1750,21 @@ final class ImageFileDirectory extends DataCube {
                     missing = TAG_PHOTOMETRIC_INTERPRETATION;
                     break;
                 }
-                case  PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO:
-                case  PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO: {
-                    final Color[] colors = {Color.BLACK, Color.WHITE};
-                    if (photometricInterpretation == 
PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) {
-                        ArraysExt.swap(colors, 0, 1);
-                    }
-                    double min = 0;
-                    double max = Numerics.bitmask(bitsPerSample);              
     // Exclusive.
-                    if (sampleFormat != UNSIGNED) {
-                        max /= 2;
-                        min = -max;
-                    }
-                    if (minValues != null) min = Math.max(min, 
minValues.doubleValue(visibleBand));
-                    if (maxValues != null) max = Math.min(max, 
maxValues.doubleValue(visibleBand) + 1);
-                    colorModel = ColorModelFactory.createColorScale(dataType, 
samplesPerPixel, visibleBand, min, max, colors);
+                case PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO: {
+                    createSingleBandColorModel(Color.WHITE, Color.BLACK);
+                    break;
+                }
+                case PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO: {
+                    createSingleBandColorModel(Color.BLACK, Color.WHITE);
                     break;
                 }
                 case PHOTOMETRIC_INTERPRETATION_RGB: {
-                    final int numBands = sm.getNumBands();
-                    if (numBands < 3 || numBands > 4) {
-                        throw new 
DataStoreContentException(Errors.format(Errors.Keys.UnexpectedValueInElement_2, 
"numBands", numBands));
+                    if (alphaBand >= 0) alphaBand += 3;     // Must add the 
number of color bands.
+                    if (getSampleModel(null) instanceof 
SinglePixelPackedSampleModel) {
+                        colorModel = 
ColorModelFactory.createPackedRGB(bitsPerSample, alphaBand, 
isAlphaPremultiplied);
+                    } else {
+                        colorModel = 
ColorModelFactory.createBandedRGB(bitsPerSample, alphaBand, 
isAlphaPremultiplied);
                     }
-                    final boolean hasAlpha = (numBands >= 4);
-                    final boolean packed = (sm instanceof 
SinglePixelPackedSampleModel);
-                    colorModel = ColorModelFactory.createRGB(bitsPerSample, 
packed, hasAlpha);
                     break;
                 }
                 case PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR: {
@@ -1767,18 +1781,39 @@ final class ImageFileDirectory extends DataCube {
                                 | ((colorMap.intValue(gi++) & 0xFF00))
                                 | ((colorMap.intValue(bi++) & 0xFF00) >>> 
Byte.SIZE);
                     }
-                    colorModel = 
ColorModelFactory.createIndexColorModel(samplesPerPixel, visibleBand, ARGB,
-                                             true, Double.isFinite(noData) ? 
(int) Math.round(noData) : -1);
+                    int transparent = Double.isFinite(noData) ? (int) 
Math.round(noData) : -1;
+                    colorModel = 
ColorModelFactory.createIndexColorModel(samplesPerPixel, VISIBLE_BAND, ARGB, 
true, transparent);
                     break;
                 }
             }
             if (missing != 0) {
                 missingTag(missing, "GrayScale", false, true);
             }
+            if (colorModel == null) {
+                createSingleBandColorModel(Color.BLACK, Color.WHITE);
+            }
         }
         return colorModel;
     }
 
+    /**
+     * Creates a color model for a (theoretically) one-banded image.
+     * May also be invoked as a fallback for image with more bands if more 
suitable color model couldn't be created.
+     */
+    private void createSingleBandColorModel(final Color zero, final Color 
high) throws DataStoreContentException {
+        double min = 0;
+        double max = Numerics.bitmask(bitsPerSample);   // Exclusive.
+        switch (sampleFormat) {
+            default: break;
+            case FLOAT:  max = 1; break;
+            case SIGNED: max /= 2; min = -max; break;
+        }
+        if (minValues != null) min = Math.max(min, 
minValues.doubleValue(VISIBLE_BAND));
+        if (maxValues != null) max = Math.min(max, 
maxValues.doubleValue(VISIBLE_BAND) + 1);
+        colorModel = 
ColorModelFactory.createColorScale(getSampleModel(null).getDataType(),
+                        samplesPerPixel, VISIBLE_BAND, min, max, zero, high);
+    }
+
     /**
      * Returns the fill value to be replaced by NaN at reading time.
      * This is possible only for images of floating point type.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 4dd261c294..ac69b69b37 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -337,7 +337,7 @@ final class Writer extends IOBase implements Flushable {
     private TileMatrix writeImageFileDirectory(final ReformattedImage image, 
final GridGeometry grid, final Metadata metadata,
             final boolean overview) throws IOException, DataStoreException
     {
-        final SampleModel sm = image.visibleBands.getSampleModel();
+        final SampleModel sm = image.exportable.getSampleModel();
         Compression compression = 
store.getCompression().orElse(Compression.DEFLATE);
         if (!ImageUtilities.isIntegerType(sm)) {
             compression = compression.withPredictor(PREDICTOR_NONE);
@@ -355,6 +355,9 @@ final class Writer extends IOBase implements Flushable {
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
             numberOfTags++;
         }
+        if (image.extraSamples != null) {
+            numberOfTags++;
+        }
         final int   sampleFormat  = image.getSampleFormat();
         final int[] bitsPerSample = sm.getSampleSize();
         final int   numBands      = sm.getNumBands();
@@ -406,12 +409,15 @@ final class Writer extends IOBase implements Flushable {
                 isBigTIFF ? UpdatableWrite.of(output, (long)  numberOfTags)
                           : UpdatableWrite.of(output, (short) numberOfTags);
 
-        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD,
+        final var tiling = new TileMatrix(image.exportable, numPlanes, 
bitsPerSample, offsetIFD,
                                           compression.method, 
compression.level, compression.predictor);
+        /*
+         * Reminder: TIFF tags should be written in increasing numerical order.
+         */
         numberOfTags = 0;
         writeTag((short) TAG_NEW_SUBFILE_TYPE,           (short) 
TIFFTag.TIFF_LONG,  overview ? 1 : 0);
-        writeTag((short) TAG_IMAGE_WIDTH,                (short) 
TIFFTag.TIFF_LONG,  image.visibleBands.getWidth());
-        writeTag((short) TAG_IMAGE_LENGTH,               (short) 
TIFFTag.TIFF_LONG,  image.visibleBands.getHeight());
+        writeTag((short) TAG_IMAGE_WIDTH,                (short) 
TIFFTag.TIFF_LONG,  image.exportable.getWidth());
+        writeTag((short) TAG_IMAGE_LENGTH,               (short) 
TIFFTag.TIFF_LONG,  image.exportable.getHeight());
         writeTag((short) TAG_BITS_PER_SAMPLE,            (short) 
TIFFTag.TIFF_SHORT, bitsPerSample);
         writeTag((short) TAG_COMPRESSION,                (short) 
TIFFTag.TIFF_SHORT, compression.method.code);
         writeTag((short) TAG_PHOTOMETRIC_INTERPRETATION, (short) 
TIFFTag.TIFF_SHORT, colorInterpretation);
@@ -436,12 +442,13 @@ final class Writer extends IOBase implements Flushable {
             writeTag((short) TAG_PREDICTOR, (short) TIFFTag.TIFF_SHORT, 
compression.predictor.code);
         }
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
-            writeColorPalette((IndexColorModel) 
image.visibleBands.getColorModel(), 1L << bitsPerSample[0]);
+            writeColorPalette((IndexColorModel) 
image.exportable.getColorModel(), 1L << bitsPerSample[0]);
         }
         writeTag((short) TAG_TILE_WIDTH,                 /* TIFF_LONG */       
      tiling, false);
         writeTag((short) TAG_TILE_LENGTH,                /* TIFF_LONG */       
      tiling, false);
         writeTag((short) TAG_TILE_OFFSETS,               /* TIFF_LONG */       
      tiling, false);
         writeTag((short) TAG_TILE_BYTE_COUNTS,           /* TIFF_LONG */       
      tiling, false);
+        writeTag((short) TAG_EXTRA_SAMPLES,              /* TIFF_SHORT */      
      image.extraSamples);
         writeTag((short) TAG_SAMPLE_FORMAT,              (short) 
TIFFTag.TIFF_SHORT, sampleFormat);
         writeTag((short) TAG_S_MIN_SAMPLE_VALUE,         (short) 
TIFFTag.TIFF_FLOAT, statistics[0]);
         writeTag((short) TAG_S_MAX_SAMPLE_VALUE,         (short) 
TIFFTag.TIFF_FLOAT, statistics[1]);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
index bb3e9f8452..c9cbff5c26 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
@@ -28,12 +28,12 @@ import org.apache.sis.math.Statistics;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.coverage.privy.ImageUtilities;
-import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.io.stream.HyperRectangleWriter;
 
 
 /**
- * An image prepared for writing with bands separated in the way they are 
stored in a TIFF file.
- * The TIFF specification stores visible bands, alpha channel and extra bands 
separately.
+ * An image prepared for writing in a TIFF file. The TIFF specification puts 
some restrictions on tile size
+ * (must be a multiple of 16), and {@code HyperRectangleWriter} has some other 
restrictions on data layout.
  *
  * @todo Force tile size to multiple of 16.
  *
@@ -41,14 +41,30 @@ import org.apache.sis.storage.IncompatibleResourceException;
  */
 public final class ReformattedImage {
     /**
-     * The main image with visible bands.
+     * Number of color bands before the extra bands. This number does not 
include the alpha channel,
+     * which is considered an extra band in TIFF. This number does not apply 
to gray scale images,
+     * which use only one band.
      */
-    public final RenderedImage visibleBands;
+    private static final int NUM_COLOR_BANDS = 3;
 
-    /*
-     * TODO: alpha and extra bands not yet stored.
-     * This will be handled in a future version.
+    /**
+     * Whether the set of color bands includes only one band: gray scale or 
color palette.
+     * If {@code false}, then the number of color bands is {@value 
#NUM_COLOR_BANDS}.
+     * Alpha channel and extra components are ignored.
+     */
+    private final boolean singleBand;
+
+    /**
+     * The values to write in the {@code ExtraSamples} TIFF tag, or {@code 
null} if none.
+     * Values are some of the {@code EXTRA_SAMPLES_*} constants.
+     * The alpha channel, if presents, is declared here.
+     */
+    public short[] extraSamples;
+
+    /**
+     * The image reformatted in a way that can be written in a TIFF file.
      */
+    public final RenderedImage exportable;
 
     /**
      * Formats the given image into something that can be written in a GeoTIFF 
file.
@@ -58,27 +74,62 @@ public final class ReformattedImage {
      * @param  image      the image to separate into visible, alpha and extra 
bands.
      * @param  processor  supplier of the image processor to use if the image 
must be transformed.
      */
-    public ReformattedImage(final RenderedImage image, final 
Supplier<ImageProcessor> processor) {
+    public ReformattedImage(RenderedImage image, final 
Supplier<ImageProcessor> processor) {
+        int alphaBand = -1;
+        boolean isAlphaPremultiplied = false;
         final int numBands = ImageUtilities.getNumBands(image);
-select: if (numBands > 1) {
-            final int[] bands;
-            final int band = ImageUtilities.getVisibleBand(image);
-            if (band >= 0) {
-                bands = new int[] {band};
-            } else {
-                int max = 3;                                // TIFF can store 
only 3 bands (ignoring extra bands).
-                if (ImageUtilities.hasAlpha(image)) {
-                    max = Math.min(max, numBands - 1);      // The alpha band 
is always the last one.
-                }
-                if (numBands <= max) {
-                    break select;
+        final int visibleBand = ImageUtilities.getVisibleBand(image);
+        if (visibleBand >= 0) {
+            /*
+             * Indexed color model or gray scale image. This is conceptually a 
single band.
+             * But as an extension, Apache SIS allows the image to contain 
additional bands
+             * where only one band is shown. We need to order the visible band 
first.
+             * All other bands will be extra samples.
+             */
+            singleBand = true;
+            if (visibleBand != 0) {
+                final int[] bands = ArraysExt.range(0, numBands);
+                System.arraycopy(bands, 0, bands, 1, visibleBand);
+                bands[0] = visibleBand;
+                image = processor.get().selectBands(image, bands);      // 
Reorder bands without copy.
+            }
+        } else {
+            /*
+             * Any case with 2 or more bands. GeoTIFF wants us to store either 
1 or 3 bands.
+             * So if we cannot store 3 bands (excluding alpha channel), we 
will handle the
+             * image as gray scale.
+             */
+            final ColorModel cm = image.getColorModel();
+            if (cm != null) {
+                isAlphaPremultiplied = cm.isAlphaPremultiplied();
+                alphaBand = cm.getNumColorComponents();     // Alpha channel 
is right after color components.
+                if (alphaBand >= cm.getNumComponents()) {
+                    alphaBand = -1;
                 }
-                bands = ArraysExt.range(0, max);
             }
-            visibleBands = processor.get().selectBands(image, bands);
-            return;
+            singleBand = numBands < (alphaBand >= 0 ? 1 + NUM_COLOR_BANDS : 
NUM_COLOR_BANDS);
         }
-        visibleBands = image;
+        /*
+         * Check if there is any extra samples. If yes, prepare an 
`extraSamples` array with all
+         * values initialized to `EXTRA_SAMPLES_UNSPECIFIED` (value 0) except 
for the alpha channel.
+         */
+        final int numColors = singleBand ? 1 : Math.min(numBands, 
NUM_COLOR_BANDS);
+        if (numBands > numColors) {
+            extraSamples = new short[numBands - numColors];
+            if (alphaBand >= 0) {
+                extraSamples[alphaBand - numColors] = isAlphaPremultiplied
+                        ? (short) EXTRA_SAMPLES_ASSOCIATED_ALPHA
+                        : (short) EXTRA_SAMPLES_UNASSOCIATED_ALPHA;
+            }
+        }
+        /*
+         * If the image cannot be written directly, reformat. Because this 
operation
+         * forces the copy of pixel values, it should be executed in last 
resort.
+         */
+        if (!HyperRectangleWriter.Builder.isSupported(image.getSampleModel())) 
{
+            // TODO: reformat the image here.
+        }
+        exportable = image;
     }
 
     /**
@@ -90,7 +141,7 @@ select: if (numBands > 1) {
      *         Array elements may be {@code null} if there is no statistics.
      */
     public double[][] statistics(final int numBands) {
-        final Object property = 
visibleBands.getProperty(PlanarImage.STATISTICS_KEY);
+        final Object property = 
exportable.getProperty(PlanarImage.STATISTICS_KEY);
 found:  if (property instanceof Statistics[]) {
             final var stats = (Statistics[]) property;
             final var min = new double[numBands];
@@ -112,7 +163,7 @@ found:  if (property instanceof Statistics[]) {
      * @return One of {@code SAMPLE_FORMAT_*} constants.
      */
     public int getSampleFormat() {
-        final SampleModel sm = visibleBands.getSampleModel();
+        final SampleModel sm = exportable.getSampleModel();
         if (ImageUtilities.isUnsignedType(sm)) return 
SAMPLE_FORMAT_UNSIGNED_INTEGER;
         if (ImageUtilities.isIntegerType(sm))  return 
SAMPLE_FORMAT_SIGNED_INTEGER;
         return SAMPLE_FORMAT_FLOATING_POINT;
@@ -122,40 +173,47 @@ found:  if (property instanceof Statistics[]) {
      * Returns the TIFF color interpretation.
      *
      * @return One of {@code PHOTOMETRIC_INTERPRETATION_*} constants.
-     * @throws IncompatibleResourceException if the color model is not 
supported.
      */
-    public int getColorInterpretation() throws IncompatibleResourceException {
-        final ColorModel cm = visibleBands.getColorModel();
-        if (cm instanceof IndexColorModel) {
-            final var   icm   = (IndexColorModel) cm;
-            final int   last  = icm.getMapSize() - 1;
-            final float scale = 255f / last;
-            boolean white = true;
-            boolean black = true;
-            for (int i=0; i <= last; i++) {
-                final int expected = Math.round(i * scale);
-                if (black) black = icm.getRGB(     i) == expected;
-                if (white) white = icm.getRGB(last-i) == expected;
-                if (!(black | white)) break;
+    public int getColorInterpretation() {
+        final ColorModel cm = exportable.getColorModel();
+        if (singleBand) {
+            if (cm instanceof IndexColorModel) {
+                final var   icm   = (IndexColorModel) cm;
+                final int   last  = icm.getMapSize() - 1;
+                final float scale = 255f / last;
+                boolean white = true;
+                boolean black = true;
+                for (int i=0; i <= last; i++) {
+                    final int expected = Math.round(i * scale);
+                    if (black) black = icm.getRGB(     i) == expected;
+                    if (white) white = icm.getRGB(last-i) == expected;
+                    if (!(black | white)) break;
+                }
+                if (black) return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
+                if (white) return PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
+                return PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR;
             }
-            if (black) return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
-            if (white) return PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
-            return PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR;
+            return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
         }
         if (cm != null) {
-            switch (cm.getColorSpace().getType()) {
-                case ColorSpace.TYPE_GRAY:  return 
PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
-                case ColorSpace.TYPE_RGB:   return 
PHOTOMETRIC_INTERPRETATION_RGB;
-                case ColorSpace.TYPE_CMY:   return 
PHOTOMETRIC_INTERPRETATION_CMYK;
-                case ColorSpace.TYPE_Lab:   return 
PHOTOMETRIC_INTERPRETATION_CIELAB;
-                case ColorSpace.TYPE_YCbCr: return 
PHOTOMETRIC_INTERPRETATION_Y_CB_CR;
-                // A future version may add support for more color models.
+            /*
+             * All color interpretations returned in this block shall expect 3 
bands.
+             * Gray scale images are handled as RGB (should not be a concern, 
because
+             * they should have been handled in the previous block). If a 
color space
+             * has 4 or more components, all components after 3 are extra 
samples.
+             */
+            final ColorSpace cs = cm.getColorSpace();
+            if (cs != null) {
+                switch (cs.getType()) {
+                    //   ColorSpace.TYPE_GRAY:  should never happen here.
+                    case ColorSpace.TYPE_RGB:   return 
PHOTOMETRIC_INTERPRETATION_RGB;
+                    case ColorSpace.TYPE_CMY:   return 
PHOTOMETRIC_INTERPRETATION_CMYK;    // Black stored as extra sample.
+                    case ColorSpace.TYPE_Lab:   return 
PHOTOMETRIC_INTERPRETATION_CIELAB;
+                    case ColorSpace.TYPE_YCbCr: return 
PHOTOMETRIC_INTERPRETATION_Y_CB_CR;
+                    // A future version may add support for more color models.
+                }
             }
         }
-        if (ImageUtilities.getNumBands(visibleBands) >= 3) {
-            return PHOTOMETRIC_INTERPRETATION_RGB;
-        } else {
-            return PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
-        }
+        return PHOTOMETRIC_INTERPRETATION_RGB;      // Default value for 
unknown color space.
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
index eb415c5647..5577acd807 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
@@ -237,7 +237,7 @@ public final class WriterTest extends TestCase {
     @Test
     public void testUntiledRGB() throws IOException, DataStoreException {
         initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 3, 1, 1);
-        
image.setColorModel(ColorModelFactory.createRGB(image.getSampleModel()));
+        
image.setColorModel(ColorModelFactory.createRGB(image.getSampleModel(), false));
         writeImage();
         verifyHeader(false, IOBase.LITTLE_ENDIAN);
         verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1,          // 
One less tag because stripped layout.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
index 84ad97dc08..0827136e77 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
@@ -316,7 +316,7 @@ public final class RasterReader extends RasterFormat {
         final int dataType = sm.getDataType();
         final int numBands = sm.getNumBands();
         if ((numBands == 3) && (dataType == DataBuffer.TYPE_BYTE)) {
-            cm = ColorModelFactory.createRGB(Byte.SIZE, false, false);
+            cm = ColorModelFactory.createBandedRGB(Byte.SIZE, -1, false);
         } else {
             final int visibleBand = 0;              // Arbitrary value (could 
be configurable).
             final double minimum, maximum;
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
index 5bdd13ea3f..61125670af 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java
@@ -188,6 +188,22 @@ public class HyperRectangleWriter {
         public Builder() {
         }
 
+        /**
+         * Returns whether the given sample model is supported. If this method 
returns {@code true},
+         * then invoking a {@code create(…)} method should not throw {@link 
RasterFormatException}.
+         *
+         * @param  sm  the sample model of the rasters to write, or {@code 
null}.
+         * @return whether the given sample model is non-null and supported.
+         *
+         * @see #create(SampleModel)
+         */
+        public static boolean isSupported(final SampleModel sm) {
+            if (sm instanceof ComponentSampleModel)         return true;
+            if (sm instanceof SinglePixelPackedSampleModel) return 
isSupported(null, (SinglePixelPackedSampleModel) sm);
+            if (sm instanceof MultiPixelPackedSampleModel)  return 
isSupported(null, (MultiPixelPackedSampleModel)  sm);
+            return false;
+        }
+
         /**
          * Creates a new writer for raster data described by the given sample 
model and strides.
          * The {@link #pixelStride} and {@link #scanlineStride} fields must be 
set before this method is invoked.
@@ -277,17 +293,32 @@ public class HyperRectangleWriter {
             bankOffsets    = bankIndices;
             pixelStride    = 1;
             scanlineStride = sm.getScanlineStride();
-            final int[] d  = sm.getBitMasks();
+            if (isSupported(this, sm)) {
+                return create(sm, null);
+            } else {
+                throw new RasterFormatException(sm.toString());
+            }
+        }
+
+        /**
+         * Tests whether the given sample model is supported by this builder.
+         *
+         * @param  builder  the builder in which to store information, or 
{@code null} if none.
+         * @param  sm       the sample model of the rasters to test for 
support.
+         * @return whether the given sample model is supported.
+         */
+        private static boolean isSupported(final Builder builder, final 
SinglePixelPackedSampleModel sm) {
             /*
              * If there is only one band, it is okay to store the values using 
the current data type.
              * We require that all bits are used because otherwise, there is a 
risk to write garbage
              * and we are not sure that the destination format can encode the 
mask.
              */
             final int dataSize = DataBuffer.getDataTypeSize(sm.getDataType());
+            final int[] d  = sm.getBitMasks();
             if (d.length == 1) {
                 int mask = (int) ((1L << dataSize) - 1);
                 if ((d[0] & mask) == mask) {
-                    return create(sm, null);
+                    return true;
                 }
             }
             /*
@@ -295,17 +326,19 @@ public class HyperRectangleWriter {
              * many formats such as GeoTIFF will interpret those values as 
bytes.
              */
             if ((dataSize % (d.length * Byte.SIZE)) != 0) {
-                throw new RasterFormatException(sm.toString());
+                return false;
             }
             int mask = 0xFF;
             for (int i = d.length; --i >= 0;) {
                 if (d[i] != mask) {
-                    throw new RasterFormatException(sm.toString());
+                    return false;
                 }
                 mask <<= Byte.SIZE;
             }
-            requiresBigEndian = true;
-            return create(sm, null);
+            if (builder != null) {
+                builder.requiresBigEndian = true;
+            }
+            return true;
         }
 
         /**
@@ -336,18 +369,35 @@ public class HyperRectangleWriter {
             bankOffsets    = bankIndices;
             pixelStride    = 1;
             scanlineStride = sm.getScanlineStride();
+            if (isSupported(this, sm)) {
+                return create(sm, null);
+            } else {
+                throw new RasterFormatException(sm.toString());
+            }
+        }
+
+        /**
+         * Tests whether the given sample model is supported by this builder.
+         *
+         * @param  builder  the builder in which to store information, or 
{@code null} if none.
+         * @param  sm       the sample model of the rasters to test for 
support.
+         * @return whether the given sample model is supported.
+         */
+        private static boolean isSupported(final Builder builder, final 
MultiPixelPackedSampleModel sm) {
             final int[] d  = sm.getSampleSize();
             if (d.length == 1) {
                 final int sampleSize = d[0];
                 if (sm.getPixelBitStride() == sampleSize) {
                     final int dataSize = 
DataBuffer.getDataTypeSize(sm.getDataType());
                     if (dataSize % sampleSize == 0) {   // Check that all bits 
are used.
-                        requiresBigEndian = Math.max(sampleSize, Byte.SIZE) != 
dataSize;
-                        return create(sm, null);
+                        if (builder != null) {
+                            builder.requiresBigEndian = Math.max(sampleSize, 
Byte.SIZE) != dataSize;
+                        }
+                        return true;
                     }
                 }
             }
-            throw new RasterFormatException(sm.toString());
+            return false;
         }
 
         /**
@@ -358,6 +408,8 @@ public class HyperRectangleWriter {
          * @param  sm  the sample model of the rasters to write.
          * @return writer for rasters using the specified sample model (never 
{@code null}).
          * @throws RasterFormatException if the given sample model is not 
supported.
+         *
+         * @see #isSupported(SampleModel)
          */
         public HyperRectangleWriter create(final SampleModel sm) {
             if (sm instanceof ComponentSampleModel)         return 
create((ComponentSampleModel)         sm);
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 75e4d8c67f..5fea017665 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
@@ -396,16 +396,16 @@ abstract class RasterStore extends PRJDataStore 
implements GridCoverageResource
              */
             if (band == VISIBLE_BAND) {
                 if (isRGB) {
-                    colorModel = ColorModelFactory.createRGB(sm);       // 
Should never be null.
+                    colorModel = ColorModelFactory.createRGB(sm, false);    // 
Should never be null.
                 } else {
                     try {
                         colorModel = readColorMap(dataType, (int) (maximum + 
1), bands.length);
                     } catch (URISyntaxException | IOException | 
NumberFormatException e) {
                         cannotReadAuxiliaryFile(CLR, e);
                     }
-                    if (colorModel == null) {
-                        colorModel = 
ColorModelFactory.createGrayScale(dataType, bands.length, band, minimum, 
maximum);
-                    }
+                }
+                if (colorModel == null) {
+                    colorModel = ColorModelFactory.createGrayScale(dataType, 
bands.length, band, minimum, maximum);
                 }
             }
         }
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 75050c3f36..7e72b37161 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
@@ -473,7 +473,7 @@ final class TiledResource extends TiledGridResource {
              */
         }
         if ((red | green | blue) >= 0) {
-            colorModel = ColorModelFactory.createRGB(dataType.numBits, false, 
alpha >= 0);
+            colorModel = ColorModelFactory.createBandedRGB(dataType.numBits, 
alpha, false);
             // 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);


Reply via email to