View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotNull;
21  
22  import java.awt.image.BufferedImage;
23  import java.io.BufferedOutputStream;
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.nio.ByteOrder;
28  import java.nio.file.Path;
29  
30  import org.apache.commons.imaging.FormatCompliance;
31  import org.apache.commons.imaging.ImagingException;
32  import org.apache.commons.imaging.bytesource.ByteSource;
33  import org.apache.commons.imaging.common.ImageBuilder;
34  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
35  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint.PhotometricInterpreterFloat;
36  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
37  import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
38  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
39  import org.junit.jupiter.api.Test;
40  import org.junit.jupiter.api.io.TempDir;
41  
42  /**
43   * Performs a test in which a TIFF file with the special-purpose floating-point sample type is used to store data to a file. The file is then read to see if it
44   * matches the original values.
45   * <p>
46   * At this time, Commons Imaging does not fully implement the floating-point specification. Currently, this class only tests the use of uncompressed floating
47   * point values in the Strips format. The Tiles format is not exercised.
48   */
49  public class TiffFloatingPointRoundTripTest extends TiffBaseTest {
50  
51      @TempDir
52      Path tempDir;
53  
54      int width = 48;
55      int height = 23;
56      float f0 = 0.0F;
57      float f1 = 1.0F;
58      float[] f = new float[width * height];
59      int[] argb = new int[width * height];
60  
61      public TiffFloatingPointRoundTripTest() throws ImagingException, IOException {
62          // populate the image data
63          for (int iCol = 0; iCol < width; iCol++) {
64              final float s = iCol / (float) (width - 1);
65              for (int iRow = 0; iRow < height; iRow++) {
66                  final int index = iRow * width + iCol;
67                  f[index] = s;
68              }
69          }
70  
71          // apply the photometric interpreter to assign colors to the
72          // floating-point input data. The ultimate goal of the test is to verify
73          // that the values read back from the TIFF file match the input.
74          final PhotometricInterpreterFloat pi = getPhotometricInterpreter();
75          final ImageBuilder builder = new ImageBuilder(width, height, false);
76          final int[] samples = new int[1];
77          for (int iCol = 0; iCol < width; iCol++) {
78              for (int iRow = 0; iRow < height; iRow++) {
79                  final int index = iRow * width + iCol;
80                  samples[0] = Float.floatToRawIntBits(f[index]);
81                  pi.interpretPixel(builder, samples, iCol, iRow);
82                  argb[index] = builder.getRgb(iCol, iRow);
83              }
84          }
85      }
86  
87      /**
88       * Gets the bytes for output for a 32 bit floating point format. Note that this method operates over "blocks" of data which may represent either TIFF Strips
89       * 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,
90       * there may be one or more columns of blocks and the block coverage may extend beyond both the last row and last column.
91       *
92       * @param f            an array of the grid of output values in row major order
93       * @param width        the width of the overall image
94       * @param height       the height of the overall image
95       * @param nRowsInBlock the number of rows in the Strip or Tile
96       * @param nColsInBlock the number of columns in the Strip or Tile
97       * @param byteOrder    little-endian or big-endian
98       * @return a valid array of equally sized array.
99       */
100     private byte[][] getBytesForOutput32(final float[] f, final int width, final int height, final int nRowsInBlock, final int nColsInBlock,
101             final ByteOrder byteOrder) {
102         final int nColsOfBlocks = (width + nColsInBlock - 1) / nColsInBlock;
103         final int nRowsOfBlocks = (height + nRowsInBlock + 1) / nRowsInBlock;
104         final int bytesPerPixel = 4;
105         final int nBlocks = nRowsOfBlocks * nColsOfBlocks;
106         final int nBytesInBlock = bytesPerPixel * nRowsInBlock * nColsInBlock;
107         final byte[][] blocks = new byte[nBlocks][nBytesInBlock];
108         for (int i = 0; i < height; i++) {
109             final int blockRow = i / nRowsInBlock;
110             final int rowInBlock = i - blockRow * nRowsInBlock;
111             final int blockOffset = rowInBlock * nColsInBlock;
112             for (int j = 0; j < width; j++) {
113                 final int sample = Float.floatToRawIntBits(f[i * width + j]);
114                 final int blockCol = j / nColsInBlock;
115                 final int colInBlock = j - blockCol * nColsInBlock;
116                 final int index = blockOffset + colInBlock;
117                 final int offset = index * bytesPerPixel;
118                 final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol];
119                 if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
120                     b[offset] = (byte) (sample & 0xff);
121                     b[offset + 1] = (byte) (sample >> 8 & 0xff);
122                     b[offset + 2] = (byte) (sample >> 16 & 0xff);
123                     b[offset + 3] = (byte) (sample >> 24 & 0xff);
124                 } else {
125                     b[offset] = (byte) (sample >> 24 & 0xff);
126                     b[offset + 1] = (byte) (sample >> 16 & 0xff);
127                     b[offset + 2] = (byte) (sample >> 8 & 0xff);
128                     b[offset + 3] = (byte) (sample & 0xff);
129                 }
130             }
131         }
132 
133         return blocks;
134     }
135 
136     /**
137      * Gets the bytes for output for a 64 bit floating point format. Note that this method operates over "blocks" of data which may represent either TIFF Strips
138      * 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,
139      * there may be one or more columns of blocks and the block coverage may extend beyond both the last row and last column.
140      *
141      * @param f            an array of the grid of output values in row major order
142      * @param width        the width of the overall image
143      * @param height       the height of the overall image
144      * @param nRowsInBlock the number of rows in the Strip or Tile
145      * @param nColsInBlock the number of columns in the Strip or Tile
146      * @param byteOrder    little-endian or big-endian
147      * @return a valid array of equally sized array.
148      */
149     private byte[][] getBytesForOutput64(final float[] f, final int width, final int height, final int nRowsInBlock, final int nColsInBlock,
150             final ByteOrder byteOrder) {
151         final int nColsOfBlocks = (width + nColsInBlock - 1) / nColsInBlock;
152         final int nRowsOfBlocks = (height + nRowsInBlock + 1) / nRowsInBlock;
153         final int bytesPerPixel = 8;
154         final int nBlocks = nRowsOfBlocks * nColsOfBlocks;
155         final int nBytesInBlock = bytesPerPixel * nRowsInBlock * nColsInBlock;
156         final byte[][] blocks = new byte[nBlocks][nBytesInBlock];
157         for (int i = 0; i < height; i++) {
158             final int blockRow = i / nRowsInBlock;
159             final int rowInBlock = i - blockRow * nRowsInBlock;
160             final int blockOffset = rowInBlock * nColsInBlock;
161             for (int j = 0; j < width; j++) {
162                 final long sample = Double.doubleToRawLongBits(f[i * width + j]);
163                 final int blockCol = j / nColsInBlock;
164                 final int colInBlock = j - blockCol * nColsInBlock;
165                 final int index = blockOffset + colInBlock;
166                 final int offset = index * bytesPerPixel;
167                 final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol];
168                 if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
169                     b[offset] = (byte) (sample & 0xff);
170                     b[offset + 1] = (byte) (sample >> 8 & 0xff);
171                     b[offset + 2] = (byte) (sample >> 16 & 0xff);
172                     b[offset + 3] = (byte) (sample >> 24 & 0xff);
173                     b[offset + 4] = (byte) (sample >> 32 & 0xff);
174                     b[offset + 5] = (byte) (sample >> 40 & 0xff);
175                     b[offset + 6] = (byte) (sample >> 48 & 0xff);
176                     b[offset + 7] = (byte) (sample >> 56 & 0xff);
177                 } else {
178                     b[offset] = (byte) (sample >> 56 & 0xff);
179                     b[offset + 1] = (byte) (sample >> 48 & 0xff);
180                     b[offset + 2] = (byte) (sample >> 40 & 0xff);
181                     b[offset + 3] = (byte) (sample >> 32 & 0xff);
182                     b[offset + 4] = (byte) (sample >> 24 & 0xff);
183                     b[offset + 5] = (byte) (sample >> 16 & 0xff);
184                     b[offset + 6] = (byte) (sample >> 8 & 0xff);
185                     b[offset + 7] = (byte) (sample & 0xff);
186                 }
187             }
188         }
189 
190         return blocks;
191     }
192 
193     /**
194      * Constructs a photometric interpreter. This initialization is performed in a dedicated method to ensure consistency throughout different phases of the
195      * test.
196      *
197      * @return a valid instance.
198      */
199     private PhotometricInterpreterFloat getPhotometricInterpreter() {
200         return new PhotometricInterpreterFloat(f0, f1 + 1.0e-5f);
201     }
202 
203     @Test
204     public void test() throws Exception {
205         // we set up the 32 and 64 bit test cases. At this time,
206         // the Tile format is not supported for floating-point samples by the
207         // TIFF datareaders classes. So that format is not yet exercised.
208         // Note also that the compressed floating-point with predictor=3
209         // is processed in other tests, but not here.
210         final File[] testFile = new File[8];
211         testFile[0] = writeFile(32, ByteOrder.LITTLE_ENDIAN, false);
212         testFile[1] = writeFile(64, ByteOrder.LITTLE_ENDIAN, false);
213         testFile[2] = writeFile(32, ByteOrder.BIG_ENDIAN, false);
214         testFile[3] = writeFile(64, ByteOrder.BIG_ENDIAN, false);
215         testFile[4] = writeFile(32, ByteOrder.LITTLE_ENDIAN, true);
216         testFile[5] = writeFile(64, ByteOrder.LITTLE_ENDIAN, true);
217         testFile[6] = writeFile(32, ByteOrder.BIG_ENDIAN, true);
218         testFile[7] = writeFile(64, ByteOrder.BIG_ENDIAN, true);
219         for (int i = 0; i < testFile.length; i++) {
220             final String name = testFile[i].getName();
221             final ByteSource byteSource = ByteSource.file(testFile[i]);
222             final TiffReader tiffReader = new TiffReader(true);
223             final TiffContents contents = tiffReader.readDirectories(byteSource, true, // indicates that application should read image data, if present
224                     FormatCompliance.getDefault());
225             final TiffDirectory directory = contents.directories.get(0);
226             final PhotometricInterpreterFloat pi = getPhotometricInterpreter();
227             final TiffImagingParameters params = new TiffImagingParameters();
228             params.setCustomPhotometricInterpreter(pi);
229             final ByteOrder byteOrder = tiffReader.getByteOrder();
230             final BufferedImage bImage = directory.getTiffImage(byteOrder, params);
231             assertNotNull(bImage, "Failed to get image from " + name);
232             final int[] pixel = new int[width * height];
233             bImage.getRGB(0, 0, width, height, pixel, 0, width);
234             for (int k = 0; k < pixel.length; k++) {
235                 assertEquals(argb[k], pixel[k], "Extracted data does not match original, test " + i + ", index " + k);
236             }
237             final float meanValue = pi.getMeanFound();
238             assertEquals(0.5, meanValue, 1.0e-5, "Invalid numeric values in " + name);
239             // To write out an image file for inspection, use the following
240             // (with appropriate adjustments for path and OS)
241             // File imFile = new File("C:/Users/public", testFile[i].getName() + ".png");
242             // ImageIO.write(bImage, "PNG", imFile);
243 
244         }
245     }
246 
247     private File writeFile(final int bitsPerSample, final ByteOrder byteOrder, final boolean useTiles) throws IOException, ImagingException {
248         final String name = String.format("FpRoundTrip_%2d_%s_%s.tiff", bitsPerSample, byteOrder == ByteOrder.LITTLE_ENDIAN ? "LE" : "BE",
249                 useTiles ? "Tiles" : "Strips");
250         final File outputFile = new File(tempDir.toFile(), name);
251 
252         final int bytesPerSample = bitsPerSample / 8;
253         int nRowsInBlock;
254         int nColsInBlock;
255         int nBytesInBlock;
256         if (useTiles) {
257             // Define the tiles so that they will not evenly subdivide
258             // the image. This will allow the test to evaluate how the
259             // data reader processes tiles that are only partially used.
260             nRowsInBlock = 12;
261             nColsInBlock = 20;
262         } else {
263             // Define the strips so that they will not evenly subdivide
264             // the image. This will allow the test to evaluate how the
265             // data reader processes strips that are only partially used.
266             nRowsInBlock = 2;
267             nColsInBlock = width;
268         }
269         nBytesInBlock = nRowsInBlock * nColsInBlock * bytesPerSample;
270 
271         byte[][] blocks;
272         if (bitsPerSample == 32) {
273             blocks = this.getBytesForOutput32(f, width, height, nRowsInBlock, nColsInBlock, byteOrder);
274         } else {
275             blocks = getBytesForOutput64(f, width, height, nRowsInBlock, nColsInBlock, byteOrder);
276         }
277 
278         // NOTE: At this time, Tile format is not supported.
279         // When it is, modify the tags below to populate
280         // TIFF_TAG_TILE_* appropriately.
281         final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
282         final TiffOutputDirectory outDir = outputSet.addRootDirectory();
283         outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
284         outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
285         outDir.add(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, (short) TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT);
286         outDir.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) 1);
287         outDir.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample);
288         outDir.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO);
289         outDir.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) TiffTagConstants.COMPRESSION_VALUE_UNCOMPRESSED);
290 
291         outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_CHUNKY);
292 
293         if (useTiles) {
294             outDir.add(TiffTagConstants.TIFF_TAG_TILE_WIDTH, nColsInBlock);
295             outDir.add(TiffTagConstants.TIFF_TAG_TILE_LENGTH, nRowsInBlock);
296             outDir.add(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS, nBytesInBlock);
297         } else {
298             outDir.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, 2);
299             outDir.add(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS, nBytesInBlock);
300         }
301 
302         final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[blocks.length];
303         for (int i = 0; i < blocks.length; i++) {
304             imageData[i] = new AbstractTiffImageData.Data(0, blocks[i].length, blocks[i]);
305         }
306 
307         AbstractTiffImageData abstractTiffImageData;
308         if (useTiles) {
309             abstractTiffImageData = new AbstractTiffImageData.Tiles(imageData, nColsInBlock, nRowsInBlock);
310         } else {
311             abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, nRowsInBlock);
312         }
313         outDir.setTiffImageData(abstractTiffImageData);
314 
315         try (FileOutputStream fos = new FileOutputStream(outputFile);
316                 BufferedOutputStream bos = new BufferedOutputStream(fos)) {
317             final TiffImageWriterLossy writer = new TiffImageWriterLossy(byteOrder);
318             writer.write(bos, outputSet);
319             bos.flush();
320         }
321         return outputFile;
322     }
323 }