TMI-70: BMP image metadata + bonus listener delegation.

This commit is contained in:
Harald Kuhr 2014-10-31 12:35:55 +01:00
parent 711d2bdc32
commit 98d82fb093
7 changed files with 832 additions and 117 deletions

View File

@ -459,6 +459,16 @@ public class XMLSerializer {
pOut.print(pNode.getTagName()); pOut.print(pNode.getTagName());
pOut.println(">"); 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 { else {
pOut.println("/>"); pOut.println("/>");
} }

View File

@ -32,11 +32,15 @@ import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.LittleEndianDataInputStream; import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.xml.XMLSerializer; import com.twelvemonkeys.xml.XMLSerializer;
import javax.imageio.*; import javax.imageio.*;
import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
@ -45,7 +49,6 @@ import java.awt.*;
import java.awt.color.ColorSpace; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.DataInput;
import java.io.EOFException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteOrder; import java.nio.ByteOrder;
@ -58,15 +61,16 @@ import java.util.Iterator;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$ * @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$
*
* @see com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader * @see com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader
*/ */
public final class BMPImageReader extends ImageReaderBase { public final class BMPImageReader extends ImageReaderBase {
private long pixelOffset; private long pixelOffset;
private DIBHeader header; private DIBHeader header;
private int[] colors;
private IndexColorModel colorMap;
private transient ImageReader jpegReaderDelegate; private ImageReader jpegReaderDelegate;
private transient ImageReader pngReaderDelegate; private ImageReader pngReaderDelegate;
public BMPImageReader() { public BMPImageReader() {
super(new BMPImageReaderSpi()); super(new BMPImageReaderSpi());
@ -80,6 +84,8 @@ public final class BMPImageReader extends ImageReaderBase {
protected void resetMembers() { protected void resetMembers() {
pixelOffset = 0; pixelOffset = 0;
header = null; header = null;
colors = null;
colorMap = null;
if (pngReaderDelegate != null) { if (pngReaderDelegate != null) {
pngReaderDelegate.dispose(); 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 @Override
public int getWidth(int pImageIndex) throws IOException { public int getWidth(int pImageIndex) throws IOException {
checkBounds(pImageIndex); checkBounds(pImageIndex);
@ -143,40 +196,6 @@ public final class BMPImageReader extends ImageReaderBase {
return Arrays.asList(getRawImageType(pImageIndex)).iterator(); 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 @Override
public ImageTypeSpecifier getRawImageType(int pImageIndex) throws IOException { public ImageTypeSpecifier getRawImageType(int pImageIndex) throws IOException {
checkBounds(pImageIndex); checkBounds(pImageIndex);
@ -190,22 +209,17 @@ public final class BMPImageReader extends ImageReaderBase {
case 2: case 2:
case 4: case 4:
case 8: case 8:
// TODO: Get rid of the fake DirectoryEntry and support color maps directly return IndexedImageTypeSpecifier.createFromIndexColorModel(readColorMap());
BitmapIndexed indexed = new BitmapIndexed(new DirectoryEntry() {}, header);
readColorMap(indexed);
return IndexedImageTypeSpecifier.createFromIndexColorModel(indexed.createColorModel());
case 16: case 16:
if (header.hasMasks()) { if (header.hasMasks()) {
int[] masks = getMasks(); int[] masks = getMasks();
return ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), return ImageTypeSpecifier.createPacked(
masks[0], ColorSpace.getInstance(ColorSpace.CS_sRGB),
masks[1], masks[0], masks[1], masks[2], masks[3],
masks[2], DataBuffer.TYPE_USHORT, false
masks[3], );
DataBuffer.TYPE_USHORT,
false);
} }
// Default if no mask is 555 // Default if no mask is 555
@ -222,13 +236,11 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.hasMasks()) { if (header.hasMasks()) {
int[] masks = getMasks(); int[] masks = getMasks();
return ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), return ImageTypeSpecifier.createPacked(
masks[0], ColorSpace.getInstance(ColorSpace.CS_sRGB),
masks[1], masks[0], masks[1], masks[2], masks[3],
masks[2], DataBuffer.TYPE_INT, false
masks[3], );
DataBuffer.TYPE_INT,
false);
} }
// Default if no mask // Default if no mask
@ -290,10 +302,18 @@ public final class BMPImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex); ImageTypeSpecifier rawType = getRawImageType(imageIndex);
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); 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 // BMP rows are padded to 4 byte boundary
int rowSizeBytes = ((header.getBitCount() * width + 31) / 32) * 4; int rowSizeBytes = ((header.getBitCount() * width + 31) / 32) * 4;
// Wrap // Wrap input according to compression
imageInput.seek(pixelOffset); imageInput.seek(pixelOffset);
DataInput input; DataInput input;
@ -369,17 +389,7 @@ public final class BMPImageReader extends ImageReaderBase {
case 8: case 8:
case 24: case 24:
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
try { readRowByte(input, height, srcRegion, xSub, ySub, rowDataByte, destRaster, clippedRow, y);
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);
}
break; break;
case 16: case 16:
@ -414,9 +424,7 @@ public final class BMPImageReader extends ImageReaderBase {
} }
private BufferedImage readUsingDelegate(final int compression, final ImageReadParam param) throws IOException { private BufferedImage readUsingDelegate(final int compression, final ImageReadParam param) throws IOException {
ImageReader reader = initReaderDelegate(compression); return initReaderDelegate(compression).read(0, param);
return reader.read(0, param);
} }
private ImageReader initReaderDelegate(int compression) throws IOException { 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. // Consider looking for specific PNG and JPEG implementations.
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(format); Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(format);
if (!readers.hasNext()) { if (!readers.hasNext()) {
throw new IIOException(String.format("Delegate ImageReader for %s format not found", format)); throw new IIOException(String.format("Delegate ImageReader for %s format not found", format));
} }
ImageReader reader = readers.next(); ImageReader reader = readers.next();
// Install listener
ListenerDelegator listenerDelegator = new ListenerDelegator();
reader.addIIOReadWarningListener(listenerDelegator);
reader.addIIOReadProgressListener(listenerDelegator);
reader.addIIOReadUpdateListener(listenerDelegator);
// Cache for later use // Cache for later use
switch (compression) { switch (compression) {
case DIB.COMPRESSION_JPEG: case DIB.COMPRESSION_JPEG:
@ -503,8 +518,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) { if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel); destChannel.setDataElements(0, y, srcChannel);
} } else {
else {
// Flip into position // Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub; int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel); destChannel.setDataElements(0, dstY, srcChannel);
@ -536,8 +550,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) { if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel); destChannel.setDataElements(0, y, srcChannel);
} } else {
else {
// Flip into position // Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub; int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel); 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, 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 subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) { if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataInt.length * 4); input.skipBytes(rowDataInt.length * 4);
@ -564,8 +577,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (header.topDown) { if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel); destChannel.setDataElements(0, y, srcChannel);
} } else {
else {
// Flip into position // Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub; int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel); destChannel.setDataElements(0, dstY, srcChannel);
@ -577,8 +589,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (input instanceof ImageInputStream) { if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go // Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(shorts, 0, shorts.length); ((ImageInputStream) input).readFully(shorts, 0, shorts.length);
} } else {
else {
for (int i = 0; i < shorts.length; i++) { for (int i = 0; i < shorts.length; i++) {
shorts[i] = input.readShort(); shorts[i] = input.readShort();
} }
@ -590,8 +601,7 @@ public final class BMPImageReader extends ImageReaderBase {
if (input instanceof ImageInputStream) { if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go // Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(ints, 0, ints.length); ((ImageInputStream) input).readFully(ints, 0, ints.length);
} } else {
else {
for (int i = 0; i < ints.length; i++) { for (int i = 0; i < ints.length; i++) {
ints[i] = input.readInt(); 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); 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 { public static void main(String[] args) throws IOException {
BMPImageReaderSpi provider = new BMPImageReaderSpi(); BMPImageReaderSpi provider = new BMPImageReaderSpi();
BMPImageReader reader = new BMPImageReader(provider); BMPImageReader reader = new BMPImageReader(provider);
@ -656,14 +691,12 @@ public final class BMPImageReader extends ImageReaderBase {
if (imageMetadata != null) { if (imageMetadata != null) {
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
} }
} } catch (Throwable t) {
catch (Throwable t) {
if (args.length > 1) { if (args.length > 1) {
System.err.println("---"); System.err.println("---");
System.err.println("---> " + t.getClass().getSimpleName() + ": " + t.getMessage() + " for " + arg); System.err.println("---> " + t.getClass().getSimpleName() + ": " + t.getMessage() + " for " + arg);
System.err.println("---"); System.err.println("---");
} } else {
else {
throwAs(RuntimeException.class, t); throwAs(RuntimeException.class, t);
} }
} }
@ -674,4 +707,79 @@ public final class BMPImageReader extends ImageReaderBase {
static <T extends Throwable> void throwAs(final Class<T> pType, final Throwable pThrowable) throws T { static <T extends Throwable> void throwAs(final Class<T> pType, final Throwable pThrowable) throws T {
throw (T) pThrowable; 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);
}
}
} }

View File

@ -65,10 +65,10 @@ public final class BMPImageReaderSpi extends ImageReaderSpi {
}, },
"com.twelvemonkeys.imageio.plugins.bmp.BMPImageReader", "com.twelvemonkeys.imageio.plugins.bmp.BMPImageReader",
new Class[]{ImageInputStream.class}, new Class[]{ImageInputStream.class},
null, new String[]{"com.sun.imageio.plugins.bmp.BMPImageWriterSpi"}, // We support the same native metadata format
true, null, null, null, null, false, null, null, null, null,
true, true,
null, null, BMPMetadata.nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat",
null, null null, null
); );
} }

View File

@ -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;
}
}

View File

@ -39,8 +39,6 @@ package com.twelvemonkeys.imageio.plugins.bmp;
* @see <a href="http://en.wikipedia.org/wiki/ICO_(icon_image_file_format)">ICO file format (Wikipedia)</a> * @see <a href="http://en.wikipedia.org/wiki/ICO_(icon_image_file_format)">ICO file format (Wikipedia)</a>
*/ */
interface DIB { interface DIB {
int TYPE_UNKNOWN = 0;
int TYPE_ICO = 1; int TYPE_ICO = 1;
int TYPE_CUR = 2; int TYPE_CUR = 2;
@ -89,6 +87,13 @@ interface DIB {
// int COMPRESSION_CMYK_RLE8 = 12; // int COMPRESSION_CMYK_RLE8 = 12;
// int COMPRESSION_CMYK_RLE5 = 13; // 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 */ /** PNG "magic" identifier */
long PNG_MAGIC = 0x89l << 56 | (long) 'P' << 48 | (long) 'N' << 40 | (long) 'G' << 32 | 0x0dl << 24 | 0x0al << 16 | 0x1al << 8 | 0x0al; long PNG_MAGIC = 0x89l << 56 | (long) 'P' << 48 | (long) 'N' << 40 | (long) 'G' << 32 | 0x0dl << 24 | 0x0al << 16 | 0x1al << 8 | 0x0al;
} }

View File

@ -41,6 +41,9 @@ import java.io.IOException;
* @see <a href="http://en.wikipedia.org/wiki/BMP_file_format">BMP file format (Wikipedia)</a> * @see <a href="http://en.wikipedia.org/wiki/BMP_file_format">BMP file format (Wikipedia)</a>
*/ */
abstract class DIBHeader { abstract class DIBHeader {
// Roughly 72 DPI
private final int DEFAULT_PIXELS_PER_METER = 2835;
protected int size; protected int size;
protected int width; protected int width;
// NOTE: If a bitmask is present, this value includes the height of the mask // 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 // 0 means all colors are important
protected int colorsImportant; protected int colorsImportant;
// V4+ members below
protected int[] masks; 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() { protected DIBHeader() {
} }
@ -139,19 +151,19 @@ abstract class DIBHeader {
} }
public int getXPixelsPerMeter() { public int getXPixelsPerMeter() {
return xPixelsPerMeter; return xPixelsPerMeter != 0 ? xPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
} }
public int getYPixelsPerMeter() { public int getYPixelsPerMeter() {
return yPixelsPerMeter; return yPixelsPerMeter != 0 ? yPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
} }
public int getColorsUsed() { public int getColorsUsed() {
return colorsUsed; return colorsUsed != 0 ? colorsUsed : 1 << bitCount;
} }
public int getColorsImportant() { public int getColorsImportant() {
return colorsImportant; return colorsImportant != 0 ? colorsImportant : getColorsUsed();
} }
public boolean hasMasks() { public boolean hasMasks() {
@ -167,10 +179,11 @@ abstract class DIBHeader {
"colors used: %d%s, colors important: %d%s", "colors used: %d%s, colors important: %d%s",
getClass().getSimpleName(), getClass().getSimpleName(),
getSize(), getWidth(), getHeight(), getPlanes(), getBitCount(), getCompression(), getSize(), getWidth(), getHeight(), getPlanes(), getBitCount(), getCompression(),
getImageSize(), (getImageSize() == 0 ? " (unknown)" : ""), getImageSize(), (imageSize == 0 ? " (calculated)" : ""),
getXPixelsPerMeter(), getYPixelsPerMeter(), getXPixelsPerMeter(),
getColorsUsed(), (getColorsUsed() == 0 ? " (unknown)" : ""), getYPixelsPerMeter(),
getColorsImportant(), (getColorsImportant() == 0 ? " (all)" : "") getColorsUsed(), (colorsUsed == 0 ? " (unknown)" : ""),
getColorsImportant(), (colorsImportant == 0 ? " (all)" : "")
); );
} }
@ -183,6 +196,8 @@ abstract class DIBHeader {
return masks; return masks;
} }
protected abstract String getBMPVersion();
// TODO: Get rid of code duplication below... // TODO: Get rid of code duplication below...
static final class BitmapCoreHeader extends DIBHeader { static final class BitmapCoreHeader extends DIBHeader {
@ -204,10 +219,10 @@ abstract class DIBHeader {
planes = pStream.readUnsignedShort(); planes = pStream.readUnsignedShort();
bitCount = pStream.readUnsignedShort(); bitCount = pStream.readUnsignedShort();
}
// Roughly 72 DPI public String getBMPVersion() {
xPixelsPerMeter = 2835; return "BMP v. 2.x";
yPixelsPerMeter = 2835;
} }
} }
@ -239,12 +254,7 @@ abstract class DIBHeader {
planes = pStream.readUnsignedShort(); planes = pStream.readUnsignedShort();
bitCount = pStream.readUnsignedShort(); bitCount = pStream.readUnsignedShort();
if (pSize == DIB.OS2_V2_HEADER_16_SIZE) { if (pSize != DIB.OS2_V2_HEADER_16_SIZE) {
// Roughly 72 DPI
xPixelsPerMeter = 2835;
yPixelsPerMeter = 2835;
}
else {
compression = pStream.readInt(); compression = pStream.readInt();
imageSize = pStream.readInt(); imageSize = pStream.readInt();
@ -256,6 +266,7 @@ abstract class DIBHeader {
colorsImportant = pStream.readInt(); colorsImportant = pStream.readInt();
} }
// TODO: Use? These fields are not reflected in metadata as per now...
int units = pStream.readShort(); int units = pStream.readShort();
int reserved = pStream.readShort(); int reserved = pStream.readShort();
int recording = pStream.readShort(); // Recording algorithm int recording = pStream.readShort(); // Recording algorithm
@ -265,6 +276,10 @@ abstract class DIBHeader {
int colorEncoding = pStream.readInt(); // Color model used in bitmap int colorEncoding = pStream.readInt(); // Color model used in bitmap
int identifier = pStream.readInt(); // Reserved for application use 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(); colorsUsed = pStream.readInt();
colorsImportant = 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); masks = readMasks(pStream);
} }
public String getBMPVersion() {
return "BMP v. 3.x Photoshop";
}
} }
/** /**
@ -377,10 +401,22 @@ abstract class DIBHeader {
masks = readMasks(pStream); masks = readMasks(pStream);
byte[] data = new byte[52]; colorSpaceType = pStream.readInt(); // Should be 0 for V4
pStream.readFully(data); 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); masks = readMasks(pStream);
byte[] data = new byte[68]; colorSpaceType = pStream.readInt();
pStream.readFully(data);
// 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";
} }
} }
} }

View File

@ -2,19 +2,30 @@ package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test; 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.ImageReader;
import javax.imageio.ImageTypeSpecifier; 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 javax.imageio.spi.ImageReaderSpi;
import java.awt.*; import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.lang.reflect.Constructor;
import java.net.URISyntaxException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue; 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 * BMPImageReaderTest
@ -160,4 +171,189 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCase<BMPImageRead
} }
} }
@Test
public void testAddIIOReadProgressListenerCallbacksJPEG() {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/bmpsuite/q/rgb24jpeg.bmp"), new Dimension(127, 64));
reader.setInput(data.getInputStream());
IIOReadProgressListener listener = mock(IIOReadProgressListener.class);
reader.addIIOReadProgressListener(listener);
try {
reader.read(0);
}
catch (IOException e) {
fail("Could not read image");
}
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener).imageComplete(reader);
}
@Test
public void testAddIIOReadProgressListenerCallbacksPNG() {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/bmpsuite/q/rgb24png.bmp"), new Dimension(127, 64));
reader.setInput(data.getInputStream());
IIOReadProgressListener listener = mock(IIOReadProgressListener.class);
reader.addIIOReadProgressListener(listener);
try {
reader.read(0);
}
catch (IOException e) {
fail("Could not read image");
}
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener).imageComplete(reader);
}
@Test
public void testMetadataEqualsJRE() throws IOException, URISyntaxException {
// Ignore this test if not on an Oracle JRE (com.sun...BMPImageReader not available)
ImageReader jreReader;
try {
@SuppressWarnings("unchecked")
Class<ImageReader> jreReaderClass = (Class<ImageReader>) Class.forName("com.sun.imageio.plugins.bmp.BMPImageReader");
Constructor<ImageReader> 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();
}
} }