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:38 UTC

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

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());
+            }
+        }
+    }
+}