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 9252ad9e1d6941540009f5f5e65ab61848807479
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Apr 13 19:21:30 2022 +0200

    First draft of World File reader as a wrapper around standard Java Image 
I/O.
    
    https://issues.apache.org/jira/browse/SIS-541
---
 .../apache/sis/internal/storage/PRJDataStore.java  |  92 +++-
 .../sis/internal/storage/image/FormatFilter.java   | 202 +++++++++
 .../apache/sis/internal/storage/image/Image.java   | 208 +++++++++
 .../apache/sis/internal/storage/image/Store.java   | 502 +++++++++++++++++++++
 .../sis/internal/storage/image/StoreProvider.java  |  81 ++++
 .../internal/storage/image/WarningListener.java    |  56 +++
 .../sis/internal/storage/image/package-info.java   |  57 +++
 7 files changed, 1192 insertions(+), 6 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
index ad49f1d563..d0175e3e3d 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
@@ -143,7 +143,7 @@ public abstract class PRJDataStore extends URIDataStore {
      */
     protected final void readPRJ() throws DataStoreException {
         try {
-            final String wkt = readAuxiliaryFile(PRJ, encoding);
+            final String wkt = readAuxiliaryFile(PRJ, encoding).toString();
             if (wkt != null) {
                 final StoreFormat format = new StoreFormat(locale, timezone, 
null, listeners);
                 format.setConvention(Convention.WKT1_COMMON_UNITS);
@@ -162,16 +162,17 @@ public abstract class PRJDataStore extends URIDataStore {
      * This method uses the same URI than {@link #location},
      * except for the extension which is replaced by the given value.
      * This method is suitable for reasonably small files.
+     * An arbitrary size limit is applied for safety.
      *
-     * @param  extension  the filename extension of the auxiliary file to open.
-     * @param  encoding   the encoding to use for reading the file content, or 
{@code null} for default.
-     * @return a stream opened on the specified file.
+     * @param  extension    the filename extension of the auxiliary file to 
open.
+     * @param  encoding     the encoding to use for reading the file content, 
or {@code null} for default.
+     * @return the file content together with the source. Should be 
short-lived.
      * @throws NoSuchFileException if the auxiliary file has not been found 
(when opened from path).
      * @throws FileNotFoundException if the auxiliary file has not been found 
(when opened from URL).
      * @throws IOException if another error occurred while opening the stream.
      * @throws DataStoreException if the auxiliary file content seems too 
large.
      */
-    protected final String readAuxiliaryFile(final String extension, Charset 
encoding)
+    protected final AuxiliaryContent readAuxiliaryFile(final String extension, 
Charset encoding)
             throws IOException, DataStoreException
     {
         if (encoding == null) {
@@ -215,7 +216,81 @@ public abstract class PRJDataStore extends URIDataStore {
                     buffer = Arrays.copyOf(buffer, offset*2);
                 }
             }
-            return new String(buffer, 0, offset);
+            return new AuxiliaryContent(source, buffer, 0, offset);
+        }
+    }
+
+    /**
+     * Content of a file read by {@link #readAuxiliaryFile(String, Charset)}.
+     * This is used as a workaround for not being able to return multiple 
values from a single method.
+     * Instances of this class should be short lived, because they hold larger 
arrays than necessary.
+     */
+    protected static final class AuxiliaryContent implements CharSequence {
+        /** {@link Path} or {@link URL} that have been read. */
+        private final Object source;
+
+        /** The textual content of the auxiliary file. */
+        private final char[] buffer;
+
+        /** Index of the first valid character in {@link #buffer}. */
+        private final int offset;
+
+        /** Number of valid characters in {@link #buffer}. */
+        private final int length;
+
+        /** Wraps (without copying) the given array as the content of an 
auxiliary file. */
+        private AuxiliaryContent(final Object source, final char[] buffer, 
final int offset, final int length) {
+            this.source = source;
+            this.buffer = buffer;
+            this.offset = offset;
+            this.length = length;
+        }
+
+        /**
+         * Returns the filename (without path) of the auxiliary file.
+         * This information is mainly for producing error messages.
+         *
+         * @return name of the auxiliary file that have been read.
+         */
+        public String getFilename() {
+            return IOUtilities.filename(source);
+        }
+
+        /**
+         * Returns the number of valid characters in this sequence.
+         */
+        @Override
+        public int length() {
+            return length;
+        }
+
+        /**
+         * Returns the character at the given index. For performance reasons 
this method does not check index bounds.
+         * The behavior of this method is undefined if the given index is not 
smaller than {@link #length()}.
+         * We skip bounds check because this class should be used for Apache 
SIS internal purposes only.
+         */
+        @Override
+        public char charAt(final int index) {
+            return buffer[offset + index];
+        }
+
+        /**
+         * Returns a sub-sequence of this auxiliary file content. For 
performance reasons this method does not
+         * perform bound checks. The behavior of this method is undefined if 
arguments are out of bounds.
+         * We skip bounds check because this class should be used for Apache 
SIS internal purposes only.
+         */
+        @Override
+        public CharSequence subSequence(final int start, final int end) {
+            return new AuxiliaryContent(source, buffer, offset + start, end - 
start);
+        }
+
+        /**
+         * Copies this auxiliary file content in a {@link String}.
+         * This method does not cache the result; caller should invoke at most 
once.
+         */
+        @Override
+        public String toString() {
+            return new String(buffer, offset, length);
         }
     }
 
@@ -330,6 +405,11 @@ public abstract class PRJDataStore extends URIDataStore {
         return paths;
     }
 
+    /**
+     * Returns the filename of the given path without the file suffix.
+     * The returned string always ends in {@code '.'}, making it ready
+     * for concatenation of a new suffix.
+     */
     private static String getBaseFilename(final Path path) {
         final String base = path.getFileName().toString();
         final int s = base.lastIndexOf('.');
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
new file mode 100644
index 0000000000..603852c9ab
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
@@ -0,0 +1,202 @@
+/*
+ * 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.Iterator;
+import java.util.function.Function;
+import java.net.URI;
+import java.net.URL;
+import java.io.File;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.awt.image.RenderedImage;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.spi.ImageReaderWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
+
+
+/**
+ * Specify the property to use as a filtering criterion for choosing an image 
reader or writer.
+ * This is used for providing utility methods about image formats.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+enum FormatFilter {
+    /**
+     * Filter the providers by format name.
+     */
+    NAME(ImageReaderWriterSpi::getFormatNames),
+
+    /**
+     * Filter the providers by file extension.
+     */
+    SUFFIX(ImageReaderWriterSpi::getFileSuffixes),
+
+    /**
+     * Filter the providers by MIME type.
+     */
+    MIME(ImageReaderWriterSpi::getMIMETypes);
+
+    /**
+     * The method to invoke for getting the property values
+     * (name, suffix or MIME type) to use for filtering.
+     */
+    private final Function<ImageReaderWriterSpi, String[]> property;
+
+    /**
+     * Valid types of inputs accepted by this class.
+     */
+    private static final Class<?>[] VALID_INPUTS = {
+        // ImageInputStream case included by DataInput.
+        DataInput.class, InputStream.class, File.class, Path.class, URL.class, 
URI.class
+    };
+
+    /**
+     * Valid types of outputs accepted by this class.
+     */
+    private static final Class<?>[] VALID_OUTPUTS = {
+        // ImageOutputStream case included by DataOutput.
+        DataOutput.class, OutputStream.class, File.class, Path.class, 
URL.class, URI.class
+    };
+
+    /**
+     * Creates a new enumeration value.
+     */
+    private FormatFilter(final Function<ImageReaderWriterSpi, String[]> 
property) {
+        this.property = property;
+    }
+
+    /**
+     * Returns an iterator over all providers of the given category having the 
given name,
+     * suffix or MIME type.
+     *
+     * @param  <T>         the compile-time type of the {@code category} 
argument.
+     * @param  category    either {@link ImageReaderSpi} or {@link 
ImageWriterSpi}.
+     * @param  identifier  the property value to use as a filtering criterion, 
or {@code null} if none.
+     * @return an iterator over the requested providers.
+     */
+    private <T extends ImageReaderWriterSpi> Iterator<T> 
getServiceProviders(final Class<T> category, final String identifier) {
+        final IIORegistry registry = IIORegistry.getDefaultInstance();
+        if (identifier != null) {
+            final IIORegistry.Filter filter = (provider) -> {
+                final String[] identifiers = 
property.apply((ImageReaderWriterSpi) provider);
+                return ArraysExt.contains(identifiers, identifier);
+            };
+            return registry.getServiceProviders(category, filter, true);
+        } else {
+            return registry.getServiceProviders(category, true);
+        }
+    }
+
+    /**
+     * Creates a new reader for the given input. Caller needs to invoke this 
method with an initially empty
+     * {@code deferred} map, which will be populated by this method. Providers 
associated to {@code TRUE}
+     * should be tested again by the caller with an {@link ImageInputStream} 
created by the caller.
+     * 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  input       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 Iterator<ImageReaderSpi> it = 
getServiceProviders(ImageReaderSpi.class, identifier);
+        while (it.hasNext()) {
+            final ImageReaderSpi provider = it.next();
+            if (deferred.putIfAbsent(provider, Boolean.FALSE) == null) {
+                for (final Class<?> type : provider.getInputTypes()) {
+                    if (Classes.isAssignableToAny(type, VALID_INPUTS)) {
+                        final Object input = connector.getStorageAs(type);
+                        if (input != null) {
+                            if (provider.canDecodeInput(input)) {
+                                connector.closeAllExcept(input);
+                                final ImageReader reader = 
provider.createReaderInstance();
+                                reader.setInput(input, false, true);
+                                return reader;
+                            }
+                        } else if (type == ImageInputStream.class) {
+                            deferred.put(provider, Boolean.TRUE);
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates a new writer for the given output. Caller needs to invoke this 
method with an initially empty
+     * {@code deferred} map, which will be populated by this method. Providers 
associated to {@code TRUE}
+     * should be tested again by the caller with an {@link ImageOutputStream} 
created by the caller.
+     * 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  image       the image to write.
+     * @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 Map<ImageWriterSpi,Boolean> deferred) 
throws IOException, DataStoreException
+    {
+        final Iterator<ImageWriterSpi> it = 
getServiceProviders(ImageWriterSpi.class, identifier);
+        while (it.hasNext()) {
+            final ImageWriterSpi provider = it.next();
+            if (deferred.putIfAbsent(provider, Boolean.FALSE) == null) {
+                if (provider.canEncodeImage(image)) {
+                    for (final Class<?> type : provider.getOutputTypes()) {
+                        if (Classes.isAssignableToAny(type, VALID_OUTPUTS)) {
+                            final Object output = connector.getStorageAs(type);
+                            if (output != null) {
+                                connector.closeAllExcept(output);
+                                final ImageWriter writer = 
provider.createWriterInstance();
+                                writer.setOutput(output);
+                                return writer;
+                            } else if (type == ImageOutputStream.class) {
+                                deferred.put(provider, Boolean.TRUE);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+}
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
new file mode 100644
index 0000000000..0b6b8805e0
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
@@ -0,0 +1,208 @@
+/*
+ * 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 java.awt.Rectangle;
+import java.awt.image.RenderedImage;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+import org.opengis.util.GenericName;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridDerivation;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.storage.AbstractGridCoverageResource;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.internal.storage.StoreResource;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.util.iso.Names;
+
+import static java.lang.Math.toIntExact;
+import org.apache.sis.coverage.grid.GridCoverage2D;
+
+
+/**
+ * A single image in a {@link Store}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+class Image extends AbstractGridCoverageResource implements StoreResource {
+    /**
+     * The dimensions of <var>x</var> and <var>y</var> axes.
+     * Static constants for now, may become configurable fields in the future.
+     */
+    private static final int X_DIMENSION = 0, Y_DIMENSION = 1;
+
+    /**
+     * The parent data store.
+     */
+    private final Store store;
+
+    /**
+     * Index of the image to read.
+     */
+    private final int imageIndex;
+
+    /**
+     * The identifier as a sequence number in the namespace of the {@link 
Store}.
+     * The first image has the sequence number "1". This is computed when 
first needed.
+     *
+     * @see #getIdentifier()
+     */
+    private GenericName identifier;
+
+    /**
+     * The grid geometry of this resource. The grid extent is the image size.
+     *
+     * @see #getGridGeometry()
+     */
+    private final GridGeometry gridGeometry;
+
+    /**
+     * The ranges of sample values, computed when first needed. Shall be an 
unmodifiable list.
+     *
+     * @see #getSampleDimensions()
+     */
+    private List<SampleDimension> sampleDimensions;
+
+    /**
+     * 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.
+     */
+    Image(final Store store, final StoreListeners parent, final int 
imageIndex, final GridGeometry gridGeometry) {
+        super(parent);
+        this.store        = store;
+        this.imageIndex   = imageIndex;
+        this.gridGeometry = gridGeometry;
+    }
+
+    /**
+     * Returns the data store that produced this resource.
+     */
+    @Override
+    public final DataStore getOriginator() {
+        return store;
+    }
+
+    /**
+     * Returns the resource identifier. The name space is the file name and
+     * the local part of the name is the image index number, starting at 1.
+     */
+    @Override
+    public Optional<GenericName> getIdentifier() throws DataStoreException {
+        synchronized (store) {
+            if (identifier == null) {
+                identifier = Names.createLocalName(store.getDisplayName(), 
null, String.valueOf(imageIndex + 1));
+            }
+            return Optional.of(identifier);
+        }
+    }
+
+    /**
+     * 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 this image is not
+     * the {@linkplain Store#MAIN_IMAGE main image}, or if the {@code *.prj} 
and/or world auxiliary file has
+     * not been found.
+     */
+    @Override
+    public final GridGeometry getGridGeometry() throws DataStoreException {
+        return gridGeometry;
+    }
+
+    /**
+     * Returns the ranges of sample values.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public final List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        synchronized (store) {
+            if (sampleDimensions == null) try {
+                final ImageReader        reader = store.reader();
+                final ImageTypeSpecifier type   = 
reader.getRawImageType(imageIndex);
+                final SampleDimension[]  bands  = new 
SampleDimension[type.getNumBands()];
+                final SampleDimension.Builder b = new 
SampleDimension.Builder();
+                for (int i=0; i<bands.length; i++) {
+                    /*
+                     * TODO: we could consider a mechanism similar to 
org.apache.sis.internal.geotiff.SchemaModifier
+                     * if there is a need to customize the sample dimensions. 
`SchemaModifier` could become a shared
+                     * public interface.
+                     */
+                    bands[i] = b.setName(i + 1).build();
+                    b.clear();
+                }
+                sampleDimensions = UnmodifiableArrayList.wrap(bands);
+            } catch (IOException e) {
+                throw new DataStoreException(e);
+            }
+            return sampleDimensions;
+        }
+    }
+
+    /**
+     * 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.
+     * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     */
+    @Override
+    public final GridCoverage read(GridGeometry domain, final int... range) 
throws DataStoreException {
+        synchronized (store) {
+            final ImageReader reader = store.reader();
+            final ImageReadParam param = reader.getDefaultReadParam();
+            if (domain == null) {
+                domain = gridGeometry;
+            } else {
+                final GridDerivation gd = 
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
+                final GridExtent extent = gd.getIntersection();
+                final int[] subsampling = gd.getSubsampling();
+                final int[] offsets     = gd.getSubsamplingOffsets();
+                domain = gd.build();
+                param.setSourceSubsampling(subsampling[X_DIMENSION], 
subsampling[Y_DIMENSION],
+                                           offsets[X_DIMENSION], 
offsets[Y_DIMENSION]);
+                param.setSourceRegion(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);
+            }
+            final List<SampleDimension> sampleDimensions = 
getSampleDimensions();
+            final RenderedImage image;
+            try {
+                image = reader.readAsRenderedImage(imageIndex, param);
+            } catch (IOException e) {
+                throw new DataStoreException(e);
+            }
+            return new GridCoverage2D(domain, sampleDimensions, image);
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
new file mode 100644
index 0000000000..299b1db493
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
@@ -0,0 +1,502 @@
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.io.IOException;
+import java.io.EOFException;
+import java.io.FileNotFoundException;
+import java.io.UncheckedIOException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.StandardOpenOption;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.referencing.datum.PixelInCell;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.DataStoreContentException;
+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;
+import org.apache.sis.metadata.sql.MetadataStoreException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.setup.OptionKey;
+
+
+/**
+ * A data store which creates grid coverages from Image I/O.
+ * The store is considered as an aggregate, with one resource per image.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class Store extends PRJDataStore implements Aggregate {
+    /**
+     * Index of the main image. This is relevant only with formats capable to 
store an arbitrary amount of images.
+     * Current implementation assumes that the main image is always the first 
one, but it may become configurable
+     * in a future version if useful.
+     *
+     * @see #width
+     * @see #height
+     */
+    private static final int MAIN_IMAGE = 0;
+
+    /**
+     * The default World File suffix when it can not be determined from {@link 
#location}.
+     * This is a GDAL convention.
+     */
+    private static final String DEFAULT_SUFFIX = "wld";
+
+    /**
+     * The filename extension (may be an empty string), or {@code null} if 
unknown.
+     * It does not include the leading dot.
+     */
+    private final String suffix;
+
+    /**
+     * The image reader, set by the constructor and cleared when no longer 
needed.
+     */
+    private ImageReader reader;
+
+    /**
+     * Width and height of the main image.
+     * The {@link #gridGeometry} is assumed valid only for images having this 
size.
+     *
+     * @see #MAIN_IMAGE
+     * @see #gridGeometry
+     */
+    private int width, height;
+
+    /**
+     * The conversion from pixel center to CRS, or {@code null} if none or not 
yet computed.
+     * The grid extent has the size given by {@link #width} and {@link 
#height}.
+     *
+     * @see #crs
+     * @see #width
+     * @see #height
+     * @see #getGridGeometry(int)
+     */
+    private GridGeometry gridGeometry;
+
+    /**
+     * All images in this resource, created when first needed.
+     * Elements in this list will also be created when first needed.
+     *
+     * @see #components()
+     */
+    private List<Image> components;
+
+    /**
+     * The metadata object, or {@code null} if not yet created.
+     *
+     * @see #getMetadata()
+     */
+    private Metadata metadata;
+
+    /**
+     * Creates a new store from the given file, URL or stream.
+     *
+     * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @throws DataStoreException if an error occurred while opening the 
stream.
+     * @throws IOException if an error occurred while creating the image 
reader instance.
+     */
+    public Store(final StoreProvider provider, final StorageConnector 
connector)
+            throws DataStoreException, IOException
+    {
+        super(provider, connector);
+        final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
+        final Object storage = connector.getStorage();
+        suffix = IOUtilities.extension(storage);
+        /*
+         * 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.
+         */
+        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) {
+                            stream = ImageIO.createImageInputStream(storage);
+                            if (stream == null) break;
+                        }
+                        final ImageReaderSpi p = entry.getKey();
+                        if (p.canDecodeInput(stream)) {
+                            connector.closeAllExcept(storage);
+                            reader = p.createReaderInstance();
+                            reader.setInput(stream, false, true);
+                            break fallback;
+                        }
+                    }
+                }
+                throw new UnsupportedStorageException(super.getLocale(), 
StoreProvider.NAME,
+                            storage, 
connector.getOption(OptionKey.OPEN_OPTIONS));
+            }
+        }
+        /*
+         * Sets the locale to use for warning messages, if supported. If the 
reader
+         * does not support the locale, the reader's default locale will be 
used.
+         */
+        try {
+            reader.setLocale(listeners.getLocale());
+        } catch (IllegalArgumentException e) {
+            // Ignore
+        }
+    }
+
+    /**
+     * Returns the preferred suffix for the auxiliary world file. For TIFF 
images, this is {@code "tfw"}.
+     * This method tries to use the same case (lower-case or upper-case) than 
the suffix of the main file.
+     */
+    private String getWorldFileSuffix() {
+        if (suffix != null) {
+            final int length = suffix.length();
+            if (suffix.codePointCount(0, length) >= 2) {
+                boolean lower = true;
+                for (int i = length; i > 0;) {
+                    final int c = suffix.codePointBefore(i);
+                    lower =  Character.isLowerCase(c); if ( lower) break;
+                    lower = !Character.isUpperCase(c); if (!lower) break;
+                    i -= Character.charCount(c);
+                }
+                // If the case can not be determined, `lower` will default to 
`true`.
+                return new StringBuilder(3)
+                        .appendCodePoint(suffix.codePointAt(0))
+                        .appendCodePoint(suffix.codePointBefore(length))
+                        .append(lower ? 'w' : 'W').toString();
+            }
+        }
+        return DEFAULT_SUFFIX;
+    }
+
+    /**
+     * Reads the "World file" by searching for an auxiliary file with a suffix 
inferred from
+     * the suffix of the main file. This method tries suffixes with the 
following conventions,
+     * in preference order.
+     *
+     * <ol>
+     *   <li>First letter of main file suffix, followed by last letter, 
followed by {@code 'w'}.</li>
+     *   <li>Full suffix of the main file followed by {@code 'w'}.</li>
+     *   <li>{@value #DEFAULT_SUFFIX}.</li>
+     * </ol>
+     *
+     * @return the "World file" content as an affine transform, or {@code 
null} if none was found.
+     * @throws IOException if an I/O error occurred.
+     * @throws DataStoreException if the auxiliary file content can not be 
parsed.
+     */
+    private AffineTransform2D readWorldFile() throws IOException, 
DataStoreException {
+        IOException warning = null;
+        final String preferred = getWorldFileSuffix();
+loop:   for (int convention=0;; convention++) {
+            final String wld;
+            switch (convention) {
+                default: break loop;
+                case 0:  wld = preferred;      break;       // First file 
suffix to search.
+                case 2:  wld = DEFAULT_SUFFIX; break;       // File suffix to 
search in last resort.
+                case 1: {
+                    if (preferred.equals(DEFAULT_SUFFIX)) break loop;
+                    wld = suffix + preferred.charAt(preferred.length() - 1);
+                    break;
+                }
+            }
+            try {
+                return readWorldFile(wld);
+            } catch (NoSuchFileException | FileNotFoundException e) {
+                if (warning == null) {
+                    warning = e;
+                } else {
+                    warning.addSuppressed(e);
+                }
+            }
+        }
+        if (warning != null) {
+            
listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, 
preferred), warning);
+        }
+        return null;
+    }
+
+    /**
+     * Reads the "World file" by parsing an auxiliary file with the given 
suffix.
+     *
+     * @param  wld  suffix of the auxiliary file.
+     * @return the "World file" content as an affine transform.
+     * @throws IOException if an I/O error occurred.
+     * @throws DataStoreException if the file content can not be parsed.
+     */
+    private AffineTransform2D readWorldFile(final String wld) throws 
IOException, DataStoreException {
+        final AuxiliaryContent content = readAuxiliaryFile(wld, encoding);
+        final CharSequence[] lines = 
CharSequences.splitOnEOL(readAuxiliaryFile(wld, encoding));
+        int count = 0;
+        final int expected = 6;                     // Expected number of 
elements.
+        final double[] m = new double[expected];
+        for (int i=0; i<expected; i++) {
+            final String line = lines[i].toString().trim();
+            if (!line.isEmpty() && line.charAt(0) != '#') {
+                if (count >= expected) {
+                    throw new 
DataStoreContentException(errors().getString(Errors.Keys.TooManyOccurrences_2, 
expected, "coefficient"));
+                }
+                try {
+                    m[count++] = Double.parseDouble(line);
+                } catch (NumberFormatException e) {
+                    throw new 
DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2, 
content.getFilename(), i), e);
+                }
+            }
+        }
+        if (count != expected) {
+            throw new 
EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, 
content.getFilename()));
+        }
+        // TODO: provide a more direct way.
+        return new AffineTransform2D(new java.awt.geom.AffineTransform(m));
+    }
+
+    /**
+     * Returns the localized resources for producing error messages.
+     */
+    private Errors errors() {
+        return Errors.getResources(listeners.getLocale());
+    }
+
+    /**
+     * Gets the grid geometry for image at the given index.
+     * This method should be invoked only once per image, and the result 
cached.
+     *
+     * @param  index  index of the image for which to read the grid geometry.
+     * @return grid geometry of the image at the given index.
+     * @throws IndexOutOfBoundsException if the image index is out of bounds.
+     * @throws IOException if an I/O error occurred.
+     * @throws DataStoreException if the {@code *.prj} or {@code *.tfw} 
auxiliary file content can not be parsed.
+     */
+    private GridGeometry getGridGeometry(final int index) throws IOException, 
DataStoreException {
+        assert Thread.holdsLock(this);
+        final ImageReader reader = reader();
+        if (gridGeometry == null) {
+            final AffineTransform2D gridToCRS;
+            width     = reader.getWidth (MAIN_IMAGE);
+            height    = reader.getHeight(MAIN_IMAGE);
+            gridToCRS = readWorldFile();
+            readPRJ();
+            gridGeometry = new GridGeometry(new GridExtent(width, height), 
PixelInCell.CELL_CENTER, gridToCRS, crs);
+        }
+        if (index != MAIN_IMAGE) {
+            final int w = reader.getWidth (index);
+            final int h = reader.getHeight(index);
+            if (w != width || h != height) {
+                return new GridGeometry(new GridExtent(w, h), 
PixelInCell.CELL_CENTER, null, null);
+            }
+        }
+        return gridGeometry;
+    }
+
+    /**
+     * Returns information about the data store as a whole.
+     */
+    @Override
+    public synchronized Metadata getMetadata() throws DataStoreException {
+        if (metadata == null) try {
+            final MetadataBuilder builder = new MetadataBuilder();
+            final String format = reader().getFormatName();
+            try {
+                builder.setFormat(format);
+            } catch (MetadataStoreException e) {
+                builder.addFormatName(format);
+                listeners.warning(Level.FINE, null, e);
+            }
+            builder.addResourceScope(ScopeCode.COVERAGE, null);
+            builder.addSpatialRepresentation(null, 
getGridGeometry(MAIN_IMAGE), true);
+            addTitleOrIdentifier(builder);
+            builder.setISOStandards(false);
+            metadata = builder.buildAndFreeze();
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        return metadata;
+    }
+
+    /**
+     * Returns all images in this store. Note that fetching the size of the 
list is a potentially costly operation.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public final synchronized Collection<? extends Resource> components() 
throws DataStoreException {
+        if (components == null) try {
+            components = new Components();
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        return components;
+    }
+
+    /**
+     * A list of images where each {@link Image} instance is initialized when 
first needed.
+     * Fetching the list size may be a costly operation and will be done only 
if requested.
+     */
+    private final class Components extends ListOfUnknownSize<Image> {
+        /**
+         * Size of this list, or -1 if unknown.
+         */
+        private int size;
+
+        /**
+         * All elements in this list. Some array element may be {@code null} 
if the image
+         * as never been requested.
+         */
+        private Image[] images;
+
+        /**
+         * Creates a new list of images.
+         */
+        private Components() throws DataStoreException, IOException {
+            size = reader().getNumImages(false);
+            images = new Image[size >= 0 ? size : 1];
+        }
+
+        /**
+         * Returns the number of images in this list.
+         * This method may be costly when invoked for the first time.
+         */
+        @Override
+        public int size() {
+            synchronized (Store.this) {
+                if (size < 0) try {
+                    size   = reader().getNumImages(true);
+                    images = ArraysExt.resize(images, size);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                } catch (DataStoreException e) {
+                    throw new BackingStoreException(e);
+                }
+                return size;
+            }
+        }
+
+        /**
+         * Returns the number of images if this information is known, or -1 
otherwise.
+         * This is used by {@link ListOfUnknownSize} for optimizing some 
operations.
+         */
+        @Override
+        protected int sizeIfKnown() {
+            synchronized (Store.this) {
+                return size;
+            }
+        }
+
+        /**
+         * Returns {@code true} if an element exists at the given index.
+         * Current implementations is not more efficient than {@link 
#get(int)}.
+         */
+        @Override
+        protected boolean exists(final int index) {
+            synchronized (Store.this) {
+                if (size >= 0) {
+                    return index >= 0 && index < size;
+                }
+                return get(index) != null;
+            }
+        }
+
+        /**
+         * Returns the image at the given index. New instances are created 
when first requested.
+         */
+        @Override
+        public Image get(final int index) {
+            synchronized (Store.this) {
+                Image image = null;
+                if (index < images.length) {
+                    image = images[index];
+                }
+                if (image == null) try {
+                    image = new Image(Store.this, listeners, index, 
getGridGeometry(index));
+                    if (index >= images.length) {
+                        images = Arrays.copyOf(images, Math.max(images.length 
* 2, index + 1));
+                    }
+                    images[index] = image;
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                } catch (DataStoreException e) {
+                    throw new BackingStoreException(e);
+                }
+                return image;
+            }
+        }
+    }
+
+    /**
+     * Returns the reader if it has not been closed.
+     */
+    final ImageReader reader() throws DataStoreException {
+        final ImageReader in = reader;
+        if (in == null) {
+            throw new DataStoreClosedException(getLocale(), 
StoreProvider.NAME, StandardOpenOption.READ);
+        }
+        return in;
+    }
+
+    /**
+     * Closes this data store and releases any underlying resources.
+     *
+     * @throws DataStoreException if an error occurred while closing this data 
store.
+     */
+    @Override
+    public synchronized void close() throws DataStoreException {
+        final ImageReader r = reader;
+        reader = null;
+        if (r != null) try {
+            final Object input = r.getInput();
+            r.setInput(null);
+            r.dispose();
+            if (input instanceof AutoCloseable) {
+                ((AutoCloseable) input).close();
+            }
+        } catch (Exception e) {
+            throw new DataStoreException(e);
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
new file mode 100644
index 0000000000..795e6fe4bb
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
@@ -0,0 +1,81 @@
+/*
+ * 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.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.PRJDataStore;
+import org.apache.sis.storage.ProbeResult;
+
+
+/**
+ * The provider of {@link Store} instances.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+@StoreMetadata(formatName   = StoreProvider.NAME,
+               capabilities = Capability.READ)
+public final class StoreProvider extends PRJDataStore.Provider {
+    /**
+     * The format name.
+     */
+    static final String NAME = "World file";
+
+    /**
+     * Creates a new provider.
+     */
+    public StoreProvider() {
+    }
+
+    /**
+     * Returns a generic name for this data store, used mostly in warnings or 
error messages.
+     *
+     * @return a short name or abbreviation for the data format.
+     */
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    /**
+     * Returns a {@link Store} implementation associated with this provider.
+     *
+     * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @return a data store implementation associated with this provider for 
the given storage.
+     * @throws DataStoreException if an error occurred while creating the data 
store instance.
+     */
+    @Override
+    public DataStore open(final StorageConnector connector) throws 
DataStoreException {
+        try {
+            return new Store(this, connector);
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws 
DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
new file mode 100644
index 0000000000..fd0e389270
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
@@ -0,0 +1,56 @@
+/*
+ * 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 javax.imageio.ImageReader;
+import javax.imageio.event.IIOReadWarningListener;
+import org.apache.sis.storage.event.StoreListeners;
+
+
+/**
+ * A listener for warnings emitted during read or write operations.
+ * This class forwards the warnings to the listeners associated to the data 
store.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class WarningListener implements IIOReadWarningListener {
+    /**
+     * The set of registered {@link StoreListener}s for the data store.
+     */
+    private final StoreListeners listeners;
+
+    /**
+     * Creates a new image I/O listener.
+     */
+    WarningListener(final StoreListeners listeners) {
+        this.listeners = listeners;
+    }
+
+    /**
+     * Reports a non-fatal error in decoding.
+     *
+     * @param source   the reader calling this method.
+     * @param message  the warning.
+     */
+    @Override
+    public void warningOccurred(final ImageReader reader, final String 
message) {
+        listeners.warning(message);
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
new file mode 100644
index 0000000000..2912efae95
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+/**
+ * {@link org.apache.sis.storage.DataStore} implementation for Image I/O.
+ * This data store wraps Image I/O reader and wrapper for image format such as 
TIFF, PNG or JPEG.
+ * The data store delegates the reading and writing of pixel values to the 
wrapped reader or writer,
+ * and additionally looks for two small text files in the same directory than 
the image file
+ * with the same filename but a different extension:
+ *
+ * <ul class="verbose">
+ *   <li>A text file containing the coefficients of the affine transform 
mapping pixel
+ *       coordinates to geodesic coordinates. The reader expects one 
coefficient per line,
+ *       in the same order than the one expected by the
+ *       {@link java.awt.geom.AffineTransform#AffineTransform(double[]) 
AffineTransform(double[])}
+ *       constructor, which is <var>scaleX</var>, <var>shearY</var>, 
<var>shearX</var>,
+ *       <var>scaleY</var>, <var>translateX</var>, <var>translateY</var>.
+ *       The reader looks for a file having the following extensions, in 
preference order:
+ *       <ol>
+ *         <li>The first letter of the image file extension, followed by the 
last letter of
+ *             the image file extension, followed by {@code 'w'}. Example: 
{@code "tfw"} for
+ *             {@code "tiff"} images, and {@code "jgw"} for {@code "jpeg"} 
images.</li>
+ *         <li>The extension of the image file with a {@code 'w'} 
appended.</li>
+ *         <li>The {@code "wld"} extension.</li>
+ *       </ol>
+ *   </li>
+ *   <li>A text file containing the <cite>Coordinate Reference System</cite> 
(CRS)
+ *       definition in <cite>Well Known Text</cite> (WKT) syntax. The reader 
looks
+ *       for a file having the {@code ".prj"} extension.</li>
+ * </ul>
+ *
+ * Every text file are expected to be encoded in ISO-8859-1 (a.k.a. 
ISO-LATIN-1)
+ * and every numbers are expected to be formatted in US locale.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/World_file";>World File Format 
Description</a>
+ *
+ * @since 1.2
+ * @module
+ */
+package org.apache.sis.internal.storage.image;

Reply via email to