diff --git a/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java b/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java
index 86b8e46d..c1945f11 100755
--- a/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java
+++ b/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java
@@ -459,6 +459,16 @@ public class XMLSerializer {
pOut.print(pNode.getTagName());
pOut.println(">");
}
+ else if (pNode.getNodeValue() != null) {
+ // NOTE: This is NOT AS SPECIFIED, but we do this to support
+ // the weirdness that is the javax.imageio.metadata.IIOMetadataNode.
+ // According to the spec, the nodeValue of an Element is null.
+ pOut.print(">");
+ pOut.print(pNode.getNodeValue());
+ pOut.print("");
+ pOut.print(pNode.getTagName());
+ pOut.println(">");
+ }
else {
pOut.println("/>");
}
diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReader.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReader.java
index 8999640e..c50dccd9 100755
--- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReader.java
+++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReader.java
@@ -32,11 +32,15 @@ import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
+import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
+import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.xml.XMLSerializer;
import javax.imageio.*;
+import javax.imageio.event.IIOReadUpdateListener;
+import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
@@ -45,7 +49,6 @@ import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
-import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
@@ -58,15 +61,16 @@ import java.util.Iterator;
* @author Harald Kuhr
* @author last modified by $Author: haraldk$
* @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$
- *
* @see com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader
*/
public final class BMPImageReader extends ImageReaderBase {
private long pixelOffset;
private DIBHeader header;
+ private int[] colors;
+ private IndexColorModel colorMap;
- private transient ImageReader jpegReaderDelegate;
- private transient ImageReader pngReaderDelegate;
+ private ImageReader jpegReaderDelegate;
+ private ImageReader pngReaderDelegate;
public BMPImageReader() {
super(new BMPImageReaderSpi());
@@ -80,6 +84,8 @@ public final class BMPImageReader extends ImageReaderBase {
protected void resetMembers() {
pixelOffset = 0;
header = null;
+ colors = null;
+ colorMap = null;
if (pngReaderDelegate != null) {
pngReaderDelegate.dispose();
@@ -121,6 +127,53 @@ public final class BMPImageReader extends ImageReaderBase {
}
}
+ private IndexColorModel readColorMap() throws IOException {
+ readHeader();
+
+ if (colors == null) {
+ if (header.getBitCount() > 8 && header.colorsUsed == 0) {
+ // RGB without color map
+ colors = new int[0];
+ }
+ else {
+ int offset = DIB.BMP_FILE_HEADER_SIZE + header.getSize();
+ if (offset != imageInput.getStreamPosition()) {
+ imageInput.seek(offset);
+ }
+
+ if (header.getSize() == DIB.BITMAP_CORE_HEADER_SIZE) {
+ colors = new int[Math.min(header.getColorsUsed(), (int) (pixelOffset - DIB.BMP_FILE_HEADER_SIZE - header.getSize()) / 3)];
+
+ // Byte triplets in BGR form
+ for (int i = 0; i < colors.length; i++) {
+ int b = imageInput.readUnsignedByte();
+ int g = imageInput.readUnsignedByte();
+ int r = imageInput.readUnsignedByte();
+ colors[i] = r << 16 | g << 8 | b | 0xff000000;
+ }
+ }
+ else {
+ colors = new int[Math.min(header.getColorsUsed(), (int) (pixelOffset - DIB.BMP_FILE_HEADER_SIZE - header.getSize()) / 4)];
+
+ // Byte quadruples in BGRa (or little-endian ints in aRGB) form, where a is "Reserved"
+ for (int i = 0; i < colors.length; i++) {
+ colors[i] = imageInput.readInt() & 0x00ffffff | 0xff000000;
+ }
+ }
+
+ // There might be more entries in the color map, but we ignore these for reading
+ int mapSize = Math.min(colors.length, 1 << header.getBitCount());
+
+ // Compute bits for > 8 bits (used only for meta data)
+ int bits = header.getBitCount() <= 8 ? header.getBitCount() : mapSize <= 256 ? 8 : 16;
+
+ colorMap = new IndexColorModel(bits, mapSize, colors, 0, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
+ }
+ }
+
+ return colorMap;
+ }
+
@Override
public int getWidth(int pImageIndex) throws IOException {
checkBounds(pImageIndex);
@@ -143,40 +196,6 @@ public final class BMPImageReader extends ImageReaderBase {
return Arrays.asList(getRawImageType(pImageIndex)).iterator();
}
- private void readColorMap(final BitmapIndexed pBitmap) throws IOException {
- int offset = DIB.BMP_FILE_HEADER_SIZE + header.getSize();
- if (offset != imageInput.getStreamPosition()) {
- imageInput.seek(offset);
- }
-
- switch (header.getCompression()) {
- case DIB.COMPRESSION_RGB:
- case DIB.COMPRESSION_RLE4:
- case DIB.COMPRESSION_RLE8:
- break;
- default:
- throw new IIOException("Unsupported compression for palette: " + header.getCompression());
- }
-
- int colorCount = pBitmap.getColorCount();
-
- if (header.getSize() == DIB.BITMAP_CORE_HEADER_SIZE) {
- // Byte triplets in BGR form
- for (int i = 0; i < colorCount; i++) {
- int b = imageInput.readUnsignedByte();
- int g = imageInput.readUnsignedByte();
- int r = imageInput.readUnsignedByte();
- pBitmap.colors[i] = r << 16 | g << 8 | b | 0xff000000;
- }
- }
- else {
- // Byte quadruples in BGRa (or ints in aRGB) form (where a is "Reserved")
- for (int i = 0; i < colorCount; i++) {
- pBitmap.colors[i] = (imageInput.readInt() & 0xffffff) | 0xff000000;
- }
- }
- }
-
@Override
public ImageTypeSpecifier getRawImageType(int pImageIndex) throws IOException {
checkBounds(pImageIndex);
@@ -190,22 +209,17 @@ public final class BMPImageReader extends ImageReaderBase {
case 2:
case 4:
case 8:
- // TODO: Get rid of the fake DirectoryEntry and support color maps directly
- BitmapIndexed indexed = new BitmapIndexed(new DirectoryEntry() {}, header);
- readColorMap(indexed);
- return IndexedImageTypeSpecifier.createFromIndexColorModel(indexed.createColorModel());
+ return IndexedImageTypeSpecifier.createFromIndexColorModel(readColorMap());
case 16:
if (header.hasMasks()) {
int[] masks = getMasks();
- return ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
- masks[0],
- masks[1],
- masks[2],
- masks[3],
- DataBuffer.TYPE_USHORT,
- false);
+ return ImageTypeSpecifier.createPacked(
+ ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ masks[0], masks[1], masks[2], masks[3],
+ DataBuffer.TYPE_USHORT, false
+ );
}
// Default if no mask is 555
@@ -222,13 +236,11 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.hasMasks()) {
int[] masks = getMasks();
- return ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
- masks[0],
- masks[1],
- masks[2],
- masks[3],
- DataBuffer.TYPE_INT,
- false);
+ return ImageTypeSpecifier.createPacked(
+ ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ masks[0], masks[1], masks[2], masks[3],
+ DataBuffer.TYPE_INT, false
+ );
}
// Default if no mask
@@ -290,10 +302,18 @@ public final class BMPImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
+ ColorModel colorModel = destination.getColorModel();
+ if (colorModel instanceof IndexColorModel && ((IndexColorModel) colorModel).getMapSize() < header.getColorsUsed()) {
+ processWarningOccurred(
+ String.format("Color map contains more colors than raster allows (%d). Ignoring entries above %d.",
+ header.getColorsUsed(), ((IndexColorModel) colorModel).getMapSize())
+ );
+ }
+
// BMP rows are padded to 4 byte boundary
int rowSizeBytes = ((header.getBitCount() * width + 31) / 32) * 4;
- // Wrap
+ // Wrap input according to compression
imageInput.seek(pixelOffset);
DataInput input;
@@ -369,17 +389,7 @@ public final class BMPImageReader extends ImageReaderBase {
case 8:
case 24:
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
- try {
- readRowByte(input, height, srcRegion, xSub, ySub, rowDataByte, destRaster, clippedRow, y);
- }
- catch (IndexOutOfBoundsException ioob) {
- System.err.println("IOOB: " + ioob);
- System.err.println("y: " + y);
- }
- catch (EOFException eof) {
- System.err.println("EOF: " + eof);
- System.err.println("y: " + y);
- }
+ readRowByte(input, height, srcRegion, xSub, ySub, rowDataByte, destRaster, clippedRow, y);
break;
case 16:
@@ -414,9 +424,7 @@ public final class BMPImageReader extends ImageReaderBase {
}
private BufferedImage readUsingDelegate(final int compression, final ImageReadParam param) throws IOException {
- ImageReader reader = initReaderDelegate(compression);
-
- return reader.read(0, param);
+ return initReaderDelegate(compression).read(0, param);
}
private ImageReader initReaderDelegate(int compression) throws IOException {
@@ -454,12 +462,19 @@ public final class BMPImageReader extends ImageReaderBase {
// Consider looking for specific PNG and JPEG implementations.
Iterator readers = ImageIO.getImageReadersByFormatName(format);
+
if (!readers.hasNext()) {
throw new IIOException(String.format("Delegate ImageReader for %s format not found", format));
}
ImageReader reader = readers.next();
+ // Install listener
+ ListenerDelegator listenerDelegator = new ListenerDelegator();
+ reader.addIIOReadWarningListener(listenerDelegator);
+ reader.addIIOReadProgressListener(listenerDelegator);
+ reader.addIIOReadUpdateListener(listenerDelegator);
+
// Cache for later use
switch (compression) {
case DIB.COMPRESSION_JPEG:
@@ -503,8 +518,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
- }
- else {
+ } else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
@@ -536,8 +550,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
- }
- else {
+ } else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
@@ -545,7 +558,7 @@ public final class BMPImageReader extends ImageReaderBase {
}
private void readRowInt(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
- final int [] rowDataInt, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
+ final int[] rowDataInt, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataInt.length * 4);
@@ -564,8 +577,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
- }
- else {
+ } else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
@@ -577,8 +589,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(shorts, 0, shorts.length);
- }
- else {
+ } else {
for (int i = 0; i < shorts.length; i++) {
shorts[i] = input.readShort();
}
@@ -590,8 +601,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(ints, 0, ints.length);
- }
- else {
+ } else {
for (int i = 0; i < ints.length; i++) {
ints[i] = input.readInt();
}
@@ -617,6 +627,31 @@ public final class BMPImageReader extends ImageReaderBase {
return raster.createWritableChild(rect.x, rect.y, rect.width, rect.height, 0, 0, bands);
}
+ @Override
+ public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
+ readHeader();
+
+ switch (header.getBitCount()) {
+ case 1:
+ case 2:
+ case 4:
+ case 8:
+ readColorMap();
+ break;
+
+ default:
+ if (header.colorsUsed > 0) {
+ readColorMap();
+ }
+ break;
+ }
+
+ // Why, oh why..? Instead of accepting it's own native format as it should,
+ // The BMPImageWriter only accepts instances of com.sun.imageio.plugins.bmp.BMPMetadata...
+ // TODO: Consider reflectively construct a BMPMetadata and inject fields
+ return new BMPMetadata(header, colors);
+ }
+
public static void main(String[] args) throws IOException {
BMPImageReaderSpi provider = new BMPImageReaderSpi();
BMPImageReader reader = new BMPImageReader(provider);
@@ -656,14 +691,12 @@ public final class BMPImageReader extends ImageReaderBase {
if (imageMetadata != null) {
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
}
- }
- catch (Throwable t) {
+ } catch (Throwable t) {
if (args.length > 1) {
System.err.println("---");
System.err.println("---> " + t.getClass().getSimpleName() + ": " + t.getMessage() + " for " + arg);
System.err.println("---");
- }
- else {
+ } else {
throwAs(RuntimeException.class, t);
}
}
@@ -674,4 +707,79 @@ public final class BMPImageReader extends ImageReaderBase {
static void throwAs(final Class pType, final Throwable pThrowable) throws T {
throw (T) pThrowable;
}
+
+ private class ListenerDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {
+ @Override
+ public void imageComplete(ImageReader source) {
+ processImageComplete();
+ }
+
+ @Override
+ public void imageProgress(ImageReader source, float percentageDone) {
+ processImageProgress(percentageDone);
+ }
+
+ @Override
+ public void imageStarted(ImageReader source, int imageIndex) {
+ processImageStarted(imageIndex);
+ }
+
+ @Override
+ public void readAborted(ImageReader source) {
+ processReadAborted();
+ }
+
+ @Override
+ public void sequenceComplete(ImageReader source) {
+ processSequenceComplete();
+ }
+
+ @Override
+ public void sequenceStarted(ImageReader source, int minIndex) {
+ processSequenceStarted(minIndex);
+ }
+
+ @Override
+ public void thumbnailComplete(ImageReader source) {
+ processThumbnailComplete();
+ }
+
+ @Override
+ public void thumbnailProgress(ImageReader source, float percentageDone) {
+ processThumbnailProgress(percentageDone);
+ }
+
+ @Override
+ public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
+ processThumbnailStarted(imageIndex, thumbnailIndex);
+ }
+
+ public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
+ processPassStarted(theImage, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
+ }
+
+ public void imageUpdate(ImageReader source, BufferedImage theImage, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
+ processImageUpdate(theImage, minX, minY, width, height, periodX, periodY, bands);
+ }
+
+ public void passComplete(ImageReader source, BufferedImage theImage) {
+ processPassComplete(theImage);
+ }
+
+ public void thumbnailPassStarted(ImageReader source, BufferedImage theThumbnail, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
+ processThumbnailPassStarted(theThumbnail, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
+ }
+
+ public void thumbnailUpdate(ImageReader source, BufferedImage theThumbnail, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
+ processThumbnailUpdate(theThumbnail, minX, minY, width, height, periodX, periodY, bands);
+ }
+
+ public void thumbnailPassComplete(ImageReader source, BufferedImage theThumbnail) {
+ processThumbnailPassComplete(theThumbnail);
+ }
+
+ public void warningOccurred(ImageReader source, String warning) {
+ processWarningOccurred(warning);
+ }
+ }
}
diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderSpi.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderSpi.java
index 1a26ca84..9d7f4817 100755
--- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderSpi.java
+++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderSpi.java
@@ -65,10 +65,10 @@ public final class BMPImageReaderSpi extends ImageReaderSpi {
},
"com.twelvemonkeys.imageio.plugins.bmp.BMPImageReader",
new Class[]{ImageInputStream.class},
- null,
- true, null, null, null, null,
+ new String[]{"com.sun.imageio.plugins.bmp.BMPImageWriterSpi"}, // We support the same native metadata format
+ false, null, null, null, null,
true,
- null, null,
+ BMPMetadata.nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat",
null, null
);
}
diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java
new file mode 100755
index 00000000..1148032a
--- /dev/null
+++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/BMPMetadata.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.bmp;
+
+import com.twelvemonkeys.lang.Validate;
+import org.w3c.dom.Node;
+
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataFormatImpl;
+import javax.imageio.metadata.IIOMetadataNode;
+import java.awt.image.IndexColorModel;
+
+/**
+ * BMPMetadata.
+ */
+final class BMPMetadata extends IIOMetadata {
+ /** We return metadata in the exact same form as the JRE built-in, to be compatible with the BMPImageWriter. */
+ public static final String nativeMetadataFormatName = "javax_imageio_bmp_1.0";
+
+ private final DIBHeader header;
+ private final int[] colorMap;
+
+ BMPMetadata(final DIBHeader header, final int[] colorMap) {
+ this.header = Validate.notNull(header, "header");
+ this.colorMap = colorMap == null || colorMap.length == 0 ? null : colorMap;
+
+ standardFormatSupported = true;
+ }
+
+ @Override public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override public Node getAsTree(final String formatName) {
+ if (formatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
+ return getStandardTree();
+ }
+ else if (formatName.equals(nativeMetadataFormatName)) {
+ return getNativeTree();
+ }
+ else {
+ throw new IllegalArgumentException("Unsupported metadata format: " + formatName);
+ }
+ }
+
+ @Override public void mergeTree(final String formatName, final Node root) {
+ if (isReadOnly()) {
+ throw new IllegalStateException("Metadata is read-only");
+ }
+ }
+
+ @Override public void reset() {
+ if (isReadOnly()) {
+ throw new IllegalStateException("Metadata is read-only");
+ }
+ }
+
+ @Override
+ public String getNativeMetadataFormatName() {
+ return nativeMetadataFormatName;
+ }
+
+ private Node getNativeTree() {
+ IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);
+
+ addChildNode(root, "BMPVersion", header.getBMPVersion());
+ addChildNode(root, "Width", header.getWidth());
+ addChildNode(root, "Height", header.getHeight());
+ addChildNode(root, "BitsPerPixel", (short) header.getBitCount());
+ addChildNode(root, "Compression", header.getCompression());
+ addChildNode(root, "ImageSize", header.getImageSize());
+
+ IIOMetadataNode pixelsPerMeter = addChildNode(root, "PixelsPerMeter", null);
+ addChildNode(pixelsPerMeter, "X", header.xPixelsPerMeter);
+ addChildNode(pixelsPerMeter, "Y", header.yPixelsPerMeter);
+
+ addChildNode(root, "ColorsUsed", header.colorsUsed);
+ addChildNode(root, "ColorsImportant", header.colorsImportant);
+
+ if (header.getSize() == DIB.BITMAP_V4_INFO_HEADER_SIZE || header.getSize() == DIB.BITMAP_V5_INFO_HEADER_SIZE) {
+ IIOMetadataNode mask = addChildNode(root, "Mask", null);
+ addChildNode(mask, "Red", header.masks[0]);
+ addChildNode(mask, "Green", header.masks[1]);
+ addChildNode(mask, "Blue", header.masks[2]);
+ addChildNode(mask, "Alpha", header.masks[3]);
+
+ addChildNode(root, "ColorSpaceType", header.colorSpaceType);
+
+ // It makes no sense to include these if colorSpaceType != 0, but native format does it...
+ IIOMetadataNode cieXYZEndPoints = addChildNode(root, "CIEXYZEndPoints", null);
+ addXYZPoints(cieXYZEndPoints, "Red", header.cieXYZEndpoints[0], header.cieXYZEndpoints[1], header.cieXYZEndpoints[2]);
+ addXYZPoints(cieXYZEndPoints, "Green", header.cieXYZEndpoints[3], header.cieXYZEndpoints[4], header.cieXYZEndpoints[5]);
+ addXYZPoints(cieXYZEndPoints, "Blue", header.cieXYZEndpoints[6], header.cieXYZEndpoints[7], header.cieXYZEndpoints[8]);
+
+ // TODO: Gamma?! Will need a new native format version...
+
+ addChildNode(root, "Intent", header.intent);
+
+ // TODO: Profile data & profile size
+ }
+
+ // Palette
+ if (colorMap != null) {
+ IIOMetadataNode paletteNode = addChildNode(root, "Palette", null);
+
+ // The original BitmapCoreHeader has only RGB values in the palette, all others have RGBA
+ boolean hasAlpha = header.getSize() != DIB.BITMAP_CORE_HEADER_SIZE;
+
+ for (int color : colorMap) {
+ // NOTE: The native format has the red and blue values mixed up, we'll report the correct values
+ IIOMetadataNode paletteEntry = addChildNode(paletteNode, "PaletteEntry", null);
+ addChildNode(paletteEntry, "Red", (byte) ((color >> 16) & 0xff));
+ addChildNode(paletteEntry, "Green", (byte) ((color >> 8) & 0xff));
+ addChildNode(paletteEntry, "Blue", (byte) (color & 0xff));
+
+ // Not sure why the native format specifies this, as no palette-based BMP has alpha
+ if (hasAlpha) {
+ addChildNode(paletteEntry, "Alpha", (byte) ((color >>> 24) & 0xff));
+ }
+ }
+ }
+
+ return root;
+ }
+
+ private void addXYZPoints(IIOMetadataNode cieXYZNode, String color, double colorX, double colorY, double colorZ) {
+ IIOMetadataNode colorNode = addChildNode(cieXYZNode, color, null);
+ addChildNode(colorNode, "X", colorX);
+ addChildNode(colorNode, "Y", colorY);
+ addChildNode(colorNode, "Z", colorZ);
+ }
+
+ private IIOMetadataNode addChildNode(final IIOMetadataNode parent,
+ final String name,
+ final Object object) {
+ IIOMetadataNode child = new IIOMetadataNode(name);
+
+ if (object != null) {
+ child.setUserObject(object); // TODO: Should we always store user object?!?!
+ child.setNodeValue(object.toString()); // TODO: Fix this line
+ }
+
+ parent.appendChild(child);
+
+ return child;
+ }
+
+ @Override protected IIOMetadataNode getStandardChromaNode() {
+ // NOTE: BMP files may contain a color map, even if true color...
+ // Not sure if this is a good idea to expose to the meta data,
+ // as it might be unexpected... Then again...
+ if (colorMap != null) {
+ IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
+
+ IIOMetadataNode palette = new IIOMetadataNode("Palette");
+ chroma.appendChild(palette);
+
+ for (int i = 0; i < colorMap.length; i++) {
+ IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry");
+ paletteEntry.setAttribute("index", Integer.toString(i));
+
+ paletteEntry.setAttribute("red", Integer.toString((colorMap[i] >> 16) & 0xff));
+ paletteEntry.setAttribute("green", Integer.toString((colorMap[i] >> 8) & 0xff));
+ paletteEntry.setAttribute("blue", Integer.toString(colorMap[i] & 0xff));
+
+ palette.appendChild(paletteEntry);
+ }
+
+ return chroma;
+ }
+
+ return null;
+ }
+
+ @Override protected IIOMetadataNode getStandardCompressionNode() {
+ IIOMetadataNode compression = new IIOMetadataNode("Compression");
+ IIOMetadataNode compressionTypeName = addChildNode(compression, "CompressionTypeName", null);
+ compressionTypeName.setAttribute("value", "NONE");
+
+ return compression;
+// switch (header.getImageType()) {
+// case TGA.IMAGETYPE_COLORMAPPED_RLE:
+// case TGA.IMAGETYPE_TRUECOLOR_RLE:
+// case TGA.IMAGETYPE_MONOCHROME_RLE:
+// case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
+// case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
+// IIOMetadataNode node = new IIOMetadataNode("Compression");
+// IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
+//
+// // Compression can be RLE4, RLE8, PNG, JPEG or NONE
+// String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE
+// ? "Uknown" : "RLE";
+// compressionTypeName.setAttribute("value", value);
+// node.appendChild(compressionTypeName);
+//
+// IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
+// lossless.setAttribute("value", "TRUE"); // TODO: Unless JPEG!
+// node.appendChild(lossless);
+//
+// return node;
+// default:
+// // No compression
+// return null;
+// }
+ }
+
+ @Override protected IIOMetadataNode getStandardDataNode() {
+ IIOMetadataNode node = new IIOMetadataNode("Data");
+
+// IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
+// planarConfiguration.setAttribute("value", "PixelInterleaved");
+// node.appendChild(planarConfiguration);
+
+// IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
+// switch (header.getImageType()) {
+// case TGA.IMAGETYPE_COLORMAPPED:
+// case TGA.IMAGETYPE_COLORMAPPED_RLE:
+// case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
+// case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
+// sampleFormat.setAttribute("value", "Index");
+// break;
+// default:
+// sampleFormat.setAttribute("value", "UnsignedIntegral");
+// break;
+// }
+
+// node.appendChild(sampleFormat);
+
+ IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
+ switch (header.getBitCount()) {
+ case 1:
+ case 2:
+ case 4:
+ case 8:
+ bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getBitCount())));
+ break;
+ case 16:
+ // TODO: Consult masks here!
+ bitsPerSample.setAttribute("value", createListValue(4, Integer.toString(4)));
+ break;
+ case 24:
+ bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8)));
+ break;
+ case 32:
+ bitsPerSample.setAttribute("value", createListValue(4, Integer.toString(8)));
+ break;
+ }
+
+ node.appendChild(bitsPerSample);
+
+ // TODO: Do we need MSB?
+// IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
+// sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0"));
+
+ return node;
+ }
+
+ private String createListValue(final int itemCount, final String... values) {
+ StringBuilder buffer = new StringBuilder();
+
+ for (int i = 0; i < itemCount; i++) {
+ if (buffer.length() > 0) {
+ buffer.append(' ');
+ }
+
+ buffer.append(values[i % values.length]);
+ }
+
+ return buffer.toString();
+ }
+
+ @Override protected IIOMetadataNode getStandardDimensionNode() {
+ if (header.xPixelsPerMeter > 0 || header.yPixelsPerMeter > 0) {
+ IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
+
+ addChildNode(dimension, "PixelAspectRatio", null);
+ addChildNode(dimension, "HorizontalPhysicalPixelSpacing", null);
+ addChildNode(dimension, "VerticalPhysicalPixelSpacing", null);
+
+ // IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
+ //
+ // if (header.topDown) {
+ // imageOrientation.setAttribute("value", "FlipH");
+ // }
+ // else {
+ // imageOrientation.setAttribute("value", "Normal");
+ // }
+ //
+ // dimension.appendChild(imageOrientation);
+
+ return dimension;
+ }
+
+ return null;
+ }
+
+ // No document node
+
+ // No text node
+
+ // No tiling
+
+ @Override protected IIOMetadataNode getStandardTransparencyNode() {
+ return null;
+
+// IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
+//
+// IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
+//
+// // TODO: Consult masks
+// alpha.setAttribute("value", header.getBitCount() == 32 ? "nonpremultiplied" : "none");
+// transparency.appendChild(alpha);
+//
+// return transparency;
+ }
+}
diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIB.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIB.java
index 0fab58fe..1384ab92 100755
--- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIB.java
+++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIB.java
@@ -39,8 +39,6 @@ package com.twelvemonkeys.imageio.plugins.bmp;
* @see ICO file format (Wikipedia)
*/
interface DIB {
- int TYPE_UNKNOWN = 0;
-
int TYPE_ICO = 1;
int TYPE_CUR = 2;
@@ -89,6 +87,13 @@ interface DIB {
// int COMPRESSION_CMYK_RLE8 = 12;
// int COMPRESSION_CMYK_RLE5 = 13;
+ /* Color space types. */
+ int LCS_CALIBRATED_RGB = 0;
+ int LCS_sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B'; // 0x73524742
+ int LCS_WINDOWS_COLOR_SPACE = 'W' << 24 | 'i' << 16 | 'n' << 8 | ' '; // 0x57696e20
+ int PROFILE_LINKED = 'L' << 24 | 'I' << 16 | 'N' << 8 | 'K'; // 0x4c494e4b
+ int PROFILE_EMBEDDED = 'M' << 24 | 'B' << 16 | 'E' << 8 | 'D'; // 0x4d424544
+
/** PNG "magic" identifier */
long PNG_MAGIC = 0x89l << 56 | (long) 'P' << 48 | (long) 'N' << 40 | (long) 'G' << 32 | 0x0dl << 24 | 0x0al << 16 | 0x1al << 8 | 0x0al;
}
diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java
index 2a858f9a..cf696f51 100755
--- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java
+++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java
@@ -41,6 +41,9 @@ import java.io.IOException;
* @see BMP file format (Wikipedia)
*/
abstract class DIBHeader {
+ // Roughly 72 DPI
+ private final int DEFAULT_PIXELS_PER_METER = 2835;
+
protected int size;
protected int width;
// NOTE: If a bitmask is present, this value includes the height of the mask
@@ -70,7 +73,16 @@ abstract class DIBHeader {
// 0 means all colors are important
protected int colorsImportant;
+ // V4+ members below
protected int[] masks;
+ protected int colorSpaceType;
+ protected double[] cieXYZEndpoints;
+ protected int[] gamma;
+
+ // V5+ members below
+ protected int intent;
+ protected long profileData;
+ protected long profileSize;
protected DIBHeader() {
}
@@ -139,19 +151,19 @@ abstract class DIBHeader {
}
public int getXPixelsPerMeter() {
- return xPixelsPerMeter;
+ return xPixelsPerMeter != 0 ? xPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
}
public int getYPixelsPerMeter() {
- return yPixelsPerMeter;
+ return yPixelsPerMeter != 0 ? yPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
}
public int getColorsUsed() {
- return colorsUsed;
+ return colorsUsed != 0 ? colorsUsed : 1 << bitCount;
}
public int getColorsImportant() {
- return colorsImportant;
+ return colorsImportant != 0 ? colorsImportant : getColorsUsed();
}
public boolean hasMasks() {
@@ -167,10 +179,11 @@ abstract class DIBHeader {
"colors used: %d%s, colors important: %d%s",
getClass().getSimpleName(),
getSize(), getWidth(), getHeight(), getPlanes(), getBitCount(), getCompression(),
- getImageSize(), (getImageSize() == 0 ? " (unknown)" : ""),
- getXPixelsPerMeter(), getYPixelsPerMeter(),
- getColorsUsed(), (getColorsUsed() == 0 ? " (unknown)" : ""),
- getColorsImportant(), (getColorsImportant() == 0 ? " (all)" : "")
+ getImageSize(), (imageSize == 0 ? " (calculated)" : ""),
+ getXPixelsPerMeter(),
+ getYPixelsPerMeter(),
+ getColorsUsed(), (colorsUsed == 0 ? " (unknown)" : ""),
+ getColorsImportant(), (colorsImportant == 0 ? " (all)" : "")
);
}
@@ -183,6 +196,8 @@ abstract class DIBHeader {
return masks;
}
+ protected abstract String getBMPVersion();
+
// TODO: Get rid of code duplication below...
static final class BitmapCoreHeader extends DIBHeader {
@@ -204,10 +219,10 @@ abstract class DIBHeader {
planes = pStream.readUnsignedShort();
bitCount = pStream.readUnsignedShort();
+ }
- // Roughly 72 DPI
- xPixelsPerMeter = 2835;
- yPixelsPerMeter = 2835;
+ public String getBMPVersion() {
+ return "BMP v. 2.x";
}
}
@@ -239,12 +254,7 @@ abstract class DIBHeader {
planes = pStream.readUnsignedShort();
bitCount = pStream.readUnsignedShort();
- if (pSize == DIB.OS2_V2_HEADER_16_SIZE) {
- // Roughly 72 DPI
- xPixelsPerMeter = 2835;
- yPixelsPerMeter = 2835;
- }
- else {
+ if (pSize != DIB.OS2_V2_HEADER_16_SIZE) {
compression = pStream.readInt();
imageSize = pStream.readInt();
@@ -256,6 +266,7 @@ abstract class DIBHeader {
colorsImportant = pStream.readInt();
}
+ // TODO: Use? These fields are not reflected in metadata as per now...
int units = pStream.readShort();
int reserved = pStream.readShort();
int recording = pStream.readShort(); // Recording algorithm
@@ -265,6 +276,10 @@ abstract class DIBHeader {
int colorEncoding = pStream.readInt(); // Color model used in bitmap
int identifier = pStream.readInt(); // Reserved for application use
}
+
+ public String getBMPVersion() {
+ return "BMP v. 2.2";
+ }
}
@@ -306,6 +321,11 @@ abstract class DIBHeader {
colorsUsed = pStream.readInt();
colorsImportant = pStream.readInt();
}
+
+ public String getBMPVersion() {
+ // This is to be compatible with the native metadata of the original com.sun....BMPMetadata
+ return compression == DIB.COMPRESSION_BITFIELDS ? "BMP v. 3.x NT" : "BMP v. 3.x";
+ }
}
/**
@@ -342,6 +362,10 @@ abstract class DIBHeader {
masks = readMasks(pStream);
}
+
+ public String getBMPVersion() {
+ return "BMP v. 3.x Photoshop";
+ }
}
/**
@@ -377,10 +401,22 @@ abstract class DIBHeader {
masks = readMasks(pStream);
- byte[] data = new byte[52];
- pStream.readFully(data);
+ colorSpaceType = pStream.readInt(); // Should be 0 for V4
+ cieXYZEndpoints = new double[9];
-// System.out.println("data = " + Arrays.toString(data));
+ for (int i = 0; i < cieXYZEndpoints.length; i++) {
+ cieXYZEndpoints[i] = pStream.readInt(); // TODO: Hmmm...?
+ }
+
+ gamma = new int[3];
+
+ for (int i = 0; i < gamma.length; i++) {
+ gamma[i] = pStream.readInt();
+ }
+ }
+
+ public String getBMPVersion() {
+ return "BMP v. 4.x";
}
}
@@ -417,10 +453,28 @@ abstract class DIBHeader {
masks = readMasks(pStream);
- byte[] data = new byte[68];
- pStream.readFully(data);
+ colorSpaceType = pStream.readInt();
-// System.out.println("data = " + Arrays.toString(data));
+ cieXYZEndpoints = new double[9];
+
+ for (int i = 0; i < cieXYZEndpoints.length; i++) {
+ cieXYZEndpoints[i] = pStream.readInt(); // TODO: Hmmm...?
+ }
+
+ gamma = new int[3];
+
+ for (int i = 0; i < gamma.length; i++) {
+ gamma[i] = pStream.readInt();
+ }
+
+ intent = pStream.readInt(); // TODO: Verify if this is same as ICC intent
+ profileData = pStream.readInt() & 0xffffffffL;
+ profileSize = pStream.readInt() & 0xffffffffL;
+ pStream.readInt(); // Reserved
+ }
+
+ public String getBMPVersion() {
+ return "BMP v. 5.x";
}
}
}
\ No newline at end of file
diff --git a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java
index 8f44135a..d1417675 100755
--- a/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java
+++ b/imageio/imageio-bmp/src/test/java/com/twelvemonkeys/imageio/plugins/bmp/BMPImageReaderTest.java
@@ -2,19 +2,30 @@ package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test;
+import org.mockito.InOrder;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.event.IIOReadProgressListener;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.io.IOException;
-import java.net.URL;
+import java.lang.reflect.Constructor;
+import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
/**
* BMPImageReaderTest
@@ -160,4 +171,189 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCase jreReaderClass = (Class) Class.forName("com.sun.imageio.plugins.bmp.BMPImageReader");
+ Constructor constructor = jreReaderClass.getConstructor(ImageReaderSpi.class);
+ jreReader = constructor.newInstance(new Object[] {null});
+ }
+ catch (Exception e) {
+ System.err.println("WARNING: Skipping metadata tests: " + e);
+ e.printStackTrace();
+ return;
+ }
+
+ ImageReader reader = createReader();
+
+ for (TestData data : getTestData()) {
+ if (data.getInput().toString().contains("pal8offs")) {
+ continue;
+ }
+
+ reader.setInput(data.getInputStream());
+ jreReader.setInput(data.getInputStream());
+
+ IIOMetadata metadata = reader.getImageMetadata(0);
+
+ // WORKAROUND: JRE reader does not reset metadata on setInput. Invoking getWidth forces re-read of header and metadata.
+ try {
+ jreReader.getWidth(0);
+ }
+ catch (Exception e) {
+ System.err.println("WARNING: Reading " + data + " caused exception: " + e.getMessage());
+ continue;
+ }
+ IIOMetadata jreMetadata = jreReader.getImageMetadata(0);
+
+ assertEquals(true, metadata.isStandardMetadataFormatSupported());
+ assertEquals(jreMetadata.getNativeMetadataFormatName(), metadata.getNativeMetadataFormatName());
+ assertArrayEquals(jreMetadata.getExtraMetadataFormatNames(), metadata.getExtraMetadataFormatNames());
+
+ // TODO: Allow our standard metadata to be richer, but contain at least the information from the JRE impl
+
+ for (String format : jreMetadata.getMetadataFormatNames()) {
+ String absolutePath = data.toString();
+ String localPath = absolutePath.substring(absolutePath.lastIndexOf("test-classes") + 12);
+
+ Node expectedTree = jreMetadata.getAsTree(format);
+ Node actualTree = metadata.getAsTree(format);
+
+// try {
+ assertNodeEquals(localPath + " - " + format, expectedTree, actualTree);
+// }
+// catch (AssertionError e) {
+// ByteArrayOutputStream expected = new ByteArrayOutputStream();
+// ByteArrayOutputStream actual = new ByteArrayOutputStream();
+//
+// new XMLSerializer(expected, "UTF-8").serialize(expectedTree, false);
+// new XMLSerializer(actual, "UTF-8").serialize(actualTree, false);
+//
+// assertEquals(e.getMessage(), new String(expected.toByteArray(), "UTF-8"), new String(actual.toByteArray(), "UTF-8"));
+//
+// throw e;
+// }
+ }
+ }
+ }
+
+ private void assertNodeEquals(final String message, final Node expected, final Node actual) {
+ assertEquals(message + " class differs", expected.getClass(), actual.getClass());
+
+ if (!excludeEqualValueTest(expected)) {
+ assertEquals(message, expected.getNodeValue(), actual.getNodeValue());
+
+ if (expected instanceof IIOMetadataNode) {
+ IIOMetadataNode expectedIIO = (IIOMetadataNode) expected;
+ IIOMetadataNode actualIIO = (IIOMetadataNode) actual;
+
+ assertEquals(message, expectedIIO.getUserObject(), actualIIO.getUserObject());
+ }
+ }
+
+ NodeList expectedChildNodes = expected.getChildNodes();
+ NodeList actualChildNodes = actual.getChildNodes();
+
+ assertEquals(message + " child length differs: " + toString(expectedChildNodes) + " != " + toString(actualChildNodes),
+ expectedChildNodes.getLength(), actualChildNodes.getLength());
+
+ for (int i = 0; i < expectedChildNodes.getLength(); i++) {
+ Node expectedChild = expectedChildNodes.item(i);
+ Node actualChild = actualChildNodes.item(i);
+
+ assertEquals(message + " node name differs", expectedChild.getLocalName(), actualChild.getLocalName());
+ assertNodeEquals(message + "/" + expectedChild.getLocalName(), expectedChild, actualChild);
+ }
+ }
+
+ private boolean excludeEqualValueTest(final Node expected) {
+ if (expected.getLocalName().equals("ImageSize")) {
+ // JRE metadata returns 0, even if known in reader...
+ return true;
+ }
+ if (expected.getLocalName().equals("ColorsImportant")) {
+ // JRE metadata returns 0, even if known in reader...
+ return true;
+ }
+ if (expected.getParentNode() != null && expected.getParentNode().getLocalName().equals("PaletteEntry") && !expected.getNodeValue().equals("Green")) {
+ // JRE metadata returns RGB colors in BGR order
+ // JRE metadata returns 0 for alpha, when -1 (0xff) is at least just as correct (why contain alpha at all?)
+ return true;
+ }
+ if (expected.getLocalName().equals("Height") && expected.getNodeValue().startsWith("-")) {
+ // JRE metadata returns negative height for bottom/up images
+ // TODO: Decide if we should do the same, as there is no "orientation" or flag for bottom/up
+ return true;
+ }
+
+ return false;
+ }
+
+ private String toString(final NodeList list) {
+ if (list.getLength() == 0) {
+ return "[]";
+ }
+
+ StringBuilder builder = new StringBuilder("[");
+ for (int i = 0; i < list.getLength(); i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+
+ Node node = list.item(i);
+ builder.append(node.getLocalName());
+ }
+ builder.append("]");
+
+ return builder.toString();
+ }
}
\ No newline at end of file