check for interupts/cancelation in ImageUtils methods/tasks; set cancelation text on progress bars.

This commit is contained in:
millmanorama 2017-06-08 17:05:36 +02:00
parent 0d8855d8b4
commit 4938ad4487
2 changed files with 98 additions and 52 deletions

View File

@ -32,10 +32,10 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.SortOrder; import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker; import javax.swing.SwingWorker;
import javax.swing.Timer; import javax.swing.Timer;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -213,13 +213,11 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
} }
/** /**
* Node that wraps around original node and adds the bitmap icon * Node that wraps around original node and adds the bitmap icon
* representing the picture * representing the picture
*/ */
private class ThumbnailViewNode extends FilterNode { class ThumbnailViewNode extends FilterNode {
private Logger logger = Logger.getLogger(ThumbnailViewNode.class.getName()); private Logger logger = Logger.getLogger(ThumbnailViewNode.class.getName());
@ -282,19 +280,28 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
private final Content content; private final Content content;
private final ProgressHandle progressHandle; private final ProgressHandle progressHandle;
private volatile boolean started = false;
private final String progressText;
ThumbnailLoadTask(Content content) { ThumbnailLoadTask(Content content) {
this.content = content; this.content = content;
final String progressText = Bundle.ThumbnailViewNode_progressHandle_text(content.getName()); progressText = Bundle.ThumbnailViewNode_progressHandle_text(content.getName());
progressHandle = ProgressHandle.createHandle(progressText); progressHandle = ProgressHandle.createHandle(progressText);
} }
@Override @Override
protected Image doInBackground() throws Exception { protected Image doInBackground() throws Exception {
progressHandle.start(); synchronized (progressHandle) {
progressHandle.start();
started = true;
}
return ImageUtils.getThumbnail(content, iconSize); return ImageUtils.getThumbnail(content, iconSize);
} }
private void cancel() {
SwingUtilities.invokeLater(() -> progressHandle.setDisplayName(progressText + " (Cancelling)"));
}
@Override @Override
protected void done() { protected void done() {
super.done(); super.done();
@ -302,11 +309,18 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
thumbCache = new SoftReference<>(super.get()); thumbCache = new SoftReference<>(super.get());
fireIconChange(); fireIconChange();
} catch (CancellationException ex) { } catch (CancellationException ex) {
//do nothing, it was cancelled //Task was cancelled, do nothing
} catch (InterruptedException | ExecutionException ex) { } catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, "Error getting thumbnail icon for " + content.getName(), ex); //NON-NLS if (ex.getCause() instanceof CancellationException) {
} else {
logger.log(Level.SEVERE, "Error getting thumbnail icon for " + content.getName(), ex); //NON-NLS
}
} finally { } finally {
progressHandle.finish(); synchronized (progressHandle) {
if (started) {
progressHandle.finish();
}
}
if (timer != null) { if (timer != null) {
timer.stop(); timer.stop();
timer = null; timer = null;
@ -314,24 +328,30 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
thumbTask = null; thumbTask = null;
} }
} }
} }
} }
private final ExecutorService executor = Executors.newFixedThreadPool(4, private final ExecutorService executor = Executors.newFixedThreadPool(4,
new ThreadFactoryBuilder().setNameFormat("Thumbnail-Loader-%d").build()); new ThreadFactoryBuilder().setNameFormat("Thumbnail-Loader-%d").build());
private final List<Future<?>> futures = new ArrayList<>(); private final List<ThumbnailViewNode.ThumbnailLoadTask> tasks = new ArrayList<>();
synchronized void cancelLoadingThumbnails() { synchronized void cancelLoadingThumbnails() {
futures.forEach(future -> future.cancel(true)); tasks.forEach(ThumbnailViewNode.ThumbnailLoadTask::cancel);
futures.clear(); tasks.clear();
executor.shutdownNow();
} }
private synchronized ThumbnailViewNode.ThumbnailLoadTask loadThumbnail(ThumbnailViewNode node, Content content) { private synchronized ThumbnailViewNode.ThumbnailLoadTask loadThumbnail(ThumbnailViewNode node, Content content) {
ThumbnailViewNode.ThumbnailLoadTask task = node.new ThumbnailLoadTask(content); if (executor.isShutdown() == false) {
futures.add(task); ThumbnailViewNode.ThumbnailLoadTask task = node.new ThumbnailLoadTask(content);
executor.submit(task); tasks.add(task);
return task; executor.submit(task);
return task;
} else {
return null;
}
} }
/** /**

View File

@ -40,6 +40,7 @@ import java.util.List;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -164,8 +165,8 @@ public class ImageUtils {
/** /**
* Thread/Executor that saves generated thumbnails to disk in the background * Thread/Executor that saves generated thumbnails to disk in the background
*/ */
private static final Executor imageSaver private static final Executor imageSaver =
= Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder()
.namingPattern("thumbnail-saver-%d").build()); //NON-NLS .namingPattern("thumbnail-saver-%d").build()); //NON-NLS
public static List<String> getSupportedImageExtensions() { public static List<String> getSupportedImageExtensions() {
@ -213,7 +214,7 @@ public class ImageUtils {
* @param file the AbstractFile to test * @param file the AbstractFile to test
* *
* @return true if the file is an image we can read and generate thumbnail * @return true if the file is an image we can read and generate thumbnail
* for. * for.
*/ */
public static boolean isImageThumbnailSupported(AbstractFile file) { public static boolean isImageThumbnailSupported(AbstractFile file) {
return isMediaThumbnailSupported(file, "image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) || hasImageFileHeader(file);//NON-NLS return isMediaThumbnailSupported(file, "image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) || hasImageFileHeader(file);//NON-NLS
@ -239,16 +240,17 @@ public class ImageUtils {
* VideoUtils both implement/extend some base interface/abstract class. That * VideoUtils both implement/extend some base interface/abstract class. That
* would be the natural place to put this. * would be the natural place to put this.
* *
* @param file the AbstractFile to test * @param file the AbstractFile to test
* @param mimeTypePrefix a MIME 'top-level type name' such as "image/", * @param mimeTypePrefix a MIME 'top-level type name' such as "image/",
* including the "/". In addition to the list of supported MIME types, any * including the "/". In addition to the list of
* type that starts with this prefix will be regarded as supported * supported MIME types, any type that starts with
* this prefix will be regarded as supported
* @param supportedMimeTypes a collection of mimetypes that are supported * @param supportedMimeTypes a collection of mimetypes that are supported
* @param supportedExtension a collection of extensions that are supported * @param supportedExtension a collection of extensions that are supported
* *
* @return true if a thumbnail can be generated for the given file based on * @return true if a thumbnail can be generated for the given file based on
* the given MIME type prefix and lists of supported MIME types and * the given MIME type prefix and lists of supported MIME types and
* extensions * extensions
*/ */
static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix, final Collection<String> supportedMimeTypes, final List<String> supportedExtension) { static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix, final Collection<String> supportedMimeTypes, final List<String> supportedExtension) {
if (false == file.isFile() || file.getSize() <= 0) { if (false == file.isFile() || file.getSize() <= 0) {
@ -282,7 +284,7 @@ public class ImageUtils {
* @return a FileTypeDetector * @return a FileTypeDetector
* *
* @throws FileTypeDetectorInitException if initializing the * @throws FileTypeDetectorInitException if initializing the
* FileTypeDetector failed. * FileTypeDetector failed.
*/ */
synchronized private static FileTypeDetector getFileTypeDetector() throws FileTypeDetector.FileTypeDetectorInitException { synchronized private static FileTypeDetector getFileTypeDetector() throws FileTypeDetector.FileTypeDetectorInitException {
if (fileTypeDetector == null) { if (fileTypeDetector == null) {
@ -295,11 +297,11 @@ public class ImageUtils {
* Get a thumbnail of a specified size for the given image. Generates the * Get a thumbnail of a specified size for the given image. Generates the
* thumbnail if it is not already cached. * thumbnail if it is not already cached.
* *
* @param content the content to generate a thumbnail for * @param content the content to generate a thumbnail for
* @param iconSize the size (one side of a square) in pixels to generate * @param iconSize the size (one side of a square) in pixels to generate
* *
* @return A thumbnail for the given image or a default one if there was a * @return A thumbnail for the given image or a default one if there was a
* problem making a thumbnail. * problem making a thumbnail.
*/ */
public static BufferedImage getThumbnail(Content content, int iconSize) { public static BufferedImage getThumbnail(Content content, int iconSize) {
if (content instanceof AbstractFile) { if (content instanceof AbstractFile) {
@ -310,8 +312,14 @@ public class ImageUtils {
* to rescale easily, but we lose animations. * to rescale easily, but we lose animations.
*/ */
try (BufferedInputStream bufferedReadContentStream = getBufferedReadContentStream(file);) { try (BufferedInputStream bufferedReadContentStream = getBufferedReadContentStream(file);) {
if (Thread.interrupted()) {
return DEFAULT_THUMBNAIL;
}
final BufferedImage image = ImageIO.read(bufferedReadContentStream); final BufferedImage image = ImageIO.read(bufferedReadContentStream);
if (image != null) { if (image != null) {
if (Thread.interrupted()) {
return DEFAULT_THUMBNAIL;
}
return ScalrWrapper.resizeHighQuality(image, iconSize, iconSize); return ScalrWrapper.resizeHighQuality(image, iconSize, iconSize);
} }
} catch (IOException iOException) { } catch (IOException iOException) {
@ -321,6 +329,9 @@ public class ImageUtils {
} }
Task<javafx.scene.image.Image> thumbnailTask = newGetThumbnailTask(file, iconSize, true); Task<javafx.scene.image.Image> thumbnailTask = newGetThumbnailTask(file, iconSize, true);
if (Thread.interrupted()) {
return DEFAULT_THUMBNAIL;
}
thumbnailTask.run(); thumbnailTask.run();
try { try {
return SwingFXUtils.fromFXImage(thumbnailTask.get(), null); return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
@ -338,7 +349,7 @@ public class ImageUtils {
* @param file The AbstractFile to get a stream for. * @param file The AbstractFile to get a stream for.
* *
* @return A BufferedInputStream wrapped around a ReadContentStream for the * @return A BufferedInputStream wrapped around a ReadContentStream for the
* given AbstractFile * given AbstractFile
*/ */
private static BufferedInputStream getBufferedReadContentStream(AbstractFile file) { private static BufferedInputStream getBufferedReadContentStream(AbstractFile file) {
return new BufferedInputStream(new ReadContentInputStream(file)); return new BufferedInputStream(new ReadContentInputStream(file));
@ -348,11 +359,11 @@ public class ImageUtils {
* Get a thumbnail of a specified size for the given image. Generates the * Get a thumbnail of a specified size for the given image. Generates the
* thumbnail if it is not already cached. * thumbnail if it is not already cached.
* *
* @param content the content to generate a thumbnail for * @param content the content to generate a thumbnail for
* @param iconSize the size (one side of a square) in pixels to generate * @param iconSize the size (one side of a square) in pixels to generate
* *
* @return File object for cached image. Is guaranteed to exist, as long as * @return File object for cached image. Is guaranteed to exist, as long as
* there was not an error generating or saving the thumbnail. * there was not an error generating or saving the thumbnail.
*/ */
@Nullable @Nullable
public static File getCachedThumbnailFile(Content content, int iconSize) { public static File getCachedThumbnailFile(Content content, int iconSize) {
@ -367,8 +378,8 @@ public class ImageUtils {
* @param fileID the fileID to get the cached thumbnail location for * @param fileID the fileID to get the cached thumbnail location for
* *
* @return A File object representing the location of the cached thumbnail. * @return A File object representing the location of the cached thumbnail.
* This file may not actually exist(yet). Returns null if there was any * This file may not actually exist(yet). Returns null if there was
* problem getting the file, such as no case was open. * any problem getting the file, such as no case was open.
*/ */
private static File getCachedThumbnailLocation(long fileID) { private static File getCachedThumbnailLocation(long fileID) {
return cacheFileMap.computeIfAbsent(fileID, id -> { return cacheFileMap.computeIfAbsent(fileID, id -> {
@ -426,7 +437,7 @@ public class ImageUtils {
* @param file the AbstractFile to parse * @param file the AbstractFile to parse
* *
* @return Offset of first Start Of Image marker, or 0 if none found. This * @return Offset of first Start Of Image marker, or 0 if none found. This
* will let ImageIO try to open it from offset 0. * will let ImageIO try to open it from offset 0.
*/ */
private static long getJfifStartOfImageOffset(AbstractFile file) { private static long getJfifStartOfImageOffset(AbstractFile file) {
byte[] fileHeaderBuffer; byte[] fileHeaderBuffer;
@ -506,7 +517,7 @@ public class ImageUtils {
* @return the width in pixels * @return the width in pixels
* *
* @throws IOException If the file is not a supported image or the width * @throws IOException If the file is not a supported image or the width
* could not be determined. * could not be determined.
*/ */
static public int getImageWidth(AbstractFile file) throws IOException { static public int getImageWidth(AbstractFile file) throws IOException {
return getImageProperty(file, return getImageProperty(file,
@ -523,7 +534,7 @@ public class ImageUtils {
* @return the height in pixels * @return the height in pixels
* *
* @throws IOException If the file is not a supported image or the height * @throws IOException If the file is not a supported image or the height
* could not be determined. * could not be determined.
*/ */
static public int getImageHeight(AbstractFile file) throws IOException { static public int getImageHeight(AbstractFile file) throws IOException {
return getImageProperty(file, return getImageProperty(file,
@ -552,17 +563,18 @@ public class ImageUtils {
* public methods that pull particular (usually meta-)data out of a image * public methods that pull particular (usually meta-)data out of a image
* file. * file.
* *
* @param file the file to extract the data from * @param file the file to extract the data from
* @param errorTemplate a message template used to log errors. Should take * @param errorTemplate a message template used to log errors. Should
* one parameter: the file's unique path or name. * take one parameter: the file's unique path or
* name.
* @param propertyExtractor an implementation of {@link PropertyExtractor} * @param propertyExtractor an implementation of {@link PropertyExtractor}
* used to retrieve the specific property. * used to retrieve the specific property.
* *
* @return the the value of the property extracted by the given * @return the the value of the property extracted by the given
* propertyExtractor * propertyExtractor
* *
* @throws IOException if there was a problem reading the property from the * @throws IOException if there was a problem reading the property from the
* file. * file.
* *
* @see PropertyExtractor * @see PropertyExtractor
* @see #getImageHeight(org.sleuthkit.datamodel.AbstractFile) * @see #getImageHeight(org.sleuthkit.datamodel.AbstractFile)
@ -606,8 +618,8 @@ public class ImageUtils {
* but is not started automatically. Clients are responsible for running the * but is not started automatically. Clients are responsible for running the
* task, monitoring its progress, and using its result. * task, monitoring its progress, and using its result.
* *
* @param file The file to create a thumbnail for. * @param file The file to create a thumbnail for.
* @param iconSize The size of the thumbnail. * @param iconSize The size of the thumbnail.
* @param defaultOnFailure Whether or not to default on failure. * @param defaultOnFailure Whether or not to default on failure.
* *
* @return a new Task that returns a thumbnail as its result. * @return a new Task that returns a thumbnail as its result.
@ -695,7 +707,9 @@ public class ImageUtils {
throw new IIOException(msg); throw new IIOException(msg);
} }
updateProgress(-1, 1); updateProgress(-1, 1);
if (isCancelled()) {
return null;
}
//resize, or if that fails, crop it //resize, or if that fails, crop it
try { try {
thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize);
@ -709,6 +723,9 @@ public class ImageUtils {
final int cropHeight = Math.min(iconSize, height); final int cropHeight = Math.min(iconSize, height);
final int cropWidth = Math.min(iconSize, width); final int cropWidth = Math.min(iconSize, width);
try { try {
if (isCancelled()) {
return null;
}
thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight); thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight);
} catch (Exception cropException) { } catch (Exception cropException) {
LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS
@ -720,17 +737,15 @@ public class ImageUtils {
} }
} }
if (isCancelled()) {
return null;
}
updateProgress(-1, 1); updateProgress(-1, 1);
//if we got a valid thumbnail save it //if we got a valid thumbnail save it
if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) { if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
saveThumbnail(thumbnail); saveThumbnail(thumbnail);
} }
if (isCancelled()) {
return null;
}
return SwingFXUtils.toFXImage(thumbnail, null); return SwingFXUtils.toFXImage(thumbnail, null);
} }
@ -806,6 +821,9 @@ public class ImageUtils {
} }
protected javafx.scene.image.Image readImage() throws IOException { protected javafx.scene.image.Image readImage() throws IOException {
if (isCancelled()) {
return null;
}
if (ImageUtils.isGIF(file)) { if (ImageUtils.isGIF(file)) {
//use JavaFX to directly read GIF to preserve potential animation //use JavaFX to directly read GIF to preserve potential animation
javafx.scene.image.Image image = new javafx.scene.image.Image(getBufferedReadContentStream(file)); javafx.scene.image.Image image = new javafx.scene.image.Image(getBufferedReadContentStream(file));
@ -865,6 +883,14 @@ public class ImageUtils {
} }
} }
@Override
public boolean isCancelled() {
if (Thread.interrupted()) {
this.cancel(true);
}
return super.isCancelled();
}
@Override @Override
protected void succeeded() { protected void succeeded() {
super.succeeded(); super.succeeded();
@ -976,7 +1002,7 @@ public class ImageUtils {
* @param iconSize * @param iconSize
* *
* @return a thumbnail for the given image or a default one if there was a * @return a thumbnail for the given image or a default one if there was a
* problem making a thumbnail. * problem making a thumbnail.
* *
* @deprecated use getThumbnail(org.sleuthkit.datamodel.Content, int) * @deprecated use getThumbnail(org.sleuthkit.datamodel.Content, int)
* instead. * instead.
@ -995,7 +1021,7 @@ public class ImageUtils {
* @param iconSize * @param iconSize
* *
* @return File object for cached image. Is guaranteed to exist, as long as * @return File object for cached image. Is guaranteed to exist, as long as
* there was not an error generating or saving the thumbnail. * there was not an error generating or saving the thumbnail.
* *
* @deprecated use getCachedThumbnailFile(org.sleuthkit.datamodel.Content, * @deprecated use getCachedThumbnailFile(org.sleuthkit.datamodel.Content,
* int) instead. * int) instead.