/*
* BufferedImageEncoder.java
* Transform
*
* Copyright (c) 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.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import com.flagstone.transform.coder.Coder;
import com.flagstone.transform.image.DefineImage;
import com.flagstone.transform.image.DefineImage2;
import com.flagstone.transform.image.ImageFormat;
import com.flagstone.transform.image.ImageTag;
/**
* BufferedImageEncoder generates BufferedImages from Flash image definitions.
*/
public final class BufferedImageEncoder {
/** The number of bytes per pixel in a RGBA format image. */
private static final int BYTES_PER_PIXEL = 4;
/** The alpha channel level for an opaque pixel. */
private static final int OPAQUE = -1;
/** Position in 32-bit word of red channel. */
private static final int RED = 0;
/** Position in 32-bit word of green channel. */
private static final int GREEN = 1;
/** Position in 32-bit word of blue channel. */
private static final int BLUE = 2;
/** Position in 32-bit word of alpha channel. */
private static final int ALPHA = 3;
/** Mask applied to extract 5-bit values. */
private static final int MASK_5BIT = 0x001F;
/** Mask applied to extract 8-bit values. */
private static final int MASK_8BIT = 0x00FF;
/**
* Number of bits to shift when aligning to the second byte in a 16-bit
* or 32-bit word. */
private static final int ALIGN_BYTE2 = 8;
/**
* Number of bits to shift when aligning to the second byte in a 16-bit
* or 32-bit word. */
private static final int ALIGN_BYTE3 = 16;
/**
* Number of bits to shift when aligning to the second byte in a 32-bit
* word. */
private static final int ALIGN_BYTE4 = 24;
/**
* Value added to offsets to ensure image width is aligned on a 16-bit
* boundary, 3 == 2 bytes + 1.
*/
private static final int WORD_ALIGN = 3;
/** Size of a pixel in a RGB555 true colour image. */
private static final int RGB5_SIZE = 16;
/** 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;
/**
* Decode an ImageTeg definition.
*
* @param definition
* a DefineImage object.
*
* @throws DataFormatException
* if there is a problem decoding the image definition.
*/
public void setImage(final ImageTag definition)
throws DataFormatException {
if (definition instanceof DefineImage) {
setImage((DefineImage) definition);
} else if (definition instanceof DefineImage2) {
setImage((DefineImage2) definition);
}
}
/**
* Decode a DefineImage definition.
*
* @param definition
* a DefineImage object.
*
* @throws DataFormatException
* if there is a problem decoding the image definition.
*/
public void setImage(final DefineImage definition)
throws DataFormatException {
width = definition.getWidth();
height = definition.getHeight();
if (definition.getTableSize() > 0) {
setIDX(definition);
} else {
if (definition.getPixelSize() == RGB5_SIZE) {
setRGB5(definition);
} else {
setRGB8(definition);
}
}
}
/**
* Decode a DefineImage2 definition.
*
* @param definition
* a DefineImage2 object.
*
* @throws DataFormatException
* if there is a problem decoding the image definition.
*/
public void setImage(final DefineImage2 definition)
throws DataFormatException {
if (definition.getTableSize() > 0) {
setIDXA(definition);
} else {
setRGBA(definition);
}
}
/**
* Get the width of the image.
* @return the width of the image in pixels.
*/
public int getWidth() {
return width;
}
/**
* Get the height of the image.
* @return the height of the image in pixels.
*/
public int getHeight() {
return height;
}
/**
* Get the array of bytes that make up the image.
*
* @return the array of bytes representing the image.
*/
public byte[] getImage() {
return Arrays.copyOf(image, image.length);
}
/**
* Decode an indexed image from a Flash image definition.
* @param definition the Flash object containing the indexed image.
* @throws DataFormatException if the image is in an unsupported format.
*/
private void setIDX(final DefineImage definition)
throws DataFormatException {
final byte[] data = unzip(definition.getImage(), width, height);
final int scanLength = (width + WORD_ALIGN) & ~WORD_ALIGN;
final int tableLength = definition.getTableSize();
int pos = 0;
int index = 0;
format = ImageFormat.IDX8;
table = new byte[tableLength * BYTES_PER_PIXEL];
image = new byte[height * width];
for (int i = 0; i < tableLength; i++, index += BYTES_PER_PIXEL) {
table[index + ALPHA] = OPAQUE;
table[index + BLUE] = data[pos++];
table[index + GREEN] = data[pos++];
table[index] = data[pos++];
}
index = 0;
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++, index++) {
image[index] = data[pos++];
}
pos += (scanLength - width);
}
}
/**
* Decode a true-colour image from a Flash image definition. The image
* contains 16-bit pixels.
*
* @param definition the Flash object containing the image.
* @throws DataFormatException if the image is in an unsupported format.
*/
private void setRGB5(final DefineImage definition)
throws DataFormatException {
final byte[] data = unzip(definition.getImage(), width, height);
final int scanLength = (width + WORD_ALIGN) & ~WORD_ALIGN;
int pos = 0;
int index = 0;
format = ImageFormat.RGB8;
image = new byte[height * width * BYTES_PER_PIXEL];
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++) {
final int color = (data[pos++] << ALIGN_BYTE2
| (data[pos++] & MASK_8BIT)) & Coder.LOWEST15;
image[index + ALPHA] = OPAQUE;
image[index + RED] = (byte) (color >> 10);
image[index + GREEN] = (byte) ((color >> 5)
& MASK_5BIT);
image[index + BLUE] = (byte) (color & MASK_5BIT);
index += BYTES_PER_PIXEL;
}
pos += (scanLength - width);
}
}
/**
* Decode a true-colour image from a Flash image definition. The image
* contains 24-bit pixels.
*
* @param definition the Flash object containing the image.
* @throws DataFormatException if the image is in an unsupported format.
*/
private void setRGB8(final DefineImage definition)
throws DataFormatException {
final byte[] data = unzip(definition.getImage(), width, height);
final int scanLength = (width + WORD_ALIGN) & ~WORD_ALIGN;
int pos = 0;
int index = 0;
format = ImageFormat.RGB8;
image = new byte[height * width * BYTES_PER_PIXEL];
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++) {
image[index + ALPHA] = OPAQUE;
image[index + RED] = data[pos++];
image[index + GREEN] = data[pos++];
image[index + BLUE] = data[pos++];
index += BYTES_PER_PIXEL;
}
pos += (scanLength - width);
}
}
/**
* Decode a indexed image from a Flash image definition. The colour table
* contains 32-bit pixels.
*
* @param definition the Flash object containing the image.
* @throws DataFormatException if the image is in an unsupported format.
*/
private void setIDXA(final DefineImage2 definition)
throws DataFormatException {
width = definition.getWidth();
height = definition.getHeight();
final byte[] data = unzip(definition.getImage(), width, height);
final int scanLength = (width + WORD_ALIGN) & ~WORD_ALIGN;
final int tableLength = definition.getTableSize();
int pos = 0;
int index = 0;
format = ImageFormat.IDXA;
table = new byte[tableLength * BYTES_PER_PIXEL];
image = new byte[height * width];
for (int i = 0; i < tableLength; i++, index += BYTES_PER_PIXEL) {
table[index + ALPHA] = data[pos++];
table[index + BLUE] = data[pos++];
table[index + GREEN] = data[pos++];
table[index] = data[pos++];
}
index = 0;
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++, index++) {
image[index] = data[pos++];
}
pos += (scanLength - width);
}
}
/**
* Decode a true-colour image from a Flash image definition. The image
* contains 32-bit pixels.
*
* @param definition the Flash object containing the image.
* @throws DataFormatException if the image is in an unsupported format.
*/
private void setRGBA(final DefineImage2 definition)
throws DataFormatException {
width = definition.getWidth();
height = definition.getHeight();
final byte[] data = unzip(definition.getImage(), width, height);
// final int scanLength = (imgWidth + WORD_ALIGN) & ~WORD_ALIGN;
int pos = 0;
int index = 0;
image = new byte[height * width * BYTES_PER_PIXEL];
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++, index += BYTES_PER_PIXEL) {
image[index + ALPHA] = data[pos++];
image[index + RED] = data[pos++];
image[index + GREEN] = data[pos++];
image[index + BLUE] = data[pos++];
}
}
}
/**
* Create a BufferedImage from the decoded Flash image.
*
* @return a BufferedImage containing the image.
*/
public BufferedImage getBufferedImage() {
BufferedImage bufferedImage;
if (format == ImageFormat.IDX8 || format == ImageFormat.IDXA) {
bufferedImage = getIndexedImage();
} else {
bufferedImage = getRGBAImage();
}
return bufferedImage;
}
/**
* Return the indexed image as a BufferedImage.
*
* @return A BufferedImage containing the image data.
*/
private BufferedImage getIndexedImage() {
final byte[] red = new byte[table.length];
final byte[] green = new byte[table.length];
final byte[] blue = new byte[table.length];
final byte[] alpha = new byte[table.length];
final int count = table.length / BYTES_PER_PIXEL;
int index = 0;
for (int i = 0; i < count; i++) {
red[i] = table[index + BLUE];
green[i] = table[index + GREEN];
blue[i] = table[index + RED];
alpha[i] = table[index + ALPHA];
index += BYTES_PER_PIXEL;
}
final BufferedImage bufferedImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
final int[] row = new int[width];
int color;
index = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++, index++) {
color = (image[index] & MASK_8BIT) << 2;
row[j] = (table[color + ALPHA] & MASK_8BIT)
<< ALIGN_BYTE4;
row[j] = row[j]
| ((table[color + 2] & MASK_8BIT)
<< ALIGN_BYTE3);
row[j] = row[j]
| ((table[color + 1] & MASK_8BIT)
<< ALIGN_BYTE2);
row[j] = row[j]
| (table[color + 0] & MASK_8BIT);
}
bufferedImage.setRGB(0, i, width, 1, row, 0, width);
}
return bufferedImage;
}
/**
* Return the 32-bit true-colour image as a BufferedImage.
*
* @return A BufferedImage containing the image data.
*/
private BufferedImage getRGBAImage() {
final BufferedImage bufferedImage = new BufferedImage(width,
height, BufferedImage.TYPE_INT_ARGB);
final int[] buffer = new int[width];
int index = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++, index += BYTES_PER_PIXEL) {
buffer[j] = (image[index + ALPHA] & MASK_8BIT)
<< ALIGN_BYTE4;
buffer[j] = buffer[j]
| ((image[index + RED] & MASK_8BIT)
<< ALIGN_BYTE3);
buffer[j] = buffer[j]
| ((image[index + GREEN] & MASK_8BIT)
<< ALIGN_BYTE2);
buffer[j] = buffer[j]
| (image[index + BLUE] & MASK_8BIT);
}
bufferedImage.setRGB(0, i, width, 1, buffer, 0, width);
}
return bufferedImage;
}
/**
* Resizes a BufferedImage to the specified width and height. The aspect
* ratio of the image is maintained so the area in the new image not covered
* by the resized original will be transparent.
*
* @param bufferedImg
* the BufferedImage to resize.
* @param imgWidth
* the width of the resized image in pixels.
* @param imgHeight
* the height of the resized image in pixels.
* @return a new BufferedImage with the specified width and height.
*/
public BufferedImage resizeImage(final BufferedImage bufferedImg,
final int imgWidth, final int imgHeight) {
int imageType = bufferedImg.getType();
if (imageType == BufferedImage.TYPE_CUSTOM) {
imageType = BufferedImage.TYPE_4BYTE_ABGR;
}
final BufferedImage resized = new BufferedImage(imgWidth, imgHeight,
BufferedImage.TYPE_4BYTE_ABGR);
final double widthRatio = (double) bufferedImg.getWidth()
/ (double) imgWidth;
final double heightRatio = (double) bufferedImg.getHeight()
/ (double) imgHeight;
double ratio = (widthRatio > heightRatio ? widthRatio : heightRatio);
if (ratio < 1.0) {
ratio = 1.0;
}
final int imageWidth = (int) (bufferedImg.getWidth() / ratio);
final int imageHeight = (int) (bufferedImg.getHeight() / ratio);
final int xCoord = (imgWidth - imageWidth) >> 1;
final int yCoord = (imgHeight - imageHeight) >> 1;
final Graphics2D graphics = resized.createGraphics();
graphics.setColor(new Color(0.0f, 0.0f, 0.0f, 0.0f));
graphics.fillRect(0, 0, imgWidth, imgHeight);
final java.awt.Image scaled = bufferedImg.getScaledInstance(imageWidth,
imageHeight, java.awt.Image.SCALE_SMOOTH);
new javax.swing.ImageIcon(scaled);
graphics.drawImage(scaled, xCoord, yCoord, null);
graphics.dispose();
resized.flush();
new javax.swing.ImageIcon(resized).getImage();
return resized;
}
/**
* Uncompress the image using the ZIP format.
* @param bytes the compressed image data.
* @param imgWidth the width of the image in pixels.
* @param imgHeight the height of the image in pixels.
* @return the uncompressed image.
* @throws DataFormatException if the compressed image is not in the ZIP
* format or cannot be uncompressed.
*/
private byte[] unzip(final byte[] bytes, final int imgWidth,
final int imgHeight) throws DataFormatException {
final byte[] data = new byte[imgWidth * imgHeight * 8];
int count = 0;
final Inflater inflater = new Inflater();
inflater.setInput(bytes);
count = inflater.inflate(data);
inflater.end();
final byte[] uncompressedData = new byte[count];
System.arraycopy(data, 0, uncompressedData, 0, count);
return uncompressedData;
}
}