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 81614ecb87 Add a "World File" writer.
81614ecb87 is described below

commit 81614ecb870fec12f2b11260603b51401d456c5a
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Apr 19 19:22:57 2022 +0200

    Add a "World File" writer.
    
    https://issues.apache.org/jira/browse/SIS-541
---
 .../org/apache/sis/coverage/grid/GridGeometry.java |   4 +-
 .../sis/internal/referencing/j2d/AffineMatrix.java |   3 +-
 .../sis/internal/util/ListOfUnknownSize.java       |   4 +-
 .../apache/sis/internal/storage/PRJDataStore.java  |   2 +-
 .../org/apache/sis/internal/storage/Resources.java |  10 +
 .../sis/internal/storage/Resources.properties      |   2 +
 .../sis/internal/storage/Resources_fr.properties   |   2 +
 .../sis/internal/storage/image/FormatFilter.java   |   8 +-
 .../apache/sis/internal/storage/image/Image.java   | 141 +++---
 .../apache/sis/internal/storage/image/Store.java   | 337 ++++++++++++--
 .../sis/internal/storage/image/StoreProvider.java  |  13 +-
 .../internal/storage/image/WarningListener.java    |  18 +-
 .../sis/internal/storage/image/WritableImage.java  |  76 +++
 .../sis/internal/storage/image/WritableStore.java  | 515 +++++++++++++++++++++
 .../storage/image/SelfConsistencyTest.java         |   2 +-
 .../sis/internal/storage/image/StoreTest.java      |   2 +-
 16 files changed, 1018 insertions(+), 121 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 16c57d9f77..7e5ea0705c 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1266,8 +1266,8 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      *
      * @param  bitmask  any combination of {@link #CRS}, {@link #ENVELOPE}, 
{@link #EXTENT},
      *         {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
-     * @return {@code true} if all specified attributes are defined (i.e. 
invoking the
-     *         corresponding method will not thrown an {@link 
IncompleteGridGeometryException}).
+     * @return {@code true} if all specified properties are defined (i.e. 
invoking the
+     *         corresponding getter methods will not throw {@link 
IncompleteGridGeometryException}).
      * @throws IllegalArgumentException if the specified bitmask is not a 
combination of known masks.
      *
      * @see #getCoordinateReferenceSystem()
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
index 3c2067e819..c934204fff 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
@@ -124,7 +124,8 @@ final class AffineMatrix implements 
ExtendedPrecisionMatrix, Serializable, Clone
     }
 
     /**
-     * Returns all matrix elements.
+     * Returns all matrix elements in row-major order.
+     * Note that this is not the same order than {@link AffineTransform} 
constructor.
      */
     @Override
     public double[] getExtendedElements() {
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
 
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
index fd28e24f2c..6f5c0f33ce 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
@@ -49,11 +49,11 @@ public abstract class ListOfUnknownSize<E> extends 
AbstractSequentialList<E> {
     }
 
     /**
-     * Returns {@link #size()} if its value is already known, or -1 if the 
size is still unknown.
+     * Returns {@link #size()} if its value is already known, or a negative 
value if the size is still unknown.
      * The size may become known for example if it has been cached by the 
subclass. In such case,
      * some {@code ListOfUnknownSize} methods will take a more efficient path.
      *
-     * @return {@link #size()} if its value is already known, or -1 if it 
still costly to compute.
+     * @return {@link #size()} if its value is already known, or any negative 
value if it still costly to compute.
      */
     protected int sizeIfKnown() {
         return -1;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
index f373297557..da82d2fa69 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
@@ -308,7 +308,7 @@ public abstract class PRJDataStore extends URIDataStore {
                 final StoreFormat format = new StoreFormat(locale, timezone, 
null, listeners);
                 format.setConvention(Convention.WKT1_COMMON_UNITS);
                 format.format(crs, out);
-                out.write(System.lineSeparator());
+                out.newLine();
             }
         } catch (IOException e) {
             Object identifier = getIdentifier().orElse(null);
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index 34e207be31..d27cddc3e1 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -270,6 +270,11 @@ public final class Resources extends IndexedResourceBundle 
{
          */
         public static final short IllegalOutputTypeForWriter_2 = 9;
 
+        /**
+         * All coverages must have the same grid geometry.
+         */
+        public static final short IncompatibleGridGeometry = 72;
+
         /**
          * Components of the “{1}” name are inconsistent with those of the 
name previously binded in
          * “{0}” data store.
@@ -346,6 +351,11 @@ public final class Resources extends IndexedResourceBundle 
{
          */
         public static final short ResourceNotFound_2 = 24;
 
+        /**
+         * This resource has been removed from its data store.
+         */
+        public static final short ResourceRemoved = 73;
+
         /**
          * The “{0}” format does not support rotations.
          */
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index f6bbcfd450..cd6a783721 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -61,6 +61,7 @@ IllegalEventType_1                = This resource should not 
fire events of type
 IllegalFeatureType_2              = The {0} data store does not accept 
features of type \u201c{1}\u201d.
 IllegalInputTypeForReader_2       = The {0} reader does not accept inputs of 
type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = The {0} writer does not accept outputs of 
type \u2018{1}\u2019.
+IncompatibleGridGeometry          = All coverages must have the same grid 
geometry.
 InconsistentNameComponents_2      = Components of the \u201c{1}\u201d name are 
inconsistent with those of the name previously binded in \u201c{0}\u201d data 
store.
 InvalidExpression_2               = Invalid or unsupported \u201c{1}\u201d 
expression at index {0}.
 InvalidSampleDimensionIndex_2     = Sample dimension index {1} is invalid. 
Expected an index from 0 to {0} inclusive.
@@ -75,6 +76,7 @@ ProcessingExecutedOn_1            = Processing executed on 
{0}.
 ResourceAlreadyExists_1           = A resource already exists at 
\u201c{0}\u201d.
 ResourceIdentifierCollision_2     = More than one resource have the 
\u201c{1}\u201d identifier in the \u201c{0}\u201d data store.
 ResourceNotFound_2                = No resource found for the \u201c{1}\u201d 
identifier in the \u201c{0}\u201d data store.
+ResourceRemoved                   = This resource has been removed from its 
data store.
 RequestOutOfBounds_5              = The request [{3} \u2026 {4}] is outside 
the [{1} \u2026 {2}] domain for \u201c{0}\u201d axis.
 RotationNotSupported_1            = The \u201c{0}\u201d format does not 
support rotations.
 ShallBeDeclaredBefore_2           = The \u201c{1}\u201d element must be 
declared before \u201c{0}\u201d.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 0dcaf8238a..6f87021ddc 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -66,6 +66,7 @@ IllegalEventType_1                = Cette ressource ne 
devrait pas lancer des \u
 IllegalFeatureType_2              = Le format {0} ne stocke pas de 
donn\u00e9es de type \u00ab\u202f{1}\u202f\u00bb.
 IllegalInputTypeForReader_2       = Le lecteur {0} n\u2019accepte pas des 
entr\u00e9s de type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = L\u2019encodeur {0} n\u2019accepte pas des 
sorties de type \u2018{1}\u2019.
+IncompatibleGridGeometry          = Toutes les couvertures de donn\u00e9es 
doivent avoir la m\u00eame g\u00e9om\u00e9trie de grille.
 InvalidExpression_2               = Expression \u00ab\u202f{1}\u202f\u00bb 
invalide ou non-support\u00e9e \u00e0 l\u2019index {0}.
 InvalidSampleDimensionIndex_2     = L\u2019index de dimension 
d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 
{0} inclusif.
 InconsistentNameComponents_2      = Les \u00e9l\u00e9ments qui composent le 
nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui 
avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de 
\u00ab\u202f{0}\u202f\u00bb.
@@ -80,6 +81,7 @@ ProcessingExecutedOn_1            = Traitement 
ex\u00e9cut\u00e9 sur {0}.
 ResourceAlreadyExists_1           = Une ressource existe d\u00e9j\u00e0 \u00e0 
l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb.
 ResourceIdentifierCollision_2     = Plusieurs ressources utilisent 
l\u2019identifiant \u00ab\u202f{1}\u202f\u00bb dans les donn\u00e9es de 
\u00ab\u202f{0}\u202f\u00bb.
 ResourceNotFound_2                = Aucune ressource n\u2019a \u00e9t\u00e9 
trouv\u00e9e pour l\u2019identifiant \u00ab\u202f{1}\u202f\u00bb dans les 
donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
+ResourceRemoved                   = Cette ressource a \u00e9t\u00e9 
supprim\u00e9e de sa source de donn\u00e9es.
 RequestOutOfBounds_5              = La demande [{3} \u2026 {4}] est en dehors 
du domaine [{1} \u2026 {2}] pour l\u2019axe \u00ab\u202f{0}\u202f\u00bb.
 RotationNotSupported_1            = Le format \u00ab\u202f{0}\u202f\u00bb ne 
supporte pas les rotations.
 ShallBeDeclaredBefore_2           = L\u2019\u00e9l\u00e9ment 
\u00ab\u202f{1}\u202f\u00bb doit \u00eatre d\u00e9clar\u00e9 avant 
\u00ab\u202f{0}\u202f\u00bb.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
index 8b66e05ce0..97d8a7e50c 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
@@ -89,7 +89,8 @@ enum FormatFilter {
      * if an image reader requests a sub-type, we can probably not provide it 
ourselves.
      */
     private static final Class<?>[] VALID_OUTPUTS = {
-        ImageOutputStream.class, DataOutput.class, OutputStream.class, 
File.class, Path.class, URL.class, URI.class
+        // `ImageOutputStream` intentionally excluded because not handled by 
`StorageConnector`.
+        DataOutput.class, OutputStream.class, File.class, Path.class, 
URL.class, URI.class
     };
 
     /**
@@ -231,10 +232,11 @@ enum FormatFilter {
                                 final ImageWriter writer = 
provider.createWriterInstance();
                                 writer.setOutput(output);
                                 return writer;
-                            } else if (type == ImageOutputStream.class) {
-                                deferred.put(provider, Boolean.TRUE);
                             }
                         }
+                        if (type == ImageOutputStream.class) {
+                            deferred.put(provider, Boolean.TRUE);
+                        }
                     }
                 }
             }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
index a212a1fe9e..268f8619d1 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
@@ -39,6 +39,7 @@ import org.apache.sis.storage.AbstractGridCoverageResource;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.storage.StoreResource;
 import org.apache.sis.internal.storage.RangeArgument;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
@@ -63,17 +64,17 @@ class Image extends AbstractGridCoverageResource implements 
StoreResource {
      * The dimensions of <var>x</var> and <var>y</var> axes.
      * Static constants for now, may become configurable fields in the future.
      */
-    private static final int X_DIMENSION = 0, Y_DIMENSION = 1;
+    static final int X_DIMENSION = 0, Y_DIMENSION = 1;
 
     /**
-     * The parent data store.
+     * The parent data store, or {@code null} if this resource is not valid 
anymore.
      */
-    private final Store store;
+    private volatile Store store;
 
     /**
-     * Index of the image to read.
+     * Index of the image to read or write in the image file. This is usually 
0.
      */
-    private final int imageIndex;
+    int imageIndex;
 
     /**
      * The identifier as a sequence number in the namespace of the {@link 
Store}.
@@ -116,12 +117,26 @@ class Image extends AbstractGridCoverageResource 
implements StoreResource {
         return store;
     }
 
+    /**
+     * Returns the data store.
+     *
+     * @throws DataStoreException if this resource is not valid anymore.
+     */
+    final Store store() throws DataStoreException {
+        final Store store = this.store;
+        if (store != null) {
+            return store;
+        }
+        throw new 
DataStoreException(Resources.format(Resources.Keys.ResourceRemoved));
+    }
+
     /**
      * Returns the resource identifier. The name space is the file name and
      * the local part of the name is the image index number, starting at 1.
      */
     @Override
-    public Optional<GenericName> getIdentifier() throws DataStoreException {
+    public final Optional<GenericName> getIdentifier() throws 
DataStoreException {
+        final Store store = store();
         synchronized (store) {
             if (identifier == null) {
                 identifier = Names.createLocalName(store.getDisplayName(), 
null, String.valueOf(imageIndex + 1));
@@ -147,6 +162,7 @@ class Image extends AbstractGridCoverageResource implements 
StoreResource {
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public final List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        final Store store = store();
         synchronized (store) {
             if (sampleDimensions == null) try {
                 final ImageReader        reader = store.reader();
@@ -188,46 +204,47 @@ class Image extends AbstractGridCoverageResource 
implements StoreResource {
      */
     @Override
     public final GridCoverage read(GridGeometry domain, int... range) throws 
DataStoreException {
-        synchronized (store) {
-            final ImageReader reader = store.reader();
-            final ImageReadParam param = reader.getDefaultReadParam();
-            if (domain == null) {
-                domain = gridGeometry;
-            } else {
-                final GridDerivation gd = 
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
-                final GridExtent extent = gd.getIntersection();
-                final int[] subsampling = gd.getSubsampling();
-                final int[] offsets     = gd.getSubsamplingOffsets();
-                final int   subX        = subsampling[X_DIMENSION];
-                final int   subY        = subsampling[Y_DIMENSION];
-                final Rectangle region  = new Rectangle(
-                        toIntExact(extent.getLow (X_DIMENSION)),
-                        toIntExact(extent.getLow (Y_DIMENSION)),
-                        toIntExact(extent.getSize(X_DIMENSION)),
-                        toIntExact(extent.getSize(Y_DIMENSION)));
-                /*
-                 * Ths subsampling offset Δx is defined differently in Image 
I/O and `GridGeometry`.
-                 * The conversion from coordinate x in subsampled image to xₒ 
in original image is:
-                 *
-                 *     Image I/O:     xₒ = xᵣ + (x⋅s + Δx′)
-                 *     GridGeometry:  xₒ = (truncate(xᵣ/s) + x)⋅s + Δx
-                 *
-                 * Where xᵣ is the the lower coordinate of `region`, s is the 
subsampling and
-                 * `truncate(xᵣ/s)` is given by the lower coordinate of 
subsampled extent.
-                 * Rearranging equations:
-                 *
-                 *     Δx′ = truncate(xᵣ/s)⋅s + Δx - xᵣ
-                 */
-                domain = gd.build();
-                GridExtent subExtent = domain.getExtent();
-                param.setSourceRegion(region);
-                param.setSourceSubsampling(subX, subY,
-                        toIntExact(subExtent.getLow(X_DIMENSION) * subX + 
offsets[X_DIMENSION] - region.x),
-                        toIntExact(subExtent.getLow(Y_DIMENSION) * subY + 
offsets[Y_DIMENSION] - region.y));
-            }
-            RenderedImage image;
-            List<SampleDimension> sampleDimensions = getSampleDimensions();
-            try {
+        RenderedImage image;
+        List<SampleDimension> bands;
+        final Store store = store();
+        try {
+            synchronized (store) {
+                final ImageReader reader = store.reader();
+                final ImageReadParam param = reader.getDefaultReadParam();
+                if (domain == null) {
+                    domain = gridGeometry;
+                } else {
+                    final GridDerivation gd = 
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
+                    final GridExtent extent = gd.getIntersection();
+                    final int[] subsampling = gd.getSubsampling();
+                    final int[] offsets     = gd.getSubsamplingOffsets();
+                    final int   subX        = subsampling[X_DIMENSION];
+                    final int   subY        = subsampling[Y_DIMENSION];
+                    final Rectangle region  = new Rectangle(
+                            toIntExact(extent.getLow (X_DIMENSION)),
+                            toIntExact(extent.getLow (Y_DIMENSION)),
+                            toIntExact(extent.getSize(X_DIMENSION)),
+                            toIntExact(extent.getSize(Y_DIMENSION)));
+                    /*
+                     * Ths subsampling offset Δx is defined differently in 
Image I/O and `GridGeometry`.
+                     * The conversion from coordinate x in subsampled image to 
xₒ in original image is:
+                     *
+                     *     Image I/O:     xₒ = xᵣ + (x⋅s + Δx′)
+                     *     GridGeometry:  xₒ = (truncate(xᵣ/s) + x)⋅s + Δx
+                     *
+                     * Where xᵣ is the the lower coordinate of `region`, s is 
the subsampling and
+                     * `truncate(xᵣ/s)` is given by the lower coordinate of 
subsampled extent.
+                     * Rearranging equations:
+                     *
+                     *     Δx′ = truncate(xᵣ/s)⋅s + Δx - xᵣ
+                     */
+                    domain = gd.build();
+                    GridExtent subExtent = domain.getExtent();
+                    param.setSourceRegion(region);
+                    param.setSourceSubsampling(subX, subY,
+                            toIntExact(subExtent.getLow(X_DIMENSION) * subX + 
offsets[X_DIMENSION] - region.x),
+                            toIntExact(subExtent.getLow(Y_DIMENSION) * subY + 
offsets[Y_DIMENSION] - region.y));
+                }
                 /*
                  * If a subset of the bands is requested, ideally we should 
forward this request to the `ImageReader`.
                  * But experience suggests that not all `ImageReader` 
implementations support band subsetting well.
@@ -235,13 +252,14 @@ class Image extends AbstractGridCoverageResource 
implements StoreResource {
                  * be the easiest cases. More difficult cases will be handled 
after the reading.
                  * Those heuristic rules may be changed in any future version.
                  */
+                bands = getSampleDimensions();
                 if (range != null) {
                     final ImageTypeSpecifier type = 
reader.getRawImageType(imageIndex);
                     final RangeArgument args = 
RangeArgument.validate(type.getNumBands(), range, listeners);
                     if (args.isIdentity()) {
                         range = null;
                     } else {
-                        sampleDimensions = 
UnmodifiableArrayList.wrap(args.select(sampleDimensions));
+                        bands = UnmodifiableArrayList.wrap(args.select(bands));
                         if (args.hasAllBands || type.getSampleModel() 
instanceof BandedSampleModel) {
                             range = args.getSelectedBands();
                             param.setSourceBands(range);
@@ -251,18 +269,25 @@ class Image extends AbstractGridCoverageResource 
implements StoreResource {
                     }
                 }
                 image = reader.readAsRenderedImage(imageIndex, param);
-            } catch (IOException e) {
-                throw new DataStoreException(e);
-            }
-            /*
-             * If the reader was presumed unable to handle the band 
subsetting, apply it now.
-             * It waste some memory because unused bands still in memory. But 
we do that as a
-             * workaround for limitations in some `ImageReader` 
implementations.
-             */
-            if (range != null) {
-                image = new ImageProcessor().selectBands(image, range);
             }
-            return new GridCoverage2D(domain, sampleDimensions, image);
+        } catch (IOException | RuntimeException e) {
+            throw canNotRead(store.getDisplayName(), domain, e);
         }
+        /*
+         * If the reader was presumed unable to handle the band subsetting, 
apply it now.
+         * It waste some memory because unused bands still in memory. But we 
do that as a
+         * workaround for limitations in some `ImageReader` implementations.
+         */
+        if (range != null) {
+            image = new ImageProcessor().selectBands(image, range);
+        }
+        return new GridCoverage2D(domain, bands, image);
+    }
+
+    /**
+     * Notifies this resource that it should not be used anymore.
+     */
+    final void dispose() {
+        store = null;
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
index c3ac161e47..9b4846d6d8 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
@@ -19,13 +19,14 @@ package org.apache.sis.internal.storage.image;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.logging.Level;
 import java.io.IOException;
 import java.io.EOFException;
 import java.io.FileNotFoundException;
 import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.StandardOpenOption;
 import javax.imageio.ImageIO;
@@ -38,6 +39,7 @@ import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.GridCoverageResource;
@@ -45,6 +47,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.storage.PRJDataStore;
@@ -69,7 +72,7 @@ import org.apache.sis.setup.OptionKey;
  * @since   1.2
  * @module
  */
-final class Store extends PRJDataStore implements Aggregate {
+class Store extends PRJDataStore implements Aggregate {
     /**
      * Image I/O format names (ignoring case) for which we have an entry in 
the {@code SpatialMetadata} database.
      */
@@ -85,7 +88,7 @@ final class Store extends PRJDataStore implements Aggregate {
      * @see #width
      * @see #height
      */
-    private static final int MAIN_IMAGE = 0;
+    static final int MAIN_IMAGE = 0;
 
     /**
      * The default World File suffix when it can not be determined from {@link 
#location}.
@@ -93,14 +96,31 @@ final class Store extends PRJDataStore implements Aggregate 
{
      */
     private static final String DEFAULT_SUFFIX = "wld";
 
+    /**
+     * The "cell center" versus "cell corner" interpretation of translation 
coefficients.
+     * The ESRI specification said that the coefficients map to pixel center.
+     */
+    static final PixelInCell CELL_ANCHOR = PixelInCell.CELL_CENTER;
+
     /**
      * The filename extension (may be an empty string), or {@code null} if 
unknown.
      * It does not include the leading dot.
      */
-    private final String suffix;
+    final String suffix;
+
+    /**
+     * The filename extension for the auxiliary "world file".
+     * For the TIFF format, this is typically {@code "tfw"}.
+     * This is computed as a side-effect of {@link #readWorldFile()}.
+     */
+    private String suffixWLD;
 
     /**
-     * The image reader, set by the constructor and cleared when no longer 
needed.
+     * The image reader, set by the constructor and cleared when the store is 
closed.
+     * May also be null if the store is initially write-only, in which case a 
reader
+     * may be created the first time than an image is read.
+     *
+     * @see #reader()
      */
     private ImageReader reader;
 
@@ -130,7 +150,7 @@ final class Store extends PRJDataStore implements Aggregate 
{
      *
      * @see #components()
      */
-    private List<Image> components;
+    private Components components;
 
     /**
      * The metadata object, or {@code null} if not yet created.
@@ -144,21 +164,30 @@ final class Store extends PRJDataStore implements 
Aggregate {
      *
      * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
      * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @param  readOnly   whether to fail if the channel can not be opened at 
least in read mode.
      * @throws DataStoreException if an error occurred while opening the 
stream.
      * @throws IOException if an error occurred while creating the image 
reader instance.
      */
-    public Store(final StoreProvider provider, final StorageConnector 
connector)
+    Store(final StoreProvider provider, final StorageConnector connector, 
final boolean readOnly)
             throws DataStoreException, IOException
     {
         super(provider, connector);
-        final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
         final Object storage = connector.getStorage();
         suffix = IOUtilities.extension(storage);
+        if (!(readOnly || fileExists(connector))) {
+            /*
+             * If the store is opened in read-write mode, create the image 
reader only
+             * if the file exists and is non-empty. Otherwise we let `reader` 
to null
+             * and the caller will create an image writer instead.
+             */
+            return;
+        }
         /*
          * Search for a reader that claim to be able to read the storage input.
          * First we try readers associated to the file suffix. If no reader is
          * found, we try all other readers.
          */
+        final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
         if (suffix != null) {
             reader = FormatFilter.SUFFIX.createReader(suffix, connector, 
deferred);
         }
@@ -174,14 +203,20 @@ fallback:   if (reader == null) {
                 for (final Map.Entry<ImageReaderSpi,Boolean> entry : 
deferred.entrySet()) {
                     if (entry.getValue()) {
                         if (stream == null) {
-                            stream = ImageIO.createImageInputStream(storage);
-                            if (stream == null) break;
+                            if (!readOnly) {
+                                // ImageOutputStream is both read and write.
+                                stream = 
ImageIO.createImageOutputStream(storage);
+                            }
+                            if (stream == null) {
+                                stream = 
ImageIO.createImageInputStream(storage);
+                                if (stream == null) break;
+                            }
                         }
                         final ImageReaderSpi p = entry.getKey();
                         if (p.canDecodeInput(stream)) {
                             connector.closeAllExcept(storage);
                             reader = p.createReaderInstance();
-                            reader.setInput(stream, false, true);
+                            reader.setInput(stream);
                             break fallback;
                         }
                     }
@@ -190,15 +225,43 @@ fallback:   if (reader == null) {
                             storage, 
connector.getOption(OptionKey.OPEN_OPTIONS));
             }
         }
+        configureReader();
         /*
-         * Sets the locale to use for warning messages, if supported. If the 
reader
-         * does not support the locale, the reader's default locale will be 
used.
+         * Do not invoke any method that may cause the image reader to start 
reading the stream,
+         * because the `WritableStore` subclass will want to save the initial 
stream position.
          */
+    }
+
+    /**
+     * Sets the locale to use for warning messages, if supported. If the reader
+     * does not support the locale, the reader's default locale will be used.
+     */
+    private void configureReader() {
         try {
             reader.setLocale(listeners.getLocale());
         } catch (IllegalArgumentException e) {
             // Ignore
         }
+        reader.addIIOReadWarningListener(new WarningListener(listeners));
+    }
+
+    /**
+     * Returns {@code true} if the image file exists and is non-empty.
+     * This is used for checking if an {@link ImageReader} should be created.
+     * If the file is going to be truncated, then it is considered already 
empty.
+     *
+     * @param  connector  the connector to use for opening the file.
+     * @return whether the image file exists and is non-empty.
+     */
+    private boolean fileExists(final StorageConnector connector) throws 
DataStoreException, IOException {
+        if (!ArraysExt.contains(connector.getOption(OptionKey.OPEN_OPTIONS), 
StandardOpenOption.TRUNCATE_EXISTING)) {
+            for (Path path : super.getComponentFiles()) {
+                if (Files.isRegularFile(path) && Files.size(path) > 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
 
     /**
@@ -267,7 +330,7 @@ loop:   for (int convention=0;; convention++) {
             }
         }
         if (warning != null) {
-            
listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, 
preferred), warning);
+            
listeners.warning(resources().getString(Resources.Keys.CanNotReadAuxiliaryFile_1,
 preferred), warning);
         }
         return null;
     }
@@ -281,11 +344,12 @@ loop:   for (int convention=0;; convention++) {
      * @throws DataStoreException if the file content can not be parsed.
      */
     private AffineTransform2D readWorldFile(final String wld) throws 
IOException, DataStoreException {
-        final AuxiliaryContent content = readAuxiliaryFile(wld, encoding);
-        final CharSequence[] lines = 
CharSequences.splitOnEOL(readAuxiliaryFile(wld, encoding));
-        int count = 0;
-        final int expected = 6;                     // Expected number of 
elements.
-        final double[] elements = new double[expected];
+        final AuxiliaryContent content  = readAuxiliaryFile(wld, encoding);
+        final String           filename = content.getFilename();
+        final CharSequence[]   lines    = 
CharSequences.splitOnEOL(readAuxiliaryFile(wld, encoding));
+        final int              expected = 6;        // Expected number of 
elements.
+        int                    count    = 0;        // Actual number of 
elements.
+        final double[]         elements = new double[expected];
         for (int i=0; i<expected; i++) {
             final String line = lines[i].toString().trim();
             if (!line.isEmpty() && line.charAt(0) != '#') {
@@ -295,16 +359,29 @@ loop:   for (int convention=0;; convention++) {
                 try {
                     elements[count++] = Double.parseDouble(line);
                 } catch (NumberFormatException e) {
-                    throw new 
DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2, 
content.getFilename(), i), e);
+                    throw new 
DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2, 
filename, i), e);
                 }
             }
         }
         if (count != expected) {
-            throw new 
EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, 
content.getFilename()));
+            throw new 
EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, filename));
+        }
+        if (filename != null) {
+            final int s = filename.lastIndexOf('.');
+            if (s >= 0) {
+                suffixWLD = filename.substring(s+1);
+            }
         }
         return new AffineTransform2D(elements);
     }
 
+    /**
+     * Returns the localized resources for producing warnings or error 
messages.
+     */
+    final Resources resources() {
+        return Resources.forLocale(listeners.getLocale());
+    }
+
     /**
      * Returns the localized resources for producing error messages.
      */
@@ -312,6 +389,22 @@ loop:   for (int convention=0;; convention++) {
         return Errors.getResources(listeners.getLocale());
     }
 
+    /**
+     * Returns paths to the main file together with auxiliary files.
+     *
+     * @return paths to the main file and auxiliary files, or an empty array 
if unknown.
+     * @throws DataStoreException if the URI can not be converted to a {@link 
Path}.
+     */
+    @Override
+    public final synchronized Path[] getComponentFiles() throws 
DataStoreException {
+        if (suffixWLD == null) try {
+            getGridGeometry(MAIN_IMAGE);                // Will compute 
`suffixWLD` as a side effect.
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        return listComponentFiles(suffixWLD, PRJ);      // `suffixWLD` still 
null if file was not found.
+    }
+
     /**
      * Gets the grid geometry for image at the given index.
      * This method should be invoked only once per image, and the result 
cached.
@@ -322,7 +415,7 @@ loop:   for (int convention=0;; convention++) {
      * @throws IOException if an I/O error occurred.
      * @throws DataStoreException if the {@code *.prj} or {@code *.tfw} 
auxiliary file content can not be parsed.
      */
-    private GridGeometry getGridGeometry(final int index) throws IOException, 
DataStoreException {
+    final GridGeometry getGridGeometry(final int index) throws IOException, 
DataStoreException {
         assert Thread.holdsLock(this);
         final ImageReader reader = reader();
         if (gridGeometry == null) {
@@ -331,23 +424,48 @@ loop:   for (int convention=0;; convention++) {
             height    = reader.getHeight(MAIN_IMAGE);
             gridToCRS = readWorldFile();
             readPRJ();
-            gridGeometry = new GridGeometry(new GridExtent(width, height), 
PixelInCell.CELL_CENTER, gridToCRS, crs);
+            gridGeometry = new GridGeometry(new GridExtent(width, height), 
CELL_ANCHOR, gridToCRS, crs);
         }
         if (index != MAIN_IMAGE) {
             final int w = reader.getWidth (index);
             final int h = reader.getHeight(index);
             if (w != width || h != height) {
-                return new GridGeometry(new GridExtent(w, h), 
PixelInCell.CELL_CENTER, null, null);
+                // Can not use `gridToCRS` and `crs` because they may not 
apply.
+                return new GridGeometry(new GridExtent(w, h), CELL_ANCHOR, 
null, null);
             }
         }
         return gridGeometry;
     }
 
+    /**
+     * Sets the store-wide grid geometry when a new coverage is written. The 
{@link WritableStore} implementation
+     * is responsible for making sure that the new grid geometry is compatible 
with preexisting grid geometry.
+     *
+     * @param  index  index of the image for which to set the grid geometry.
+     * @param  gg     the new grid geometry.
+     * @return suffix of the "world file", or {@code null} if the image can 
not be written.
+     */
+    String setGridGeometry(final int index, final GridGeometry gg) throws 
IOException, DataStoreException {
+        if (index != MAIN_IMAGE) {
+            return null;
+        }
+        final GridExtent extent = gg.getExtent();
+        final int w = Math.toIntExact(extent.getSize(Image.X_DIMENSION));
+        final int h = Math.toIntExact(extent.getSize(Image.Y_DIMENSION));
+        final String s = (suffixWLD != null) ? suffixWLD : 
getWorldFileSuffix();
+        crs = gg.isDefined(GridGeometry.CRS) ? 
gg.getCoordinateReferenceSystem() : null;
+        gridGeometry = gg;                  // Set only after success of all 
the above.
+        width        = w;
+        height       = h;
+        suffixWLD    = s;
+        return s;
+    }
+
     /**
      * Returns information about the data store as a whole.
      */
     @Override
-    public synchronized Metadata getMetadata() throws DataStoreException {
+    public final synchronized Metadata getMetadata() throws DataStoreException 
{
         if (metadata == null) try {
             final MetadataBuilder builder = new MetadataBuilder();
             String format = reader().getFormatName();
@@ -381,40 +499,58 @@ loop:   for (int convention=0;; convention++) {
 
     /**
      * Returns all images in this store. Note that fetching the size of the 
list is a potentially costly operation.
+     *
+     * @return list of images in this store.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public final synchronized Collection<? extends GridCoverageResource> 
components() throws DataStoreException {
         if (components == null) try {
-            components = new Components();
+            components = new Components(reader().getNumImages(false));
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
         return components;
     }
 
+    /**
+     * Returns all images in this store, or {@code null} if none and {@code 
create} is false.
+     *
+     * @param  create     whether to create the component list if it was not 
already created.
+     * @param  numImages  number of images, or any negative value if unknown.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    final Components components(final boolean create, final int numImages) {
+        if (components == null && create) {
+            components = new Components(numImages);
+        }
+        return components;
+    }
+
     /**
      * A list of images where each {@link Image} instance is initialized when 
first needed.
      * Fetching the list size may be a costly operation and will be done only 
if requested.
      */
-    private final class Components extends ListOfUnknownSize<Image> {
+    final class Components extends ListOfUnknownSize<Image> {
         /**
-         * Size of this list, or -1 if unknown.
+         * Size of this list, or any negative value if unknown.
          */
         private int size;
 
         /**
-         * All elements in this list. Some array element may be {@code null} 
if the image
-         * as never been requested.
+         * All elements in this list. Some array elements may be {@code null} 
if the image
+         * has never been requested.
          */
         private Image[] images;
 
         /**
          * Creates a new list of images.
+         *
+         * @param  numImages  number of images, or any negative value if 
unknown.
          */
-        private Components() throws DataStoreException, IOException {
-            size = reader().getNumImages(false);
-            images = new Image[size >= 0 ? size : 1];
+        private Components(final int numImages) {
+            size = numImages;
+            images = new Image[Math.max(numImages, 1)];
         }
 
         /**
@@ -437,7 +573,7 @@ loop:   for (int convention=0;; convention++) {
         }
 
         /**
-         * Returns the number of images if this information is known, or -1 
otherwise.
+         * Returns the number of images if this information is known, or any 
negative value otherwise.
          * This is used by {@link ListOfUnknownSize} for optimizing some 
operations.
          */
         @Override
@@ -457,12 +593,20 @@ loop:   for (int convention=0;; convention++) {
                 if (size >= 0) {
                     return index >= 0 && index < size;
                 }
-                return get(index) != null;
+                try {
+                    return get(index) != null;
+                } catch (IndexOutOfBoundsException e) {
+                    return false;
+                }
             }
         }
 
         /**
          * Returns the image at the given index. New instances are created 
when first requested.
+         *
+         * @param  index  index of the image for which to get a resource.
+         * @return resource for the image identified by the given index.
+         * @throws IndexOutOfBoundsException if the image index is out of 
bounds.
          */
         @Override
         public Image get(final int index) {
@@ -472,7 +616,7 @@ loop:   for (int convention=0;; convention++) {
                     image = images[index];
                 }
                 if (image == null) try {
-                    image = new Image(Store.this, listeners, index, 
getGridGeometry(index));
+                    image = createImageResource(index);
                     if (index >= images.length) {
                         images = Arrays.copyOf(images, Math.max(images.length 
* 2, index + 1));
                     }
@@ -485,17 +629,113 @@ loop:   for (int convention=0;; convention++) {
                 return image;
             }
         }
+
+        /**
+         * Invoked <em>after</em> an image has been added to the image file.
+         * This method adds in this list a reference to the newly added file.
+         *
+         * @param  image  the image to add to this list.
+         */
+        final void added(final Image image) {
+            size = image.imageIndex;
+            if (size >= images.length) {
+                images = Arrays.copyOf(images, size * 2);
+            }
+            images[size++] = image;
+        }
+
+        /**
+         * Invoked <em>after</em> an image has been removed from the image 
file.
+         * This method performs no bounds check (it must be done by the 
caller).
+         *
+         * @param  index  index of the image that has been removed.
+         */
+        final void removed(int index) {
+            final int last = images.length - 1;
+            System.arraycopy(images, index+1, images, index, last - index);
+            images[last] = null;
+            size--;
+            while (index < last) {
+                final Image image = images[index++];
+                if (image != null) image.imageIndex--;
+            }
+        }
+
+        /**
+         * Removes the element at the specified position in this list.
+         */
+        @Override
+        public Image remove(final int index) {
+            final Image image = get(index);
+            try {
+                Store.this.remove(image);
+            } catch (DataStoreException e) {
+                throw new UnsupportedOperationException(e);
+            }
+            return image;
+        }
+    }
+
+    /**
+     * Invoked by {@link Components} when the caller want to remove a resource.
+     * The actual implementation is provided by {@link WritableStore}.
+     */
+    void remove(final Resource resource) throws DataStoreException {
+        throw new ReadOnlyStorageException();
+    }
+
+    /**
+     * Creates a {@link GridCoverageResource} for the specified image.
+     * This method is invoked by {@link Components} when first needed
+     * and the result is cached by the caller.
+     *
+     * @param  index  index of the image for which to create a resource.
+     * @return resource for the image identified by the given index.
+     * @throws IndexOutOfBoundsException if the image index is out of bounds.
+     */
+    Image createImageResource(final int index) throws DataStoreException, 
IOException {
+        return new Image(this, listeners, index, getGridGeometry(index));
+    }
+
+    /**
+     * Prepares an image reader compatible with the writer and sets its input.
+     * This method is invoked for switching from write mode to read mode.
+     * Its actual implementation is provided by {@link WritableImage}.
+     *
+     * @param  current  the current image reader, or {@code null} if none.
+     * @return the image reader to use, or {@code null} if none.
+     * @throws IOException if an error occurred while preparing the reader.
+     */
+    ImageReader prepareReader(ImageReader current) throws IOException {
+        return null;
+    }
+
+    /**
+     * Returns the reader without doing any validation. The reader may be 
{@code null} either
+     * because the store is closed or because the store is initially opened in 
write-only mode.
+     * The reader may have a {@code null} input.
+     */
+    final ImageReader getCurrentReader() {
+        return reader;
     }
 
     /**
      * Returns the reader if it has not been closed.
+     *
+     * @throws DataStoreClosedException if this data store is closed.
+     * @throws IOException if an error occurred while preparing the reader.
      */
-    final ImageReader reader() throws DataStoreException {
-        final ImageReader in = reader;
-        if (in == null) {
-            throw new DataStoreClosedException(getLocale(), 
StoreProvider.NAME, StandardOpenOption.READ);
+    final ImageReader reader() throws DataStoreException, IOException {
+        assert Thread.holdsLock(this);
+        ImageReader current = reader;
+        if (current == null || current.getInput() == null) {
+            reader = current = prepareReader(current);
+            if (current == null) {
+                throw new DataStoreClosedException(getLocale(), 
StoreProvider.NAME, StandardOpenOption.READ);
+            }
+            configureReader();
         }
-        return in;
+        return current;
     }
 
     /**
@@ -505,12 +745,15 @@ loop:   for (int convention=0;; convention++) {
      */
     @Override
     public synchronized void close() throws DataStoreException {
-        final ImageReader r = reader;
-        reader = null;
-        if (r != null) try {
-            final Object input = r.getInput();
-            r.setInput(null);
-            r.dispose();
+        final ImageReader codec = reader;
+        reader       = null;
+        metadata     = null;
+        components   = null;
+        gridGeometry = null;
+        if (codec != null) try {
+            final Object input = codec.getInput();
+            codec.setInput(null);
+            codec.dispose();
             if (input instanceof AutoCloseable) {
                 ((AutoCloseable) input).close();
             }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
index 435264a72d..d987c2ca45 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
@@ -27,6 +27,7 @@ import org.apache.sis.internal.storage.Capability;
 import org.apache.sis.internal.storage.StoreMetadata;
 import org.apache.sis.internal.storage.PRJDataStore;
 import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.ProbeResult;
 
 
@@ -38,8 +39,10 @@ import org.apache.sis.storage.ProbeResult;
  * @since   1.2
  * @module
  */
-@StoreMetadata(formatName   = StoreProvider.NAME,
-               capabilities = Capability.READ)
+@StoreMetadata(formatName    = StoreProvider.NAME,
+               fileSuffixes  = {"jpeg", "jpg", "png", "gif", "bmp"},    // 
Non-exhaustive list.
+               capabilities  = {Capability.READ, Capability.WRITE, 
Capability.CREATE},
+               resourceTypes = GridCoverageResource.class)
 public final class StoreProvider extends PRJDataStore.Provider {
     /**
      * The format name.
@@ -72,7 +75,11 @@ public final class StoreProvider extends 
PRJDataStore.Provider {
     @Override
     public DataStore open(final StorageConnector connector) throws 
DataStoreException {
         try {
-            return new Store(this, connector);
+            if (isWritable(connector)) {
+                return new WritableStore(this, connector);
+            } else {
+                return new Store(this, connector, true);
+            }
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
index fd0e389270..c879d00a06 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
@@ -17,7 +17,9 @@
 package org.apache.sis.internal.storage.image;
 
 import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
 import javax.imageio.event.IIOReadWarningListener;
+import javax.imageio.event.IIOWriteWarningListener;
 import org.apache.sis.storage.event.StoreListeners;
 
 
@@ -30,7 +32,7 @@ import org.apache.sis.storage.event.StoreListeners;
  * @since   1.2
  * @module
  */
-final class WarningListener implements IIOReadWarningListener {
+final class WarningListener implements IIOReadWarningListener, 
IIOWriteWarningListener {
     /**
      * The set of registered {@link StoreListener}s for the data store.
      */
@@ -50,7 +52,19 @@ final class WarningListener implements 
IIOReadWarningListener {
      * @param message  the warning.
      */
     @Override
-    public void warningOccurred(final ImageReader reader, final String 
message) {
+    public void warningOccurred(final ImageReader source, final String 
message) {
+        listeners.warning(message);
+    }
+
+    /**
+     * Reports a non-fatal error in encoding.
+     *
+     * @param source      the writer calling this method.
+     * @param imageIndex  index of the image being written.
+     * @param message     the warning.
+     */
+    @Override
+    public void warningOccurred(final ImageWriter source, final int 
imageIndex, final String message) {
         listeners.warning(message);
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
new file mode 100644
index 0000000000..fcc92c97da
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
@@ -0,0 +1,76 @@
+/*
+ * 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.storage.image;
+
+import java.io.IOException;
+import java.awt.image.RenderedImage;
+import javax.imageio.ImageWriter;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.internal.storage.WritableResourceSupport;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.storage.event.StoreListeners;
+
+
+/**
+ * An image which can be replaced or updated.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class WritableImage extends Image implements 
WritableGridCoverageResource {
+    /**
+     * Creates a new resource.
+     */
+    WritableImage(final WritableStore store, final StoreListeners parent, 
final int imageIndex,
+                  final GridGeometry gridGeometry) throws DataStoreException
+    {
+        super(store, parent, imageIndex, gridGeometry);
+    }
+
+    /**
+     * Writes a new coverage in the data store for this resource. If a 
coverage already exists for this resource,
+     * then it will be overwritten only if the {@code TRUNCATE} or {@code 
UPDATE} option is specified.
+     *
+     * @param  coverage  new data to write in the data store for this resource.
+     * @param  options   configuration of the write operation.
+     * @throws DataStoreException if an error occurred while writing data in 
the underlying data store.
+     */
+    @Override
+    public void write(GridCoverage coverage, final Option... options) throws 
DataStoreException {
+        final WritableResourceSupport h = new WritableResourceSupport(this, 
options);   // Does argument validation.
+        final WritableStore store = (WritableStore) store();
+        try {
+            synchronized (store) {
+                if (imageIndex != Store.MAIN_IMAGE || (store.isMultiImages() 
!= 0 && !h.replace(null))) {
+                    // TODO: we should use `ImageWriter.replacePixels(…)` 
methods instead.
+                    coverage = h.update(coverage);
+                }
+                final RenderedImage data = coverage.render(null);              
     // Fail if not two-dimensional.
+                store.setGridGeometry(imageIndex, coverage.getGridGeometry()); 
// May use the image reader.
+                final ImageWriter writer = store.writer();                     
     // Should be after `setGridGeometry(…)`.
+                writer.write(data);
+            }
+        } catch (IOException | RuntimeException e) {
+            throw new 
DataStoreException(store.resources().getString(Resources.Keys.CanNotWriteResource_1,
 store.getDisplayName()), e);
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
new file mode 100644
index 0000000000..986105f849
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
@@ -0,0 +1,515 @@
+/*
+ * 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.storage.image;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.function.BiConsumer;
+import java.io.File;
+import java.io.IOException;
+import java.io.BufferedWriter;
+import java.nio.file.StandardOpenOption;
+import java.awt.geom.AffineTransform;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.spi.ImageReaderWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.FileImageOutputStream;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.WritableAggregate;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.setup.OptionKey;
+
+
+/**
+ * A data store with writing capabilities.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class WritableStore extends Store implements WritableAggregate {
+    /**
+     * Position of the input/output stream beginning. This is usually 0.
+     */
+    private final long streamBeginning;
+
+    /**
+     * The image writer, created when first needed and cleared when the store 
is closed.
+     * Only one of {@link #reader} and {@link #writer} should have its input 
or output set
+     * at a given time.
+     *
+     * @see #writer()
+     */
+    private ImageWriter writer;
+
+    /**
+     * Number of images in this store, or any negative value if unknown. This 
information is redundant
+     * with {@link ImageReader#getNumImages(boolean)} but is stored here 
because {@link #reader} may be
+     * null and {@link ImageWriter} does not have a {@code getNumImages(…)} 
method.
+     *
+     * @see #isMultiImages()
+     */
+    private int numImages;
+
+    /**
+     * Creates a new store from the given file, URL or stream.
+     *
+     * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @throws DataStoreException if an error occurred while opening the 
stream.
+     * @throws IOException if an error occurred while creating the image 
reader instance.
+     */
+    WritableStore(final StoreProvider provider, final StorageConnector 
connector)
+            throws DataStoreException, IOException
+    {
+        super(provider, connector, false);
+        final ImageReader reader = getCurrentReader();
+        final Object inout;
+        if (reader != null) {
+            inout = reader.getInput();
+            numImages = -1;
+        } else {
+            /*
+             * If it was possible to initialize an image reader, wait to see 
if an image writer is needed.
+             * Otherwise (i.e. if the destination file does not exist), create 
the image writer immediately.
+             * The code below is a copy of the code in parent class 
constructor (for creating `ImageReader`),
+             * but adapted to the case of creating an `ImageWriter`.
+             */
+            final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>();
+            if (suffix != null) {
+                writer = FormatFilter.SUFFIX.createWriter(suffix, connector, 
null, deferred);
+            }
+            if (writer == null) {
+                writer = FormatFilter.SUFFIX.createWriter(null, connector, 
null, deferred);
+fallback:       if (writer == null) {
+                    ImageOutputStream stream = null;
+                    final Object storage = connector.getStorage();
+                    for (final Map.Entry<ImageWriterSpi,Boolean> entry : 
deferred.entrySet()) {
+                        if (entry.getValue()) {
+                            if (stream == null) {
+                                final File file = 
connector.getStorageAs(File.class);
+                                if (file != null) {
+                                    stream = new FileImageOutputStream(file);
+                                } else {
+                                    stream = 
ImageIO.createImageOutputStream(storage);
+                                    if (stream == null) break;
+                                }
+                            }
+                            final ImageWriterSpi p = entry.getKey();
+                            connector.closeAllExcept(storage);
+                            writer = p.createWriterInstance();
+                            writer.setOutput(stream);
+                            break fallback;
+                        }
+                    }
+                    throw new UnsupportedStorageException(super.getLocale(), 
StoreProvider.NAME,
+                                storage, 
connector.getOption(OptionKey.OPEN_OPTIONS));
+                }
+            }
+            configureWriter();
+            inout = writer.getOutput();
+            // Leave `numImages` to 0 because we know that the stream is empty.
+        }
+        streamBeginning = (inout instanceof ImageInputStream) ? 
((ImageInputStream) inout).getStreamPosition() : 0;
+    }
+
+    /**
+     * Sets the locale to use for warning messages, if supported. If the writer
+     * does not support the locale, the writer's default locale will be used.
+     */
+    private void configureWriter() {
+        try {
+            writer.setLocale(listeners.getLocale());
+        } catch (IllegalArgumentException e) {
+            // Ignore
+        }
+        writer.addIIOWriteWarningListener(new WarningListener(listeners));
+    }
+
+    /**
+     * Returns whether this data store contains more than one image.
+     * This is used for deciding if {@link WritableStore} can overwrite a grid 
geometry.
+     *
+     * @return 0 if this store is empty, 1 if it contains exactly one image,
+     *         or a value greater than 1 if it contains more than one image.
+     *         The returned value is not necessarily the number of images.
+     * @see #setGridGeometry(int, GridGeometry)
+     */
+    final int isMultiImages() throws IOException, DataStoreException {
+        assert Thread.holdsLock(this);
+        if (numImages < 0) {
+            // This case happens only when we opened an existing file.
+            final Components components = components(true, numImages);
+            if (components.isEmpty()) {
+                numImages = 0;
+            } else if (components.exists(1)) {
+                return 2;
+            } else {
+                numImages = 1;
+            }
+        }
+        return numImages;
+    }
+
+    /**
+     * Sets the store-wide grid geometry. Only one grid geometry can be set 
for a data store.
+     * If a grid geometry already exists and the specified grid geometry is 
incompatible,
+     * then an {@link IncompatibleResourceException} is thrown.
+     *
+     * <p>This method may use the {@link ImageReader} for checking the number 
of images,
+     * so it is better to invoke this method before {@link #writer()}.</p>
+     *
+     * @param  index  index of the image for which to read the grid geometry.
+     * @param  gg     the new grid geometry.
+     * @return suffix of the "world file", or {@code null} if this method 
wrote nothing.
+     * @throws IncompatibleResourceException if the "grid to CRS" is not 
affine,
+     *         or if a different grid geometry already exists.
+     *
+     * @see #getGridGeometry(int)
+     */
+    @Override
+    String setGridGeometry(final int index, GridGeometry gg) throws 
IOException, DataStoreException {
+        /*
+         * Make sure that the grid geometry starts at (0,0).
+         * Must be done before to compare with existing grid.
+         */
+        final GridExtent extent = gg.getExtent();
+        final long[] translation = new long[extent.getDimension()];
+        for (int i=0; i<translation.length; i++) {
+            translation[i] = Math.negateExact(extent.getLow(i));
+        }
+        gg = gg.translate(translation);
+        /*
+         * If the data store already contains a coverage, then the given grid 
geometry
+         * must be identical to the existing one, in which case there is 
nothing to do.
+         */
+        if (index != MAIN_IMAGE || isMultiImages() > 1) {
+            if (!getGridGeometry(MAIN_IMAGE).equals(gg, 
ComparisonMode.IGNORE_METADATA)) {
+                throw new IncompatibleResourceException(
+                        
resources().getString(Resources.Keys.IncompatibleGridGeometry));
+            }
+        }
+        /*
+         * Get the two-dimensional affine transform (it provides the "World 
file" content).
+         * Only after we successfully got all the information, assign the grid 
geometry to
+         * this store.
+         */
+        AffineTransform gridToCRS = null;
+        if (gg.isDefined(GridGeometry.GRID_TO_CRS)) try {
+            gridToCRS = 
AffineTransforms2D.castOrCopy(gg.getGridToCRS(CELL_ANCHOR));
+        } catch (IllegalArgumentException e) {
+            throw new IncompatibleResourceException(e.getLocalizedMessage(), 
e);
+        }
+        final String suffix = super.setGridGeometry(index, gg);         // May 
throw `ArithmeticException`.
+        /*
+         * If the image is the main one, overwrite (possibly with same 
content) the previous auxiliary files.
+         * Otherwise above checks should have ensured that the existing 
auxiliary files are applicable.
+         */
+        if (suffix != null) {
+            if (gridToCRS == null) {
+                deleteAuxiliaryFile(suffix);
+            } else try (BufferedWriter out = writeAuxiliaryFile(suffix, 
encoding)) {
+writeCoeffs:    for (int i=0;; i++) {
+                    final double c;
+                    switch (i) {
+                        case 0: c = gridToCRS.getScaleX(); break;
+                        case 1: c = gridToCRS.getShearY(); break;
+                        case 2: c = gridToCRS.getShearX(); break;
+                        case 3: c = gridToCRS.getScaleY(); break;
+                        case 4: c = gridToCRS.getTranslateX(); break;
+                        case 5: c = gridToCRS.getTranslateY(); break;
+                        default: break writeCoeffs;
+                    }
+                    out.write(Double.toString(c));
+                    out.newLine();
+                }
+            }
+            writePRJ();
+        }
+        return suffix;
+    }
+
+    /**
+     * Creates a {@link GridCoverageResource} for the specified image.
+     * This method is invoked by {@link Components} when first needed
+     * and the result is cached by the caller.
+     *
+     * @param  index  index of the image for which to create a resource.
+     * @return resource for the image identified by the given index.
+     * @throws IndexOutOfBoundsException if the image index is out of bounds.
+     */
+    @Override
+    Image createImageResource(final int index) throws DataStoreException, 
IOException {
+        return new WritableImage(this, listeners, index, 
getGridGeometry(index));
+    }
+
+    /**
+     * Adds a new {@code Resource} in this {@code Aggregate}.
+     * The given {@link Resource} will be copied, and the <cite>effectively 
added</cite> resource returned.
+     *
+     * @param  resource  the resource to copy in this {@code Aggregate}.
+     * @return the effectively added resource.
+     * @throws DataStoreException if the given resource can not be stored in 
this {@code Aggregate}.
+     */
+    @Override
+    public synchronized Resource add(final Resource resource) throws 
DataStoreException {
+        Exception cause = null;
+        if (resource instanceof GridCoverageResource) try {
+            final Components components = components(true, numImages);
+            if (numImages < 0) {
+                numImages = components.size();      // For this method, we 
need an accurate count.
+            }
+            /*
+             * If we are adding the first image, the grid geometry of the 
coverage will determine
+             * the new grid geometry of the data store. Otherwise (if we are 
adding more images)
+             * the coverage grid geometry must be the same as the current data 
store grid geometry.
+             */
+            GridGeometry domain = null;
+            if (numImages != 0) {
+                domain = getGridGeometry(MAIN_IMAGE);
+            }
+            final GridCoverage coverage = ((GridCoverageResource) 
resource).read(domain, null);
+            if (domain == null) {
+                domain = coverage.getGridGeometry();        // We are adding 
the first image.
+            }
+            final WritableImage image = new WritableImage(this, listeners, 
numImages, domain);
+            image.write(coverage);
+            components.added(image);        // Must be invoked only after 
above succeeded.
+            numImages++;
+            return image;
+        } catch (IOException | RuntimeException e) {
+            cause = e;
+        }
+        throw new 
DataStoreException(resources().getString(Resources.Keys.CanNotWriteResource_1, 
label(resource)), cause);
+    }
+
+    /**
+     * Removes a {@code Resource} from this {@code Aggregate}.
+     * The given resource should be one of the instances returned by {@link 
#components()}.
+     *
+     * @param  resource  child resource to remove from this {@code Aggregate}.
+     * @throws DataStoreException if the given resource could not be removed.
+     */
+    @Override
+    public synchronized void remove(final Resource resource) throws 
DataStoreException {
+        Exception cause = null;
+        if (resource instanceof WritableImage) {
+            final WritableImage image = (WritableImage) resource;
+            if (image.store() == this) try {
+                final int imageIndex = image.imageIndex;
+                writer().removeImage(imageIndex);
+                final Components components = components(false, numImages);
+                if (components != null) {
+                    components.removed(imageIndex);
+                    image.dispose();
+                    numImages--;            // Okay if negative.
+                }
+            } catch (IOException | RuntimeException e) {
+                cause = e;
+            }
+        }
+        throw new DataStoreException(resources().getString(
+                Resources.Keys.CanNotRemoveResource_2, getDisplayName(), 
label(resource)), cause);
+    }
+
+    /**
+     * Returns a label for the given resource in error messages.
+     */
+    private static String label(final Resource resource) throws 
DataStoreException {
+        return resource.getIdentifier().map(Object::toString).orElse("?");
+    }
+
+    /**
+     * Prepares an image reader compatible with the writer and sets its input.
+     * This method is invoked for switching from write mode to read mode.
+     *
+     * @param  current  the current image reader, or {@code null} if none.
+     * @return the image reader to use, or {@code null} if none.
+     */
+    @Override
+    ImageReader prepareReader(ImageReader current) throws IOException {
+        final ImageWriter writer = this.writer;
+        if (writer != null) {
+            final Object output = writer.getOutput();
+            if (output != null) {
+                if (current == null) {
+                    final ImageWriterSpi wp = writer.getOriginatingProvider();
+                    if (wp != null) {
+                        final ImageReaderSpi rp = 
getProviderByClass(ImageReaderSpi.class, wp.getImageReaderSpiNames(), wp);
+                        if (rp != null) {
+                            current = rp.createReaderInstance();
+                        }
+                    }
+                }
+                if (current != null) {
+                    writer.setOutput(null);
+                    setStream(current, output, ImageReader::setInput);
+                    return current;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the writer if it has not been closed.
+     * If the data store was in read mode, invoking this method switch to 
write mode.
+     *
+     * @throws DataStoreClosedException if this data store is closed.
+     * @throws IOException if an error occurred while preparing the writer.
+     */
+    final ImageWriter writer() throws DataStoreException, IOException {
+        assert Thread.holdsLock(this);
+        ImageWriter current = writer;
+        if (current != null && current.getOutput() != null) {
+            return current;
+        }
+        final ImageReader reader = getCurrentReader();
+        if (reader != null) {
+            final Object input = reader.getInput();
+            if (input != null) {
+                if (current == null) {
+                    final ImageReaderSpi rp = reader.getOriginatingProvider();
+                    if (rp != null) {
+                        final ImageWriterSpi wp = 
getProviderByClass(ImageWriterSpi.class, rp.getImageWriterSpiNames(), rp);
+                        if (wp != null) {
+                            current = wp.createWriterInstance();
+                        }
+                    }
+                }
+                if (current != null) {
+                    reader.setInput(null);
+                    setStream(current, input, ImageWriter::setOutput);
+                    writer = current;
+                    configureWriter();
+                    return current;
+                }
+            }
+        }
+        throw new DataStoreClosedException(getLocale(), StoreProvider.NAME, 
StandardOpenOption.WRITE);
+    }
+
+    /**
+     * Sets the input or output stream on the given image reader or writer.
+     * If the operation fails, the stream is closed.
+     *
+     * @param  <T>     class of the {@code codec} argument.
+     * @param  codec   the {@link ImageReader} or {@link ImageWriter} on which 
to set the stream.
+     * @param  stream  the input or output to set on the specified {@code 
codec}.
+     * @param  setter  for calling the {@code setInput(Object)} or {@code 
setOutput(Object)} method.
+     */
+    private <T> void setStream(final T codec, final Object stream, final 
BiConsumer<T,Object> setter) throws IOException {
+        try {
+            /*
+             * `ImageOutputStream` extends `ImageInputStream`,
+             * so there is no need to check the output stream case.
+             */
+            if (stream instanceof ImageInputStream) {
+                ((ImageInputStream) stream).seek(streamBeginning);
+            }
+            setter.accept(codec, stream);
+        } catch (Throwable exception) {
+            if (stream instanceof AutoCloseable) try {
+                ((AutoCloseable) stream).close();
+            } catch (Throwable s) {
+                exception.addSuppressed(s);
+            }
+            throw exception;
+        }
+    }
+
+    /**
+     * Returns the first service provider that we can get from the given list 
of class names.
+     *
+     * @param  <T>          compile-time value of {@code type} argument.
+     * @param  type         type of the provider to get.
+     * @param  classNames   class names of provider implementations, or {@code 
null} if none.
+     * @param  originating  the originating provider, used for fetching the 
class loader.
+     * @return first provider found, or {@code null} if none.
+     */
+    private <T extends ImageReaderWriterSpi> T getProviderByClass(final 
Class<T> type,
+                    final String[] classNames, final ImageReaderWriterSpi 
originating)
+    {
+        if (classNames != null) {
+            final IIORegistry registry = IIORegistry.getDefaultInstance();
+            final ClassLoader loader = originating.getClass().getClassLoader();
+            for (final String name : classNames) {
+                final Class<? extends T> impl;
+                try {
+                    impl = Class.forName(name, true, loader).asSubclass(type);
+                } catch (ClassNotFoundException | ClassCastException e) {
+                    listeners.warning(e);
+                    continue;
+                }
+                final T candidate = registry.getServiceProviderByClass(impl);
+                if (candidate != null) {
+                    return candidate;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Closes this data store and releases any underlying resources.
+     *
+     * @throws DataStoreException if an error occurred while closing this data 
store.
+     */
+    @Override
+    public synchronized void close() throws DataStoreException {
+        try {
+            final ImageWriter codec = writer;
+            writer = null;
+            if (codec != null) try {
+                final Object output = codec.getOutput();
+                codec.setOutput(null);
+                codec.dispose();
+                if (output instanceof AutoCloseable) {
+                    ((AutoCloseable) output).close();
+                }
+            } catch (Exception e) {
+                throw new DataStoreException(e);
+            }
+        } catch (Throwable e) {
+            try {
+                super.close();
+            } catch (Throwable s) {
+                e.addSuppressed(s);
+            }
+            throw e;
+        }
+        super.close();
+    }
+}
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
index bf7eebff62..ea99ffe540 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
@@ -55,7 +55,7 @@ public final strictfp class SelfConsistencyTest extends 
CoverageReadConsistency
     public static void openFile() throws IOException, DataStoreException {
         final URL url = StoreTest.class.getResource("gradient.png");
         assumeNotNull(url);
-        store = new Store(null, new StorageConnector(url));
+        store = new Store(null, new StorageConnector(url), true);
     }
 
     /**
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
index 42feef4317..d3f6c02ddb 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
@@ -67,7 +67,7 @@ public final strictfp class StoreTest extends TestCase {
      */
     @Test
     public void testMetadata() throws DataStoreException, IOException {
-        try (Store store = new Store(null, testData())) {
+        try (Store store = new Store(null, testData(), true)) {
             assertEquals("gradient", store.getIdentifier().get().toString());
             final Metadata metadata = store.getMetadata();
             final Identification id = 
getSingleton(metadata.getIdentificationInfo());

Reply via email to