/*
* 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.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import com.shavenpuppy.jglib.resources.ImageWrapper;
/**
* An Image.
*/
public class Image implements Serializable, ImageWrapper {
private static final long serialVersionUID = 8L;
private static final int MAGIC = 0x1234;
/** 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
*/
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
*/
void decompress(ByteBuffer src, ByteBuffer dest) throws Exception;
}
/** JPEG compressor */
private static JPEGCompressor compressor;
/** JPEG decompressor */
private static JPEGDecompressor decompressor;
/*
* 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;
/*
* Maps types to pixel sizes in bytes
*/
private static final int[] TYPE_TO_SIZE = new int[] {3,4,1,2,4,4,3,4};
/** The image data */
private transient WrappedBuffer wrappedData;
private transient ByteBuffer data;
/** The image dimensions */
private transient int width, height;
/** Image type */
private transient int type;
/** Use JPEG compressor on serialize/deserialize */
private transient boolean useJPEG;
/**
* Constructor for SpriteImage, used by serialization.
*/
public Image() {
super();
}
/**
* Dispose of the data buffer
*/
public void dispose() {
data = null;
wrappedData.dispose();
}
/**
* C'tor
* @param width
* @param height
* @param type
*/
public Image(int width, int height, int type) {
this.width = width;
this.height = height;
this.type = type;
wrappedData = DirectBufferAllocator.allocate(width * height * TYPE_TO_SIZE[type]);
data = wrappedData.getBuffer();
}
/**
* C'tor
* @param width
* @param height
* @param type
* @param img
*/
public Image(int width, int height, int type, byte[] img) {
this.width = width;
this.height = height;
this.type = type;
assert width * height * TYPE_TO_SIZE[type] == img.length : "Image is incorrect size.";
wrappedData = DirectBufferAllocator.allocate(img.length);
data = wrappedData.getBuffer();
data.put(img);
data.flip();
}
/**
* 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 * TYPE_TO_SIZE[type] == imageData.remaining() : "Image is incorrect size.";
assert imageData.isDirect() : "Image must be stored in a direct byte buffer.";
data = imageData.slice();
}
/**
* SpriteImage loader
*/
public static Image read(InputStream is) throws Exception {
Image ret = new Image();
ret.readExternal(new DataInputStream(is));
return ret;
}
/**
* SpriteImage writer
*/
public static void write(Image image, OutputStream os) throws Exception {
image.writeExternal(os);
}
public void writeExternal(OutputStream os) throws IOException {
DataOutputStream dos = new DataOutputStream(os);
doWrite(dos);
dos.flush();
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
DataOutputStream dos = new DataOutputStream(stream);
doWrite(dos);
dos.flush();
}
private void doWrite(DataOutputStream stream) throws IOException {
stream.writeInt(MAGIC);
stream.writeInt(width);
stream.writeInt(height);
stream.writeInt(type);
stream.writeInt(useJPEG ? 1 : 0);
data.rewind();
if (useJPEG && hasAlpha() && TYPE_TO_SIZE[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() && TYPE_TO_SIZE[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 {
byte[] buf = new byte[data.limit() - data.position()];
data.get(buf);
data.flip();
stream.writeInt(buf.length);
stream.write(buf);
}
}
public void readExternal(InputStream is) throws IOException {
DataInputStream stream = new DataInputStream(is);
int magic = stream.readInt();
if (magic != MAGIC) {
throw new IOException("Stream corrupt - expected magic number "+MAGIC+" but got "+magic);
}
width = stream.readInt();
if (width <= 0) {
throw new IOException("Illegal width: got "+width);
}
height = stream.readInt();
if (height <= 0) {
throw new IOException("Illegal height: got "+height);
}
type = stream.readInt();
if (type < 0 || type > 7) {
throw new IOException("Illegal type: got "+type);
}
useJPEG = stream.readInt() == 1;
int length = width * height * TYPE_TO_SIZE[type];
wrappedData = DirectBufferAllocator.allocate(length);
data = wrappedData.getBuffer();
if (useJPEG && hasAlpha() && TYPE_TO_SIZE[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() && TYPE_TO_SIZE[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 {
// Fast image read
int actualLength = stream.readInt();
if (actualLength != length) {
throw new IOException("Corrupt: expected length "+length+", but read "+actualLength+" from stream");
}
byte[] buf = new byte[length];
stream.readFully(buf);
data.put(buf);
data.flip();
}
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
DataInputStream dis = new DataInputStream(stream);
readExternal(dis);
}
// /**
// * 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;
}
}
/**
* 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;
}
/**
* 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("]");
return buffer.toString();
}
/**
* 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 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;
}
@Override
public Image getImage() {
return this;
}
}