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 ebcc3ec698 Fix an `AssertionError` when using `BandedSampleConverter` 
with a tile size which is not a divisor of the source image size.
ebcc3ec698 is described below

commit ebcc3ec698c6cda9c21ab1161fea8ddea7d298b0
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Dec 2 12:00:14 2025 +0100

    Fix an `AssertionError` when using `BandedSampleConverter` with a tile size 
which is not a divisor of the source image size.
---
 .../apache/sis/image/BandedSampleConverter.java    | 63 ++++++++++++++++------
 .../main/org/apache/sis/image/ImageProcessor.java  | 10 ++--
 .../main/org/apache/sis/image/MaskedImage.java     |  4 +-
 .../main/org/apache/sis/image/Transferer.java      | 23 +++++---
 .../main/org/apache/sis/image/Visualization.java   |  2 +-
 .../sis/image/BandedSampleConverterTest.java       |  3 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  5 +-
 7 files changed, 78 insertions(+), 32 deletions(-)

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 a5a0c78fc7..e85f4c51a5 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
@@ -36,6 +36,7 @@ import 
org.apache.sis.coverage.internal.shared.SampleDimensions;
 import org.apache.sis.image.internal.shared.ImageUtilities;
 import org.apache.sis.image.internal.shared.TileOpExecutor;
 import org.apache.sis.image.internal.shared.ColorScaleBuilder;
+import org.apache.sis.image.internal.shared.FillValues;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Disposable;
 import org.apache.sis.util.logging.Logging;
@@ -104,6 +105,12 @@ class BandedSampleConverter extends WritableComputedImage {
      */
     private final double[] sampleResolutions;
 
+    /**
+     * Values to assign to the pixels outside source image.
+     * Can be {@code null} if the target image is fully contained inside the 
source image.
+     */
+    private final FillValues fillValues;
+
     /**
      * Creates a new image which will compute values using the given 
converters.
      *
@@ -114,17 +121,29 @@ class BandedSampleConverter extends WritableComputedImage 
{
      * @param  converters   the transfer functions to apply on each band of 
the source image.
      *                      If this array was a user-provided parameter, 
should be cloned by caller.
      * @param  sampleDimensions  description of conversion result, or {@code 
null} if unknown.
+     * @param  fillValues   values to assign to pixels outside source image, 
or {@code null} for default values.
      */
-    private BandedSampleConverter(final RenderedImage source,  final 
BandedSampleModel sampleModel,
-                                  final ColorModel colorModel, final 
NumberRange<?>[] ranges,
-                                  final MathTransform1D[] converters,
-                                  final List<SampleDimension> sampleDimensions)
+    private BandedSampleConverter(final RenderedImage         source,
+                                  final BandedSampleModel     sampleModel,
+                                  final ColorModel            colorModel,
+                                  final NumberRange<?>[]      ranges,
+                                  final MathTransform1D[]     converters,
+                                  final List<SampleDimension> sampleDimensions,
+                                  final Number[]              fillValues)
     {
         super(sampleModel, source);
-        this.colorModel = colorModel;
-        this.converters = converters;
+        this.colorModel       = colorModel;
+        this.converters       = converters;
         this.sampleDimensions = sampleDimensions;
         ensureCompatible(sampleModel, colorModel);
+        FillValues fill = null;
+        if (!ImageUtilities.getBounds(source).contains(super.getBounds())) {
+            fill = new FillValues(sampleModel, fillValues, true);
+            if (fill.isFullyZero) {
+                fill = null;
+            }
+        }
+        this.fillValues = fill;
         /*
          * Get an estimation of the resolution, arbitrarily looking in the 
middle of the range of values.
          * If the converters are linear (which is the most common case), the 
middle value does not matter
@@ -197,13 +216,18 @@ class BandedSampleConverter extends WritableComputedImage 
{
      * @param  converters    the transfer functions to apply on each band of 
the source image.
      * @param  targetType    the type of this image resulting from conversion 
of given image.
      * @param  colorizer     provider of color model for the expected range of 
values, or {@code null}.
+     * @param  fillValues    values to assign to pixels outside source image, 
or {@code null} for default values.
      * @return the image which compute converted values from the given source.
      *
      * @see ImageProcessor#convert(RenderedImage, NumberRange[], 
MathTransform1D[], DataType)
      */
-    static BandedSampleConverter create(RenderedImage source, final 
ImageLayout layout,
-            final NumberRange<?>[] sourceRanges, final MathTransform1D[] 
converters,
-            final DataType targetType, final Colorizer colorizer)
+    static BandedSampleConverter create(RenderedImage     source,
+                                  final ImageLayout       layout,
+                                  final NumberRange<?>[]  sourceRanges,
+                                  final MathTransform1D[] converters,
+                                  final DataType          targetType,
+                                  final Colorizer         colorizer,
+                                  final Number[]          fillValues)
     {
         /*
          * Since this operation applies its own ColorModel anyway, skip 
operation that was doing nothing else
@@ -250,11 +274,12 @@ class BandedSampleConverter extends WritableComputedImage 
{
             for (int i=0; i<numBands; i++) {
                 inverses[i] = converters[i].inverse();
             }
-            return new Writable((WritableRenderedImage) source, sampleModel, 
colorModel, sourceRanges, converters, inverses, sampleDimensions);
+            return new Writable((WritableRenderedImage) source, sampleModel, 
colorModel,
+                                sourceRanges, converters, inverses, 
sampleDimensions, fillValues);
         } catch (NoninvertibleTransformException e) {
             Logging.recoverableException(LOGGER, ImageProcessor.class, 
"convert", e);
         }
-        return new BandedSampleConverter(source, sampleModel, colorModel, 
sourceRanges, converters, sampleDimensions);
+        return new BandedSampleConverter(source, sampleModel, colorModel, 
sourceRanges, converters, sampleDimensions, fillValues);
     }
 
     /**
@@ -391,7 +416,7 @@ class BandedSampleConverter extends WritableComputedImage {
         if (target == null) {
             target = createTile(tileX, tileY);
         }
-        Transferer.create(getSource(), target).compute(converters);
+        Transferer.create(getSource(), target, fillValues).compute(converters);
         return target;
     }
 
@@ -424,12 +449,16 @@ class BandedSampleConverter extends WritableComputedImage 
{
         /**
          * Creates a new writable image which will compute values using the 
given converters.
          */
-        Writable(final WritableRenderedImage source,  final BandedSampleModel 
sampleModel,
-                 final ColorModel colorModel, final NumberRange<?>[] ranges,
-                 final MathTransform1D[] converters, final MathTransform1D[] 
inverses,
-                 final List<SampleDimension> sampleDimensions)
+        Writable(final WritableRenderedImage source,
+                 final BandedSampleModel     sampleModel,
+                 final ColorModel            colorModel,
+                 final NumberRange<?>[]      ranges,
+                 final MathTransform1D[]     converters,
+                 final MathTransform1D[]     inverses,
+                 final List<SampleDimension> sampleDimensions,
+                 final Number[]              fillValues)
         {
-            super(source, sampleModel, colorModel, ranges, converters, 
sampleDimensions);
+            super(source, sampleModel, colorModel, ranges, converters, 
sampleDimensions, fillValues);
             this.inverses = inverses;
         }
 
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 174f50b33d..3202254ded 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
@@ -1115,13 +1115,15 @@ public class ImageProcessor implements Cloneable {
             ArgumentChecks.ensureNonNullElement("converters", i, 
converters[i]);
         }
         final ImageLayout layout;
-        final Colorizer colorizer;
+        final Colorizer   colorizer;
+        final Number[]    fillValues;
         synchronized (this) {
-            layout = this.layout;
-            colorizer = this.colorizer;
+            layout     = this.layout;
+            colorizer  = this.colorizer;
+            fillValues = this.fillValues;
         }
         // No need to clone `sourceRanges` because it is not stored by 
`BandedSampleConverter`.
-        return unique(BandedSampleConverter.create(source, layout, 
sourceRanges, converters, targetType, colorizer));
+        return unique(BandedSampleConverter.create(source, layout, 
sourceRanges, converters, targetType, colorizer, fillValues));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskedImage.java
index 2c1412ed79..a3f325f1b1 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskedImage.java
@@ -180,6 +180,7 @@ final class MaskedImage extends SourceAlignedImage {
     private synchronized ByteBuffer getMask() {
         ByteBuffer mask;
         if (maskRef == null || (mask = maskRef.get()) == null) {
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
             final Rectangle maskBounds = this.maskBounds;
             int size = ceilDiv(maskBounds.width, Byte.SIZE) * 
maskBounds.height;
             final int r = size & (Long.BYTES - 1);
@@ -250,6 +251,7 @@ final class MaskedImage extends SourceAlignedImage {
          * Tile may intersect the mask. Computation is necessary, but we may 
discover at
          * the end of this method that the result is still an empty tile or 
source tile.
          */
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Rectangle maskBounds = this.maskBounds;
         final LongBuffer mask = getMask().asLongBuffer();
         final int xmax   = xmin + source.getTileWidth();
@@ -466,7 +468,7 @@ complete:   for (int border = 0; ; border++) {
     @Override
     public boolean equals(final Object object) {
         if (super.equals(object)) {
-            final MaskedImage other = (MaskedImage) object;
+            final var other = (MaskedImage) object;
             return clip.equals(other.clip) && 
fillValues.equals(other.fillValues);
         }
         return false;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Transferer.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Transferer.java
index 79c7aedeb4..c86e539bdc 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Transferer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Transferer.java
@@ -29,6 +29,7 @@ import java.awt.image.RasterFormatException;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.image.internal.shared.ImageUtilities;
+import org.apache.sis.image.internal.shared.FillValues;
 import org.apache.sis.system.Configuration;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.util.internal.shared.Numerics;
@@ -537,17 +538,27 @@ abstract class Transferer {
      * Suggests a strategy for transferring data from the given source image 
to the given target.
      * This method assumes that the source image uses the same pixel 
coordinate system than the
      * target raster (i.e. that pixels at the same coordinates are at the same 
location on Earth).
-     * It also assumes that the target tile is fully included in the bounds of 
a single source tile.
-     * That later condition is met if the target grid tiles has been created 
by {@link ImageLayout}.
      *
-     * @param  source  image from which to read sample values.
-     * @param  target  image tile where to write sample values after 
processing.
+     * <p>The target tile should be fully included in the bounds of a single 
source image's tile.
+     * That later condition is met if the target tile matrix was computed by 
{@link ImageLayout}.
+     * However, the target tile may be larger in the last row and last column 
of the tile matrix
+     * if {@link ImageLayout#isImageBoundsAdjustmentAllowed} was {@code true}. 
This method clips
+     * the area of interest for that reason.</p>
+     *
+     * @param  source      image from which to read sample values.
+     * @param  target      image tile where to write sample values after 
processing.
+     * @param  fillValues  the fill values, or {@code null} if none.
      * @return object to use for applying the operation.
      */
-    static Transferer create(final RenderedImage source, final WritableRaster 
target) {
+    static Transferer create(final RenderedImage source, final WritableRaster 
target, final FillValues fillValues) {
+        final Rectangle aoi = target.getBounds();
+        ImageUtilities.clipBounds(source, aoi);
+        if (fillValues != null && (aoi.width != target.getWidth() || 
aoi.height != target.getHeight())) {
+            fillValues.fill(target);
+        }
         int tileX = ImageUtilities.pixelToTileX(source, target.getMinX());
         int tileY = ImageUtilities.pixelToTileY(source, target.getMinY());
-        return create(source.getTile(tileX, tileY), target, 
target.getBounds());
+        return create(source.getTile(tileX, tileY), target, aoi);
     }
 
     /**
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 6a7b97239c..002563ee75 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
@@ -529,7 +529,7 @@ final class Visualization extends ResampledImage {
             tile = createTile(tileX, tileY);
         }
         // Conversion only, when no resampling is needed.
-        Transferer.create(getSource(), tile).compute(converters);
+        Transferer.create(getSource(), tile, null).compute(converters);
         return tile;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
index 42ffbd71c8..38ffe78401 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
@@ -32,6 +32,7 @@ import static 
org.apache.sis.feature.Assertions.assertValuesEqual;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class BandedSampleConverterTest extends ImageTestCase {
     /**
      * Size of tiles in this test. The width should be different than the 
height
@@ -70,7 +71,7 @@ public final class BandedSampleConverterTest extends 
ImageTestCase {
         source.initializeAllTiles(0);
         image = BandedSampleConverter.create(source, ImageLayout.DEFAULT, null,
                 new MathTransform1D[] {(MathTransform1D) 
MathTransforms.linear(scale, 0)},
-                targetType, null);
+                targetType, null, null);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 84004a13d4..f806703e7d 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -702,13 +702,14 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      *
      * @since 1.5
      */
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     public synchronized GridCoverageResource append(final RenderedImage image, 
final GridGeometry grid, final Metadata metadata)
             throws DataStoreException
     {
         final int index;
         try {
-            @SuppressWarnings("LocalVariableHidesMemberVariable") final Writer 
writer = writer();
-            @SuppressWarnings("LocalVariableHidesMemberVariable") final Reader 
reader = this.reader;
+            final Reader reader = this.reader;
+            final Writer writer = writer();
             writer.synchronize(reader, false);
             final long offsetIFD;
             try {

Reply via email to