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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 482e27bf24 Consolidation of the handling of 
`PlanarImage.SAMPLE_DIMENSIONS_KEY`: - Document that some elements may be null, 
and adjust codes accordingly. - Ensure that `ImageProcessor.statistics(…)` 
never return null values. - Change some internal from `SampleDimensions[]` to 
`List<SampleDimension>`.   It reduces the number of conversions between those 
two types.
482e27bf24 is described below

commit 482e27bf2414a39daa5d4c6d6806c0e4f5279ad4
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Dec 26 13:52:39 2024 +0100

    Consolidation of the handling of `PlanarImage.SAMPLE_DIMENSIONS_KEY`:
    - Document that some elements may be null, and adjust codes accordingly.
    - Ensure that `ImageProcessor.statistics(…)` never return null values.
    - Change some internal from `SampleDimensions[]` to `List<SampleDimension>`.
      It reduces the number of conversions between those two types.
---
 .../coverage/grid/BandAggregateGridCoverage.java   |  4 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |  5 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  4 +-
 .../sis/coverage/privy/SampleDimensions.java       | 27 +++++----
 .../org/apache/sis/image/BandAggregateImage.java   | 67 ++++++++++++++++++----
 .../org/apache/sis/image/BandAggregateLayout.java  | 49 +++++++---------
 .../main/org/apache/sis/image/BandSelectImage.java | 12 +++-
 .../apache/sis/image/BandedSampleConverter.java    | 23 ++++----
 .../main/org/apache/sis/image/Colorizer.java       |  2 +
 .../main/org/apache/sis/image/ImageProcessor.java  | 47 ++++++++++++---
 .../main/org/apache/sis/image/PlanarImage.java     | 20 +++++--
 .../main/org/apache/sis/image/RecoloredImage.java  |  3 +-
 .../main/org/apache/sis/image/Visualization.java   | 14 +++--
 .../apache/sis/image/BandAggregateImageTest.java   | 34 +++++++++++
 .../org/apache/sis/map/coverage/RenderingData.java |  6 +-
 .../storage/geotiff/writer/ReformattedImage.java   |  4 +-
 16 files changed, 225 insertions(+), 96 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
index 695876c2c8..c51ab5cc97 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
@@ -136,7 +136,7 @@ final class BandAggregateGridCoverage extends GridCoverage {
     /**
      * Returns a two-dimensional slice of grid data as a rendered image.
      * This operation is potentially costly if the {@code sliceExtent} 
argument changes often because
-     * the previously computed images are unlikely to be reused if the 
coordinate systems are different.
+     * the previously computed images are unlikely to be reused when the 
coordinate systems are different.
      * It may result in the same bands being copied may times in different 
{@link RenderedImage} instances.
      *
      * <h4>Implementation note</h4>
@@ -155,7 +155,7 @@ final class BandAggregateGridCoverage extends GridCoverage {
         if (sliceExtent == null) {
             sliceExtent = gridGeometry.getExtent();
         }
-        final RenderedImage[] images = new RenderedImage[sources.length];
+        final var images = new RenderedImage[sources.length];
         for (int i=0; i<images.length; i++) {
             images[i] = 
sources[i].render(sliceExtent.translate(gridTranslations[i]));
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
index 5ea04a79e4..520def8287 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
@@ -213,7 +213,7 @@ public abstract class GridCoverage extends BandedCoverage {
      * @see SampleDimension#getBackground()
      */
     final Number[] getBackground() {
-        return SampleDimensions.backgrounds(sampleDimensions);
+        return SampleDimensions.backgrounds(getSampleDimensions());
     }
 
     /**
@@ -312,8 +312,9 @@ public abstract class GridCoverage extends BandedCoverage {
     final RenderedImage convert(final RenderedImage source, final DataType 
bandType,
             final MathTransform1D[] converters, final ImageProcessor processor)
     {
+        final List<SampleDimension> ranges = getSampleDimensions();
         try {
-            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(sampleDimensions);
+            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(ranges);
             return processor.convert(source, getRanges(), converters, 
bandType);
         } finally {
             SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 69fcbc0b47..9d8a33a514 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -937,10 +937,10 @@ public class GridCoverageProcessor implements Cloneable {
      */
     public RenderedImage visualize(final GridCoverage source, final GridExtent 
slice) {
         ArgumentChecks.ensureNonNull("source", source);
-        final SampleDimension[] bands = 
source.getSampleDimensions().toArray(SampleDimension[]::new);
+        final List<SampleDimension> ranges = source.getSampleDimensions();
         final RenderedImage image = source.render(slice);
         try {
-            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(bands);
+            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(ranges);
             return imageProcessor.visualize(image);
         } finally {
             SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java
index 8b3a7a15b9..e76e23ce7f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java
@@ -46,6 +46,8 @@ public final class SampleDimensions extends Static {
      * This is used in:
      * <ul>
      *   <li>The <em>target</em> sample dimensions of a {@link 
org.apache.sis.image.BandedSampleConverter} image.</li>
+     *   <li>The <em>target</em> sample dimensions of a {@link 
org.apache.sis.image.BandAggregateImage} image.</li>
+     *   <li>The <em>target</em> sample dimensions of a {@link 
org.apache.sis.image.BandSelectImage} image.</li>
      *   <li>The <em>source</em> sample dimensions of a {@link 
org.apache.sis.image.Visualization} image.</li>
      * </ul>
      *
@@ -60,10 +62,9 @@ public final class SampleDimensions extends Static {
      *     }
      *     }
      *
-     * The content of the array in this thread-local variable shall not be 
modified,
-     * because it may be a direct reference to an internal array (not a clone).
+     * The list in this thread-local variable should be unmodifiable.
      */
-    public static final ThreadLocal<SampleDimension[]> 
IMAGE_PROCESSOR_ARGUMENT = new ThreadLocal<>();
+    public static final ThreadLocal<List<SampleDimension>> 
IMAGE_PROCESSOR_ARGUMENT = new ThreadLocal<>();
 
     /**
      * Do not allow instantiation of this class.
@@ -114,16 +115,18 @@ public final class SampleDimensions extends Static {
      * @return the background values, or {@code null} if the given argument 
was null.
      *         Otherwise the returned array is never null but may contain null 
elements.
      */
-    public static Number[] backgrounds(final SampleDimension... bands) {
+    public static Number[] backgrounds(final List<SampleDimension> bands) {
         if (bands == null) {
             return null;
         }
-        final Number[] fillValues = new Number[bands.length];
+        final Number[] fillValues = new Number[bands.size()];
         for (int i=fillValues.length; --i >= 0;) {
-            final SampleDimension band = bands[i];
-            final Optional<Number> bg = band.getBackground();
-            if (bg.isPresent()) {
-                fillValues[i] = bg.get();
+            final SampleDimension band = bands.get(i);
+            if (band != null) {
+                final Optional<Number> bg = band.getBackground();
+                if (bg.isPresent()) {
+                    fillValues[i] = bg.get();
+                }
             }
         }
         return fillValues;
@@ -144,13 +147,13 @@ public final class SampleDimensions extends Static {
      *
      * @see ImageProcessor#statistics(RenderedImage, Shape, 
DoubleUnaryOperator...)
      */
-    public static DoubleUnaryOperator[] toSampleFilters(final 
SampleDimension... bands) {
+    public static DoubleUnaryOperator[] toSampleFilters(final 
List<SampleDimension> bands) {
         if (bands == null) {
             return null;
         }
-        final DoubleUnaryOperator[] sampleFilters = new 
DoubleUnaryOperator[bands.length];
+        final DoubleUnaryOperator[] sampleFilters = new 
DoubleUnaryOperator[bands.size()];
         for (int i = 0; i < sampleFilters.length; i++) {
-            final SampleDimension band = bands[i];
+            final SampleDimension band = bands.get(i);
             if (band != null) {
                 final List<Category> categories = band.getCategories();
                 final int count = categories.size();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java
index ca2e6c4c27..dfb8c1ae2c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java
@@ -16,7 +16,10 @@
  */
 package org.apache.sis.image;
 
+import java.util.List;
 import java.util.Arrays;
+import java.util.Objects;
+import java.util.LinkedHashSet;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.BandedSampleModel;
@@ -25,6 +28,7 @@ import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.math.Statistics;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.privy.ImageUtilities;
 import org.apache.sis.coverage.privy.BandAggregateArgument;
@@ -52,10 +56,9 @@ class BandAggregateImage extends MultiSourceImage {
     private final boolean allowSharing;
 
     /**
-     * Concatenated array of the sample dimensions declared in all sources, or 
{@code null} if none.
-     * This field is non-null only if this information is present in all 
sources.
+     * Concatenated list of the sample dimensions declared in all sources, or 
{@code null} if none.
      */
-    private final SampleDimension[] sampleDimensions;
+    private final List<SampleDimension> sampleDimensions;
 
     /*
      * The method declaration order below is a little bit unusual,
@@ -243,23 +246,63 @@ class BandAggregateImage extends MultiSourceImage {
      */
     @Override
     public String[] getPropertyNames() {
+        final var names = new LinkedHashSet<String>();
         if (sampleDimensions != null) {
-            return new String[] {SAMPLE_DIMENSIONS_KEY};
-        } else {
-            return null;
+            names.add(SAMPLE_DIMENSIONS_KEY);
+        }
+        final int numSources = getNumSources();
+        for (int i=0; i<numSources; i++) {
+            String[] more = getSource(i).getPropertyNames();
+            if (more != null) {
+                names.addAll(Arrays.asList(more));
+            }
         }
+        names.retainAll(BandSelectImage.REDUCED_PROPERTIES);
+        return names.isEmpty() ? null : names.toArray(String[]::new);
     }
 
     /**
      * Gets a property of this image as a value derived from all source images.
      */
     @Override
+    @SuppressWarnings("SuspiciousSystemArraycopy")
     public Object getProperty(final String key) {
-        if (sampleDimensions != null && SAMPLE_DIMENSIONS_KEY.equals(key)) {
-            return sampleDimensions.clone();
-        } else {
-            return super.getProperty(key);
+        final int numBands = sampleModel.getNumBands();
+        final Object result;
+        switch (key) {
+            case SAMPLE_DIMENSIONS_KEY: {
+                if (sampleDimensions != null) {
+                    return sampleDimensions.toArray(SampleDimension[]::new);
+                }
+                result = new SampleDimension[numBands];
+                break;
+            }
+            case STATISTICS_KEY: {
+                result = new Statistics[numBands];
+                break;
+            }
+            case SAMPLE_RESOLUTIONS_KEY: {
+                var r = new double[numBands];
+                Arrays.fill(r, Double.NaN);
+                result = r;
+                break;
+            }
+            default: return super.getProperty(key);
+        }
+        int offset = 0;
+        boolean found = false;
+        final int numSources = getNumSources();
+        for (int i=0; i<numSources; i++) {
+            final RenderedImage source = getSource(i);
+            final int n = ImageUtilities.getNumBands(source);
+            final Object value = source.getProperty(key);
+            if (result.getClass().isInstance(value)) {
+                System.arraycopy(value, 0, result, offset, n);
+                found = true;
+            }
+            offset += n;
         }
+        return found ? result : null;
     }
 
     /**
@@ -366,7 +409,7 @@ class BandAggregateImage extends MultiSourceImage {
         if (super.equals(object)) {
             final var that = (BandAggregateImage) object;
             return that.allowSharing == allowSharing &&
-                   Arrays.equals(that.sampleDimensions, sampleDimensions);
+                   Objects.equals(that.sampleDimensions, sampleDimensions);
         }
         return false;
     }
@@ -378,6 +421,6 @@ class BandAggregateImage extends MultiSourceImage {
     public int hashCode() {
         return super.hashCode()
                 + Boolean.hashCode(allowSharing)
-                + Arrays.hashCode(sampleDimensions);
+                + Objects.hashCode(sampleDimensions);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java
index 71094b89ba..976a8e4136 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java
@@ -16,8 +16,7 @@
  */
 package org.apache.sis.image;
 
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 import java.awt.Point;
 import java.awt.Dimension;
@@ -38,6 +37,7 @@ import org.apache.sis.coverage.privy.ImageUtilities;
 import org.apache.sis.coverage.privy.ColorModelFactory;
 import org.apache.sis.coverage.privy.BandAggregateArgument;
 import org.apache.sis.coverage.privy.CommonDomainFinder;
+import org.apache.sis.coverage.privy.SampleDimensions;
 
 
 /**
@@ -106,7 +106,7 @@ final class BandAggregateLayout {
      * Concatenated array of the sample dimensions declared in all sources, or 
{@code null} if none.
      * This field is non-null only if this information is present in all 
sources.
      */
-    final SampleDimension[] sampleDimensions;
+    final List<SampleDimension> sampleDimensions;
 
     /**
      * Whether to allow the sharing of data buffers (instead of copying) if 
possible.
@@ -355,7 +355,7 @@ search: for (int i=0; i < sources.length; i++) {
             base += (bands != null) ? bands.length : 
ImageUtilities.getNumBands(source);
         }
         if (colorizer != null) {
-            var target = new Colorizer.Target(sampleModel, 
UnmodifiableArrayList.wrap(sampleDimensions), visibleBand);
+            var target = new Colorizer.Target(sampleModel, sampleDimensions, 
visibleBand);
             Optional<ColorModel> candidate = colorizer.apply(target);
             if (candidate.isPresent()) {
                 return candidate.get();
@@ -369,32 +369,27 @@ search: for (int i=0; i < sources.length; i++) {
     }
 
     /**
-     * Gets a concatenated array of the sample dimensions declared in all 
sources, or {@code null} if none.
-     * This method returns a non-null array only if this information is 
present in all sources.
+     * Gets a concatenated list of the sample dimensions declared in all 
sources, or {@code null} if none.
+     * The returned list should not contain null element (i.e., this method 
does not return partial list).
      */
-    private SampleDimension[] getSampleDimensions() {
-        final var selected = new ArrayList<SampleDimension>();
-        for (int i=0; i < sources.length; i++) {
-            final Object value = 
sources[i].getProperty(PlanarImage.SAMPLE_DIMENSIONS_KEY);
-            if (!(value instanceof SampleDimension[])) {
-                return null;
-            }
-            final var sd = (SampleDimension[]) value;
-            final int[] bands = bandsPerSource[i];
-            if (bands == null) {
-                selected.addAll(Arrays.asList(sd));
-            } else for (int j=0; j < bands.length; j++) {
-                final int t = bands[j];
-                if (t < 0 || t >= sd.length) {
-                    return null;
-                }
-                selected.add(sd[t]);
-            }
+    private List<SampleDimension> getSampleDimensions() {
+        List<SampleDimension> ranges = 
SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get();
+        if (ranges != null) {
+            return ranges;
         }
+        int offset = 0;
         final var result = new SampleDimension[bandSelect.length];
-        for (int i=0; i < result.length; i++) {
-            result[i] = selected.get(bandSelect[i]);
+        for (RenderedImage source : filteredSources) {
+            final Object value = 
source.getProperty(PlanarImage.SAMPLE_DIMENSIONS_KEY);
+            if (value instanceof SampleDimension[]) {
+                final var sd = (SampleDimension[]) value;
+                final int n = ImageUtilities.getNumBands(source);   // Do not 
trust the array length.
+                System.arraycopy(sd, 0, result, offset, Math.min(sd.length, 
n));
+                offset += n;
+            } else {
+                return null;
+            }
         }
-        return result;
+        return UnmodifiableArrayList.wrap(result);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
index b9a3727cd2..c2dc3b0c68 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
@@ -60,7 +60,7 @@ class BandSelectImage extends SourceAlignedImage {
      * Shall be a subset of {@link #INHERITED_PROPERTIES}.
      * All values must be arrays.
      */
-    private static final Set<String> REDUCED_PROPERTIES = Set.of(
+    static final Set<String> REDUCED_PROPERTIES = Set.of(
             SAMPLE_DIMENSIONS_KEY, SAMPLE_RESOLUTIONS_KEY, STATISTICS_KEY);
 
     /**
@@ -166,6 +166,9 @@ class BandSelectImage extends SourceAlignedImage {
     /**
      * Returns the names of all recognized properties,
      * or {@code null} if this image has no properties.
+     * This method may conservatively return the names of properties that 
<em>may</em> exist.
+     * It does not check if the property would be an array with only null 
values,
+     * because doing that check may cause potentially costly computation.
      */
     @Override
     public String[] getPropertyNames() {
@@ -195,10 +198,13 @@ class BandSelectImage extends SourceAlignedImage {
             final Class<?> componentType = value.getClass().getComponentType();
             if (componentType != null) {
                 final Object reduced = Array.newInstance(componentType, 
bands.length);
+                boolean hasValue = false;
                 for (int i=0; i<bands.length; i++) {
-                    Array.set(reduced, i, Array.get(value, bands[i]));
+                    Object element = Array.get(value, bands[i]);
+                    Array.set(reduced, i, element);
+                    hasValue |= (element != null);
                 }
-                return reduced;
+                return hasValue ? reduced : Image.UndefinedProperty;
             }
         }
         return value;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java
index 9d18e48b88..8a42951f09 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
 import java.awt.Rectangle;
@@ -37,7 +38,6 @@ import org.apache.sis.coverage.privy.SampleDimensions;
 import org.apache.sis.coverage.privy.ColorScaleBuilder;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Disposable;
-import org.apache.sis.util.privy.UnmodifiableArrayList;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.NumberRange;
@@ -97,7 +97,7 @@ class BandedSampleConverter extends WritableComputedImage {
      *
      * @see #getProperty(String)
      */
-    private final SampleDimension[] sampleDimensions;
+    private final List<SampleDimension> sampleDimensions;
 
     /**
      * The sample resolutions, or {@code null} if unknown.
@@ -118,7 +118,7 @@ class BandedSampleConverter extends WritableComputedImage {
     private BandedSampleConverter(final RenderedImage source,  final 
BandedSampleModel sampleModel,
                                   final ColorModel colorModel, final 
NumberRange<?>[] ranges,
                                   final MathTransform1D[] converters,
-                                  final SampleDimension[] sampleDimensions)
+                                  final List<SampleDimension> sampleDimensions)
     {
         super(sampleModel, source);
         this.colorModel = colorModel;
@@ -169,14 +169,15 @@ class BandedSampleConverter extends WritableComputedImage 
{
                 r = Double.NaN;
             }
             /*
-             * The implicit source resolution if 1 on the assumption that we 
are converting from
+             * The implicit source resolution is 1 on the assumption that we 
are converting from
              * integer values. But if the source image specifies a resolution, 
use the specified
              * value instead of the implicit 1 value.
              */
             if (i < n) {
                 final Number v = (Number) Array.get(sr, i);
                 if (v != null) {
-                    r *= (v instanceof Float) ? 
DecimalFunctions.floatToDouble(v.floatValue()) : v.doubleValue();
+                    double f = (v instanceof Float) ? 
DecimalFunctions.floatToDouble(v.floatValue()) : v.doubleValue();
+                    if (f > 0) r *= f;      // Ignore also NaN.
                 }
             }
             resolutions[i] = r;
@@ -214,11 +215,11 @@ class BandedSampleConverter extends WritableComputedImage 
{
         }
         final int numBands = converters.length;
         final BandedSampleModel sampleModel = 
layout.createBandedSampleModel(source, null, targetType, numBands, 0);
-        final SampleDimension[] sampleDimensions = 
SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get();
+        final List<SampleDimension> sampleDimensions = 
SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get();
         final int visibleBand = ImageUtilities.getVisibleBand(source);
         ColorModel colorModel = ColorScaleBuilder.NULL_COLOR_MODEL;
         if (colorizer != null) {
-            var target = new Colorizer.Target(sampleModel, 
UnmodifiableArrayList.wrap(sampleDimensions), visibleBand);
+            var target = new Colorizer.Target(sampleModel, sampleDimensions, 
visibleBand);
             colorModel = colorizer.apply(target).orElse(null);
         }
         if (colorModel == null) {
@@ -228,8 +229,8 @@ class BandedSampleConverter extends WritableComputedImage {
              * If no sample dimension is specified, infer value range from 
data type.
              */
             SampleDimension sd = null;
-            if (sampleDimensions != null && visibleBand >= 0 && visibleBand < 
sampleDimensions.length) {
-                sd = sampleDimensions[visibleBand];
+            if (sampleDimensions != null && visibleBand >= 0 && visibleBand < 
sampleDimensions.size()) {
+                sd = sampleDimensions.get(visibleBand);
             }
             final var builder = new 
ColorScaleBuilder(ColorScaleBuilder.GRAYSCALE, null, false);
             if (builder.initialize(source.getSampleModel(), sd) ||
@@ -269,7 +270,7 @@ class BandedSampleConverter extends WritableComputedImage {
         switch (key) {
             case SAMPLE_DIMENSIONS_KEY: {
                 if (sampleDimensions != null) {
-                    return sampleDimensions.clone();
+                    return sampleDimensions.toArray(SampleDimension[]::new);
                 }
                 break;
             }
@@ -426,7 +427,7 @@ class BandedSampleConverter extends WritableComputedImage {
         Writable(final WritableRenderedImage source,  final BandedSampleModel 
sampleModel,
                  final ColorModel colorModel, final NumberRange<?>[] ranges,
                  final MathTransform1D[] converters, final MathTransform1D[] 
inverses,
-                 final SampleDimension[] sampleDimensions)
+                 final List<SampleDimension> sampleDimensions)
         {
             super(source, sampleModel, colorModel, ranges, converters, 
sampleDimensions);
             this.inverses = inverses;
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 2c6c9e363a..35bb45ab2a 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
@@ -150,6 +150,8 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
          * This information may be present if the image operation is invoked 
by a
          * {@link org.apache.sis.coverage.grid.GridCoverageProcessor} 
operation,
          * or if the source image contains the {@value 
PlanarImage#SAMPLE_DIMENSIONS_KEY} property
+         * Note that in the latter case, the list may contain null elements if 
this information is
+         * missing in some bands.
          *
          * @return description of the bands of the image to colorize.
          * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
index f3e660e94e..b547c827da 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
@@ -603,14 +603,15 @@ public class ImageProcessor implements Cloneable {
     /**
      * Returns statistics (minimum, maximum, mean, standard deviation) on each 
bands of the given image.
      * Invoking this method is equivalent to invoking the {@link #statistics 
statistics(…)} method and
-     * extracting immediately the statistics property value, except that custom
-     * {@linkplain #setErrorHandler error handlers} are supported.
+     * extracting immediately the statistics property value, except that this 
method guarantees that all
+     * statistics are non-null and supports custom {@linkplain 
#setErrorHandler error handlers}.
      *
      * <p>If {@code areaOfInterest} is null and {@code sampleFilters} is 
{@code null} or empty,
      * then the default behavior is as below:</p>
      * <ul>
      *   <li>If the {@value PlanarImage#STATISTICS_KEY} property value exists 
in the given image,
-     *       then that value is returned. Note that they are not necessarily 
statistics for the whole image.
+     *       then that value is returned with the null array elements (if any) 
replaced by computed values.
+     *       Note that the returned statistics are not necessarily for the 
whole image.
      *       They are whatever statistics the property provider considered as 
representative.</li>
      *   <li>Otherwise statistics are computed for the whole image.</li>
      * </ul>
@@ -629,7 +630,7 @@ public class ImageProcessor implements Cloneable {
      * </ul>
      *
      * <h4>Result relationship with source</h4>
-     * This method computes statistics immediately.
+     * This method fetches (from property values) or computes statistics 
immediately.
      * Changes in the {@code source} image after this method call do not 
change the results.
      *
      * @param  source          the image for which to compute statistics.
@@ -637,7 +638,7 @@ public class ImageProcessor implements Cloneable {
      * @param  sampleFilters   converters to apply on sample values before to 
add them to statistics, or
      *         {@code null} or an empty array if none. The array may have any 
length and may contain null elements.
      *         For all {@code i < numBands}, non-null {@code sampleFilters[i]} 
are applied to band <var>i</var>.
-     * @return the statistics of sample values in each band.
+     * @return the statistics of sample values in each band. Guaranteed 
non-null and without null element.
      * @throws ImagingOpException if an error occurred during calculation
      *         and the error handler is {@link ErrorHandler#THROW}.
      *
@@ -645,16 +646,36 @@ public class ImageProcessor implements Cloneable {
      * @see #filterNodataValues(Number...)
      * @see PlanarImage#STATISTICS_KEY
      */
-    public Statistics[] valueOfStatistics(final RenderedImage source, final 
Shape areaOfInterest,
+    public Statistics[] valueOfStatistics(RenderedImage source, final Shape 
areaOfInterest,
                                           final DoubleUnaryOperator... 
sampleFilters)
     {
         ArgumentChecks.ensureNonNull("source", source);
+        int[] bandsToCompute = null;
+        Statistics[] statistics = null;
         if (areaOfInterest == null && (sampleFilters == null || 
ArraysExt.allEquals(sampleFilters, null))) {
             final Object property = 
source.getProperty(PlanarImage.STATISTICS_KEY);
             if (property instanceof Statistics[]) {
-                return (Statistics[]) property;
+                statistics = ArraysExt.resize((Statistics[]) property, 
ImageUtilities.getNumBands(source));
+                /*
+                 * Verify that all array elements are non-null. If any null 
element is found,
+                 * we will compute statistics but only for the missing bands.
+                 */
+                bandsToCompute = new int[statistics.length];
+                int n = 0;
+                for (int i=0; i<statistics.length; i++) {
+                    if (statistics[i] == null) {
+                        bandsToCompute[n++] = i;
+                    }
+                }
+                if (n == 0) return statistics;
+                bandsToCompute = ArraysExt.resize(bandsToCompute, n);
+                source = selectBands(source, bandsToCompute);
             }
         }
+        /*
+         * Compute statistics either of all bands, or on a subset
+         * of the bands if only some of them have null statistics.
+         */
         final boolean parallel, failOnException;
         final ErrorHandler errorListener;
         synchronized (this) {
@@ -667,10 +688,17 @@ public class ImageProcessor implements Cloneable {
          * The way AnnotatedImage cache mechanism is implemented, if 
statistics results already
          * exist, they will be used.
          */
-        final AnnotatedImage calculator = new StatisticsCalculator(source, 
areaOfInterest, sampleFilters, parallel, failOnException);
+        final var calculator = new StatisticsCalculator(source, 
areaOfInterest, sampleFilters, parallel, failOnException);
         final Object property = 
calculator.getProperty(PlanarImage.STATISTICS_KEY);
         calculator.logAndClearError(ImageProcessor.class, "valueOfStatistics", 
errorListener);
-        return (Statistics[]) property;
+        final var computed = (Statistics[]) property;
+        if (bandsToCompute == null) {
+            return computed;
+        }
+        for (int i=0; i<bandsToCompute.length; i++) {
+            statistics[bandsToCompute[i]] = computed[i];
+        }
+        return statistics;
     }
 
     /**
@@ -803,6 +831,7 @@ public class ImageProcessor implements Cloneable {
      *
      * <b>Note:</b> if no value is associated to the {@code 
"sampleDimensions"} key, then the default
      * value will be the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY} image 
property value if defined.
+     * That value can be an array, in which case the sample dimension of the 
visible band is taken.
      *
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
index 05583a59a2..1e504d103d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
@@ -149,6 +149,11 @@ public abstract class PlanarImage implements RenderedImage 
{
      * Key for a property defining a conversion from pixel values to the units 
of measurement.
      * The value should be an array of {@link SampleDimension} instances.
      * The array length should be the number of bands.
+     * The array may contain null elements if this information is missing in 
some bands.
+     *
+     * <div class="note"><b>Example:</b> null elements may happen if this 
image is an
+     * {@linkplain ImageProcessor#aggregateBands(RenderedImage...) aggregation 
of bands}
+     * of two or more images, and some but not all images define this 
property.</div>
      *
      * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()
      *
@@ -169,7 +174,8 @@ public abstract class PlanarImage implements RenderedImage {
      * <p>Values should be instances of {@code double[]}.
      * The array length should be the number of bands. This property may be 
computed automatically during
      * {@linkplain 
org.apache.sis.coverage.grid.GridCoverage#forConvertedValues(boolean) 
conversions from
-     * integer values to floating point values}.</p>
+     * integer values to floating point values}. Values should be strictly 
positive and finite but may be
+     * {@link Double#NaN} if this information is unknown for a band.</p>
      */
     public static final String SAMPLE_RESOLUTIONS_KEY = 
"org.apache.sis.SampleResolutions";
 
@@ -180,11 +186,13 @@ public abstract class PlanarImage implements 
RenderedImage {
      * actually used in an image.
      *
      * <p>Values should be instances of <code>{@linkplain 
org.apache.sis.math.Statistics}[]</code>.
-     * The array length should be the number of bands. If this property is not 
provided, Apache SIS
-     * may have to {@linkplain ImageProcessor#statistics compute statistics 
itself}
-     * (by iterating over pixel values) when needed.</p>
+     * The array length should be the number of bands. Some array elements may 
be {@code null}
+     * if the statistics are not available for all bands.</p>
      *
-     * <p>Statistics are only indicative. They may be computed on an image 
sub-region.</p>
+     * <p>Statistics are only indicative. They may be computed on a subset of 
the sample values.
+     * If this property is not provided, some image rendering or exportation 
processes may have
+     * to {@linkplain ImageProcessor#statistics compute statistics themselves} 
by iterating over
+     * pixel values, which can be costly.</p>
      *
      * @see ImageProcessor#statistics(RenderedImage, Shape, 
DoubleUnaryOperator...)
      */
@@ -282,6 +290,8 @@ public abstract class PlanarImage implements RenderedImage {
     /**
      * Returns the names of all recognized properties,
      * or {@code null} if this image has no properties.
+     * This method may conservatively return the names of properties that 
<em>may</em> exist,
+     * when checking if they actually exist would cause a potentially costly 
computation.
      *
      * <p>The default implementation returns {@code null}.</p>
      *
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 ac3569851d..7c6f30f023 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
@@ -253,6 +253,7 @@ final class RecoloredImage extends ImageAdapter {
                 } else if (value instanceof Statistics) {
                     statistics = (Statistics) value;
                 } else if (value instanceof Statistics[]) {
+                    // Undocumented: one element per band, will keep only the 
visible band.
                     statsAllBands = (Statistics[]) value;
                 } else {
                     throw illegalPropertyType(modifiers, "statistics", value);
@@ -273,7 +274,7 @@ final class RecoloredImage extends ImageAdapter {
         if (Double.isNaN(minimum) || Double.isNaN(maximum)) {
             if (statistics == null) {
                 if (statsAllBands == null) {
-                    final DoubleUnaryOperator[] sampleFilters = new 
DoubleUnaryOperator[visibleBand + 1];
+                    final var sampleFilters = new 
DoubleUnaryOperator[visibleBand + 1];
                     sampleFilters[visibleBand] = 
ImageProcessor.filterNodataValues(nodataValues);
                     statsAllBands = processor.valueOfStatistics(statsSource, 
areaOfInterest, sampleFilters);
                 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
index a712ae34d9..06d0c8caa7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.Collections;
 import java.util.function.Function;
 import java.util.function.DoubleUnaryOperator;
 import java.awt.Color;
@@ -48,6 +49,7 @@ import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.privy.UnmodifiableArrayList;
 
 
 /**
@@ -149,7 +151,7 @@ final class Visualization extends ResampledImage {
         private MathTransform toSource;
 
         /** Description of {@link #source} bands, or {@code null} if none. */
-        private SampleDimension[] sampleDimensions;
+        private List<SampleDimension> sampleDimensions;
 
         //  ┌─────────────────────────────────────┐
         //  │ Given by ImageProcesor.configure(…) │
@@ -198,7 +200,7 @@ final class Visualization extends ResampledImage {
             if (sampleDimensions == null) {
                 Object ranges = source.getProperty(SAMPLE_DIMENSIONS_KEY);
                 if (ranges instanceof SampleDimension[]) {
-                    sampleDimensions = (SampleDimension[]) ranges;
+                    sampleDimensions = 
UnmodifiableArrayList.wrap((SampleDimension[]) ranges);
                 }
             }
         }
@@ -250,8 +252,8 @@ final class Visualization extends ResampledImage {
                 }
             }
             source = BandSelectImage.create(source, true, visibleBand);
-            final SampleDimension visibleSD = (sampleDimensions != null && 
visibleBand < sampleDimensions.length)
-                                            ? sampleDimensions[visibleBand] : 
null;
+            final SampleDimension visibleSD = (sampleDimensions != null && 
visibleBand < sampleDimensions.size())
+                                            ? 
sampleDimensions.get(visibleBand) : null;
             /*
              * If there is no conversion of pixel coordinates, there is no 
need for interpolations.
              * In such case the `Visualization.computeTile(…)` implementation 
takes a shortcut which
@@ -287,7 +289,7 @@ final class Visualization extends ResampledImage {
              * In precedence order:
              *
              *    - rangeColors      : Map<NumberRange<?>,Color[]>
-             *    - sampleDimensions : SampleDimension[]
+             *    - sampleDimensions : List<SampleDimension>
              *    - statistics
              */
             boolean initialized;
@@ -337,7 +339,7 @@ final class Visualization extends ResampledImage {
                  * If none of above `ColorScaleBuilder` configurations worked, 
use statistics in last resort.
                  * We do that after we reduced the image to a single band in 
order to reduce the amount of calculation.
                  */
-                final DoubleUnaryOperator[] sampleFilters = 
SampleDimensions.toSampleFilters(visibleSD);
+                final DoubleUnaryOperator[] sampleFilters = 
SampleDimensions.toSampleFilters(Collections.singletonList(visibleSD));
                 final Statistics statistics = 
processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND];
                 builder.initialize(statistics.minimum(), statistics.maximum(), 
sourceSM.getDataType());
             }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
index 18bd44bb2a..af6862fcba 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
@@ -18,11 +18,13 @@ package org.apache.sis.image;
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Hashtable;
 import java.util.stream.IntStream;
 import java.util.function.ObjIntConsumer;
 import java.awt.Rectangle;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -525,4 +527,36 @@ public final class BandAggregateImageTest extends TestCase 
{
             }
         }
     }
+
+    /**
+     * Verifies the aggregation of property values.
+     */
+    @Test
+    public void testProperties() {
+        final var p1 = new Hashtable<String,Object>();
+        final var p2 = new Hashtable<String,Object>();
+        assertNull(p1.put(PlanarImage.SAMPLE_RESOLUTIONS_KEY, new double[] {4, 
1, 3, 7}));
+        assertNull(p2.put(PlanarImage.SAMPLE_RESOLUTIONS_KEY, new double[] {2, 
8, 5, 6}));
+        final ColorModel cm = ColorModel.getRGBdefault();
+        final WritableRaster raster = cm.createCompatibleWritableRaster(1, 1);
+        final RenderedImage[] sources = {
+            new BufferedImage(cm, raster, false, p1),
+            new BufferedImage(cm, raster, false, p2)
+        };
+        RenderedImage result;
+        result = BandAggregateImage.create(sources, null, null, false, 
allowSharing, false);
+        assertArrayEquals(new String[] {PlanarImage.SAMPLE_RESOLUTIONS_KEY}, 
result.getPropertyNames());
+        assertArrayEquals(new double[] {4, 1, 3, 7, 2, 8, 5, 6},
+                (double[]) 
result.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY));
+        /*
+         * Same tests, but with a subset of the bands.
+         * This part of the test depends on `BandSelectImage`.
+         */
+        sources[0] = BandSelectImage.create(sources[0], false, 0, 2);
+        sources[1] = BandSelectImage.create(sources[1], false, 1, 3);
+        result = BandAggregateImage.create(sources, null, null, false, 
allowSharing, false);
+        assertArrayEquals(new String[] {PlanarImage.SAMPLE_RESOLUTIONS_KEY}, 
result.getPropertyNames());
+        assertArrayEquals(new double[] {4, 3, 8, 6},
+                (double[]) 
result.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY));
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
index c43355260f..c52769e002 100644
--- 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
+++ 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
@@ -200,7 +200,7 @@ public class RenderingData implements CloneAccess {
      * @see #setImageSpace(GridGeometry, List, int[])
      * @see #statistics()
      */
-    private SampleDimension[] dataRanges;
+    private List<SampleDimension> dataRanges;
 
     /**
      * Conversion or transformation from {@linkplain #data} CRS to {@linkplain 
PlanarCanvas#getObjectiveCRS()
@@ -312,7 +312,7 @@ public class RenderingData implements CloneAccess {
      */
     @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
     public final void setImageSpace(final GridGeometry domain, final 
List<SampleDimension> ranges, final int[] xyDims) {
-        dataRanges   = (ranges != null) ? 
ranges.toArray(SampleDimension[]::new) : null;
+        dataRanges   = ranges;
         dataGeometry = domain;
         xyDimensions = xyDims;
         processor.setFillValues(SampleDimensions.backgrounds(dataRanges));
@@ -544,7 +544,7 @@ public class RenderingData implements CloneAccess {
             }
             statistics = processor.valueOfStatistics(image, null, 
SampleDimensions.toSampleFilters(dataRanges));
         }
-        final Map<String,Object> modifiers = new HashMap<>(8);
+        final var modifiers = new HashMap<String,Object>(8);
         modifiers.put("statistics", statistics);
         modifiers.put("sampleDimensions", dataRanges);
         return modifiers;
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 e1a4218dba..d698893567 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
@@ -190,7 +190,9 @@ found:  if (property instanceof Statistics[]) {
             final var max = new double[numBands];
             for (int i=0; i<numBands; i++) {
                 final Statistics s = stats[i];
-                if (s.count() == 0) break found;
+                if (s == null || s.count() == 0) {
+                    break found;    // Some statistics are missing.
+                }
                 min[i] = s.minimum();
                 max[i] = s.maximum();
             }

Reply via email to