Author: damjan Date: Fri Jun 1 15:59:00 2012 New Revision: 1345260 URL: http://svn.apache.org/viewvc?rev=1345260&view=rev Log: Add a streamlined TIFF reader that reduces load time by a factor of 5.
Jira issue key: IMAGING-69 Submitted by: Gary Lucas <gwlucas at sonalysts dot com> Modified: commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java Modified: commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java URL: http://svn.apache.org/viewvc/commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java?rev=1345260&r1=1345259&r2=1345260&view=diff ============================================================================== --- commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java (original) +++ commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java Fri Jun 1 15:59:00 2012 @@ -25,6 +25,7 @@ import org.apache.commons.imaging.common import org.apache.commons.imaging.formats.tiff.TiffDirectory; import org.apache.commons.imaging.formats.tiff.TiffImageData; import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; public final class DataReaderStrips extends DataReader { @@ -52,12 +53,122 @@ public final class DataReaderStrips exte private void interpretStrip(ImageBuilder imageBuilder, byte bytes[], int pixels_per_strip) throws ImageReadException, IOException { - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - BitInputStream bis = new BitInputStream(bais, byteOrder); - if (y >= height) { return; } + + // changes added May 2012 + // In the original implementation, a general-case bit reader called + // getSamplesAsBytes is used to retrieve the samples (raw data values) + // for each pixel in the strip. These samples are then passed into a + // photogrammetric interpreter that converts them to ARGB pixel values + // and stores them in the image. Because the bit-reader must handle + // a large number of formats, it involves several conditional + // branches that must be executed each time a pixel is read. + // Depending on the size of an image, the same evaluations must be + // executed redundantly thousands and perhaps millions of times + // in order to process the complete collection of pixels. + // This code attempts to remove that redundancy by + // evaluating the format up-front and bypassing the general-format + // code for two commonly used data formats: the 8 bits-per-pixel + // and 24 bits-per-pixel cases. For these formats, the + // special case code achieves substantial reductions in image-loading + // time. In other cases, it simply falls through to the original code + // and continues to read the data correctly as it did in previous + // versions of this class. + // In addition to bypassing the getBytesForSample() method, + // the 24-bit case also implements a special block for RGB + // formatted images. To get a sense of the contributions of each + // optimization (removing getSamplesAsBytes and removing the + // photometric interpreter), consider the following results from tests + // conducted with large TIFF images using the 24-bit RGB format + // bypass getSamplesAsBytes: 67.5 % reduction + // bypass both optimizations: 77.2 % reduction + // + // + // Future Changes + // Both of the 8-bit and 24-bit blocks make the assumption that a strip + // always begins on x = 0 and that each strip exactly fills out the rows + // it contains (no half rows). The original code did not make this + // assumption, but the approach is consistent with the TIFF 6.0 spec + // (1992), + // and should probably be considered as an enhancement to the + // original general-case code block that remains from the original + // implementation. Taking this approach saves one conditional + // operation per pixel or about 5 percent of the total run time + // in the 8 bits/pixel case. + + // verify that all samples are one byte in size + boolean allSamplesAreOneByte = true; + for (int i = 0; i < bitsPerSample.length; i++) { + if (bitsPerSample[i] != 8) { + allSamplesAreOneByte = false; + break; + } + } + + if (predictor != 2 && bitsPerPixel == 8 && allSamplesAreOneByte) { + int k = 0; + int nRows = pixels_per_strip / width; + if (y + nRows > height) { + nRows = height - y; + } + int i0 = y; + int i1 = y + nRows; + x = 0; + y += nRows; + int[] samples = new int[1]; + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++) { + samples[0] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + return; + } else if (predictor != 2 && bitsPerPixel == 24 && allSamplesAreOneByte) { + int k = 0; + int nRows = pixels_per_strip / width; + if (y + nRows > height) { + nRows = height - y; + } + int i0 = y; + int i1 = y + nRows; + x = 0; + y += nRows; + if (photometricInterpreter instanceof PhotometricInterpreterRgb) { + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++, k += 3) { + int rgb = 0xff000000 + | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8) + | (bytes[k + 2] & 0xff); + imageBuilder.setRGB(j, i, rgb); + } + } + } else { + int samples[] = new int[3]; + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++) { + samples[0] = bytes[k++] & 0xff; + samples[1] = bytes[k++] & 0xff; + samples[2] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + } + + return; + } + + // ------------------------------------------------------------ + // original code before May 2012 modification + // this logic will handle all cases not conforming to the + // special case handled above + + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + BitInputStream bis = new BitInputStream(bais, byteOrder); + int[] samples = new int[bitsPerSample.length]; resetPredictor(); for (int i = 0; i < pixels_per_strip; i++) { Modified: commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java URL: http://svn.apache.org/viewvc/commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java?rev=1345260&r1=1345259&r2=1345260&view=diff ============================================================================== --- commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java (original) +++ commons/proper/imaging/trunk/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java Fri Jun 1 15:59:00 2012 @@ -25,6 +25,7 @@ import org.apache.commons.imaging.common import org.apache.commons.imaging.formats.tiff.TiffDirectory; import org.apache.commons.imaging.formats.tiff.TiffImageData; import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; public final class DataReaderTiled extends DataReader { @@ -58,6 +59,65 @@ public final class DataReaderTiled exten private void interpretTile(ImageBuilder imageBuilder, byte bytes[], int startX, int startY) throws ImageReadException, IOException { + // changes introduced May 2012 + // The following block of code implements changes that + // reduce image loading time by using special-case processing + // instead of the general-purpose logic from the original + // implementation. For a detailed discussion, see the comments for + // a similar treatment in the DataReaderStrip class + // + + // verify that all samples are one byte in size + boolean allSamplesAreOneByte = true; + for (int i = 0; i < bitsPerSample.length; i++) { + if (bitsPerSample[i] != 8) { + allSamplesAreOneByte = false; + break; + } + } + + if (predictor != 2 && bitsPerPixel == 24 && allSamplesAreOneByte) { + int k = 0; + int i0 = startY; + int i1 = startY + tileLength; + if (i1 > height) { + // the tile is padded past bottom of image + i1 = height - startY; + } + int j0 = startX; + int j1 = startX + tileWidth; + if (j1 > width) { + // the tile is padded to beyond the tile width + j1 = width - startX; + } + if (photometricInterpreter instanceof PhotometricInterpreterRgb) { + for (int i = i0; i < i1; i++) { + k = (i - i0) * tileWidth * 3; + for (int j = j0; j < j1; j++, k += 3) { + int rgb = 0xff000000 + | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8) + | (bytes[k + 2] & 0xff); + imageBuilder.setRGB(j, i, rgb); + } + } + } else { + int samples[] = new int[3]; + for (int i = i0; i < i1; i++) { + k = (i - i0) * tileWidth * 3; + for (int j = j0; j < j1; j++) { + samples[0] = bytes[k++] & 0xff; + samples[1] = bytes[k++] & 0xff; + samples[2] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + } + return; + } + + // End of May 2012 changes + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); BitInputStream bis = new BitInputStream(bais, byteOrder);