#473: Fix for ColorMap (Indexed) TIFF with non-alpha ExtraSamples.

This commit is contained in:
Harald Kuhr 2019-02-12 20:34:31 +01:00
parent 5ade293445
commit 3e4b14f984
5 changed files with 165 additions and 49 deletions

View File

@ -37,20 +37,26 @@ import static com.twelvemonkeys.lang.Validate.notNull;
/**
* This class represents a hybrid between an {@link IndexColorModel} and a {@link ComponentColorModel},
* having both a color map and a full, discrete alpha channel.
* having both a color map and a full, discrete alpha channel and/or one or more "extra" channels.
* The color map entries are assumed to be fully opaque and should have no transparent index.
* <p>
* ColorSpace will always be the default sRGB color space (as with {@code IndexColorModel}).
* <p>
* Component order is always P, A, where P is a palette index, and A is the alpha value.
* Component order is always I, A, X<sub>1</sub>, X<sub>2</sub>... X<sub>n</sub>,
* where I is a palette index, A is the alpha value and X<sub>n</sub> are extra samples (ignored for display).
*
* @see IndexColorModel
* @see ComponentColorModel
*/
// TODO: ExtraSamplesIndexColorModel might be a better name?
// TODO: Allow specifying which channel is the transparency mask?
public final class DiscreteAlphaIndexColorModel extends ColorModel {
// Our IndexColorModel delegate
private final IndexColorModel icm;
private final int samples;
private final boolean hasAlpha;
/**
* Creates a {@code DiscreteAlphaIndexColorModel}, delegating color map look-ups
* to the given {@code IndexColorModel}.
@ -59,13 +65,34 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel {
* fully opaque, any transparency or transparent index will be ignored.
*/
public DiscreteAlphaIndexColorModel(final IndexColorModel icm) {
this(icm, 1, true);
}
/**
* Creates a {@code DiscreteAlphaIndexColorModel}, delegating color map look-ups
* to the given {@code IndexColorModel}.
*
* @param icm The {@code IndexColorModel} delegate. Color map entries are assumed to be
* fully opaque, any transparency or transparent index will be ignored.
* @param extraSamples the number of extra samples in the color model.
* @param hasAlpha {@code true} if the extra samples contains alpha, otherwise {@code false}.
*/
public DiscreteAlphaIndexColorModel(final IndexColorModel icm, int extraSamples, boolean hasAlpha) {
super(
notNull(icm, "IndexColorModel").getPixelSize() * 2,
notNull(icm, "IndexColorModel").getPixelSize() * (1 + extraSamples),
new int[] {icm.getPixelSize(), icm.getPixelSize(), icm.getPixelSize(), icm.getPixelSize()},
icm.getColorSpace(), true, false, Transparency.TRANSLUCENT, icm.getTransferType()
icm.getColorSpace(), hasAlpha, false, hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE,
icm.getTransferType()
);
this.icm = icm;
this.samples = 1 + extraSamples;
this.hasAlpha = hasAlpha;
}
@Override
public int getNumComponents() {
return samples;
}
@Override
@ -85,7 +112,7 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel {
@Override
public final int getAlpha(final int pixel) {
return (int) ((((float) pixel) / ((1 << getComponentSize(3))-1)) * 255.0f + 0.5f);
return hasAlpha ? (int) ((((float) pixel) / ((1 << getComponentSize(3))-1)) * 255.0f + 0.5f) : 0xff;
}
private int getSample(final Object inData, final int index) {
@ -128,17 +155,27 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel {
@Override
public final int getAlpha(final Object inData) {
return getAlpha(getSample(inData, 1));
return hasAlpha ? getAlpha(getSample(inData, 1)) : 0xff;
}
@Override
public final SampleModel createCompatibleSampleModel(final int w, final int h) {
return new PixelInterleavedSampleModel(transferType, w, h, 2, w * 2, new int[] {0, 1});
return new PixelInterleavedSampleModel(transferType, w, h, samples, w * samples, createOffsets(samples));
}
private int[] createOffsets(int samples) {
int[] offsets = new int[samples];
for (int i = 0; i < samples; i++) {
offsets[i] = i;
}
return offsets;
}
@Override
public final boolean isCompatibleSampleModel(final SampleModel sm) {
return sm instanceof PixelInterleavedSampleModel && sm.getNumBands() == 2;
return sm instanceof PixelInterleavedSampleModel && sm.getNumBands() == samples;
}
@Override
@ -150,7 +187,7 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel {
public final boolean isCompatibleRaster(final Raster raster) {
int size = raster.getSampleModel().getSampleSize(0);
return ((raster.getTransferType() == transferType) &&
(raster.getNumBands() == 2) && ((1 << size) >= icm.getMapSize()));
(raster.getNumBands() == samples) && ((1 << size) >= icm.getMapSize()));
}
@Override

View File

@ -44,7 +44,7 @@ import static com.twelvemonkeys.lang.Validate.notNull;
* Fixes some subtle bugs in {@code ImageTypeSpecifier}'s factory methods, but
* in most cases, this class will delegate to the corresponding methods in {@link ImageTypeSpecifier}.
*
* @see javax.imageio.ImageTypeSpecifier
* @see ImageTypeSpecifier
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ImageTypeSpecifiers.java,v 1.0 24.01.11 17.51 haraldk Exp$
@ -186,4 +186,9 @@ public final class ImageTypeSpecifiers {
ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
public static ImageTypeSpecifier createDiscreteExtraSamplesIndexedFromIndexColorModel(final IndexColorModel pColorModel, int extraSamples, boolean hasAlpha) {
ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel, extraSamples, hasAlpha);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
}

View File

@ -55,11 +55,14 @@ import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.*;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
import javax.imageio.spi.IIORegistry;
@ -71,6 +74,7 @@ import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
@ -80,6 +84,7 @@ import java.util.zip.InflaterInputStream;
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName;
import static java.util.Arrays.asList;
/**
* ImageReader implementation for Aldus/Adobe Tagged Image File Format (TIFF).
@ -111,7 +116,8 @@ import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName;
* </ul>
*
* @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a>
* @see <a href="http://en.wikipedia.org/wiki/Tagged_Image_File_Format">Wikipedia</a>
* @see <a href="http://www.alternatiff.com/resources/TIFF6.pdf">TIFF 6.0 specification</a>
* @see <a href="http://en.wikipedia.org/wiki/Tagged_Image_File_Format">Wikipedia TIFF</a>
* @see <a href="http://www.awaresystems.be/imaging/tiff.html">AWare Systems TIFF pages</a>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@ -122,10 +128,6 @@ public final class TIFFImageReader extends ImageReaderBase {
// TODOs ImageIO basic functionality:
// TODO: Thumbnail support (what is a TIFF thumbnail anyway? Photoshop way? Or use subfiletype?)
// TODOs Full BaseLine support:
// TODO: Support ExtraSamples (an array, if multiple extra samples!)
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
// TODOs ImageIO advanced functionality:
// TODO: Tiling support (readTile, readTileRaster)
// TODO: Implement readAsRenderedImage to allow tiled RenderedImage?
@ -148,6 +150,8 @@ public final class TIFFImageReader extends ImageReaderBase {
// Support ICCProfile
// Support PlanarConfiguration 2
// Support Compression 3 & 4 (CCITT T.4 & T.6)
// Support ExtraSamples (an array, if multiple extra samples!)
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
@ -469,6 +473,10 @@ public final class TIFFImageReader extends ImageReaderBase {
// as some software will treat black/white runs as-is, regardless of photometric.
// Special handling is also in the normalizeColor method
if (significantSamples == 1 && bitsPerSample == 1) {
if (profile != null) {
processWarningOccurred("Ignoring embedded ICC color profile for Bi-level/Gray TIFF");
}
byte[] lut = new byte[] {-1, 0};
return ImageTypeSpecifier.createIndexed(lut, lut, lut, null, bitsPerSample, dataType);
}
@ -591,8 +599,8 @@ public final class TIFFImageReader extends ImageReaderBase {
IndexColorModel icm = createIndexColorModel(bitsPerSample, dataType, (int[]) colorMap.getValue());
if (hasAlpha) {
return ImageTypeSpecifiers.createDiscreteAlphaIndexedFromIndexColorModel(icm);
if (extraSamples != null) {
return ImageTypeSpecifiers.createDiscreteExtraSamplesIndexedFromIndexColorModel(icm, extraSamples.length, hasAlpha);
}
return ImageTypeSpecifiers.createFromIndexColorModel(icm);
@ -921,6 +929,10 @@ public final class TIFFImageReader extends ImageReaderBase {
if (stripTileByteCounts == null) {
processWarningOccurred("Missing TileByteCounts for tiled TIFF with compression: " + compression);
}
else if (stripTileByteCounts.length == 0 || containsZero(stripTileByteCounts)) {
stripTileByteCounts = null;
processWarningOccurred("Ignoring all-zero TileByteCounts for tiled TIFF with compression: " + compression);
}
stripTileWidth = getValueAsInt(TIFF.TAG_TILE_WIDTH, "TileWidth");
stripTileHeight = getValueAsInt(TIFF.TAG_TILE_HEIGTH, "TileHeight");
@ -931,6 +943,10 @@ public final class TIFFImageReader extends ImageReaderBase {
if (stripTileByteCounts == null) {
processWarningOccurred("Missing StripByteCounts for TIFF with compression: " + compression);
}
else if (stripTileByteCounts.length == 0 || containsZero(stripTileByteCounts)) {
stripTileByteCounts = null;
processWarningOccurred("Ignoring all-zero StripByteCounts for TIFF with compression: " + compression);
}
// NOTE: This is really against the spec, but libTiff seems to handle it. TIFF 6.0 says:
// "Do not use both strip- oriented and tile-oriented fields in the same TIFF file".
@ -1309,13 +1325,11 @@ public final class TIFFImageReader extends ImageReaderBase {
int len = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Integer.MAX_VALUE;
imageInput.seek(stripTileOffsets != null ? stripTileOffsets[i] : realJPEGOffset);
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
Arrays.asList(
new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, len),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)))) {
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList(
new ByteArrayInputStream(jpegHeader),
createStreamAdapter(imageInput, len),
new ByteArrayInputStream(new byte[]{(byte) 0xff, (byte) 0xd9}) // EOI
))))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setSourceSubsampling(xSub, ySub, 0, 0);
@ -1460,7 +1474,7 @@ public final class TIFFImageReader extends ImageReaderBase {
}
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
Arrays.asList(
asList(
createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling),
createStreamAdapter(imageInput, length),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
@ -1538,6 +1552,16 @@ public final class TIFFImageReader extends ImageReaderBase {
return destination;
}
private boolean containsZero(long[] byteCounts) {
for (long byteCount : byteCounts) {
if (byteCount <= 0) {
return true;
}
}
return false;
}
private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) throws IOException {
try {
return jpegReader.getImageMetadata(0);
@ -2026,7 +2050,7 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
private void normalizeColor(int photometricInterpretation, byte[] data) throws IIOException {
private void normalizeColor(int photometricInterpretation, byte[] data) throws IOException {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
// NOTE: Preserve WhiteIsZero for 1 bit monochrome, for CCITT compatibility
@ -2543,6 +2567,13 @@ public final class TIFFImageReader extends ImageReaderBase {
try {
ImageReadParam param = reader.getDefaultReadParam();
if (param.getClass().getName().equals("com.twelvemonkeys.imageio.plugins.svg.SVGReadParam")) {
Method setBaseURI = param.getClass().getMethod("setBaseURI", String.class);
String uri = file.getAbsoluteFile().toURI().toString();
setBaseURI.invoke(param, uri);
}
int numImages = reader.getNumImages(true);
for (int imageNo = 0; imageNo < numImages; imageNo++) {
// if (args.length > 1) {
@ -2557,6 +2588,7 @@ public final class TIFFImageReader extends ImageReaderBase {
// int height = reader.getHeight(imageNo);
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
// param.setSourceRegion(new Rectangle(100, 300, 400, 400));
// param.setSourceRegion(new Rectangle(95, 105, 100, 100));
// param.setSourceRegion(new Rectangle(3, 3, 9, 9));
// param.setDestinationOffset(new Point(50, 150));
// param.setSourceSubsampling(2, 2, 0, 0);
@ -2564,16 +2596,18 @@ public final class TIFFImageReader extends ImageReaderBase {
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
// IIOMetadata metadata = reader.getImageMetadata(imageNo);
// if (metadata != null) {
// if (metadata.getNativeMetadataFormatName() != null) {
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
// }
// /*else*/
// if (metadata.isStandardMetadataFormatSupported()) {
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
// }
// }
IIOMetadata metadata = reader.getImageMetadata(imageNo);
if (metadata != null) {
if (metadata.getNativeMetadataFormatName() != null) {
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
replaceBytesWithUndefined((IIOMetadataNode) tree);
new XMLSerializer(System.out, "UTF-8").serialize(tree, false);
}
/*else*/
if (metadata.isStandardMetadataFormatSupported()) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
}
}
System.err.println("image: " + image);
@ -2659,6 +2693,47 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
// XMP Spec says "The field type should be UNDEFINED (7) or BYTE (1)"
// Adobe Photoshop® TIFF Technical Notes says (for Image Source Data): "Type: UNDEFINED"
private static final Set<String> BYTE_TO_UNDEFINED_NODES = new HashSet<>(asList(
"700", // XMP
"34377", // Photoshop Image Resources
"37724" // Image Source Data
));
private static void replaceBytesWithUndefined(IIOMetadataNode tree) {
// The output of the TIFFUndefined tag is just much more readable (or easier to skip)
NodeList nodes = tree.getElementsByTagName("TIFFBytes");
for (int i = 0; i < nodes.getLength(); i++) {
IIOMetadataNode node = (IIOMetadataNode) nodes.item(i);
IIOMetadataNode parentNode = (IIOMetadataNode) node.getParentNode();
NodeList childNodes = node.getChildNodes();
if (BYTE_TO_UNDEFINED_NODES.contains(parentNode.getAttribute("number")) && childNodes.getLength() > 16) {
IIOMetadataNode undefined = new IIOMetadataNode("TIFFUndefined");
StringBuilder values = new StringBuilder();
IIOMetadataNode child = (IIOMetadataNode) node.getFirstChild();
while (child != null) {
if (values.length() > 0) {
values.append(", ");
}
String value = child.getAttribute("value");
values.append(value);
child = (IIOMetadataNode) child.getNextSibling();
}
undefined.setAttribute("value", values.toString());
parentNode.replaceChild(undefined, node);
}
}
}
protected static void showIt(BufferedImage image, String title) {
ImageReaderBase.showIt(image, title);
}

View File

@ -43,10 +43,8 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Arrays;
@ -108,6 +106,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/scan-mono-iccgray.tif"), new Dimension(2408, 3436)), // B/W, PackBits w/gray ICC profile
new TestData(getClassLoaderResource("/tiff/planar-striped-lzw.tif"), new Dimension(229, 229)), // RGB 8 bit/sample, planar, LZW compression
new TestData(getClassLoaderResource("/tiff/colormap-with-extrasamples.tif"), new Dimension(10, 10)), // Palette, 8 bit/sample, 2 samples/pixel, extra samples, LZW
new TestData(getClassLoaderResource("/tiff/indexed-unspecified-extrasamples.tif"), new Dimension(98, 106)), // Palette, 8 bit/sample, 2 samples/pixel, extra samples
new TestData(getClassLoaderResource("/tiff/packbits-fillorder-2.tif"), new Dimension(3508, 2481)), // B/W, PackBits, FillOrder 2
// CCITT
new TestData(getClassLoaderResource("/tiff/ccitt/group3_1d.tif"), new Dimension(6, 4)), // B/W, CCITT T4 1D
@ -596,6 +595,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
@Test
public void testAlphaRasterForMultipleExtraSamples() throws IOException {
ImageReader reader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/extra-channels.tif"))) {
reader.setInput(stream);
@ -646,15 +646,14 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
assertEquals(160, image.getWidth());
assertEquals(227, image.getHeight());
// This TIFF does not contain a ICC profile, making the RGB result depend on the platforms "Generic CMYK" profile
if (ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK) instanceof ICC_ColorSpace) {
assertRGBEquals("Wrong RGB (0,0)", 0xff1E769D, image.getRGB(0, 0), 4);
assertRGBEquals("Wrong RGB (159,226)", 0xff1E769D, image.getRGB(159, 226), 4);
}
else {
assertRGBEquals("Wrong RGB (0,0)", 0xff2896d9, image.getRGB(0, 0), 4);
assertRGBEquals("Wrong RGB (159,226)", 0xff2896d9, image.getRGB(159, 226), 4);
}
// This TIFF does not contain an ICC profile, making the RGB result depend on the platforms "Generic CMYK" profile
ColorSpace genericCMYK = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
ComponentColorModel cmyk = new ComponentColorModel(genericCMYK, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
// Input (0,0): -41, 104, 37, 1 (C, M, Y, K)
int expected = cmyk.getRGB(new byte[]{-41, 104, 37, 1});
assertRGBEquals("Wrong RGB (0,0)", expected, image.getRGB(0, 0), 4);
assertRGBEquals("Wrong RGB (159,226)", expected, image.getRGB(159, 226), 4);
}
}