/*
* Copyright (c) 2003-onwards Shaven Puppy 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 'Shaven Puppy' 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.shavenpuppy.jglib;
import java.io.*;
import java.nio.ByteBuffer;
import com.shavenpuppy.jglib.resources.ImageWrapper;
/**
* An Image.
*/
public class Image implements Serializable, ImageWrapper {
static final long serialVersionUID = 7L;
/** JPEG Compression interface */
public interface JPEGCompressor {
/**
* Compress the specified RGB data and return a ByteBuffer with the compressed image in it.
* @param width
* @param height
* @param src
* @return dest
* @throws Exception
*/
public ByteBuffer compress(int width, int height, ByteBuffer src) throws Exception;
}
/** JPEG Decompression interface */
public interface JPEGDecompressor {
/**
* Decompress an incoming RGB JPEG image into the specified ByteBuffer.
* @param src
* @param dest
* @throws Exception
*/
public void decompress(ByteBuffer src, ByteBuffer dest) throws Exception;
}
/** JPEG compressor */
private static JPEGCompressor compressor;
/** JPEG decompressor */
private static JPEGDecompressor decompressor;
/** The image data */
private transient WrappedBuffer wrappedData;
private transient ByteBuffer data;
/** The image dimensions */
private int width, height;
/** Image type */
private int type;
/** Use JPEG compressor on serialize/deserialize */
private boolean useJPEG;
/** Use delta-planar compression */
private boolean deltaPlanar;
/** Palette, if any */
private Palette palette;
/*
* Supported image types.
*
* All RGB(A) types use 8-bits per color, packed.
* Paletted uses an 8-bit index palette.
* Luminance is 8-bit greyscale
* Luminance is 8-bit greyscale + 8 bit alpha
*/
public static final int RGB = 0;
public static final int RGBA = 1;
public static final int LUMINANCE = 2;
public static final int LUMINANCE_ALPHA = 3;
public static final int ARGB = 4;
public static final int ABGR = 5;
public static final int BGR = 6;
public static final int BGRA = 7;
public static final int PALETTED = 8;
/*
* Maps types to pixel sizes in bytes
*/
private static final int[] typeToSize = new int[] {3,4,1,2,4,4,3,4,1};
// /*
// * Magic number
// */
// private static final int MAGIC = 0xF00B;
// private static final int VERSION = 3;
//
// private transient int numDisposed, numRead;
/**
* Constructor for SpriteImage, used by serialization.
*/
public Image() {
super();
}
/**
* Dispose of the data buffer and palette.
*/
public void dispose() {
// System.out.println("Image "+this+" disposed "+(++numDisposed)+" times, read "+numRead+" times");
data = null;
palette = null;
wrappedData.dispose();
}
/**
* Constructor for SpriteImage.
*/
public Image(int width, int height, int type) {
this.width = width;
this.height = height;
this.type = type;
wrappedData = DirectBufferAllocator.allocate(width * height * typeToSize[type]);
data = wrappedData.getBuffer();
//data = ByteBuffer.allocateDirect(width * height * typeToSize[type]).order(ByteOrder.nativeOrder());
if (type == PALETTED) {
palette = new Palette(Palette.RGBA, 256);
}
}
/**
* Constructor for SpriteImage.
*/
public Image(int width, int height, int type, byte[] img) {
this.width = width;
this.height = height;
this.type = type;
assert width * height * typeToSize[type] == img.length : "Image is incorrect size.";
//data = ByteBuffer.allocateDirect(img.length).order(ByteOrder.nativeOrder());
wrappedData = DirectBufferAllocator.allocate(img.length);
data = wrappedData.getBuffer();
data.put(img);
data.flip();
if (type == PALETTED) {
palette = new Palette(Palette.RGBA, 256);
}
}
/**
* Constructor for SpriteImage. The incoming ByteBuffer is sliced() to get the image
* data (no reference to the incoming buffer is retained).
* @param width The image width
* @param height The image height
* @param typ The image type
* @param imageData A direct bytebuffer whose position() and limit() mark the image data
*/
public Image(int width, int height, int type, ByteBuffer imageData) {
this.width = width;
this.height = height;
this.type = type;
assert width * height * typeToSize[type] == imageData.remaining() : "Image is incorrect size.";
assert imageData.isDirect() : "Image must be stored in a direct byte buffer.";
data = imageData.slice();
if (type == PALETTED) {
palette = new Palette(Palette.RGBA, 256);
}
}
/**
* SpriteImage loader
*/
public static Image read(InputStream is) throws Exception {
return (Image) (new ObjectInputStream(is)).readObject();
// Image ret = new Image();
// ret.readExternal(new ObjectInputStream(is));
// return ret;
}
/**
* SpriteImage writer
*/
public static void write(Image image, OutputStream os) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(image);
oos.flush();
oos.reset();
// image.writeExternal(oos);
// oos.flush();
// oos.reset();
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
data.rewind();
if (useJPEG && hasAlpha() && typeToSize[type] == 4) {
// Use special JPEG compression. First extract a 3byte BGR image from our source
ByteBuffer bgr = extractBGR();
// Compress it
ByteBuffer compressed;
try {
compressed = compressor.compress(width, height, bgr);
} catch (Exception e) {
e.printStackTrace(System.err);
throw new IOException("Failed to compress: "+e.getMessage());
}
stream.writeInt(compressed.capacity());
System.out.println("Compressed image to "+compressed.capacity()+" bytes, down from "+(width * height * 4));
stream.write(compressed.array());
// Extract the alpha
compressed = createAlphaDelta();
stream.write(compressed.array());
} else if (useJPEG && !hasAlpha() && typeToSize[type] == 3) {
// Use normal JPEG compression.
ByteBuffer compressed;
try {
compressed = compressor.compress(width, height, data);
} catch (Exception e) {
e.printStackTrace(System.err);
throw new IOException("Failed to compress: "+e.getMessage());
}
stream.writeInt(compressed.capacity());
System.out.println("Compressed image to "+compressed.capacity()+" bytes, down from "+(width * height * 3));
stream.write(compressed.array());
} else if (deltaPlanar) {
// Use delta planar compression.
ByteBuffer split = splitIntoPlanes();
split.rewind();
stream.write(split.array());
} else {
byte[] buf = new byte[data.limit() - data.position()];
data.get(buf);
data.flip();
stream.write(buf);
}
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
int length = width * height * typeToSize[type];
wrappedData = DirectBufferAllocator.allocate(length);
data = wrappedData.getBuffer();
//data = ByteBuffer.allocateDirect(length).order(ByteOrder.nativeOrder());
if (useJPEG && hasAlpha() && typeToSize[type] == 4) {
// Use special JPEG decompression
int compressedSize = stream.readInt();
ByteBuffer compressed = ByteBuffer.allocate(compressedSize);
byte[] buf = new byte[compressedSize];
stream.readFully(buf);
compressed.put(buf);
compressed.flip();
ByteBuffer uncompressed = ByteBuffer.allocateDirect(width * height * 3);
try {
decompressor.decompress(compressed, uncompressed);
} catch (Exception e) {
e.printStackTrace(System.err);
throw new IOException("Failed to decompress: "+e.getMessage(), e);
}
insertBGR(uncompressed);
ByteBuffer alphaDelta = ByteBuffer.allocate(width * height);
buf = null;
buf = new byte[width * height];
stream.readFully(buf);
alphaDelta.put(buf).flip();
mergeAlphaDelta(alphaDelta);
} else if (useJPEG && !hasAlpha() && typeToSize[type] == 3) {
// Use normal JPEG decompression
int compressedSize = stream.readInt();
ByteBuffer compressed = ByteBuffer.allocate(compressedSize);
byte[] buf = new byte[compressedSize];
stream.readFully(buf);
compressed.put(buf);
compressed.flip();
data = ByteBuffer.allocateDirect(width * height * 3);
try {
decompressor.decompress(compressed, data);
} catch (Exception e) {
e.printStackTrace(System.err);
throw new IOException("Failed to decompress: "+e.getMessage(), e);
}
data.rewind();
} else if (deltaPlanar) {
// Use normal delta-planar decompression
ByteBuffer deltaPlanes = ByteBuffer.allocateDirect(length);
byte[] buf = new byte[length];
stream.readFully(buf);
deltaPlanes.put(buf);
mergePlanes(deltaPlanes);
} else {
// Fast image read
byte[] buf = new byte[length];
stream.readFully(buf);
data.put(buf);
data.flip();
}
}
// /**
// * Is this number a power of 2?
// */
// private static boolean isPowerOf2(int n) {
// // Scan for first set bit...
// int i = 0;
// int p = 1;
// for (; i < 32; i ++) {
// if ( (n & p) == p)
// break;
// else
// p <<= 1;
// }
// // Now make sure no other bits are set
// for (; i < 31; i ++) {
// p <<= 1;
// if ( (n & p) == p)
// return false;
// }
// return true;
// }
/**
* Extract a BGR image from a 4-byte image with alpha
* @return ByteBuffer
*/
private ByteBuffer extractBGR() {
ByteBuffer ret = ByteBuffer.allocate(width * height * 3);
int n = width * height * 4;
switch (type) {
case RGBA:
for (int i = 0; i < n; i += 4) {
ret.put(data.get(i + 2));
ret.put(data.get(i + 1));
ret.put(data.get(i + 0));
}
break;
case BGRA:
for (int i = 0; i < n; i += 4) {
ret.put(data.get(i + 0));
ret.put(data.get(i + 1));
ret.put(data.get(i + 2));
}
break;
case ARGB:
for (int i = 0; i < n; i += 4) {
ret.put(data.get(i + 3));
ret.put(data.get(i + 2));
ret.put(data.get(i + 1));
}
break;
case ABGR:
for (int i = 0; i < n; i += 4) {
ret.put(data.get(i + 1));
ret.put(data.get(i + 2));
ret.put(data.get(i + 3));
}
break;
default:
assert false;
}
return ret;
}
/**
* Insert a BGR image into a 4-byte image with alpha
* @param buf The BGR image source
*/
private void insertBGR(ByteBuffer buf) {
int n = width * height * 4;
switch (type) {
case RGBA:
for (int i = 0; i < n; i += 4) {
data.put(i + 2, buf.get());
data.put(i + 1, buf.get());
data.put(i + 0, buf.get());
}
break;
case BGRA:
for (int i = 0; i < n; i += 4) {
data.put(i + 0, buf.get());
data.put(i + 1, buf.get());
data.put(i + 2, buf.get());
}
break;
case ARGB:
for (int i = 0; i < n; i += 4) {
data.put(i + 3, buf.get());
data.put(i + 2, buf.get());
data.put(i + 1, buf.get());
}
break;
case ABGR:
for (int i = 0; i < n; i += 4) {
data.put(i + 1, buf.get());
data.put(i + 2, buf.get());
data.put(i + 3, buf.get());
}
break;
default:
assert false;
}
}
/**
* Splits the data up into separate planes
* @returns a new ByteBuffer with the data in it
*/
private ByteBuffer splitIntoPlanes() {
ByteBuffer buf = ByteBuffer.allocate(data.capacity());
switch (type) {
case LUMINANCE:
case PALETTED:
buf.put(data);
return buf;
case LUMINANCE_ALPHA:
buf.position(buf.capacity() / 2);
ByteBuffer alpha = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int ol = 0, oa = 0, nl = 0, na = 0;
for (int x = 0; x < width; x ++) {
nl = data.get();
na = data.get();
buf.put((byte) (nl - ol));
alpha.put((byte) (na - oa));
ol = nl;
oa = na;
}
}
break;
case RGB:
case BGR:
{
buf.position(buf.capacity() / 3);
ByteBuffer buf1 = buf.slice();
buf.position(2 * buf.capacity() / 3);
ByteBuffer buf2 = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int o0 = 0, o1 = 0, o2 = 0, n0 = 0, n1 = 0, n2 = 0;
for (int x = 0; x < width; x ++) {
n0 = data.get();
n1 = data.get();
n2 = data.get();
buf.put((byte) (n0 - o0));
buf1.put((byte) (n1 - o1));
buf2.put((byte) (n2 - o2));
o0 = n0;
o1 = n1;
o2 = n2;
}
}
}
break;
case RGBA:
case ABGR:
case ARGB:
case BGRA:
{
buf.position(buf.capacity() / 4);
ByteBuffer buf1 = buf.slice();
buf.position(2 * buf.capacity() / 4);
ByteBuffer buf2 = buf.slice();
buf.position(3 * buf.capacity() / 4);
ByteBuffer buf3 = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int o0 = 0, o1 = 0, o2 = 0, o3 = 0, n0 = 0, n1 = 0, n2 = 0, n3 = 0;
for (int x = 0; x < width; x ++) {
n0 = data.get();
n1 = data.get();
n2 = data.get();
n3 = data.get();
buf.put((byte) (n0 - o0));
buf1.put((byte) (n1 - o1));
buf2.put((byte) (n2 - o2));
buf3.put((byte) (n3 - o3));
o0 = n0;
o1 = n1;
o2 = n2;
o3 = n3;
}
}
}
break;
}
data.rewind();
return buf;
}
/**
* Return a delta-compressed alpha channel
* @return alpha
*/
private ByteBuffer createAlphaDelta() {
ByteBuffer buf = ByteBuffer.allocate(width * height);
int pos;
if (type == RGBA || type == BGRA) {
pos = 3;
} else {
pos = 0;
}
for (int y = 0; y < height; y ++) {
int o0 = 0, n0 = 0;
for (int x = 0; x < width; x ++) {
n0 = data.get(pos);
buf.put((byte) (n0 - o0));
o0 = n0;
pos += 4;
}
}
return buf;
}
/**
* Merges the data from separate planes
* @param buf The source data
*/
private void mergePlanes(ByteBuffer buf) {
buf.flip();
switch (type) {
case LUMINANCE:
case PALETTED:
data.put(buf);
return;
case LUMINANCE_ALPHA:
buf.position(buf.capacity() / 2);
ByteBuffer alpha = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int ol = 0, oa = 0;
for (int x = 0; x < width; x ++) {
ol += buf.get();
oa += alpha.get();
data.put((byte) ol);
data.put((byte) oa);
}
}
break;
case RGB:
case BGR:
{
buf.position(buf.capacity() / 3);
ByteBuffer buf0 = buf.slice();
buf.position(2 * buf.capacity() / 3);
ByteBuffer buf1 = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int o0 = 0, o1 = 0, o2 = 0;
for (int x = 0; x < width; x ++) {
o0 += buf.get();
o1 += buf0.get();
o2 += buf1.get();
data.put((byte) o0);
data.put((byte) o1);
data.put((byte) o2);
}
}
}
break;
case RGBA:
case ABGR:
case ARGB:
case BGRA:
{
buf.position(buf.capacity() / 4);
ByteBuffer buf0 = buf.slice();
buf.position(2 * buf.capacity() / 4);
ByteBuffer buf1 = buf.slice();
buf.position(3 * buf.capacity() / 4);
ByteBuffer buf2 = buf.slice();
buf.position(0);
for (int y = 0; y < height; y ++) {
int o0 = 0, o1 = 0, o2 = 0, o3 = 0;
for (int x = 0; x < width; x ++) {
o0 += buf.get();
o1 += buf0.get();
o2 += buf1.get();
o3 += buf2.get();
data.put((byte) o0);
data.put((byte) o1);
data.put((byte) o2);
data.put((byte) o3);
}
}
}
break;
}
data.flip();
}
/**
* Merge an incoming alpha-delta channel with the existing color data
* @param buf
*/
private void mergeAlphaDelta(ByteBuffer buf) {
int pos;
if (type == RGBA || type == BGRA) {
pos = 3;
} else {
pos = 0;
}
for (int y = 0; y < height; y ++) {
int o0 = 0;
for (int x = 0; x < width; x ++) {
o0 += buf.get();
data.put(pos, (byte) o0);
pos += 4;
}
}
}
/**
* Gets the data.
* @return Returns an ByteBuffer
*/
public ByteBuffer getData() {
// System.out.println("Image "+this+" read "+(++numRead)+" times, disposed "+numDisposed+" times");
return data;
}
/**
* Gets the height.
* @return Returns a int
*/
public int getHeight() {
return height;
}
/**
* Gets the type.
* @return Returns a int
*/
public int getType() {
return type;
}
/**
* Gets the width.
* @return Returns a int
*/
public int getWidth() {
return width;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append("Image [width=");
buffer.append(width);
buffer.append(", height=");
buffer.append(height);
buffer.append(", type=");
buffer.append(type);
buffer.append(", useJPEG=");
buffer.append(useJPEG);
buffer.append(", deltaPlanar=");
buffer.append(deltaPlanar);
buffer.append(", ");
if (palette != null) {
buffer.append("palette=");
buffer.append(palette);
}
buffer.append("]");
return buffer.toString();
}
/**
* Returns the palette.
* @return Palette
*/
public Palette getPalette() {
return palette;
}
/**
* Sets the palette.
* @param palette The palette to set
*/
public void setPalette(Palette palette) {
assert type == PALETTED : "Not a paletted image.";
assert palette != null : "Cannot set a null palette.";
this.palette = palette;
}
/**
* Sets the compression method. This is only used when serializing the image
* to an ObjectOutputStream; hence there is no method to query the compression
* method. Note that only RGBA/ARGB/BGRA/ABGR formats support JPEG compression.
* @param useJPEG true to use JPEG image compression
*/
public void setUseJPEG(boolean useJPEG) {
this.useJPEG = useJPEG;
}
/**
* Sets lossless compression method.
* @param deltaPlanar true to use delta-planar compression
*/
public void setDeltaPlanar(boolean deltaPlanar) {
this.deltaPlanar = deltaPlanar;
}
/**
* Sets the compressor that shall be used to compress image data when it is serialized
* and it is specified as COMPRESSION_JPEG.
* @param compressor The compressor to set.
*/
public static void setCompressor(JPEGCompressor compressor) {
Image.compressor = compressor;
}
/**
* Sets the decompressor that shall be used to decompress JPEG-compressed image data.
* @param decompressor The decompressor to set.
*/
public static void setDecompressor(JPEGDecompressor decompressor) {
Image.decompressor = decompressor;
}
/**
* Has this image got an alpha channel?
* @return boolean
*/
public boolean hasAlpha() {
return type == LUMINANCE_ALPHA || type == RGBA || type == ARGB || type == ABGR || type == BGRA;
}
/* (non-Javadoc)
* @see com.shavenpuppy.jglib.resources.ImageWrapper#getImage()
*/
@Override
public Image getImage() {
return this;
}
}