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 cddbe5f8a07f66e82d29d5af096cc8520341933d
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Apr 18 19:36:09 2022 +0200

    Add tests for World File image reader and fix the `GridCoverageResource` 
implementation.
---
 ide-project/NetBeans/build.xml                     |   2 +
 .../apache/sis/internal/storage/image/Image.java   |  73 +++++++++++++++---
 .../storage/image/SelfConsistencyTest.java         |  83 +++++++++++++++++++++
 .../sis/internal/storage/image/StoreTest.java      |  58 ++++++++++++++
 .../apache/sis/test/suite/StorageTestSuite.java    |   2 +
 .../apache/sis/internal/storage/image/README.md    |  11 +++
 .../apache/sis/internal/storage/image/gradient.pgw |   6 ++
 .../apache/sis/internal/storage/image/gradient.png | Bin 0 -> 176 bytes
 .../apache/sis/internal/storage/image/gradient.prj |   8 ++
 9 files changed, 231 insertions(+), 12 deletions(-)

diff --git a/ide-project/NetBeans/build.xml b/ide-project/NetBeans/build.xml
index ba9d889f9a..7e9bc0715d 100644
--- a/ide-project/NetBeans/build.xml
+++ b/ide-project/NetBeans/build.xml
@@ -303,6 +303,8 @@
       <fileset dir="${project.root}/storage/sis-storage/src/test/resources">
         <include name="**/*.txt"/>
         <include name="**/*.asc"/>
+        <include name="**/*.png"/>
+        <include name="**/*.pgw"/>
         <include name="**/*.prj"/>
         <include name="**/*.xml"/>
       </fileset>
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 0b6b8805e0..c803dee70b 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
@@ -21,12 +21,15 @@ import java.util.Optional;
 import java.io.IOException;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
+import java.awt.image.BandedSampleModel;
 import javax.imageio.ImageReader;
 import javax.imageio.ImageReadParam;
 import javax.imageio.ImageTypeSpecifier;
 import org.opengis.util.GenericName;
+import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridCoverage2D;
 import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -36,11 +39,12 @@ 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.StoreResource;
+import org.apache.sis.internal.storage.RangeArgument;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.iso.Names;
 
 import static java.lang.Math.toIntExact;
-import org.apache.sis.coverage.grid.GridCoverage2D;
 
 
 /**
@@ -172,7 +176,7 @@ class Image extends AbstractGridCoverageResource implements 
StoreResource {
      * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
      */
     @Override
-    public final GridCoverage read(GridGeometry domain, final int... range) 
throws DataStoreException {
+    public final GridCoverage read(GridGeometry domain, int... range) throws 
DataStoreException {
         synchronized (store) {
             final ImageReader reader = store.reader();
             final ImageReadParam param = reader.getDefaultReadParam();
@@ -183,25 +187,70 @@ class Image extends AbstractGridCoverageResource 
implements StoreResource {
                 final GridExtent extent = gd.getIntersection();
                 final int[] subsampling = gd.getSubsampling();
                 final int[] offsets     = gd.getSubsamplingOffsets();
-                domain = gd.build();
-                param.setSourceSubsampling(subsampling[X_DIMENSION], 
subsampling[Y_DIMENSION],
-                                           offsets[X_DIMENSION], 
offsets[Y_DIMENSION]);
-                param.setSourceRegion(new Rectangle(
+                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))));
-            }
-            if (range != null) {
-                param.setSourceBands(range);
+                        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));
             }
-            final List<SampleDimension> sampleDimensions = 
getSampleDimensions();
-            final RenderedImage image;
+            RenderedImage image;
+            List<SampleDimension> sampleDimensions = getSampleDimensions();
             try {
+                /*
+                 * 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.
+                 * This code applies heuristic rules forwarding the request to 
the image reader only for what should
+                 * be the easiest cases. More difficult cases will be handled 
after the reading.
+                 * Those heuristic rules may be changed in any future version.
+                 */
+                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));
+                        if (args.hasAllBands || type.getSampleModel() 
instanceof BandedSampleModel) {
+                            range = args.getSelectedBands();
+                            param.setSourceBands(range);
+                            param.setDestinationBands(ArraysExt.range(0, 
range.length));
+                            range = null;
+                        }
+                    }
+                }
                 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);
         }
     }
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
new file mode 100644
index 0000000000..bf7eebff62
--- /dev/null
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.net.URL;
+import java.io.IOException;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.test.storage.CoverageReadConsistency;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import static org.junit.Assume.assumeNotNull;
+
+
+/**
+ * Test consistency of read operations in random domains.
+ * Assuming that the code reading the full extent is correct, this class can 
detect some bugs
+ * in the code reading sub-regions or applying sub-sampling. This assumption 
is reasonable if
+ * we consider that the code reading the full extent is usually simpler than 
the code reading
+ * a subset of data.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public final strictfp class SelfConsistencyTest extends 
CoverageReadConsistency {
+    /**
+     * The store used for the test, opened only once.
+     */
+    private static Store store;
+
+    /**
+     * Opens the test file to be used for all tests.
+     *
+     * @throws IOException if an error occurred while opening the file.
+     * @throws DataStoreException if an error occurred while reading the file.
+     */
+    @BeforeClass
+    public static void openFile() throws IOException, DataStoreException {
+        final URL url = StoreTest.class.getResource("gradient.png");
+        assumeNotNull(url);
+        store = new Store(null, new StorageConnector(url));
+    }
+
+    /**
+     * Closes the test file used by all tests.
+     *
+     * @throws DataStoreException if an error occurred while closing the file.
+     */
+    @AfterClass
+    public static void closeFile() throws DataStoreException {
+        final Store s = store;
+        if (s != null) {
+            store = null;       // Clear first in case of failure.
+            s.close();
+        }
+    }
+
+    /**
+     * Creates a new test case.
+     *
+     * @throws DataStoreException if an error occurred while fetching the 
first image.
+     */
+    public SelfConsistencyTest() throws DataStoreException {
+        super(store.components().iterator().next());
+    }
+}
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
new file mode 100644
index 0000000000..c23bfca33b
--- /dev/null
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link Store} and {@link StoreProvider}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public final strictfp class StoreTest extends TestCase {
+    /**
+     * Returns a storage connector with the URL to the test data.
+     */
+    private static StorageConnector testData() {
+        return new 
StorageConnector(StoreTest.class.getResource("gradient.png"));
+    }
+
+    /**
+     * Tests {@link StoreProvider#probeContent(StorageConnector)} method.
+     *
+     * @throws DataStoreException if en error occurred while reading the CSV 
file.
+     */
+    @Test
+    public void testProbeContent() throws DataStoreException {
+        final StoreProvider p = new StoreProvider();
+        final ProbeResult r = p.probeContent(testData());
+        assertTrue(r.isSupported());
+        assertEquals("image/png", r.getMimeType());
+    }
+
+
+}
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index 72f51311fe..073c8ed5f1 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -57,6 +57,8 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.storage.wkt.StoreTest.class,
     org.apache.sis.internal.storage.csv.StoreProviderTest.class,
     org.apache.sis.internal.storage.csv.StoreTest.class,
+    org.apache.sis.internal.storage.image.StoreTest.class,
+    org.apache.sis.internal.storage.image.SelfConsistencyTest.class,
     org.apache.sis.internal.storage.ascii.StoreTest.class,
     org.apache.sis.internal.storage.ascii.WritableStoreTest.class,
     org.apache.sis.internal.storage.folder.StoreTest.class,
diff --git 
a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md
 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md
new file mode 100644
index 0000000000..d009776ba6
--- /dev/null
+++ 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md
@@ -0,0 +1,11 @@
+This repository contains an image of size 128×64 pixels georefenced
+as if it was covering the world from −180° to 180° of longitude and
+from −90° to 90° of latitude. Pixel values in all bands vary from 0
+inclusive to 256 exclusive with the following formulas:
+
+   Band 0: x⋅2
+   Band 1: y⋅4
+   Band 2: y⋅2 + x
+
+It makes easy to determine the location error when the wrong pixel
+is read.
diff --git 
a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw
 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw
new file mode 100644
index 0000000000..74494b8da4
--- /dev/null
+++ 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw
@@ -0,0 +1,6 @@
+2.8125
+0
+0
+-2.8125
+-178.59375
+88.59375
diff --git 
a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png
 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png
new file mode 100644
index 0000000000..e1dc33c1e7
Binary files /dev/null and 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png
 differ
diff --git 
a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj
 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj
new file mode 100644
index 0000000000..74c958c5be
--- /dev/null
+++ 
b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj
@@ -0,0 +1,8 @@
+GEODCRS["WGS 84",
+  DATUM["World Geodetic System 1984",
+    ELLIPSOID["WGS 84", 6378137.0, 298.257223563, LENGTHUNIT["metre", 1]]],
+  CS[ellipsoidal, 2],
+    AXIS["Longitude (L)", east, ORDER[1]],
+    AXIS["Latitude (B)", north, ORDER[2]],
+    ANGLEUNIT["degree", 0.017453292519943295],
+  ID["CRS", 84, CITATION["OGC:WMS"]]]

Reply via email to