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