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  
18  /*
19  * Implementation notes:
20  *    See ImageDataReader and DataReaderStrips for notes on development
21  * with particular emphasis on run-time performance.
22  */
23  package org.apache.commons.imaging.formats.tiff.datareaders;
24  
25  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_JPEG;
26  
27  import java.awt.Rectangle;
28  import java.io.ByteArrayInputStream;
29  import java.io.IOException;
30  import java.nio.ByteOrder;
31  
32  import org.apache.commons.imaging.ImagingException;
33  import org.apache.commons.imaging.common.Allocator;
34  import org.apache.commons.imaging.common.ImageBuilder;
35  import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
36  import org.apache.commons.imaging.formats.tiff.TiffDirectory;
37  import org.apache.commons.imaging.formats.tiff.TiffRasterData;
38  import org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat;
39  import org.apache.commons.imaging.formats.tiff.TiffRasterDataInt;
40  import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
41  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
42  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
43  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb;
44  
45  /**
46   * Provides a data reader for TIFF file images organized by tiles.
47   */
48  public final class DataReaderTiled extends ImageDataReader {
49  
50      private final int tileWidth;
51      private final int tileLength;
52  
53      private final int bitsPerPixel;
54  
55      private final int compression;
56      private final ByteOrder byteOrder;
57  
58      private final AbstractTiffImageData.Tiles imageData;
59  
60      public DataReaderTiled(final TiffDirectory directory, final PhotometricInterpreter photometricInterpreter, final int tileWidth, final int tileLength,
61              final int bitsPerPixel, final int[] bitsPerSample, final int predictor, final int samplesPerPixel, final int sampleFormat, final int width,
62              final int height, final int compression, final TiffPlanarConfiguration planarConfiguration, final ByteOrder byteOrder,
63              final AbstractTiffImageData.Tiles imageData) {
64          super(directory, photometricInterpreter, bitsPerSample, predictor, samplesPerPixel, sampleFormat, width, height, planarConfiguration);
65  
66          this.tileWidth = tileWidth;
67          this.tileLength = tileLength;
68  
69          this.bitsPerPixel = bitsPerPixel;
70          this.compression = compression;
71  
72          this.imageData = imageData;
73          this.byteOrder = byteOrder;
74      }
75  
76      private void interpretTile(final ImageBuilder imageBuilder, final byte[] bytes, final int startX, final int startY, final int xLimit, final int yLimit)
77              throws ImagingException, IOException {
78  
79          // March 2020 change to handle floating-point with compression
80          // for the compressed floating-point, there is a standard that allows
81          // 16 bit floats (which is an IEEE 754 standard) and 24 bits (which is
82          // a non-standard format implemented for TIFF). At this time, this
83          // code only supports the 32-bit and 64-bit formats.
84          if (sampleFormat == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) {
85              // tileLength: number of rows in tile
86              // tileWidth: number of columns in tile
87              final int i0 = startY;
88              int i1 = startY + tileLength;
89              if (i1 > yLimit) {
90                  // the tile is padded past bottom of image
91                  i1 = yLimit;
92              }
93              final int j0 = startX;
94              int j1 = startX + tileWidth;
95              if (j1 > xLimit) {
96                  // the tile is padded to beyond the tile width
97                  j1 = xLimit;
98              }
99              final int[] samples = new int[4];
100             final int[] b = unpackFloatingPointSamples(j1 - j0, i1 - i0, tileWidth, bytes, bitsPerPixel, byteOrder);
101             for (int i = i0; i < i1; i++) {
102                 final int row = i - startY;
103                 final int rowOffset = row * tileWidth;
104                 for (int j = j0; j < j1; j++) {
105                     final int column = j - startX;
106                     final int k = (rowOffset + column) * samplesPerPixel;
107                     samples[0] = b[k];
108                     photometricInterpreter.interpretPixel(imageBuilder, samples, j, i);
109                 }
110             }
111             return;
112         }
113 
114         // End of March 2020 changes to support floating-point format
115         // changes introduced May 2012
116         // The following block of code implements changes that
117         // reduce image loading time by using special-case processing
118         // instead of the general-purpose logic from the original
119         // implementation. For a detailed discussion, see the comments for
120         // a similar treatment in the DataReaderStrip class
121         //
122         // verify that all samples are one byte in size
123         final boolean allSamplesAreOneByte = isHomogenous(8);
124 
125         if ((bitsPerPixel == 24 || bitsPerPixel == 32) && allSamplesAreOneByte && photometricInterpreter instanceof PhotometricInterpreterRgb) {
126             int i1 = startY + tileLength;
127             if (i1 > yLimit) {
128                 // the tile is padded past bottom of image
129                 i1 = yLimit;
130             }
131             int j1 = startX + tileWidth;
132             if (j1 > xLimit) {
133                 // the tile is padded to beyond the tile width
134                 j1 = xLimit;
135             }
136 
137             if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
138                 applyPredictorToBlock(tileWidth, i1 - startY, samplesPerPixel, bytes);
139             }
140 
141             if (bitsPerPixel == 24) {
142                 // 24 bit case, we don't mask the red byte because any
143                 // sign-extended bits get covered by opacity mask
144                 for (int i = startY; i < i1; i++) {
145                     int k = (i - startY) * tileWidth * 3;
146                     for (int j = startX; j < j1; j++, k += 3) {
147                         final int rgb = 0xff000000 | bytes[k] << 16 | (bytes[k + 1] & 0xff) << 8 | bytes[k + 2] & 0xff;
148                         imageBuilder.setRgb(j, i, rgb);
149                     }
150                 }
151             } else if (bitsPerPixel == 32) {
152                 // 32 bit case, we don't mask the high byte because any
153                 // sign-extended bits get shifted up and out of result.
154                 for (int i = startY; i < i1; i++) {
155                     int k = (i - startY) * tileWidth * 4;
156                     for (int j = startX; j < j1; j++, k += 4) {
157                         final int rgb = (bytes[k] & 0xff) << 16 | (bytes[k + 1] & 0xff) << 8 | bytes[k + 2] & 0xff | bytes[k + 3] << 24;
158                         imageBuilder.setRgb(j, i, rgb);
159                     }
160                 }
161             }
162 
163             return;
164         }
165 
166         // End of May 2012 changes
167         try (BitInputStream bis = new BitInputStream(new ByteArrayInputStream(bytes), byteOrder)) {
168 
169             final int pixelsPerTile = tileWidth * tileLength;
170 
171             int tileX = 0;
172             int tileY = 0;
173 
174             int[] samples = Allocator.intArray(bitsPerSampleLength);
175             resetPredictor();
176             for (int i = 0; i < pixelsPerTile; i++) {
177 
178                 final int x = tileX + startX;
179                 final int y = tileY + startY;
180 
181                 getSamplesAsBytes(bis, samples);
182 
183                 if (x < xLimit && y < yLimit) {
184                     samples = applyPredictor(samples);
185                     photometricInterpreter.interpretPixel(imageBuilder, samples, x, y);
186                 }
187 
188                 tileX++;
189 
190                 if (tileX >= tileWidth) {
191                     tileX = 0;
192                     resetPredictor();
193                     tileY++;
194                     bis.flushCache();
195                     if (tileY >= tileLength) {
196                         break;
197                     }
198                 }
199 
200             }
201         }
202     }
203 
204     @Override
205     public ImageBuilder readImageData(final Rectangle subImageSpecification, final boolean hasAlpha, final boolean isAlphaPreMultiplied)
206             throws IOException, ImagingException {
207 
208         final Rectangle subImage;
209         if (subImageSpecification == null) {
210             // configure subImage to read entire image
211             subImage = new Rectangle(0, 0, width, height);
212         } else {
213             subImage = subImageSpecification;
214         }
215 
216         final int bitsPerRow = tileWidth * bitsPerPixel;
217         final int bytesPerRow = (bitsPerRow + 7) / 8;
218         final int bytesPerTile = bytesPerRow * tileLength;
219 
220         // tileWidth is the width of the tile
221         // tileLength is the height of the tile
222         final int col0 = subImage.x / tileWidth;
223         final int col1 = (subImage.x + subImage.width - 1) / tileWidth;
224         final int row0 = subImage.y / tileLength;
225         final int row1 = (subImage.y + subImage.height - 1) / tileLength;
226 
227         final int nCol = col1 - col0 + 1;
228         final int nRow = row1 - row0 + 1;
229         final int workingWidth = nCol * tileWidth;
230         final int workingHeight = nRow * tileLength;
231 
232         final int nColumnsOfTiles = (width + tileWidth - 1) / tileWidth;
233 
234         final int x0 = col0 * tileWidth;
235         final int y0 = row0 * tileLength;
236 
237         // When processing a subimage, the workingBuilder width and height
238         // are set to be integral multiples of the tile width and height.
239         // So the working image may be larger than the specified size of the subimage.
240         // If necessary, the subimage is extracted from the workingBuilder
241         // at the end of this method. This approach avoids the need for the
242         // interpretTile method to implement bounds checking for a subimage.
243         final ImageBuilder workingBuilder = new ImageBuilder(workingWidth, workingHeight, hasAlpha, isAlphaPreMultiplied);
244 
245         for (int iRow = row0; iRow <= row1; iRow++) {
246             for (int iCol = col0; iCol <= col1; iCol++) {
247                 final int tile = iRow * nColumnsOfTiles + iCol;
248                 final byte[] compressed = imageData.tiles[tile].getData();
249                 final int x = iCol * tileWidth - x0;
250                 final int y = iRow * tileLength - y0;
251                 // Handle JPEG based compression
252                 if (compression == TIFF_COMPRESSION_JPEG) {
253                     if (planarConfiguration == TiffPlanarConfiguration.PLANAR) {
254                         throw new ImagingException("TIFF file in non-supported configuration: JPEG compression used in planar configuration.");
255                     }
256                     DataInterpreterJpeg.intepretBlock(directory, workingBuilder, x, y, tileWidth, tileLength, compressed);
257                     continue;
258                 }
259 
260                 final byte[] decompressed = decompress(compressed, compression, bytesPerTile, tileWidth, tileLength);
261 
262                 interpretTile(workingBuilder, decompressed, x, y, width, height);
263             }
264         }
265 
266         if (subImage.x == x0 && subImage.y == y0 && subImage.width == workingWidth && subImage.height == workingHeight) {
267             return workingBuilder;
268         }
269 
270         return workingBuilder.getSubset(subImage.x - x0, subImage.y - y0, subImage.width, subImage.height);
271     }
272 
273     @Override
274     public TiffRasterData readRasterData(final Rectangle subImage) throws ImagingException, IOException {
275         switch (sampleFormat) {
276         case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT:
277             return readRasterDataFloat(subImage);
278         case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER:
279             return readRasterDataInt(subImage);
280         default:
281             throw new ImagingException("Unsupported sample format, value=" + sampleFormat);
282         }
283     }
284 
285     private TiffRasterData readRasterDataFloat(final Rectangle subImage) throws ImagingException, IOException {
286         final int bitsPerRow = tileWidth * bitsPerPixel;
287         final int bytesPerRow = (bitsPerRow + 7) / 8;
288         final int bytesPerTile = bytesPerRow * tileLength;
289         int xRaster;
290         int yRaster;
291         int rasterWidth;
292         int rasterHeight;
293         if (subImage != null) {
294             xRaster = subImage.x;
295             yRaster = subImage.y;
296             rasterWidth = subImage.width;
297             rasterHeight = subImage.height;
298         } else {
299             xRaster = 0;
300             yRaster = 0;
301             rasterWidth = width;
302             rasterHeight = height;
303         }
304         final float[] rasterDataFloat = Allocator.floatArray(rasterWidth * rasterHeight * samplesPerPixel);
305 
306         // tileWidth is the width of the tile
307         // tileLength is the height of the tile
308         final int col0 = xRaster / tileWidth;
309         final int col1 = (xRaster + rasterWidth - 1) / tileWidth;
310         final int row0 = yRaster / tileLength;
311         final int row1 = (yRaster + rasterHeight - 1) / tileLength;
312 
313         final int nColumnsOfTiles = (width + tileWidth - 1) / tileWidth;
314 
315         for (int iRow = row0; iRow <= row1; iRow++) {
316             for (int iCol = col0; iCol <= col1; iCol++) {
317                 final int tile = iRow * nColumnsOfTiles + iCol;
318                 final byte[] compressed = imageData.tiles[tile].getData();
319                 final byte[] decompressed = decompress(compressed, compression, bytesPerTile, tileWidth, tileLength);
320                 final int x = iCol * tileWidth;
321                 final int y = iRow * tileLength;
322 
323                 final int[] blockData = unpackFloatingPointSamples(tileWidth, tileLength, tileWidth, decompressed, bitsPerPixel, byteOrder);
324                 transferBlockToRaster(x, y, tileWidth, tileLength, blockData, xRaster, yRaster, rasterWidth, rasterHeight, samplesPerPixel, rasterDataFloat);
325             }
326         }
327 
328         return new TiffRasterDataFloat(rasterWidth, rasterHeight, samplesPerPixel, rasterDataFloat);
329     }
330 
331     private TiffRasterData readRasterDataInt(final Rectangle subImage) throws ImagingException, IOException {
332         final int bitsPerRow = tileWidth * bitsPerPixel;
333         final int bytesPerRow = (bitsPerRow + 7) / 8;
334         final int bytesPerTile = bytesPerRow * tileLength;
335         int xRaster;
336         int yRaster;
337         int rasterWidth;
338         int rasterHeight;
339         if (subImage != null) {
340             xRaster = subImage.x;
341             yRaster = subImage.y;
342             rasterWidth = subImage.width;
343             rasterHeight = subImage.height;
344         } else {
345             xRaster = 0;
346             yRaster = 0;
347             rasterWidth = width;
348             rasterHeight = height;
349         }
350         final int[] rasterDataInt = Allocator.intArray(rasterWidth * rasterHeight);
351 
352         // tileWidth is the width of the tile
353         // tileLength is the height of the tile
354         final int col0 = xRaster / tileWidth;
355         final int col1 = (xRaster + rasterWidth - 1) / tileWidth;
356         final int row0 = yRaster / tileLength;
357         final int row1 = (yRaster + rasterHeight - 1) / tileLength;
358 
359         final int nColumnsOfTiles = (width + tileWidth - 1) / tileWidth;
360 
361         for (int iRow = row0; iRow <= row1; iRow++) {
362             for (int iCol = col0; iCol <= col1; iCol++) {
363                 final int tile = iRow * nColumnsOfTiles + iCol;
364                 final byte[] compressed = imageData.tiles[tile].getData();
365                 final byte[] decompressed = decompress(compressed, compression, bytesPerTile, tileWidth, tileLength);
366                 final int x = iCol * tileWidth;
367                 final int y = iRow * tileLength;
368                 final int[] blockData = unpackIntSamples(tileWidth, tileLength, tileWidth, decompressed, predictor, bitsPerPixel, byteOrder);
369                 transferBlockToRaster(x, y, tileWidth, tileLength, blockData, xRaster, yRaster, rasterWidth, rasterHeight, rasterDataInt);
370             }
371         }
372         return new TiffRasterDataInt(rasterWidth, rasterHeight, rasterDataInt);
373     }
374 }