BMPMetadata.java
/*
* 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 of the copyright holder 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 HOLDER 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.imageio.AbstractMetadata;
import com.twelvemonkeys.lang.Validate;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
/**
* BMPMetadata.
*/
final class BMPMetadata extends AbstractMetadata {
/** 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) {
super(true, nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat", null, null);
this.header = Validate.notNull(header, "header");
this.colorMap = colorMap == null || colorMap.length == 0 ? null : colorMap;
}
@Override
protected 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 metadata,
// 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);
// TODO: Should the compression names always match the compression names used in the ImageWriteParam?
// OR should they be as standard as possible..?
// The built-in plugin uses "BI_RGB", "BI_RLE8", "BI_RLE4", "BI_BITFIELDS", "BI_JPEG and "BI_PNG"
switch (header.compression) {
case DIB.COMPRESSION_RLE4:
case DIB.COMPRESSION_RLE8:
compressionTypeName.setAttribute("value", "RLE");
break;
case DIB.COMPRESSION_JPEG:
compressionTypeName.setAttribute("value", "JPEG");
break;
case DIB.COMPRESSION_PNG:
compressionTypeName.setAttribute("value", "PNG");
break;
case DIB.COMPRESSION_RGB:
case DIB.COMPRESSION_BITFIELDS:
case DIB.COMPRESSION_ALPHA_BITFIELDS:
default:
compressionTypeName.setAttribute("value", "NONE");
break;
}
return compression;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
switch (header.getBitCount()) {
// TODO: case 0: determined by embedded format (PNG/JPEG)
case 1:
case 2:
case 4:
case 8:
bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getBitCount())));
break;
case 16:
// Default is 555
bitsPerSample.setAttribute("value", header.hasMasks()
? createBitsPerSampleForBitMasks()
: createListValue(3, Integer.toString(5)));
break;
case 24:
bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8)));
break;
case 32:
// Default is 888
bitsPerSample.setAttribute("value", header.hasMasks()
? createBitsPerSampleForBitMasks()
: createListValue(3, Integer.toString(8)));
break;
}
node.appendChild(bitsPerSample);
return node;
}
private String createBitsPerSampleForBitMasks() {
boolean hasAlpha = header.masks[3] != 0;
return createListValue(hasAlpha ? 4 : 3,
Integer.toString(countMaskBits(header.masks[0])), Integer.toString(countMaskBits(header.masks[1])),
Integer.toString(countMaskBits(header.masks[2])), Integer.toString(countMaskBits(header.masks[3])));
}
private int countMaskBits(int mask) {
// See https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan
int count;
for (count = 0; mask != 0; count++) {
mask &= mask - 1; // clear the least significant bit set
}
return count;
}
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() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
if (header.xPixelsPerMeter > 0 && header.yPixelsPerMeter > 0) {
float ratio = header.xPixelsPerMeter / (float) header.yPixelsPerMeter;
addChildNode(dimension, "PixelAspectRatio", null)
.setAttribute("value", String.valueOf(ratio));
addChildNode(dimension, "HorizontalPixelSize", null)
.setAttribute("value", String.valueOf(1f / header.xPixelsPerMeter * 1000));
addChildNode(dimension, "VerticalPixelSize", null)
.setAttribute("value", String.valueOf(1f / header.yPixelsPerMeter * 1000));
// Hmmm.. The JRE version includes these for some reason, even if values seem to be same as default...
addChildNode(dimension, "HorizontalPhysicalPixelSpacing", null)
.setAttribute("value", String.valueOf(0));
addChildNode(dimension, "VerticalPhysicalPixelSpacing", null)
.setAttribute("value", String.valueOf(0));
}
if (header.topDown) {
addChildNode(dimension, "ImageOrientation", null)
.setAttribute("value", "FlipH"); // For BMP, bottom-up is "normal"...
}
return dimension;
}
// No document node
// No text node
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
if (header.hasMasks() && header.masks[3] != 0) {
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", "nonpremultiplied");
transparency.appendChild(alpha);
return transparency;
}
return null;
}
}