package org.limewire.ui.swing.images;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.SwingUtilities;
import org.limewire.ui.swing.util.GraphicsUtilities;
import org.limewire.util.FileUtils;
/**
* Loads an image and creates a thumbnail of it on the ImageExceturService thread.
* If an error occurs while reading the file and error Image will be loaded in
* its place. If a component is passed in, it will be treated as a callback to
* refresh the component after the thumbnail has been created.
* <p>
* If a component is passed in expecting a callback, the component must still be
* visible when the thumbnail is set to be created. If the component is a list
* that is expecting a callback, the index for the thumbnail must still be
* visible prior to the thumbnail being loaded.
*/
public class ThumbnailCallable implements Callable<Void> {
private final Map<File,Icon> thumbnailMap;
private final Map<File,String> loadingMap;
private File file;
private final Icon errorIcon;
private final JComponent callback;
private int index = -1;
/**
* Reads the image file and create a thumbnail, storing the thumbnail
* in the thumbnail map. If an error occurs, store the error icon instead.
*
* @param thumbnailMap map to store the thumbnail in
* @param loadingMap map of files waiting to be loaded as thumbnails
* @param file image file to read and create a thumbnail from
* @param errorIcon icon to show if the file can't be read
*/
public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon) {
this(thumbnailMap, loadingMap, file, errorIcon, null);
}
/**
* Reads the image file and create a thumbnail, storing the thumbnail
* in the thumbnail map. If an error occurs, store the error icon instead.
* Call a repaint on the component once the thumbnail has been created. If
* the component is no longer showing don't bother repainting it.
* <p>
* If the callback is no longer visible when the load method is created, the
* thumbnail is not created.
*
* @param thumbnailMap map to store the thumbnail in
* @param loadingMap map of files waiting to be loaded as thumbnails
* @param file image file to read and create a thumbnail from
* @param errorIcon icon to show if the file can't be read
* @param callback component to repaint once the thumbnail has been created.
*/
public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon, JComponent callback) {
this.thumbnailMap = thumbnailMap;
this.loadingMap = loadingMap;
this.file = file;
this.errorIcon = errorIcon;
this.callback = callback;
}
/**
* Reads the image file and create a thumbnail, storing the thumbnail
* in the thumbnail map. If an error occurs, store the error icon instead.
* Call a repaint on the component once the thumbnail has been created. If
* the component is no longer showing don't bother repainting it.
* <p>
* If the callback is no longer visible when the load method is created, the
* thumbnail is not created. If the list is still visible but the index is
* no longer shown in the list, the image is not loaded.
*
* @param thumbnailMap map to store the thumbnail in
* @param loadingMap map of files waiting to be loaded as thumbnails
* @param file image file to read and create a thumbnail from
* @param errorIcon icon to show if the file can't be read
* @param callback component to repaint once the thumbnail has been created.
* @param index index within this list, this thumbnail is intended for
*/
public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon, JList list, int index) {
this.thumbnailMap = thumbnailMap;
this.loadingMap = loadingMap;
this.file = file;
this.errorIcon = errorIcon;
this.callback = list;
this.index = index;
}
@Override
public Void call() throws Exception {
// do some testing. If the component this thumbnail is intended for isn't showing anymore,
// don't waste the time loading the thumbnail
if(callback != null) {
if(!callback.isShowing()) {
// thumbnail wasn't loaded, remove it from the list of pending thumbnails
loadingMap.remove(file);
return null;
}
if((callback instanceof JList) && index != -1 && (((JList)callback).getFirstVisibleIndex() > index || ((JList)callback).getLastVisibleIndex() < index)){
// thumbnail wasn't loaded, remove it from the list of pending thumbnails
loadingMap.remove(file);
return null;
}
}
BufferedImage image = null;
try {
image = getSubSampleImage(file);
} catch (Throwable e) {
try {
image = ImageIO.read(file);
} catch(Throwable ee) {
handleUpdate(errorIcon);
return null;
}
}
if(image == null) {
handleUpdate(errorIcon);
return null;
}
// if the image is larger than our viewport, resize the image before saving
if(image.getWidth() > ThumbnailManager.WIDTH || image.getHeight() > ThumbnailManager.HEIGHT) {
// image manipulation can cause a whole host of errors, it should always be wrapped in a try/catch block
try {
image = GraphicsUtilities.createRatioPreservedThumbnail(image, ThumbnailManager.WIDTH, ThumbnailManager.HEIGHT);
} catch(Throwable t) {
try { // if there was an error, try creating a less detailed thumbnail
image = GraphicsUtilities.createRatioPreservedThumbnailFast(image, ThumbnailManager.WIDTH, ThumbnailManager.HEIGHT);
} catch (Throwable e) { // give up
image = null;
}
}
} else {
// if the image didn't need to be scaled, make sure it can be accelerated
image = GraphicsUtilities.toCompatibleImage(image);
}
if(image == null || image.getWidth() == 0 || image.getHeight() == 0) {
handleUpdate(errorIcon);
return null;
}
ImageIcon imageIcon = new ImageIcon(image);
handleUpdate(imageIcon);
return null;
}
/**
* Store the image in a hashmap. If a component callback was passed in,
* see if the component is still showing and if so call a repaint on it.
*/
private void handleUpdate(Icon icon) {
thumbnailMap.put(file, icon);
loadingMap.remove(file);
if(callback != null && callback.isShowing()) {
SwingUtilities.invokeLater(new Runnable(){
public void run() {
callback.repaint();
}
});
}
}
private static final float subSamplingFactor = ThumbnailManager.WIDTH * 4.20f;
/**
* Loads a file into a BufferedImage. If the image is longer than a subSamplingFactor,
* rows are sampled out to reduce the size of the BufferedImage. This can go a long way
* towards reducing OutOfMemoryExceptions when loading large compressed images.
*/
private BufferedImage getSubSampleImage(File file) throws FileNotFoundException, IOException {
BufferedImage image;
ImageInputStream bufferedInput = null;
try {
final ImageReader imgReader = ImageIO.getImageReadersBySuffix( FileUtils.getFileExtension(file) ).next();
bufferedInput = ImageIO.createImageInputStream( new BufferedInputStream( new FileInputStream( file ) ) );
imgReader.setInput(bufferedInput);
int imgHeight = imgReader.getHeight( 0 );
int imgWidth = imgReader.getWidth( 0 );
int longEdge = (Math.max(imgHeight, imgWidth));
int subSample = (int)(longEdge/subSamplingFactor);
final ImageReadParam readParam = imgReader.getDefaultReadParam();
if(subSample > 1) {
readParam.setSourceSubsampling(subSample, subSample, 0, 0);
}
image = imgReader.read(0, readParam);
} finally {
if(bufferedInput != null)
bufferedInput.close();
}
return image;
}
}