diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java index b7bdf420..0f5eb4e4 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java @@ -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. *

* ColorSpace will always be the default sRGB color space (as with {@code IndexColorModel}). *

- * Component order is always P, A, where P is a palette index, and A is the alpha value. + * Component order is always I, A, X1, X2... Xn, + * where I is a palette index, A is the alpha value and Xn 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 diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java index 0f3f6259..aaaa618c 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java @@ -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 Harald Kuhr * @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)); + } } 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 da664ce5..59466d61 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 @@ -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; * * * @see Adobe TIFF developer resources - * @see Wikipedia + * @see TIFF 6.0 specification + * @see Wikipedia TIFF * @see AWare Systems TIFF pages * * @author Harald Kuhr @@ -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 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); } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 0ce685b0..89ad7e50 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -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