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 2021/07/29 13:07:18 UTC
[sis] 03/07: Put in place the abstract classes needed for
uncompression. Support uncompressed image of bytes for testing and for the
case with subsampling along X axis (in which case we can not use
`HyperRectangleReader`).
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 a061e01940ae3660a6110e0fbac19adc8f7c4411
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jul 23 19:28:33 2021 +0200
Put in place the abstract classes needed for uncompression.
Support uncompressed image of bytes for testing and for the case with subsampling along X axis (in which case we can not use `HyperRectangleReader`).
---
.../sis/internal/coverage/j2d/RasterFactory.java | 25 ++++
.../org/apache/sis/internal/geotiff/Inflater.java | 98 +++++++++++++
.../apache/sis/internal/geotiff/Uncompressed.java | 151 +++++++++++++++++++++
.../apache/sis/internal/geotiff/package-info.java | 6 +-
.../sis/storage/geotiff/CompressedSubset.java | 117 ++++++++++++++++
.../org/apache/sis/storage/geotiff/DataCube.java | 18 ++-
.../org/apache/sis/storage/geotiff/DataSubset.java | 119 ++++++++++------
.../sis/internal/storage/TiledGridResource.java | 10 ++
.../sis/internal/storage/io/ChannelData.java | 18 ++-
.../sis/test/storage/CoverageReadConsistency.java | 37 ++++-
10 files changed, 550 insertions(+), 49 deletions(-)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index e350521..0afb1a4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -33,6 +33,11 @@ import java.awt.image.RasterFormatException;
import java.awt.image.WritableRaster;
import java.awt.image.BufferedImage;
import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.IntBuffer;
+import java.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
import java.nio.ReadOnlyBufferException;
import org.apache.sis.image.DataType;
import org.apache.sis.internal.feature.Resources;
@@ -200,6 +205,26 @@ public final class RasterFactory extends Static {
}
/**
+ * Creates a NIO buffer of the specified capacity.
+ * The buffer position will be 0 and its limit will be its capacity.
+ *
+ * @param dataType type of buffer to create.
+ * @param capacity the {@code Buffer} size.
+ * @return buffer of the specified type and size.
+ */
+ public static Buffer createBuffer(final DataType dataType, final int capacity) {
+ switch (dataType) {
+ case USHORT: // Fallthrough
+ case SHORT: return ShortBuffer .allocate(capacity);
+ case BYTE: return ByteBuffer .allocate(capacity);
+ case INT: return IntBuffer .allocate(capacity);
+ case FLOAT: return FloatBuffer .allocate(capacity);
+ case DOUBLE: return DoubleBuffer.allocate(capacity);
+ default: throw new AssertionError(dataType);
+ }
+ }
+
+ /**
* Wraps the backing arrays of given NIO buffers into Java2D buffers.
* This method wraps the underlying array of primitive types; data are not copied.
* For each buffer, the data starts at {@linkplain Buffer#position() buffer position}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
new file mode 100644
index 0000000..e5fa40c
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
@@ -0,0 +1,98 @@
+/*
+ * 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.geotiff;
+
+import java.io.IOException;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+
+import static org.apache.sis.util.ArgumentChecks.ensureStrictlyPositive;
+import static org.apache.sis.util.ArgumentChecks.ensurePositive;
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+
+/**
+ * Decompression algorithm.
+ * Decompression is applied row-by-row.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public abstract class Inflater {
+ /**
+ * The source of data to decompress.
+ */
+ protected final ChannelDataInput input;
+
+ /**
+ * Number of chunk per row, as a strictly positive integer.
+ * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+ */
+ protected final int chunksPerRow;
+
+ /**
+ * Number of sample values per chunk, as a strictly positive integer.
+ * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+ */
+ protected final int samplesPerChunk;
+
+ /**
+ * Number of sample values to skip between pixels. Positive but often zero.
+ */
+ protected final int interpixels;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param input the source of data to decompress.
+ * @param pixelsPerRow number of pixels per row. Must be strictly positive.
+ * @param samplesPerPixel number of sample values per pixel. Must be strictly positive.
+ * @param interpixels number of sample values to skip between pixels. May be zero.
+ */
+ protected Inflater(final ChannelDataInput input, final int pixelsPerRow, final int samplesPerPixel, final int interpixels) {
+ ensureStrictlyPositive("pixelsPerRow", pixelsPerRow);
+ ensureStrictlyPositive("samplesPerPixel", samplesPerPixel);
+ ensurePositive ("interpixels", interpixels);
+ ensureNonNull ("input", input);
+ if (interpixels == 0) {
+ chunksPerRow = 1;
+ samplesPerChunk = Math.multiplyExact(samplesPerPixel, pixelsPerRow);
+ } else {
+ chunksPerRow = pixelsPerRow;
+ samplesPerChunk = samplesPerPixel;
+ }
+ this.interpixels = interpixels;
+ this.input = input;
+ }
+
+ /**
+ * Reads a row of sample values and stores them in the target buffer.
+ *
+ * @throws IOException if an error occurred while reading the input channel.
+ */
+ public abstract void uncompressRow() throws IOException;
+
+ /**
+ * Reads the given amount of sample values without storing them.
+ * The given value is in units of sample values, not in bytes.
+ *
+ * @param n number of uncompressed sample values to ignore.
+ * @throws IOException if an error occurred while reading the input channel.
+ */
+ public abstract void skip(long n) throws IOException;
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
new file mode 100644
index 0000000..3cb3ce5
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
@@ -0,0 +1,151 @@
+/*
+ * 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.geotiff;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Classes;
+
+
+/**
+ * A pseudo-inflater which copy values unchanged.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public abstract class Uncompressed extends Inflater {
+ /**
+ * Stream position where to perform the next reading.
+ */
+ private long streamPosition;
+
+ /**
+ * Whether {@link #streamPosition} needs to be refreshed by
+ * a call to {@link ChannelDataInput#getStreamPosition()}.
+ */
+ private boolean positionNeedsRefresh;
+
+ /**
+ * Number of bytes in a sample value.
+ */
+ private final int sampleSize;
+
+ /**
+ * For constructors in inner classes.
+ */
+ private Uncompressed(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, final int sampleSize) {
+ super(input, pixelsPerRow, samplesPerPixel, interpixels);
+ this.streamPosition = start;
+ this.sampleSize = sampleSize;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param input the source of data to decompress.
+ * @param start stream position where to start reading.
+ * @param pixelsPerRow number of pixels per row. Must be strictly positive.
+ * @param samplesPerPixel number of sample values per pixel. Must be strictly positive.
+ * @param interpixels number of sample values to skip between pixels. May be zero.
+ * @param target where to store sample values.
+ * @return the inflater for the given targe type.
+ * @throws IllegalArgumentException if the buffer type is not recognized.
+ */
+ public static Uncompressed create(final ChannelDataInput input, final long start,
+ final int pixelsPerRow, final int samplesPerPixel, final int interpixels, final Buffer target)
+ {
+ if (target instanceof ByteBuffer) return new Bytes(input, start, pixelsPerRow, samplesPerPixel, interpixels, (ByteBuffer) target);
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(target)));
+ }
+
+ /**
+ * Reads a row of sample values and stores them in the target buffer.
+ * Subclasses must override and invoke {@code super.uncompress()} before to do the actual reading.
+ */
+ @Override
+ public void uncompressRow() throws IOException {
+ if (!positionNeedsRefresh) {
+ positionNeedsRefresh = true;
+ input.seek(streamPosition);
+ }
+ }
+
+ /**
+ * Skips the given amount of sample values without storing them.
+ * The given value is in units of sample values, not in bytes.
+ *
+ * @param n number of uncompressed sample values to ignore.
+ * @throws IOException if an error occurred while reading the input channel.
+ */
+ @Override
+ public final void skip(final long n) throws IOException {
+ if (n != 0) {
+ if (positionNeedsRefresh) {
+ positionNeedsRefresh = false;
+ streamPosition = input.getStreamPosition();
+ }
+ streamPosition = Math.addExact(streamPosition, n * sampleSize);
+ }
+ }
+
+ /**
+ * Inflater when the values to read and store are bytes.
+ */
+ private static final class Bytes extends Uncompressed {
+ /** Where to copy the values that we will read. */
+ private final ByteBuffer target;
+
+ /** Creates a new inflater which will write in the given buffer. */
+ Bytes(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, ByteBuffer target) {
+ super(input, start, pixelsPerRow, samplesPerPixel, interpixels, Byte.BYTES);
+ this.target = target;
+ }
+
+ /**
+ * Reads and decompress a row of sample values.
+ */
+ @Override public void uncompressRow() throws IOException {
+ super.uncompressRow();
+ for (int i = chunksPerRow; --i > 0;) { // (chunksPerRow - 1) iterations.
+ int n = samplesPerChunk;
+ do target.put(input.readByte());
+ while (--n != 0);
+ /*
+ * Following loop is executed only if there is subsampling on the X axis.
+ * We invoke `readByte()` in a loop instead of invoking `skip` because if
+ * the number of bytes to skip is small, this is more efficient.
+ */
+ for (n = interpixels; --n >= 0;) {
+ input.readByte();
+ }
+ }
+ /*
+ * Read the last element that was not read in first above `for` loop, but without
+ * skipping `interpixels` values after it. This is necessary for avoiding EOF if
+ * the last pixel to read is in the last column of the tile.
+ */
+ int n = samplesPerChunk;
+ do target.put(input.readByte());
+ while (--n != 0);
+ }
+ }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
index 65e8c85..a802b5e 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
@@ -23,8 +23,12 @@
* This package is for internal use by SIS only. Classes in this package
* may change in incompatible ways in any future version without notice.
*
+ * <p>Current version contains the classes for decompressing sample values.
+ * Those classes may move in another package in a future version if we want
+ * to share them with other image formats.</p>
+ *
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
* @since 0.8
* @module
*/
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
new file mode 100644
index 0000000..bf1b2b2
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
@@ -0,0 +1,117 @@
+/*
+ * 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.storage.geotiff;
+
+import java.awt.Point;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.nio.Buffer;
+import org.apache.sis.internal.storage.TiledGridResource;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.geotiff.Uncompressed;
+import org.apache.sis.internal.geotiff.Inflater;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.image.DataType;
+
+import static java.lang.Math.toIntExact;
+import static org.apache.sis.internal.jdk9.JDK9.multiplyFull;
+
+
+/**
+ * Raster data obtained from a compressed GeoTIFF file in the domain requested by user.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class CompressedSubset extends DataSubset {
+ /**
+ * Number of sample values to skip for moving to the next row of a tile.
+ */
+ private final long scanlineStride;
+
+ /**
+ * Creates a new data subset. All parameters should have been validated
+ * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
+ * This constructor should be invoked inside a synchronized block.
+ *
+ * @param source the resource which contain this {@code DataSubset}.
+ * @param subset description of the {@code owner} subset to cover.
+ * @param rasters potentially shared cache of rasters read by this {@code DataSubset}.
+ * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
+ */
+ CompressedSubset(final DataCube source, final TiledGridResource.Subset subset) throws DataStoreException {
+ super(source, subset);
+ scanlineStride = multiplyFull(getTileSize(0), numInterleaved);
+ }
+
+ /**
+ * Computes the number of pixels to read in dimension <var>i</var>
+ * The arguments given to this method are the ones given to the {@code readSlice(…)} method.
+ */
+ private static int pixelCount(final long[] lower, final long[] upper, final int[] subsampling, final int i) {
+ final int n = toIntExact((upper[i] - lower[i] - 1) / subsampling[i] + 1);
+ assert (n > 0) : n;
+ return n;
+ }
+
+ /**
+ * Reads a two-dimensional slice of the data cube from the given input channel.
+ *
+ * @param offsets position in the channel where tile data begins, one value per bank.
+ * @param byteCounts number of bytes for the compressed tile data, one value per bank.
+ * @param lower (<var>x</var>, <var>y</var>) coordinates of the first pixel to read relative to the tile.
+ * @param upper (<var>x</var>, <var>y</var>) coordinates after the last pixel to read relative to the tile.
+ * @param subsampling (<var>sx</var>, <var>sy</var>) subsampling factors.
+ * @param location pixel coordinates in the upper-left corner of the tile to return.
+ * @return image decoded from the GeoTIFF file.
+ */
+ @Override
+ WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
+ final int[] subsampling, final Point location) throws IOException, DataStoreException
+ {
+ final DataType type = getDataType();
+ final int width = pixelCount(lower, upper, subsampling, 0);
+ final int height = pixelCount(lower, upper, subsampling, 1);
+ final int skipY = subsampling[1] - 1;
+ final int skipX = numInterleaved * (subsampling[0] - 1);
+ final long head = numInterleaved * lower[0];
+ final long tail = numInterleaved * (getTileSize(0) - (width*subsampling[0] + lower[0])) + skipX;
+ final Buffer[] banks = new Buffer[numBanks];
+ for (int b=0; b<numBanks; b++) {
+ final Buffer bank = RasterFactory.createBuffer(type, capacity);
+ final Inflater algo = Uncompressed.create(reader().input, offsets[b], width, numInterleaved, skipX, bank);
+ for (long y = lower[1]; --y >= 0;) {
+ algo.skip(scanlineStride);
+ }
+ for (int y = height; --y > 0;) { // (height - 1) iterations.
+ algo.skip(head);
+ algo.uncompressRow();
+ algo.skip(tail);
+ for (int j=skipY; --j>=0;) {
+ algo.skip(scanlineStride);
+ }
+ }
+ algo.skip(head); // Last iteration without last `skip(…)` calls.
+ algo.uncompressRow();
+ fillRemainingRows(bank.flip());
+ banks[b] = bank;
+ }
+ return WritableRaster.createWritableRaster(model, RasterFactory.wrap(type, banks), location);
+ }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index 1cbc0c1..0c2794d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.geotiff;
import java.nio.file.Path;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.coverage.grid.GridCoverage;
@@ -121,6 +122,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
abstract Compression getCompression();
/**
+ * Returns {@code true} if the sample model interleaves 2 or more sample values per pixel.
+ */
+ private boolean isInterleaved() throws DataStoreException {
+ final SampleModel model = getSampleModel();
+ return model.getNumBands() != 1 && !(model instanceof BandedSampleModel);
+ }
+
+ /**
* Creates a {@link GridCoverage} which will load pixel data in the given domain.
*
* @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
@@ -141,7 +150,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
Resources.Keys.MissingValue_2, Tags.name(Tags.Compression)));
}
switch (compression) {
- case NONE: coverage = new DataSubset(this, subset); break;
+ case NONE: {
+ if (subset.hasSubsampling(0) && isInterleaved()) {
+ coverage = new CompressedSubset(this, subset);
+ } else {
+ coverage = new DataSubset(this, subset);
+ }
+ break;
+ }
default: {
throw new DataStoreContentException(reader.resources().getString(
Resources.Keys.UnsupportedCompressionMethod_1, compression));
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index c23fc34..920ecb7 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -27,7 +27,6 @@ import java.awt.image.WritableRaster;
import org.apache.sis.image.DataType;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.InternalDataStoreException;
import org.apache.sis.internal.storage.io.Region;
import org.apache.sis.internal.storage.io.HyperRectangleReader;
import org.apache.sis.internal.storage.TiledGridCoverage;
@@ -101,6 +100,25 @@ class DataSubset extends TiledGridCoverage implements Localized {
private final int numTiles;
/**
+ * Number of banks in the image data buffer.
+ * This is equal to the number of bands only for planar images, and 1 in all other cases.
+ */
+ protected final int numBanks;
+
+ /**
+ * Number of interleaved sample values in a pixel. For planar images, this is equal to 1.
+ * For interleaved sample model, this is equal to the number of bands. This value is often
+ * equal to the {@linkplain java.awt.image.ComponentSampleModel#getPixelStride() pixel stride}.
+ */
+ protected final int numInterleaved;
+
+ /**
+ * Number of sample values in a bank (not necessarily a band).
+ * This is tile width × height × {@link #numInterleaved}.
+ */
+ protected final int capacity;
+
+ /**
* Creates a new data subset. All parameters should have been validated
* by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
* This constructor should be invoked inside a synchronized block.
@@ -117,6 +135,23 @@ class DataSubset extends TiledGridCoverage implements Localized {
final Vector[] tileArrayInfo = source.getTileArrayInfo();
this.tileOffsets = tileArrayInfo[0];
this.tileByteCounts = tileArrayInfo[1];
+ /*
+ * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
+ * Otherwise there is only one bank no matter the amount of bands. Each bank will be read separately.
+ */
+ if (model instanceof BandedSampleModel) {
+ numBanks = model.getNumBands();
+ numInterleaved = 1;
+ } else {
+ numBanks = 1;
+ numInterleaved = model.getNumBands();
+ }
+ final int n = tileOffsets.size();
+ if (numBanks > n / numTiles) {
+ throw new DataStoreContentException(source.reader.errors().getString(
+ Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles, n));
+ }
+ capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
}
/**
@@ -136,6 +171,20 @@ class DataSubset extends TiledGridCoverage implements Localized {
}
/**
+ * Returns the type of data in all tiles.
+ */
+ protected final DataType getDataType() {
+ return DataType.forDataBufferType(model.getDataType());
+ }
+
+ /**
+ * Returns the GeoTIFF reader which contains this subset.
+ */
+ final Reader reader() {
+ return source.reader;
+ }
+
+ /**
* Information about a tile to be read. A list of {@code Tile} is created and sorted by increasing offsets
* before the read operation begins, in order to read tiles in the order they are written in the TIFF file.
*/
@@ -218,7 +267,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
final WritableRaster[] result = new WritableRaster[iterator.tileCountInQuery];
final Tile[] missings = new Tile[iterator.tileCountInQuery];
int numMissings = 0;
- synchronized (source.reader.store) {
+ synchronized (reader().store) {
do {
final WritableRaster tile = iterator.getCachedTile();
if (tile != null) {
@@ -250,6 +299,9 @@ class DataSubset extends TiledGridCoverage implements Localized {
tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL);
tile.copyTileInfo(tileOffsets, offsets, numTiles);
tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
+ for (int b=0; b<offsets.length; b++) {
+ offsets[b] = addExact(offsets[b], reader().origin);
+ }
result[tile.indexInResultArray] = tile.cache(
readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
}
@@ -283,70 +335,61 @@ class DataSubset extends TiledGridCoverage implements Localized {
WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
final int[] subsampling, final Point location) throws IOException, DataStoreException
{
- final DataType type = DataType.forDataBufferType(model.getDataType());
+ final DataType type = getDataType();
final long width = subtractExact(upper[0], lower[0]);
final long height = subtractExact(upper[1], lower[1]);
/*
- * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
- * Otherwise there is only one bank not matter the amount of bands. Each bank is read separately.
- */
- int numBanks = 1;
- int numInterleaved = model.getNumBands();
- if (model instanceof BandedSampleModel) {
- numBanks = numInterleaved;
- numInterleaved = 1;
- }
- if (numBanks > offsets.length) {
- throw new DataStoreContentException(source.reader.errors().getString(
- Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles, tileOffsets.size()));
- }
- /*
* The number of bytes to read should not be greater than `byteCount`. It may be smaller however if only
* a subregion is read. Note that the `length` value may be different than `capacity` if the tile to read
* is smaller than the "standard" tile size of the image. It happens often when reading the last strip.
*/
final long length = multiplyExact(type.size() / Byte.SIZE,
multiplyExact(multiplyExact(width, height), numInterleaved));
- final int capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
final long[] size = new long[] {multiplyFull(numInterleaved, getTileSize(0)), getTileSize(1)};
/*
* If we use an interleaved sample model, each "element" from `HyperRectangleReader` perspective is actually
* a group of `numInterleaved` values. Note that in such case, we can not handle subsampling on the first axis.
+ * Such case should be handled by the `CompressedSubset` subclass instead, even if there is no compression.
*/
- if (numInterleaved != 1 && subsampling[0] != 1) {
- throw new InternalDataStoreException();
- }
+ assert numInterleaved == 1 || subsampling[0] == 1;
lower[0] *= numInterleaved;
upper[0] *= numInterleaved;
/*
* Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
* in the interleaved sample model case.
*/
- final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()), source.reader.input);
+ final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()), reader().input);
final Region region = new Region(size, lower, upper, subsampling);
final Buffer[] banks = new Buffer[numBanks];
for (int b=0; b<numBanks; b++) {
if (b < byteCounts.length && length > byteCounts[b]) {
- throw new DataStoreContentException(source.reader.resources().getString(
+ throw new DataStoreContentException(reader().resources().getString(
Resources.Keys.UnexpectedTileLength_2, length, byteCounts[b]));
}
- hr.setOrigin(addExact(source.reader.origin, offsets[b]));
- final Buffer buffer = hr.readAsBuffer(region, capacity);
- /*
- * The buffer returned by `readAsBuffer(…)` may have less data than the buffer capacity
- * if the current tile is smaller than the expected tile size (e.g. truncated last tile).
- * Following code applies the fill value if it is different than the default value (zero).
- */
- if (fillValue != null) {
- final int end = buffer.limit();
- if (end != capacity) {
- Vector.create(buffer.limit(capacity), ImageUtilities.isUnsignedType(model))
- .fill(end, capacity, fillValue);
- }
- }
- banks[b] = buffer.limit(capacity);
+ hr.setOrigin(offsets[b]);
+ final Buffer bank = hr.readAsBuffer(region, capacity);
+ fillRemainingRows(bank);
+ banks[b] = bank;
}
final DataBuffer buffer = RasterFactory.wrap(type, banks);
return WritableRaster.createWritableRaster(model, buffer, location);
}
+
+ /**
+ * Applies the fill value if it is different than the default value (zero) to all remaining rows.
+ * This method is needed because the buffer filled by read methods may have less data than the buffer
+ * capacity if the current tile is smaller than the expected tile size (e.g. last tile is truncated).
+ *
+ * @param bank the buffer where to fill remaining rows.
+ */
+ final void fillRemainingRows(final Buffer bank) {
+ if (fillValue != null) {
+ final int end = bank.limit();
+ if (end != capacity) {
+ Vector.create(bank.limit(capacity), ImageUtilities.isUnsignedType(model))
+ .fill(end, capacity, fillValue);
+ bank.limit(capacity);
+ }
+ }
+ }
}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index fd0c6ee..8a13cbd 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -195,5 +195,15 @@ public abstract class TiledGridResource extends AbstractGridResource {
*/
cache = sharedCache ? caller.rasters : new WeakValueHashMap<>(Integer.class);
}
+
+ /**
+ * Returns {@code true} if this subset contains a subsampling in the given dimension.
+ *
+ * @param dimension the dimension to test.
+ * @return {@code true} if there is a subsampling in the given dimension.
+ */
+ public boolean hasSubsampling(final int dimension) {
+ return subsampling[dimension] != 1;
+ }
}
}
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 49e5195..0a3059b 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 0.5
+ * @version 1.1
* @since 0.5 (derived from 0.3)
* @module
*/
@@ -135,7 +135,7 @@ public abstract class ChannelData implements Markable {
* @return the bit offset of the stream.
*/
public final int getBitOffset() {
- final long position = bufferOffset + buffer.position();
+ final long position = position();
if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
bitPosition = position << BIT_OFFSET_SIZE;
}
@@ -149,8 +149,7 @@ public abstract class ChannelData implements Markable {
*/
public final void setBitOffset(final int bitOffset) {
ensureBetween("bitOffset", 0, Byte.SIZE - 1, bitOffset);
- final long position = bufferOffset + buffer.position();
- bitPosition = (position << BIT_OFFSET_SIZE) | bitOffset;
+ bitPosition = (position() << BIT_OFFSET_SIZE) | bitOffset;
}
/**
@@ -167,7 +166,14 @@ public abstract class ChannelData implements Markable {
*/
@Override
public long getStreamPosition() {
- return bufferOffset + buffer.position();
+ return position();
+ }
+
+ /**
+ * Returns the current byte position of the stream, ignoring overriding by subclasses.
+ */
+ private long position() {
+ return Math.addExact(bufferOffset, buffer.position());
}
/**
@@ -187,7 +193,7 @@ public abstract class ChannelData implements Markable {
* @param position the new position of the stream.
*/
public final void setStreamPosition(final long position) {
- bufferOffset = position - buffer.position();
+ bufferOffset = Math.subtractExact(position, buffer.position());
// Clearing the bit offset is needed if we don't want to handle the case of ChannelDataOutput,
// which use a different stream position calculation when the bit offset is non-zero.
clearBitOffset();
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 1d4424a..585db35 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -266,9 +266,11 @@ nextSlice: for (;;) {
if (!failOnMismatch) break;
final Point pr = itr.getPosition();
final Point pc = itc.getPosition();
- assertArrayEquals("Mismatch at position (" + pr.x + ", " + pr.y + ") in full image " +
- "and (" + pc.x + ", " + pc.y + ") in tested sub-image",
- expected, actual, STRICT);
+ final StringBuilder message = new StringBuilder(100).append("Mismatch at position (")
+ .append(pr.x).append(", ").append(pr.y).append(") in full image and (")
+ .append(pc.x).append(", ").append(pc.y).append(") in tested sub-image");
+ findMatchPosition(itr, pr, expected, message);
+ assertArrayEquals(message.toString(), expected, actual, STRICT);
}
}
assertFalse(itc.next());
@@ -363,4 +365,33 @@ nextSlice: for (;;) {
}
return new PixelIterator.Builder().setRegionOfInterest(sliceAOI).create(image);
}
+
+ /**
+ * Explores pixel values around the given position in search for a pixel having the expected values.
+ * If a match is found, the error message is completed with information about the match position.
+ */
+ private static void findMatchPosition(final PixelIterator ir, final Point pr, final double[] expected, final StringBuilder message) {
+ double[] actual = null;
+ for (int dy=0; dy<10; dy++) {
+ for (int dx=0; dx<10; dx++) {
+ if ((dx | dy) != 0) {
+ for (int c=0; c<4; c++) {
+ final int x = (c & 1) == 0 ? -dx : dx;
+ final int y = (c & 2) == 0 ? -dy : dy;
+ try {
+ ir.moveTo(pr.x + x, pr.y + y);
+ } catch (IndexOutOfBoundsException e) {
+ continue;
+ }
+ actual = ir.getPixel(actual);
+ if (Arrays.equals(expected, actual)) {
+ message.append(" (note: found a match at offset (").append(x).append(", ").append(y)
+ .append(") in full image)");
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
}