This is an automated email from the ASF dual-hosted git repository. kinow pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-imaging.git
commit 263dddeb92204f840a4136c351b5f2631d646a17 Author: gwlucastrig <contact.tinf...@gmail.com> AuthorDate: Wed Sep 8 22:02:03 2021 -0400 [IMAGING-266] Read numeric data from GeoTIFFs --- src/conf/spotbugs-exclude-filter.xml | 18 ++ .../imaging/formats/tiff/TiffDirectory.java | 67 ++++-- .../imaging/formats/tiff/TiffImageParser.java | 118 ++++++---- .../imaging/formats/tiff/TiffRasterData.java | 163 +++++++------ ...iffRasterData.java => TiffRasterDataFloat.java} | 130 +++++++---- ...{TiffRasterData.java => TiffRasterDataInt.java} | 134 +++++++---- .../imaging/formats/tiff/TiffRasterDataType.java} | 38 ++- .../formats/tiff/constants/TiffTagConstants.java | 2 +- .../formats/tiff/datareaders/DataReaderStrips.java | 84 ++++++- .../formats/tiff/datareaders/DataReaderTiled.java | 82 ++++++- .../formats/tiff/datareaders/ImageDataReader.java | 190 +++++++++++++-- .../commons/imaging/TestImageReadException.java | 24 +- .../commons/imaging/TestImageWriteException.java | 68 +++--- .../tiff/ExampleReadFloatingPointData.java | 2 +- .../imaging/examples/tiff/SurveyTiffFile.java | 3 +- .../formats/tiff/TiffFloatingPointReadTest.java | 2 +- ...terDataTest.java => TiffRasterDataIntTest.java} | 91 ++++---- .../imaging/formats/tiff/TiffRasterDataTest.java | 55 ++++- .../formats/tiff/TiffRasterStatisticsTest.java | 4 +- .../formats/tiff/TiffShortIntRoundTripTest.java | 255 +++++++++++++++++++++ 20 files changed, 1126 insertions(+), 404 deletions(-) diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml index 2f99342..0cfc7cb 100644 --- a/src/conf/spotbugs-exclude-filter.xml +++ b/src/conf/spotbugs-exclude-filter.xml @@ -72,6 +72,24 @@ <Bug pattern="EI_EXPOSE_REP2" /> </Match> <Match> + <Class name="org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat" /> + <Method name="getData" /> + <Bug pattern="EI_EXPOSE_REP" /> + </Match> + <Match> + <Class name="org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat" /> + <Bug pattern="EI_EXPOSE_REP2" /> + </Match> + <Match> + <Class name="org.apache.commons.imaging.formats.tiff.TiffRasterDataInt" /> + <Method name="getIntData" /> + <Bug pattern="EI_EXPOSE_REP" /> + </Match> + <Match> + <Class name="org.apache.commons.imaging.formats.tiff.TiffRasterDataInt" /> + <Bug pattern="EI_EXPOSE_REP2" /> + </Match> + <Match> <Class name="org.apache.commons.imaging.formats.tiff.datareaders.ImageDataReader" /> <Bug pattern="EI_EXPOSE_REP2" /> </Match> diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java index e84f780..d548365 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java @@ -92,6 +92,16 @@ public class TiffDirectory extends TiffElement { this.headerByteOrder = byteOrder; } + /** + * Gets the byte order used by the source file for storing this directory + * and its content. + * + * @return A valid byte order instance. + */ + public ByteOrder getByteOrder() { + return headerByteOrder; + } + public String description() { return TiffDirectory.description(type); } @@ -845,11 +855,11 @@ public class TiffDirectory extends TiffElement { } /** - * Reads the floating-point data stored in this TIFF directory, if - * available. Note that this method is defined only for TIFF directories - * that contain floating-point data. + * Reads the numerical data stored in this TIFF directory, if available. + * Note that this method is defined only for TIFF directories that contain + * floating-point data or two-byte signed integer data. * <p> - * TIFF directories that provide floating-point data do not directly specify + * TIFF directories that provide numerical data do not directly specify * images, though it is possible to interpret the data as an image using * this library. TIFF files may contain multiple directories which are * allowed to have different formats. Thus it is possible for a TIFF file to @@ -872,37 +882,62 @@ public class TiffDirectory extends TiffElement { * TiffRasterData raster = * directory.readFloatingPointRasterData(params); * </pre> - + * * @param params an optional parameter map instance * @return a valid instance * @throws ImageReadException in the event of incompatible or malformed data * @throws IOException in the event of an I/O error */ - public TiffRasterData getFloatingPointRasterData( - final Map<String, Object> params) - throws ImageReadException, IOException { + public TiffRasterData getRasterData( + final Map<String, Object> params) + throws ImageReadException, IOException { final TiffImageParser parser = new TiffImageParser(); - return parser.getFloatingPointRasterData(this, headerByteOrder, params); + return parser.getRasterData(this, headerByteOrder, params); } /** * Indicates whether the directory definition specifies a float-point data * format. * - * @return true if the directory contains floating point data; otherwise, - * false + * @return {@code true} if the directory contains floating point data; + * otherwise, {@code false} + * * @throws ImageReadException in the event of an invalid or malformed * specification. */ public boolean hasTiffFloatingPointRasterData() throws ImageReadException { - if (this.hasTiffImageData()) { - final short[] sSampleFmt = getFieldValue( + if (!this.hasTiffImageData()) { + return false; + } + final short[] s = getFieldValue( TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, false); - return sSampleFmt != null && sSampleFmt.length > 0 - && sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT; + return s != null + && s.length > 0 + && s[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT; + + } + /** + * Indicates whether the content associated with the directory is given in a + * supported numerical-data format. If this method returns {@code true}, the + * Imaging API will be able to extract a TiffRasterData instance from the + * associated TIFF file using this directory. + * + * @return {@code true} if the directory contains a supported raster data + * format; otherwise, {@code false}. + * @throws ImageReadException in the event of an invalid or malformed + * specification. + */ + public boolean hasTiffRasterData() throws ImageReadException { + if (!this.hasTiffImageData()) { + return false; } - return false; + final short[] s = getFieldValue( + TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, false); + return s != null + && s.length > 0 + && (s[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT + || s[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER); } } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java index 1418b4f..4a8279f 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java @@ -34,6 +34,7 @@ import java.io.PrintWriter; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -762,12 +763,12 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable { } /** - * Reads the content of a TIFF file that contains floating-point data - * samples. + * Reads the content of a TIFF file that contains numerical data samples + * rather than image-related pixels. * <p> - * If desired, sub-image data can be read from the file by using a Java Map - * instance to specify the subsection of the image that is required. The - * following code illustrates the approach: + * If desired, sub-image data can be read from the file by using a Java + * {@code Map} instance to specify the subsection of the image that + * is required. The following code illustrates the approach: * <pre> * int x; // coordinate (column) of corner of sub-image * int y; // coordinate (row) of corner of sub-image @@ -791,62 +792,57 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable { * @throws ImageReadException in the event of incompatible or malformed data * @throws IOException in the event of an I/O error */ - TiffRasterData getFloatingPointRasterData( - final TiffDirectory directory, - final ByteOrder byteOrder, - final Map<String, Object> params) - throws ImageReadException, IOException { + TiffRasterData getRasterData( + final TiffDirectory directory, + final ByteOrder byteOrder, + Map<String, Object> params) + throws ImageReadException, IOException { final List<TiffField> entries = directory.entries; if (entries == null) { throw new ImageReadException("TIFF missing entries"); } + if (params == null) { + params = new HashMap<>(); + } + final short[] sSampleFmt = directory.getFieldValue( - TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, true); - if (sSampleFmt[0] != TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) { - throw new ImageReadException("TIFF does not provide floating-point data"); + TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, true); + if (sSampleFmt == null || sSampleFmt.length < 1) { + throw new ImageReadException( + "Directory does not specify numeric raster data"); } int samplesPerPixel = 1; final TiffField samplesPerPixelField = directory.findField( - TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); + TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); if (samplesPerPixelField != null) { samplesPerPixel = samplesPerPixelField.getIntValue(); } - if (samplesPerPixel != 1) { - throw new ImageReadException( - "TIFF floating-point data uses unsupported samples per pixel: " - + samplesPerPixel); - } int[] bitsPerSample = {1}; int bitsPerPixel = samplesPerPixel; final TiffField bitsPerSampleField = directory.findField( - TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); + TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); if (bitsPerSampleField != null) { bitsPerSample = bitsPerSampleField.getIntArrayValue(); bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum(); } - if (bitsPerPixel != 32 && bitsPerPixel != 64) { - throw new ImageReadException( - "TIFF floating-point data uses unsupported bits-per-pixel: " - + bitsPerPixel); - } - final short compressionFieldValue; if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) { compressionFieldValue - = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); + = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); } else { compressionFieldValue = TIFF_COMPRESSION_UNCOMPRESSED_1; } final int compression = 0xffff & compressionFieldValue; + final int width - = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH); + = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH); final int height - = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); + = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); Rectangle subImage = checkForSubImage(params); if (subImage != null) { @@ -874,9 +870,9 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable { // if the subimage is just the same thing as the whole // image, suppress the subimage processing if (subImage.x == 0 - && subImage.y == 0 - && subImage.width == width - && subImage.height == height) { + && subImage.y == 0 + && subImage.width == width + && subImage.height == height) { subImage = null; } } @@ -891,32 +887,70 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable { // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); final TiffField predictorField = directory.findField( - TiffTagConstants.TIFF_TAG_PREDICTOR); + TiffTagConstants.TIFF_TAG_PREDICTOR); if (null != predictorField) { predictor = predictorField.getIntValueOrArraySum(); } } - if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) { - throw new ImageReadException( - "TIFF floating-point data uses unsupported horizontal-differencing predictor"); + if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) { + + if (samplesPerPixel != 1) { + throw new ImageReadException( + "TIFF floating-point data uses unsupported samples per pixel: " + + samplesPerPixel); + } + + if (bitsPerPixel != 32 && bitsPerPixel != 64) { + throw new ImageReadException( + "TIFF floating-point data uses unsupported bits-per-pixel: " + + bitsPerPixel); + } + + if (predictor != -1 + && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE + && predictor != TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING) { + throw new ImageReadException( + "TIFF floating-point data uses unsupported horizontal-differencing predictor"); + } + } else if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER) { + + if (samplesPerPixel != 1) { + throw new ImageReadException( + "TIFF integer data uses unsupported samples per pixel: " + + samplesPerPixel); + } + + if (bitsPerPixel != 16) { + throw new ImageReadException( + "TIFF integer data uses unsupported bits-per-pixel: " + + bitsPerPixel); + } + + if (predictor != -1 + && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE + && predictor != TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) { + throw new ImageReadException( + "TIFF integer data uses unsupported horizontal-differencing predictor"); + } + } else { + throw new ImageReadException("TIFF does not provide a supported raster-data format"); } // The photometric interpreter is not used, but the image-based // data reader classes require one. So we create a dummy interpreter. final PhotometricInterpreter photometricInterpreter - = new PhotometricInterpreterBiLevel(samplesPerPixel, - bitsPerSample, predictor, width, height, false); + = new PhotometricInterpreterBiLevel(samplesPerPixel, + bitsPerSample, predictor, width, height, false); final TiffImageData imageData = directory.getTiffImageData(); final ImageDataReader dataReader = imageData.getDataReader(directory, - photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, - samplesPerPixel, width, height, compression, - TiffPlanarConfiguration.CHUNKY, byteOrder); + photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, + samplesPerPixel, width, height, compression, + TiffPlanarConfiguration.CHUNKY, byteOrder); return dataReader.readRasterData(subImage); } - } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java index 9574d59..1966034 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java @@ -17,16 +17,21 @@ package org.apache.commons.imaging.formats.tiff; /** - * Provides a simple container for floating-point data. Some TIFF files are used - * to store floating-point data rather than images. This class is intended to - * support access to those TIFF files. + * Provides a simple container for numeric-raster data. Some TIFF files are used + * to store floating-point or integer data rather than images. This class is + * intended to support access to those TIFF files. + * <p> + * <strong>Note:</strong> The getData() and getIntData() methods can return + * direct references to the internal arrays stored in instances of this class. + * Because these are not safe copies of the data, an application that + * modified the arrays returned by these methods will change the content + * of the associated instance. This approach is used for purposes of efficiency + * when dealing with very large TIFF images. */ -public class TiffRasterData { +public abstract class TiffRasterData { - - private final int width; - private final int height; - private final float[] data; + protected final int width; + protected final int height; /** * Construct an instance allocating memory for the specified dimensions. @@ -37,38 +42,46 @@ public class TiffRasterData { public TiffRasterData(final int width, final int height) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); + "Raster dimensions less than or equal to zero are not supported"); } - final int nCells = width * height; - data = new float[nCells]; this.width = width; this.height = height; + } + protected final int checkCoordinatesAndComputeIndex(final int x, final int y) { + if (x < 0 || x >= width || y < 0 || y >= height) { + throw new IllegalArgumentException( + "Coordinates out of range (" + x + ", " + y + ")"); + } + return y * width + x; } /** - * Construct an instance allocating memory for the specified dimensions. + * Gets the width (number of columns) of the raster. * - * @param width a value of 1 or greater - * @param height a value of 1 or greater - * @param data the data to be stored in the raster. + * @return the width of the raster */ - public TiffRasterData(final int width, final int height, final float[] data) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); - } - if (data == null || data.length < width * height) { - throw new IllegalArgumentException( - "Specified data does not contain sufficient elements"); - } - this.width = width; - this.height = height; - this.data = data; + public final int getWidth() { + return width; + } + /** + * Gets the height (number of rows) of the raster. + * + * @return the height of the raster. + */ + public final int getHeight() { + return height; } /** + * Gets the raster data type from the instance. + * + * @return a valid enumeration value. + */ + public abstract TiffRasterDataType getDataType(); + + /** * Sets the value stored at the specified raster coordinates. * * @param x integer coordinate in the columnar direction @@ -76,13 +89,7 @@ public class TiffRasterData { * @param value the value to be stored at the specified location; * potentially a Float.NaN. */ - public void setValue(final int x, final int y, final float value) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - data[y * width + x] = value; - } + public abstract void setValue(int x, int y, float value); /** * Gets the value stored at the specified raster coordinates. @@ -92,23 +99,34 @@ public class TiffRasterData { * @return the value stored at the specified location; potentially a * Float.NaN. */ - public float getValue(final int x, final int y) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - return data[y * width + x]; - } + public abstract float getValue(int x, int y); + + /** + * Sets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @param value the value to be stored at the specified location. + */ + public abstract void setIntValue(int x, int y, int value); + + /** + * Gets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @return the value stored at the specified location + */ + public abstract int getIntValue(int x, int y); /** * Tabulates simple statistics for the raster and returns an instance * containing general metadata. * * @return a valid instance containing a safe copy of the current simple - * statistics for the raster. */ - public TiffRasterStatistics getSimpleStatistics() { - return new TiffRasterStatistics(this, Float.NaN); - } + * statistics for the raster. + */ + public abstract TiffRasterStatistics getSimpleStatistics(); /** * Tabulates simple statistics for the raster excluding the specified value @@ -117,42 +135,37 @@ public class TiffRasterData { * @param valueToExclude exclude samples with this specified value. * @return a valid instance. */ - public TiffRasterStatistics getSimpleStatistics(final float valueToExclude) { - return new TiffRasterStatistics(this, valueToExclude); - } + public abstract TiffRasterStatistics getSimpleStatistics(float valueToExclude); /** - * Gets the width (number of columns) of the raster. + * Returns the content stored as an array in this instance. Note that in + * many cases, the returned array is <strong>not</strong> a safe copy of the + * data but a direct reference to the member element. In such cases, + * modifying it would directly affect the content of the instance. While + * this design approach carries some risk in terms of data security, it was + * chosen for reasons of performance and memory conservation. TIFF images + * that contain floating-point data are often quite large. Sizes of 100 + * million raster cells are common. Making a redundant copy of such a large + * in-memory object might exceed the resources available to a Java + * application. * - * @return the width of the raster + * @return the data content stored in this instance. */ - public int getWidth() { - return width; - } + public abstract float[] getData(); /** - * Gets the height (number of rows) of the raster. + * Returns the content stored as an array in this instance. Note that in + * many cases, the returned array is <strong>not</strong> a safe copy of the + * data but a direct reference to the member element. In such cases, + * modifying it would directly affect the content of the instance. While + * this design approach carries some risk in terms of data security, it was + * chosen for reasons of performance and memory conservation. TIFF images + * that contain floating-point data are often quite large. Sizes of 100 + * million raster cells are common. Making a redundant copy of such a large + * in-memory object might exceed the resources available to a Java + * application. * - * @return the height of the raster. + * @return the data content stored in this instance. */ - public int getHeight() { - return height; - } - - /** - * Returns a reference to the data array stored in this instance. Note that - * value is <strong>not</strong> a safe copy and that modifying it would - * directly affect the content of the instance. While this design approach - * carries some risk in terms of data security, it was chosen for reasons of - * performance and memory conservation. TIFF images that contain - * floating-point data are often quite large. Sizes of 100 million raster - * cells are common. Making a redundant copy of such a large in-memory object - * might exceed the resources available to a Java application. - * - * @return a direct reference to the data array stored in this instance. - */ - public float[] getData() { - return data; - } - + public abstract int[] getIntData(); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataFloat.java similarity index 59% copy from src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java copy to src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataFloat.java index 9574d59..e7454ea 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataFloat.java @@ -16,16 +16,23 @@ */ package org.apache.commons.imaging.formats.tiff; +import java.util.stream.IntStream; + /** * Provides a simple container for floating-point data. Some TIFF files are used * to store floating-point data rather than images. This class is intended to * support access to those TIFF files. + * <p> + * <strong>Note:</strong> The getData() and getIntData() methods can return + * direct references to the internal arrays stored in instances of this class. + * Because these are not safe copies of the data, an application that + * modified the arrays returned by these methods will change the content + * of the associated instance. This approach is used for purposes of efficiency + * when dealing with very large TIFF images. */ -public class TiffRasterData { +public class TiffRasterDataFloat extends TiffRasterData { - private final int width; - private final int height; private final float[] data; /** @@ -34,16 +41,10 @@ public class TiffRasterData { * @param width a value of 1 or greater * @param height a value of 1 or greater */ - public TiffRasterData(final int width, final int height) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); - } + public TiffRasterDataFloat(final int width, final int height) { + super(width, height); final int nCells = width * height; data = new float[nCells]; - this.width = width; - this.height = height; - } /** @@ -53,21 +54,27 @@ public class TiffRasterData { * @param height a value of 1 or greater * @param data the data to be stored in the raster. */ - public TiffRasterData(final int width, final int height, final float[] data) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); - } + public TiffRasterDataFloat(final int width, final int height, final float[] data) { + super(width, height); + if (data == null || data.length < width * height) { throw new IllegalArgumentException( "Specified data does not contain sufficient elements"); } - this.width = width; - this.height = height; this.data = data; + } + /** + * Gets the raster data type from the instance. + * + * @return a value of TiffRasterDataType.FLOAT. + */ + @Override + public TiffRasterDataType getDataType() { + return TiffRasterDataType.FLOAT; } + /** * Sets the value stored at the specified raster coordinates. * @@ -76,12 +83,10 @@ public class TiffRasterData { * @param value the value to be stored at the specified location; * potentially a Float.NaN. */ + @Override public void setValue(final int x, final int y, final float value) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - data[y * width + x] = value; + int index = checkCoordinatesAndComputeIndex(x, y); + data[index] = value; } /** @@ -92,12 +97,37 @@ public class TiffRasterData { * @return the value stored at the specified location; potentially a * Float.NaN. */ + @Override public float getValue(final int x, final int y) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - return data[y * width + x]; + int index = checkCoordinatesAndComputeIndex(x, y); + return data[index]; + } + + + /** + * Sets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @param value the value to be stored at the specified location + */ + @Override + public void setIntValue(final int x, final int y, final int value) { + int index = checkCoordinatesAndComputeIndex(x, y); + data[index] = value; + } + + /** + * Gets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @return the value stored at the specified location + */ + @Override + public int getIntValue(final int x, final int y) { + int index = checkCoordinatesAndComputeIndex(x, y); + return (int)data[index]; } /** @@ -105,7 +135,9 @@ public class TiffRasterData { * containing general metadata. * * @return a valid instance containing a safe copy of the current simple - * statistics for the raster. */ + * statistics for the raster. + */ + @Override public TiffRasterStatistics getSimpleStatistics() { return new TiffRasterStatistics(this, Float.NaN); } @@ -117,32 +149,15 @@ public class TiffRasterData { * @param valueToExclude exclude samples with this specified value. * @return a valid instance. */ + @Override public TiffRasterStatistics getSimpleStatistics(final float valueToExclude) { return new TiffRasterStatistics(this, valueToExclude); } /** - * Gets the width (number of columns) of the raster. - * - * @return the width of the raster - */ - public int getWidth() { - return width; - } - - /** - * Gets the height (number of rows) of the raster. - * - * @return the height of the raster. - */ - public int getHeight() { - return height; - } - - /** * Returns a reference to the data array stored in this instance. Note that - * value is <strong>not</strong> a safe copy and that modifying it would - * directly affect the content of the instance. While this design approach + * the array returned is <strong>not</strong> a safe copy and that modifying + * it directly affects the content of the instance. While this design approach * carries some risk in terms of data security, it was chosen for reasons of * performance and memory conservation. TIFF images that contain * floating-point data are often quite large. Sizes of 100 million raster @@ -151,8 +166,25 @@ public class TiffRasterData { * * @return a direct reference to the data array stored in this instance. */ + @Override public float[] getData() { return data; } + /** + * Returns an array of integer approximations for the floating-point content + * stored as an array in this instance. + * + * @return the integer equivalents to the data content stored + * in this instance. + */ + @Override + public int[] getIntData() { + final int nCells = width * height; + return IntStream.range(0, nCells) + .map(i -> (int) data[i]) + .toArray(); + } + + } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java similarity index 58% copy from src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java copy to src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java index 9574d59..913b2a1 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterData.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java @@ -20,13 +20,18 @@ package org.apache.commons.imaging.formats.tiff; * Provides a simple container for floating-point data. Some TIFF files are used * to store floating-point data rather than images. This class is intended to * support access to those TIFF files. + * <p> + * <strong>Note:</strong> The getData() and getIntData() methods can return + * direct references to the internal arrays stored in instances of this class. + * Because these are not safe copies of the data, an application that + * modified the arrays returned by these methods will change the content + * of the associated instance. This approach is used for purposes of efficiency + * when dealing with very large TIFF images. */ -public class TiffRasterData { +public class TiffRasterDataInt extends TiffRasterData { - private final int width; - private final int height; - private final float[] data; + private final int[] data; /** * Construct an instance allocating memory for the specified dimensions. @@ -34,16 +39,10 @@ public class TiffRasterData { * @param width a value of 1 or greater * @param height a value of 1 or greater */ - public TiffRasterData(final int width, final int height) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); - } + public TiffRasterDataInt(final int width, final int height) { + super(width, height); final int nCells = width * height; - data = new float[nCells]; - this.width = width; - this.height = height; - + data = new int[nCells]; } /** @@ -53,21 +52,27 @@ public class TiffRasterData { * @param height a value of 1 or greater * @param data the data to be stored in the raster. */ - public TiffRasterData(final int width, final int height, final float[] data) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException( - "Raster dimensions less than or equal to zero are not supported"); - } + public TiffRasterDataInt(final int width, final int height, final int[] data) { + super(width, height); + if (data == null || data.length < width * height) { throw new IllegalArgumentException( "Specified data does not contain sufficient elements"); } - this.width = width; - this.height = height; this.data = data; + } + /** + * Gets the raster data type from the instance. + * + * @return a value of TiffRasterDataType.FLOAT. + */ + @Override + public TiffRasterDataType getDataType() { + return TiffRasterDataType.INTEGER; } + /** * Sets the value stored at the specified raster coordinates. * @@ -76,12 +81,10 @@ public class TiffRasterData { * @param value the value to be stored at the specified location; * potentially a Float.NaN. */ + @Override public void setValue(final int x, final int y, final float value) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - data[y * width + x] = value; + int index = checkCoordinatesAndComputeIndex(x, y); + data[index] = (int)value; } /** @@ -92,20 +95,48 @@ public class TiffRasterData { * @return the value stored at the specified location; potentially a * Float.NaN. */ + @Override public float getValue(final int x, final int y) { - if (x < 0 || x >= width || y < 0 || y >= height) { - throw new IllegalArgumentException( - "Coordinates out of range (" + x + ", " + y + ")"); - } - return data[y * width + x]; + int index = checkCoordinatesAndComputeIndex(x, y); + return data[index]; + } + + + /** + * Sets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @param value the value to be stored at the specified location + */ + @Override + public void setIntValue(final int x, final int y, final int value) { + int index = checkCoordinatesAndComputeIndex(x, y); + data[index] = value; + } + + /** + * Gets the value stored at the specified raster coordinates. + * + * @param x integer coordinate in the columnar direction + * @param y integer coordinate in the row direction + * @return the value stored at the specified location + */ + @Override + public int getIntValue(final int x, final int y) { + int index = checkCoordinatesAndComputeIndex(x, y); + return data[index]; } + + /** * Tabulates simple statistics for the raster and returns an instance * containing general metadata. * * @return a valid instance containing a safe copy of the current simple * statistics for the raster. */ + @Override public TiffRasterStatistics getSimpleStatistics() { return new TiffRasterStatistics(this, Float.NaN); } @@ -117,32 +148,16 @@ public class TiffRasterData { * @param valueToExclude exclude samples with this specified value. * @return a valid instance. */ + @Override public TiffRasterStatistics getSimpleStatistics(final float valueToExclude) { return new TiffRasterStatistics(this, valueToExclude); } - /** - * Gets the width (number of columns) of the raster. - * - * @return the width of the raster - */ - public int getWidth() { - return width; - } - - /** - * Gets the height (number of rows) of the raster. - * - * @return the height of the raster. - */ - public int getHeight() { - return height; - } /** * Returns a reference to the data array stored in this instance. Note that - * value is <strong>not</strong> a safe copy and that modifying it would - * directly affect the content of the instance. While this design approach + * the array returned is <strong>not</strong> a safe copy and that modifying + * it directly affects the content of the instance. While this design approach * carries some risk in terms of data security, it was chosen for reasons of * performance and memory conservation. TIFF images that contain * floating-point data are often quite large. Sizes of 100 million raster @@ -151,8 +166,27 @@ public class TiffRasterData { * * @return a direct reference to the data array stored in this instance. */ - public float[] getData() { + @Override + public int [] getIntData() { return data; } + /** + * Returns an array of floating-point equivalents to the integer + * values stored in this instance. To do so, a float array is + * allocated and each integer value in the source + * data is cast to a float. + * + * @return the floating-point equivalents of the content stored + * in this instance. + */ + @Override + public float[] getData() { + final int nCells = width * height; + final float[] result = new float[nCells]; + for (int i = 0; i < nCells; i++) { + result[i] = (int) data[i]; + } + return result; + } } diff --git a/src/test/java/org/apache/commons/imaging/TestImageReadException.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataType.java similarity index 50% copy from src/test/java/org/apache/commons/imaging/TestImageReadException.java copy to src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataType.java index f0adf5c..21ca2ec 100644 --- a/src/test/java/org/apache/commons/imaging/TestImageReadException.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataType.java @@ -14,30 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.commons.imaging; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.junit.jupiter.api.Test; +package org.apache.commons.imaging.formats.tiff; /** - * Tests for {@link ImageReadException}. + * Provides an enumeration indicating the type of data for + * an instance of a TiffRasterData class. */ -public class TestImageReadException { - - @Test - public void testCreateExceptionWithMessage() { - final ImageReadException exception = new ImageReadException("imaging"); - assertEquals("imaging", exception.getMessage()); - assertNull(exception.getCause()); - } +public enum TiffRasterDataType { + /** + * Indicates that the raster contains integer data. + * Attempts to access floating-point data from the raster + * will result in the nearest floating point value. + */ + INTEGER, - @Test - public void testCreateExceptionWithMessageAndCause() { - final ImageReadException exception = new ImageReadException("imaging", new Exception("cause")); - assertEquals("imaging", exception.getMessage()); - assertNotNull(exception.getCause()); - } + /** + * Indicates that the raster contains single-precision floating-point + * data. Attempts to access integer data from the raster + * may result in a truncated value. + */ + FLOAT; } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java index 27eeabb..7b3bf89 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java @@ -358,7 +358,7 @@ public final class TiffTagConstants { public static final int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT = 3; public static final int SAMPLE_FORMAT_VALUE_UNDEFINED = 4; public static final int SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER = 5; - public static final int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT_1 = 6; + public static final int SAMPLE_FORMAT_VALUE_IEEE_COMPLEX_FLOAT = 6; public static final TagInfoAny TIFF_TAG_SMIN_SAMPLE_VALUE = new TagInfoAny( "SMinSampleValue", 0x154, -1, diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java index 6f3a159..e235754 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java @@ -27,6 +27,8 @@ import org.apache.commons.imaging.common.ImageBuilder; import org.apache.commons.imaging.formats.tiff.TiffRasterData; import org.apache.commons.imaging.formats.tiff.TiffDirectory; import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat; +import org.apache.commons.imaging.formats.tiff.TiffRasterDataInt; import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration; import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; @@ -352,8 +354,20 @@ public final class DataReaderStrips extends ImageDataReader { @Override public TiffRasterData readRasterData(final Rectangle subImage) - throws ImageReadException, IOException { + throws ImageReadException, IOException { + switch(sampleFormat){ + case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT: + return readRasterDataFloat(subImage); + case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER: + return readRasterDataInt(subImage); + default: + throw new ImageReadException("Unsupported sample format, value=" + +sampleFormat); + } + } + private TiffRasterData readRasterDataFloat(final Rectangle subImage) + throws ImageReadException, IOException { int xRaster; int yRaster; int rasterWidth; @@ -369,7 +383,8 @@ public final class DataReaderStrips extends ImageDataReader { rasterWidth = width; rasterHeight = height; } - final float[] rasterData = new float[rasterWidth * rasterHeight]; + + float[] rasterDataFloat = new float[rasterWidth * rasterHeight]; // the legacy code is optimized to the reading of whole // strips (except for the last strip in the image, which can @@ -390,16 +405,69 @@ public final class DataReaderStrips extends ImageDataReader { final byte[] compressed = imageData.getImageData(strip).getData(); final byte[] decompressed = decompress(compressed, compression, - bytesPerStrip, width, rowsInThisStrip); + bytesPerStrip, width, rowsInThisStrip); final int[] blockData = unpackFloatingPointSamples( - width, (int) rowsInThisStrip, width, - decompressed, - predictor, bitsPerPixel, byteOrder); + width, + rowsInThisStrip, + width, + decompressed, + predictor, bitsPerPixel, byteOrder); transferBlockToRaster(0, yStrip, width, (int) rowsInThisStrip, blockData, - xRaster, yRaster, rasterWidth, rasterHeight, rasterData); + xRaster, yRaster, rasterWidth, rasterHeight, rasterDataFloat); } - return new TiffRasterData(rasterWidth, rasterHeight, rasterData); + return new TiffRasterDataFloat(rasterWidth, rasterHeight, rasterDataFloat); } + private TiffRasterData readRasterDataInt(final Rectangle subImage) + throws ImageReadException, IOException { + int xRaster; + int yRaster; + int rasterWidth; + int rasterHeight; + if (subImage != null) { + xRaster = subImage.x; + yRaster = subImage.y; + rasterWidth = subImage.width; + rasterHeight = subImage.height; + } else { + xRaster = 0; + yRaster = 0; + rasterWidth = width; + rasterHeight = height; + } + + int[] rasterDataInt = new int[rasterWidth * rasterHeight]; + + // the legacy code is optimized to the reading of whole + // strips (except for the last strip in the image, which can + // be a partial). So create a working image with compatible + // dimensions and read that. Later on, the working image + // will be sub-imaged to the proper size. + // strip0 and strip1 give the indices of the strips containing + // the first and last rows of pixels in the subimage + final int strip0 = yRaster / rowsPerStrip; + final int strip1 = (yRaster + rasterHeight - 1) / rowsPerStrip; + + for (int strip = strip0; strip <= strip1; strip++) { + final int yStrip = strip * rowsPerStrip; + final int rowsRemaining = height - yStrip; + final int rowsInThisStrip = Math.min(rowsRemaining, rowsPerStrip); + final int bytesPerRow = (bitsPerPixel * width + 7) / 8; + final int bytesPerStrip = rowsInThisStrip * bytesPerRow; + + final byte[] compressed = imageData.getImageData(strip).getData(); + final byte[] decompressed = decompress(compressed, compression, + bytesPerStrip, width, rowsInThisStrip); + final int[] blockData = unpackIntSamples( + width, + rowsInThisStrip, + width, + decompressed, + predictor, bitsPerPixel, byteOrder); + transferBlockToRaster(0, yStrip, width, rowsInThisStrip, blockData, + xRaster, yRaster, rasterWidth, rasterHeight, rasterDataInt); + } + return new TiffRasterDataInt(rasterWidth, rasterHeight, rasterDataInt); + } } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java index d745301..8912589 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java @@ -33,6 +33,8 @@ import org.apache.commons.imaging.common.ImageBuilder; import org.apache.commons.imaging.formats.tiff.TiffRasterData; import org.apache.commons.imaging.formats.tiff.TiffDirectory; import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat; +import org.apache.commons.imaging.formats.tiff.TiffRasterDataInt; import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration; import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; @@ -286,7 +288,20 @@ public final class DataReaderTiled extends ImageDataReader { @Override public TiffRasterData readRasterData(final Rectangle subImage) - throws ImageReadException, IOException { + throws ImageReadException, IOException { + switch (sampleFormat) { + case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT: + return readRasterDataFloat(subImage); + case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER: + return readRasterDataInt(subImage); + default: + throw new ImageReadException("Unsupported sample format, value=" + + sampleFormat); + } + } + + private TiffRasterData readRasterDataFloat(final Rectangle subImage) + throws ImageReadException, IOException { final int bitsPerRow = tileWidth * bitsPerPixel; final int bytesPerRow = (bitsPerRow + 7) / 8; final int bytesPerTile = bytesPerRow * tileLength; @@ -305,7 +320,7 @@ public final class DataReaderTiled extends ImageDataReader { rasterWidth = width; rasterHeight = height; } - final float[] rasterData = new float[rasterWidth * rasterHeight]; + float[] rasterDataFloat = new float[rasterWidth * rasterHeight]; // tileWidth is the width of the tile // tileLength is the height of the tile @@ -321,19 +336,68 @@ public final class DataReaderTiled extends ImageDataReader { final int tile = iRow * nColumnsOfTiles + iCol; final byte[] compressed = imageData.tiles[tile].getData(); final byte[] decompressed = decompress(compressed, compression, - bytesPerTile, tileWidth, tileLength); + bytesPerTile, tileWidth, tileLength); final int x = iCol * tileWidth; final int y = iRow * tileLength; + final int[] blockData = unpackFloatingPointSamples( - tileWidth, tileLength, tileWidth, - decompressed, - predictor, bitsPerPixel, byteOrder); + tileWidth, tileLength, tileWidth, + decompressed, + predictor, bitsPerPixel, byteOrder); transferBlockToRaster(x, y, tileWidth, tileLength, blockData, - xRaster, yRaster, rasterWidth, rasterHeight, rasterData); + xRaster, yRaster, rasterWidth, rasterHeight, rasterDataFloat); } } - - return new TiffRasterData(rasterWidth, rasterHeight, rasterData); + return new TiffRasterDataFloat(rasterWidth, rasterHeight, rasterDataFloat); } + private TiffRasterData readRasterDataInt(final Rectangle subImage) + throws ImageReadException, IOException { + final int bitsPerRow = tileWidth * bitsPerPixel; + final int bytesPerRow = (bitsPerRow + 7) / 8; + final int bytesPerTile = bytesPerRow * tileLength; + int xRaster; + int yRaster; + int rasterWidth; + int rasterHeight; + if (subImage != null) { + xRaster = subImage.x; + yRaster = subImage.y; + rasterWidth = subImage.width; + rasterHeight = subImage.height; + } else { + xRaster = 0; + yRaster = 0; + rasterWidth = width; + rasterHeight = height; + } + int[] rasterDataInt = new int[rasterWidth * rasterHeight]; + + // tileWidth is the width of the tile + // tileLength is the height of the tile + final int col0 = xRaster / tileWidth; + final int col1 = (xRaster + rasterWidth - 1) / tileWidth; + final int row0 = yRaster / tileLength; + final int row1 = (yRaster + rasterHeight - 1) / tileLength; + + final int nColumnsOfTiles = (width + tileWidth - 1) / tileWidth; + + for (int iRow = row0; iRow <= row1; iRow++) { + for (int iCol = col0; iCol <= col1; iCol++) { + final int tile = iRow * nColumnsOfTiles + iCol; + final byte[] compressed = imageData.tiles[tile].getData(); + final byte[] decompressed = decompress(compressed, compression, + bytesPerTile, tileWidth, tileLength); + final int x = iCol * tileWidth; + final int y = iRow * tileLength; + final int[] blockData = unpackIntSamples( + tileWidth, tileLength, tileWidth, + decompressed, + predictor, bitsPerPixel, byteOrder); + transferBlockToRaster(x, y, tileWidth, tileLength, blockData, + xRaster, yRaster, rasterWidth, rasterHeight, rasterDataInt); + } + } + return new TiffRasterDataInt(rasterWidth, rasterHeight, rasterDataInt); + } } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java index ba6df5a..d6ab52e 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java @@ -142,7 +142,6 @@ import org.apache.commons.imaging.formats.tiff.photometricinterpreters.Photometr * is implemented, it should get their own block of code so as not to interfere * with the processing of the more common non-interleaved variations. */ -@SuppressWarnings("PMD.TooManyStaticImports") public abstract class ImageDataReader { protected final TiffDirectory directory; protected final PhotometricInterpreter photometricInterpreter; @@ -172,7 +171,6 @@ public abstract class ImageDataReader { last = new int[samplesPerPixel]; } - /** * Read the image data from the IFD associated with this * instance of ImageDataReader using the optional sub-image specification @@ -211,11 +209,9 @@ public abstract class ImageDataReader { /** * Reads samples and returns them in an int array. * - * @param bis - * the stream to read from - * @param result - * the samples array to populate, must be the same length as - * bitsPerSample.length + * @param bis the stream to read from + * @param result the samples array to populate, must be the same length as + * bitsPerSample.length * @throws IOException */ void getSamplesAsBytes(final BitInputStream bis, final int[] result) throws IOException { @@ -371,7 +367,7 @@ public abstract class ImageDataReader { * * @param width the width of the data block to be extracted * @param height the height of the data block to be extracted - * @param scansize the number of pixels in a single row of the block + * @param scanSize the number of pixels in a single row of the block * @param bytes the raw bytes * @param predictor the predictor specified by the source, only predictor 3 * is supported. @@ -384,16 +380,17 @@ public abstract class ImageDataReader { protected int[] unpackFloatingPointSamples( final int width, final int height, - final int scansize, + final int scanSize, final byte[] bytes, final int predictor, - final int bitsPerSample, final ByteOrder byteOrder) + final int bitsPerSample, + final ByteOrder byteOrder) throws ImageReadException { final int bytesPerSample = bitsPerSample / 8; - final int nBytes = bytesPerSample * scansize * height; - final int length = bytes.length < nBytes ? nBytes / scansize : height; + final int nBytes = bytesPerSample * scanSize * height; + final int length = bytes.length < nBytes ? nBytes / scanSize : height; - final int[] samples = new int[scansize * height]; + final int[] samples = new int[scanSize * height]; // floating-point differencing is indicated by a predictor value of 3. if (predictor == TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING) { // at this time, this class supports the 32-bit format. The @@ -405,12 +402,12 @@ public abstract class ImageDataReader { + " with predictor type 3 for " + bitsPerSample + " bits per sample"); } - final int bytesInRow = scansize * 4; + final int bytesInRow = scanSize * 4; for (int i = 0; i < length; i++) { final int aOffset = i * bytesInRow; - final int bOffset = aOffset + scansize; - final int cOffset = bOffset + scansize; - final int dOffset = cOffset + scansize; + final int bOffset = aOffset + scanSize; + final int cOffset = bOffset + scanSize; + final int dOffset = cOffset + scanSize; // in this loop, the source bytes give delta values. // we adjust them to give true values. This operation is // done on a row-by-row basis. @@ -419,7 +416,7 @@ public abstract class ImageDataReader { } // pack the bytes into the integer bit-equivalent of // floating point values - int index = i * scansize; + int index = i * scanSize; for (int j = 0; j < width; j++) { final int a = bytes[aOffset + j]; final int b = bytes[bOffset + j]; @@ -441,7 +438,7 @@ public abstract class ImageDataReader { int k = 0; int index = 0; for (int i = 0; i < length; i++) { - for (int j = 0; j < scansize; j++) { + for (int j = 0; j < scanSize; j++) { final long b0 = bytes[k++] & 0xffL; final long b1 = bytes[k++] & 0xffL; final long b2 = bytes[k++] & 0xffL; @@ -483,7 +480,7 @@ public abstract class ImageDataReader { int k = 0; int index = 0; for (int i = 0; i < length; i++) { - for (int j = 0; j < scansize; j++) { + for (int j = 0; j < scanSize; j++) { final int b0 = bytes[k++] & 0xff; final int b1 = bytes[k++] & 0xff; final int b2 = bytes[k++] & 0xff; @@ -519,8 +516,80 @@ public abstract class ImageDataReader { return samples; } + /** + * Given a source file that specifies numerical data as short integers, unpack + * the raw bytes obtained from the source file and organize them into an + * array of integers. + * <p> + * This method supports either the tile format or the strip format of TIFF + * source files. The scan size indicates the number of columns to be + * extracted. For strips, the width and the scan size are always the full + * width of the image. For tiles, the scan size is the full width of the + * tile, but the width may be smaller in the cases where the tiles do not + * evenly divide the width (for example, a 256 pixel wide tile in a 257 + * pixel wide image would result in two columns of tiles, the second column + * having only one column of pixels that were worth extracting. * + * @param width the width of the data block to be extracted + * @param height the height of the data block to be extracted + * @param scanSize the number of pixels in a single row of the block + * @param bytes the raw bytes + * @param predictor the predictor specified by the source, only predictor 3 + * is supported. + * @param bitsPerSample the number of bits per sample, 32 or 64. + * @param byteOrder the byte order for the source data + * @return a valid array of integers in row major order, dimensions + * scan-size wide and height height. + */ + protected int[] unpackIntSamples( + final int width, + final int height, + final int scanSize, + final byte[] bytes, + final int predictor, + final int bitsPerSample, + final ByteOrder byteOrder) { + final int bytesPerSample = bitsPerSample / 8; + final int nBytes = bytesPerSample * scanSize * height; + final int length = bytes.length < nBytes ? nBytes / scanSize : height; + + final int[] samples = new int[scanSize * height]; + // At this time, Commons Imaging only supports two-byte + // two's complement short integers. It is assumed that + // the calling module already checked the arguments for + // compliance, so this method simply assumes that they are correct. + + // The logic that follows is simplified by the fact that + // the existing API only supports two-byte signed integers. + boolean useDifferencing + = predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING; + + for (int i = 0; i < length; i++) { + int index = i * scanSize; + int offset = index * 2; + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + for (int j = 0; j < width; j++, offset += 2) { + samples[index + j] = (bytes[offset + 1] << 8) | (bytes[offset] & 0xff); + } + } else { + for (int j = 0; j < width; j++, offset += 2) { + samples[index + j] = (bytes[offset] << 8) | (bytes[offset + 1] & 0xff); + } + } + if (useDifferencing) { + for (int j = 1; j < width; j++) { + samples[index + j] += samples[index + j - 1]; + } + } + } + + return samples; + } + + /** + * Transfer samples obtained from the TIFF file to a floating-point + * raster. * @param xBlock coordinate of block relative to source data * @param yBlock coordinate of block relative to source data * @param blockWidth width of block, in pixels @@ -603,6 +672,87 @@ public abstract class ImageDataReader { } /** + * Transfer samples obtained from the TIFF file to an integer raster. + * @param xBlock coordinate of block relative to source data + * @param yBlock coordinate of block relative to source data + * @param blockWidth width of block, in pixels + * @param blockHeight height of block in pixels + * @param blockData the data for the block + * @param xRaster coordinate of raster relative to source data + * @param yRaster coordinate of raster relative to source data + * @param rasterWidth width of the raster (always smaller than source data) + * @param rasterHeight height of the raster (always smaller than source + * data) + * @param rasterData the raster data. + */ + void transferBlockToRaster(final int xBlock, final int yBlock, + final int blockWidth, final int blockHeight, final int[] blockData, + final int xRaster, final int yRaster, + final int rasterWidth, final int rasterHeight, final int[] rasterData) { + + // xR0, yR0 are the coordinates within the raster (upper-left corner) + // xR1, yR1 are ONE PAST the coordinates of the lower-right corner + int xR0 = xBlock - xRaster; // xR0, yR0 coordinates relative to + int yR0 = yBlock - yRaster; // the raster + int xR1 = xR0 + blockWidth; + int yR1 = yR0 + blockHeight; + if (xR0 < 0) { + xR0 = 0; + } + if (yR0 < 0) { + yR0 = 0; + } + if (xR1 > rasterWidth) { + xR1 = rasterWidth; + } + if (yR1 > rasterHeight) { + yR1 = rasterHeight; + } + + // Recall that the above logic may have adjusted xR0, xY0 so that + // they are not necessarily point to the source pixel at xRaster, yRaster + // we compute xSource = xR0+xRaster. + // xOffset = xSource-xBlock + // since the block cannot be accessed with a negative offset, + // we check for negatives and adjust xR0, yR0 upward as necessary + int xB0 = xR0 + xRaster - xBlock; + int yB0 = yR0 + yRaster - yBlock; + if (xB0 < 0) { + xR0 -= xB0; + xB0 = 0; + } + if (yB0 < 0) { + yR0 -= yB0; + yB0 = 0; + } + + int w = xR1 - xR0; + int h = yR1 - yR0; + if (w <= 0 || h <= 0) { + // The call to this method puts the block outside the + // bounds of the raster. There is nothing to do. Ideally, + // this situation never arises, because it would mean that + // the data was read from the file unnecessarily. + return; + } + // see if the xR1, yR1 would extend past the limits of the block + if (w > blockWidth) { + w = blockWidth; + } + if (h > blockHeight) { + h = blockHeight; + } + + for (int i = 0; i < h; i++) { + final int yR = yR0 + i; + final int yB = yB0 + i; + final int rOffset = yR * rasterWidth + xR0; + final int bOffset = yB * blockWidth + xB0; + System.arraycopy(blockData, bOffset, rasterData, rOffset, w); + } + } + + /** * Defines a method for accessing the floating-point raster data in a TIFF * image. These implementations of this method in DataReaderStrips and * DataReaderTiled assume that this instance is of a compatible data type diff --git a/src/test/java/org/apache/commons/imaging/TestImageReadException.java b/src/test/java/org/apache/commons/imaging/TestImageReadException.java index f0adf5c..59b50e2 100644 --- a/src/test/java/org/apache/commons/imaging/TestImageReadException.java +++ b/src/test/java/org/apache/commons/imaging/TestImageReadException.java @@ -27,17 +27,17 @@ import org.junit.jupiter.api.Test; */ public class TestImageReadException { - @Test - public void testCreateExceptionWithMessage() { - final ImageReadException exception = new ImageReadException("imaging"); - assertEquals("imaging", exception.getMessage()); - assertNull(exception.getCause()); - } + @Test + public void testCreateExceptionWithMessage() { + final ImageReadException exception = new ImageReadException("imaging"); + assertEquals("imaging", exception.getMessage()); + assertNull(exception.getCause()); + } - @Test - public void testCreateExceptionWithMessageAndCause() { - final ImageReadException exception = new ImageReadException("imaging", new Exception("cause")); - assertEquals("imaging", exception.getMessage()); - assertNotNull(exception.getCause()); - } + @Test + public void testCreateExceptionWithMessageAndCause() { + final ImageReadException exception = new ImageReadException("imaging", new Exception("cause")); + assertEquals("imaging", exception.getMessage()); + assertNotNull(exception.getCause()); + } } diff --git a/src/test/java/org/apache/commons/imaging/TestImageWriteException.java b/src/test/java/org/apache/commons/imaging/TestImageWriteException.java index 6781681..b830f54 100644 --- a/src/test/java/org/apache/commons/imaging/TestImageWriteException.java +++ b/src/test/java/org/apache/commons/imaging/TestImageWriteException.java @@ -31,41 +31,41 @@ import org.junit.jupiter.params.provider.MethodSource; */ public class TestImageWriteException { - public static Stream<Object[]> data() { - final ImageWriteException exception = new ImageWriteException(null); - return Stream.of( - new Object[] {null, "null"}, - new Object[] {new Object[] {Integer.valueOf(1)}, "[Object[]: 1]"}, - new Object[] {new char[] {'a', 'b', 'c'}, "[char[]: 3]"}, - new Object[] {new byte[] {0, 1}, "[byte[]: 2]"}, - new Object[] {new short[] {0}, "[short[]: 1]"}, - new Object[] {new int[] {-1, -2, 4, 100}, "[int[]: 4]"}, - new Object[] {new long[] {-1, -2, 4, 100}, "[long[]: 4]"}, - new Object[] {new float[] {-1.0f, 2.0f}, "[float[]: 2]"}, - new Object[] {new double[] {-1.0d, 2.0d}, "[double[]: 2]"}, - new Object[] {new boolean[] {true, false, true}, "[boolean[]: 3]"}, - new Object[] {exception, exception.getClass().getName()} - ); - } + public static Stream<Object[]> data() { + final ImageWriteException exception = new ImageWriteException(null); + return Stream.of( + new Object[] {null, "null"}, + new Object[] {new Object[] {Integer.valueOf(1)}, "[Object[]: 1]"}, + new Object[] {new char[] {'a', 'b', 'c'}, "[char[]: 3]"}, + new Object[] {new byte[] {0, 1}, "[byte[]: 2]"}, + new Object[] {new short[] {0}, "[short[]: 1]"}, + new Object[] {new int[] {-1, -2, 4, 100}, "[int[]: 4]"}, + new Object[] {new long[] {-1, -2, 4, 100}, "[long[]: 4]"}, + new Object[] {new float[] {-1.0f, 2.0f}, "[float[]: 2]"}, + new Object[] {new double[] {-1.0d, 2.0d}, "[double[]: 2]"}, + new Object[] {new boolean[] {true, false, true}, "[boolean[]: 3]"}, + new Object[] {exception, exception.getClass().getName()} + ); + } - @Test - public void testCreateExceptionWithMessage() { - final ImageWriteException exception = new ImageWriteException("imaging"); - assertEquals("imaging", exception.getMessage()); - assertNull(exception.getCause()); - } + @Test + public void testCreateExceptionWithMessage() { + final ImageWriteException exception = new ImageWriteException("imaging"); + assertEquals("imaging", exception.getMessage()); + assertNull(exception.getCause()); + } - @Test - public void testCreateExceptionWithMessageAndCause() { - final ImageWriteException exception = new ImageWriteException("imaging", new Exception("cause")); - assertEquals("imaging", exception.getMessage()); - assertNotNull(exception.getCause()); - } + @Test + public void testCreateExceptionWithMessageAndCause() { + final ImageWriteException exception = new ImageWriteException("imaging", new Exception("cause")); + assertEquals("imaging", exception.getMessage()); + assertNotNull(exception.getCause()); + } - @ParameterizedTest - @MethodSource("data") - public void testCreateExceptionWithData(final Object data, final String expectedType) { - final ImageWriteException exception = new ImageWriteException("imaging", data); - assertEquals(String.format("imaging: %s (%s)", data, expectedType), exception.getMessage()); - } + @ParameterizedTest + @MethodSource("data") + public void testCreateExceptionWithData(final Object data, final String expectedType) { + final ImageWriteException exception = new ImageWriteException("imaging", data); + assertEquals(String.format("imaging: %s (%s)", data, expectedType), exception.getMessage()); + } } diff --git a/src/test/java/org/apache/commons/imaging/examples/tiff/ExampleReadFloatingPointData.java b/src/test/java/org/apache/commons/imaging/examples/tiff/ExampleReadFloatingPointData.java index 4756f96..0e1aa51 100644 --- a/src/test/java/org/apache/commons/imaging/examples/tiff/ExampleReadFloatingPointData.java +++ b/src/test/java/org/apache/commons/imaging/examples/tiff/ExampleReadFloatingPointData.java @@ -117,7 +117,7 @@ public class ExampleReadFloatingPointData { final long time0Nanos = System.nanoTime(); final HashMap<String, Object> params = new HashMap<>(); final TiffRasterData rasterData - = directory.getFloatingPointRasterData(params); + = directory.getRasterData(params); final long time1Nanos = System.nanoTime(); System.out.println("Data read in " + ((time1Nanos - time0Nanos) / 1.0e+6) + " ms"); diff --git a/src/test/java/org/apache/commons/imaging/examples/tiff/SurveyTiffFile.java b/src/test/java/org/apache/commons/imaging/examples/tiff/SurveyTiffFile.java index 8e2499a..bcedb65 100644 --- a/src/test/java/org/apache/commons/imaging/examples/tiff/SurveyTiffFile.java +++ b/src/test/java/org/apache/commons/imaging/examples/tiff/SurveyTiffFile.java @@ -237,8 +237,9 @@ public class SurveyTiffFile { case TiffTagConstants.SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER: return "Comp I" + heterogeneous; case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT: - case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT_1: return "Float" + heterogeneous; + case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_COMPLEX_FLOAT: + return "Comp F" + heterogeneous; case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER: return "Sgn Int" + heterogeneous; case TiffTagConstants.SAMPLE_FORMAT_VALUE_UNSIGNED_INTEGER: diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffFloatingPointReadTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffFloatingPointReadTest.java index ab6b3c9..b9aebb6 100644 --- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffFloatingPointReadTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffFloatingPointReadTest.java @@ -121,7 +121,7 @@ public class TiffFloatingPointReadTest { true, // indicates that application should read image data, if present FormatCompliance.getDefault()); final TiffDirectory directory = contents.directories.get(0); - return directory.getFloatingPointRasterData(params); + return directory.getRasterData(params); } @Test diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java similarity index 66% copy from src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java copy to src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java index eccaf96..7ff6bfe 100644 --- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java @@ -18,6 +18,8 @@ package org.apache.commons.imaging.formats.tiff; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; @@ -25,17 +27,17 @@ import org.junit.jupiter.api.Test; /** * Provides unit test for the raster-data class. */ -public class TiffRasterDataTest { +public class TiffRasterDataIntTest { int width = 11; int height = 10; - float[] data; + int[] data; TiffRasterData raster; float meanValue; - public TiffRasterDataTest() { + public TiffRasterDataIntTest() { double sum = 0; - data = new float[width * height]; + data = new int[width * height]; int k = 0; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { @@ -44,7 +46,7 @@ public class TiffRasterDataTest { k++; } } - raster = new TiffRasterData(width, height, data); + raster = new TiffRasterDataInt(width, height, data); meanValue = (float) (sum / k); } @@ -53,13 +55,16 @@ public class TiffRasterDataTest { */ @Test public void testSetValue() { - final TiffRasterData instance = new TiffRasterData(width, height); + final TiffRasterData instance = new TiffRasterDataInt(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int index = y * width + height; - instance.setValue(x, y, index); - final int test = (int) instance.getValue(x, y); + instance.setValue(x, y, index+0.4f); + int test = (int) instance.getValue(x, y); assertEquals(index, test, "Set/get value test failed"); + instance.setIntValue(x, y, index); + test = instance.getIntValue(x, y); + assertEquals(index, test, "Set/get int value test failed"); } } } @@ -72,7 +77,9 @@ public class TiffRasterDataTest { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int index = y * width + x; - final int test = (int) raster.getValue(x, y); + int test = (int) raster.getValue(x, y); + assertEquals(index, test, "Get into source data test failed at (" + x + "," + y + ")"); + test = raster.getIntValue(x, y); assertEquals(index, test, "Get into source data test failed at (" + x + "," + y + ")"); } } @@ -123,7 +130,20 @@ public class TiffRasterDataTest { @Test public void testGetData() { final float[] result = raster.getData(); - assertArrayEquals(data, result); + for(int i=0; i<result.length; i++){ + assertEquals((int)result[i], data[i]); + } + final int []iResult = raster.getIntData(); + assertArrayEquals(data, iResult); + } + + /** + * Test of getData method, of class TiffRasterData. + */ + @Test + public void testGetDataType() { + TiffRasterDataType dataType = raster.getDataType(); + assertTrue(dataType == TiffRasterDataType.INTEGER, "Unexpected data type "+dataType.name()); } @@ -132,39 +152,12 @@ public class TiffRasterDataTest { */ @Test public void testBadConstructor() { - try{ - final TiffRasterData raster = new TiffRasterData(-1, 10); - fail("Constructor did not detect bad width"); - }catch(final IllegalArgumentException illArgEx){ - // success! - } - try{ - final TiffRasterData raster = new TiffRasterData(10, -1); - fail("Constructor did not detect bad height"); - }catch(final IllegalArgumentException illArgEx){ - // success! - } - try{ - final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(2, 10, f); - fail("Constructor did not detect insufficient input array size"); - }catch(final IllegalArgumentException illArgEx){ - // success! - } - try{ - final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(-1, 10, f); - fail("Constructor did not detect bad width"); - }catch(final IllegalArgumentException illArgEx){ - // success! - } - try{ - final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(10, -1, f); - fail("Constructor did not detect bad height"); - }catch(final IllegalArgumentException illArgEx){ - // success! - } + final int []sample = new int[10]; + assertThrows(IllegalArgumentException.class, ()-> new TiffRasterDataInt(-1, 10)); + assertThrows(IllegalArgumentException.class, ()-> new TiffRasterDataInt(10, -1)); + assertThrows(IllegalArgumentException.class, ()-> new TiffRasterDataInt(2, 10, sample)); + assertThrows(IllegalArgumentException.class, ()-> new TiffRasterDataInt(-1, 10, sample)); + assertThrows(IllegalArgumentException.class, ()-> new TiffRasterDataInt(10, -1, sample)); } /** @@ -174,18 +167,18 @@ public class TiffRasterDataTest { public void testBadCoordinates() { try{ - final float []f = new float[100]; - final TiffRasterData raster = new TiffRasterData(10, 10, f); - raster.getValue(11, 11); + final int []sample = new int[100]; + final TiffRasterData raster = new TiffRasterDataInt(10, 10, sample); + raster.getIntValue(11, 11); fail("Access method getValue() did not detect bad coordinates"); }catch(final IllegalArgumentException illArgEx){ // success! } try{ - final float []f = new float[100]; - final TiffRasterData raster = new TiffRasterData(10, 10, f); + final int []sample = new int[100]; + final TiffRasterData raster = new TiffRasterDataInt(10, 10, sample); raster.setValue(11, 11, 5.0f); - fail("Access method getValue() did not detect bad coordinates"); + fail("Access method setValue() did not detect bad coordinates"); }catch(final IllegalArgumentException illArgEx){ // success! } diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java index eccaf96..797c93b 100644 --- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataTest.java @@ -18,6 +18,7 @@ package org.apache.commons.imaging.formats.tiff; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; @@ -44,7 +45,7 @@ public class TiffRasterDataTest { k++; } } - raster = new TiffRasterData(width, height, data); + raster = new TiffRasterDataFloat(width, height, data); meanValue = (float) (sum / k); } @@ -53,13 +54,16 @@ public class TiffRasterDataTest { */ @Test public void testSetValue() { - final TiffRasterData instance = new TiffRasterData(width, height); + final TiffRasterData instance = new TiffRasterDataFloat(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int index = y * width + height; instance.setValue(x, y, index); final int test = (int) instance.getValue(x, y); - assertEquals(index, test, "Set/get value test failed"); + assertEquals(index, test, "Set/get value test failed at (" + x + "," + y + ")"); + instance.setIntValue(x, y, index); + final int iTest = instance.getIntValue(x, y); + assertEquals(index, iTest, "Get/set value test failed at (" + x + "," + y + ")"); } } } @@ -74,6 +78,8 @@ public class TiffRasterDataTest { final int index = y * width + x; final int test = (int) raster.getValue(x, y); assertEquals(index, test, "Get into source data test failed at (" + x + "," + y + ")"); + final int iTest = raster.getIntValue(x, y); + assertEquals(index, iTest, "Get into source data test failed at (" + x + "," + y + ")"); } } } @@ -126,41 +132,66 @@ public class TiffRasterDataTest { assertArrayEquals(data, result); } + /** + * Test of getData method, of class TiffRasterData. + */ + @Test + public void testGetIntData() { + final int[] result = raster.getIntData(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int index = y * width + x; + final int test = (int) data[index]; + assertEquals(index, test, "Integer array access test failed at (" + x + "," + y + ")"); + } + } + } - /** + + /** + * Test of getData method, of class TiffRasterData. + */ + @Test + public void testGetDataType() { + TiffRasterDataType dataType = raster.getDataType(); + assertTrue(dataType == TiffRasterDataType.FLOAT, "Unexpected data type "+dataType.name()); + } + + + /** * Test of constructors with bad arguments, of class TiffRasterData. */ @Test public void testBadConstructor() { try{ - final TiffRasterData raster = new TiffRasterData(-1, 10); + final TiffRasterData raster = new TiffRasterDataFloat(-1, 10); fail("Constructor did not detect bad width"); }catch(final IllegalArgumentException illArgEx){ // success! } try{ - final TiffRasterData raster = new TiffRasterData(10, -1); + final TiffRasterData raster = new TiffRasterDataFloat(10, -1); fail("Constructor did not detect bad height"); }catch(final IllegalArgumentException illArgEx){ // success! } try{ final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(2, 10, f); + final TiffRasterData raster = new TiffRasterDataFloat(2, 10, f); fail("Constructor did not detect insufficient input array size"); }catch(final IllegalArgumentException illArgEx){ // success! } try{ final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(-1, 10, f); + final TiffRasterData raster = new TiffRasterDataFloat(-1, 10, f); fail("Constructor did not detect bad width"); }catch(final IllegalArgumentException illArgEx){ // success! } try{ final float []f = new float[10]; - final TiffRasterData raster = new TiffRasterData(10, -1, f); + final TiffRasterData raster = new TiffRasterDataFloat(10, -1, f); fail("Constructor did not detect bad height"); }catch(final IllegalArgumentException illArgEx){ // success! @@ -175,7 +206,7 @@ public class TiffRasterDataTest { try{ final float []f = new float[100]; - final TiffRasterData raster = new TiffRasterData(10, 10, f); + final TiffRasterData raster = new TiffRasterDataFloat(10, 10, f); raster.getValue(11, 11); fail("Access method getValue() did not detect bad coordinates"); }catch(final IllegalArgumentException illArgEx){ @@ -183,9 +214,9 @@ public class TiffRasterDataTest { } try{ final float []f = new float[100]; - final TiffRasterData raster = new TiffRasterData(10, 10, f); + final TiffRasterData raster = new TiffRasterDataFloat(10, 10, f); raster.setValue(11, 11, 5.0f); - fail("Access method getValue() did not detect bad coordinates"); + fail("Access method setValue() did not detect bad coordinates"); }catch(final IllegalArgumentException illArgEx){ // success! } diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterStatisticsTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterStatisticsTest.java index 458f0a1..7721f9e 100644 --- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterStatisticsTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterStatisticsTest.java @@ -50,7 +50,7 @@ public class TiffRasterStatisticsTest { } } data[width * height / 2] = Float.NaN; - raster = new TiffRasterData(width, height, data); + raster = new TiffRasterDataFloat(width, height, data); meanValue = (float) (sum / k); stat0 = raster.getSimpleStatistics(); stat1 = raster.getSimpleStatistics(stat0.getMaxValue()); @@ -101,7 +101,7 @@ public class TiffRasterStatisticsTest { final float[] zero = new float[100]; Arrays.fill(zero, 10); - final TiffRasterData zeroData = new TiffRasterData(10, 10, zero); + final TiffRasterData zeroData = new TiffRasterDataFloat(10, 10, zero); final TiffRasterStatistics zeroStat = zeroData.getSimpleStatistics(10); assertEquals(0.0f, zeroStat.getMeanValue(), "Invalid mean data for excluded value"); diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffShortIntRoundTripTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffShortIntRoundTripTest.java new file mode 100644 index 0000000..dfaf878 --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffShortIntRoundTripTest.java @@ -0,0 +1,255 @@ +/* + * 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.commons.imaging.formats.tiff; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Performs a test in which a TIFF file with the special-purpose short-integer + * sample type is used to store data to a file. The file is then read to see if + * it matches the original values. The primary purpose of this test is to verify + * that the TIFF data reader classes behave correctly when reading raster data + * in various formats. + */ +public class TiffShortIntRoundTripTest extends TiffBaseTest { + + @TempDir + Path tempDir; + + int width = 48; + int height = 23; + + short[] sample = new short[width * height]; + + public TiffShortIntRoundTripTest() { + // populate the image data + for (int iCol = 0; iCol < width; iCol++) { + for (int iRow = 0; iRow < height; iRow++) { + final int index = iRow * width + iCol; + sample[index] = (short)(index-10); // -10 so at least some are negative + } + } + } + + + + @Test + public void test() throws Exception { + final File[] testFile = new File[4]; + testFile[0] = writeFile(16, ByteOrder.LITTLE_ENDIAN, false); + testFile[1] = writeFile(16, ByteOrder.BIG_ENDIAN, false); + testFile[2] = writeFile(16, ByteOrder.LITTLE_ENDIAN, true); + testFile[3] = writeFile(16, ByteOrder.BIG_ENDIAN, true); + for (int i = 0; i < testFile.length; i++) { + final String name = testFile[i].getName(); + final ByteSourceFile byteSource = new ByteSourceFile(testFile[i]); + final TiffReader tiffReader = new TiffReader(true); + final TiffContents contents = tiffReader.readDirectories( + byteSource, + true, // indicates that application should read image data, if present + FormatCompliance.getDefault()); + final TiffDirectory directory = contents.directories.get(0); + TiffRasterData rdInt = directory.getRasterData(null); + int []test = rdInt.getIntData(); + for(int j=0; j<sample.length; j++){ + assertEquals(sample[j], test[j], + "Extracted data does not match original, test "+name+": " + + i + ", index " + j); + } + final Map<String, Object> params = new HashMap<>(); + params.put(TiffConstants.PARAM_KEY_SUBIMAGE_X, 2); + params.put(TiffConstants.PARAM_KEY_SUBIMAGE_Y, 2); + params.put(TiffConstants.PARAM_KEY_SUBIMAGE_WIDTH, width-4); + params.put(TiffConstants.PARAM_KEY_SUBIMAGE_HEIGHT, height-4); + TiffRasterData rdSub = directory.getRasterData(params); + assertEquals(width-4, rdSub.getWidth(), "Invalid sub-image width"); + assertEquals(height-4, rdSub.getHeight(), "Invalid sub-image height"); + for(int x = 2; x<width-2; x++){ + for(int y=2; y<height-2; y++){ + final int a = rdInt.getIntValue(x, y); + final int b = rdSub.getIntValue(x-2, y-2); + assertEquals(a, b, "Sub Image test failed at (" + x + "," + y + ")"); + } + } + final Map<String, Object> xparams = new HashMap<>(); + xparams.put(TiffConstants.PARAM_KEY_SUBIMAGE_X, 2); + xparams.put(TiffConstants.PARAM_KEY_SUBIMAGE_Y, 2); + xparams.put(TiffConstants.PARAM_KEY_SUBIMAGE_WIDTH, width); + xparams.put(TiffConstants.PARAM_KEY_SUBIMAGE_HEIGHT, height); + assertThrows(ImageReadException.class, ()->directory.getRasterData(xparams), + "Failed to catch bad subimage for test "+name); + } + } + + private File writeFile(final int bitsPerSample, final ByteOrder byteOrder, final boolean useTiles) + throws IOException, ImageWriteException { + final String name = String.format("ShortIntRoundTrip_%2d_%s_%s.tiff", + bitsPerSample, + byteOrder == ByteOrder.LITTLE_ENDIAN ? "LE" : "BE", + useTiles ? "Tiles" : "Strips"); + final File outputFile = new File(tempDir.toFile(), name); + + final int bytesPerSample = bitsPerSample / 8; + int nRowsInBlock; + int nColsInBlock; + int nBytesInBlock; + if (useTiles) { + // Define the tiles so that they will not evenly subdivide + // the image. This will allow the test to evaluate how the + // data reader processes tiles that are only partially used. + nRowsInBlock = 12; + nColsInBlock = 20; + } else { + // Define the strips so that they will not evenly subdivide + // the image. This will allow the test to evaluate how the + // data reader processes strips that are only partially used. + nRowsInBlock = 2; + nColsInBlock = width; + } + nBytesInBlock = nRowsInBlock * nColsInBlock * bytesPerSample; + + byte[][] blocks; + blocks = this.getBytesForOutput16(sample, width, height, nRowsInBlock, nColsInBlock, byteOrder); + + + // NOTE: At this time, Tile format is not supported. + // When it is, modify the tags below to populate + // TIFF_TAG_TILE_* appropriately. + final TiffOutputSet outputSet = new TiffOutputSet(byteOrder); + final TiffOutputDirectory outDir = outputSet.addRootDirectory(); + outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width); + outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height); + outDir.add(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, + (short) TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER); + outDir.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) 1); + outDir.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample); + outDir.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, + (short) TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO); + outDir.add(TiffTagConstants.TIFF_TAG_COMPRESSION, + (short) TiffTagConstants.COMPRESSION_VALUE_UNCOMPRESSED); + + outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, + (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_CHUNKY); + + if (useTiles) { + outDir.add(TiffTagConstants.TIFF_TAG_TILE_WIDTH, nColsInBlock); + outDir.add(TiffTagConstants.TIFF_TAG_TILE_LENGTH, nRowsInBlock); + outDir.add(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS, nBytesInBlock); + } else { + outDir.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, 2); + outDir.add(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS, nBytesInBlock); + } + + final TiffElement.DataElement[] imageData = new TiffElement.DataElement[blocks.length]; + for (int i = 0; i < blocks.length; i++) { + imageData[i] = new TiffImageData.Data(0, blocks[i].length, blocks[i]); + } + + TiffImageData tiffImageData; + if (useTiles) { + tiffImageData + = new TiffImageData.Tiles(imageData, nColsInBlock, nRowsInBlock); + } else { + tiffImageData + = new TiffImageData.Strips(imageData, nRowsInBlock); + } + outDir.setTiffImageData(tiffImageData); + + try (FileOutputStream fos = new FileOutputStream(outputFile); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + final TiffImageWriterLossy writer = new TiffImageWriterLossy(byteOrder); + writer.write(bos, outputSet); + bos.flush(); + } + return outputFile; + } + + /** + * Gets the bytes for output for a 16 bit floating point format. Note that + * this method operates over "blocks" of data which may represent either + * TIFF Strips or Tiles. When processing strips, there is always one column + * of blocks and each strip is exactly the full width of the image. When + * processing tiles, there may be one or more columns of blocks and the + * block coverage may extend beyond both the last row and last column. + * + * @param s an array of the grid of output values in row major order + * @param width the width of the overall image + * @param height the height of the overall image + * @param nRowsInBlock the number of rows in the Strip or Tile + * @param nColsInBlock the number of columns in the Strip or Tile + * @param byteOrder little endian or big endian + * @return a two-dimensional array of bytes dimensioned by the number of blocks and samples + */ + private byte[][] getBytesForOutput16( + final short[] s, + final int width, final int height, + final int nRowsInBlock, final int nColsInBlock, + final ByteOrder byteOrder) { + final int nColsOfBlocks = (width + nColsInBlock - 1) / nColsInBlock; + final int nRowsOfBlocks = (height + nRowsInBlock + 1) / nRowsInBlock; + final int bytesPerPixel = 2; + final int nBlocks = nRowsOfBlocks * nColsOfBlocks; + final int nBytesInBlock = bytesPerPixel * nRowsInBlock * nColsInBlock; + final byte[][] blocks = new byte[nBlocks][nBytesInBlock]; + for (int i = 0; i < height; i++) { + final int blockRow = i / nRowsInBlock; + final int rowInBlock = i - blockRow * nRowsInBlock; + final int blockOffset = rowInBlock * nColsInBlock; + for (int j = 0; j < width; j++) { + final int value = s[i * width + j]; + final int blockCol = j / nColsInBlock; + final int colInBlock = j - blockCol * nColsInBlock; + final int index = blockOffset + colInBlock; + final int offset = index * bytesPerPixel; + final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol]; + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + b[offset] = (byte) (value & 0xff); + b[offset + 1] = (byte) ((value >> 8) & 0xff); + } else { + b[offset] = (byte) ((value >> 8) & 0xff); + b[offset + 1] = (byte) (value & 0xff); + } + } + } + + return blocks; + } + + +}