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(""); + } 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