001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018/*
019* Implementation notes:
020*    See ImageDataReader and DataReaderStrips for notes on development
021* with particular emphasis on run-time performance.
022*/
023package org.apache.commons.imaging.formats.tiff.datareaders;
024
025import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_JPEG;
026
027import java.awt.Rectangle;
028import java.io.ByteArrayInputStream;
029import java.io.IOException;
030import java.nio.ByteOrder;
031
032import org.apache.commons.imaging.ImagingException;
033import org.apache.commons.imaging.common.Allocator;
034import org.apache.commons.imaging.common.ImageBuilder;
035import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
036import org.apache.commons.imaging.formats.tiff.AbstractTiffRasterData;
037import org.apache.commons.imaging.formats.tiff.TiffDirectory;
038import org.apache.commons.imaging.formats.tiff.TiffRasterDataFloat;
039import org.apache.commons.imaging.formats.tiff.TiffRasterDataInt;
040import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
041import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
042import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter;
043import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb;
044
045/**
046 * Provides a data reader for TIFF file images organized by tiles.
047 */
048public final class DataReaderTiled extends AbstractImageDataReader {
049
050    private final int tileWidth;
051    private final int tileLength;
052
053    private final int bitsPerPixel;
054
055    private final int compression;
056    private final ByteOrder byteOrder;
057
058    private final AbstractTiffImageData.Tiles imageData;
059
060    public DataReaderTiled(final TiffDirectory directory, final AbstractPhotometricInterpreter photometricInterpreter, final int tileWidth,
061            final int tileLength, final int bitsPerPixel, final int[] bitsPerSample, final int predictor, final int samplesPerPixel, final int sampleFormat,
062            final int width, final int height, final int compression, final TiffPlanarConfiguration planarConfiguration, final ByteOrder byteOrder,
063            final AbstractTiffImageData.Tiles imageData) {
064        super(directory, photometricInterpreter, bitsPerSample, predictor, samplesPerPixel, sampleFormat, width, height, planarConfiguration);
065        this.tileWidth = tileWidth;
066        this.tileLength = tileLength;
067        this.bitsPerPixel = bitsPerPixel;
068        this.compression = compression;
069        this.imageData = imageData;
070        this.byteOrder = byteOrder;
071    }
072
073    private void interpretTile(final ImageBuilder imageBuilder, final byte[] bytes, final int startX, final int startY, final int xLimit, final int yLimit)
074            throws ImagingException, IOException {
075
076        // March 2020 change to handle floating-point with compression
077        // for the compressed floating-point, there is a standard that allows
078        // 16 bit floats (which is an IEEE 754 standard) and 24 bits (which is
079        // a non-standard format implemented for TIFF). At this time, this
080        // code only supports the 32-bit and 64-bit formats.
081        if (sampleFormat == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) {
082            // tileLength: number of rows in tile
083            // tileWidth: number of columns in tile
084            final int i0 = startY;
085            int i1 = startY + tileLength;
086            if (i1 > yLimit) {
087                // the tile is padded past bottom of image
088                i1 = yLimit;
089            }
090            final int j0 = startX;
091            int j1 = startX + tileWidth;
092            if (j1 > xLimit) {
093                // the tile is padded to beyond the tile width
094                j1 = xLimit;
095            }
096            final int[] samples = new int[4];
097            final int[] b = unpackFloatingPointSamples(j1 - j0, i1 - i0, tileWidth, bytes, bitsPerPixel, byteOrder);
098            for (int i = i0; i < i1; i++) {
099                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}