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/08/13 18:37:40 UTC
[sis] 01/03: Rename `samplesPerElement` as `pixelsPerElement` (this
is not the same value in `SinglePixelPackedSampleModel` case) and
consolidate the way that this value is computed from the `SampleModel`.
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 aa876ee1ba8eb24c224d6e6c0435b091712be585
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 13 14:52:27 2021 +0200
Rename `samplesPerElement` as `pixelsPerElement` (this is not the same value in `SinglePixelPackedSampleModel` case)
and consolidate the way that this value is computed from the `SampleModel`.
---
.../apache/sis/internal/geotiff/CopyFromBytes.java | 42 ++++----
.../org/apache/sis/internal/geotiff/Inflater.java | 76 ++++++-------
.../sis/storage/geotiff/CompressedSubset.java | 29 ++---
.../org/apache/sis/storage/geotiff/DataCube.java | 4 +-
.../org/apache/sis/storage/geotiff/DataSubset.java | 34 ++----
.../sis/internal/storage/TiledGridCoverage.java | 56 +++++++++-
.../sis/internal/storage/TiledGridResource.java | 119 +++++++++++++--------
7 files changed, 209 insertions(+), 151 deletions(-)
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
index e18d8b6..0bc6e11 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/CopyFromBytes.java
@@ -60,30 +60,36 @@ abstract class CopyFromBytes extends Inflater {
private final int bytesPerElement;
/**
- * Number of sample values per primitive element.
+ * Number of pixels per primitive element.
* Always 1 except for multi-pixels packed images.
+ *
+ * <div class="note"><b>Note:</b>
+ * this is "pixels per element", not "samples per element", because the value of this field shall be 1
+ * in the {@link java.awt.image.SinglePixelPackedSampleModel} case (by contrast a "samples per element"
+ * would have a value greater than 1). But this field can nevertheless be understood as a "samples per
+ * element" value where only one band is considered at a time.</div>
*/
- private final int samplesPerElement;
+ private final int pixelsPerElement;
/**
* For constructors in inner classes.
*
- * @param input the source of data to decompress.
- * @param start position in the input stream of the first byte to read.
- * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
- * @param samplesPerChunk number of sample values per chunk (sample, pixel or row). Must be strictly positive.
- * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
- * @param samplesPerElement number of sample values per primitive element. Always 1 except for multi-pixels packed images.
- * @param bytesPerElement number of bytes in a bank element (a bank element is usually a sample value).
+ * @param input the source of data to decompress.
+ * @param start position in the input stream of the first byte to read.
+ * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
+ * @param samplesPerChunk number of sample values per chunk (sample, pixel or row). Must be strictly positive.
+ * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
+ * @param pixelsPerElement number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+ * @param bytesPerElement number of bytes in a bank element (a bank element is usually a sample value).
*/
private CopyFromBytes(final ChannelDataInput input, final long start, final int chunksPerRow,
final int samplesPerChunk, final int[] skipAfterChunks,
- final int samplesPerElement, final int bytesPerElement)
+ final int pixelsPerElement, final int bytesPerElement)
{
- super(input, chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, input.buffer.capacity() / bytesPerElement);
- this.streamPosition = start;
- this.bytesPerElement = bytesPerElement;
- this.samplesPerElement = samplesPerElement;
+ super(input, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, input.buffer.capacity() / bytesPerElement);
+ this.streamPosition = start;
+ this.bytesPerElement = bytesPerElement;
+ this.pixelsPerElement = pixelsPerElement;
}
/**
@@ -129,7 +135,7 @@ abstract class CopyFromBytes extends Inflater {
* The given value is in units of sample values, not in bytes.
*
* <h4>Case of multi-pixels packed image</h4>
- * It is caller's responsibility to ensure that <var>n</var> is a multiple of {@link #samplesPerElement}
+ * It is caller's responsibility to ensure that <var>n</var> is a multiple of {@link #pixelsPerElement}
* if this method is not invoked for skipping all remaining values until end of row.
* See {@link Inflater#skip(long)} for more information.
*
@@ -145,12 +151,12 @@ abstract class CopyFromBytes extends Inflater {
}
/*
* Convert number of sample values to number of elements, then to number of bytes.
- * The number of sample values to skip shall be a multiple of `samplesPerElement`,
+ * The number of sample values to skip shall be a multiple of `pixelsPerElement`,
* except when skipping all remaining values until end of row. We do not verify
* because this method does not know when the row ends.
*/
- final boolean r = (n % samplesPerElement) > 0;
- n /= samplesPerElement; if (r) n++; // Round after division to next element boundary.
+ final boolean r = (n % pixelsPerElement) > 0;
+ n /= pixelsPerElement; if (r) n++; // Round after division to next element boundary.
n *= bytesPerElement; // Must be multiplied only after above rounding.
streamPosition = Math.addExact(streamPosition, n);
}
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
index b158ed5..2c81c28 100644
--- 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
@@ -84,31 +84,31 @@ public abstract class Inflater {
*
* <h4>Note on sample definition</h4>
* A "sample" is usually a primitive type such as a byte or a float, but may be a unit smaller than a byte
- * (e.g. 1 bit) if {@code samplesPerElement} is greater than 1. In that case, the {@link #elementsPerChunk}
- * and {@link #skipAfterChunks} values will be divided by {@code samplesPerElement}.
+ * (e.g. 1 bit) if {@code pixelsPerElement} is greater than 1. In that case, the {@link #elementsPerChunk}
+ * and {@link #skipAfterChunks} values will be divided by {@code pixelsPerElement}.
*
- * @param input the source of data to decompress.
- * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
- * @param samplesPerChunk number of sample values per chunk (sample or pixel). Must be strictly positive.
- * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
- * @param samplesPerElement number of sample values per primitive element. Always 1 except for multi-pixels packed images.
- * @param maxChunkSize maximal value (in number of samples) for the {@link #elementsPerChunk} field.
+ * @param input the source of data to decompress.
+ * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
+ * @param samplesPerChunk number of sample values per chunk (sample or pixel). Must be strictly positive.
+ * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
+ * @param pixelsPerElement number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+ * @param maxChunkSize maximal value (in number of samples) for the {@link #elementsPerChunk} field.
*/
protected Inflater(final ChannelDataInput input, int chunksPerRow, int samplesPerChunk,
- int[] skipAfterChunks, final int samplesPerElement, final int maxChunkSize)
+ int[] skipAfterChunks, final int pixelsPerElement, final int maxChunkSize)
{
this.input = input;
- ArgumentChecks.ensureStrictlyPositive("chunksPerRow", chunksPerRow);
- ArgumentChecks.ensureStrictlyPositive("samplesPerChunk", samplesPerChunk);
- ArgumentChecks.ensureStrictlyPositive("samplesPerElement", samplesPerElement);
+ ArgumentChecks.ensureStrictlyPositive("chunksPerRow", chunksPerRow);
+ ArgumentChecks.ensureStrictlyPositive("samplesPerChunk", samplesPerChunk);
+ ArgumentChecks.ensureStrictlyPositive("pixelsPerElement", pixelsPerElement);
if (skipAfterChunks != null) {
- if (samplesPerElement != 1) {
+ if (pixelsPerElement != 1) {
skipAfterChunks = skipAfterChunks.clone();
for (int i=0; i<skipAfterChunks.length; i++) {
final int s = skipAfterChunks[i];
ArgumentChecks.ensurePositive("skipAfterChunks", s);
- ArgumentChecks.ensureMultiple("skipAfterChunks", samplesPerElement, s);
- skipAfterChunks[i] /= samplesPerElement;
+ ArgumentChecks.ensureMultiple("skipAfterChunks", pixelsPerElement, s);
+ skipAfterChunks[i] /= pixelsPerElement;
}
}
} else {
@@ -124,8 +124,8 @@ public abstract class Inflater {
int i = Arrays.binarySearch(divisors, Numerics.ceilDiv(samplesPerChunk, maxChunkSize));
if (i < 0) i = ~i; // No need for array bound check.
/*
- * Following loop iterates exactly once unless `samplesPerElement` > 1.
- * The intend is to ensure that `samplesPerChunk` is also a divisor of `samplesPerElement`.
+ * Following loop iterates exactly once unless `pixelsPerElement` > 1.
+ * The intend is to ensure that `samplesPerChunk` is also a divisor of `pixelsPerElement`.
* If we can not find such value, current implementation will fail at `ensureDivisor` call.
*
* TODO: to avoid this problem, one possible approach could be to force `maxChunkSize` to be
@@ -133,7 +133,7 @@ public abstract class Inflater {
*/
do {
chunksPerRow = divisors[i];
- if ((samplesPerChunk % (chunksPerRow * samplesPerElement)) == 0) break;
+ if ((samplesPerChunk % (chunksPerRow * pixelsPerElement)) == 0) break;
} while (++i < divisors.length);
samplesPerChunk /= chunksPerRow;
}
@@ -144,9 +144,9 @@ public abstract class Inflater {
* element boundary.
*/
if (chunksPerRow != 1) {
- ArgumentChecks.ensureDivisor("samplesPerElement", samplesPerChunk, samplesPerElement);
+ ArgumentChecks.ensureDivisor("pixelsPerElement", samplesPerChunk, pixelsPerElement);
}
- this.elementsPerChunk = ceilDiv(samplesPerChunk, samplesPerElement);
+ this.elementsPerChunk = ceilDiv(samplesPerChunk, pixelsPerElement);
this.skipAfterChunks = skipAfterChunks;
this.chunksPerRow = chunksPerRow;
}
@@ -157,26 +157,26 @@ public abstract class Inflater {
*
* <h4>Note on sample definition</h4>
* A "sample" is usually a primitive type such as a byte or a float, but may be a unit smaller than a byte
- * (e.g. 1 bit) if {@code samplesPerElement} is greater than 1. If that case, the {@link #elementsPerChunk}
- * and {@link #skipAfterChunks} values will be divided by {@code samplesPerElement}.
+ * (e.g. 1 bit) if {@code pixelsPerElement} is greater than 1. If that case, the {@link #elementsPerChunk}
+ * and {@link #skipAfterChunks} values will be divided by {@code pixelsPerElement}.
*
- * @param compression the compression method.
- * @param input the source of data to decompress.
- * @param start stream position where to start reading.
- * @param byteCount number of bytes to read before decompression.
- * @param sourceWidth number of pixels in a row of source image.
- * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
- * @param samplesPerChunk number of sample values per chunk (sample or pixel). Must be strictly positive.
- * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
- * @param samplesPerElement number of sample values per primitive element. Always 1 except for multi-pixels packed images.
- * @param banks where to store sample values.
+ * @param compression the compression method.
+ * @param input the source of data to decompress.
+ * @param start stream position where to start reading.
+ * @param byteCount number of bytes to read before decompression.
+ * @param sourceWidth number of pixels in a row of source image.
+ * @param chunksPerRow number of chunks (usually pixels) per row in target image. Must be strictly positive.
+ * @param samplesPerChunk number of sample values per chunk (sample or pixel). Must be strictly positive.
+ * @param skipAfterChunks number of sample values to skip between chunks. May be empty or null.
+ * @param pixelsPerElement number of pixels per primitive element. Always 1 except for multi-pixels packed images.
+ * @param banks where to store sample values.
* @return the inflater for the given targe type, or {@code null} if the compression method is unknown.
* @throws IOException if an I/O operation was required and failed.
*/
public static Inflater create(final Compression compression,
final ChannelDataInput input, final long start, final long byteCount, final int sourceWidth,
final int chunksPerRow, final int samplesPerChunk, final int[] skipAfterChunks,
- final int samplesPerElement, final Buffer banks)
+ final int pixelsPerElement, final Buffer banks)
throws IOException
{
ArgumentChecks.ensureNonNull("input", input);
@@ -184,14 +184,14 @@ public abstract class Inflater {
final InflaterChannel inflated;
switch (compression) {
case NONE: {
- return CopyFromBytes.create(input, start, chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, banks);
+ return CopyFromBytes.create(input, start, chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, banks);
}
case PACKBITS: inflated = new PackBits(input, start, byteCount); break;
case CCITTRLE: inflated = new CCITTRLE(input, start, byteCount, sourceWidth); break;
default: return null;
}
return CopyFromBytes.create(inflated.createDataInput(), 0,
- chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, banks);
+ chunksPerRow, samplesPerChunk, skipAfterChunks, pixelsPerElement, banks);
}
/**
@@ -205,17 +205,17 @@ public abstract class Inflater {
* one byte for the first 8 bits, then 2 bits rounded to the next byte boundary.
*
* <p>The exact rounding mode depends on the compression algorithm. To avoid erratic behavior, callers shall
- * restrict <var>n</var> to multiples of {@code samplesPerElement} before to read the first pixel in a row.
+ * restrict <var>n</var> to multiples of {@code pixelsPerElement} before to read the first pixel in a row.
* This restriction does not apply when skipping remaining data after the last pixels in a row, because the
* next row will be realigned on an element boundary anyway. The way to perform this realignment depends on
* the compression, which is the reason why the exact rounding mode may vary with compression algorithms.</p>
*
- * <p>Restricting <var>n</var> to multiples of {@code samplesPerElement} implies the following restrictions:</p>
+ * <p>Restricting <var>n</var> to multiples of {@code pixelsPerElement} implies the following restrictions:</p>
* <ul>
* <li>No subsampling on the <var>x</var> axis (however subsampling on other axes is still allowed).</li>
* <li>No band subset if interleaved sample model, because the effect is similar to subsampling.</li>
* <li>If {@link org.apache.sis.coverage.grid.GridDerivation} is used for computing the sub-region to read,
- * it should have been configured with {@code chunkSize(samplesPerElement)}
+ * it should have been configured with {@code chunkSize(pixelsPerElement)}
* (unless chunk size is already used for tile size).</li>
* </ul>
*
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
index bf62d2d..93be17b 100644
--- 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
@@ -19,7 +19,6 @@ package org.apache.sis.storage.geotiff;
import java.io.IOException;
import java.nio.Buffer;
import java.awt.Point;
-import java.awt.image.RasterFormatException;
import java.awt.image.WritableRaster;
import org.apache.sis.internal.storage.TiledGridResource;
import org.apache.sis.internal.coverage.j2d.RasterFactory;
@@ -28,7 +27,6 @@ import org.apache.sis.internal.geotiff.Inflater;
import org.apache.sis.internal.geotiff.Resources;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.util.resources.Errors;
import org.apache.sis.image.DataType;
import static java.lang.Math.toIntExact;
@@ -201,7 +199,6 @@ final class CompressedSubset extends DataSubset {
final int[] subsampling, final Point location) throws IOException, DataStoreException
{
final DataType type = getDataType();
- final int typeSize = type.size();
final int width = pixelCount(lower, upper, subsampling, 0);
final int height = pixelCount(lower, upper, subsampling, 1);
final int chunksPerRow = width * (targetPixelStride / samplesPerChunk);
@@ -213,30 +210,26 @@ final class CompressedSubset extends DataSubset {
* `betweenPixels` is the number of sample values to skip between each pixel, but the actual skips
* are more complicated if only a subset of the bands are read. The actual number of sample values
* to skip between "chunks" is detailed by `skipAfterChunks`.
+ *
+ * `pixelsPerElement` below is a factor for converting a count of pixels to a count of primitive elements
+ * in the bank. The `pixelsPerElement` factor is usually 1, except when more than one pixel is packed in
+ * each single primitive type (e.g. 8 bits per byte in bilevel image). The `head` needs to be a multiple
+ * of `pixelsPerElement`; this restriction is documented in `Inflater.skip(long)` and should have been
+ * verified by `TiledGridResource`.
*/
+ final int pixelsPerElement = getPixelsPerElement(); // Always ≥ 1 and usually = 1.
+ assert (head % pixelsPerElement) == 0 : head;
+ final int capacity = getBankCapacity(pixelsPerElement);
final Buffer[] banks = new Buffer[numBanks];
for (int b=0; b<numBanks; b++) {
/*
- * Factor for converting a count of sample values to a count of primitive elements in the bank.
- * The `samplesPerElement` factor is usually 1, except when more than one pixel is packed
- * in each single primitive type (e.g. 8 bits per byte in bilevel image).
- */
- final int sampleSize = getSampleSize(b); // Size in bits of sample values.
- final int samplesPerElement = typeSize / sampleSize; // Always ≥ 1 and usually = 1.
- if (typeSize % sampleSize != 0) {
- throw new RasterFormatException(reader().errors().getString(
- Errors.Keys.NotADivisorOrMultiple_4, "BitsPerSample", 0, typeSize, sampleSize));
- }
- // TiledGridResource shall ensure that following `Inflater.skip(long)` restriction is met.
- assert (head % samplesPerElement) == 0 : head;
- /*
* Prepare the object which will perform the actual decompression row-by-row,
* optionally skipping chunks if a subsampling is applied.
*/
- final Buffer bank = RasterFactory.createBuffer(type, getBankCapacity(samplesPerElement));
+ final Buffer bank = RasterFactory.createBuffer(type, capacity);
final Inflater algo = Inflater.create(compression, reader().input, offsets[b], byteCounts[b],
getTileSize(0), chunksPerRow, samplesPerChunk, skipAfterChunks,
- samplesPerElement, bank);
+ pixelsPerElement, bank);
if (algo == null) {
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/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index c5b4a7c..79a2bc4 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
@@ -140,7 +140,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
final SampleModel model = getSampleModel();
int b = model.getNumBands();
if (b != 1 && !(model instanceof BandedSampleModel)) { // First condition (see Javadoc).
- if (subset.hasBandSubset() || subset.hasSubsampling(0)) { // Exception to first consition.
+ if (!subset.isXContiguous()) { // Exception to first consition.
return false;
}
}
@@ -165,7 +165,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
GridCoverage coverage;
try {
synchronized (getSynchronizationLock()) {
- final Subset subset = new Subset(domain, range, false);
+ final Subset subset = new Subset(domain, range);
final Compression compression = getCompression();
if (compression == null) {
throw new DataStoreContentException(reader.resources().getString(
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 9657b38..fcbfcaf 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
@@ -22,7 +22,6 @@ import java.nio.Buffer;
import java.io.IOException;
import java.awt.Point;
import java.awt.image.BandedSampleModel;
-import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import org.apache.sis.image.DataType;
@@ -195,37 +194,16 @@ class DataSubset extends TiledGridCoverage implements Localized {
}
/**
- * Returns the size in bits of samples for the specified <em>bank</em> (not band).
- * If the sample model packs all pixel samples in a single bank element, then this
- * method returns the sum of the size of sample sizes for all bands.
- *
- * @param bank the bank for which to get sample size.
- * @return the size of the samples of the specified bank.
- *
- * @see java.awt.image.SampleModel#getSampleSize(int)
- */
- protected final int getSampleSize(final int bank) {
- if (model instanceof ComponentSampleModel) {
- return DataBuffer.getDataTypeSize(model.getDataType());
- }
- int size = 0;
- for (int b = model.getNumBands(); --b >= 0;) {
- size += model.getSampleSize(b);
- }
- return size;
- }
-
- /**
* Returns the size of a bank (not necessarily a band) in number of primitive elements (bytes, integers, …).
* This is tile width × height × {@link #targetPixelStride} divided by the number of sample values per element,
* with each row starting on an element boundary.
*
- * @param samplesPerElement always 1 except when two or more pixels are packed in each element.
+ * @param pixelsPerElement always 1 except when two or more pixels are packed in each element.
* @return expected number of primitive elements in the bank.
*/
- protected final int getBankCapacity(final int samplesPerElement) {
+ protected final int getBankCapacity(final int pixelsPerElement) {
// `ceilDiv(…)` must happen before multiplication by image height.
- final int scanlineStride = ceilDiv(multiplyExact(model.getWidth(), targetPixelStride), samplesPerElement);
+ final int scanlineStride = ceilDiv(multiplyExact(model.getWidth(), targetPixelStride), pixelsPerElement);
return multiplyExact(scanlineStride, model.getHeight());
}
@@ -407,6 +385,8 @@ class DataSubset extends TiledGridCoverage implements Localized {
* @throws DataStoreException if a logical error occurred.
* @throws RuntimeException if the Java2D image can not be created for another reason
* (too many exception types to list them all).
+ *
+ * @see DataCube#canReadDirect(TiledGridResource.Subset)
*/
WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
final int[] subsampling, final Point location) throws IOException, DataStoreException
@@ -432,7 +412,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
lower[0] *= sourcePixelStride;
upper[0] *= sourcePixelStride;
/*
- * Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
+ * Read each plane ("banks" in Java2D terminology). Note that a single bank contains all bands
* in the interleaved sample model case. This block assumes that each bank element contains
* exactly one sample value (verified by assertion), as documented in the Javadoc of this method.
* If that assumption was not true, we would have to adjust `capacity`, `lower[0]` and `upper[0]`
@@ -447,7 +427,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
Resources.Keys.UnexpectedTileLength_2, length, byteCounts[b]));
}
hr.setOrigin(offsets[b]);
- assert getSampleSize(b) == sampleSize; // See above comment.
+ assert model.getSampleSize(b) == sampleSize; // See above comment.
final Buffer bank = hr.readAsBuffer(region, getBankCapacity(1));
fillRemainingRows(bank);
banks[b] = bank;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index 6ceb19a..8de9830 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -19,8 +19,11 @@ package org.apache.sis.internal.storage;
import java.util.Map;
import java.util.Locale;
import java.io.IOException;
+import java.awt.Point;
+import java.awt.image.DataBuffer;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import org.opengis.coverage.CannotEvaluateException;
@@ -292,6 +295,50 @@ public abstract class TiledGridCoverage extends GridCoverage {
}
/**
+ * Returns the number of pixels in a single bank element. This is usually 1, except for
+ * {@link MultiPixelPackedSampleModel} which packs many pixels in a single bank element.
+ * This value is a power of 2 according {@code MultiPixelPackedSampleModel} specification.
+ *
+ * <div class="note"><b>Note:</b>
+ * this is "pixels per element", not "samples per element". It makes a difference in the
+ * {@link java.awt.image.SinglePixelPackedSampleModel} case, for which this method returns 1
+ * (by contrast a "samples per element" would give a value greater than 1).
+ * But this value can nevertheless be understood as a "samples per element" value
+ * where only one band is considered at a time.</div>
+ *
+ * @return number of pixels in a single bank element. Usually 1.
+ *
+ * @see SampleModel#getSampleSize(int)
+ * @see MultiPixelPackedSampleModel#getPixelBitStride()
+ */
+ protected final int getPixelsPerElement() {
+ return getPixelsPerElement(model);
+ }
+
+ /**
+ * Implementation of {@link #getPixelsPerElement()}.
+ *
+ * @param model the sample model from which to infer the number of pixels per bank element.
+ * @return number of pixels in a single bank element. Usually 1.
+ */
+ static int getPixelsPerElement(final SampleModel model) {
+ if (model instanceof MultiPixelPackedSampleModel) {
+ /*
+ * The following code performs the same computation than `MultiPixelPackedSampleModel`
+ * constructor when computing its package-private field `pixelsPerDataElement`.
+ * That constructor ensured that `sampleSize` is a divisor of `typeSize`.
+ */
+ final int typeSize = DataBuffer.getDataTypeSize(model.getDataType());
+ final int sampleSize = ((MultiPixelPackedSampleModel) model).getPixelBitStride();
+ final int pixelsPerElement = typeSize / sampleSize;
+ if (pixelsPerElement > 0) { // Paranoiac check.
+ return pixelsPerElement;
+ }
+ }
+ return 1;
+ }
+
+ /**
* Returns a two-dimensional slice of grid data as a rendered image.
*
* @param sliceExtent a subspace of this grid coverage extent, or {@code null} for the whole image.
@@ -478,10 +525,15 @@ public abstract class TiledGridCoverage extends GridCoverage {
public WritableRaster getCachedTile() {
WritableRaster tile = rasters.get(createCacheKey(indexInTileVector));
if (tile != null) {
- // Found a tile. Make sure that the raster starts at the expected coordinates.
+ /*
+ * Found a tile, but the sample model may be different because band order may be different.
+ * In both cases, we need to make sure that the raster starts at the expected coordinates.
+ */
final int x = getTileOrigin(0);
final int y = getTileOrigin(1);
- if (tile.getMinX() != x || tile.getMinY() != y) {
+ if (!model.equals(tile.getSampleModel())) {
+ tile = WritableRaster.createWritableRaster(model, tile.getDataBuffer(), new Point(x, y));
+ } else if (tile.getMinX() != x || tile.getMinY() != y) {
tile = tile.createWritableTranslatedChild(x, y);
}
}
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 45df226..e5d4c10 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
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.awt.image.DataBuffer;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
@@ -148,26 +149,47 @@ public abstract class TiledGridResource extends AbstractGridResource {
* By declaring an "atom" size of 8 sample values in dimension 0, the {@link Subset} constructor
* will ensure than the sub-region to read starts at a byte boundary when reading a bilevel image.
*
+ * <p>The default implementation returns the {@linkplain TiledGridCoverage#getPixelsPerElement()
+ * number of pixels per data element} for dimension 0 and returns 1 for all other dimensions.</p>
+ *
* @param dimension the dimension for which to get the atom size.
* This is in units of sample values (may be bits, bytes, floats, <i>etc</i>).
* @return indivisible amount of sample values to read in the specified dimension. Must be ≥ 1.
* @throws DataStoreException if an error occurred while fetching the sample model.
*/
- private int getAtomSize(final int dimension) throws DataStoreException {
- if (dimension == 0) {
- final SampleModel model = getSampleModel();
- if (model != null && !(model instanceof ComponentSampleModel)) {
- int size = 1;
- for (int b = model.getNumBands(); --b >= 0;) {
- size = Math.max(size, model.getSampleSize(b));
- }
- final int samplesPerElement = DataBuffer.getDataTypeSize(model.getDataType()) / size;
- if (samplesPerElement >= 1) {
- return samplesPerElement;
- }
- }
- }
- return 1;
+ protected int getAtomSize(final int dimension) throws DataStoreException {
+ return (dimension == 0) ? TiledGridCoverage.getPixelsPerElement(getSampleModel()) : 1;
+ }
+
+ /**
+ * Returns {@code true} if the reader can load only the requested bands and skip the other bands,
+ * or {@code false} if the reader must load all bands. This value controls the amount of data to
+ * be loaded by {@link #read(GridGeometry, int...)}:
+ *
+ * <ul class="verbose">
+ * <li>If {@code false}, then {@link TiledGridCoverage#model} will expect the same {@link DataBuffer}
+ * than the one expected by the {@linkplain #getSampleModel() sample model of this resource}.
+ * All bands will be loaded but the coverage sample model will ignore the bands that were not
+ * enumerated in the {@code range} argument. This strategy is convenient when skipping bands
+ * at reading time is hard.</li>
+ * <li>If {@code true}, then {@link TiledGridCoverage#model} will have its band indices and bit masks
+ * "compressed" for making them consecutive. For example if the {@code range} argument specifies that
+ * the bands to read are {1, 3, 4, 6}, then after "compression" band indices become {0, 1, 2, 3}.
+ * This strategy is efficient when the reader is capable to skip bands at reading time.</li>
+ * </ul>
+ *
+ * <p>The default implementation returns {@code true} if the sample model is a {@link ComponentSampleModel}
+ * and {@code false} if all other cases, because skipping bands in a packed sample model is more difficult
+ * to implement.</p>
+ *
+ * @return {@code true} if the reader can load only the requested bands and skip other bands, or
+ * {@code false} if the reader needs to load all bands regardless the {@code range} subset.
+ * @throws DataStoreException if an error occurred while fetching the sample model.
+ *
+ * @see AbstractGridResource.RangeArgument#select(SampleModel, boolean)
+ */
+ protected boolean getDissociableBands() throws DataStoreException {
+ return getSampleModel() instanceof ComponentSampleModel;
}
/**
@@ -178,6 +200,9 @@ public abstract class TiledGridResource extends AbstractGridResource {
* If this resource has more than 2 dimensions, then this model is for the two first ones (usually horizontal).
* The images for all levels in additional dimensions shall use the same sample model.
*
+ * <h4>Performance note</h4>
+ * Implementation should return a cached value, because this method may be invoked many times.
+ *
* @return the sample model for tiles at full resolution with all their bands.
* @throws DataStoreException if an error occurred during sample model construction.
*/
@@ -228,8 +253,12 @@ public abstract class TiledGridResource extends AbstractGridResource {
final GridGeometry domain;
/**
- * Sample dimensions for each image band.
- * This is the range of the grid coverage to create.
+ * Sample dimensions for each image band. This is the range of the grid coverage to create.
+ * If {@link #includedBands} is non-null, then the the size of this list should be equal to
+ * {@link #includedBands} array length. However bands are not necessarily in the same order:
+ * the order of bands in this {@code ranges} list is the order specified by user, while the
+ * order of bands in {@link #includedBands} is always increasing index order for efficiency
+ * reasons.
*/
final List<? extends SampleDimension> ranges;
@@ -294,10 +323,6 @@ public abstract class TiledGridResource extends AbstractGridResource {
*
* @param domain the domain argument specified by user in a call to {@code GridCoverageResource.read(…)}.
* @param range the range argument specified by user in a call to {@code GridCoverageResource.read(…)}.
- * @param loadAllBands {@code true} if the reader will load all bands regardless the {@code range} subset,
- * or {@code false} if only requested bands will be loaded and other bands will be skipped. See
- * {@link AbstractGridResource.RangeArgument#select(SampleModel, boolean) RangeArgument.select(…)}
- * for more information.
*
* @throws ArithmeticException if pixel indices exceed 64 bits integer capacity.
* @throws DataStoreException if a call to {@link TiledGridResource} method failed.
@@ -305,7 +330,7 @@ public abstract class TiledGridResource extends AbstractGridResource {
* @throws IllegalArgumentException if an error occurred in an operation
* such as creating the {@code SampleModel} subset for selected bands.
*/
- public Subset(GridGeometry domain, final int[] range, final boolean loadAllBands) throws DataStoreException {
+ public Subset(GridGeometry domain, final int[] range) throws DataStoreException {
List<SampleDimension> bands = getSampleDimensions();
final RangeArgument rangeIndices = validateRangeArgument(bands.size(), range);
final GridGeometry gridGeometry = getGridGeometry();
@@ -343,16 +368,20 @@ public abstract class TiledGridResource extends AbstractGridResource {
* be handled by the `SampleModel`, not in `includedBands` array.
*/
int[] includedBands = null;
- if (!rangeIndices.isIdentity()) {
+ boolean loadAllBands = rangeIndices.isIdentity();
+ if (!loadAllBands) {
bands = Arrays.asList(rangeIndices.select(bands));
- includedBands = new int[rangeIndices.getNumBands()];
- for (int i=0; i<includedBands.length; i++) {
- includedBands[i] = rangeIndices.getSourceIndex(i);
- }
- assert ArraysExt.isSorted(includedBands, true);
- if (rangeIndices.hasAllBands) {
- assert ArraysExt.isRange(0, includedBands);
- includedBands = null;
+ loadAllBands = !getDissociableBands();
+ if (!loadAllBands) {
+ includedBands = new int[rangeIndices.getNumBands()];
+ for (int i=0; i<includedBands.length; i++) {
+ includedBands[i] = rangeIndices.getSourceIndex(i);
+ }
+ assert ArraysExt.isSorted(includedBands, true);
+ if (rangeIndices.hasAllBands) {
+ assert ArraysExt.isRange(0, includedBands);
+ includedBands = null;
+ }
}
}
this.domain = domain;
@@ -369,24 +398,22 @@ public abstract class TiledGridResource extends AbstractGridResource {
}
/**
- * Returns {@code true} if this subset contains a subsampling in the given dimension.
+ * Returns {@code true} if reading data in this subset will read contiguous values on the <var>x</var> axis.
+ * This method returns {@code true} if all following conditions are met:
*
- * @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;
- }
-
- /**
- * Returns {@code true} if the user requested a subset of bands, or {@code false}
- * if all bands shall be read in order. For this method, a change of band order
- * is considered as a "subset".
+ * <ul>
+ * <li>All bands will be read (ignoring change of band order
+ * because this change is handled by the sample model).</li>
+ * <li>There is no subsampling on the <var>x</var> axis.</li>
+ * </ul>
+ *
+ * Note that the first criterion can often be relaxed when the sample model is an instance
+ * of {@link BandedSampleModel}. This method does not check the sample model type.
*
- * @return {@code true} if only a subset of bands will be read or if bands will be read out of order.
+ * @return whether the values to read on a row are contiguous.
*/
- public boolean hasBandSubset() {
- return includedBands != null;
+ public boolean isXContiguous() {
+ return includedBands == null && subsampling[0] == 1;
}
}