You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ki...@apache.org on 2020/11/17 05:24:37 UTC

[commons-imaging] branch master updated (1133f3b -> 80e3079)

This is an automated email from the ASF dual-hosted git repository.

kinow pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git.


    from 1133f3b  Merge branch 'pr-107'
     new 74ee172  [Issue-216] Support alpha channel in TIFF RGB formats
     new 0265f93  [Imaging-216] Updated documentation for TIFF writer base class
     new a7957bd  [IMAGING-216] changelog
     new 80e3079  Merge branch 'pr-106'

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/changes/changes.xml                            |   3 +
 .../commons/imaging/common/ImageBuilder.java       | 135 ++++++++++++-------
 .../imaging/formats/tiff/TiffImageParser.java      |  23 +++-
 .../formats/tiff/constants/TiffConstants.java      |  13 ++
 .../formats/tiff/constants/TiffTagConstants.java   |   2 +
 .../formats/tiff/datareaders/DataReaderStrips.java |  62 +++++----
 .../formats/tiff/datareaders/DataReaderTiled.java  |  60 +++++----
 .../formats/tiff/datareaders/ImageDataReader.java  |  26 +++-
 .../formats/tiff/write/TiffImageWriterBase.java    |  98 +++++++++++++-
 .../commons/imaging/common/ImageBuilderTest.java   | 146 +++++++++++++++++++++
 .../formats/tiff/TiffAlphaRoundTripTest.java       | 134 +++++++++++++++++++
 .../imaging/formats/tiff/TiffReadAlphaTest.java    |  91 +++++++++++++
 12 files changed, 678 insertions(+), 115 deletions(-)
 create mode 100644 src/test/java/org/apache/commons/imaging/common/ImageBuilderTest.java
 create mode 100644 src/test/java/org/apache/commons/imaging/formats/tiff/TiffAlphaRoundTripTest.java
 create mode 100644 src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadAlphaTest.java


[commons-imaging] 03/04: [IMAGING-216] changelog

Posted by ki...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit a7957bd6ce82f204a9135239aa207865b673d5b2
Author: Bruno P. Kinoshita <ki...@apache.org>
AuthorDate: Tue Nov 17 18:17:27 2020 +1300

    [IMAGING-216] changelog
---
 src/changes/changes.xml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 020fbb8..f78de5d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,6 +45,9 @@ The <action> type attribute can be add,update,fix,remove.
   </properties>
   <body>
     <release version="1.0-alpha3" date="2020-??-??" description="Third 1.0 alpha release">
+      <action issue="IMAGING-216" dev="kinow" type="add" due-to="Gary Lucas">
+        Imaging.getBufferedImage() drops alpha layer for TIFF images.
+      </action>
       <action issue="IMAGING-269" dev="kinow" type="add" due-to="Gary Lucas">
         Consolidate redundant methods in TIFF datareaders.
       </action>


[commons-imaging] 01/04: [Issue-216] Support alpha channel in TIFF RGB formats

Posted by ki...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit 74ee172f3de061152b780c15eef732a3189e0eac
Author: gwlucastrig <co...@gmail.com>
AuthorDate: Fri Oct 30 07:59:52 2020 -0400

    [Issue-216] Support alpha channel in TIFF RGB formats
---
 .../commons/imaging/common/ImageBuilder.java       | 135 ++++++++++++-------
 .../imaging/formats/tiff/TiffImageParser.java      |  23 +++-
 .../formats/tiff/constants/TiffConstants.java      |  13 ++
 .../formats/tiff/constants/TiffTagConstants.java   |   2 +
 .../formats/tiff/datareaders/DataReaderStrips.java |  62 +++++----
 .../formats/tiff/datareaders/DataReaderTiled.java  |  60 +++++----
 .../formats/tiff/datareaders/ImageDataReader.java  |  26 +++-
 .../formats/tiff/write/TiffImageWriterBase.java    |  88 ++++++++++++-
 .../commons/imaging/common/ImageBuilderTest.java   | 146 +++++++++++++++++++++
 .../formats/tiff/TiffAlphaRoundTripTest.java       | 134 +++++++++++++++++++
 .../imaging/formats/tiff/TiffReadAlphaTest.java    |  91 +++++++++++++
 11 files changed, 665 insertions(+), 115 deletions(-)

diff --git a/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java
index 6479739..1d56b52 100644
--- a/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java
+++ b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java
@@ -40,8 +40,10 @@
  */
 package org.apache.commons.imaging.common;
 
+import java.awt.color.ColorSpace;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
 import java.awt.image.DataBufferInt;
 import java.awt.image.DirectColorModel;
 import java.awt.image.Raster;
@@ -58,6 +60,7 @@ public class ImageBuilder {
     private final int width;
     private final int height;
     private final boolean hasAlpha;
+    private final boolean isAlphaPremultiplied;
 
     /**
      * Construct an ImageBuilder instance
@@ -68,6 +71,38 @@ public class ImageBuilder {
      * requirements for the ImageBuilder or resulting BufferedImage.
      */
     public ImageBuilder(final int width, final int height, final boolean hasAlpha) {
+        checkDimensions(width, height);
+
+        data = new int[width * height];
+        this.width = width;
+        this.height = height;
+        this.hasAlpha = hasAlpha;
+        this.isAlphaPremultiplied = false;
+    }
+
+
+    /**
+     * Construct an ImageBuilder instance
+     * @param width the width of the image to be built
+     * @param height the height of the image to be built
+     * @param hasAlpha indicates whether the image has an alpha channel
+     * (the selection of alpha channel does not change the memory
+     * requirements for the ImageBuilder or resulting BufferedImage.
+     * @param isAlphaPremultiplied indicates whether alpha values are
+     * pre-multiplied; this setting is relevant only if alpha is true.
+     *
+     */
+    public ImageBuilder(final int width, final int height,
+        final boolean hasAlpha, boolean isAlphaPremultiplied) {
+        checkDimensions(width, height);
+        data = new int[width * height];
+        this.width = width;
+        this.height = height;
+        this.hasAlpha = hasAlpha;
+        this.isAlphaPremultiplied = isAlphaPremultiplied;
+    }
+
+    private void checkDimensions(int width, int height) {
         if (width <= 0) {
             throw new RasterFormatException("zero or negative width value");
         }
@@ -75,10 +110,6 @@ public class ImageBuilder {
             throw new RasterFormatException("zero or negative height value");
         }
 
-        data = new int[width * height];
-        this.width = width;
-        this.height = height;
-        this.hasAlpha = hasAlpha;
     }
 
     /**
@@ -131,25 +162,18 @@ public class ImageBuilder {
         return makeBufferedImage(data, width, height, hasAlpha);
     }
 
-
-     /**
-     * Gets a subset of the ImageBuilder content using the specified parameters
-     * to indicate an area of interest. If the parameters specify a rectangular
-     * region that is not entirely contained within the bounds defined
-     * by the ImageBuilder, this method will throw a RasterFormatException.
-     * This run- time exception is consistent with the behavior of the
-     * getSubimage method provided by BufferedImage.
+    /**
+     * Performs a check on the specified sub-region to verify
+     * that it is within the constraints of the ImageBuilder bounds.
+     *
      * @param x the X coordinate of the upper-left corner of the
-     *          specified rectangular region
+     * specified rectangular region
      * @param y the Y coordinate of the upper-left corner of the
-     *          specified rectangular region
+     * specified rectangular region
      * @param w the width of the specified rectangular region
      * @param h the height of the specified rectangular region
-     * @return a valid instance of the specified width and height.
-     * @throws RasterFormatException if the specified area is not contained
-     *         within this ImageBuilder
      */
-    public ImageBuilder getSubset(final int x, final int y, final int w, final int h) {
+    private void checkBounds(int x, int y, int w, int h) {
         if (w <= 0) {
             throw new RasterFormatException("negative or zero subimage width");
         }
@@ -161,17 +185,37 @@ public class ImageBuilder {
         }
         if (x + w > width) {
             throw new RasterFormatException(
-                    "subimage (x+width) is outside raster");
+                "subimage (x+width) is outside raster");
         }
         if (y < 0 || y >= height) {
             throw new RasterFormatException("subimage y is outside raster");
         }
         if (y + h > height) {
             throw new RasterFormatException(
-                    "subimage (y+height) is outside raster");
+                "subimage (y+height) is outside raster");
         }
+    }
 
-        ImageBuilder b = new ImageBuilder(w, h, hasAlpha);
+     /**
+     * Gets a subset of the ImageBuilder content using the specified parameters
+     * to indicate an area of interest. If the parameters specify a rectangular
+     * region that is not entirely contained within the bounds defined
+     * by the ImageBuilder, this method will throw a RasterFormatException.
+     * This run- time exception is consistent with the behavior of the
+     * getSubimage method provided by BufferedImage.
+     * @param x the X coordinate of the upper-left corner of the
+     *          specified rectangular region
+     * @param y the Y coordinate of the upper-left corner of the
+     *          specified rectangular region
+     * @param w the width of the specified rectangular region
+     * @param h the height of the specified rectangular region
+     * @return a valid instance of the specified width and height.
+     * @throws RasterFormatException if the specified area is not contained
+     *         within this ImageBuilder
+     */
+    public ImageBuilder getSubset(final int x, final int y, final int w, final int h) {
+        checkBounds(x, y, w, h);
+        ImageBuilder b = new ImageBuilder(w, h, hasAlpha, isAlphaPremultiplied);
         for(int i=0; i<h; i++){
             int srcDex = (i+y)*width+x;
             int outDex = i*w;
@@ -200,27 +244,7 @@ public class ImageBuilder {
      *         within this ImageBuilder
      */
     public BufferedImage getSubimage(final int x, final int y, final int w, final int h) {
-        if (w <= 0) {
-            throw new RasterFormatException("negative or zero subimage width");
-        }
-        if (h <= 0) {
-            throw new RasterFormatException("negative or zero subimage height");
-        }
-        if (x < 0 || x >= width) {
-            throw new RasterFormatException("subimage x is outside raster");
-        }
-        if (x + w > width) {
-            throw new RasterFormatException(
-                    "subimage (x+width) is outside raster");
-        }
-        if (y < 0 || y >= height) {
-            throw new RasterFormatException("subimage y is outside raster");
-        }
-        if (y + h > height) {
-            throw new RasterFormatException(
-                    "subimage (y+height) is outside raster");
-        }
-
+        checkBounds(x, y, w, h);
 
         // Transcribe the data to an output image array
         final int[] argb = new int[w * h];
@@ -242,16 +266,29 @@ public class ImageBuilder {
         WritableRaster raster;
         final DataBufferInt buffer = new DataBufferInt(argb, w * h);
         if (useAlpha) {
-            colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00,
-                    0x000000ff, 0xff000000);
-            raster = Raster.createPackedRaster(buffer, w, h,
-                    w, new int[]{0x00ff0000, 0x0000ff00, 0x000000ff,
-                            0xff000000}, null);
+            colorModel = new DirectColorModel(
+                    ColorSpace.getInstance(ColorSpace.CS_sRGB),
+                    32,
+                    0x00ff0000, 0x0000ff00,
+                    0x000000ff, 0xff000000,
+                    isAlphaPremultiplied, DataBuffer.TYPE_INT);
+            raster = Raster.createPackedRaster(
+                    buffer, w, h, w,
+                    new int[]{
+                            0x00ff0000,
+                            0x0000ff00,
+                            0x000000ff,
+                            0xff000000},
+                    null);
         } else {
             colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00,
                     0x000000ff);
-            raster = Raster.createPackedRaster(buffer, w, h,
-                    w, new int[]{0x00ff0000, 0x0000ff00, 0x000000ff},
+            raster = Raster.createPackedRaster(
+                    buffer, w, h, w,
+                    new int[]{
+                        0x00ff0000,
+                        0x0000ff00,
+                        0x000000ff},
                     null);
         }
         return new BufferedImage(colorModel, raster,
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java
index deadb89..0cd7b5c 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java
@@ -554,8 +554,6 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable {
             throw new ImageReadException("TIFF missing entries");
         }
 
-        final int photometricInterpretation = 0xffff & directory.getFieldValue(
-                TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);
         final short compressionFieldValue;
         if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
             compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
@@ -628,6 +626,24 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable {
                     + bitsPerSample.length + ")");
         }
 
+
+        final int photometricInterpretation = 0xffff & directory.getFieldValue(
+                TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);
+
+        final boolean hasAlpha =
+            photometricInterpretation == TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB
+            && samplesPerPixel==4;
+        boolean isAlphaPremultiplied = false;
+        if(hasAlpha){
+            final TiffField extraSamplesField =
+                directory.findField(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES);
+            if (extraSamplesField != null) {
+                int extraSamplesValue = extraSamplesField.getIntValue();
+                isAlphaPremultiplied =
+                    (extraSamplesValue==TiffTagConstants.EXTRA_SAMPLE_ASSOCIATED_ALPHA);
+            }
+        }
+
         PhotometricInterpreter photometricInterpreter;
         Object test = params == null
             ? null
@@ -669,7 +685,8 @@ public class TiffImageParser extends ImageParser implements XmpEmbeddable {
           samplesPerPixel, width, height, compression,
           planarConfiguration, byteOrder);
 
-        final ImageBuilder iBuilder = dataReader.readImageData(subImage);
+        final ImageBuilder iBuilder = dataReader.readImageData(
+            subImage, hasAlpha, isAlphaPremultiplied);
         return iBuilder.getBufferedImage();
     }
 
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java
index e2bcb7c..42505bf 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java
@@ -18,6 +18,10 @@ package org.apache.commons.imaging.formats.tiff.constants;
 
 import java.nio.ByteOrder;
 
+/**
+ * Defines constants for internal elements from TIFF files and for allowing
+ * applications to define parameters for reading and writing TIFF files.
+ */
 public final class TiffConstants {
 
     public static final ByteOrder DEFAULT_TIFF_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
@@ -70,6 +74,15 @@ public final class TiffConstants {
     public static final String PARAM_KEY_SUBIMAGE_WIDTH = "SUBIMAGE_WIDTH";
     public static final String PARAM_KEY_SUBIMAGE_HEIGHT = "SUBIMAGE_HEIGHT";
 
+
+    /**
+     * Specifies that an application-specified photometric interpreter
+     * is to be used when reading TIFF files to convert raster data samples
+     * to RGB values for the output image.
+     * <p>
+     * The value supplied with this key should be a valid instance of
+     * a class that implements PhotometricInterpreter.
+     */
     public static final String PARAM_KEY_CUSTOM_PHOTOMETRIC_INTERPRETER
         = "CUSTOM_PHOTOMETRIC_INTERPRETER";
 
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java
index ccdd842..27eeabb 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java
@@ -347,6 +347,8 @@ public final class TiffTagConstants {
     public static final TagInfoShorts TIFF_TAG_EXTRA_SAMPLES = new TagInfoShorts(
             "ExtraSamples", 0x152, -1,
             TiffDirectoryType.TIFF_DIRECTORY_ROOT);
+    public static final int EXTRA_SAMPLE_ASSOCIATED_ALPHA = 1;
+    public static final int EXTRA_SAMPLE_UNASSOCIATED_ALPHA = 2;
 
     public static final TagInfoShorts TIFF_TAG_SAMPLE_FORMAT = new TagInfoShorts(
             "SampleFormat", 0x153, -1,
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java
index 8cd5637..943b071 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java
@@ -24,9 +24,9 @@ import java.nio.ByteOrder;
 
 import org.apache.commons.imaging.ImageReadException;
 import org.apache.commons.imaging.common.ImageBuilder;
+import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.TiffDirectory;
 import org.apache.commons.imaging.formats.tiff.TiffImageData;
-import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
 import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
@@ -167,7 +167,7 @@ public final class DataReaderStrips extends ImageDataReader {
                 }
             }
             return;
-        } else if (bitsPerPixel == 24 && allSamplesAreOneByte
+        } else if ((bitsPerPixel == 24  || bitsPerPixel==32) && allSamplesAreOneByte
             && photometricInterpreter instanceof PhotometricInterpreterRgb) {
             int k = 0;
             int nRows = pixelsPerStrip / width;
@@ -178,29 +178,36 @@ public final class DataReaderStrips extends ImageDataReader {
             final int i1 = y + nRows;
             x = 0;
             y += nRows;
-            if (predictor == 2) {
+            if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
+                applyPredictorToBlock(width, nRows, samplesPerPixel, bytes);
+            }
+
+            if (bitsPerPixel == 24) {
+                // 24 bit case, we don't mask the red byte because any
+                // sign-extended bits get covered by opacity mask
+                k = 0;
                 for (int i = i0; i < i1; i++) {
-                    int p0 = bytes[k++] & 0xff;
-                    int p1 = bytes[k++] & 0xff;
-                    int p2 = bytes[k++] & 0xff;
-                    for (int j = 1; j < width; j++) {
-                        p0 = (bytes[k] + p0) & 0xff;
-                        bytes[k++] = (byte) p0;
-                        p1 = (bytes[k] + p1) & 0xff;
-                        bytes[k++] = (byte) p1;
-                        p2 = (bytes[k] + p2) & 0xff;
-                        bytes[k++] = (byte) p2;
+                    for (int j = 0; j < width; j++, k += 3) {
+                        final int rgb = 0xff000000
+                            | (bytes[k] << 16)
+                            | ((bytes[k + 1] & 0xff) << 8)
+                            | (bytes[k + 2] & 0xff);
+                        imageBuilder.setRGB(j, i, rgb);
                     }
                 }
-            }
-
-            k = 0;
-            for (int i = i0; i < i1; i++) {
-                for (int j = 0; j < width; j++, k += 3) {
-                    final int rgb = 0xff000000
-                        | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8)
-                        | (bytes[k + 2] & 0xff);
-                    imageBuilder.setRGB(j, i, rgb);
+            } else {
+                // 32 bit case, we don't mask the high byte because any
+                // sign-extended bits get shifted up and out of result
+                k = 0;
+                for (int i = i0; i < i1; i++) {
+                    for (int j = 0; j < width; j++, k += 4) {
+                        final int rgb
+                            = ((bytes[k] & 0xff) << 16)
+                            | ((bytes[k + 1] & 0xff) << 8)
+                            | (bytes[k + 2] & 0xff)
+                            | (bytes[k + 3] << 24);
+                        imageBuilder.setRGB(j, i, rgb);
+                    }
                 }
             }
 
@@ -241,7 +248,9 @@ public final class DataReaderStrips extends ImageDataReader {
 
 
     @Override
-    public ImageBuilder readImageData(final Rectangle subImageSpecification)
+    public ImageBuilder readImageData(final Rectangle subImageSpecification,
+        final boolean hasAlpha,
+        final boolean isAlphaPremultiplied)
             throws ImageReadException, IOException {
 
         final Rectangle subImage;
@@ -278,7 +287,8 @@ public final class DataReaderStrips extends ImageDataReader {
         // TO DO: we can probably save some processing by using yLimit instead
         //        or working
         final ImageBuilder workingBuilder =
-                new ImageBuilder(width, workingHeight, false);
+                new ImageBuilder(width, workingHeight,
+                    hasAlpha, isAlphaPremultiplied);
         if (planarConfiguration != TiffPlanarConfiguration.PLANAR) {
             for (int strip = strip0; strip <= strip1; strip++) {
                 final long rowsPerStripLong = 0xFFFFffffL & rowsPerStrip;
@@ -384,10 +394,10 @@ public final class DataReaderStrips extends ImageDataReader {
                 bytesPerStrip, width, rowsInThisStrip);
 
             int[] blockData = unpackFloatingPointSamples(
-                width, rowsInThisStrip, width,
+                width, (int) rowsInThisStrip, width,
                 decompressed,
                 predictor, bitsPerPixel, byteOrder);
-            transferBlockToRaster(0, yStrip, width, rowsInThisStrip, blockData,
+            transferBlockToRaster(0, yStrip, width, (int) rowsInThisStrip, blockData,
                 xRaster, yRaster, rasterWidth, rasterHeight, rasterData);
         }
         return new TiffRasterData(rasterWidth, rasterHeight, rasterData);
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java
index 82067bb..fa43254 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java
@@ -30,9 +30,9 @@ import java.nio.ByteOrder;
 
 import org.apache.commons.imaging.ImageReadException;
 import org.apache.commons.imaging.common.ImageBuilder;
+import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.TiffDirectory;
 import org.apache.commons.imaging.formats.tiff.TiffImageData;
-import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
 import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
@@ -127,7 +127,7 @@ public final class DataReaderTiled extends ImageDataReader {
         // verify that all samples are one byte in size
         final boolean allSamplesAreOneByte = isHomogenous(8);
 
-          if (bitsPerPixel == 24 && allSamplesAreOneByte
+        if ((bitsPerPixel == 24 || bitsPerPixel == 32) && allSamplesAreOneByte
             && photometricInterpreter instanceof PhotometricInterpreterRgb) {
             final int i0 = startY;
             int i1 = startY + tileLength;
@@ -141,32 +141,37 @@ public final class DataReaderTiled extends ImageDataReader {
                 // the tile is padded to beyond the tile width
                 j1 = xLimit;
             }
-            if (predictor == 2) {
-                // pre-apply the predictor logic before feeding
-                // the bytes to the photometric interpretor.
+
+            if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
+                applyPredictorToBlock(tileWidth, i1 - i0, samplesPerPixel, bytes);
+            }
+
+            if (bitsPerPixel == 24) {
+                // 24 bit case, we don't mask the red byte because any
+                // sign-extended bits get covered by opacity mask
                 for (int i = i0; i < i1; i++) {
                     int k = (i - i0) * tileWidth * 3;
-                    int p0 = bytes[k++] & 0xff;
-                    int p1 = bytes[k++] & 0xff;
-                    int p2 = bytes[k++] & 0xff;
-                    for (int j = 1; j < tileWidth; j++) {
-                        p0 = (bytes[k] + p0) & 0xff;
-                        bytes[k++] = (byte) p0;
-                        p1 = (bytes[k] + p1) & 0xff;
-                        bytes[k++] = (byte) p1;
-                        p2 = (bytes[k] + p2) & 0xff;
-                        bytes[k++] = (byte) p2;
+                    for (int j = j0; j < j1; j++, k += 3) {
+                        final int rgb = 0xff000000
+                            | (bytes[k] << 16)
+                            | ((bytes[k + 1] & 0xff) << 8)
+                            | (bytes[k + 2] & 0xff);
+                        imageBuilder.setRGB(j, i, rgb);
                     }
                 }
-            }
-
-            for (int i = i0; i < i1; i++) {
-                int k = (i - i0) * tileWidth * 3;
-                for (int j = j0; j < j1; j++, k += 3) {
-                    final int rgb = 0xff000000
-                        | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8)
-                        | (bytes[k + 2] & 0xff);
-                    imageBuilder.setRGB(j, i, rgb);
+            } else if (bitsPerPixel == 32) {
+                // 32 bit case, we don't mask the high byte because any
+                // sign-extended bits get shifted up and out of result.
+                for (int i = i0; i < i1; i++) {
+                    int k = (i - i0) * tileWidth * 4;
+                    for (int j = j0; j < j1; j++, k += 4) {
+                        final int rgb
+                            = ((bytes[k] & 0xff) << 16)
+                            | ((bytes[k + 1] & 0xff) << 8)
+                            | (bytes[k + 2] & 0xff)
+                            | (bytes[k + 3] << 24);
+                        imageBuilder.setRGB(j, i, rgb);
+                    }
                 }
             }
 
@@ -213,7 +218,9 @@ public final class DataReaderTiled extends ImageDataReader {
     }
 
     @Override
-    public ImageBuilder readImageData(final Rectangle subImageSpecification)
+    public ImageBuilder readImageData(final Rectangle subImageSpecification,
+        final boolean hasAlpha,
+        final boolean isAlphaPremultiplied)
             throws ImageReadException, IOException {
 
         final Rectangle subImage;
@@ -248,7 +255,8 @@ public final class DataReaderTiled extends ImageDataReader {
         final int y0 = row0 * tileLength;
 
         final ImageBuilder workingBuilder =
-                new ImageBuilder(workingWidth, workingHeight, false);
+                new ImageBuilder(workingWidth, workingHeight,
+                hasAlpha, isAlphaPremultiplied);
 
         for (int iRow = row0; iRow <= row1; iRow++) {
             for (int iCol = col0; iCol <= col1; iCol++) {
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java
index 8660251..ade9052 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/ImageDataReader.java
@@ -19,8 +19,8 @@ package org.apache.commons.imaging.formats.tiff.datareaders;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_1D;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4;
-import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_PKZIP;
+import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_LZW;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_PACKBITS;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED;
@@ -39,12 +39,12 @@ import java.util.Arrays;
 import org.apache.commons.imaging.ImageReadException;
 import org.apache.commons.imaging.common.ImageBuilder;
 import org.apache.commons.imaging.common.PackBits;
-import org.apache.commons.imaging.common.ZlibDeflate;
 import org.apache.commons.imaging.common.itu_t4.T4AndT6Compression;
 import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor;
+import org.apache.commons.imaging.common.ZlibDeflate;
+import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.TiffDirectory;
 import org.apache.commons.imaging.formats.tiff.TiffField;
-import org.apache.commons.imaging.formats.tiff.TiffRasterData;
 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
 import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
 
@@ -179,12 +179,19 @@ public abstract class ImageDataReader {
      * if desired.
      * @param subImageSpecification a rectangle describing a sum-region of
      * the image for reading, or a null if the whole image is to be read.
+     * @param hasAlpha indicates that the image has an alpha (transparency)
+     * channel (RGB color model only).
+     * @param isAlphaPremultiplied indicates that the image uses the
+     * associated alpha channel format (pre-multiplied alpha).
      * @return a valid instance containing the pixel data from the image.
      * @throws ImageReadException in the event of a data format error
      * or other TIFF-specific failure.
      * @throws IOException in the event of an unrecoverable I/O error.
      */
-    public abstract ImageBuilder readImageData(Rectangle subImageSpecification)
+    public abstract ImageBuilder readImageData(
+        Rectangle subImageSpecification,
+        boolean hasAlpha,
+        boolean isAlphaPremultiplied)
             throws ImageReadException, IOException;
 
     /**
@@ -244,6 +251,17 @@ public abstract class ImageDataReader {
         return samples;
     }
 
+    protected void applyPredictorToBlock(int width, int height, int nSamplesPerPixel, byte []p ){
+        final int k = width*nSamplesPerPixel;
+        for(int i=0; i<height; i++){
+            int j0  = i*k+nSamplesPerPixel;
+            int j1 = (i+1)*k;
+            for(int j=j0; j<j1; j++){
+                p[j]+=p[j-nSamplesPerPixel];
+            }
+        }
+    }
+
     protected byte[] decompress(final byte[] compressedInput, final int compression,
             final int expectedSize, final int tileWidth, final int tileHeight)
             throws ImageReadException, IOException {
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
index 6a6eea5..ec890fd 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
@@ -23,14 +23,15 @@ import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.PA
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_1D;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4;
-import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_LZW;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_PACKBITS;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED;
+import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE;
 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_HEADER_SIZE;
 
 import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.ByteOrder;
@@ -48,9 +49,9 @@ import org.apache.commons.imaging.PixelDensity;
 import org.apache.commons.imaging.common.BinaryOutputStream;
 import org.apache.commons.imaging.common.PackBits;
 import org.apache.commons.imaging.common.RationalNumber;
-import org.apache.commons.imaging.common.ZlibDeflate;
 import org.apache.commons.imaging.common.itu_t4.T4AndT6Compression;
 import org.apache.commons.imaging.common.mylzw.MyLzwCompressor;
+import org.apache.commons.imaging.common.ZlibDeflate;
 import org.apache.commons.imaging.formats.tiff.TiffElement;
 import org.apache.commons.imaging.formats.tiff.TiffImageData;
 import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
@@ -257,6 +258,48 @@ public abstract class TiffImageWriterBase {
         // Debug.debug();
     }
 
+    private static final int MAX_PIXELS_FOR_RGB = 1024*1024;
+    /**
+     * Check an image to see if any of its pixels are non-opaque.
+     * @param src a valid image
+     * @return true if at least one non-opaque pixel is found.
+     */
+    private boolean checkForActualAlpha(BufferedImage src){
+        // to conserve memory, very large images may be read
+        // in pieces.
+        final int width = src.getWidth();
+        final int height = src.getHeight();
+        int nRowsPerRead = MAX_PIXELS_FOR_RGB/width;
+        if(nRowsPerRead<1){
+            nRowsPerRead = 1;
+        }
+        int nReads = (height+nRowsPerRead-1)/nRowsPerRead;
+        int []argb = new int[nRowsPerRead*width];
+        for(int iRead=0; iRead<nReads; iRead++){
+            final int i0 = iRead*nRowsPerRead;
+            final int i1 = i0+nRowsPerRead>height? height: i0+nRowsPerRead;
+            src.getRGB(0, i0, width, i1-i0, argb, 0, width);
+            int n = (i1-i0)*width;
+            for(int i=0; i<n; i++){
+                if((argb[i]&0xff000000)!=0xff000000){
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void applyPredictor(int width, int bytesPerSample, byte[] b) {
+        int nBytesPerRow = bytesPerSample * width;
+        int nRows = b.length / nBytesPerRow;
+        for (int iRow = 0; iRow < nRows; iRow++) {
+            int offset = iRow * nBytesPerRow;
+            for (int i = nBytesPerRow-1; i >= bytesPerSample; i--) {
+                b[offset + i] -= b[offset + i - bytesPerSample];
+            }
+        }
+    }
+
     public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
             throws ImageWriteException, IOException {
         // make copy of params; we'll clear keys as we consume them.
@@ -287,7 +330,20 @@ public abstract class TiffImageWriterBase {
         final int width = src.getWidth();
         final int height = src.getHeight();
 
-        int compression = TIFF_COMPRESSION_LZW; // LZW is default
+        final ColorModel cModel = src.getColorModel();
+        final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
+
+        // 10/2020: In the case of an image with pre-multiplied alpha
+        // (what the TIFF specification calls "associated alpha"), the
+        // Java getRGB method adjusts the value to a non-premultiplied
+        // alpha state.  However, this class could access the pre-multiplied
+        // alpha data by obtaining the underlying raster.  At this time,
+        // the value of such a little-used feature does not seem
+        // commensurate with the complexity of the extra code it would require.
+
+        int compression = TIFF_COMPRESSION_LZW;
+        short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE;
+
         int stripSizeInBits = 64000; // the default from legacy implementation
         if (params.containsKey(ImagingConstants.PARAM_KEY_COMPRESSION)) {
             final Object value = params.get(ImagingConstants.PARAM_KEY_COMPRESSION);
@@ -335,7 +391,7 @@ public abstract class TiffImageWriterBase {
             bitsPerSample = 1;
             photometricInterpretation = 0;
         } else {
-            samplesPerPixel = 3;
+            samplesPerPixel = hasAlpha? 4: 3;
             bitsPerSample = 8;
             photometricInterpretation = 2;
         }
@@ -402,19 +458,21 @@ public abstract class TiffImageWriterBase {
                 strips[i] = new PackBits().compress(strips[i]);
             }
         } else if (compression == TIFF_COMPRESSION_LZW) {
+            predictor =  TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
             for (int i = 0; i < strips.length; i++) {
                 final byte[] uncompressed = strips[i];
+                this.applyPredictor(width, samplesPerPixel, strips[i]);
 
                 final int LZW_MINIMUM_CODE_SIZE = 8;
-
                 final MyLzwCompressor compressor = new MyLzwCompressor(
                         LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN, true);
                 final byte[] compressed = compressor.compress(uncompressed);
-
                 strips[i] = compressed;
             }
         } else if (compression == TIFF_COMPRESSION_DEFLATE_ADOBE) {
+            predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
             for (int i = 0; i < strips.length; i++) {
+                this.applyPredictor(width, samplesPerPixel, strips[i]);
                 strips[i] = ZlibDeflate.compress(strips[i]);
             }
         } else if (compression == TIFF_COMPRESSION_UNCOMPRESSED) {
@@ -449,6 +507,12 @@ public abstract class TiffImageWriterBase {
                 directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE,
                         (short) bitsPerSample, (short) bitsPerSample,
                         (short) bitsPerSample);
+            }else if (samplesPerPixel == 4) {
+                directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE,
+                        (short) bitsPerSample, (short) bitsPerSample,
+                        (short) bitsPerSample, (short) bitsPerSample);
+                directory.add(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES,
+                    (short)TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA);
             } else if (samplesPerPixel == 1) {
                 directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE,
                         (short) bitsPerSample);
@@ -502,6 +566,10 @@ public abstract class TiffImageWriterBase {
                 directory.add(TiffTagConstants.TIFF_TAG_XMP, xmpXmlBytes);
             }
 
+            if(predictor==TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING){
+                directory.add(TiffTagConstants.TIFF_TAG_PREDICTOR, predictor);
+            }
+
         }
 
         final TiffImageData tiffImageData = new TiffImageData.Strips(imageData,
@@ -586,7 +654,13 @@ public abstract class TiffImageWriterBase {
                                 bitCache = 0;
                                 bitsInCache = 0;
                             }
-                        } else {
+                        } else if(samplesPerPixel==4){
+                            uncompressed[counter++] = (byte) red;
+                            uncompressed[counter++] = (byte) green;
+                            uncompressed[counter++] = (byte) blue;
+                            uncompressed[counter++] = (byte) (rgb>>24);
+                        }else {
+                            // samples per pixel is 3
                             uncompressed[counter++] = (byte) red;
                             uncompressed[counter++] = (byte) green;
                             uncompressed[counter++] = (byte) blue;
diff --git a/src/test/java/org/apache/commons/imaging/common/ImageBuilderTest.java b/src/test/java/org/apache/commons/imaging/common/ImageBuilderTest.java
new file mode 100644
index 0000000..f03d2c2
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/common/ImageBuilderTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.imaging.common;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.RasterFormatException;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Provides unit tests for the ImageBuilder class.
+ */
+public class ImageBuilderTest {
+
+
+    /**
+     * Test of bad dimensions in constructor
+     */
+    @Test
+    public void testConstructorBounds() {
+        executeBadConstructor(0, 10);
+        executeBadConstructor(10, 0);
+    }
+
+
+    /**
+     * Test of bad bounds in sub-image
+     */
+    @Test
+    public void testBoundsCheck() {
+
+        ImageBuilder imageBuilder = new ImageBuilder(100, 100, false );
+
+        executeBadBounds(imageBuilder, -1,  0, 50, 50);
+        executeBadBounds(imageBuilder,  0, -1, 50, 50);
+        executeBadBounds(imageBuilder,  0,  0,  0, 50);
+        executeBadBounds(imageBuilder,  0,  0, 50, 0);
+        executeBadBounds(imageBuilder, 90,  0, 50, 50);
+        executeBadBounds(imageBuilder,  0, 90, 50, 50);
+    }
+
+    /**
+     * Test whether sub-image is consistent with source
+     */
+    @Test
+    public void testSubimageAccess() {
+        ImageBuilder imageBuilder = new ImageBuilder(100, 100, false );
+        populate(imageBuilder);
+        BufferedImage bImage = imageBuilder.getSubimage(25, 25, 25, 25);
+        int w = bImage.getWidth();
+        int h = bImage.getHeight();
+        assertEquals(w, 25, "Width of subimage does not match");
+        assertEquals(h, 25, "Height of subimage does not match");
+
+        for(int x=25; x<50; x++){
+            for(int y=25; y<50; y++){
+                int k = bImage.getRGB(x-25, y-25);
+                int rgb = imageBuilder.getRGB(x, y);
+                assertEquals(k, rgb, "Invalid buffered image subpixel at "+x+", "+y);
+            }
+        }
+
+        ImageBuilder testBuilder = imageBuilder.getSubset(25, 25, 25, 25);
+        for(int x=25; x<50; x++){
+            for(int y=25; y<50; y++){
+                int k = testBuilder.getRGB(x-25, y-25);
+                int rgb = imageBuilder.getRGB(x, y);
+                assertEquals(k, rgb, "Invalid image builder subpixel at "+x+", "+y);
+            }
+        }
+    }
+
+    /**
+     * Test whether color model is properly applied to buffered images
+     */
+    @Test
+    void testImageColorModel() {
+        ImageBuilder  imageBuilder;
+        BufferedImage bImage;
+        ColorModel    model;
+        imageBuilder = new ImageBuilder(100, 100, false );
+        bImage = imageBuilder.getBufferedImage();
+        model = bImage.getColorModel();
+        assertFalse(model.hasAlpha(), "Output image has alpha where not specified");
+
+        imageBuilder = new ImageBuilder(100, 100, true, false);
+        bImage = imageBuilder.getBufferedImage();
+        model = bImage.getColorModel();
+        assertTrue(model.hasAlpha(), "Output image does not have alpha where specified");
+        assertFalse(model.isAlphaPremultiplied(), "Output image has alpha pre-multiplied where not specified");
+
+        imageBuilder = new ImageBuilder(100, 100, true, true);
+        bImage = imageBuilder.getBufferedImage();
+        model = bImage.getColorModel();
+        assertTrue(model.hasAlpha(), "Output image does not have alpha where specified");
+        assertTrue(model.isAlphaPremultiplied(), "Output image does not have alpha pre-multiplied where specified");
+    }
+
+    void executeBadBounds(ImageBuilder imageBuilder, int x, int y, int w, int h){
+        try{
+            ImageBuilder sub = imageBuilder.getSubset(x, y, w, h);
+            fail("Failed to detect bad bounds "+x+", "+y+", "+w+", "+h);
+        }catch(RasterFormatException rfe){
+            // success, no action required
+        }
+    }
+
+    void executeBadConstructor(int w, int h){
+        try{
+            ImageBuilder iBuilder = new ImageBuilder(w, h, true);
+            fail("Failed to detect bad constructor "+w+", "+h);
+        }catch(RasterFormatException rfe){
+            // success, no action required
+        }
+    }
+
+
+    void populate(ImageBuilder imageBuilder){
+        for(int x=0; x<100; x++){
+            for(int y=0; y<100; y++){
+                int k = y*100+x;
+                int rgb = 0xff000000|k;
+                imageBuilder.setRGB(x, y, rgb);
+            }
+        }
+    }
+
+
+
+}
diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffAlphaRoundTripTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffAlphaRoundTripTest.java
new file mode 100644
index 0000000..d8c7e08
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffAlphaRoundTripTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.imaging.formats.tiff;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashMap;
+
+import org.apache.commons.imaging.ImageFormats;
+import org.apache.commons.imaging.Imaging;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Performs a round-trip that writes an image containing Alpha and then reads it
+ * back.
+ * Selected non-opaque pixels are tested for correctness,
+ */
+public class TiffAlphaRoundTripTest {
+
+    @TempDir
+    Path tempDir;
+
+    @Test
+    public void test() throws Exception {
+
+        // This test will exercise two passes to test the implementation
+        // of the TIFF support for writing and reading images containing
+        // an alpha channel.  In the first pass, the alpha writing is enabled
+        // in the second pass it is suppressed.
+        for (int i = 0; i < 2; i++) {
+            // Step 0, create a buffered image that includes transparency
+            // in the form of two rectangles, one completely opaque,
+            // and one giving 50 percent opaque red.
+            int width = 400;
+            int height = 400;
+            BufferedImage image0;
+            if (i == 0) {
+                image0 = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+            } else {
+                image0 = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+            }
+            Graphics2D g2d = image0.createGraphics();
+            g2d.setColor(Color.red);
+            g2d.fillRect(0, 0, width, height);
+            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
+            g2d.setColor(new Color(0, 0, 0, 0));
+            g2d.fillRect(100, 100, 100, 100);
+            g2d.setColor(new Color(0xff, 0, 0, 0x80));
+            g2d.fillRect(200, 200, 100, 100);
+
+            // Step 1: write the Buffered Image to an output file and
+            //         then read it back in.  This action will test the
+            //         correctness of a round-trip test.
+            File file = new File(tempDir.toFile(), "TiffAlphaRoundTripTest.tif");
+            file.delete();
+            HashMap<String, Object> params = new HashMap<>();
+
+            Imaging.writeImage(image0, file, ImageFormats.TIFF, params);
+            BufferedImage image1 = Imaging.getBufferedImage(file);
+
+            // Step 2:  create a composite image overlaying a white background
+            //          with the results from the TIFF file.
+            BufferedImage compImage
+                = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+            g2d = compImage.createGraphics();
+            g2d.setColor(Color.white);
+            g2d.fillRect(0, 0, width, height);
+            g2d.drawImage(image1, 0, 0, null);
+
+            // Step 3, verify that the correct values are in the image.
+            int test1 = compImage.getRGB(150, 150); // in the transparent rectangle
+            int test2 = compImage.getRGB(250, 250);
+            if (i == 0) {
+                doPixelsMatch(150, 150, 0xffffffff, test1);
+                doPixelsMatch(250, 250, 0xffff7f7f, test2);
+            } else {
+                doPixelsMatch(151, 151, 0xff000000, test1);
+                doPixelsMatch(251, 251, 0xffff0000, test2);
+            }
+        }
+    }
+
+    void doPixelsMatch(int x, int y, int a, int b) {
+        if (!componentMatch(a, b, 0, 2)
+            || !componentMatch(a, b, 8, 2)
+            || !componentMatch(a, b, 16, 2)
+            || !componentMatch(a, b, 24, 2)) {
+
+            String complaint = String.format("Pixel mismatch at (%d,%d): 0x%08x 0x%08x",
+                x, y, a, b);
+            fail(complaint);
+        }
+    }
+
+    /**
+     * Checks to see if a pixel component (A, R, G, or B) for two specified
+     * values are within a specified tolerance.
+     *
+     * @param a the first value
+     * @param b the second value
+     * @param iShift a multiple of 8 telling how far to shift values
+     * to extract components (24, 16, 8, or zero for ARGB)
+     * @param iTolerance a small positive integer
+     * @return true if the components of the values match
+     */
+    boolean componentMatch(int a, int b, int iShift, int iTolerance) {
+        int delta = ((a >> iShift) & 0xff) - ((b >> iShift) & 0xff);
+        if (delta < 0) {
+            delta = -delta;
+        }
+        return delta < iTolerance;
+    }
+}
diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadAlphaTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadAlphaTest.java
new file mode 100644
index 0000000..8f6a709
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadAlphaTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.imaging.formats.tiff;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.imaging.ImageReadException;
+import org.apache.commons.imaging.Imaging;
+import org.apache.commons.imaging.ImagingTestConstants;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Performs tests that access the content of TIFF files
+ * containing non-opaque alpha-channel pixels
+ */
+public class TiffReadAlphaTest {
+
+    private final static String[] names = {
+        "TransparencyTestStripAssociated.tif",
+        "TransparencyTestStripUnassociated.tif",
+        "TransparencyTestTileAssociated.tif",
+        "TransparencyTestTileUnassociated.tif"
+    };
+
+    private final static int[][] testSite = {
+        {40, 40, 0xffff0000},
+        {60, 40, 0xff77ff77},
+        {40, 60, 0xffff0000},
+        {60, 60, 0xff008800}
+    };
+
+    /**
+     * Gets a file from the TIFF test directory that contains floating-point
+     * data.
+     *
+     * @param name a valid file name
+     * @return a valid file reference.
+     */
+    private File getTiffFile(String name) {
+        File tiffFolder = new File(ImagingTestConstants.TEST_IMAGE_FOLDER, "tiff");
+        File alphaFolder = new File(tiffFolder, "12");
+        return new File(alphaFolder, name);
+    }
+
+    @Test
+    public void test() {
+        for (String name : names) {
+            try {
+                File subject = getTiffFile(name);
+                BufferedImage overlay = Imaging.getBufferedImage(subject);
+                BufferedImage composite = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
+                Graphics2D g2d = composite.createGraphics();
+                g2d.setColor(Color.white);
+                g2d.fillRect(0, 0, 101, 101);
+                g2d.setColor(Color.black);
+                g2d.fillRect(0, 50, 101, 51);
+                g2d.drawImage(overlay, 0, 0, null);
+
+                for (int i = 0; i < testSite.length; i++) {
+                    int x = testSite[i][0];
+                    int y = testSite[i][1];
+                    int p = testSite[i][2];
+                    int t = composite.getRGB(x, y);
+                    assertEquals(t, p, "Error for " + name + " at position " + x + ", " + y);
+                }
+            } catch (ImageReadException | IOException ex) {
+                fail("Exception reading " + name + ", " + ex.getMessage());
+            }
+        }
+    }
+}


[commons-imaging] 02/04: [Imaging-216] Updated documentation for TIFF writer base class

Posted by ki...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit 0265f93162ec441aaeb78d497fd919296a0b490a
Author: gwlucastrig <co...@gmail.com>
AuthorDate: Sun Nov 15 13:21:45 2020 -0500

    [Imaging-216] Updated documentation for TIFF writer base class
---
 .../imaging/formats/tiff/write/TiffImageWriterBase.java        | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
index ec890fd..43c8da4 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java
@@ -330,9 +330,19 @@ public abstract class TiffImageWriterBase {
         final int width = src.getWidth();
         final int height = src.getHeight();
 
+        // If the source image has a color model that supports alpha,
+        // this module performs a call to checkForActualAlpha() to see whether
+        // the image that was supplied to the API actually contains
+        // non-opaque data in its alpha channel. It is common for applications
+        // to create a BufferedImage using TYPE_INT_ARGB, and fill the entire
+        // image with opaque pixels. In such a case, the file size of the output
+        // can be reduced by 25 percent by storing the image in an 3-byte RGB
+        // format. This approach will also make a small reduction in the runtime
+        // to read the resulting file when it is accessed by an application.
         final ColorModel cModel = src.getColorModel();
         final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
 
+
         // 10/2020: In the case of an image with pre-multiplied alpha
         // (what the TIFF specification calls "associated alpha"), the
         // Java getRGB method adjusts the value to a non-premultiplied


[commons-imaging] 04/04: Merge branch 'pr-106'

Posted by ki...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit 80e307923060079dc5817a3a43dc9f22bedc0e4e
Merge: 1133f3b a7957bd
Author: Bruno P. Kinoshita <ki...@apache.org>
AuthorDate: Tue Nov 17 18:17:38 2020 +1300

    Merge branch 'pr-106'
    
    This closes #106

 src/changes/changes.xml                            |   3 +
 .../commons/imaging/common/ImageBuilder.java       | 135 ++++++++++++-------
 .../imaging/formats/tiff/TiffImageParser.java      |  23 +++-
 .../formats/tiff/constants/TiffConstants.java      |  13 ++
 .../formats/tiff/constants/TiffTagConstants.java   |   2 +
 .../formats/tiff/datareaders/DataReaderStrips.java |  62 +++++----
 .../formats/tiff/datareaders/DataReaderTiled.java  |  60 +++++----
 .../formats/tiff/datareaders/ImageDataReader.java  |  26 +++-
 .../formats/tiff/write/TiffImageWriterBase.java    |  98 +++++++++++++-
 .../commons/imaging/common/ImageBuilderTest.java   | 146 +++++++++++++++++++++
 .../formats/tiff/TiffAlphaRoundTripTest.java       | 134 +++++++++++++++++++
 .../imaging/formats/tiff/TiffReadAlphaTest.java    |  91 +++++++++++++
 12 files changed, 678 insertions(+), 115 deletions(-)