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 2fc21218f5b3041ca7e191133fb7d7efd9dfbcb9 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Aug 12 01:14:53 2021 +0200 When reading multi-pixels packed images, restrict sub-region to element boundaries (typically byte boundaries). It allows the `SelfConsistencyCheck` test to pass for random sub-regions. --- .../java/org/apache/sis/util/ArgumentChecks.java | 23 +++++++++++-- .../java/org/apache/sis/util/resources/Errors.java | 4 +-- .../apache/sis/util/resources/Errors.properties | 2 +- .../apache/sis/util/resources/Errors_fr.properties | 2 +- .../apache/sis/internal/geotiff/CopyFromBytes.java | 20 ++++++++--- .../org/apache/sis/internal/geotiff/Inflater.java | 23 ++++++++++++- .../sis/storage/geotiff/CompressedSubset.java | 12 +++---- .../org/apache/sis/storage/geotiff/DataSubset.java | 3 -- .../sis/internal/storage/TiledGridResource.java | 39 ++++++++++++++++++++-- 9 files changed, 104 insertions(+), 24 deletions(-) diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java index a855838..8acf005 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java @@ -703,7 +703,7 @@ public final class ArgumentChecks extends Static { * * @param name name of the argument for the divisor value. Used only if an exception is thrown. * @param number the number to be divided. - * @param divisor the divisor to verify. + * @param divisor the value to verify. * @throws IllegalArgumentException if {@code (number % divisor) != 0}. * @throws ArithmeticException if {@code divisor == 0}. * @@ -711,7 +711,26 @@ public final class ArgumentChecks extends Static { */ public static void ensureDivisor(final String name, final int number, final int divisor) { if ((number % divisor) != 0) { - throw new IllegalArgumentException(Errors.format(Errors.Keys.NotADivisor_3, name, number, divisor)); + throw new IllegalArgumentException(Errors.format(Errors.Keys.NotADivisorOrMultiple_4, name, 0, number, divisor)); + } + } + + /** + * Ensures that a given value is a multiple of specified number. + * This method verifies that {@code (multiple % number) == 0}. + * If above condition is not met, the value considered to be wrong is the multiple. + * + * @param name name of the argument for the multiple value. Used only if an exception is thrown. + * @param number the number to be multiplied. + * @param multiple the value to verify. + * @throws IllegalArgumentException if {@code (multiple % number) != 0}. + * @throws ArithmeticException if {@code number == 0}. + * + * @since 1.1 + */ + public static void ensureMultiple(final String name, final int number, final int multiple) { + if ((multiple % number) != 0) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.NotADivisorOrMultiple_4, name, 1, number, multiple)); } } diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java index 312eb45..cfc3af0 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java @@ -700,9 +700,9 @@ public final class Errors extends IndexedResourceBundle { public static final short NotABackwardReference_1 = 108; /** - * Value of ‘{0}’ shall be a divisor of {1} but the provided value {2} is not. + * Value of ‘{0}’ shall be a {1,choice,0#divisor|1#multiple} of {2} but the given value is {3}. */ - public static final short NotADivisor_3 = 193; + public static final short NotADivisorOrMultiple_4 = 193; /** * “{0}” is not a key-value pair. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties index 8c2cbf2..eee7216 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties @@ -149,7 +149,7 @@ NonTemporalUnit_1 = \u201c{0}\u201d is not a time unit. NonSystemUnit_1 = \u201c{0}\u201d is not a fundamental or derived unit. NonRatioUnit_1 = The scale of measurement for \u201c{0}\u201d unit is not a ratio scale. NotABackwardReference_1 = No element for the \u201c{0}\u201d identifier, or the identifier is a forward reference. -NotADivisor_3 = Value of \u2018{0}\u2019 shall be a divisor of {1} but the provided value {2} is not. +NotADivisorOrMultiple_4 = Value of \u2018{0}\u2019 shall be a {1,choice,0#divisor|1#multiple} of {2} but the given value is {3}. NotAnInteger_1 = {0} is not an integer value. NotAKeyValuePair_1 = \u201c{0}\u201d is not a key-value pair. NotANumber_1 = Argument \u2018{0}\u2019 shall not be NaN (Not-a-Number). diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties index ce9e5ed..e0ac843 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties @@ -146,7 +146,7 @@ NonTemporalUnit_1 = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas u NonSystemUnit_1 = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas une unit\u00e9 fondamentale ou d\u00e9riv\u00e9e. NonRatioUnit_1 = L\u2019\u00e9chelle de mesure de l\u2019unit\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas une \u00e9chelle de rapports. NotABackwardReference_1 = Il n\u2019y a pas d\u2019\u00e9l\u00e9ment pour l\u2019identifiant \u201c{0}\u201d, ou l\u2019identifiant est une r\u00e9f\u00e9rence vers l\u2019avant. -NotADivisor_3 = La valeur de \u2018{0}\u2019 doit \u00eatre un diviseur de {1}, mais la valeur fournie {2} ne l\u2019est pas. +NotADivisorOrMultiple_4 = La valeur de \u2018{0}\u2019 doit \u00eatre un {1,choice,0#diviseur|1#multiple} de {2}, mais la valeur donn\u00e9e est {3}. NotAnInteger_1 = {0} n\u2019est pas un nombre entier. NotAKeyValuePair_1 = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas une paire cl\u00e9-valeur. NotANumber_1 = L\u2019argument \u2018{0}\u2019 ne doit pas \u00eatre NaN (Not-a-Number). 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 b476a70..e18d8b6 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 @@ -128,6 +128,11 @@ abstract class CopyFromBytes extends Inflater { * Skips the given amount of sample values without storing them. * 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} + * if this method is not invoked for skipping all remaining values until end of row. + * See {@link Inflater#skip(long)} for more information. + * * @param n number of uncompressed sample values to ignore. * @throws IOException if an error occurred while reading the input channel. */ @@ -138,11 +143,16 @@ abstract class CopyFromBytes extends Inflater { positionNeedsRefresh = false; streamPosition = input.getStreamPosition(); } - n *= bytesPerElement; - if (n % samplesPerElement != 0) { - throw new IllegalArgumentException(); // Condition should have been verified before construction. - } - streamPosition = Math.addExact(streamPosition, n / samplesPerElement); + /* + * 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`, + * 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. + 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 e5987c2..b158ed5 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 @@ -107,7 +107,7 @@ public abstract class Inflater { for (int i=0; i<skipAfterChunks.length; i++) { final int s = skipAfterChunks[i]; ArgumentChecks.ensurePositive("skipAfterChunks", s); - ArgumentChecks.ensureDivisor("samplesPerElement", s, samplesPerElement); + ArgumentChecks.ensureMultiple("skipAfterChunks", samplesPerElement, s); skipAfterChunks[i] /= samplesPerElement; } } @@ -198,6 +198,27 @@ public abstract class Inflater { * Reads the given amount of sample values without storing them. * The given value is in units of sample values, not in bytes. * + * <h4>Case of multi-pixels packed image</h4> + * If there is more than one sample value per element, then this method may round (at implementation choice) + * the stream position to the first element boundary after skipping <var>n</var> sample values. For example + * a bilevel image packs 8 sample values per byte. Consequently a call to {@code skip(10)} may skip 2 bytes: + * 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. + * 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> + * <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)} + * (unless chunk size is already used for tile size).</li> + * </ul> + * * @param n number of uncompressed sample values to ignore. * @throws IOException if an error occurred while reading the input channel. */ 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 92b50bd..bf62d2d 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 @@ -128,7 +128,7 @@ final class CompressedSubset extends DataSubset { // Number of sample values to skip after each band. skips[i] = b - (b = includedBands[i]) - 1; } - beforeFirstBand = b; + beforeFirstBand = b; // After above loop, `b` became the index of first band. afterLastBand += skips[m]; // Add trailing bands that were left unread. skips[m] += between + beforeFirstBand; // Add pixels skipped by subsampling and move to first band. /* @@ -225,13 +225,15 @@ final class CompressedSubset extends DataSubset { final int samplesPerElement = typeSize / sampleSize; // Always ≥ 1 and usually = 1. if (typeSize % sampleSize != 0) { throw new RasterFormatException(reader().errors().getString( - Errors.Keys.NotADivisor_3, "BitsPerSample", typeSize, sampleSize)); + Errors.Keys.NotADivisorOrMultiple_4, "BitsPerSample", 0, typeSize, sampleSize)); } - final Buffer bank = RasterFactory.createBuffer(type, getBankCapacity(samplesPerElement)); + // 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 Inflater algo = Inflater.create(compression, reader().input, offsets[b], byteCounts[b], getTileSize(0), chunksPerRow, samplesPerChunk, skipAfterChunks, samplesPerElement, bank); @@ -240,9 +242,7 @@ final class CompressedSubset extends DataSubset { Resources.Keys.UnsupportedCompressionMethod_1, compression)); } for (long y = lower[1]; --y >= 0;) { - algo.skip(scanlineStride); - // TODO: after we finished to implement decompression algorithms, - // revisit if we can safely replace the loop by a multiplication. + algo.skip(scanlineStride); // `skip(…)` may round to next element boundary. } for (int y = height; --y > 0;) { // (height - 1) iterations. algo.skip(head); 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 92d1624..9657b38 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 @@ -208,9 +208,6 @@ class DataSubset extends TiledGridCoverage implements Localized { if (model instanceof ComponentSampleModel) { return DataBuffer.getDataTypeSize(model.getDataType()); } - if (numBanks != 1) { - return model.getSampleSize(bank); - } int size = 0; for (int b = model.getNumBands(); --b >= 0;) { size += model.getSampleSize(b); 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 0c171cf..45df226 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 @@ -18,8 +18,10 @@ package org.apache.sis.internal.storage; import java.util.List; import java.util.Arrays; +import java.awt.image.DataBuffer; import java.awt.image.ColorModel; import java.awt.image.SampleModel; +import java.awt.image.ComponentSampleModel; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.awt.image.RasterFormatException; @@ -133,11 +135,42 @@ public abstract class TiledGridResource extends AbstractGridResource { * Returns the size of tiles in this resource. * The length of the returned array is the number of dimensions. * - * @return the size of tiles in this resource. + * @return the size of tiles (in pixels) in this resource. */ protected abstract int[] getTileSize(); /** + * Returns the number of sample values in an indivisible element of a tile. + * An element is a primitive type such as {@code byte}, {@code int} or {@code float}. + * This value is usually 1 because each sample value is usually stored in a separated element. + * However in multi-pixels packed sample model (e.g. bilevel image with 8 pixels per byte), + * it is difficult to start reading an image at <var>x</var> location other than a byte boundary. + * 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. + * + * @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; + } + + /** * Returns the Java2D sample model describing pixel type and layout for all bands. * The raster size is the {@linkplain #getTileSize() tile size} as stored in the resource. * @@ -295,8 +328,8 @@ public abstract class TiledGridResource extends AbstractGridResource { */ int tileWidth = tileSize[0]; int tileHeight = tileSize[1]; - if (tileWidth >= sourceExtent.getSize(0)) {tileWidth = 1; sharedCache = false;} - if (tileHeight >= sourceExtent.getSize(1)) {tileHeight = 1; sharedCache = false;} + if (tileWidth >= sourceExtent.getSize(0)) {tileWidth = getAtomSize(0); sharedCache = false;} + if (tileHeight >= sourceExtent.getSize(1)) {tileHeight = getAtomSize(1); sharedCache = false;} final GridDerivation target = gridGeometry.derive().chunkSize(tileWidth, tileHeight) .rounding(GridRoundingMode.ENCLOSING).subgrid(domain); domain = target.build();