You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ki...@apache.org on 2022/01/08 05:31:21 UTC

[commons-imaging] 01/03: IMAGING-320 Read TIFFs with 32-bit samples

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 a27e3d335b0f40138715aa5cff5eec274d13c29b
Author: gwlucastrig <co...@gmail.com>
AuthorDate: Fri Jan 7 07:02:12 2022 -0500

    IMAGING-320 Read TIFFs with 32-bit samples
---
 .../imaging/formats/tiff/TiffImageParser.java      |   2 +-
 .../formats/tiff/datareaders/ImageDataReader.java  |  36 ++-
 .../formats/tiff/TiffRasterDataIntTest.java        |   2 +-
 .../formats/tiff/TiffRoundTripInt32Test.java       | 259 +++++++++++++++++++++
 4 files changed, 290 insertions(+), 9 deletions(-)

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 6fa866d..04eb259 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
@@ -940,7 +940,7 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable {
                         + samplesPerPixel);
             }
 
-            if (bitsPerPixel != 16) {
+            if (bitsPerPixel != 16 && bitsPerPixel != 32) {
                 throw new ImageReadException(
                         "TIFF integer data uses unsupported bits-per-pixel: "
                         + bitsPerPixel);
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 17a1817..242b126 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
@@ -636,14 +636,36 @@ public abstract class ImageDataReader {
 
         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);
+            int offset = index * bytesPerSample;
+            if (bitsPerSample == 16) {
+                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);
+                    }
                 }
-            } else {
-                for (int j = 0; j < width; j++, offset += 2) {
-                    samples[index + j] = (bytes[offset] << 8) | (bytes[offset + 1] & 0xff);
+            } else if (bitsPerSample == 32) {
+                if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
+                    for (int j = 0; j < width; j++, offset += 4) {
+                        samples[index + j]
+                          = (bytes[offset + 3] << 24)
+                          | ((bytes[offset + 2] & 0xff) << 16)
+                          | ((bytes[offset + 1] & 0xff) << 8)
+                          | (bytes[offset] & 0xff);
+                    }
+                } else {
+                    for (int j = 0; j < width; j++, offset += 4) {
+                        samples[index + j]
+                          = (bytes[offset] << 24)
+                          | ((bytes[offset + 1] & 0xff) << 16)
+                          | ((bytes[offset + 2] & 0xff) << 8)
+                          | (bytes[offset + 3] & 0xff);
+                    }
                 }
             }
             if (useDifferencing) {
diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java
index 9937997..b917074 100644
--- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataIntTest.java
@@ -114,7 +114,7 @@ public class TiffRasterDataIntTest {
         TiffRasterDataInt instance = new TiffRasterDataInt(width, height, 2, data);
         int test = instance.getIntValue(0, 0, 1);
         assertEquals(77, test, "Get into source data test failed at (0, 0, 1)");
-        
+
         for (int y = 0; y < height; y++) {
             for (int x = 0; x < width; x++) {
                 final int index = y * width + x;
diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRoundTripInt32Test.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRoundTripInt32Test.java
new file mode 100644
index 0000000..f0b9811
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffRoundTripInt32Test.java
@@ -0,0 +1,259 @@
+/*
+ * 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 32-bit 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 TiffRoundTripInt32Test extends TiffBaseTest {
+
+    @TempDir
+    Path tempDir;
+
+    int width = 48;
+    int height = 23;
+
+    int[] sample = new int[width * height];
+
+    public TiffRoundTripInt32Test() {
+        // 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] = index-10;  // -10 so at least some are negative
+            }
+        }
+    }
+
+
+
+    @Test
+    public void test() throws Exception {
+        final File[] testFile = new File[4];
+        testFile[0] = writeFile(32, ByteOrder.LITTLE_ENDIAN, false);
+        testFile[1] = writeFile(32, ByteOrder.BIG_ENDIAN, false);
+        testFile[2] = writeFile(32, ByteOrder.LITTLE_ENDIAN, true);
+        testFile[3] = writeFile(32, 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("Int32RoundTrip_%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.getBytesForOutput32(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, nRowsInBlock);
+            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[][] getBytesForOutput32(
+        final int[] 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 = 4;
+        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);
+                    b[offset + 2] = (byte) ((value >> 16) & 0xff);
+                    b[offset + 3] = (byte) ((value >> 24) & 0xff);
+                } else {
+                    b[offset] = (byte) ((value >> 24) & 0xff);
+                    b[offset + 1] = (byte) ((value >> 16) & 0xff);
+                    b[offset + 2] = (byte) ((value >> 8) & 0xff);
+                    b[offset + 3] = (byte) (value & 0xff);
+                }
+            }
+        }
+
+        return blocks;
+    }
+
+
+}