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.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.nio.ByteOrder;
27  import java.nio.file.Path;
28  import java.util.ArrayList;
29  import java.util.List;
30  
31  import org.apache.commons.imaging.FormatCompliance;
32  import org.apache.commons.imaging.ImagingException;
33  import org.apache.commons.imaging.bytesource.ByteSource;
34  import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
35  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
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 TiffFloatingPointMultivariableTest extends TiffBaseTest {
50  
51      @TempDir
52      Path tempDir;
53  
54      int width = 48;
55      int height = 23;
56      int samplesPerPixel = 2;
57      float f0 = 0.0F;
58      float f1 = 1.0F;
59      float[] fSample = new float[width * height * samplesPerPixel];
60  
61      public TiffFloatingPointMultivariableTest() {
62          for (int iPlane = 0; iPlane < 2; iPlane++) {
63              final int pOffset = iPlane * width * height;
64              for (int iRow = 0; iRow < height; iRow++) {
65                  for (int iCol = 0; iCol < width; iCol++) {
66                      final int index = pOffset + iRow * width + iCol;
67                      fSample[index] = index;
68                  }
69              }
70          }
71      }
72  
73      private void applyTilePredictor(final int nRowsInBlock, final int nColsInBlock, final byte[] bytes) {
74          // The floating-point horizonal predictor breaks the samples into
75          // separate sets of bytes. The first set contains the high-order bytes.
76          // The second the second-highest order bytes, etc. Once the bytes are
77          // separated, differencing is applied. This treatment improves the
78          // statistical predictability of the data. By doing so, it improves
79          // its compressibility.
80          // More extensive discussions of this technique are given in the
81          // Javadoc for the TIFF-specific ImageDataReader class.
82          final byte[] b = new byte[bytes.length];
83          final int bytesInRow = nColsInBlock * 4;
84          for (int iPlane = 0; iPlane < samplesPerPixel; iPlane++) {
85              // separate out the groups of bytes
86              final int planarByteOffset = iPlane * nRowsInBlock * nColsInBlock * 4;
87              for (int i = 0; i < nRowsInBlock; i++) {
88                  final int aOffset = planarByteOffset + i * bytesInRow;
89                  final int bOffset = aOffset + nColsInBlock;
90                  final int cOffset = bOffset + nColsInBlock;
91                  final int dOffset = cOffset + nColsInBlock;
92                  for (int j = 0; j < nColsInBlock; j++) {
93                      b[aOffset + j] = bytes[aOffset + j * 4];
94                      b[bOffset + j] = bytes[aOffset + j * 4 + 1];
95                      b[cOffset + j] = bytes[aOffset + j * 4 + 2];
96                      b[dOffset + j] = bytes[aOffset + j * 4 + 3];
97                  }
98                  // apply differencing
99                  for (int j = bytesInRow - 1; j > 0; j--) {
100                     b[aOffset + j] -= b[aOffset + j - 1];
101                 }
102             }
103         }
104         // copy the results back over the input byte array
105         System.arraycopy(b, 0, bytes, 0, bytes.length);
106     }
107 
108     /**
109      * 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
110      * 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,
111      * there may be one or more columns of blocks and the block coverage may extend beyond both the last row and last column.
112      *
113      * @param f            an array of the grid of output values in row major order
114      * @param width        the width of the overall image
115      * @param height       the height of the overall image
116      * @param nRowsInBlock the number of rows in the Strip or Tile
117      * @param nColsInBlock the number of columns in the Strip or Tile
118      * @param byteOrder    little-endian or big-endian
119      * @return a valid array of equally sized array.
120      */
121     private byte[][] getBytesForOutput32(final int nRowsInBlock, final int nColsInBlock, final ByteOrder byteOrder, final boolean useTiles,
122             final TiffPlanarConfiguration planarConfiguration) {
123         final int nColsOfBlocks = (width + nColsInBlock - 1) / nColsInBlock;
124         final int nRowsOfBlocks = (height + nRowsInBlock + 1) / nRowsInBlock;
125         final int bytesPerPixel = 4 * samplesPerPixel;
126         final int nBlocks = nRowsOfBlocks * nColsOfBlocks;
127         final int nBytesInBlock = bytesPerPixel * nRowsInBlock * nColsInBlock;
128         final byte[][] blocks = new byte[nBlocks][nBytesInBlock];
129         if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
130             for (int i = 0; i < height; i++) {
131                 final int blockRow = i / nRowsInBlock;
132                 final int rowInBlock = i - blockRow * nRowsInBlock;
133                 for (int j = 0; j < width; j++) {
134                     final int blockCol = j / nColsInBlock;
135                     final int colInBlock = j - blockCol * nColsInBlock;
136                     final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol]; // reference to relevant block
137                     for (int k = 0; k < 2; k++) {
138                         final float sValue = fSample[k * width * height + i * width + j];
139                         final int sample = Float.floatToRawIntBits(sValue);
140                         final int offset = (rowInBlock * nColsInBlock + colInBlock) * 8 + k * 4;
141                         if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
142                             b[offset] = (byte) (sample & 0xff);
143                             b[offset + 1] = (byte) (sample >> 8 & 0xff);
144                             b[offset + 2] = (byte) (sample >> 16 & 0xff);
145                             b[offset + 3] = (byte) (sample >> 24 & 0xff);
146                         } else {
147                             b[offset] = (byte) (sample >> 24 & 0xff);
148                             b[offset + 1] = (byte) (sample >> 16 & 0xff);
149                             b[offset + 2] = (byte) (sample >> 8 & 0xff);
150                             b[offset + 3] = (byte) (sample & 0xff);
151                         }
152                     }
153                 }
154             }
155         } else {
156             for (int i = 0; i < height; i++) {
157                 final int blockRow = i / nRowsInBlock;
158                 final int rowInBlock = i - blockRow * nRowsInBlock;
159                 int blockPlanarOffset = nRowsInBlock * nColsInBlock;
160                 if (!useTiles && (blockRow + 1) * nRowsInBlock > height) {
161                     // For TIFF files using the Strip format, the convention
162                     // is to not include any extra padding in the data. So if the
163                     // height of the image is not evenly divided by the number
164                     // of rows per strip, an adjustmnet is made to the size of the block.
165                     // However, the TIFF specification calls for tiles to always be padded.
166                     final int nRowsAdjusted = height - blockRow * nRowsInBlock;
167                     blockPlanarOffset = nRowsAdjusted * nColsInBlock;
168                 }
169                 for (int j = 0; j < width; j++) {
170                     final int blockCol = j / nColsInBlock;
171                     final int colInBlock = j - blockCol * nColsInBlock;
172                     final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol]; // reference to relevant block
173                     for (int k = 0; k < 2; k++) {
174                         final float sValue = fSample[k * width * height + i * width + j];
175                         final int sample = Float.floatToRawIntBits(sValue);
176                         final int offset = (k * blockPlanarOffset + rowInBlock * nColsInBlock + colInBlock) * 4;
177                         if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
178                             b[offset] = (byte) (sample & 0xff);
179                             b[offset + 1] = (byte) (sample >> 8 & 0xff);
180                             b[offset + 2] = (byte) (sample >> 16 & 0xff);
181                             b[offset + 3] = (byte) (sample >> 24 & 0xff);
182                         } else {
183                             b[offset] = (byte) (sample >> 24 & 0xff);
184                             b[offset + 1] = (byte) (sample >> 16 & 0xff);
185                             b[offset + 2] = (byte) (sample >> 8 & 0xff);
186                             b[offset + 3] = (byte) (sample & 0xff);
187                         }
188                     }
189                 }
190             }
191         }
192 
193         return blocks;
194     }
195 
196     @Test
197     public void test() throws Exception {
198         // we set up the 32 and 64 bit test cases. At this time,
199         // the Tile format is not supported for floating-point samples by the
200         // TIFF datareaders classes. So that format is not yet exercised.
201         // Note also that the compressed floating-point with predictor=3
202         // is processed in other tests, but not here.
203         final List<File> testFiles = new ArrayList<>();
204         testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, false, false, TiffPlanarConfiguration.CHUNKY));
205         testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, false, false, TiffPlanarConfiguration.CHUNKY));
206         testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, true, false, TiffPlanarConfiguration.CHUNKY));
207         testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, false, TiffPlanarConfiguration.CHUNKY));
208         testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, false, false, TiffPlanarConfiguration.PLANAR));
209         testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, false, false, TiffPlanarConfiguration.PLANAR));
210         testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, true, false, TiffPlanarConfiguration.PLANAR));
211         testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, false, TiffPlanarConfiguration.PLANAR));
212 
213         // To exercise the horizontal-differencing-predictor logic, we include a writer that will
214         // reorganize the bytes into the form used by the floating-pont horizontal predictor.
215         // This test does not apply data compression, but it does apply the predictor.
216         // Note that although the TIFF predictor does not require big-endian formats, per se,
217         // the test logic implemented here does.
218         testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, true, TiffPlanarConfiguration.PLANAR));
219 
220         for (final File testFile : testFiles) {
221             final String name = testFile.getName();
222             final ByteSource byteSource = ByteSource.file(testFile);
223             final TiffReader tiffReader = new TiffReader(true);
224             final TiffContents contents = tiffReader.readDirectories(byteSource, true, // indicates that application should read image data, if present
225                     FormatCompliance.getDefault());
226             final TiffDirectory directory = contents.directories.get(0);
227             final TiffRasterData raster = directory.getRasterData(new TiffImagingParameters());
228             assertNotNull(raster, "Failed to get raster from " + name);
229             assertEquals(2, raster.getSamplesPerPixel(), "Invalid samples per pixel in " + name);
230             for (int iPlane = 0; iPlane < 2; iPlane++) {
231                 final int pOffset = iPlane * width * height;
232                 for (int iRow = 0; iRow < height; iRow++) {
233                     for (int iCol = 0; iCol < width; iCol++) {
234                         final int index = pOffset + iRow * width + iCol;
235                         final float tValue = fSample[index];
236                         final float rValue = raster.getValue(iCol, iRow, iPlane);
237                         assertEquals(tValue, rValue, "Failed at index x=" + iCol + ", y=" + iRow + ", iPlane=" + iPlane);
238                     }
239                 }
240             }
241         }
242     }
243 
244     private File writeFile(final ByteOrder byteOrder, final boolean useTiles, final boolean usePredictorForTiles,
245             final TiffPlanarConfiguration planarConfiguration) throws IOException, ImagingException {
246 
247         final String name = String.format("FpMultiVarRoundTrip_%s_%s%s.tiff", planarConfiguration == TiffPlanarConfiguration.CHUNKY ? "Chunky" : "Planar",
248                 useTiles ? "Tiles" : "Strips", usePredictorForTiles ? "_Predictor" : "");
249         final File outputFile = new File(tempDir.toFile(), name);
250 
251         final int bytesPerSample = 4 * samplesPerPixel;
252         final int bitsPerSample = 8 * bytesPerSample;
253 
254         int nRowsInBlock;
255         int nColsInBlock;
256         int nBytesInBlock;
257         if (useTiles) {
258             // Define the tiles so that they will not evenly subdivide
259             // the image. This will allow the test to evaluate how the
260             // data reader processes tiles that are only partially used.
261             nRowsInBlock = 12;
262             nColsInBlock = 20;
263         } else {
264             // Define the strips so that they will not evenly subdivide
265             // the image. This will allow the test to evaluate how the
266             // data reader processes strips that are only partially used.
267             nRowsInBlock = 2;
268             nColsInBlock = width;
269         }
270         nBytesInBlock = nRowsInBlock * nColsInBlock * bytesPerSample;
271 
272         byte[][] blocks;
273         blocks = this.getBytesForOutput32(nRowsInBlock, nColsInBlock, byteOrder, useTiles, planarConfiguration);
274 
275         final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
276         final TiffOutputDirectory outDir = outputSet.addRootDirectory();
277         outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
278         outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
279         outDir.add(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, (short) TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT);
280         outDir.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) samplesPerPixel);
281         outDir.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample);
282         outDir.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO);
283         outDir.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) TiffTagConstants.COMPRESSION_VALUE_UNCOMPRESSED);
284 
285         if (useTiles && usePredictorForTiles) {
286             outDir.add(TiffTagConstants.TIFF_TAG_PREDICTOR, (short) TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING);
287             for (final byte[] block : blocks) {
288                 applyTilePredictor(nRowsInBlock, nColsInBlock, block);
289             }
290         }
291 
292         if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
293             outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_CHUNKY);
294         } else {
295             outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_PLANAR);
296         }
297 
298         if (useTiles) {
299             outDir.add(TiffTagConstants.TIFF_TAG_TILE_WIDTH, nColsInBlock);
300             outDir.add(TiffTagConstants.TIFF_TAG_TILE_LENGTH, nRowsInBlock);
301             outDir.add(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS, nBytesInBlock);
302         } else {
303             outDir.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, nRowsInBlock);
304             outDir.add(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS, nBytesInBlock);
305         }
306 
307         final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[blocks.length];
308         for (int i = 0; i < blocks.length; i++) {
309             imageData[i] = new AbstractTiffImageData.Data(0, blocks[i].length, blocks[i]);
310         }
311 
312         AbstractTiffImageData abstractTiffImageData;
313         if (useTiles) {
314             abstractTiffImageData = new AbstractTiffImageData.Tiles(imageData, nColsInBlock, nRowsInBlock);
315         } else {
316             abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, nRowsInBlock);
317         }
318         outDir.setTiffImageData(abstractTiffImageData);
319 
320         try (FileOutputStream fos = new FileOutputStream(outputFile);
321                 BufferedOutputStream bos = new BufferedOutputStream(fos)) {
322             final TiffImageWriterLossy writer = new TiffImageWriterLossy(byteOrder);
323             writer.write(bos, outputSet);
324             bos.flush();
325         }
326         return outputFile;
327     }
328 
329 }