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 2022/05/04 17:00:47 UTC

[sis] branch geoapi-4.0 updated (9a87ff27c0 -> 9e5697263d)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from 9a87ff27c0 `skipBytes` method implementation should skip all specified bytes when possible, because some `ImageReader` implementation don't check the return value.
     new 8a881f42c4 Leverage more a simple helper method for getting the filename without suffix.
     new 6b8b00c6a2 Add a boolean argument to `AbstractResource` for making possible to use the parent listeners directly. The use case for direct use is when the resource is only a proxy for a "real" resource.
     new 9e5697263d If the World File uses a format which is known to support only one image (PNG, BMP, WBMP and maybe JPEG), returns a data store which implements directly `GridCoverageResource` instead of `Aggregate`.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../MultiResolutionCoverageLoaderTest.java         |   2 +-
 .../org/apache/sis/storage/landsat/BandGroup.java  |   2 +-
 .../sis/storage/landsat/MetadataReaderTest.java    |   2 +-
 .../sis/internal/netcdf/DiscreteSampling.java      |   2 +-
 .../apache/sis/internal/netcdf/RasterResource.java |   2 +-
 .../org/apache/sis/internal/netcdf/TestCase.java   |   2 +-
 .../org/apache/sis/internal/sql/feature/Table.java |   4 +-
 .../sis/internal/storage/AggregatedFeatureSet.java |   2 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |   2 +-
 .../sis/internal/storage/MemoryGridResource.java   |   2 +-
 .../sis/internal/storage/TiledGridResource.java    |   2 +-
 .../org/apache/sis/internal/storage/csv/Store.java |   6 +-
 .../sis/internal/storage/image/FormatFilter.java   |  19 +-
 .../sis/internal/storage/image/FormatFinder.java   | 288 +++++++++++++++++++++
 .../internal/storage/image/MultiImageStore.java    |  62 +++++
 .../internal/storage/image/SingleImageStore.java   | 191 ++++++++++++++
 .../internal/storage/image/WorldFileResource.java  |  16 +-
 .../sis/internal/storage/image/WorldFileStore.java | 116 +++------
 .../storage/image/WorldFileStoreProvider.java      |  67 +++--
 .../sis/internal/storage/image/WritableStore.java  |  81 ++----
 .../org/apache/sis/storage/AbstractFeatureSet.java |  20 +-
 .../sis/storage/AbstractGridCoverageResource.java  |  20 +-
 .../org/apache/sis/storage/AbstractResource.java   |  24 +-
 .../org/apache/sis/storage/CoverageSubset.java     |   2 +-
 .../java/org/apache/sis/storage/DataStore.java     |   2 +-
 .../java/org/apache/sis/storage/FeatureNaming.java |   1 +
 .../java/org/apache/sis/storage/FeatureSubset.java |   2 +-
 .../apache/sis/storage/event/StoreListeners.java   |   2 +-
 .../sis/internal/storage/RangeArgumentTest.java    |   2 +-
 .../storage/image/SelfConsistencyTest.java         |   2 +-
 .../internal/storage/image/WorldFileStoreTest.java |  12 +-
 .../org/apache/sis/storage/GridResourceMock.java   |   2 +-
 32 files changed, 760 insertions(+), 201 deletions(-)
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java


[sis] 02/03: Add a boolean argument to `AbstractResource` for making possible to use the parent listeners directly. The use case for direct use is when the resource is only a proxy for a "real" resource.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 6b8b00c6a2e8dc23149191e0ff429b5ce2077aa1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed May 4 13:06:59 2022 +0200

    Add a boolean argument to `AbstractResource` for making possible to use the parent listeners directly.
    The use case for direct use is when the resource is only a proxy for a "real" resource.
---
 .../MultiResolutionCoverageLoaderTest.java         |  2 +-
 .../org/apache/sis/storage/landsat/BandGroup.java  |  2 +-
 .../sis/storage/landsat/MetadataReaderTest.java    |  2 +-
 .../sis/internal/netcdf/DiscreteSampling.java      |  2 +-
 .../apache/sis/internal/netcdf/RasterResource.java |  2 +-
 .../org/apache/sis/internal/netcdf/TestCase.java   |  2 +-
 .../org/apache/sis/internal/sql/feature/Table.java |  4 ++--
 .../sis/internal/storage/AggregatedFeatureSet.java |  2 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |  2 +-
 .../sis/internal/storage/MemoryGridResource.java   |  2 +-
 .../sis/internal/storage/TiledGridResource.java    |  2 +-
 .../internal/storage/image/WorldFileResource.java  |  2 +-
 .../org/apache/sis/storage/AbstractFeatureSet.java | 20 +++++++++++++-----
 .../sis/storage/AbstractGridCoverageResource.java  | 20 +++++++++++++-----
 .../org/apache/sis/storage/AbstractResource.java   | 24 +++++++++++++++++-----
 .../org/apache/sis/storage/CoverageSubset.java     |  2 +-
 .../java/org/apache/sis/storage/FeatureSubset.java |  2 +-
 .../apache/sis/storage/event/StoreListeners.java   |  2 +-
 .../sis/internal/storage/RangeArgumentTest.java    |  2 +-
 .../org/apache/sis/storage/GridResourceMock.java   |  2 +-
 20 files changed, 67 insertions(+), 33 deletions(-)

diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoaderTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoaderTest.java
index e12505c98a..1368203d67 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoaderTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoaderTest.java
@@ -105,7 +105,7 @@ public final strictfp class MultiResolutionCoverageLoaderTest extends TestCase {
     private static final class DummyResource extends AbstractGridCoverageResource {
         /** Creates a dummy resource. */
         DummyResource() {
-            super(null);
+            super(null, false);
         }
 
         /** Returns the preferred resolutions in units of CRS axes. */
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/BandGroup.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/BandGroup.java
index 9b28c581c6..6cb675acdc 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/BandGroup.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/BandGroup.java
@@ -70,7 +70,7 @@ final class BandGroup extends AbstractResource implements Aggregate {
     private BandGroup(final StoreListeners parent, final BandGroupName group,
                       final Band[] resources, final int count)
     {
-        super(parent);
+        super(parent, false);
         this.group = group;
         int n = 0;
         Band[] components = new Band[resources.length];
diff --git a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
index a02db92183..fda6250105 100644
--- a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
+++ b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
@@ -269,7 +269,7 @@ public class MetadataReaderTest extends TestCase {
         final class DummyResource extends AbstractResource {
             /** Creates a dummy resource without parent. */
             DummyResource() {
-                super(null);
+                super(null, false);
             }
 
             /** Makes listeners accessible to this package. */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
index 1ede6de9fb..2da2f47c92 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
@@ -58,7 +58,7 @@ public abstract class DiscreteSampling extends AbstractFeatureSet {
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
     protected DiscreteSampling(final GeometryLibrary library, final StoreListeners listeners, final Object lock) {
-        super(listeners);
+        super(listeners, false);
         factory = Geometries.implementation(library);
         this.lock = lock;
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 87eba30b96..25ca8e6a05 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -188,7 +188,7 @@ public final class RasterResource extends AbstractGridCoverageResource implement
     private RasterResource(final Decoder decoder, final String name, final GridGeometry grid, final List<Variable> bands,
             final int numBands, final int bandDim, final Object lock)
     {
-        super(decoder.listeners);
+        super(decoder.listeners, false);
         this.lock     = lock;
         gridGeometry  = grid;
         bandDimension = bandDim;
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
index 5cbab55a27..bd2adecb2c 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
@@ -124,7 +124,7 @@ public abstract strictfp class TestCase extends org.apache.sis.test.TestCase {
         final class DummyResource extends AbstractResource {
             /** Creates a dummy resource without parent. */
             DummyResource() {
-                super(null);
+                super(null, false);
             }
 
             /** Makes listeners accessible to this package. */
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
index 05b889d543..6e5b40c17e 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
@@ -173,7 +173,7 @@ final class Table extends AbstractFeatureSet {
      *                   the table identified by {@link #name}.
      */
     Table(final Database<?> database, final FeatureAnalyzer analyzer, final String query) throws Exception {
-        super(database.listeners);
+        super(database.listeners, false);
         this.database = database;
         this.query    = query;
         name          = analyzer.id;
@@ -194,7 +194,7 @@ final class Table extends AbstractFeatureSet {
      *       A starting point is {@link org.apache.sis.storage.FeatureQuery#expectedType(FeatureType)}.
      */
     Table(final Table parent) {
-        super(parent.listeners);
+        super(parent.listeners, false);
         database = parent.database;
         query    = parent.query;
         name     = parent.name;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
index f71a6dc628..44870f51d6 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
@@ -69,7 +69,7 @@ abstract class AggregatedFeatureSet extends AbstractFeatureSet {
      *         that created this resource.
      */
     protected AggregatedFeatureSet(final StoreListeners parent) {
-        super(parent);
+        super(parent, false);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
index 46d842eb46..7249bbbb6c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
@@ -60,7 +60,7 @@ public class MemoryFeatureSet extends AbstractFeatureSet {
      * @param features   collection of stored features. This collection will not be copied.
      */
     public MemoryFeatureSet(final StoreListeners parent, final FeatureType type, final Collection<Feature> features) {
-        super(parent);
+        super(parent, false);
         ArgumentChecks.ensureNonNull("type",     type);
         ArgumentChecks.ensureNonNull("features", features);
         this.type     = type;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
index 9e352e3c26..bc28cec023 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
@@ -57,7 +57,7 @@ public class MemoryGridResource extends AbstractGridCoverageResource {
      * @param  coverage  stored coverage retained as-is (not copied). Can not be null.
      */
     public MemoryGridResource(final StoreListeners parent, final GridCoverage coverage) {
-        super(parent);
+        super(parent, false);
         ArgumentChecks.ensureNonNull("coverage", coverage);
         this.coverage = coverage;
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index 200f531923..09dc84c23a 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -125,7 +125,7 @@ public abstract class TiledGridResource extends AbstractGridCoverageResource {
      *         that created this resource.
      */
     protected TiledGridResource(final StoreListeners parent) {
-        super(parent);
+        super(parent, false);
         rasters = new WeakValueHashMap<>(CacheKey.class);
     }
 
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index 467cce78cc..05da7f95f6 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@ -117,7 +117,7 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes
     WorldFileResource(final WorldFileStore store, final StoreListeners parent,
                       final int imageIndex, final GridGeometry gridGeometry)
     {
-        super(parent);
+        super(parent, false);
         this.store        = store;
         this.imageIndex   = imageIndex;
         this.gridGeometry = gridGeometry;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java
index e8c094b156..be5fd48eb3 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java
@@ -51,14 +51,24 @@ import org.opengis.feature.FeatureType;
  */
 public abstract class AbstractFeatureSet extends AbstractResource implements FeatureSet {
     /**
-     * Creates a new resource. This resource will have its own set of listeners,
-     * but the listeners of the data store that created this resource will be notified as well.
+     * Creates a new resource which can send notifications to the given set of listeners.
+     * If {@code hidden} is {@code false} (the recommended value), then this resource will have its own set of
+     * listeners with this resource declared as the {@linkplain StoreListeners#getSource() source of events}.
+     * It will be possible to add and remove listeners independently from the set of parent listeners.
+     * Conversely if {@code hidden} is {@code true}, then the given listeners will be used directly
+     * and this resource will not appear as the source of any event.
      *
-     * @param  parent  listeners of the parent resource or data store, or {@code null} if none.
+     * <p>In any cases, the listeners of all parents (ultimately the data store that created this resource)
+     * will always be notified, either directly if {@code hidden} is {@code true}
+     * or indirectly if {@code hidden} is {@code false}.</p>
+     *
+     * @param  parentListeners  listeners of the parent resource, or {@code null} if none.
      *         This is usually the listeners of the {@link DataStore} that created this resource.
+     * @param  hidden  {@code false} if this resource shall use its own {@link StoreListeners}
+     *         with the specified parent, or {@code true} for using {@code parentListeners} directly.
      */
-    protected AbstractFeatureSet(final StoreListeners parent) {
-        super(parent);
+    protected AbstractFeatureSet(final StoreListeners parentListeners, final boolean hidden) {
+        super(parentListeners, hidden);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
index 09650502fb..f188e9fd4f 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
@@ -67,14 +67,24 @@ import org.apache.sis.internal.storage.Resources;
  */
 public abstract class AbstractGridCoverageResource extends AbstractResource implements GridCoverageResource {
     /**
-     * Creates a new resource. This resource will have its own set of listeners,
-     * but the listeners of the data store that created this resource will be notified as well.
+     * Creates a new resource which can send notifications to the given set of listeners.
+     * If {@code hidden} is {@code false} (the recommended value), then this resource will have its own set of
+     * listeners with this resource declared as the {@linkplain StoreListeners#getSource() source of events}.
+     * It will be possible to add and remove listeners independently from the set of parent listeners.
+     * Conversely if {@code hidden} is {@code true}, then the given listeners will be used directly
+     * and this resource will not appear as the source of any event.
      *
-     * @param  parent  listeners of the parent resource or data store, or {@code null} if none.
+     * <p>In any cases, the listeners of all parents (ultimately the data store that created this resource)
+     * will always be notified, either directly if {@code hidden} is {@code true}
+     * or indirectly if {@code hidden} is {@code false}.</p>
+     *
+     * @param  parentListeners  listeners of the parent resource, or {@code null} if none.
      *         This is usually the listeners of the {@link DataStore} that created this resource.
+     * @param  hidden  {@code false} if this resource shall use its own {@link StoreListeners}
+     *         with the specified parent, or {@code true} for using {@code parentListeners} directly.
      */
-    protected AbstractGridCoverageResource(final StoreListeners parent) {
-        super(parent);
+    protected AbstractGridCoverageResource(final StoreListeners parentListeners, final boolean hidden) {
+        super(parentListeners, hidden);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
index 4cf0f90984..38c2f29891 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
@@ -75,14 +75,28 @@ public abstract class AbstractResource implements Resource {
     private volatile Metadata metadata;
 
     /**
-     * Creates a new resource. This resource will have its own set of listeners,
-     * but the listeners of the data store that created this resource will be notified as well.
+     * Creates a new resource which can send notifications to the given set of listeners.
+     * If {@code hidden} is {@code false} (the recommended value), then this resource will have its own set of
+     * listeners with this resource declared as the {@linkplain StoreListeners#getSource() source of events}.
+     * It will be possible to add and remove listeners independently from the set of parent listeners.
+     * Conversely if {@code hidden} is {@code true}, then the given listeners will be used directly
+     * and this resource will not appear as the source of any event.
      *
-     * @param  parent  listeners of the parent resource or data store, or {@code null} if none.
+     * <p>In any cases, the listeners of all parents (ultimately the data store that created this resource)
+     * will always be notified, either directly if {@code hidden} is {@code true}
+     * or indirectly if {@code hidden} is {@code false}.</p>
+     *
+     * @param  parentListeners  listeners of the parent resource, or {@code null} if none.
      *         This is usually the listeners of the {@link DataStore} that created this resource.
+     * @param  hidden  {@code false} if this resource shall use its own {@link StoreListeners}
+     *         with the specified parent, or {@code true} for using {@code parentListeners} directly.
      */
-    protected AbstractResource(final StoreListeners parent) {
-        listeners = new StoreListeners(parent, this);
+    protected AbstractResource(final StoreListeners parentListeners, final boolean hidden) {
+        if (hidden && parentListeners != null) {
+            listeners = parentListeners;
+        } else {
+            listeners = new StoreListeners(parentListeners, this);
+        }
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
index 6e8e290de6..a5a6c7bd00 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
@@ -61,7 +61,7 @@ final class CoverageSubset extends AbstractGridCoverageResource {
      * @param query   the domain and range to read from the {@code source} coverage.
      */
     CoverageSubset(final GridCoverageResource source, final CoverageQuery query) {
-        super(source instanceof StoreListeners ? (StoreListeners) source : null);
+        super(source instanceof StoreListeners ? (StoreListeners) source : null, false);
         this.source = source;
         this.query  = query;
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
index ea99c4433c..8823cfa2cb 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
@@ -64,7 +64,7 @@ final class FeatureSubset extends AbstractFeatureSet {
      * This given query is stored as-is (it is not cloned neither optimized).
      */
     FeatureSubset(final FeatureSet source, final FeatureQuery query) {
-        super(source instanceof StoreListeners ? (StoreListeners) source : null);
+        super(source instanceof StoreListeners ? (StoreListeners) source : null, false);
         this.source = source;
         this.query = query;
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
index cf338bb321..8e4e4ac2d9 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
@@ -93,7 +93,7 @@ public class StoreListeners implements Localized {
     /**
      * The declared source of events. This is not necessarily the real source,
      * but this is the source that the implementer wants to declare as public API.
-     * Never {@code null} but may be {@code this}.
+     * Shall not be {@code null}.
      */
     private final Resource source;
 
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
index 9a5ea5540e..b1a89ec116 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
@@ -42,7 +42,7 @@ public final strictfp class RangeArgumentTest extends TestCase implements Locali
     /**
      * A resource performing no operation.
      */
-    private final AbstractGridCoverageResource resource = new AbstractGridCoverageResource(null) {
+    private final AbstractGridCoverageResource resource = new AbstractGridCoverageResource(null, false) {
         @Override public GridGeometry          getGridGeometry()     {throw new UnsupportedOperationException();}
         @Override public List<SampleDimension> getSampleDimensions() {throw new UnsupportedOperationException();}
         @Override public GridCoverage read(GridGeometry d, int... r) {throw new UnsupportedOperationException();}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/GridResourceMock.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/GridResourceMock.java
index 004c33b832..08cc9f6573 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/GridResourceMock.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/GridResourceMock.java
@@ -55,7 +55,7 @@ final strictfp class GridResourceMock extends AbstractGridCoverageResource {
      * Creates a resource mock with the given grid geometry.
      */
     GridResourceMock(final GridGeometry gridGeometry) {
-        super(null);
+        super(null, false);
         assertNotNull(gridGeometry);
         this.gridGeometry     = gridGeometry;
         this.sampleDimensions = Collections.singletonList(new SampleDimension.Builder().setName(0).build());


[sis] 01/03: Leverage more a simple helper method for getting the filename without suffix.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 8a881f42c4a704f392934ba23c8b66331e379789
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed May 4 10:37:21 2022 +0200

    Leverage more a simple helper method for getting the filename without suffix.
---
 .../main/java/org/apache/sis/internal/storage/csv/Store.java  |  6 +-----
 .../apache/sis/internal/storage/image/WorldFileResource.java  | 11 ++++++++++-
 .../src/main/java/org/apache/sis/storage/DataStore.java       |  2 +-
 .../src/main/java/org/apache/sis/storage/FeatureNaming.java   |  1 +
 4 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index 7580d9ce41..50fd6de7b7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -578,11 +578,7 @@ final class Store extends URIDataStore implements FeatureSet {
             }
             properties.add(createProperty(name, type, minOccurrence, maxOccurrence, characteristics));
         }
-        String name = super.getDisplayName();
-        final int s = name.lastIndexOf('.');
-        if (s > 0) {                            // Exclude 0 because shall not be the first character.
-            name = name.substring(0, s);
-        }
+        final String name = IOUtilities.filenameWithoutExtension(super.getDisplayName());
         return new DefaultFeatureType(Collections.singletonMap(DefaultFeatureType.NAME_KEY, name),
                 false, null, properties.toArray(new PropertyType[properties.size()]));
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index 57dee0fc58..467cce78cc 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@ -44,6 +44,7 @@ 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.storage.io.IOUtilities;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.resources.Vocabulary;
@@ -179,7 +180,15 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes
                     }
                     id = base + n;
                 }
-                identifier = Names.createLocalName(store.getDisplayName(), null, id);
+                /*
+                 * Get the filename and omit the extension. The `store.suffix` field is null if the input
+                 * source was not a File/Path/URI/URL, in which case we do not try to trim the extension.
+                 */
+                String filename = store.getDisplayName();
+                if (store.suffix != null) {
+                    filename = IOUtilities.filenameWithoutExtension(filename);
+                }
+                identifier = Names.createLocalName(filename, null, id);
             }
             return Optional.of(identifier);
         }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
index 8d5e6e0abb..5731e5f275 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
@@ -263,7 +263,7 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      * in identifiers like white spaces.</p>
      *
      * <p>This method should never throw an exception since it may be invoked for producing error
-     * messages, in which case throwing an exception here would mask the original exception.</p>
+     * messages, in which case throwing an exception here would hide the original exception.</p>
      *
      * <p>This method differs from {@link #getIdentifier()} in that it is typically a file name
      * known at construction time instead of a property read from metadata.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureNaming.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureNaming.java
index 69f0668102..82b8f23c9e 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureNaming.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureNaming.java
@@ -128,6 +128,7 @@ public class FeatureNaming<E> {
     /**
      * Returns the display name for the given data store, or the localizable "unnamed" string
      * if the given data store is null or does not have a display name.
+     * This is used for error message in exceptions.
      */
     private static CharSequence name(final DataStore store) {
         if (store != null) {


[sis] 03/03: If the World File uses a format which is known to support only one image (PNG, BMP, WBMP and maybe JPEG), returns a data store which implements directly `GridCoverageResource` instead of `Aggregate`.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 9e5697263de0940bb3567249f0fa7424d1215f71
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed May 4 18:59:15 2022 +0200

    If the World File uses a format which is known to support only one image (PNG, BMP, WBMP and maybe JPEG),
    returns a data store which implements directly `GridCoverageResource` instead of `Aggregate`.
---
 .../sis/internal/storage/image/FormatFilter.java   |  19 +-
 .../sis/internal/storage/image/FormatFinder.java   | 288 +++++++++++++++++++++
 .../internal/storage/image/MultiImageStore.java    |  62 +++++
 .../internal/storage/image/SingleImageStore.java   | 191 ++++++++++++++
 .../internal/storage/image/WorldFileResource.java  |   5 +-
 .../sis/internal/storage/image/WorldFileStore.java | 116 +++------
 .../storage/image/WorldFileStoreProvider.java      |  67 +++--
 .../sis/internal/storage/image/WritableStore.java  |  81 ++----
 .../storage/image/SelfConsistencyTest.java         |   2 +-
 .../internal/storage/image/WorldFileStoreTest.java |  12 +-
 10 files changed, 681 insertions(+), 162 deletions(-)

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 ceda08a109..0d1056b864 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
@@ -168,14 +168,15 @@ enum FormatFilter {
      * This is intentionally not done automatically by {@link StorageConnector}.
      *
      * @param  identifier  the property value to use as a filtering criterion, or {@code null} if none.
-     * @param  connector   provider of the input to be given to the new reader instance.
+     * @param  format      provider of the input to be given to the new reader instance.
      * @param  deferred    initially empty map to be populated with providers tested by this method.
      * @return the new image reader instance with its input initialized, or {@code null} if none was found.
      * @throws DataStoreException if an error occurred while opening a stream from the storage connector.
      * @throws IOException if an error occurred while creating the image reader instance.
      */
-    final ImageReader createReader(final String identifier, final StorageConnector connector,
-                                   final Map<ImageReaderSpi,Boolean> deferred) throws IOException, DataStoreException
+    final ImageReader createReader(final String identifier, final FormatFinder format,
+                                   final Map<ImageReaderSpi,Boolean> deferred)
+            throws IOException, DataStoreException
     {
         final Iterator<ImageReaderSpi> it = getServiceProviders(ImageReaderSpi.class, identifier);
         while (it.hasNext()) {
@@ -183,12 +184,12 @@ enum FormatFilter {
             if (deferred.putIfAbsent(provider, Boolean.FALSE) == null) {
                 for (final Class<?> type : provider.getInputTypes()) {
                     if (ArraysExt.contains(VALID_INPUTS, type)) {
-                        final Object input = connector.getStorageAs(type);
+                        final Object input = format.connector.getStorageAs(type);
                         if (input != null) {
                             if (provider.canDecodeInput(input)) {
-                                connector.closeAllExcept(input);
                                 final ImageReader reader = provider.createReaderInstance();
                                 reader.setInput(input, false, true);
+                                format.keepOpen = input;
                                 return reader;
                             }
                             break;        // Skip other input types, try the next provider.
@@ -209,14 +210,14 @@ enum FormatFilter {
      * This is intentionally not done automatically by {@link StorageConnector}.
      *
      * @param  identifier  the property value to use as a filtering criterion, or {@code null} if none.
-     * @param  output      the output to be given to the new reader instance.
+     * @param  format      provider of the output to be given to the new writer instance.
      * @param  image       the image to write, or {@code null} if unknown.
      * @param  deferred    initially empty map to be populated with providers tested by this method.
      * @return the new image writer instance with its output initialized, or {@code null} if none was found.
      * @throws DataStoreException if an error occurred while opening a stream from the storage connector.
      * @throws IOException if an error occurred while creating the image writer instance.
      */
-    final ImageWriter createWriter(final String identifier, final StorageConnector connector, final RenderedImage image,
+    final ImageWriter createWriter(final String identifier, final FormatFinder format, final RenderedImage image,
                                    final Map<ImageWriterSpi,Boolean> deferred) throws IOException, DataStoreException
     {
         final Iterator<ImageWriterSpi> it = getServiceProviders(ImageWriterSpi.class, identifier);
@@ -226,11 +227,11 @@ enum FormatFilter {
                 if (image == null || provider.canEncodeImage(image)) {
                     for (final Class<?> type : provider.getOutputTypes()) {
                         if (ArraysExt.contains(VALID_OUTPUTS, type)) {
-                            final Object output = connector.getStorageAs(type);
+                            final Object output = format.connector.getStorageAs(type);
                             if (output != null) {
-                                connector.closeAllExcept(output);
                                 final ImageWriter writer = provider.createWriterInstance();
                                 writer.setOutput(output);
+                                format.keepOpen = output;
                                 return writer;
                             }
                         }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java
new file mode 100644
index 0000000000..d4f7f83837
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java
@@ -0,0 +1,288 @@
+/*
+ * 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.io.DataOutput;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.FileImageOutputStream;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.internal.storage.io.IOUtilities;
+
+
+/**
+ * Helper class for finding the {@link ImageReader} or {@link ImageWriter} instance to use.
+ * This is a temporary object used only at {@link WorldFileStore} construction time.
+ * It also helps to choose which {@link WorldFileStore} subclass to instantiate.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class FormatFinder implements AutoCloseable {
+    /**
+     * The factory that created this {@code DataStore} instance, or {@code null} if unspecified.
+     */
+    final WorldFileStoreProvider provider;
+
+    /**
+     * Information about the storage (URL, stream, <i>etc</i>).
+     */
+    final StorageConnector connector;
+
+    /**
+     * The {@link #connector} object to keep open if we successfully created a {@link WorldFileStore}.
+     * This is often the same object than {@link #storage} but may be different if an {@link ImageInputStream}
+     * has been created from the storage object.
+     *
+     * <p>This value is {@code null} until successful instantiation of image reader or writer.
+     * A null value means to close everything, which is the desired behavior in case of failure.</p>
+     */
+    Object keepOpen;
+
+    /**
+     * The file, URL or stream where to read or write the image.
+     * If the {@linkplain StorageConnector#getStorage() user-specified storage} was an {@link ImageReader}
+     * or {@link ImageWriter}, then the value stored in this field is the input/output of the reader/writer.
+     */
+    final Object storage;
+
+    /**
+     * The image reader if specified or created by this {@code FormatFinder}, or {@code null}.
+     */
+    private ImageReader reader;
+
+    /**
+     * The image writer if specified or created by this {@code FormatFinder}, or {@code null}.
+     */
+    private ImageWriter writer;
+
+    /**
+     * Whether we already made an attempt to find the image reader or writer using {@link ImageIO} registry.
+     */
+    private boolean readerLookupDone, writerLookupDone;
+
+    /**
+     * {@code true} if the {@linkplain #storage} seems to be writable.
+     */
+    final boolean isWritable;
+
+    /**
+     * {@code true} if the storage should be open is write mode instead of read mode.
+     * This is {@code true} if the file does not exist or the file is empty.
+     */
+    final boolean openAsWriter;
+
+    /**
+     * {@code true} if the file is known to be empty, or {@code false} in case of doubt.
+     */
+    final boolean fileIsEmpty;
+
+    /**
+     * The filename extension (may be an empty string), or {@code null} if unknown.
+     * It does not include the leading dot.
+     */
+    final String suffix;
+
+    /**
+     * Creates a new format finder.
+     *
+     * @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>).
+     */
+    FormatFinder(final WorldFileStoreProvider provider, final StorageConnector connector)
+            throws DataStoreException, IOException
+    {
+        this.provider  = provider;
+        this.connector = connector;
+        Object storage = connector.getStorage();
+        if (storage instanceof ImageReader) {
+            reader  = (ImageReader) storage;
+            storage = reader.getInput();
+            readerLookupDone = true;
+        } else if (storage instanceof ImageWriter) {
+            writer  = (ImageWriter) storage;
+            storage = writer.getOutput();
+            writerLookupDone = true;
+        }
+        this.storage = storage;
+        this.suffix  = IOUtilities.extension(storage);
+        /*
+         * Detect if the image can be opened in read/write mode.
+         * If not, it will be opened in read-only mode.
+         */
+        if (writer != null) {
+            isWritable   = true;
+            openAsWriter = true;
+            fileIsEmpty  = false;
+        } else if (reader != null) {
+            isWritable   = (reader.getInput() instanceof DataOutput);         // Parent of ImageOutputStream.
+            openAsWriter = false;
+            fileIsEmpty  = false;
+        } else {
+            isWritable = WorldFileStoreProvider.isWritable(connector);
+            if (isWritable) {
+                final Path path = connector.getStorageAs(Path.class);
+                if (path != null) {
+                    fileIsEmpty  = !Files.exists(path) || Files.size(path) == 0;
+                    openAsWriter = fileIsEmpty;
+                    return;
+                }
+            }
+            openAsWriter = false;
+            fileIsEmpty  = false;
+        }
+    }
+
+    /**
+     * Returns the name of the format.
+     *
+     * @return name of the format, or {@code null} if unknown.
+     */
+    final String[] getFormatName() throws DataStoreException, IOException {
+        if (openAsWriter) {
+            final ImageWriter writer = getOrCreateWriter();
+            if (writer != null) {
+                final ImageWriterSpi spi = writer.getOriginatingProvider();
+                if (spi != null) {
+                    return spi.getFormatNames();
+                }
+            }
+        } else {
+            final ImageReader reader = getOrCreateReader();
+            if (reader != null) {
+                final ImageReaderSpi spi = reader.getOriginatingProvider();
+                if (spi != null) {
+                    return spi.getFormatNames();
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the user-specified reader or searches for a reader that claim to be able to read the storage input.
+     * This method tries first the readers associated to the file suffix. If no reader is found, then this method
+     * tries all other readers.
+     *
+     * @return the reader, or {@code null} if none could be found.
+     */
+    final ImageReader getOrCreateReader() throws DataStoreException, IOException {
+        if (!readerLookupDone) {
+            readerLookupDone = true;
+            final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
+            if (suffix != null) {
+                reader = FormatFilter.SUFFIX.createReader(suffix, this, deferred);
+            }
+            if (reader == null) {
+                reader = FormatFilter.SUFFIX.createReader(null, this, deferred);
+                if (reader == null) {
+                    /*
+                     * If no reader has been found, maybe `StorageConnector` has not been able to create
+                     * an `ImageInputStream`. It may happen if the storage object is of unknown type.
+                     * Check if it is the case, then try all providers that we couldn't try because of that.
+                     */
+                    ImageInputStream stream = null;
+                    for (final Map.Entry<ImageReaderSpi,Boolean> entry : deferred.entrySet()) {
+                        if (entry.getValue()) {
+                            if (stream == null) {
+                                if (isWritable) {
+                                    // 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)) {
+                                reader = p.createReaderInstance();
+                                reader.setInput(stream);
+                                keepOpen = storage;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return reader;
+    }
+
+    /**
+     * Returns the user-specified writer or searches for a writer for the file suffix.
+     *
+     * @return the writer, or {@code null} if none could be found.
+     */
+    final ImageWriter getOrCreateWriter() throws DataStoreException, IOException {
+        if (!writerLookupDone) {
+            writerLookupDone = true;
+            final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>();
+            if (suffix != null) {
+                writer = FormatFilter.SUFFIX.createWriter(suffix, this, null, deferred);
+            }
+            if (writer == null) {
+                writer = FormatFilter.SUFFIX.createWriter(null, this, null, deferred);
+                if (writer == null) {
+                    ImageOutputStream stream = null;
+                    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();
+                            writer = p.createWriterInstance();
+                            writer.setOutput(stream);
+                            keepOpen = storage;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        return writer;
+    }
+
+    /**
+     * Closes all unused resources. Keep open only the find of objects needed by the image reader or writer.
+     * This method must be invoked after by {@link WorldFileStore} construction.
+     */
+    @Override
+    public final void close() throws DataStoreException {
+        connector.closeAllExcept(keepOpen);
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java
new file mode 100644
index 0000000000..5609c1ebd6
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.WritableAggregate;
+import org.apache.sis.storage.DataStoreException;
+
+
+/**
+ * A world file store exposing in the public API the fact that it is an aggregate.
+ * This class is used for image formats that may store many images per file.
+ * Examples: TIFF and GIF image formats.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class MultiImageStore extends WorldFileStore implements Aggregate {
+    /**
+     * Creates a new store from the given file, URL or stream.
+     *
+     * @param  format  information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
+     * @throws DataStoreException if an error occurred while opening the stream.
+     * @throws IOException if an error occurred while creating the image reader instance.
+     */
+    MultiImageStore(final FormatFinder format) throws DataStoreException, IOException {
+        super(format, false);
+    }
+
+    /**
+     * The writable variant of {@link MultiImageStore}.
+     */
+    static final class Writable extends WritableStore implements WritableAggregate {
+        /**
+         * Creates a new store from the given file, URL or stream.
+         *
+         * @param  format  information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
+         * @throws DataStoreException if an error occurred while opening the stream.
+         * @throws IOException if an error occurred while creating the image reader instance.
+         */
+        Writable(final FormatFinder format) throws DataStoreException, IOException {
+            super(format);
+        }
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java
new file mode 100644
index 0000000000..1c540ca55e
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java
@@ -0,0 +1,191 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import java.io.IOException;
+import org.opengis.geometry.Envelope;
+import org.apache.sis.coverage.SampleDimension;
+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.GridCoverageResource;
+import org.apache.sis.storage.RasterLoadingStrategy;
+import org.apache.sis.storage.UnsupportedQueryException;
+import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.storage.Query;
+
+
+/**
+ * A world file store which is expected to contain exactly one image.
+ * This class is used for image formats that are restricted to one image per file.
+ * Examples: PNG and BMP image formats.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+class SingleImageStore extends WorldFileStore implements GridCoverageResource {
+    /**
+     * The singleton resource in this aggregate. Fetched when first needed.
+     */
+    private volatile WorldFileResource delegate;
+
+    /**
+     * Creates a new store from the given file, URL or stream.
+     *
+     * @param  format  information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
+     * @throws DataStoreException if an error occurred while opening the stream.
+     * @throws IOException if an error occurred while creating the image reader instance.
+     */
+    SingleImageStore(final FormatFinder format) throws DataStoreException, IOException {
+        super(format, false);
+    }
+
+    /**
+     * Returns {@code true} for meaning that the singleton component will be used only for internal purposes.
+     */
+    @Override
+    final boolean isComponentHidden() {
+        return true;
+    }
+
+    /**
+     * Returns the singleton resource in this aggregate. The delegate is used for all
+     * {@link GridCoverageResource} operations but <strong>not</strong> for the following operations:
+     *
+     * <ul>
+     *   <li>{@link #getIdentifier()} because we want the filename without ":1" suffix (the image index).</li>
+     *   <li>{@link #getMetadata()} because it is richer than {@link WorldFileResource#getMetadata()}.</li>
+     * </ul>
+     */
+    final WorldFileResource delegate() throws DataStoreException {
+        WorldFileResource r = delegate;
+        if (r == null) {
+            delegate = r = ((Components) components()).get(MAIN_IMAGE);
+        }
+        return r;
+    }
+
+    /**
+     * Returns the valid extent of grid coordinates together with the conversion from those grid coordinates
+     * to real world coordinates. The CRS and "pixels to CRS" conversion may be unknown if the {@code *.prj}
+     * and/or world auxiliary file has not been found.
+     */
+    @Override
+    public final GridGeometry getGridGeometry() throws DataStoreException {
+        return delegate().getGridGeometry();
+    }
+
+    /**
+     * Returns the envelope of the grid geometry if known.
+     * The envelope is absent if the grid geometry does not provide this information.
+     */
+    @Override
+    public final Optional<Envelope> getEnvelope() throws DataStoreException {
+        return delegate().getEnvelope();
+    }
+
+    /**
+     * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store.
+     */
+    @Override
+    public final List<double[]> getResolutions() throws DataStoreException {
+        return delegate().getResolutions();
+    }
+
+    /**
+     * Returns the ranges of sample values in each band. Those sample dimensions describe colors
+     * because the World File format does not provide more information.
+     */
+    @Override
+    public final List<SampleDimension> getSampleDimensions() throws DataStoreException {
+        return delegate().getSampleDimensions();
+    }
+
+    /**
+     * Requests a subset of the coverage.
+     */
+    @Override
+    public final GridCoverageResource subset(Query query) throws UnsupportedQueryException, DataStoreException {
+        return delegate().subset(query);
+    }
+
+    /**
+     * Loads a subset of the image wrapped by this resource.
+     *
+     * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
+     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
+     * @return the grid coverage for the specified domain and range.
+     */
+    @Override
+    public final GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
+        return delegate().read(domain, range);
+    }
+
+    /**
+     * Returns an indication about when the "physical" loading of raster data will happen.
+     */
+    @Override
+    public final RasterLoadingStrategy getLoadingStrategy() throws DataStoreException {
+        return delegate().getLoadingStrategy();
+    }
+
+    /**
+     * Sets the preferred strategy about when to do the "physical" loading of raster data.
+     * Implementations are free to ignore this parameter or to replace the given strategy
+     * by the closest alternative that this resource can support.
+     *
+     * @param  strategy  the desired strategy for loading raster data.
+     * @return {@code true} if the given strategy has been accepted, or {@code false}
+     *         if this implementation replaced the given strategy by an alternative.
+     */
+    @Override
+    public final boolean setLoadingStrategy(RasterLoadingStrategy strategy) throws DataStoreException {
+        return delegate().setLoadingStrategy(strategy);
+    }
+
+    /**
+     * The writable variant of {@link MultiImageStore}.
+     */
+    static final class Writable extends SingleImageStore implements WritableGridCoverageResource {
+        /**
+         * Creates a new store from the given file, URL or stream.
+         *
+         * @param  format  information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
+         * @throws DataStoreException if an error occurred while opening the stream.
+         * @throws IOException if an error occurred while creating the image reader instance.
+         */
+        Writable(final FormatFinder format) throws DataStoreException, IOException {
+            super(format);
+        }
+
+        /**
+         * 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.
+         */
+        @Override
+        public void write(GridCoverage coverage, Option... options) throws DataStoreException {
+            ((WritableResource) delegate()).write(coverage, options);
+        }
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index 05da7f95f6..8c2f9e53ec 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@ -117,7 +117,7 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes
     WorldFileResource(final WorldFileStore store, final StoreListeners parent,
                       final int imageIndex, final GridGeometry gridGeometry)
     {
-        super(parent, false);
+        super(parent, store.isComponentHidden());
         this.store        = store;
         this.imageIndex   = imageIndex;
         this.gridGeometry = gridGeometry;
@@ -208,7 +208,8 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes
     }
 
     /**
-     * Returns the ranges of sample values.
+     * Returns the ranges of sample values in each band. Those sample dimensions describe colors
+     * because the World File format does not provide more information.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
index 4ff7398186..0d48e9e8ef 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
@@ -20,7 +20,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Map;
 import java.util.HashMap;
-import java.util.LinkedHashMap;
 import java.util.logging.Level;
 import java.io.IOException;
 import java.io.EOFException;
@@ -32,8 +31,6 @@ import java.nio.file.NoSuchFileException;
 import java.nio.file.StandardOpenOption;
 import javax.imageio.ImageIO;
 import javax.imageio.ImageReader;
-import javax.imageio.ImageWriter;
-import javax.imageio.spi.ImageReaderSpi;
 import javax.imageio.stream.ImageInputStream;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.maintenance.ScopeCode;
@@ -53,7 +50,6 @@ 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;
-import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.util.ListOfUnknownSize;
@@ -109,13 +105,16 @@ import org.apache.sis.setup.OptionKey;
  * Because some image formats can store an arbitrary amount of images,
  * this data store is considered as an aggregate with one resource per image.
  * All image should have the same size and all resources will share the same {@link GridGeometry}.
+ * However this base class does not implement the {@link Aggregate} interface directly in order to
+ * give a chance to subclasses to implement {@link GridCoverageResource} directly when the format
+ * is known to support only one image per file.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  * @since   1.2
  * @module
  */
-class WorldFileStore extends PRJDataStore implements Aggregate {
+class WorldFileStore extends PRJDataStore {
     /**
      * Image I/O format names (ignoring case) for which we have an entry in the {@code SpatialMetadata} database.
      */
@@ -217,84 +216,45 @@ class WorldFileStore 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.
      */
-    WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector, final boolean readOnly)
+    public WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector)
             throws DataStoreException, IOException
     {
-        super(provider, connector);
-        identifiers = new HashMap<>();
+        this(new FormatFinder(provider, connector), true);
+    }
+
+    /**
+     * Creates a new store from the given file, URL or stream.
+     *
+     * @param  format    information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
+     * @param  readOnly  {@code true} if the store should be open in read-only mode, ignoring {@code format}.
+     *                   This is a workaround for RFE #4093999 in Sun's bug database, for allowing us to invoke
+     *                   {@link FormatFinder#cleanup()} when invoked from the public constructor.
+     * @throws DataStoreException if an error occurred while opening the stream.
+     * @throws IOException if an error occurred while creating the image reader instance.
+     */
+    WorldFileStore(final FormatFinder format, final boolean readOnly) throws DataStoreException, IOException {
+        super(format.provider, format.connector);
         listeners.useWarningEventsOnly();
-        final Object storage = connector.getStorage();
-        if (storage instanceof ImageReader) {
-            reader = (ImageReader) storage;
-            suffix = IOUtilities.extension(reader.getInput());
+        identifiers = new HashMap<>();
+        suffix = format.suffix;
+        if (readOnly || !format.openAsWriter) {
+            reader = format.getOrCreateReader();
+            if (reader == null) {
+                throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME,
+                            format.storage, format.connector.getOption(OptionKey.OPEN_OPTIONS));
+            }
             configureReader();
-            return;
-        }
-        if (storage instanceof ImageWriter) {
-            suffix = IOUtilities.extension(((ImageWriter) storage).getOutput());
-            return;
-        }
-        suffix = IOUtilities.extension(storage);
-        if (!(readOnly || fileExists(connector))) {
+            if (readOnly) {
+                format.close();
+            }
             /*
-             * 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.
+             * 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.
              */
-            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);
-        }
-        if (reader == null) {
-            reader = FormatFilter.SUFFIX.createReader(null, connector, deferred);
-fallback:   if (reader == null) {
-                /*
-                 * If no reader has been found, maybe `StorageConnector` has not been able to create
-                 * an `ImageInputStream`. It may happen if the storage object is of unknown type.
-                 * Check if it is the case, then try all providers that we couldn't try because of that.
-                 */
-                ImageInputStream stream = null;
-                for (final Map.Entry<ImageReaderSpi,Boolean> entry : deferred.entrySet()) {
-                    if (entry.getValue()) {
-                        if (stream == null) {
-                            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);
-                            break fallback;
-                        }
-                    }
-                }
-                throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME,
-                            storage, connector.getOption(OptionKey.OPEN_OPTIONS));
-            }
-        }
-        configureReader();
-        /*
-         * 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.
-         */
     }
 
     /**
@@ -567,7 +527,6 @@ loop:   for (int convention=0;; convention++) {
      *
      * @return list of images in this store.
      */
-    @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public final synchronized Collection<? extends GridCoverageResource> components() throws DataStoreException {
         if (components == null) try {
@@ -762,6 +721,15 @@ loop:   for (int convention=0;; convention++) {
         return new WorldFileResource(this, listeners, index, getGridGeometry(index));
     }
 
+    /**
+     * Whether the component of this data store is used only as a delegate.
+     * This is {@code false} when the components will be given to the user,
+     * or {@code true} if the singleton component will be used only for internal purposes.
+     */
+    boolean isComponentHidden() {
+        return false;
+    }
+
     /**
      * Prepares an image reader compatible with the writer and sets its input.
      * This method is invoked for switching from write mode to read mode.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
index 5306122bb2..e9c3959cd8 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
@@ -18,10 +18,7 @@ package org.apache.sis.internal.storage.image;
 
 import java.util.Set;
 import java.util.HashSet;
-import java.io.DataOutput;
 import java.io.IOException;
-import javax.imageio.ImageReader;
-import javax.imageio.ImageWriter;
 import javax.imageio.spi.ImageReaderSpi;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
@@ -30,7 +27,9 @@ 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.Aggregate;
 import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
@@ -51,10 +50,42 @@ public final class WorldFileStoreProvider extends PRJDataStore.Provider {
      */
     static final String NAME = "World file";
 
+    /**
+     * Name of image formats that are considered to allow only one image.
+     * There is no public Image I/O API giving this information, so we have to use a hard-coded list.
+     * All formats not in this list are assumed to allow more than one image.
+     *
+     * <h4>Case of JPEG</h4>
+     * The JPEG image reader implementation in standard JDK seems to count a number of images that can be anything.
+     * However documentation on the web often describes the JPEG format as a container for a single image.
+     * It is not clear if we should include JPEG in this list or not.
+     */
+    private static final String[] SINGLE_IMAGE_FORMATS = {"PNG", "BMP", "WBMP", "JPEG"};
+
+    /**
+     * Whether the provider is allowed to create {@link GridCoverageResource} instances
+     * instead of {@link Aggregate} instances.
+     */
+    private final boolean allowSingleton;
+
     /**
      * Creates a new provider.
      */
     public WorldFileStoreProvider() {
+        allowSingleton = true;
+    }
+
+    /**
+     * Creates a new provider with the given configuration.
+     * If {@code allowSingleton} is {@code false}, then this provider will unconditionally create
+     * {@link WorldFileStore} instances that implement the {@link Aggregate} interface, regardless
+     * if the image format allows many pictures or not.
+     *
+     * @param allowSingleton  whether the provider is allowed to create {@code GridCoverageResource} instances
+     *        instead of {@code Aggregate} instances.
+     */
+    public WorldFileStoreProvider(final boolean allowSingleton) {
+        this.allowSingleton = allowSingleton;
     }
 
     /**
@@ -78,25 +109,29 @@ public final class WorldFileStoreProvider extends PRJDataStore.Provider {
      */
     @Override
     public WorldFileStore open(final StorageConnector connector) throws DataStoreException {
-        final Object storage = connector.getStorage();
-        boolean isWritable = (storage instanceof ImageWriter);
-        if (!isWritable) {
-            if (storage instanceof ImageReader) {
-                Object input = ((ImageReader) storage).getInput();
-                isWritable = (input instanceof DataOutput);         // Parent of ImageOutputStream.
-            } else {
-                isWritable = isWritable(connector);
+        final WorldFileStore store;
+        try (FormatFinder format = new FormatFinder(this, connector)) {
+            boolean isSingleton = false;
+            if (allowSingleton) {
+                final String[] names = format.getFormatName();
+                if (names != null) {
+                    for (final String name : names) {
+                        isSingleton = ArraysExt.containsIgnoreCase(SINGLE_IMAGE_FORMATS, name);
+                        if (isSingleton) break;
+                    }
+                }
             }
-        }
-        try {
-            if (isWritable) {
-                return new WritableStore(this, connector);
+            if (format.isWritable) {
+                store = isSingleton ? new SingleImageStore.Writable(format)
+                                    : new  MultiImageStore.Writable(format);
             } else {
-                return new WorldFileStore(this, connector, true);
+                store = isSingleton ? new SingleImageStore(format)
+                                    : new  MultiImageStore(format);
             }
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
+        return store;
     }
 
     /**
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
index bd7fe0078c..3d74d0b54e 100644
--- 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
@@ -16,10 +16,7 @@
  */
 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;
@@ -33,7 +30,6 @@ 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;
@@ -69,12 +65,20 @@ import org.apache.sis.setup.OptionKey;
  * The image writer will be {@linkplain ImageWriter#dispose() disposed}
  * and its output closed (if {@link AutoCloseable}) when this data store is {@linkplain #close() closed}.</p>
  *
+ * <h2>Handling of multi-image files</h2>
+ * Because some image formats can store an arbitrary amount of images,
+ * this data store is considered as an aggregate with one resource per image.
+ * All image should have the same size and all resources will share the same {@link GridGeometry}.
+ * However this base class does not implement the {@link WritableAggregate} interface directly in order
+ * to give a chance to subclasses to implement {@link GridCoverageResource} directly when the format is
+ * known to support only one image per file.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  * @since   1.2
  * @module
  */
-final class WritableStore extends WorldFileStore implements WritableAggregate {
+class WritableStore extends WorldFileStore {
     /**
      * Position of the input/output stream beginning. This is usually 0.
      */
@@ -101,68 +105,28 @@ final class WritableStore extends WorldFileStore implements WritableAggregate {
     /**
      * 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>).
+     * @param  format  information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
      * @throws DataStoreException if an error occurred while opening the stream.
      * @throws IOException if an error occurred while creating the image reader instance.
      */
-    WritableStore(final WorldFileStoreProvider provider, final StorageConnector connector)
-            throws DataStoreException, IOException
-    {
-        super(provider, connector, false);
-        final Object storage = connector.getStorage();
-        final ImageReader reader = getCurrentReader();
-        final Object inout;
-        if (reader != null) {
-            inout = reader.getInput();
-            numImages = -1;
-        } else if (storage instanceof ImageWriter) {
-            writer = (ImageWriter) storage;
-            inout  = writer.getOutput();
-            configureWriter();
+    WritableStore(final FormatFinder format) throws DataStoreException, IOException {
+        super(format, false);
+        if (getCurrentReader() != null) {
             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);
-            }
+            writer = format.getOrCreateWriter();
             if (writer == null) {
-                writer = FormatFilter.SUFFIX.createWriter(null, connector, null, deferred);
-fallback:       if (writer == null) {
-                    ImageOutputStream stream = null;
-                    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(), WorldFileStoreProvider.NAME,
-                                storage, connector.getOption(OptionKey.OPEN_OPTIONS));
-                }
+                throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME,
+                            format.storage, format.connector.getOption(OptionKey.OPEN_OPTIONS));
             }
             configureWriter();
-            inout = writer.getOutput();
-            // Leave `numImages` to 0 because we know that the stream is empty.
+            if (!format.fileIsEmpty) {
+                numImages = -1;
+            } else {
+                // Leave `numImages` to 0.
+            }
         }
-        streamBeginning = (inout instanceof ImageInputStream) ? ((ImageInputStream) inout).getStreamPosition() : 0;
+        streamBeginning = (format.storage instanceof ImageInputStream) ? ((ImageInputStream) format.storage).getStreamPosition() : 0;
     }
 
     /**
@@ -303,7 +267,6 @@ writeCoeffs:    for (int i=0;; i++) {
      * @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 {
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 62778d2c60..5da4c06fd3 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 = WorldFileStoreTest.class.getResource("gradient.png");
         assertNotNull("Test file not found.", url);
-        store = new WorldFileStore(null, new StorageConnector(url), true);
+        store = new WorldFileStore(null, new StorageConnector(url));
     }
 
     /**
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
index ec967d2d67..9c174d5241 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
@@ -76,7 +76,15 @@ public final strictfp class WorldFileStoreTest extends TestCase {
     public void testMetadata() throws DataStoreException {
         final WorldFileStoreProvider provider = new WorldFileStoreProvider();
         try (WorldFileStore store = provider.open(testData())) {
+            /*
+             * Opportunistic check of store type. Should be read-only,
+             * and should have been simplified to the "single image" case.
+             */
             assertFalse(store instanceof WritableStore);
+            assertTrue(store instanceof SingleImageStore);
+            /*
+             * Verify metadata content.
+             */
             assertEquals("gradient", store.getIdentifier().get().toString());
             final Metadata metadata = store.getMetadata();
             final Identification id = getSingleton(metadata.getIdentificationInfo());
@@ -98,6 +106,8 @@ public final strictfp class WorldFileStoreTest extends TestCase {
 
     /**
      * Tests reading the coverage and writing it in a new file.
+     * This test unconditionally open the data store as an aggregate,
+     * i.e. it bypasses the simplification of PNG files as {@link SingleImageStore} view.
      *
      * @throws DataStoreException if an error occurred during Image I/O or data store operations.
      * @throws IOException if an error occurred when creating, reading or deleting temporary files.
@@ -106,7 +116,7 @@ public final strictfp class WorldFileStoreTest extends TestCase {
     public void testReadWrite() throws DataStoreException, IOException {
         final Path directory = Files.createTempDirectory("SIS-");
         try {
-            final WorldFileStoreProvider provider = new WorldFileStoreProvider();
+            final WorldFileStoreProvider provider = new WorldFileStoreProvider(false);
             try (WorldFileStore source = provider.open(testData())) {
                 assertFalse(source instanceof WritableStore);
                 final GridCoverageResource resource = getSingleton(source.components());