com.codename1.ui.EncodedImage Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.ui;
import com.codename1.impl.CodenameOneImplementation;
import com.codename1.io.Log;
import com.codename1.io.Util;
import com.codename1.ui.util.ImageIO;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* {@code EncodedImage} is the workhorse of Codename One. Images returned from resource files are
* {@code EncodedImage} and many API's expect it.
*
* {@code EncodedImage} is effectively a an image that is "hidden" and extracted as needed to remove the
* memory overhead associated with loaded image. When creating an {@code EncodedImage} only the PNG
* (or JPEG etc.) is loaded to an array in RAM. Normally such images are very small (relatively) so they can be
* kept in memory without much overhead.
*
* When image information is needed (pixels) the image is decoded into RAM and kept in a weak/sort
* reference (see {@link com.codename1.ui.Display#createSoftWeakRef(java.lang.Object)}). This allows the
* image to be cached for performance and allows the garbage collector to reclaim it when the memory becomes
* scarce.
*
* Since the fully decoded image can be pretty big ({@code width X height X 4}) the ability to store just the
* encoded image can be pretty stark. E.g. A standard 50x100 image will take up 20,000 bytes of RAM for a
* standard image but an {@code EncodedImage} can reduce that to 1kb-2kb of RAM.
*
* When drawing an {@code EncodedImage} it checks the weak reference cache and if the image is cached then
* it is shown otherwise the image is loaded the encoded image cache it then drawn.
*
* {@code EncodedImage} is not final and can be derived to produce complex image fetching strategies
* e.g. the {@link com.codename1.ui.URLImage} class that can dynamically download its content from the web.
*
* {@code EncodedImage} can be instantiated via the create methods in the class. Pretty much any image
* can be converted into an `EncodedImage` via the {@link #createFromImage(com.codename1.ui.Image, boolean)}
* method.
*
* EncodedImage Locking
* Naturally loading the image is more expensive so we want the images that are on the current form to remain in
* cache (otherwise GC will thrash a lot). That's where {@link #lock()} kicks in, when {@link #lock()} is active we
* keep a hard reference to the actual native image so it won't get GC'd. This significantly improves performance!
*
* Internally this is invoked automatically for background images, icons etc. which results in a huge performance
* boost. This makes sense since these images are currently showing and they will be in RAM anyway. However,
* if you use a complex renderer or custom drawing UI you should {@link #lock()} your images where possible!
*
* To verify that locking might be a problem you can launch the performance monitor tool (accessible from
* the simulator menu), if you get log messages that indicate that an unlocked image was drawn you might
* have a problem.
*
* @author Shai Almog
*/
public class EncodedImage extends Image {
private byte[][] imageData;
private int[] dpis;
private int lastTestedDPI = -1;
private int width = -1;
private int height = -1;
private boolean opaqueChecked = false;
private boolean opaque = false;
private Object cache;
private Image hardCache;
private int locked;
private EncodedImage(byte[][] imageData) {
super(null);
this.imageData = imageData;
}
/**
* Allows subclasses to create more advanced variations of this class that
* lazily store the data in an arbitrary location.
*
* @param width -1 if unknown ideally the width/height should be known in advance
* @param height -1 if unknown ideally the width/height should be known in advance
*/
protected EncodedImage(int width, int height) {
super(null);
this.width = width;
this.height = height;
}
/**
* A subclass might choose to load asynchroniously and reset the cache when the image is ready.
*/
protected void resetCache() {
cache = null;
hardCache = null;
}
/**
* Creates an encoded image that acts as a multi-image, DO NOT USE THIS METHOD. Its for internal
* use to improve the user experience of the simulator
*
* @param dpis device DPI's
* @param data the data matching each multi-image DPI
* @return an encoded image that acts as a multi-image in runtime
* @deprecated this method is meant for internal use only, it would be very expensive to use
* this method for real applications. Its here for simulators and development purposes where
* screen DPI/resolution can vary significantly in runtime (something that just doesn't happen on devices).
*/
public static EncodedImage createMulti(int[] dpis, byte[][] data) {
EncodedImage e = new EncodedImage(data);
e.dpis = dpis;
return e;
}
/**
* Converts an image to encoded image
* @param i image
* @param jpeg true to try and set jpeg, will do a best effort but this isn't guaranteed
* @return an encoded image or null
*/
public static EncodedImage createFromImage(Image i, boolean jpeg) {
if(i instanceof EncodedImage) {
return ((EncodedImage)i);
}
ImageIO io = ImageIO.getImageIO();
if(io != null) {
String format;
if(jpeg) {
if(!io.isFormatSupported(ImageIO.FORMAT_JPEG)) {
format = ImageIO.FORMAT_PNG;
} else {
format = ImageIO.FORMAT_JPEG;
}
} else {
if(!io.isFormatSupported(ImageIO.FORMAT_PNG)) {
format = ImageIO.FORMAT_JPEG;
} else {
format = ImageIO.FORMAT_PNG;
}
}
try {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
io.save(i, bo, format, 0.9f);
EncodedImage enc = EncodedImage.create(bo.toByteArray());
Util.cleanup(bo);
enc.width = i.getWidth();
enc.height = i.getHeight();
if(format == ImageIO.FORMAT_JPEG) {
enc.opaque = true;
enc.opaqueChecked = true;
}
enc.cache = Display.getInstance().createSoftWeakRef(i);
return enc;
} catch(IOException err) {
Log.e(err);
}
}
return null;
}
/**
* Tries to create an encoded image from RGB which is more efficient,
* however if this fails it falls back to regular RGB image. This method
* is slower than creating an RGB image (not to be confused with the RGBImage class which is
* something ENTIRELY different!).
*
* @param argb an argb array
* @param width the width for the image
* @param height the height for the image
* @param jpeg uses jpeg format internally which is opaque and could be faster/smaller
* @return an image which we hope is an encoded image
*/
public static Image createFromRGB(int[] argb, int width, int height, boolean jpeg) {
Image i = Image.createImage(argb, width, height);
ImageIO io = ImageIO.getImageIO();
if(io != null) {
String format;
if(jpeg) {
if(!io.isFormatSupported(ImageIO.FORMAT_JPEG)) {
return i;
}
format = ImageIO.FORMAT_JPEG;
} else {
if(!io.isFormatSupported(ImageIO.FORMAT_PNG)) {
return i;
}
format = ImageIO.FORMAT_PNG;
}
try {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
io.save(i, bo, format, 0.9f);
EncodedImage enc = EncodedImage.create(bo.toByteArray());
Util.cleanup(bo);
enc.width = width;
enc.height = height;
if(jpeg) {
enc.opaque = true;
enc.opaqueChecked = true;
}
enc.cache = Display.getInstance().createSoftWeakRef(i);
return enc;
} catch(IOException err) {
Log.e(err);
}
}
return i;
}
/**
* Returns the byte array data backing the image allowing the image to be stored
* and discarded completely from RAM.
*
* @return byte array used to create the image, e.g. encoded PNG, JPEG etc.
*/
public byte[] getImageData() {
if(imageData.length == 1) {
return imageData[0];
}
int dpi = Display.getInstance().getDeviceDensity();
int bestFitOffset = 0;
int bestFitDPI = 0;
int dlen = dpis.length;
for(int iter = 0 ; iter < dlen ; iter++) {
int currentDPI = dpis[iter];
if(dpi == currentDPI) {
bestFitOffset = iter;
break;
}
if(bestFitDPI != dpi && dpi >= currentDPI && currentDPI >= bestFitDPI) {
bestFitDPI = currentDPI;
bestFitOffset = iter;
}
}
lastTestedDPI = dpi;
return imageData[bestFitOffset];
}
/**
* Creates an image from the given byte array
*
* @param data the data of the image
* @return newly created encoded image
*/
public static EncodedImage create(byte[] data) {
if(data == null) {
throw new NullPointerException();
}
return new EncodedImage(new byte[][] {data});
}
/**
* Creates an image from the given byte array with the variables set appropriately.
* This saves LWUIT allot of resources since it doesn't need to actually traverse the
* pixels of an image to find out details about it.
*
* @param data the data of the image
* @param width the width of the image
* @param height the height of the image
* @param opacity true for an opaque image
* @return newly created encoded image
*/
public static EncodedImage create(byte[] data, int width, int height, boolean opacity) {
if(data == null) {
throw new NullPointerException();
}
EncodedImage e = new EncodedImage(new byte[][] {data});
e.width = width;
e.height = height;
e.opaque = opacity;
e.opaqueChecked = true;
return e;
}
/**
* {@inheritDoc}
*/
public Object getImage() {
return getInternalImpl().getImage();
}
/**
* Creates an image from the input stream
*
* @param i the input stream
* @return newly created encoded image
* @throws java.io.IOException if thrown by the input stream
*/
public static EncodedImage create(InputStream i) throws IOException {
byte[] buffer = Util.readInputStream(i);
if(buffer.length > 200000) {
System.out.println("Warning: loading large images using EncodedImage.create(InputStream) might lead to memory issues, try using EncodedImage.create(InputStream, int)");
}
return new EncodedImage(new byte[][] {buffer});
}
/**
* Creates an image from the input stream, this version of the method is somewhat faster
* than the version that doesn't accept size
*
* @param i the input stream
* @param size the size of the stream
* @return newly created encoded image
* @throws java.io.IOException if thrown by the input stream
*/
public static EncodedImage create(InputStream i, int size) throws IOException {
byte[] buffer = new byte[size];
Util.readFully(i, buffer);
return new EncodedImage(new byte[][] {buffer});
}
private Image getInternalImpl() {
if(imageData != null && imageData.length > 1 && lastTestedDPI != Display.getInstance().getDeviceDensity()) {
hardCache = null;
cache = null;
width = -1;
height = -1;
}
if(hardCache != null) {
return hardCache;
}
Image i = getInternal();
if(locked > 0) {
hardCache = i;
}
return i;
}
/**
* Returns the actual image represented by the encoded image, this image will
* be cached in a weak/soft reference internally. This method is useful to detect
* when the system actually created an image instance. You shouldn't invoke this
* method manually!
*
* @return drawable image instance
*/
protected Image getInternal() {
if(cache != null) {
Image i = (Image)Display.getInstance().extractHardRef(cache);
if(i != null) {
return i;
}
}
Image i;
try {
byte[] b = getImageData();
i = Image.createImage(b, 0, b.length);
if(opaqueChecked) {
i.setOpaque(opaque);
}
CodenameOneImplementation impl = Display.impl;
impl.setImageName(i.getImage(), getImageName());
} catch(Exception err) {
Log.e(err);
i = Image.createImage(5, 5);
}
cache = Display.getInstance().createSoftWeakRef(i);
return i;
}
/**
* {@inheritDoc}
*/
public boolean isLocked() {
return locked > 0;
}
/**
* {@inheritDoc}
*/
public void asyncLock(final Image internal) {
if(locked <= 0) {
locked = 1;
if(cache != null) {
hardCache = (Image)Display.getInstance().extractHardRef(cache);
if(hardCache != null) {
return;
}
}
hardCache = internal;
Display.getInstance().scheduleBackgroundTask(new Runnable() {
public void run() {
try {
byte[] b = getImageData();
final Image i = Image.createImage(b, 0, b.length);
if(opaqueChecked) {
i.setOpaque(opaque);
}
CodenameOneImplementation impl = Display.impl;
impl.setImageName(i.getImage(), getImageName());
Display.getInstance().callSerially(new Runnable() {
public void run() {
if(locked > 0) {
hardCache = i;
}
cache = Display.getInstance().createSoftWeakRef(i);
Display.getInstance().getCurrent().repaint();
width = i.getWidth();
height = i.getHeight();
}
});
} catch(Exception err) {
Log.e(err);
}
}
});
}
}
/**
* {@inheritDoc}
*/
public void lock() {
if(locked < 1) {
locked = 1;
if(cache != null) {
hardCache = (Image)Display.getInstance().extractHardRef(cache);
}
} else {
locked ++;
}
}
/**
* {@inheritDoc}
*/
public void unlock() {
locked--;
if(locked < 1) {
if(hardCache != null) {
if(cache == null || Display.getInstance().extractHardRef(cache) == null) {
cache = Display.getInstance().createSoftWeakRef(hardCache);
}
hardCache = null;
}
locked = 0;
}
}
/**
* Creates an image from the input stream
*
* @param i the resource
* @return newly created encoded image
* @throws java.io.IOException if thrown by the input stream
*/
public static EncodedImage create(String i) throws IOException {
return create(Display.getInstance().getResourceAsStream(EncodedImage.class, i));
}
/**
* {@inheritDoc}
*/
public Image subImage(int x, int y, int width, int height, boolean processAlpha) {
return getInternalImpl().subImage(x, y, width, height, processAlpha);
}
/**
* {@inheritDoc}
*/
public Image rotate(int degrees) {
return getInternalImpl().rotate(degrees);
}
/**
* {@inheritDoc}
*/
public Image modifyAlpha(byte alpha) {
return getInternalImpl().modifyAlpha(alpha);
}
/**
* {@inheritDoc}
*/
public Image modifyAlpha(byte alpha, int removeColor) {
return getInternalImpl().modifyAlpha(alpha, removeColor);
}
/**
* {@inheritDoc}
*/
public Graphics getGraphics() {
return null;
}
/**
* {@inheritDoc}
*/
public int getWidth() {
if(width > -1) {
return width;
}
width = getInternalImpl().getWidth();
return width;
}
/**
* {@inheritDoc}
*/
public int getHeight() {
if(height > -1) {
return height;
}
height = getInternalImpl().getHeight();
return height;
}
/**
* {@inheritDoc}
*/
protected void drawImage(Graphics g, Object nativeGraphics, int x, int y) {
Display.impl.drawingEncodedImage(this);
Image internal = getInternalImpl();
if(width > -1 && height > -1 && (internal.getWidth() != width || internal.getHeight() != height)) {
internal.drawImage(g, nativeGraphics, x, y, width, height);
} else {
internal.drawImage(g, nativeGraphics, x, y);
}
}
/**
* {@inheritDoc}
*/
protected void drawImage(Graphics g, Object nativeGraphics, int x, int y, int w, int h) {
Display.impl.drawingEncodedImage(this);
getInternalImpl().drawImage(g, nativeGraphics, x, y, w, h);
}
/**
* {@inheritDoc}
*/
void getRGB(int[] rgbData,
int offset,
int x,
int y,
int width,
int height) {
getInternalImpl().getRGB(rgbData, offset, x, y, width, height);
}
/**
* {@inheritDoc}
*/
public void toRGB(RGBImage image,
int destX,
int destY,
int x,
int y,
int width,
int height) {
getInternalImpl().toRGB(image, destX, destY, x, y, width, height);
}
/**
* {@inheritDoc}
*/
public Image scaledWidth(int width) {
return getInternalImpl().scaledWidth(width);
}
/**
* {@inheritDoc}
*/
public Image scaledHeight(int height) {
return getInternalImpl().scaledHeight(height);
}
/**
* {@inheritDoc}
*/
public Image scaledSmallerRatio(int width, int height) {
return getInternalImpl().scaledSmallerRatio(width, height);
}
/**
* Performs scaling using ImageIO to generate an encoded Image
* @param width the width of the image, -1 to scale based on height and preserve aspect ratio
* @param height the height of the image, -1 to scale based on width and preserve aspect ratio
* @return new encoded image
*/
public EncodedImage scaledEncoded(int width, int height) {
if(width == getWidth() && height == getHeight()) {
return this;
}
if(width < 0) {
float ratio = ((float)height) / ((float)getHeight());
width = Math.max(1, (int)(getWidth() * ratio));
} else {
if(height < 0) {
float ratio = ((float)width) / ((float)getWidth());
height = Math.max(1, (int)(getHeight() * ratio));
}
}
try {
ImageIO io = ImageIO.getImageIO();
if(io != null) {
String format = ImageIO.FORMAT_PNG;
if(isOpaque() || !io.isFormatSupported(ImageIO.FORMAT_PNG)) {
if(io.isFormatSupported(ImageIO.FORMAT_JPEG)) {
format = ImageIO.FORMAT_JPEG;
}
}
if(io.isFormatSupported(format)) {
// do an image IO scale which is more efficient
ByteArrayOutputStream bo = new ByteArrayOutputStream();
io.save(new ByteArrayInputStream(getImageData()), bo, format, width, height, 0.9f);
EncodedImage img = EncodedImage.create(bo.toByteArray());
Util.cleanup(bo);
img.opaque = opaque;
img.opaqueChecked = opaqueChecked;
if(width > -1 && height > -1) {
img.width = width;
img.height = height;
}
return img;
}
}
} catch(IOException err) {
// normally this shouldn't happen but this will keep falling back to the existing scaled code
Log.e(err);
}
return null;
}
/**
* {@inheritDoc}
*/
public Image scaled(int width, int height) {
// J2ME/RIM don't support image IO and Windows Phone doesn't support PNG which prevents
// scaling translucent images properly
if(Display.getInstance().getProperty("encodedImageScaling", "true").equals("true") &&
ImageIO.getImageIO() != null && ImageIO.getImageIO().isFormatSupported(ImageIO.FORMAT_PNG)) {
return scaledEncoded(width, height);
}
return getInternalImpl().scaled(width, height);
}
/**
* {@inheritDoc}
*/
public void scale(int width, int height) {
getInternalImpl().scale(width, height);
}
/**
* {@inheritDoc}
*/
public boolean isAnimation() {
return false;
}
/**
* {@inheritDoc}
*/
public boolean isOpaque() {
if(opaqueChecked) {
return opaque;
}
opaque = getInternalImpl().isOpaque();
return opaque;
}
}