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