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/08/17 16:05:29 UTC

[sis] branch geoapi-4.0 updated (f057125eea -> 696dfafa87)

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 f057125eea Move aggregation classes to a separated package, in anticipation for more classes to be added.
     new c094a3a015 Optimization which replaces a "resample" operation by a translation can be applied only if the source and target coverages has the same size.
     new 696dfafa87 Allow `ChannelImageInputStream` to wrap an array of `byte[]` containing all data.

The 2 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:
 .../sis/coverage/grid/GridCoverageProcessor.java   |  6 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   | 37 +++++++++--
 .../sis/coverage/grid/ResampledGridCoverage.java   | 36 +++++-----
 .../sis/coverage/grid/TranslatedGridCoverage.java  |  9 ++-
 .../sis/internal/storage/io/ChannelData.java       | 25 ++++++-
 .../sis/internal/storage/io/ChannelDataInput.java  | 15 ++++-
 .../storage/io/ChannelImageInputStream.java        | 14 +++-
 .../sis/internal/storage/io/NullChannel.java       | 77 ++++++++++++++++++++++
 .../sis/internal/storage/io/package-info.java      |  2 +-
 .../org/apache/sis/storage/StorageConnector.java   | 11 +++-
 10 files changed, 200 insertions(+), 32 deletions(-)
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/NullChannel.java


[sis] 01/02: Optimization which replaces a "resample" operation by a translation can be applied only if the source and target coverages has the same size.

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 c094a3a01586f727b832a6f034084e1dc8517f2b
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 17 16:56:32 2022 +0200

    Optimization which replaces a "resample" operation by a translation
    can be applied only if the source and target coverages has the same size.
---
 .../sis/coverage/grid/GridCoverageProcessor.java   |  6 ++--
 .../org/apache/sis/coverage/grid/GridExtent.java   | 37 ++++++++++++++++++----
 .../sis/coverage/grid/ResampledGridCoverage.java   | 36 +++++++++++----------
 .../sis/coverage/grid/TranslatedGridCoverage.java  |  9 +++++-
 4 files changed, 63 insertions(+), 25 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 031942f67f..67a458a3d0 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -164,8 +164,8 @@ public class GridCoverageProcessor implements Cloneable {
      * but may change other aspects (in a compatible way) such as the {@link GridCoverage} subclass
      * returned or the size of the underlying rendered images.
      *
-     * <p>By default all optimizations are enabled. Users may want to disable some optimizations
-     * for example in order to get more predictable results.</p>
+     * <p>By default the {@link #REPLACE_OPERATION} and {@link #REPLACE_SOURCE} optimizations are enabled.
+     * Users may want to disable some optimizations for example in order to get more predictable results.</p>
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.3
@@ -178,6 +178,7 @@ public class GridCoverageProcessor implements Cloneable {
     public enum Optimization {
         /**
          * Allows the replacement of an operation by a more efficient one.
+         * This optimization is enabled by default.
          *
          * <div class="note"><b>Example:</b>
          * if the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with parameter values
@@ -189,6 +190,7 @@ public class GridCoverageProcessor implements Cloneable {
 
         /**
          * Allows the replacement of source parameter by a more fundamental source.
+         * This optimization is enabled by default.
          *
          * <div class="note"><b>Example:</b>
          * if the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with a source
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index e71d284659..af34fe7c5a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -1703,14 +1703,28 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
     }
 
     /**
-     * Returns a hash value for this grid extent. This value needs not to remain
-     * consistent between different implementations of the same class.
+     * Returns whether this grid extent has the same size than the given extent.
+     * If the given extent is {@code null} or has a different number of dimensions,
+     * then this method returns {@code false}.
      *
-     * @return a hash value for this grid extent.
+     * <p>This method is not public because we do not yet have a policy
+     * about whether we should verify if axis {@link #types} match.</p>
+     *
+     * @param  other  the other extent to compare with this extent. Can be {@code null}.
+     * @return whether the two extents has the same size.
      */
-    @Override
-    public int hashCode() {
-        return Arrays.hashCode(coordinates) + Arrays.hashCode(types) ^ (int) serialVersionUID;
+    final boolean isSameSize(final GridExtent other) {
+        if (other == null || coordinates.length != other.coordinates.length) {
+            return false;
+        }
+        final int dimension = getDimension();
+        final long[] oc = other.coordinates;
+        for (int i=0; i<dimension; i++) {
+            if (coordinates[i+dimension] - coordinates[i] != oc[i+dimension] - oc[i]) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -1755,6 +1769,17 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
         return false;
     }
 
+    /**
+     * Returns a hash value for this grid extent. This value needs not to remain
+     * consistent between different implementations of the same class.
+     *
+     * @return a hash value for this grid extent.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(coordinates) + Arrays.hashCode(types) ^ (int) serialVersionUID;
+    }
+
     /**
      * Returns a string representation of this grid extent. The returned string
      * is implementation dependent and is provided for debugging purposes only.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index 92ffd45d8a..e3114ab04a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -184,10 +184,10 @@ final class ResampledGridCoverage extends DerivedGridCoverage {
      * If this coverage can be represented as a {@link GridCoverage2D} instance,
      * returns such instance. Otherwise returns {@code this}.
      *
-     * @param  isGeometryExplicit  whether grid extent or "grid to CRS" transform have been explicitly
-     *         specified by user. In such case, this method will not be allowed to change those values.
+     * @param  allowGeometryReplacement   whether to allow the replacement of grid geometry in the target coverage.
+     * @param  allowOperationReplacement  whether to allow the replacement of this operation by a more efficient one.
      */
-    private GridCoverage specialize(final boolean isGeometryExplicit, final boolean allowOperationReplacement)
+    private GridCoverage specialize(final boolean allowGeometryReplacement, final boolean allowOperationReplacement)
             throws TransformException
     {
         if (allowOperationReplacement) {
@@ -196,7 +196,8 @@ final class ResampledGridCoverage extends DerivedGridCoverage {
                 (translation = getIntegerTranslation(toSourceCorner)) != null)
             {
                 // No need to allow source replacement because it is already done by caller.
-                return TranslatedGridCoverage.create(source, gridGeometry, translation, false);
+                GridCoverage c = TranslatedGridCoverage.create(source, gridGeometry, translation, false);
+                if (c != null) return c;
             }
         }
         GridExtent extent = gridGeometry.getExtent();
@@ -210,7 +211,7 @@ final class ResampledGridCoverage extends DerivedGridCoverage {
          * (i.e. user specified only a target CRS), keep same image with a different `gridToCRS` transform instead
          * than doing a resampling. The intent is to avoid creating a new image if user apparently doesn't care.
          */
-        if (!isGeometryExplicit && toSourceCorner instanceof LinearTransform) {
+        if (allowGeometryReplacement && toSourceCorner instanceof LinearTransform) {
             MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CORNER);
             if (gridToCRS instanceof LinearTransform) {
                 final GridGeometry sourceGG = source.getGridGeometry();
@@ -287,8 +288,10 @@ final class ResampledGridCoverage extends DerivedGridCoverage {
      *     result.</li>
      * </ul>
      *
-     * @param  source  the grid coverage to resample.
-     * @param  target  the desired geometry of returned grid coverage. May be incomplete.
+     * @param  source     the grid coverage to resample.
+     * @param  target     the desired geometry of returned grid coverage. May be incomplete.
+     * @param  processor  the processor to use for executing the resample operation on images.
+     * @param  allowOperationReplacement  whether to allow the replacement of this operation by a more efficient one.
      * @return a grid coverage with the characteristics specified in the given grid geometry.
      * @throws IncompleteGridGeometryException if the source grid geometry is missing an information.
      * @throws TransformException if some coordinates can not be transformed to the specified target.
@@ -452,33 +455,34 @@ final class ResampledGridCoverage extends DerivedGridCoverage {
          * Build the final target GridGeometry if any components were missing.
          * If an envelope is defined, resample only that sub-region.
          */
-        GridGeometry resampled = target;
+        GridGeometry complete = target;
         ComparisonMode mode = ComparisonMode.IGNORE_METADATA;
         if (!target.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS | GridGeometry.CRS)) {
             final CoordinateReferenceSystem targetCRS = changeOfCRS.getTargetCRS();
-            resampled = new GridGeometry(targetExtent, PixelInCell.CELL_CENTER, targetCenterToCRS, targetCRS);
+            complete = new GridGeometry(targetExtent, PixelInCell.CELL_CENTER, targetCenterToCRS, targetCRS);
             mode = ComparisonMode.APPROXIMATE;
             if (target.isDefined(GridGeometry.ENVELOPE)) {
-                final MathTransform targetCornerToCRS = resampled.getGridToCRS(PixelInCell.CELL_CORNER);
-                GeneralEnvelope bounds = new GeneralEnvelope(resampled.getEnvelope());
+                final MathTransform targetCornerToCRS = complete.getGridToCRS(PixelInCell.CELL_CORNER);
+                GeneralEnvelope bounds = new GeneralEnvelope(complete.getEnvelope());
                 bounds.intersect(target.getEnvelope());
                 bounds = Envelopes.transform(targetCornerToCRS.inverse(), bounds);
                 targetExtent = new GridExtent(bounds, GridRoundingMode.NEAREST, GridClippingMode.STRICT, null, null, targetExtent, null);
-                resampled = new GridGeometry(targetExtent, PixelInCell.CELL_CENTER, targetCenterToCRS, targetCRS);
+                complete = new GridGeometry(targetExtent, PixelInCell.CELL_CENTER, targetCenterToCRS, targetCRS);
                 isGeometryExplicit = true;
             }
         }
-        if (sourceGG.equals(resampled, mode)) {
+        if (sourceGG.equals(complete, mode)) {
             return source;
         }
         /*
          * Complete the "target to source" transform.
          */
-        final MathTransform targetCornerToCRS = resampled.getGridToCRS(PixelInCell.CELL_CORNER);
-        return new ResampledGridCoverage(source, resampled,
+        final MathTransform targetCornerToCRS = complete.getGridToCRS(PixelInCell.CELL_CORNER);
+        final ResampledGridCoverage resampled = new ResampledGridCoverage(source, complete,
                 MathTransforms.concatenate(targetCornerToCRS, crsToSourceCorner),
                 MathTransforms.concatenate(targetCenterToCRS, crsToSourceCenter),
-                changeOfCRS, processor).specialize(isGeometryExplicit, allowOperationReplacement);
+                changeOfCRS, processor);
+        return resampled.specialize(!isGeometryExplicit, allowOperationReplacement);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
index 81bb354bbe..c6e2dae659 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
@@ -42,6 +42,7 @@ final class TranslatedGridCoverage extends DerivedGridCoverage {
     /**
      * Constructs a new grid coverage which will delegate the rendering operation to the given source.
      * This coverage will take the same sample dimensions than the source.
+     * The {@code domain} size must be the same than the source grid geometry size.
      *
      * @param  source       the source on which to delegate rendering operations.
      * @param  domain       the grid extent, CRS and conversion from cell indices to CRS.
@@ -56,10 +57,14 @@ final class TranslatedGridCoverage extends DerivedGridCoverage {
      * Returns a grid coverage which will use the {@code domain} grid geometry.
      * This coverage will take the same sample dimensions than the source.
      *
+     * <p>If {@code domain} is non-null, then it should have the same size than the source grid geometry size.
+     * If this is not the case, then this method returns {@code null}.</p>
+     *
      * @param  source       the source on which to delegate rendering operations.
      * @param  domain       the geometry of the grid coverage to return, or {@code null} for automatic.
      * @param  translation  translation to apply on the argument given to {@link #render(GridExtent)}.
-     * @return the coverage. May be the {@code source} returned as-is.
+     * @return the coverage, or {@code null} if {@code domain} is non-null but does not have the expected size.
+     *         May be the {@code source} returned as-is.
      */
     static GridCoverage create(GridCoverage source, GridGeometry domain, long[] translation,
                                final boolean allowSourceReplacement)
@@ -81,6 +86,8 @@ final class TranslatedGridCoverage extends DerivedGridCoverage {
         final GridGeometry gridGeometry = source.getGridGeometry();
         if (domain == null) {
             domain = gridGeometry.translate(translation);
+        } else if (!domain.extent.isSameSize(gridGeometry.extent)) {
+            return null;
         }
         if (domain.equals(gridGeometry)) {
             return source;                  // All (potentially updated) translation terms are zero.


[sis] 02/02: Allow `ChannelImageInputStream` to wrap an array of `byte[]` containing all data.

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 696dfafa870b352593dba3dd6e5449294e799cd5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 17 18:02:41 2022 +0200

    Allow `ChannelImageInputStream` to wrap an array of `byte[]` containing all data.
---
 .../sis/internal/storage/io/ChannelData.java       | 25 ++++++-
 .../sis/internal/storage/io/ChannelDataInput.java  | 15 ++++-
 .../storage/io/ChannelImageInputStream.java        | 14 +++-
 .../sis/internal/storage/io/NullChannel.java       | 77 ++++++++++++++++++++++
 .../sis/internal/storage/io/package-info.java      |  2 +-
 .../org/apache/sis/storage/StorageConnector.java   | 11 +++-
 6 files changed, 137 insertions(+), 7 deletions(-)

diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
index 19b2c50287..e45ca2fcdf 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
@@ -32,7 +32,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureBetween;
  * querying or modifying the stream position. This class does not define any read or write operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -125,6 +125,21 @@ public abstract class ChannelData implements Markable {
         this.channelOffset = (channel instanceof SeekableByteChannel) ? ((SeekableByteChannel) channel).position() : 0;
     }
 
+    /**
+     * Creates a new instance for a buffer filled with the bytes to use.
+     * This constructor uses an independent, read-only view of the given buffer.
+     * No reference to the given buffer will be retained.
+     *
+     * @param  filename  a short identifier (typically a filename without path) used for formatting error message.
+     * @param  data      the buffer filled with all bytes to read.
+     */
+    ChannelData(final String filename, final ByteBuffer data) {
+        this.filename = filename;
+        buffer = data.asReadOnlyBuffer();
+        buffer.order(data.order());
+        channelOffset = 0;
+    }
+
     /**
      * Implementation of {@link ChannelDataInput#readBit()} provided here for performance reasons.
      * It is caller responsibility to ensure that the {@link #buffer} contains at least one byte.
@@ -245,9 +260,10 @@ public abstract class ChannelData implements Markable {
     /**
      * Discards the initial portion of the stream prior to the indicated position.
      * Attempting to {@linkplain #seek(long) seek} to an offset within the flushed
-     * portion of the stream will result in an {@link IndexOutOfBoundsException}.
+     * portion of the stream may result in an {@link IndexOutOfBoundsException}.
      *
-     * <p>This method moves the data starting at the given position to the beginning of the {@link #buffer},
+     * <p>If the {@link #buffer} is read-only, then this method does nothing. Otherwise
+     * this method moves the data starting at the given position to the beginning of the {@link #buffer},
      * thus making more room for new data before the data at the given position is discarded.</p>
      *
      * @param  position  the length of the stream prefix that may be flushed.
@@ -259,6 +275,9 @@ public abstract class ChannelData implements Markable {
             throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.ValueOutOfRange_4,
                     "position", bufferOffset, currentPosition, position));
         }
+        if (buffer.isReadOnly()) {
+            return;
+        }
         final int n = (int) Math.max(position - bufferOffset, 0);
         final int p = buffer.position() - n;
         final int r = buffer.limit() - n;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
index 335667359b..389716e584 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
@@ -60,7 +60,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureBetween;
  * {@link javax.imageio} is needed.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -106,6 +106,19 @@ public class ChannelDataInput extends ChannelData {
         }
     }
 
+    /**
+     * Creates a new instance for a buffer filled with the bytes to use.
+     * This constructor uses an independent, read-only view of the given buffer.
+     * No reference to the given buffer will be retained.
+     *
+     * @param  filename  a short identifier (typically a filename without path) used for formatting error message.
+     * @param  data      the buffer filled with all bytes to read.
+     */
+    public ChannelDataInput(final String filename, final ByteBuffer data) {
+        super(filename, data);
+        channel = new NullChannel();
+    }
+
     /**
      * Returns the length of the stream (in bytes), or -1 if unknown.
      * The length is relative to the channel position at {@linkplain #ChannelDataInput construction time}.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelImageInputStream.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelImageInputStream.java
index 9a8a3c0186..3b875bfe29 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelImageInputStream.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelImageInputStream.java
@@ -42,7 +42,7 @@ import javax.imageio.stream.ImageInputStream;
  * <p>This class is used when compatibility with {@link javax.imageio.ImageReader} is needed.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see javax.imageio.stream.FileImageInputStream
  * @see javax.imageio.ImageIO#createImageInputStream(Object)
@@ -68,6 +68,18 @@ public class ChannelImageInputStream extends ChannelDataInput implements ImageIn
         super(filename, channel, buffer, filled);
     }
 
+    /**
+     * Creates a new instance for a buffer filled with the bytes to use.
+     * This constructor uses an independent, read-only view of the given buffer.
+     * No reference to the given buffer will be retained.
+     *
+     * @param  filename  a short identifier (typically a filename without path) used for formatting error message.
+     * @param  data      the buffer filled with all bytes to read.
+     */
+    public ChannelImageInputStream(final String filename, final ByteBuffer data) {
+        super(filename, data);
+    }
+
     /**
      * Creates a new input stream from the given {@code ChannelDataInput}.
      * This constructor is invoked when we need to change the implementation class
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/NullChannel.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/NullChannel.java
new file mode 100644
index 0000000000..2486e11828
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/NullChannel.java
@@ -0,0 +1,77 @@
+/*
+ * 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.io;
+
+import java.util.Objects;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.ClosedChannelException;
+
+
+/**
+ * A channel which read no values. This class behaves as if the channel already reached the end of file.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+final class NullChannel implements ReadableByteChannel {
+    /**
+     * Whether this channel has been closed.
+     */
+    private volatile boolean closed;
+
+    /**
+     * Creates an initially open channel.
+     */
+    NullChannel() {
+    }
+
+    /**
+     * Pretends to read a sequence of bytes and indicates that the channel reached the end of file.
+     * Read-only buffers are accepted (this is required for {@link ChannelImageInputStream}).
+     *
+     * @param  dst  ignored except for non-null check.
+     * @return always -1.
+     * @throws ClosedChannelException if this channel has been closed.
+     */
+    @Override
+    public int read(ByteBuffer dst) throws ClosedChannelException {
+        Objects.requireNonNull(dst);
+        if (closed) {
+            throw new ClosedChannelException();
+        }
+        return -1;
+    }
+
+    /**
+     * Returns whether this channel is still open.
+     */
+    @Override
+    public boolean isOpen() {
+        return !closed;
+    }
+
+    /**
+     * Closes this channel. If this channel is already closed, then this method does nothing.
+     */
+    @Override
+    public void close() {
+        closed = true;
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/package-info.java
index e2932d1265..de0e699a69 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/package-info.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/package-info.java
@@ -24,7 +24,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
index 2f2fab45f3..5560158acd 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
@@ -99,7 +99,7 @@ import org.apache.sis.setup.OptionKey;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -711,6 +711,10 @@ public class StorageConnector implements Serializable {
      *           (including the {@link ImageInputStream} and {@link javax.imageio.stream.ImageOutputStream} types),
      *           then it is returned unchanged.</li>
      *
+     *       <li>Otherwise if the input is an instance of {@link ByteBuffer}, then an {@link ImageInputStream}
+     *           backed by a read-only view of that buffer is created when first needed and returned.
+     *           The properties (position, mark, limit) of the original buffer are unmodified.</li>
+     *
      *       <li>Otherwise if the input is an instance of {@link java.nio.file.Path}, {@link java.io.File},
      *           {@link java.net.URI}, {@link java.net.URL}, {@link CharSequence}, {@link InputStream} or
      *           {@link java.nio.channels.ReadableByteChannel}, then an {@link ImageInputStream} backed by a
@@ -953,6 +957,11 @@ public class StorageConnector implements Serializable {
         if (storage instanceof InputStream) {
             ((InputStream) storage).mark(DEFAULT_BUFFER_SIZE);
         }
+        if (storage instanceof ByteBuffer) {
+            final ChannelDataInput asDataInput = new ChannelImageInputStream(getStorageName(), (ByteBuffer) storage);
+            addView(ChannelDataInput.class, asDataInput);
+            return asDataInput;
+        }
         /*
          * Following method call recognizes ReadableByteChannel, InputStream (with optimization for FileInputStream),
          * URL, URI, File, Path or other types that may be added in future Apache SIS versions.