diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java index f6a15c7517..6d5cd54f5b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java @@ -24,7 +24,6 @@ import java.awt.Dialog; import java.awt.EventQueue; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -52,18 +51,18 @@ import org.openide.nodes.NodeReorderEvent; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; +import static org.sleuthkit.autopsy.corecomponents.Bundle.*; import org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.SortCriterion; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; -import static org.sleuthkit.autopsy.corecomponents.Bundle.*; /** * A thumbnail viewer for the results view, with paging support. * * The paging is intended to reduce memory footprint by load only up to - * (currently) 1000 images at a time. This works whether or not the underlying + * (currently) 200 images at a time. This works whether or not the underlying * content nodes are being lazy loaded or not. * * TODO (JIRA-2658): Fix DataResultViewer extension point. When this is done, @@ -78,9 +77,10 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { private int curPage; private int totalPages; private int curPageImages; - private int iconSize = ImageUtils.ICON_SIZE_MEDIUM; + private int thumbSize = ImageUtils.ICON_SIZE_MEDIUM; private final PageUpdater pageUpdater = new PageUpdater(); private TableFilterNode tfn; + private ThumbnailViewChildren tvc; /** * Constructs a thumbnail viewer for the results view, with paging support, @@ -295,18 +295,12 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { break; } - if (iconSize != newIconSize) { - iconSize = newIconSize; + if (thumbSize != newIconSize) { + thumbSize = newIconSize; Node root = em.getRootContext(); - for (Children c : Arrays.asList(root.getChildren())) { - ((ThumbnailViewChildren) c).setIconSize(iconSize); - } + ((ThumbnailViewChildren) root.getChildren()).setThumbsSize(thumbSize); - for (Node page : root.getChildren().getNodes()) { - for (Node node : page.getChildren().getNodes()) { - ((ThumbnailViewChildren.ThumbnailViewNode) node).setIconSize(iconSize); - } - } + // Temporarily set the explored context to the root, instead of a child node. // This is a workaround hack to convince org.openide.explorer.ExplorerManager to @@ -385,6 +379,9 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { @Override public void setNode(Node givenNode) { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + if (tvc != null) { + tvc.cancelLoadingThumbnails(); + } try { if (givenNode != null) { tfn = (TableFilterNode) givenNode; @@ -393,14 +390,15 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { * produce ThumbnailPageNodes with ThumbnailViewNode children * from the child nodes of the given node. */ - ThumbnailViewChildren childNode = new ThumbnailViewChildren(givenNode, iconSize); - final Node root = new AbstractNode(childNode); + tvc = new ThumbnailViewChildren(givenNode,thumbSize); + final Node root = new AbstractNode(tvc); pageUpdater.setRoot(root); root.addNodeListener(pageUpdater); em.setRootContext(root); } else { tfn = null; + tvc = null; Node emptyNode = new AbstractNode(Children.LEAF); em.setRootContext(emptyNode); iconView.setBackground(Color.BLACK); @@ -672,4 +670,5 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { } } } + } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java index 148d3aca74..7d03ec65eb 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ResultViewerPersistence.java @@ -172,7 +172,7 @@ final class ResultViewerPersistence { } /** - * Encapsulate the property sort order and sort rank into onde data bag. + * Encapsulate the property, sort order, and sort rank into one data bag. */ static class SortCriterion { diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewChildren.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewChildren.java index 61d608b37a..bc16b97c35 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewChildren.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewChildren.java @@ -18,24 +18,31 @@ */ package org.sleuthkit.autopsy.corecomponents; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.awt.Image; import java.awt.Toolkit; -import java.awt.event.ActionEvent; import java.lang.ref.SoftReference; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import javax.swing.SortOrder; -import javax.swing.SwingWorker; +import javax.swing.SwingUtilities; import javax.swing.Timer; import org.apache.commons.lang3.StringUtils; import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.progress.ProgressHandleFactory; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.FilterNode; @@ -44,17 +51,16 @@ import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.SortCriterion; +import static org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.loadSortCriteria; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Content; -import static org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.loadSortCriteria; /** - * Complementary class to ThumbnailViewNode. Children node factory. Wraps around - * original data result children nodes of the passed in parent node, and creates - * filter nodes for the supported children nodes, adding the bitmap data. If - * original nodes are lazy loaded, this will support lazy loading. Currently, we - * add a page node hierarchy to divide children nodes into "pages". + * Wraps around original data result children nodes of the passed in parent + * node, and creates filter nodes for the supported children nodes, adding the + * thumbnail. If original nodes are lazy loaded, this will support lazy loading. + * We add a page node hierarchy to divide children nodes into "pages". * * Filter-node like class, but adds additional hierarchy (pages) as parents of * the filtered nodes. @@ -63,88 +69,68 @@ class ThumbnailViewChildren extends Children.Keys { private static final Logger logger = Logger.getLogger(ThumbnailViewChildren.class.getName()); + @NbBundle.Messages("ThumbnailViewChildren.progress.cancelling=(Cancelling)") + private static final String CANCELLING_POSTIX = Bundle.ThumbnailViewChildren_progress_cancelling(); static final int IMAGES_PER_PAGE = 200; + + private final ExecutorService executor = Executors.newFixedThreadPool(3, + new ThreadFactoryBuilder().setNameFormat("Thumbnail-Loader-%d").build()); + private final List tasks = new ArrayList<>(); + private final Node parent; - private final HashMap> pages = new HashMap<>(); - private int totalImages = 0; - private int totalPages = 0; - private int iconSize = ImageUtils.ICON_SIZE_MEDIUM; + private final List> pages = new ArrayList<>(); + private int thumbSize; /** - * the constructor + * The constructor * - * @param arg - * @param iconSize + * @param parent The node which is the parent of this children. + * @param thumbSize The hight and/or width of the thumbnails in pixels. */ - ThumbnailViewChildren(Node arg, int iconSize) { + ThumbnailViewChildren(Node parent, int thumbSize) { super(true); //support lazy loading - this.parent = arg; - this.iconSize = iconSize; + this.parent = parent; + this.thumbSize = thumbSize; } @Override protected void addNotify() { super.addNotify(); - setupKeys(); - } + /* + * TODO: When lazy loading of original nodes is fixed, we should be + * asking the datamodel for the children instead and not counting the + * children nodes (which might not be preloaded at this point). + */ + // get list of supported children sorted by persisted criteria + final List suppContent = + Stream.of(parent.getChildren().getNodes()) + .filter(ThumbnailViewChildren::isSupported) + .sorted(getComparator()) + .collect(Collectors.toList()); - int getTotalPages() { - return totalPages; - } - - int getTotalImages() { - return totalImages; - } - - private void setupKeys() { - //divide the supported content into buckets - totalImages = 0; - //TODO when lazy loading of original nodes is fixed - //we should be asking the datamodel for the children instead - //and not counting the children nodes (which might not be preloaded at this point) - final List suppContent = new ArrayList<>(); - for (Node child : parent.getChildren().getNodes()) { - if (isSupported(child)) { - ++totalImages; - suppContent.add(child); - } - } - //sort suppContent! - Collections.sort(suppContent, getComparator()); - - if (totalImages == 0) { + if (suppContent.isEmpty()) { + //if there are no images, there is nothing more to do return; } - totalPages = 0; - if (totalImages < IMAGES_PER_PAGE) { - totalPages = 1; - } else { - totalPages = totalImages / IMAGES_PER_PAGE; - if (totalPages % totalImages != 0) { - ++totalPages; - } - } + //divide the supported content into buckets + pages.addAll(Lists.partition(suppContent, IMAGES_PER_PAGE)); - int prevImages = 0; - for (int page = 1; page <= totalPages; ++page) { - int toAdd = Math.min(IMAGES_PER_PAGE, totalImages - prevImages); - List pageContent = suppContent.subList(prevImages, prevImages + toAdd); - pages.put(page, pageContent); - prevImages += toAdd; - } - - Integer[] pageNums = new Integer[totalPages]; - for (int i = 0; i < totalPages; ++i) { - pageNums[i] = i + 1; - } - setKeys(pageNums); + //the keys are just the indices into the pages list. + setKeys(IntStream.range(0, pages.size()).boxed().collect(Collectors.toList())); } + /** + * Get a comparator for the child nodes loaded from the persisted sort + * criteria. The comparator is a composite one that applies all the sort + * criteria at once. + * + * @return A Comparator used to sort the child nodes. + */ private synchronized Comparator getComparator() { - Comparator comp = (node1, node2) -> 0; + Comparator comp = (node1, node2) -> 0; //eveything is equal. if (!(parent instanceof TableFilterNode)) { return comp; @@ -155,23 +141,45 @@ class ThumbnailViewChildren extends Children.Keys { return sortCriteria.stream() .map(this::getCriterionComparator) .collect(Collectors.reducing(Comparator::thenComparing)) - .orElse(comp); - + .orElse(comp); // default to unordered if nothing is persisted } } + /** + * Make a comparator from the given criterion + * + * @param criterion The criterion to make a comparator for. + * + * @return The comparator for the given criterion. + */ private Comparator getCriterionComparator(SortCriterion criterion) { + @SuppressWarnings("unchecked") Comparator c = Comparator.comparing(node -> getPropertyValue(node, criterion.getProperty()), Comparator.nullsFirst(Comparator.naturalOrder())); return criterion.getSortOrder() == SortOrder.ASCENDING ? c : c.reversed(); } + /** + * Get the value of the given property from the given node. + * + * @param node The node to get the value from. + * @param prop The property to get the value of. + * + * @return The value of the property in the node. + */ + @SuppressWarnings("rawtypes") private Comparable getPropertyValue(Node node, Node.Property prop) { for (Node.PropertySet ps : node.getPropertySets()) { for (Node.Property p : ps.getProperties()) { if (p.equals(prop)) { try { - return (Comparable) p.getValue(); + if (p.getValue() instanceof Comparable) { + return (Comparable) p.getValue(); + } else { + //if the value is not comparable use its string representation + return p.getValue().toString(); + } + } catch (IllegalAccessException | InvocationTargetException ex) { Exceptions.printStackTrace(ex); } @@ -185,17 +193,15 @@ class ThumbnailViewChildren extends Children.Keys { protected void removeNotify() { super.removeNotify(); pages.clear(); - totalImages = 0; } @Override protected Node[] createNodes(Integer pageNum) { - final ThumbnailPageNode pageNode = new ThumbnailPageNode(pageNum); - return new Node[]{pageNode}; + return new Node[]{new ThumbnailPageNode(pageNum, pages.get(pageNum))}; } - static boolean isSupported(Node node) { + private static boolean isSupported(Node node) { if (node != null) { Content content = node.getLookup().lookup(Content.class); if (content != null) { @@ -205,29 +211,59 @@ class ThumbnailViewChildren extends Children.Keys { return false; } - public void setIconSize(int iconSize) { - this.iconSize = iconSize; + public void setThumbsSize(int thumbSize) { + this.thumbSize = thumbSize; + for (Node page : getNodes()) { + for (Node node : page.getChildren().getNodes()) { + ((ThumbnailViewNode) node).setThumbSize(thumbSize); + } + } + } + synchronized void cancelLoadingThumbnails() { + tasks.forEach(task -> task.cancel(Boolean.TRUE)); + executor.shutdownNow(); + tasks.clear(); + } + + private synchronized ThumbnailViewNode.ThumbnailLoadTask loadThumbnail(ThumbnailViewNode node) { + if (executor.isShutdown() == false) { + ThumbnailViewNode.ThumbnailLoadTask task = node.new ThumbnailLoadTask(); + tasks.add(task); + executor.submit(task); + return task; + } else { + return null; + } } /** - * Node that wraps around original node and adds the bitmap icon - * representing the picture + * Node that wraps around original node and adds the thumbnail representing + * the image/video. */ - static class ThumbnailViewNode extends FilterNode { + private class ThumbnailViewNode extends FilterNode { - private static final Image waitingIcon = Toolkit.getDefaultToolkit().createImage(ThumbnailViewNode.class.getResource("/org/sleuthkit/autopsy/images/working_spinner.gif")); - private SoftReference iconCache = null; - private int iconSize = ImageUtils.ICON_SIZE_MEDIUM; - private SwingWorker swingWorker; - private Timer timer; + private final Logger logger = Logger.getLogger(ThumbnailViewNode.class.getName()); + + private final Image waitingIcon = Toolkit.getDefaultToolkit().createImage(ThumbnailViewNode.class.getResource("/org/sleuthkit/autopsy/images/working_spinner.gif")); //NOI18N + + private SoftReference thumbCache = null; + private int thumbSize; + private final Content content; + + private ThumbnailLoadTask thumbTask; + private Timer waitSpinnerTimer; /** - * the constructor + * The constructor + * + * @param wrappedNode The original node that this Node wraps. + * @param thumbSize The hight and/or width of the thumbnail in pixels. */ - ThumbnailViewNode(Node arg, int iconSize) { - super(arg, Children.LEAF); - this.iconSize = iconSize; + private ThumbnailViewNode(Node wrappedNode, int thumbSize) { + super(wrappedNode, FilterNode.Children.LEAF); + this.thumbSize = thumbSize; + this.content = this.getLookup().lookup(Content.class); } @Override @@ -236,115 +272,150 @@ class ThumbnailViewChildren extends Children.Keys { } @Override - @NbBundle.Messages(value = {"# {0} - file name", "ThumbnailViewNode.progressHandle.text=Generating thumbnail for {0}"}) - public Image getIcon(int type) { - Image icon = null; - if (iconCache != null) { - icon = iconCache.get(); + @NbBundle.Messages({"# {0} - file name", + "ThumbnailViewNode.progressHandle.text=Generating thumbnail for {0}"}) + synchronized public Image getIcon(int type) { + if (content == null) { + return ImageUtils.getDefaultThumbnail(); } - if (icon != null) { - return icon; - } else { - final Content content = this.getLookup().lookup(Content.class); - if (content == null) { - return ImageUtils.getDefaultThumbnail(); - } - if (swingWorker == null || swingWorker.isDone()) { - swingWorker = new SwingWorker() { - private final ProgressHandle progressHandle = ProgressHandle.createHandle(Bundle.ThumbnailViewNode_progressHandle_text(content.getName())); - @Override - protected Image doInBackground() throws Exception { - progressHandle.start(); - return ImageUtils.getThumbnail(content, iconSize); - } - - @Override - protected void done() { - super.done(); - try { - iconCache = new SoftReference<>(super.get()); - fireIconChange(); - } catch (InterruptedException | ExecutionException ex) { - Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.SEVERE, "Error getting thumbnail icon for " + content.getName(), ex); //NON-NLS - } finally { - progressHandle.finish(); - if (timer != null) { - timer.stop(); - timer = null; - } - swingWorker = null; - } - } - }; - swingWorker.execute(); + if (thumbCache != null) { + Image thumbnail = thumbCache.get(); + if (thumbnail != null) { + return thumbnail; } - if (timer == null) { - timer = new Timer(100, (ActionEvent e) -> { - fireIconChange(); - }); - timer.start(); - } - return waitingIcon; } + + if (thumbTask == null) { + thumbTask = loadThumbnail(ThumbnailViewNode.this); + + } + if (waitSpinnerTimer == null) { + waitSpinnerTimer = new Timer(1, actionEvent -> fireIconChange()); + waitSpinnerTimer.start(); + } + return waitingIcon; } - public void setIconSize(int iconSize) { - this.iconSize = iconSize; - iconCache = null; - swingWorker = null; + synchronized void setThumbSize(int iconSize) { + this.thumbSize = iconSize; + thumbCache = null; + if (thumbTask != null) { + thumbTask.cancel(true); + thumbTask = null; + } + + } + + private class ThumbnailLoadTask extends FutureTask { + + private final ProgressHandle progressHandle; + private final String progressText; + private boolean cancelled = false; + + ThumbnailLoadTask() { + super(() -> ImageUtils.getThumbnail(content, thumbSize)); + progressText = Bundle.ThumbnailViewNode_progressHandle_text(content.getName()); + + progressHandle = ProgressHandleFactory.createSystemHandle(progressText); + progressHandle.setInitialDelay(500); + progressHandle.start(); + } + + @Override + synchronized public boolean cancel(boolean mayInterrupt) { + cancelled = true; + progressHandle.suspend(progressText + " " + CANCELLING_POSTIX); + return super.cancel(mayInterrupt); + } + + @Override + synchronized public boolean isCancelled() { + return cancelled || super.isCancelled(); //To change body of generated methods, choose Tools | Templates. + } + + @Override + synchronized protected void done() { + progressHandle.finish(); + SwingUtilities.invokeLater(() -> { + + if (waitSpinnerTimer != null) { + waitSpinnerTimer.stop(); + waitSpinnerTimer = null; + } + + try { + if (isCancelled() == false) { + thumbCache = new SoftReference<>(get()); + fireIconChange(); + } + } catch (CancellationException ex) { + //Task was cancelled, do nothing + } catch (InterruptedException | ExecutionException ex) { + if (false == (ex.getCause() instanceof CancellationException)) { + logger.log(Level.SEVERE, "Error getting thumbnail icon for " + content.getName(), ex); //NON-NLS + } + } + }); + } } } /** - * Node representing page node, a parent of image nodes, with a name showing - * children range + * Node representing a page of thumbnails, a parent of image nodes, with a + * name showing children range */ private class ThumbnailPageNode extends AbstractNode { - ThumbnailPageNode(Integer pageNum) { - super(new ThumbnailPageNodeChildren(pages.get(pageNum)), Lookups.singleton(pageNum)); - setName(Integer.toString(pageNum)); - int from = 1 + ((pageNum - 1) * IMAGES_PER_PAGE); - int showImages = Math.min(IMAGES_PER_PAGE, totalImages - (from - 1)); - int to = from + showImages - 1; + private ThumbnailPageNode(Integer pageNum, List childNodes) { + + super(new ThumbnailPageNodeChildren(childNodes), Lookups.singleton(pageNum)); + setName(Integer.toString(pageNum + 1)); + int from = 1 + (pageNum * IMAGES_PER_PAGE); + int to = from + ((ThumbnailPageNodeChildren) getChildren()).getChildCount() - 1; setDisplayName(from + "-" + to); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/Folder-icon.png"); //NON-NLS - } } -//TODO insert node at beginning pressing which goes back to page view + /** + * Children.Keys implementation which uses nodes as keys, and wraps them in + * ThumbnailViewNodes as the child nodes. + * + */ private class ThumbnailPageNodeChildren extends Children.Keys { - //wrapped original nodes - private List contentImages = null; + /* + * wrapped original nodes + */ + private List keyNodes = null; - ThumbnailPageNodeChildren(List contentImages) { + ThumbnailPageNodeChildren(List keyNodes) { super(true); - - this.contentImages = contentImages; + this.keyNodes = keyNodes; } @Override protected void addNotify() { super.addNotify(); - - setKeys(contentImages); + setKeys(keyNodes); } @Override protected void removeNotify() { super.removeNotify(); + setKeys(Collections.emptyList()); + } - setKeys(new ArrayList()); + int getChildCount() { + return keyNodes.size(); } @Override protected Node[] createNodes(Node wrapped) { if (wrapped != null) { - final ThumbnailViewNode thumb = new ThumbnailViewNode(wrapped, iconSize); + final ThumbnailViewNode thumb = new ThumbnailViewNode(wrapped, thumbSize); return new Node[]{thumb}; } else { return new Node[]{}; diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 25ee380a7e..eb6a16e6e0 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -164,8 +164,8 @@ public class ImageUtils { /** * Thread/Executor that saves generated thumbnails to disk in the background */ - private static final Executor imageSaver - = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() + private static final Executor imageSaver = + Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() .namingPattern("thumbnail-saver-%d").build()); //NON-NLS public static List getSupportedImageExtensions() { @@ -213,7 +213,7 @@ public class ImageUtils { * @param file the AbstractFile to test * * @return true if the file is an image we can read and generate thumbnail - * for. + * for. */ public static boolean isImageThumbnailSupported(AbstractFile file) { return isMediaThumbnailSupported(file, "image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) || hasImageFileHeader(file);//NON-NLS @@ -239,16 +239,17 @@ public class ImageUtils { * VideoUtils both implement/extend some base interface/abstract class. That * would be the natural place to put this. * - * @param file the AbstractFile to test - * @param mimeTypePrefix a MIME 'top-level type name' such as "image/", - * including the "/". In addition to the list of supported MIME types, any - * type that starts with this prefix will be regarded as supported + * @param file the AbstractFile to test + * @param mimeTypePrefix a MIME 'top-level type name' such as "image/", + * including the "/". In addition to the list of + * 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 supportedExtension a collection of extensions that are supported * * @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 - * extensions + * the given MIME type prefix and lists of supported MIME types and + * extensions */ static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix, final Collection supportedMimeTypes, final List supportedExtension) { if (false == file.isFile() || file.getSize() <= 0) { @@ -282,7 +283,7 @@ public class ImageUtils { * @return a FileTypeDetector * * @throws FileTypeDetectorInitException if initializing the - * FileTypeDetector failed. + * FileTypeDetector failed. */ synchronized private static FileTypeDetector getFileTypeDetector() throws FileTypeDetector.FileTypeDetectorInitException { if (fileTypeDetector == null) { @@ -295,11 +296,11 @@ public class ImageUtils { * Get a thumbnail of a specified size for the given image. Generates the * 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 * * @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) { if (content instanceof AbstractFile) { @@ -310,8 +311,14 @@ public class ImageUtils { * to rescale easily, but we lose animations. */ try (BufferedInputStream bufferedReadContentStream = getBufferedReadContentStream(file);) { + if (Thread.interrupted()) { + return DEFAULT_THUMBNAIL; + } final BufferedImage image = ImageIO.read(bufferedReadContentStream); if (image != null) { + if (Thread.interrupted()) { + return DEFAULT_THUMBNAIL; + } return ScalrWrapper.resizeHighQuality(image, iconSize, iconSize); } } catch (IOException iOException) { @@ -321,6 +328,9 @@ public class ImageUtils { } Task thumbnailTask = newGetThumbnailTask(file, iconSize, true); + if (Thread.interrupted()) { + return DEFAULT_THUMBNAIL; + } thumbnailTask.run(); try { return SwingFXUtils.fromFXImage(thumbnailTask.get(), null); @@ -338,7 +348,7 @@ public class ImageUtils { * @param file The AbstractFile to get a stream for. * * @return A BufferedInputStream wrapped around a ReadContentStream for the - * given AbstractFile + * given AbstractFile */ private static BufferedInputStream getBufferedReadContentStream(AbstractFile file) { return new BufferedInputStream(new ReadContentInputStream(file)); @@ -348,11 +358,11 @@ public class ImageUtils { * Get a thumbnail of a specified size for the given image. Generates the * 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 * * @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 public static File getCachedThumbnailFile(Content content, int iconSize) { @@ -367,8 +377,8 @@ public class ImageUtils { * @param fileID the fileID to get the cached thumbnail location for * * @return A File object representing the location of the cached thumbnail. - * This file may not actually exist(yet). Returns null if there was any - * problem getting the file, such as no case was open. + * This file may not actually exist(yet). Returns null if there was + * any problem getting the file, such as no case was open. */ private static File getCachedThumbnailLocation(long fileID) { return cacheFileMap.computeIfAbsent(fileID, id -> { @@ -426,7 +436,7 @@ public class ImageUtils { * @param file the AbstractFile to parse * * @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) { byte[] fileHeaderBuffer; @@ -506,7 +516,7 @@ public class ImageUtils { * @return the width in pixels * * @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 { return getImageProperty(file, @@ -523,7 +533,7 @@ public class ImageUtils { * @return the height in pixels * * @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 { return getImageProperty(file, @@ -552,17 +562,18 @@ public class ImageUtils { * public methods that pull particular (usually meta-)data out of a image * file. * - * @param file the file to extract the data from - * @param errorTemplate a message template used to log errors. Should take - * one parameter: the file's unique path or name. + * @param file the file to extract the data from + * @param errorTemplate a message template used to log errors. Should + * take one parameter: the file's unique path or + * name. * @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 - * propertyExtractor + * propertyExtractor * * @throws IOException if there was a problem reading the property from the - * file. + * file. * * @see PropertyExtractor * @see #getImageHeight(org.sleuthkit.datamodel.AbstractFile) @@ -606,8 +617,8 @@ public class ImageUtils { * but is not started automatically. Clients are responsible for running the * task, monitoring its progress, and using its result. * - * @param file The file to create a thumbnail for. - * @param iconSize The size of the thumbnail. + * @param file The file to create a thumbnail for. + * @param iconSize The size of the thumbnail. * @param defaultOnFailure Whether or not to default on failure. * * @return a new Task that returns a thumbnail as its result. @@ -642,19 +653,25 @@ public class ImageUtils { @Override protected javafx.scene.image.Image call() throws Exception { - if (isGIF(file)) { - return readImage(); - } if (isCancelled()) { return null; } + if (isGIF(file)) { + return readImage(); + } // If a thumbnail file is already saved locally, just read that. if (cacheFile != null) { synchronized (cacheFile) { if (cacheFile.exists()) { try { + if (isCancelled()) { + return null; + } BufferedImage cachedThumbnail = ImageIO.read(cacheFile); + if (isCancelled()) { + return null; + } if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { return SwingFXUtils.toFXImage(cachedThumbnail, null); } @@ -666,15 +683,14 @@ public class ImageUtils { } } - if (isCancelled()) { - return null; - } - //There was no correctly-sized cached thumbnail so make one. BufferedImage thumbnail = null; if (VideoUtils.isVideoThumbnailSupported(file)) { if (OPEN_CV_LOADED) { updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); + if (isCancelled()) { + return null; + } thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); } if (null == thumbnail) { @@ -686,6 +702,9 @@ public class ImageUtils { } } else { + if (isCancelled()) { + return null; + } //read the image into a buffered image. //TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null); @@ -695,9 +714,14 @@ public class ImageUtils { throw new IIOException(msg); } updateProgress(-1, 1); - + if (isCancelled()) { + return null; + } //resize, or if that fails, crop it try { + if (isCancelled()) { + return null; + } thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); } catch (IllegalArgumentException | OutOfMemoryError e) { // if resizing does not work due to extreme aspect ratio or oom, crop the image instead. @@ -709,6 +733,12 @@ public class ImageUtils { final int cropHeight = Math.min(iconSize, height); final int cropWidth = Math.min(iconSize, width); try { + if (isCancelled()) { + return null; + } + if (isCancelled()) { + return null; + } thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight); } catch (Exception cropException) { LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS @@ -720,17 +750,15 @@ public class ImageUtils { } } - if (isCancelled()) { - return null; - } - updateProgress(-1, 1); //if we got a valid thumbnail save it if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) { saveThumbnail(thumbnail); } - + if (isCancelled()) { + return null; + } return SwingFXUtils.toFXImage(thumbnail, null); } @@ -806,6 +834,9 @@ public class ImageUtils { } protected javafx.scene.image.Image readImage() throws IOException { + if (isCancelled()) { + return null; + } if (ImageUtils.isGIF(file)) { //use JavaFX to directly read GIF to preserve potential animation javafx.scene.image.Image image = new javafx.scene.image.Image(getBufferedReadContentStream(file)); @@ -823,10 +854,6 @@ public class ImageUtils { } } //fall through to default image reading code if there was an error - if (isCancelled()) { - return null; - } - return getImageProperty(file, "ImageIO could not read {0}: ", imageReader -> { imageReader.addIIOReadProgressListener(ReadImageTaskBase.this); @@ -840,6 +867,9 @@ public class ImageUtils { BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0)); param.setDestination(bufferedImage); try { + if (isCancelled()) { + return null; + } bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object } catch (IOException iOException) { LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS @@ -865,6 +895,15 @@ public class ImageUtils { } } + @Override + public boolean isCancelled() { + if (Thread.interrupted()) { + this.cancel(true); + return true; + } + return super.isCancelled(); + } + @Override protected void succeeded() { super.succeeded(); @@ -976,7 +1015,7 @@ public class ImageUtils { * @param iconSize * * @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) * instead. @@ -995,7 +1034,7 @@ public class ImageUtils { * @param iconSize * * @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, * int) instead. @@ -1007,5 +1046,4 @@ public class ImageUtils { return getCachedThumbnailFile(content, iconSize); } - } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java index 871444fab1..81670a878c 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java @@ -107,6 +107,9 @@ public class VideoUtils { progress.start(100); try { Files.createParentDirs(tempFile); + if (Thread.interrupted()) { + return null; + } ContentUtils.writeToFile(file, tempFile, progress, null, true); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS @@ -114,62 +117,69 @@ public class VideoUtils { progress.finish(); } } - VideoCapture videoFile = new VideoCapture(); // will contain the video - - if (!videoFile.open(tempFile.toString())) { - LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS - return null; - } - double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second - double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames - if (fps <= 0 || totalFrames <= 0) { - LOGGER.log(Level.WARNING, "Error getting fps or total frames for {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS - return null; - } - double milliseconds = 1000 * (totalFrames / fps); //total milliseconds - - double timestamp = Math.min(milliseconds, 500); //default time to check for is 500ms, unless the files is extremely small - - int framkeskip = Double.valueOf(Math.floor((milliseconds - timestamp) / (THUMB_COLUMNS * THUMB_ROWS))).intValue(); - - Mat imageMatrix = new Mat(); BufferedImage bufferedImage = null; - for (int x = 0; x < THUMB_COLUMNS; x++) { - for (int y = 0; y < THUMB_ROWS; y++) { - if (!videoFile.set(CV_CAP_PROP_POS_MSEC, timestamp + x * framkeskip + y * framkeskip * THUMB_COLUMNS)) { - LOGGER.log(Level.WARNING, "Error seeking to " + timestamp + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS - break; // if we can't set the time, return black for that frame - } - //read the frame into the image/matrix - if (!videoFile.read(imageMatrix)) { - LOGGER.log(Level.WARNING, "Error reading frames at " + timestamp + "ms from {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS - break; //if the image for some reason is bad, return black for that frame - } + try { - if (bufferedImage == null) { - bufferedImage = new BufferedImage(imageMatrix.cols() * THUMB_COLUMNS, imageMatrix.rows() * THUMB_ROWS, BufferedImage.TYPE_3BYTE_BGR); - } - - byte[] data = new byte[imageMatrix.rows() * imageMatrix.cols() * (int) (imageMatrix.elemSize())]; - imageMatrix.get(0, 0, data); //copy the image to data - - //todo: this looks like we are swapping the first and third channels. so we can use BufferedImage.TYPE_3BYTE_BGR - if (imageMatrix.channels() == 3) { - for (int k = 0; k < data.length; k += 3) { - byte temp = data[k]; - data[k] = data[k + 2]; - data[k + 2] = temp; - } - } - - bufferedImage.getRaster().setDataElements(imageMatrix.cols() * x, imageMatrix.rows() * y, imageMatrix.cols(), imageMatrix.rows(), data); + if (!videoFile.open(tempFile.toString())) { + LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS + return null; } + double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second + double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames + if (fps <= 0 || totalFrames <= 0) { + LOGGER.log(Level.WARNING, "Error getting fps or total frames for {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS + return null; + } + double milliseconds = 1000 * (totalFrames / fps); //total milliseconds + + double timestamp = Math.min(milliseconds, 500); //default time to check for is 500ms, unless the files is extremely small + + int framkeskip = Double.valueOf(Math.floor((milliseconds - timestamp) / (THUMB_COLUMNS * THUMB_ROWS))).intValue(); + + Mat imageMatrix = new Mat(); + + for (int x = 0; x < THUMB_COLUMNS; x++) { + for (int y = 0; y < THUMB_ROWS; y++) { + if (Thread.interrupted()) { + return null; + } + if (!videoFile.set(CV_CAP_PROP_POS_MSEC, timestamp + x * framkeskip + y * framkeskip * THUMB_COLUMNS)) { + LOGGER.log(Level.WARNING, "Error seeking to " + timestamp + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS + break; // if we can't set the time, return black for that frame + } + //read the frame into the image/matrix + if (!videoFile.read(imageMatrix)) { + LOGGER.log(Level.WARNING, "Error reading frames at " + timestamp + "ms from {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS + break; //if the image for some reason is bad, return black for that frame + } + + if (bufferedImage == null) { + bufferedImage = new BufferedImage(imageMatrix.cols() * THUMB_COLUMNS, imageMatrix.rows() * THUMB_ROWS, BufferedImage.TYPE_3BYTE_BGR); + } + + byte[] data = new byte[imageMatrix.rows() * imageMatrix.cols() * (int) (imageMatrix.elemSize())]; + imageMatrix.get(0, 0, data); //copy the image to data + + //todo: this looks like we are swapping the first and third channels. so we can use BufferedImage.TYPE_3BYTE_BGR + if (imageMatrix.channels() == 3) { + for (int k = 0; k < data.length; k += 3) { + byte temp = data[k]; + data[k] = data[k + 2]; + data[k + 2] = temp; + } + } + + bufferedImage.getRaster().setDataElements(imageMatrix.cols() * x, imageMatrix.rows() * y, imageMatrix.cols(), imageMatrix.rows(), data); + } + } + } finally { + videoFile.release(); // close the file} + } + if (Thread.interrupted()) { + return null; } - - videoFile.release(); // close the file - return bufferedImage == null ? null : ScalrWrapper.resizeFast(bufferedImage, iconSize); } }