You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/04/12 10:35:09 UTC

[sis] branch geoapi-4.0 updated: Chains of operations on images need `BufferedImage` to notify when data are changed.

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 f83bef1818 Chains of operations on images need `BufferedImage` to notify when data are changed.
f83bef1818 is described below

commit f83bef1818bd74e750c4aca3ba9d347e9a902b94
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Apr 12 12:32:13 2023 +0200

    Chains of operations on images need `BufferedImage` to notify when data are changed.
---
 .../sis/coverage/grid/GridCoverageBuilder.java     |   3 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   3 +-
 .../java/org/apache/sis/image/BandSelectImage.java |   3 +-
 .../apache/sis/image/WritableComputedImage.java    |   8 +-
 .../sis/internal/coverage/j2d/ObservableImage.java | 289 +++++++++++++++++++++
 .../sis/internal/coverage/j2d/RasterFactory.java   |   4 +-
 .../internal/coverage/j2d/WritableTiledImage.java  |   8 +-
 .../sis/internal/coverage/j2d/WriteSupport.java    | 100 -------
 .../sis/internal/sql/postgis/RasterReader.java     |   3 +-
 .../sis/internal/storage/esri/RasterStore.java     |   4 +-
 10 files changed, 309 insertions(+), 116 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index c6b048c31c..4119687eac 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -37,6 +37,7 @@ import org.apache.sis.image.PlanarImage;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 import org.apache.sis.internal.coverage.j2d.TiledImage;
 import org.apache.sis.internal.coverage.j2d.WritableTiledImage;
 import org.apache.sis.internal.feature.Resources;
@@ -489,7 +490,7 @@ public class GridCoverageBuilder {
                 if (raster instanceof WritableRaster) {
                     final WritableRaster wr = (WritableRaster) raster;
                     if (colors != null && (wr.getMinX() | wr.getMinY()) == 0) {
-                        image = new BufferedImage(colors, wr, false, properties);
+                        image = new ObservableImage(colors, wr, false, properties);
                     } else {
                         image = new WritableTiledImage(properties, colors, wr.getWidth(), wr.getHeight(), 0, 0, wr);
                     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index d9b0fef705..a34fe0ffe8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -43,6 +43,7 @@ import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
 import org.apache.sis.internal.coverage.j2d.DeferredProperty;
 import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 import org.apache.sis.internal.coverage.j2d.TiledImage;
 import org.apache.sis.internal.coverage.j2d.WritableTiledImage;
 import org.apache.sis.internal.feature.Resources;
@@ -792,7 +793,7 @@ public class ImageRenderer {
      * The use of a {@link BufferedImage} subclass is desired because Java2D rendering pipeline has optimizations
      * in the form {@code if (image instanceof BufferedImage)}.
      */
-    private static final class Untiled extends BufferedImage {
+    private static final class Untiled extends ObservableImage {
         /**
          * The value associated to the {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key,
          * or {@code null} if not yet computed.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index a5aeaff2cd..45ad9e2a27 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -33,6 +33,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 
 
 /**
@@ -153,7 +154,7 @@ class BandSelectImage extends SourceAlignedImage {
                     properties.put(key, value);
                 }
             }
-            image = new BufferedImage(cm,
+            image = new ObservableImage(cm,
                     bi.getRaster().createWritableChild(0, 0, bi.getWidth(), bi.getHeight(), 0, 0, bands),
                     bi.isAlphaPremultiplied(), properties);
         } else if (source instanceof WritableRenderedImage) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java
index adcfd1b6fc..5a8cdebf0f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java
@@ -22,7 +22,7 @@ import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
-import org.apache.sis.internal.coverage.j2d.WriteSupport;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 
 
 /**
@@ -106,7 +106,7 @@ abstract class WritableComputedImage extends ComputedImage {
      * @param  observer  the observer to notify.
      */
     public synchronized void addTileObserver(final TileObserver observer) {
-        observers = WriteSupport.addTileObserver(observers, observer);
+        observers = ObservableImage.addTileObserver(observers, observer);
     }
 
     /**
@@ -117,7 +117,7 @@ abstract class WritableComputedImage extends ComputedImage {
      * @param  observer  the observer to stop notifying.
      */
     public synchronized void removeTileObserver(final TileObserver observer) {
-        observers = WriteSupport.removeTileObserver(observers, observer);
+        observers = ObservableImage.removeTileObserver(observers, observer);
     }
 
     /**
@@ -131,7 +131,7 @@ abstract class WritableComputedImage extends ComputedImage {
     protected boolean markTileWritable(final int tileX, final int tileY, final boolean writing) {
         final boolean notify = super.markTileWritable(tileX, tileY, writing);
         if (notify && this instanceof WritableRenderedImage) {
-            WriteSupport.fireTileUpdate(observers, (WritableRenderedImage) this, tileX, tileY, writing);
+            ObservableImage.fireTileUpdate(observers, (WritableRenderedImage) this, tileX, tileY, writing);
         }
         return notify;
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ObservableImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ObservableImage.java
new file mode 100644
index 0000000000..e5319541cf
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ObservableImage.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.awt.Point;
+import java.awt.image.TileObserver;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * A buffered image which can notify tile observers when tile are acquired fir write operations.
+ * Provides also helper methods for {@link WritableRenderedImage} implementations.
+ *
+ * <p>This class should be used in preference to {@link BufferedImage} when the image may be the
+ * source of {@link org.apache.sis.image.ImageProcessor} operations. It is the case In particular
+ * when this image is given to {@link org.apache.sis.coverage.grid.GridCoverage2D} constructor.
+ * We can not prevent {@link BufferedImage} to implement {@link WritableRenderedImage}, but we
+ * can give a change to Apache SIS to be notified about modifications to pixel data.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.1
+ */
+public class ObservableImage extends BufferedImage {
+    /**
+     * The observers, or {@code null} if none. This is a copy-on-write array:
+     * values are never modified after construction (new arrays are created).
+     *
+     * This field is declared volatile because it is read without synchronization by
+     * {@link #getWritableTile(int, int)} and {@link #releaseWritableTile(int, int)}.
+     * Since this is a copy-on-write array, it is okay to omit synchronization for
+     * those methods but we still need the memory effect.
+     */
+    @SuppressWarnings("VolatileArrayField")
+    private volatile TileObserver[] observers;
+
+    /**
+     * Number of times that the tile has been acquired for writing and not yet released.
+     * Write operations on this field should be in synchronized blocks.
+     */
+    private volatile int writeCount;
+
+    /**
+     * Creates an image of the specified type.
+     *
+     * @param width   image width.
+     * @param height  image height.
+     * @param type    one of {@code TYPE_*} constants.
+     */
+    public ObservableImage(int width, int height, int type) {
+        super(width, height, type);
+    }
+
+    /**
+     * Creates an image using the specified raster.
+     *
+     * @param colors  color model of the new image.
+     * @param raster  the singleton raster for the image data.
+     * @param isRasterPremultiplied   whether data in the raster has been premultiplied with alpha.
+     * @param properties  image properties as ({@code String}, {@code Object}) entries.
+     */
+    public ObservableImage(ColorModel colors, WritableRaster raster, boolean isRasterPremultiplied, Hashtable<?,?> properties) {
+        super(colors, raster, isRasterPremultiplied, properties);
+    }
+
+    /**
+     * Returns a new array with the specified observer added to the array of observers.
+     * If the observer is already present, it will receive multiple notifications.
+     *
+     * @param  observers  the array where to add the observer, or {@code null}.
+     * @param  observer   the observer to add. Null values are ignored.
+     * @return the updated array of observers.
+     */
+    public static TileObserver[] addTileObserver(TileObserver[] observers, final TileObserver observer) {
+        if (observer != null) {
+            if (observers == null) {
+                return new TileObserver[] {observer};
+            }
+            final int n = observers.length;
+            observers = Arrays.copyOf(observers, n+1);
+            observers[n] = observer;
+        }
+        return observers;
+    }
+
+    /**
+     * Returns a new array with the specified observer removed from the specified array of observers.
+     * If the observer was not registered, nothing happens and the given array is returned as-is.
+     * If the observer was registered for multiple notifications, it will now be registered for one fewer.
+     *
+     * @param  observers  the array where to remove the observer, or {@code null}.
+     * @param  observer   the observer to remove.
+     * @return the updated array of observers.
+     */
+    public static TileObserver[] removeTileObserver(final TileObserver[] observers, final TileObserver observer) {
+        if (observers != null) {
+            for (int i=observers.length; --i >= 0;) {
+                if (observers[i] == observer) {
+                    return ArraysExt.remove(observers, i, 1);
+                }
+            }
+        }
+        return observers;
+    }
+
+    /**
+     * Notifies all listeners that the specified tile has been checked out for writing or has been released.
+     *
+     * @param observers       the observers to notify, or {@code null} if none.
+     * @param image           the image that owns the tile.
+     * @param tileX           the <var>x</var> index of the tile that is being updated.
+     * @param tileY           the <var>y</var> index of the tile that is being updated.
+     * @param willBeWritable  if {@code true}, the tile will be grabbed for writing; otherwise it is being released.
+     */
+    public static void fireTileUpdate(final TileObserver[] observers, final WritableRenderedImage image,
+                                      final int tileX, final int tileY, final boolean willBeWritable)
+    {
+        if (observers != null) {
+            for (final TileObserver observer : observers) {
+                observer.tileUpdate(image, tileX, tileY, willBeWritable);
+            }
+        }
+    }
+
+    /**
+     * Adds an observer to be notified when a tile is checked out for writing.
+     * If the observer is already present, it will receive multiple notifications.
+     *
+     * @param  observer  the observer to notify.
+     */
+    @Override
+    public synchronized void addTileObserver(final TileObserver observer) {
+        observers = addTileObserver(observers, observer);
+    }
+
+    /**
+     * Removes an observer from the list of observers notified when a tile is checked out for writing.
+     * If the observer was not registered, nothing happens. If the observer was registered for multiple
+     * notifications, it will now be registered for one fewer.
+     *
+     * @param  observer  the observer to stop notifying.
+     */
+    @Override
+    public synchronized void removeTileObserver(final TileObserver observer) {
+        observers = removeTileObserver(observers, observer);
+    }
+
+    /**
+     * Notifies all listeners that the specified tile has been checked out for writing or has been released.
+     * The notifications are sent only if the given {@code count} is zero.
+     *
+     * @param count           value of {@link #writeCount} before increment or after decrement.
+     * @param willBeWritable  if {@code true}, the tile will be grabbed for writing; otherwise it is being released.
+     */
+    private void fireTileUpdate(final int count, final boolean willBeWritable) {
+        if (count == 0) {
+            fireTileUpdate(observers, this, 0, 0, willBeWritable);
+        }
+    }
+
+    /**
+     * Checks out a tile for writing. If the same tile is checked out many times
+     * before to be released, only the first checkout is notified to listeners.
+     *
+     * @param  tileX  the <var>x</var> index of the tile.
+     * @param  tileY  the <var>y</var> index of the tile.
+     * @return the specified tile as a writable tile.
+     * @throws IndexOutOfBoundsException if a given tile index is out of bounds.
+     */
+    @Override
+    public WritableRaster getWritableTile(final int tileX, final int tileY) {
+        if ((tileX | tileY) != 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        final WritableRaster tile = super.getWritableTile(tileX, tileY);
+        final int count;
+        synchronized (this) {
+            count = writeCount++;
+        }
+        // Should be outside the synchronized block.
+        fireTileUpdate(count, true);
+        return tile;
+    }
+
+    /**
+     * Relinquishes the right to write to a tile. If the tile goes from having
+     * one writer to having no writers, then the listeners are notified.
+     *
+     * @param  tileX  the <var>x</var> index of the tile.
+     * @param  tileY  the <var>y</var> index of the tile.
+     * @throws IndexOutOfBoundsException if a given tile index is out of bounds.
+     */
+    @Override
+    public void releaseWritableTile(final int tileX, final int tileY) {
+        if ((tileX | tileY) != 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        final int count;
+        synchronized (this) {
+            count = --writeCount;
+            if (count < 0) writeCount = 0;
+        }
+        if (count < 0) {
+            throw new IllegalArgumentException(Resources.format(Resources.Keys.TileNotWritable_2, tileX, tileY));
+        }
+        // Should be outside the synchronized block.
+        fireTileUpdate(count, false);
+    }
+
+    /**
+     * Returns whether a tile is currently checked out for writing.
+     *
+     * @param  tileX  the <var>x</var> index of the tile.
+     * @param  tileY  the <var>y</var> index of the tile.
+     * @return {@code true} if specified tile is checked out for writing, {@code false} otherwise.
+     */
+    @Override
+    public boolean isTileWritable(final int tileX, final int tileY) {
+        return (tileX | tileY) == 0 && writeCount != 0;
+    }
+
+    /**
+     * Returns the indices of all tiles checked out for writing.
+     * Returns {@code null} if none are checked out.
+     *
+     * @return indices of tiles that are checked out for writing, or {@code null} if none.
+     */
+    @Override
+    public Point[] getWritableTileIndices() {
+        return writeCount == 0 ? null : new Point[] {new Point()};
+    }
+
+    /**
+     * Returns whether any tile is checked out for writing.
+     *
+     * @return {@code true} if any tiles are checked out for writing, or {@code false} otherwise.
+     */
+    @Override
+    public boolean hasTileWriters() {
+        return writeCount != 0;
+    }
+
+    /**
+     * Sets a region of the image to the contents of the given raster.
+     * The raster is assumed to be in the same coordinate space as this image.
+     * The operation is clipped to the bounds of this image.
+     *
+     * @param  data  the values to write in this image.
+     */
+    @Override
+    public void setData(final Raster data) {
+        int count;
+        synchronized (this) {
+            count = writeCount++;
+        }
+        fireTileUpdate(count, true);
+        try {
+            super.setData(data);
+        } finally {
+            synchronized (this) {
+                // Similar to `releaseWritableTile(…)` but without throwing exception.
+                writeCount = count = Math.max(0, writeCount - 1);
+            }
+            fireTileUpdate(count, false);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index c8a31f3b97..8cf31909e4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -93,14 +93,14 @@ public final class RasterFactory extends Static {
             case DataBuffer.TYPE_BYTE:
             case DataBuffer.TYPE_USHORT: {
                 if (numComponents == 1 && ColorModelFactory.isStandardRange(dataType, minimum, maximum)) {
-                    return new BufferedImage(width, height, (dataType == DataBuffer.TYPE_BYTE)
+                    return new ObservableImage(width, height, (dataType == DataBuffer.TYPE_BYTE)
                                 ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_USHORT_GRAY);
                 }
                 break;
             }
         }
         final ColorModel cm = ColorModelFactory.createGrayScale(dataType, numComponents, visibleBand, minimum, maximum);
-        return new BufferedImage(cm, cm.createCompatibleWritableRaster(width, height), false, null);
+        return new ObservableImage(cm, cm.createCompatibleWritableRaster(width, height), false, null);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WritableTiledImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WritableTiledImage.java
index 88f9de5e94..72facf9a4f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WritableTiledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WritableTiledImage.java
@@ -84,7 +84,7 @@ public class WritableTiledImage extends TiledImage implements WritableRenderedIm
      */
     @Override
     public synchronized void addTileObserver(final TileObserver observer) {
-        observers = WriteSupport.addTileObserver(observers, observer);
+        observers = ObservableImage.addTileObserver(observers, observer);
     }
 
     /**
@@ -96,7 +96,7 @@ public class WritableTiledImage extends TiledImage implements WritableRenderedIm
      */
     @Override
     public synchronized void removeTileObserver(final TileObserver observer) {
-        observers = WriteSupport.removeTileObserver(observers, observer);
+        observers = ObservableImage.removeTileObserver(observers, observer);
     }
 
     /**
@@ -116,7 +116,7 @@ public class WritableTiledImage extends TiledImage implements WritableRenderedIm
             count = writables.merge(key, 1, (old, one) -> old + 1);
         }
         if (count <= 1) {
-            WriteSupport.fireTileUpdate(observers, this, tileX, tileY, true);
+            ObservableImage.fireTileUpdate(observers, this, tileX, tileY, true);
         }
         return tile;
     }
@@ -145,7 +145,7 @@ public class WritableTiledImage extends TiledImage implements WritableRenderedIm
             throw new IllegalArgumentException(Resources.format(Resources.Keys.TileNotWritable_2, tileX, tileY));
         }
         if (close) {
-            WriteSupport.fireTileUpdate(observers, this, tileX, tileY, false);
+            ObservableImage.fireTileUpdate(observers, this, tileX, tileY, false);
         }
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WriteSupport.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WriteSupport.java
deleted file mode 100644
index 547120e467..0000000000
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/WriteSupport.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.coverage.j2d;
-
-import java.util.Arrays;
-import java.awt.image.TileObserver;
-import java.awt.image.WritableRenderedImage;
-import org.apache.sis.util.ArraysExt;
-
-
-/**
- * Helper methods for {@link WritableRenderedImage} implementations.
- *
- * <p>A future version of this class may extends {@code PlanarImage} or {@code ComputedImage}.
- * We have not yet decided which case would be useful.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- */
-public final class WriteSupport {
-    /**
-     * Do not allow (for now) instantiation of this class.
-     */
-    private WriteSupport() {
-    }
-
-    /**
-     * Returns a new array with the specified observer added to the array of observers.
-     * If the observer is already present, it will receive multiple notifications.
-     *
-     * @param  observers  the array where to add the observer, or {@code null}.
-     * @param  observer   the observer to add. Null values are ignored.
-     * @return the updated array of observers.
-     */
-    public static TileObserver[] addTileObserver(TileObserver[] observers, final TileObserver observer) {
-        if (observer != null) {
-            if (observers == null) {
-                return new TileObserver[] {observer};
-            }
-            final int n = observers.length;
-            observers = Arrays.copyOf(observers, n+1);
-            observers[n] = observer;
-        }
-        return observers;
-    }
-
-    /**
-     * Returns a new array with the specified observer removed from the specified array of observers.
-     * If the observer was not registered, nothing happens and the given array is returned as-is.
-     * If the observer was registered for multiple notifications, it will now be registered for one fewer.
-     *
-     * @param  observers  the array where to remove the observer, or {@code null}.
-     * @param  observer   the observer to remove.
-     * @return the updated array of observers.
-     */
-    public static TileObserver[] removeTileObserver(final TileObserver[] observers, final TileObserver observer) {
-        if (observers != null) {
-            for (int i=observers.length; --i >= 0;) {
-                if (observers[i] == observer) {
-                    return ArraysExt.remove(observers, i, 1);
-                }
-            }
-        }
-        return observers;
-    }
-
-    /**
-     * Notifies all listeners that the specified tile has been checked out for writing.
-     *
-     * @param observers       the observers to notify, or {@code null} if none.
-     * @param image           the image that owns the tile.
-     * @param tileX           the <var>x</var> index of the tile that is being updated.
-     * @param tileY           the <var>y</var> index of the tile that is being updated.
-     * @param willBeWritable  if {@code true}, the tile will be grabbed for writing; otherwise it is being released.
-     */
-    public static void fireTileUpdate(final TileObserver[] observers, final WritableRenderedImage image,
-                                      final int tileX, final int tileY, final boolean willBeWritable)
-    {
-        if (observers != null) {
-            for (final TileObserver observer : observers) {
-                observer.tileUpdate(image, tileX, tileY, willBeWritable);
-            }
-        }
-    }
-}
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
index 70c5633727..5213e9cdad 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
@@ -44,6 +44,7 @@ import org.apache.sis.coverage.grid.GridCoverage2D;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 import org.apache.sis.internal.storage.io.InputStreamArrayGetter;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
@@ -337,7 +338,7 @@ public final class RasterReader extends RasterFormat {
             }
             cm = ColorModelFactory.createGrayScale(dataType, numBands, visibleBand, minimum, maximum);
         }
-        return new BufferedImage(cm, raster, false, null);
+        return new ObservableImage(cm, raster, false, null);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
index fc83cb467c..ab1c177127 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
@@ -28,7 +28,6 @@ import java.nio.file.Path;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
-import java.awt.image.BufferedImage;
 import java.awt.image.WritableRaster;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
@@ -48,6 +47,7 @@ import org.apache.sis.internal.storage.PRJDataStore;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.ObservableImage;
 import org.apache.sis.internal.coverage.RangeArgument;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
@@ -490,7 +490,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource
                 cm = ColorModelFactory.createGrayScale(data.getSampleModel(), VISIBLE_BAND, band.getSampleRange().orElse(null));
             }
         }
-        return new GridCoverage2D(domain, Arrays.asList(bands), new BufferedImage(cm, data, false, properties));
+        return new GridCoverage2D(domain, Arrays.asList(bands), new ObservableImage(cm, data, false, properties));
     }
 
     /**