/*
* Copyright 2008 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package com.sun.dtv.lwuit;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Hashtable;
import org.thenesis.microbackend.ui.graphics.VirtualGraphics;
import org.thenesis.microbackend.ui.graphics.VirtualImage;
import com.sun.dtv.lwuit.geom.Dimension;
import com.sun.dtv.lwuit.util.Log;
/**
* Abstracts the underlying platform images allowing us to treat them as a
* uniform object.
*
* @author Chen Fishbein
*/
public class Image {
private VirtualImage image;
private int transform;
private boolean opaqueTested = false;
private boolean opaque;
private Graphics g;
private Hashtable scaleCache;
/** Creates a new instance of ImageImpl */
Image(VirtualImage image) {
this.image = image;
}
/** Creates a new instance of ImageImpl */
Image(int[] imageArray, int w, int h) {
this(new VirtualImage(imageArray, w, h, true));
}
/**
* Returns a cached scaled image
*
* @param size
* the size of the cached image
* @return cached image
*/
Image getCachedImage(Dimension size) {
if (scaleCache != null) {
WeakReference w = (WeakReference) scaleCache.get(size);
if (w != null) {
return (Image) w.get();
}
}
return null;
}
/**
* Returns a cached scaled image
*
* @param size
* the size of the cached image
* @return cached image
*/
void cacheImage(Dimension size, Image i) {
if (scaleCache == null) {
scaleCache = new Hashtable();
}
WeakReference w = new WeakReference(i);
scaleCache.put(size, w);
}
/**
* Extracts a subimage from the given image allowing us to breakdown a
* single large image into multiple smaller images in RAM, this actually
* creates a standalone version of the image for use.
*
* @param width
* the width of internal images
* @param height
* the height of internal images
* @return An array of all the possible images that can be created from the
* source
* @throws java.io.IOException
*/
public Image subImage(int x, int y, int width, int height, boolean processAlpha) {
// we use the getRGB API rather than the mutable image API to allow
// translucency to
// be maintained in the newly created image
int[] arr = new int[width * height];
image.getRGB(arr, 0, width, x, y, width, height);
Image i = new Image(new VirtualImage(arr, width, height, processAlpha));
i.opaque = opaque;
i.opaqueTested = opaqueTested;
return i;
}
/**
* Returns an instance of this image rotated by the given number of degrees.
* By default 90 degree angle divisions are supported, anything else is
* implementation dependent. This method assumes a square image. Notice that
* it is inefficient in the current implementation to rotate to non-square
* angles,
* <p>
* E.g. rotating an image to 45, 90 and 135 degrees is inefficient. Use
* rotatate to 45, 90 and then rotate the 45 to another 90 degrees to
* achieve the same effect with less memory.
*
* @param degrees
* A degree in right angle must be larer than 0 and up to 359
* degrees
* @return new image instance with the closest possible rotation
*/
public Image rotate(int degrees) {
if (Log.TRACE_ENABLED)
System.out.println("[DEBUG] Image.rotate(): not implemented yet");
return null;
// // a square angle so we can use the "fast" algorithm...
// int transform = 0;
// Image i;
// if(degrees % 90 == 0) {
// transform = fastRotate(degrees);
// i = new Image(image);
// } else {
// // rotate up to the point to a smaller than 90 degree angle then let
// the radian code do its magic...
// if(degrees > 90) {
// int deg = degrees - degrees % 90;
// transform = fastRotate(deg);
// degrees -= deg;
// }
// // can't use sprite rotation since it will lose transparency
// information...
// int width = image.getWidth();
// int height = image.getHeight();
// int[] arr = new int[width * height];
// int[] dest = new int[arr.length];
// image.getRGB(arr, 0, width, 0, 0, width, height);
// int centerX = width / 2;
// int centerY = height / 2;
//
// double radians = Math.toRadians(-degrees);
// double cosDeg = Math.cos(radians);
// double sinDeg = Math.sin(radians);
// for(int x = 0 ; x < width ; x++) {
// for(int y = 0 ; y < height ; y++) {
// int x2 = round(cosDeg * (x - centerX) - sinDeg * (y - centerY) +
// centerX);
// int y2 = round(sinDeg * (x - centerX) + cosDeg * (y - centerY) +
// centerY);
// if(!(x2 < 0 || y2 < 0 || x2 >= width || y2 >= height)) {
// int destOffset = x2 + y2 * width;
// if(destOffset >= 0 && destOffset < dest.length) {
// dest[x + y * width] = arr[destOffset];
// }
// }
// }
// }
// i = new Image(VirtualImage.createRGBImage(dest, width, height,
// true));
// }
//
// i.transform = transform;
// return i;
}
private int round(double d) {
double f = Math.floor(d);
double c = Math.ceil(d);
if (c - d < d - f) {
return (int) c;
}
return (int) f;
}
/**
* Creates a new image instance with the alpha channel of opaque/translucent
* pixels within the image using the new alpha value. Transparent (alpha ==
* 0) pixels remain transparent. All other pixels will have the new alpha
* value.
*
* @param alpha
* New value for the entire alpha channel
* @return Translucent/Opaque image based on the alpha value and the pixels
* of this image
*/
public Image modifyAlpha(byte alpha) {
int w = image.getWidth();
int h = image.getHeight();
int size = w * h;
int[] arr = new int[size];
image.getRGB(arr, 0, w, 0, 0, w, h);
int alphaInt = (((int) alpha) << 24) & 0xff000000;
for (int iter = 0; iter < size; iter++) {
if ((arr[iter] & 0xff000000) != 0) {
arr[iter] = (arr[iter] & 0xffffff) | alphaInt;
}
}
Image i = new Image(arr, w, h);
i.opaqueTested = true;
i.opaque = false;
return i;
}
/**
* Creates a new image instance with the alpha channel of opaque/translucent
* pixels within the image using the new alpha value. Transparent (alpha ==
* 0) pixels remain transparent. All other pixels will have the new alpha
* value.
*
* @param alpha
* New value for the entire alpha channel
* @param removeColor
* pixels matching this color are made transparent (alpha channel
* ignored)
* @return Translucent/Opaque image based on the alpha value and the pixels
* of this image
*/
public Image modifyAlpha(byte alpha, int removeColor) {
removeColor = removeColor & 0xffffff;
int w = image.getWidth();
int h = image.getHeight();
int size = w * h;
int[] arr = new int[size];
image.getRGB(arr, 0, w, 0, 0, w, h);
int alphaInt = (((int) alpha) << 24) & 0xff000000;
for (int iter = 0; iter < size; iter++) {
if ((arr[iter] & 0xff000000) != 0) {
arr[iter] = (arr[iter] & 0xffffff) | alphaInt;
if (removeColor == (0xffffff & arr[iter])) {
arr[iter] = 0;
}
}
}
Image i = new Image(arr, w, h);
i.opaqueTested = true;
i.opaque = false;
return i;
}
/**
* Returns a mirror instance of this image
*
* @return a mirror instance of this image
* @deprecated this method is no longer supported due to issues when mixing
* it with other methods such as rotate. Future versions of the
* API will provide an alternative
*/
public Image mirror() {
if (Log.TRACE_ENABLED)
System.out.println("[DEBUG] Image.mirror(): not implemented yet");
return null;
// Image i = new Image(image);
// if(transform != 0) {
// switch(transform) {
// case Sprite.TRANS_MIRROR:
// i.transform = 0;
// break;
// case Sprite.TRANS_MIRROR_ROT180:
// i.transform = Sprite.TRANS_ROT180;
// break;
// case Sprite.TRANS_MIRROR_ROT270:
// i.transform = Sprite.TRANS_ROT270;
// break;
// case Sprite.TRANS_MIRROR_ROT90:
// i.transform = Sprite.TRANS_ROT90;
// break;
// case Sprite.TRANS_NONE:
// i.transform = Sprite.TRANS_MIRROR;
// break;
// case Sprite.TRANS_ROT180:
// i.transform = Sprite.TRANS_MIRROR_ROT180;
// break;
// case Sprite.TRANS_ROT270:
// i.transform = Sprite.TRANS_MIRROR_ROT270;
// break;
// case Sprite.TRANS_ROT90:
// i.transform = Sprite.TRANS_MIRROR_ROT90;
// break;
// default:
// throw new
// IllegalStateException("Image could not be mirrored, error code: " +
// transform);
// }
// } else {
// i.transform = transform;
// }
// return i;
}
// /**
// * Rotates the given image array fast assuming this is a "right" angle
// * divides by 90 degrees
// */
// private int fastRotate(int degrees) {
// // if we have a preexisting rotation...
// switch(transform) {
// case javax.microedition.lcdui.game.Sprite.TRANS_ROT90:
// degrees += 90;
// break;
// case javax.microedition.lcdui.game.Sprite.TRANS_ROT180:
// degrees += 180;
// break;
// case javax.microedition.lcdui.game.Sprite.TRANS_ROT270:
// degrees += 270;
// break;
// }
//
// switch((degrees % 360) / 90) {
// case 0:
// return 0;
//
// // 90 degree angle
// case 1:
// return javax.microedition.lcdui.game.Sprite.TRANS_ROT90;
//
// // 180 degree angle
// case 2:
// return javax.microedition.lcdui.game.Sprite.TRANS_ROT180;
//
// // 270 degree angle
// case 3:
// return javax.microedition.lcdui.game.Sprite.TRANS_ROT270;
//
// default:
// throw new IllegalArgumentException("Fast rotate can't handle degrees = "
// + degrees);
// }
// }
/**
* Converts a newly loaded masked image into a translucent image. A masked
* image is an image file of even height that is two separate opaque images
* the top part is the actual image containing the opaque content. The
* bottom part is a mask that would be ovelayed as the alpha channel of the
* top part of the image. The red channel of the bottom part would just be
* assigned to the alpha portion of the top part thus allowing an image to
* contain translucency while being portable to different devices.
*
* @return Translucent image instance
* @deprecated translucency masks are no longer supported or necessary, use
* resources for portability and unification
*/
public Image extractTranslucentMask() {
if (Log.TRACE_ENABLED)
System.out.println("[DEBUG] Image.extractTranslucentMask(): not implemented yet");
return null;
// int w = image.getWidth();
// int h = image.getHeight();
// int[] arr = new int[w * h];
// image.getRGB(arr, 0, w, 0, 0, w, h);
// int offset = h * w / 2;
// for(int iter = 0 ; iter < offset ; iter++) {
// // extract the red component from the bottom portion of the image
// int alpha = arr[iter + offset] & 0xff0000;
//
// // shift the alpha 8 bits to the left
// // apply the alpha to the top portion of the image
// arr[iter] = (arr[iter] & 0xffffff) | (alpha << 8);
// }
// return new Image(VirtualImage.createRGBImage(arr, w, h / 2, true));
}
/**
* creates an image from the given path based on MIDP's createImage(path)
*
* @param path
* @throws java.io.IOException
* @return newly created image object
*/
public static Image createImage(String path) throws IOException {
try {
InputStream is = Image.class.getResourceAsStream(path);
return new Image(new VirtualImage(is));
} catch (OutOfMemoryError err) {
// Images have a major bug on many phones where they sometimes throw
// an OOM with no reason. A system.gc followed by the same call over
// solves the problem. This has something to do with the fact that
// there is no Image.dispose method in existance.
System.gc();
System.gc();
InputStream is = Image.class.getResourceAsStream(path);
return new Image(new VirtualImage(is));
}
}
/**
* creates an image from an InputStream
*
* @param stream
* a given InputStream
* @throws java.io.IOException
*/
public static Image createImage(InputStream stream) throws IOException {
try {
return new Image(new VirtualImage(stream));
} catch (OutOfMemoryError err) {
// Images have a major bug on many phones where they sometimes throw
// an OOM with no reason. A system.gc followed by the same call over
// solves the problem. This has something to do with the fact that
// there is no Image.dispose method in existance.
System.gc();
System.gc();
return new Image(new VirtualImage(stream));
}
}
/**
* creates an image from an RGB image
*
* @param rgb
* the RGB image array data
* @param width
* the image width
* @param height
* the image height
* @return an image from an RGB image
*/
public static Image createImage(int[] rgb, int width, int height) {
try {
Image i = new Image(new VirtualImage(rgb, width, height, true));
// its already set so might as well do the test
i.testOpaque(rgb);
return i;
} catch (OutOfMemoryError err) {
// Images have a major bug on many phones where they sometimes throw
// an OOM with no reason. A system.gc followed by the same call over
// solves the problem. This has something to do with the fact that
// there is no Image.dispose method in existance.
System.gc();
System.gc();
return new Image(new VirtualImage(rgb, width, height, true));
}
}
/**
* creates and buffered Image
*
* @param width
* the image width
* @param height
* the image height
* @return an image in a given width and height dimension
*/
public static Image createImage(int width, int height) {
try {
return new Image(new VirtualImage(width, height));
} catch (OutOfMemoryError err) {
// Images have a major bug on many phones where they sometimes throw
// an OOM with no reason. A system.gc followed by the same call over
// solves the problem. This has something to do with the fact that
// there is no Image.dispose method in existance.
System.gc();
System.gc();
return new Image(new VirtualImage(width, height));
}
}
/**
* creates an image from a given byte array data
*
* @param bytes
* the array of image data in a supported image format
* @param offset
* the offset of the start of the data in the array
* @param len
* the length of the data in the array
*/
public static Image createImage(byte[] bytes, int offset, int len) {
try {
try {
return new Image(new VirtualImage(bytes, offset, len));
} catch (OutOfMemoryError err) {
// Images have a major bug on many phones where they sometimes
// throw
// an OOM with no reason. A system.gc followed by the same call
// over
// solves the problem. This has something to do with the fact
// that
// there is no Image.dispose method in existance.
System.gc();
System.gc();
return new Image(new VirtualImage(bytes, offset, len));
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* If this is a mutable image a grpahics object allowing us to draw on it is
* returned.
*
* @return Graphics object allowing us to manipulate the content of a
* mutable image
*/
public Graphics getGraphics() {
if (g == null) {
g = new Graphics();
g.setGraphics(image.getGraphics());
}
return g;
}
/**
* Returns the width of the image
*
* @return the width of the image
*/
public int getWidth() {
return image.getWidth();
}
/**
* Returns the height of the image
*
* @return the height of the image
*/
public int getHeight() {
return image.getHeight();
}
void drawImage(Graphics g, int x, int y) {
g.drawImage(image, x, y, transform);
}
/**
* Obtains ARGB pixel data from the specified region of this image and
* stores it in the provided array of integers. Each pixel value is stored
* in 0xAARRGGBB format, where the high-order byte contains the alpha
* channel and the remaining bytes contain color components for red, green
* and blue, respectively. The alpha channel specifies the opacity of the
* pixel, where a value of 0x00 represents a pixel that is fully transparent
* and a value of 0xFF represents a fully opaque pixel. The rgb information
* contained within the image, this method ignors rotation and mirroring in
* some/most situations and cannot be used in such cases.
*
* @param rgbData
* an array of integers in which the ARGB pixel data is stored
* @param offset
* the index into the array where the first ARGB value is stored
* @param scanlength
* the relative offset in the array between corresponding pixels
* in consecutive rows of the region
* @param x
* the x-coordinate of the upper left corner of the region
* @param y
* the y-coordinate of the upper left corner of the region
* @param width
* the width of the region
* @param height
* the height of the region
*/
void getRGB(int[] rgbData, int offset, int scanlength, int x, int y, int width, int height) {
image.getRGB(rgbData, offset, scanlength, x, y, width, height);
}
/**
* Extracts data from this image into the given RGBImage
*
* @param image
* RGBImage that would receive pixel data
* @param destX
* x location within RGBImage into which the data will be written
* @param destY
* y location within RGBImage into which the data will be written
* @param x
* location within the source image
* @param y
* location within the source image
* @param width
* size of the image to extract from the source image
* @param height
* size of the image to extract from the source image
*/
public void toRGB(RGBImage image, int destX, int destY, int x, int y, int width, int height) {
getRGB(image.getRGB(), destX * destY, width, x, y, width, height);
}
/**
* Returns the content of this image as a newly created ARGB array.
*
* @return new array instance containing the ARGB data within this image
*/
public int[] getRGB() {
int width = getWidth();
int height = getHeight();
int[] rgbData = new int[width * height];
getRGB(rgbData, 0, width, 0, 0, width, height);
return rgbData;
}
/**
* Scales the image to the given width while updating the height based on
* the aspect ratio of the width
*
* @param width
* the given new image width
*/
public Image scaledWidth(int width) {
float ratio = ((float) width) / ((float) getWidth());
return scaled(width, (int) (getHeight() * ratio));
}
/**
* Scales the image to the given height while updating the width based on
* the aspect ratio of the height
*
* @param height
* the given new image height
*/
public Image scaledHeight(int height) {
float ratio = ((float) height) / ((float) getHeight());
return scaled((int) (getWidth() * ratio), height);
}
/**
* Scales the image while mainting the aspect ratio to the smaller size
* image
*
* @param width
* the given new image width
* @param height
* the given new image height
*/
public Image scaledSmallerRatio(int width, int height) {
float hRatio = ((float) height) / ((float) getHeight());
float wRatio = ((float) width) / ((float) getWidth());
if (hRatio < wRatio) {
return scaled(width, (int) (getHeight() * hRatio));
} else {
return scaled((int) (getWidth() * wRatio), height);
}
}
/**
* Returns a scaled version of this image image using the given width and
* height, this is a fast algorithm that preserves transulcent information
*
* @param width
* width for the scaling
* @param height
* height of the scaled image
* @return new image instance scaled to the given height and width
*/
public Image scaled(int width, int height) {
if (width == image.getWidth() && height == image.getHeight()) {
return this;
}
Dimension d = new Dimension(width, height);
Image i = getCachedImage(d);
if (i != null) {
return i;
}
i = new Image(this.image);
i.scale(width, height);
i.transform = this.transform;
cacheImage(d, i);
return i;
}
/**
* Scale the image to the given width and height, this is a fast algorithm
* that preserves transulcent information
*
* @param width
* width for the scaling
* @param height
* height of the scaled image
*
* @deprecated scale should return an image rather than modify the image in
* place use scaled(int, int) instead
*/
public void scale(int width, int height) {
int srcWidth = image.getWidth();
int srcHeight = image.getHeight();
// no need to scale
if (srcWidth == width && srcHeight == height) {
return;
}
int[] currentArray = new int[srcWidth];
int[] destinationArray = new int[width * height];
opaque = scaleArray(srcWidth, srcHeight, height, width, currentArray, destinationArray);
// if an image is opaque use a mutable image since on the device it
// takes
// far less memory
// if(opaque) {
image = new VirtualImage(width, height);
VirtualGraphics g = image.getGraphics();
g.drawRGB(destinationArray, 0, width, 0, 0, width, height, false);
/*
* } else { image = VirtualImage.createRGBImage(destinationArray, width,
* height, true); }
*/
}// resize image
VirtualImage getImage() {
return image;
}
boolean scaleArray(int srcWidth, int srcHeight, int height, int width, int[] currentArray, int[] destinationArray) {
// Horizontal Resize
int yRatio = (srcHeight << 16) / height;
int xRatio = (srcWidth << 16) / width;
int xPos = xRatio / 2;
int yPos = yRatio / 2;
// if there is more than 16bit color there is no point in using mutable
// images since they won't save any memory
boolean opaque = Display.getInstance().numColors() <= 65536;
for (int y = 0; y < height; y++) {
int srcY = yPos >> 16;
getRGB(currentArray, 0, srcWidth, 0, srcY, srcWidth, 1);
for (int x = 0; x < width; x++) {
int srcX = xPos >> 16;
int destPixel = x + y * width;
if ((destPixel >= 0 && destPixel < destinationArray.length) && (srcX < currentArray.length)) {
destinationArray[destPixel] = currentArray[srcX];
// if all the pixels have an opaque alpha channel then the
// image is opaque
opaque = opaque && (currentArray[srcX] & 0xff000000) == 0xff000000;
}
xPos += xRatio;
}
yPos += yRatio;
xPos = xRatio / 2;
}
this.opaque = opaque;
return opaque;
}
/**
* Returns true if this is an animated image
*/
public boolean isAnimation() {
return false;
}
private void testOpaque(int[] rgb) {
if (!opaqueTested) {
opaque = true;
for (int iter = 0; iter < rgb.length; iter++) {
if ((rgb[iter] & 0xff000000) != 0xff000000) {
opaque = false;
break;
}
}
opaqueTested = true;
}
}
/**
* Indicates whether this image is opaque or not
*
* @return true if the image is completely opqaque which allows for some
* heavy optimizations
*/
public boolean isOpaque() {
if (!opaqueTested) {
testOpaque(getRGB());
}
return opaque;
}
}