From f71bcc5125e743021b3b1ef99d4aea531a4d9716 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 18 Aug 2018 13:27:38 +0200 Subject: [PATCH] #432: Alternate fix + more tests + better alpha handling for TIFF --- .../plugins/tiff/ExtraSamplesColorModel.java | 34 ++- .../imageio/plugins/tiff/TIFFImageReader.java | 262 +++++++++--------- .../tiff/ExtraSamplesColorModelTest.java | 107 +++++++ 3 files changed, 263 insertions(+), 140 deletions(-) create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModelTest.java diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModel.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModel.java index 552ddf91..4a17a327 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModel.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModel.java @@ -9,6 +9,8 @@ import java.awt.image.ComponentSampleModel; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; +import static java.awt.image.DataBuffer.getDataTypeSize; + /** * ExtraSamplesColorModel. * @@ -22,10 +24,22 @@ final class ExtraSamplesColorModel extends ComponentColorModel { // still thinks it has numComponents == cs.getNumComponents() + 1 for most operations private final int numComponents; - ExtraSamplesColorModel(ColorSpace cs, boolean isAlphaPremultiplied, int dataType, int extraComponents) { - super(cs, true, isAlphaPremultiplied, Transparency.TRANSLUCENT, dataType); + ExtraSamplesColorModel(ColorSpace cs, boolean hasAlpha, boolean isAlphaPremultiplied, int dataType, int extraComponents) { + super(cs, bitsArrayHelper(cs, dataType, extraComponents + (hasAlpha ? 1 : 0)), hasAlpha, isAlphaPremultiplied, Transparency.TRANSLUCENT, dataType); Validate.isTrue(extraComponents > 0, "Extra components must be > 0"); - this.numComponents = super.getNumComponents() + extraComponents; + this.numComponents = cs.getNumComponents() + (hasAlpha ? 1 : 0) + extraComponents; + } + + private static int[] bitsArrayHelper(ColorSpace cs, int dataType, int extraComponents) { + int numBits = getDataTypeSize(dataType); + int numComponents = cs.getNumComponents() + extraComponents; + int[] bits = new int[numComponents]; + + for (int i = 0; i < numComponents; i++) { + bits[i] = numBits; + } + + return bits; } @Override @@ -45,16 +59,18 @@ final class ExtraSamplesColorModel extends ComponentColorModel { @Override public WritableRaster getAlphaRaster(WritableRaster raster) { - if (hasAlpha() == false) { + if (!hasAlpha()) { return null; } int x = raster.getMinX(); int y = raster.getMinY(); - int[] band = new int[1]; - band[0] = super.getNumComponents() - 1; - return raster.createWritableChild(x, y, raster.getWidth(), - raster.getHeight(), x, y, - band); + int[] band = new int[] {getAlphaComponent()}; + + return raster.createWritableChild(x, y, raster.getWidth(), raster.getHeight(), x, y, band); + } + + private int getAlphaComponent() { + return super.getNumComponents() - 1; } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index a5a2b53f..f9ebff9f 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -438,18 +438,20 @@ public final class TIFFImageReader extends ImageReaderBase { int opaqueSamplesPerPixel = getOpaqueSamplesPerPixel(interpretation); - // Spec says ExtraSamples are mandatory of extra samples, however known encoders + // Spec says ExtraSamples are mandatory for extra samples, however known encoders // (ie. SeaShore) writes ARGB TIFFs without ExtraSamples. long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", false); if (extraSamples == null && samplesPerPixel > opaqueSamplesPerPixel) { // TODO: Log warning! - // First extra is alpha, rest is "unspecified" + // First extra is alpha, rest is "unspecified" (0) extraSamples = new long[samplesPerPixel - opaqueSamplesPerPixel]; extraSamples[0] = TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA; } // Determine alpha - boolean hasAlpha = extraSamples != null; + boolean hasAlpha = extraSamples != null + && (extraSamples[0] == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA + || extraSamples[0] == TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA); boolean isAlphaPremultiplied = hasAlpha && extraSamples[0] == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA; int significantSamples = opaqueSamplesPerPixel + (hasAlpha ? 1 : 0); @@ -577,12 +579,12 @@ public final class TIFFImageReader extends ImageReaderBase { switch (planarConfiguration) { case TIFFBaseline.PLANARCONFIG_CHUNKY: // "TYPE_4BYTE_RGBA" if cs.isCS_sRGB() - if (extraSamples != null && extraSamples.length == 1) { + if (hasAlpha && extraSamples.length == 1) { return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, true, isAlphaPremultiplied); } else { return new ImageTypeSpecifier( - new ExtraSamplesColorModel(cs, isAlphaPremultiplied, dataType, samplesPerPixel - significantSamples), + new ExtraSamplesColorModel(cs, hasAlpha, isAlphaPremultiplied, dataType, samplesPerPixel - significantSamples), new PixelInterleavedSampleModel(dataType, 1, 1, samplesPerPixel, samplesPerPixel, createOffsets(samplesPerPixel)) ); } @@ -614,9 +616,7 @@ public final class TIFFImageReader extends ImageReaderBase { IndexColorModel icm = createIndexColorModel(bitsPerSample, dataType, (int[]) colorMap.getValue()); - if (extraSamples != null && extraSamples.length > 0 - && (extraSamples[0] == TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA - || extraSamples[0] == TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)) { + if (hasAlpha) { return ImageTypeSpecifiers.createDiscreteAlphaIndexedFromIndexColorModel(icm); } @@ -1843,158 +1843,158 @@ public final class TIFFImageReader extends ImageReaderBase { case DataBuffer.TYPE_BYTE: /*for (int band = 0; band < bands; band++)*/ { - int bank = banded ? ((BandedSampleModel) tileRowRaster.getSampleModel()).getBankIndices()[band] : band; + int bank = banded ? ((BandedSampleModel) tileRowRaster.getSampleModel()).getBankIndices()[band] : band; - byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(bank); - WritableRaster destChannel = banded - ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) - : raster; - Raster srcChannel = banded - ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) - : tileRowRaster; + byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(bank); + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } - - input.readFully(rowDataByte); - - if (row % ySub == 0 && row >= srcRegion.y) { - if (!banded) { - normalizeColor(interpretation, rowDataByte); - } - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands); - } - } - - destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); - } - // Else skip data + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } + + input.readFully(rowDataByte); + + if (row % ySub == 0 && row >= srcRegion.y) { + if (!banded) { + normalizeColor(interpretation, rowDataByte); + } + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands); + } + } + + destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); + } + // Else skip data } + } // if (banded) { // // TODO: Normalize colors for tile (need to know tile region and sample model) // // Unfortunately, this will disable acceleration... // } - break; + break; case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_SHORT: /*for (int band = 0; band < bands; band++)*/ { - short[] rowDataShort = dataBuffer.getDataType() == DataBuffer.TYPE_USHORT - ? ((DataBufferUShort) dataBuffer).getData(band) - : ((DataBufferShort) dataBuffer).getData(band); + short[] rowDataShort = dataBuffer.getDataType() == DataBuffer.TYPE_USHORT + ? ((DataBufferUShort) dataBuffer).getData(band) + : ((DataBufferShort) dataBuffer).getData(band); - WritableRaster destChannel = banded - ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) - : raster; - Raster srcChannel = banded - ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) - : tileRowRaster; + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } - - readFully(input, rowDataShort); - - if (row >= srcRegion.y) { - normalizeColor(interpretation, rowDataShort); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands); - } - } - - destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); - // TODO: Possible speedup ~30%!: -// raster.setDataElements(startCol, row - srcRegion.y, colsInTile, 1, rowDataShort); - } - // Else skip data + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - } - break; + readFully(input, rowDataShort); + + if (row >= srcRegion.y) { + normalizeColor(interpretation, rowDataShort); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands); + } + } + + destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); + // TODO: Possible speedup ~30%!: +// raster.setDataElements(startCol, row - srcRegion.y, colsInTile, 1, rowDataShort); + } + // Else skip data + } + } + + break; case DataBuffer.TYPE_INT: /*for (int band = 0; band < bands; band++)*/ { - int[] rowDataInt = ((DataBufferInt) dataBuffer).getData(band); + int[] rowDataInt = ((DataBufferInt) dataBuffer).getData(band); - WritableRaster destChannel = banded - ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) - : raster; - Raster srcChannel = banded - ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) - : tileRowRaster; + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } - - readFully(input, rowDataInt); - - if (row >= srcRegion.y) { - normalizeColor(interpretation, rowDataInt); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands); - } - } - - destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); - } - // Else skip data + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - } - break; + readFully(input, rowDataInt); + + if (row >= srcRegion.y) { + normalizeColor(interpretation, rowDataInt); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands); + } + } + + destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); + } + // Else skip data + } + } + + break; case DataBuffer.TYPE_FLOAT: /*for (int band = 0; band < bands; band++)*/ { - float[] rowDataFloat = ((DataBufferFloat) tileRowRaster.getDataBuffer()).getData(band); + float[] rowDataFloat = ((DataBufferFloat) tileRowRaster.getDataBuffer()).getData(band); - WritableRaster destChannel = banded - ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) - : raster; - Raster srcChannel = banded - ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) - : tileRowRaster; + WritableRaster destChannel = banded + ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) + : raster; + Raster srcChannel = banded + ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) + : tileRowRaster; - for (int row = startRow; row < startRow + rowsInTile; row++) { - if (row >= srcRegion.y + srcRegion.height) { - break; // We're done with this tile - } - - readFully(input, rowDataFloat); - - if (row >= srcRegion.y) { - normalizeColor(interpretation, rowDataFloat); - - // Subsample horizontal - if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataFloat, x * xSub, rowDataFloat, x, numBands); - } - } - - destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); - } - // Else skip data + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile } - } - break; + readFully(input, rowDataFloat); + + if (row >= srcRegion.y) { + normalizeColor(interpretation, rowDataFloat); + + // Subsample horizontal + if (xSub != 1) { + for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) { + System.arraycopy(rowDataFloat, x * xSub, rowDataFloat, x, numBands); + } + } + + destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); + } + // Else skip data + } + } + + break; } } @@ -2287,7 +2287,7 @@ public final class TIFFImageReader extends ImageReaderBase { private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException { int fillOrder = getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1); - switch (compression) { + switch (compression) { case TIFFBaseline.COMPRESSION_NONE: return stream; case TIFFBaseline.COMPRESSION_PACKBITS: diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModelTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModelTest.java new file mode 100644 index 00000000..d69875e2 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/ExtraSamplesColorModelTest.java @@ -0,0 +1,107 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.image.ResampleOp; +import com.twelvemonkeys.imageio.color.ColorSpaces; +import org.junit.Test; + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.util.Hashtable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ExtraSamplesColorModelTest { + + private BufferedImage createExtraSamplesImage(int w, int h, ColorSpace cs, boolean hasAlpha, int extraComponents) { + int samplesPerPixel = cs.getNumComponents() + (hasAlpha ? 1 : 0) + extraComponents; + + ExtraSamplesColorModel colorModel = new ExtraSamplesColorModel(cs, hasAlpha, true, DataBuffer.TYPE_BYTE, extraComponents); + SampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, samplesPerPixel, samplesPerPixel * w, createOffsets(samplesPerPixel)); + + WritableRaster raster = Raster.createWritableRaster(sampleModel, new Point(0, 0)); + + return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Hashtable()); + } + + private static int[] createOffsets(int samplesPerPixel) { + int[] offsets = new int[samplesPerPixel]; + for (int i = 0; i < samplesPerPixel; i++) { + offsets[i] = i; + } + return offsets; + } + + @Test + public void testImageWithExtraSamplesCanBeResampledGray() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpace.CS_GRAY), false, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } + + @Test + public void testImageWithExtraSamplesCanBeResampledGrayAlpha() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpace.CS_GRAY), true, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } + + @Test + public void testImageWithExtraSamplesCanBeResampledRGB() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), false, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } + + @Test + public void testImageWithExtraSamplesCanBeResampledRGBAlpha() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), true, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } + + @Test + public void testImageWithExtraSamplesCanBeResampledCMYK() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), false, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } + + @Test + public void testImageWithExtraSamplesCanBeResampledCMYKAlpha() { + for (int i = 1; i < 8; i++) { + BufferedImage bufferedImage = createExtraSamplesImage(10, 10, ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), true, i); + BufferedImage resampled = new ResampleOp(5, 5, ResampleOp.FILTER_LANCZOS).filter(bufferedImage, null); + + assertNotNull(resampled); + assertEquals(5, resampled.getWidth()); + assertEquals(5, resampled.getHeight()); + } + } +} \ No newline at end of file