/*
* BMPDecoder.java
* Transform
*
* Copyright (c) 2009-2010 Flagstone Software Ltd. 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 Flagstone Software Ltd. 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.flagstone.transform.util.image;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import com.flagstone.transform.coder.Coder;
import com.flagstone.transform.coder.LittleDecoder;
import com.flagstone.transform.image.DefineImage;
import com.flagstone.transform.image.DefineImage2;
import com.flagstone.transform.image.ImageFormat;
import com.flagstone.transform.image.ImageTag;
/**
* BMPDecoder decodes Bitmap images (BMP) so they can be used in a Flash file.
*/
@SuppressWarnings("PMD.TooManyMethods")
public final class BMPDecoder implements ImageProvider, ImageDecoder {
/** Level used to indicate an opaque colour. */
private static final int OPAQUE = 255;
/** Message used to signal that the image cannot be decoded. */
private static final String BAD_FORMAT = "Unsupported Format";
/** The signature identifying BMP files. */
private static final int[] SIGNATURE = {66, 77};
/** An uncompressed indexed image. */
private static final int BI_RGB = 0;
/** A run-length compressed image with 8 bits per pixel. */
private static final int BI_RLE8 = 1;
/** A run-length compressed image with 4 bits per pixel. */
private static final int BI_RLE4 = 2;
/** A true-colour image. */
private static final int BI_BITFIELDS = 3;
/** The size of the header for an uncompressed image. */
private static final int UNZIPPED_LENGTH = 12;
/** The size of the header for an compressed image. */
private static final int ZIPPED_LENGTH = 40;
/** Size of each colour table entry or pixel in a true colour image. */
private static final int COLOUR_CHANNELS = 4;
/** Size of a pixel in an indexed image with 1 bit per pixel. */
private static final int IDX_1 = 1;
/** Size of a pixel in an indexed image with 2 bits per pixel. */
private static final int IDX_2 = 2;
/** Size of a pixel in an indexed image with 4 bits per pixel. */
private static final int IDX_4 = 4;
/** Size of a pixel in an indexed image with 8 bits per pixel. */
private static final int IDX_8 = 8;
/** Size of a pixel in a RGB555 true colour image. */
private static final int RGB5_SIZE = 16;
/** Size of a pixel in a RGB8 true colour image. */
private static final int RGB8_SIZE = 24;
/** Size of a pixel in a RGB8 true colour image. */
private static final int RGBA_SIZE = 32;
/** Number of bits for each colour channel in a RGB555 pixel. */
private static final int RGB5_DEPTH = 5;
/** Number of bits for each colour channel in a RGB8 pixel. */
private static final int RGB8_DEPTH = 8;
/** Byte offset to red channel. */
private static final int RED = 0;
/** Byte offset to red channel. */
private static final int GREEN = 1;
/** Byte offset to blue channel. */
private static final int BLUE = 2;
/** Byte offset to alpha channel. */
private static final int ALPHA = 3;
/** Mask used to extract red channel from a 16-bit RGB555 pixel. */
private static final int R5_MASK = 0x7C00;
/** Mask used to extract green channel from a 16-bit RGB555 pixel. */
private static final int G5_MASK = 0x03E0;
/** Mask used to extract blue channel from a 16-bit RGB555 pixel. */
private static final int B5_MASK = 0x001F;
/** Shift used to align the RGB555 red channel to a 8-bit pixel. */
private static final int R5_SHIFT = 7;
/** Shift used to align the RGB555 green channel to a 8-bit pixel. */
private static final int G5_SHIFT = 2;
/** Shift used to align the RGB555 blue channel to a 8-bit pixel. */
private static final int B5_SHIFT = 3;
/** Mask used to extract red channel from a 16-bit RGB565 pixel. */
private static final int R6_MASK = 0x7C00;
/** Mask used to extract green channel from a 16-bit RGB565 pixel. */
private static final int G6_MASK = 0x03E0;
/** Mask used to extract blue channel from a 16-bit RGB565 pixel. */
private static final int B6_MASK = 0x001F;
/** Shift used to align the RGB565 red channel to a 8-bit pixel. */
private static final int R6_SHIFT = 8;
/** Shift used to align the RGB565 green channel to a 8-bit pixel. */
private static final int G6_SHIFT = 3;
/** Shift used to align the RGB565 blue channel to a 8-bit pixel. */
private static final int B6_SHIFT = 3;
/** The format of the decoded image. */
private transient ImageFormat format;
/** The width of the image in pixels. */
private transient int width;
/** The height of the image in pixels. */
private transient int height;
/** The colour table for indexed images. */
private transient byte[] table;
/** The image data. */
private transient byte[] image;
/** The number of bits per pixel. */
private transient int bitDepth;
/** The method used to compress the image. */
private transient int compressionMethod;
/** The bit mask used to extract the red channel from the pixel word. */
private transient int redMask;
/** Shift for the red pixel to convert to an 8-bit colour. */
private transient int redShift;
/** The bit mask used to extract the green channel from the pixel word. */
private transient int greenMask;
/** Shift for the green pixel to convert to an 8-bit colour. */
private transient int greenShift;
/** The bit mask used to extract the blue channel from the pixel word. */
private transient int blueMask;
/** Shift for the blue pixel to convert to an 8-bit colour. */
private transient int blueShift;
/** Size of a pixel in bits. */
private transient int bitsPerPixel;
/** Number of colours used in each pixel. */
private transient int coloursUsed;
/** {@inheritDoc} */
@Override
public void read(final File file) throws IOException, DataFormatException {
read(new FileInputStream(file));
}
/** {@inheritDoc} */
@Override
public void read(final URL url) throws IOException, DataFormatException {
final URLConnection connection = url.openConnection();
if (!connection.getContentType().equals("image/bmp")) {
throw new DataFormatException(BAD_FORMAT);
}
final int length = connection.getContentLength();
if (length < 0) {
throw new FileNotFoundException(url.getFile());
}
read(url.openStream());
}
/** {@inheritDoc} */
@Override
public ImageTag defineImage(final int identifier) {
ImageTag object = null;
final ImageFilter filter = new ImageFilter();
switch (format) {
case IDX8:
object = new DefineImage(identifier, width, height,
table.length / COLOUR_CHANNELS,
zip(filter.merge(filter.adjustScan(width, height, image),
table)));
break;
case IDXA:
object = new DefineImage2(identifier, width, height,
table.length / COLOUR_CHANNELS,
zip(filter.mergeAlpha(
filter.adjustScan(width, height, image), table)));
break;
case RGB5:
object = new DefineImage(identifier, width, height,
zip(filter.packColors(width, height, image)), RGB5_SIZE);
break;
case RGB8:
filter.orderAlpha(image);
object = new DefineImage(identifier, width, height, zip(image),
RGB8_SIZE);
break;
case RGBA:
filter.applyAlpha(image);
object = new DefineImage2(identifier, width, height, zip(image));
break;
default:
throw new AssertionError(BAD_FORMAT);
}
return object;
}
/** {@inheritDoc} */
@Override
public ImageDecoder newDecoder() {
return new BMPDecoder();
}
/** {@inheritDoc} */
@Override
public int getWidth() {
return width;
}
/** {@inheritDoc} */
@Override
public int getHeight() {
return height;
}
/** {@inheritDoc} */
@Override
public byte[] getImage() {
byte[] copy;
switch (format) {
case IDX8:
case IDXA:
copy = new byte[image.length * COLOUR_CHANNELS];
int tableIndex;
for (int i = 0, index = 0; i < image.length; i++) {
tableIndex = image[i] * COLOUR_CHANNELS;
copy[index++] = table[tableIndex + RED];
copy[index++] = table[tableIndex + GREEN];
copy[index++] = table[tableIndex + BLUE];
copy[index++] = table[tableIndex + ALPHA];
}
break;
case RGB5:
case RGB8:
case RGBA:
copy = Arrays.copyOf(image, image.length);
break;
default:
throw new AssertionError(BAD_FORMAT);
}
return copy;
}
/** {@inheritDoc} */
@Override
public void read(final InputStream stream)
throws DataFormatException, IOException {
final LittleDecoder coder = new LittleDecoder(stream);
coder.mark();
for (int i = 0; i < 2; i++) {
if (coder.readByte() != SIGNATURE[i]) {
throw new DataFormatException(BAD_FORMAT);
}
}
coder.readInt(); // fileSize
coder.readInt(); // reserved
final int offset = coder.readInt();
final int headerSize = coder.readInt();
if (headerSize == ZIPPED_LENGTH) {
decodeCompressedHeader(coder);
} else {
decodeHeader(coder);
}
decodeFormat(bitsPerPixel);
if (format == ImageFormat.IDX8) {
coloursUsed = 1 << bitsPerPixel;
if (headerSize == UNZIPPED_LENGTH) {
decodeTable(coloursUsed, coder);
} else {
decodeTableWithAlpha(coloursUsed, coder);
}
coder.skip(offset - coder.bytesRead());
decodeIndexedImage(coder);
} else {
coder.skip(offset - coder.bytesRead());
decodeColourImage(coder);
}
}
/**
* Decode the header record for an uncompressed image.
* @param coder the Decoder containing the data.
* @throws IOException if an error occurs during decoding.
*/
private void decodeHeader(final LittleDecoder coder) throws IOException {
width = coder.readUnsignedShort();
height = coder.readUnsignedShort();
coder.readUnsignedShort(); // bitPlanes
bitsPerPixel = coder.readUnsignedShort();
coloursUsed = 0;
}
/**
* Decode the header record for an compressed image.
* @param coder the Decoder containing the data.
* @throws IOException if an error occurs during decoding.
*/
private void decodeCompressedHeader(final LittleDecoder coder)
throws IOException {
width = coder.readInt();
height = coder.readInt();
coder.readUnsignedShort(); // bitPlanes
bitsPerPixel = coder.readUnsignedShort();
compressionMethod = coder.readInt();
coder.readInt(); // imageSize
coder.readInt(); // horizontalResolution
coder.readInt(); // verticalResolution
coloursUsed = coder.readInt();
coder.readInt(); // importantColours
if (compressionMethod == BI_BITFIELDS) {
decodeMasks(coder);
}
}
/**
* Decode the bit masks to extra colour channels from a compressed image.
* @param coder the Decoder containing the data.
* @throws IOException if an error occurs during decoding.
*/
private void decodeMasks(final LittleDecoder coder) throws IOException {
redMask = coder.readInt();
greenMask = coder.readInt();
blueMask = coder.readInt();
if (redMask == R5_MASK) {
redShift = R5_SHIFT;
} else if (redMask == R6_MASK) {
redShift = R6_SHIFT;
}
if (greenMask == G5_MASK) {
greenShift = G5_SHIFT;
} else if (greenMask == G6_MASK) {
greenShift = G6_SHIFT;
}
if (blueMask == B5_MASK) {
blueShift = B5_SHIFT;
} else if (blueMask == B6_MASK) {
blueShift = B6_SHIFT;
}
}
/**
* Set the ImageFormat inferred from the pixel size.
* @param pixelSize the number of bits in each pixel.
* @throws DataFormatException if the pixel size is not supported.
*/
private void decodeFormat(final int pixelSize)
throws DataFormatException {
switch (pixelSize) {
case IDX_1:
format = ImageFormat.IDX8;
bitDepth = pixelSize;
break;
case IDX_2:
format = ImageFormat.IDX8;
bitDepth = pixelSize;
break;
case IDX_4:
format = ImageFormat.IDX8;
bitDepth = pixelSize;
break;
case IDX_8:
format = ImageFormat.IDX8;
bitDepth = pixelSize;
break;
case RGB5_SIZE:
format = ImageFormat.RGB5;
bitDepth = RGB5_DEPTH;
break;
case RGB8_SIZE:
format = ImageFormat.RGB8;
bitDepth = RGB8_DEPTH;
break;
case RGBA_SIZE:
format = ImageFormat.RGBA;
bitDepth = RGB8_DEPTH;
break;
default:
throw new DataFormatException(BAD_FORMAT);
}
}
/**
* Decode the colour palette with opaque colours.
*
* @param numColours the number of entries in the table.
* @param coder the decoder containing the table data.
* @throws IOException if an error occurs while decoding the table.
*/
private void decodeTable(final int numColours, final LittleDecoder coder)
throws IOException {
int index = 0;
table = new byte[numColours * COLOUR_CHANNELS];
for (int i = 0; i < numColours; i++) {
table[index + ALPHA] = (byte) OPAQUE;
table[index + BLUE] = (byte) coder.readByte();
table[index + GREEN] = (byte) coder.readByte();
table[index + RED] = (byte) coder.readByte();
index += COLOUR_CHANNELS;
}
}
/**
* Decode the colour palette with transparent colours.
*
* @param numColours the number of entries in the table.
* @param coder the decoder containing the table data.
* @throws IOException if an error occurs while decoding the table.
*/
private void decodeTableWithAlpha(final int numColours,
final LittleDecoder coder) throws IOException {
int index = 0;
table = new byte[numColours * COLOUR_CHANNELS];
for (int i = 0; i < numColours; i++) {
table[index + RED] = (byte) coder.readByte();
table[index + GREEN] = (byte) coder.readByte();
table[index + BLUE] = (byte) coder.readByte();
table[index + ALPHA] = (byte) coder.readByte();
index += COLOUR_CHANNELS;
}
}
/**
* Decode an indexed image.
* @param coder LittleDecoder object containing the encoded image data.
* @throws IOException if an error occurs reading the image data.
* @throws DataFormatException if the image is encoded in an unsupported
* format.
*/
private void decodeIndexedImage(final LittleDecoder coder)
throws IOException, DataFormatException {
image = new byte[height * width];
switch (compressionMethod) {
case BI_RGB:
decodeIDX8(coder);
break;
case BI_RLE8:
decodeRLE8(coder);
break;
case BI_RLE4:
decodeRLE4(coder);
break;
default:
throw new DataFormatException(BAD_FORMAT);
}
}
/**
* Decode a true-colour image.
* @param coder LittleDecoder object containing the encoded image data.
* @throws IOException if an error occurs reading the image data.
* @throws DataFormatException if the image is encoded in an unsupported
* format.
*/
private void decodeColourImage(final LittleDecoder coder)
throws IOException, DataFormatException {
image = new byte[height * width * COLOUR_CHANNELS];
switch (format) {
case RGB5:
decodeRGB5(coder);
break;
case RGB8:
decodeRGB8(coder);
break;
case RGBA:
decodeRGBA(coder);
break;
default:
throw new DataFormatException(BAD_FORMAT);
}
}
/**
* Decode the indexed image data block (IDX8).
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeIDX8(final LittleDecoder coder) throws IOException {
int bitsRead;
int index = 0;
for (int row = height - 1; row > 0; row--) {
bitsRead = 0;
index = row * width;
for (int col = 0; col < width; col++) {
image[index++] = (byte) coder.readBits(bitDepth, false);
bitsRead += bitDepth;
}
if (bitsRead % 32 > 0) {
coder.readBits(32 - (bitsRead % 32), false);
}
}
}
/**
* Decode the run length encoded image data block (RLE4).
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeRLE4(final LittleDecoder coder) throws IOException {
int row = height - 1;
int col = 0;
int index = 0;
int value;
boolean hasMore = true;
int code;
int count;
while (hasMore) {
count = coder.readByte();
if (count == 0) {
code = coder.readByte();
switch (code) {
case 0:
col = 0;
row--;
break;
case 1:
hasMore = false;
break;
case 2:
col += coder.readUnsignedShort();
row -= coder.readUnsignedShort();
decodeRLE4Pixels(code, coder, row, col);
break;
default:
decodeRLE4Pixels(code, coder, row, col);
break;
}
} else {
value = coder.readByte();
final byte indexA = (byte) (value >>> Coder.TO_LOWER_NIB);
final byte indexB = (byte) (value & Coder.NIB0);
index = row * width + col;
for (int i = 0; (i < count) && (col < width); i++, col++) {
image[index++] = (i % 2 > 0) ? indexB : indexA;
}
}
}
}
/**
* Decode a block of run-length encoded pixels where each pixel occupies
* 4 bits.
* @param code the number of pixels to decode.
* @param coder a LittleEncoder object containing the encoded image.
* @param row the row number for the starting pixel.
* @param col the column number for the starting pixel.
* @throws IOException if there is an error reading the image data.
*/
private void decodeRLE4Pixels(final int code, final LittleDecoder coder,
final int row, final int col) throws IOException {
int index = row * width + col;
int value;
for (int i = 0; i < code; i += 2) {
value = coder.readByte();
image[index++] = (byte) (value >>> Coder.TO_LOWER_NIB);
image[index++] = (byte) (value & Coder.NIB0);
}
if ((code & 2) == 2) {
coder.readByte();
}
}
/**
* Decode the run length encoded image data block (RLE8).
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeRLE8(final LittleDecoder coder) throws IOException {
int row = height - 1;
int col = 0;
boolean hasMore = true;
int code;
int count;
while (hasMore) {
count = coder.readByte();
if (count == 0) {
code = coder.readByte();
switch (code) {
case 0:
col = 0;
row--;
break;
case 1:
hasMore = false;
break;
case 2:
col += coder.readUnsignedShort();
row -= coder.readUnsignedShort();
decodeRLE8Pixels(code, coder, row, col);
break;
default:
decodeRLE8Pixels(code, coder, row, col);
break;
}
} else {
decodeRLE8Run(count, row, col, (byte) coder.readByte());
}
}
}
/**
* Decode a block of run-length encoded pixels where each pixel occupies
* 8 bits.
* @param code the number of pixels to decode.
* @param coder a LittleEncoder object containing the encoded image.
* @param row the row number for the starting pixel.
* @param col the column number for the starting pixel.
* @throws IOException if there is an error reading the image data.
*/
private void decodeRLE8Pixels(final int code, final LittleDecoder coder,
final int row, final int col) throws IOException {
int index = row * width + col;
for (int i = 0; i < code; i++) {
image[index++] = (byte) coder.readByte();
}
if ((code & 1) == 1) {
coder.readByte();
}
}
/**
* Decode a series of pixels where each pixel occupies 8 bits and has the
* same value.
* @param count the number of pixels to decode.
* @param row the row number for the starting pixel.
* @param col the column number for the starting pixel.
* @param value value for each pixel.
*/
private void decodeRLE8Run(final int count, final int row, final int col,
final byte value) {
int index = row * width + col;
for (int i = 0; i < count; i++) {
image[index++] = value;
}
}
/**
* Decode the true colour image with each colour channel taking 5-bits.
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeRGB5(final LittleDecoder coder) throws IOException {
int index = 0;
int colour;
for (int row = height - 1; row > 0; row--) {
coder.mark();
for (int col = 0; col < width; col++) {
colour = coder.readUnsignedShort();
image[index + RED] = (byte) ((colour & redMask)
>> redShift);
image[index + GREEN] = (byte) ((colour & greenMask)
>> greenShift);
image[index + BLUE] = (byte) ((colour & blueMask)
<< blueShift);
image[index + ALPHA] = (byte) OPAQUE;
index += COLOUR_CHANNELS;
}
coder.alignToWord();
coder.unmark();
}
}
/**
* Decode the true colour image with each colour channel taking 8-bits.
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeRGB8(final LittleDecoder coder) throws IOException {
int bytesRead;
int index = 0;
for (int row = height - 1; row > 0; row--) {
bytesRead = 0;
for (int col = 0; col < width; col++) {
image[index + RED] = (byte) coder.readByte();
image[index + GREEN] = (byte) coder.readByte();
image[index + BLUE] = (byte) coder.readByte();
image[index + ALPHA] = (byte) OPAQUE;
index += COLOUR_CHANNELS;
bytesRead += 3;
}
if (bytesRead % 4 > 0) {
coder.readBytes(new byte[4 - (bytesRead % 4)]);
}
}
}
/**
* Decode the true colour image with each colour channel and alpha taking
* 8-bits.
* @param coder the decoder containing the image data.
* @throws IOException is there is an error decoding the data.
*/
private void decodeRGBA(final LittleDecoder coder) throws IOException {
int index = 0;
for (int row = height - 1; row > 0; row--) {
for (int col = 0; col < width; col++) {
image[index + BLUE] = (byte) coder.readByte();
image[index + GREEN] = (byte) coder.readByte();
image[index + RED] = (byte) coder.readByte();
// force alpha channel to be opaque
image[index + ALPHA] = (byte) coder.readByte();
image[index + ALPHA] = (byte) OPAQUE;
index += COLOUR_CHANNELS;
}
}
}
/**
* Compress the image using the ZIP format.
* @param img the image data.
* @return the compressed image.
*/
private byte[] zip(final byte[] img) {
final Deflater deflater = new Deflater();
deflater.setInput(img);
deflater.finish();
final byte[] compressedData = new byte[img.length * 2];
final int bytesCompressed = deflater.deflate(compressedData);
final byte[] newData = Arrays.copyOf(compressedData, bytesCompressed);
return newData;
}
}