Merge pull request #2851 from millmanorama/2608-video_thumbnails

2608 video thumbnails
This commit is contained in:
Richard Cordovano 2017-06-16 08:06:32 -04:00 committed by GitHub
commit 7de08414d2
5 changed files with 393 additions and 275 deletions

View File

@ -24,7 +24,6 @@ import java.awt.Dialog;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -52,18 +51,18 @@ import org.openide.nodes.NodeReorderEvent;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.NbPreferences; import org.openide.util.NbPreferences;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer;
import static org.sleuthkit.autopsy.corecomponents.Bundle.*;
import org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.SortCriterion; import org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.SortCriterion;
import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.ImageUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import static org.sleuthkit.autopsy.corecomponents.Bundle.*;
/** /**
* A thumbnail viewer for the results view, with paging support. * A thumbnail viewer for the results view, with paging support.
* *
* The paging is intended to reduce memory footprint by load only up to * 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. * content nodes are being lazy loaded or not.
* *
* TODO (JIRA-2658): Fix DataResultViewer extension point. When this is done, * 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 curPage;
private int totalPages; private int totalPages;
private int curPageImages; private int curPageImages;
private int iconSize = ImageUtils.ICON_SIZE_MEDIUM; private int thumbSize = ImageUtils.ICON_SIZE_MEDIUM;
private final PageUpdater pageUpdater = new PageUpdater(); private final PageUpdater pageUpdater = new PageUpdater();
private TableFilterNode tfn; private TableFilterNode tfn;
private ThumbnailViewChildren tvc;
/** /**
* Constructs a thumbnail viewer for the results view, with paging support, * Constructs a thumbnail viewer for the results view, with paging support,
@ -295,18 +295,12 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer {
break; break;
} }
if (iconSize != newIconSize) { if (thumbSize != newIconSize) {
iconSize = newIconSize; thumbSize = newIconSize;
Node root = em.getRootContext(); Node root = em.getRootContext();
for (Children c : Arrays.asList(root.getChildren())) { ((ThumbnailViewChildren) root.getChildren()).setThumbsSize(thumbSize);
((ThumbnailViewChildren) c).setIconSize(iconSize);
}
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. // 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 // This is a workaround hack to convince org.openide.explorer.ExplorerManager to
@ -385,6 +379,9 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer {
@Override @Override
public void setNode(Node givenNode) { public void setNode(Node givenNode) {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
if (tvc != null) {
tvc.cancelLoadingThumbnails();
}
try { try {
if (givenNode != null) { if (givenNode != null) {
tfn = (TableFilterNode) givenNode; tfn = (TableFilterNode) givenNode;
@ -393,14 +390,15 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer {
* produce ThumbnailPageNodes with ThumbnailViewNode children * produce ThumbnailPageNodes with ThumbnailViewNode children
* from the child nodes of the given node. * from the child nodes of the given node.
*/ */
ThumbnailViewChildren childNode = new ThumbnailViewChildren(givenNode, iconSize); tvc = new ThumbnailViewChildren(givenNode,thumbSize);
final Node root = new AbstractNode(childNode); final Node root = new AbstractNode(tvc);
pageUpdater.setRoot(root); pageUpdater.setRoot(root);
root.addNodeListener(pageUpdater); root.addNodeListener(pageUpdater);
em.setRootContext(root); em.setRootContext(root);
} else { } else {
tfn = null; tfn = null;
tvc = null;
Node emptyNode = new AbstractNode(Children.LEAF); Node emptyNode = new AbstractNode(Children.LEAF);
em.setRootContext(emptyNode); em.setRootContext(emptyNode);
iconView.setBackground(Color.BLACK); iconView.setBackground(Color.BLACK);
@ -672,4 +670,5 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer {
} }
} }
} }
} }

View File

@ -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 { static class SortCriterion {

View File

@ -18,24 +18,31 @@
*/ */
package org.sleuthkit.autopsy.corecomponents; 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.Image;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; 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.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.swing.SortOrder; import javax.swing.SortOrder;
import javax.swing.SwingWorker; import javax.swing.SwingUtilities;
import javax.swing.Timer; import javax.swing.Timer;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.nodes.AbstractNode; import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children; import org.openide.nodes.Children;
import org.openide.nodes.FilterNode; import org.openide.nodes.FilterNode;
@ -44,17 +51,16 @@ import org.openide.util.Exceptions;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.lookup.Lookups; import org.openide.util.lookup.Lookups;
import org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.SortCriterion; 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.ImageUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
import static org.sleuthkit.autopsy.corecomponents.ResultViewerPersistence.loadSortCriteria;
/** /**
* Complementary class to ThumbnailViewNode. Children node factory. Wraps around * Wraps around original data result children nodes of the passed in parent
* original data result children nodes of the passed in parent node, and creates * node, and creates filter nodes for the supported children nodes, adding the
* filter nodes for the supported children nodes, adding the bitmap data. If * thumbnail. If original nodes are lazy loaded, this will support lazy loading.
* original nodes are lazy loaded, this will support lazy loading. Currently, we * We add a page node hierarchy to divide children nodes into "pages".
* add a page node hierarchy to divide children nodes into "pages".
* *
* Filter-node like class, but adds additional hierarchy (pages) as parents of * Filter-node like class, but adds additional hierarchy (pages) as parents of
* the filtered nodes. * the filtered nodes.
@ -63,88 +69,68 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
private static final Logger logger = Logger.getLogger(ThumbnailViewChildren.class.getName()); 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; static final int IMAGES_PER_PAGE = 200;
private final ExecutorService executor = Executors.newFixedThreadPool(3,
new ThreadFactoryBuilder().setNameFormat("Thumbnail-Loader-%d").build());
private final List<ThumbnailViewNode.ThumbnailLoadTask> tasks = new ArrayList<>();
private final Node parent; private final Node parent;
private final HashMap<Integer, List<Node>> pages = new HashMap<>(); private final List<List<Node>> pages = new ArrayList<>();
private int totalImages = 0; private int thumbSize;
private int totalPages = 0;
private int iconSize = ImageUtils.ICON_SIZE_MEDIUM;
/** /**
* the constructor * The constructor
* *
* @param arg * @param parent The node which is the parent of this children.
* @param iconSize * @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 super(true); //support lazy loading
this.parent = arg; this.parent = parent;
this.iconSize = iconSize; this.thumbSize = thumbSize;
} }
@Override @Override
protected void addNotify() { protected void addNotify() {
super.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<Node> suppContent =
Stream.of(parent.getChildren().getNodes())
.filter(ThumbnailViewChildren::isSupported)
.sorted(getComparator())
.collect(Collectors.toList());
int getTotalPages() { if (suppContent.isEmpty()) {
return totalPages; //if there are no images, there is nothing more to do
}
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<Node> 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) {
return; return;
} }
totalPages = 0; //divide the supported content into buckets
if (totalImages < IMAGES_PER_PAGE) { pages.addAll(Lists.partition(suppContent, IMAGES_PER_PAGE));
totalPages = 1;
} else {
totalPages = totalImages / IMAGES_PER_PAGE;
if (totalPages % totalImages != 0) {
++totalPages;
}
}
int prevImages = 0; //the keys are just the indices into the pages list.
for (int page = 1; page <= totalPages; ++page) { setKeys(IntStream.range(0, pages.size()).boxed().collect(Collectors.toList()));
int toAdd = Math.min(IMAGES_PER_PAGE, totalImages - prevImages);
List<Node> 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);
} }
/**
* 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<Node> getComparator() { private synchronized Comparator<Node> getComparator() {
Comparator<Node> comp = (node1, node2) -> 0; Comparator<Node> comp = (node1, node2) -> 0; //eveything is equal.
if (!(parent instanceof TableFilterNode)) { if (!(parent instanceof TableFilterNode)) {
return comp; return comp;
@ -155,23 +141,45 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
return sortCriteria.stream() return sortCriteria.stream()
.map(this::getCriterionComparator) .map(this::getCriterionComparator)
.collect(Collectors.reducing(Comparator::thenComparing)) .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<Node> getCriterionComparator(SortCriterion criterion) { private Comparator<Node> getCriterionComparator(SortCriterion criterion) {
@SuppressWarnings("unchecked")
Comparator<Node> c = Comparator.comparing(node -> getPropertyValue(node, criterion.getProperty()), Comparator<Node> c = Comparator.comparing(node -> getPropertyValue(node, criterion.getProperty()),
Comparator.nullsFirst(Comparator.naturalOrder())); Comparator.nullsFirst(Comparator.naturalOrder()));
return criterion.getSortOrder() == SortOrder.ASCENDING ? c : c.reversed(); 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) { private Comparable getPropertyValue(Node node, Node.Property<?> prop) {
for (Node.PropertySet ps : node.getPropertySets()) { for (Node.PropertySet ps : node.getPropertySets()) {
for (Node.Property<?> p : ps.getProperties()) { for (Node.Property<?> p : ps.getProperties()) {
if (p.equals(prop)) { if (p.equals(prop)) {
try { 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) { } catch (IllegalAccessException | InvocationTargetException ex) {
Exceptions.printStackTrace(ex); Exceptions.printStackTrace(ex);
} }
@ -185,17 +193,15 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
protected void removeNotify() { protected void removeNotify() {
super.removeNotify(); super.removeNotify();
pages.clear(); pages.clear();
totalImages = 0;
} }
@Override @Override
protected Node[] createNodes(Integer pageNum) { protected Node[] createNodes(Integer pageNum) {
final ThumbnailPageNode pageNode = new ThumbnailPageNode(pageNum); return new Node[]{new ThumbnailPageNode(pageNum, pages.get(pageNum))};
return new Node[]{pageNode};
} }
static boolean isSupported(Node node) { private static boolean isSupported(Node node) {
if (node != null) { if (node != null) {
Content content = node.getLookup().lookup(Content.class); Content content = node.getLookup().lookup(Content.class);
if (content != null) { if (content != null) {
@ -205,29 +211,59 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
return false; return false;
} }
public void setIconSize(int iconSize) { public void setThumbsSize(int thumbSize) {
this.iconSize = iconSize; 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 * Node that wraps around original node and adds the thumbnail representing
* representing the picture * 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 final Logger logger = Logger.getLogger(ThumbnailViewNode.class.getName());
private SoftReference<Image> iconCache = null;
private int iconSize = ImageUtils.ICON_SIZE_MEDIUM; private final Image waitingIcon = Toolkit.getDefaultToolkit().createImage(ThumbnailViewNode.class.getResource("/org/sleuthkit/autopsy/images/working_spinner.gif")); //NOI18N
private SwingWorker<Image, Object> swingWorker;
private Timer timer; private SoftReference<Image> 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) { private ThumbnailViewNode(Node wrappedNode, int thumbSize) {
super(arg, Children.LEAF); super(wrappedNode, FilterNode.Children.LEAF);
this.iconSize = iconSize; this.thumbSize = thumbSize;
this.content = this.getLookup().lookup(Content.class);
} }
@Override @Override
@ -236,115 +272,150 @@ class ThumbnailViewChildren extends Children.Keys<Integer> {
} }
@Override @Override
@NbBundle.Messages(value = {"# {0} - file name", "ThumbnailViewNode.progressHandle.text=Generating thumbnail for {0}"}) @NbBundle.Messages({"# {0} - file name",
public Image getIcon(int type) { "ThumbnailViewNode.progressHandle.text=Generating thumbnail for {0}"})
Image icon = null; synchronized public Image getIcon(int type) {
if (iconCache != null) { if (content == null) {
icon = iconCache.get(); 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<Image, Object>() {
private final ProgressHandle progressHandle = ProgressHandle.createHandle(Bundle.ThumbnailViewNode_progressHandle_text(content.getName()));
@Override if (thumbCache != null) {
protected Image doInBackground() throws Exception { Image thumbnail = thumbCache.get();
progressHandle.start(); if (thumbnail != null) {
return ImageUtils.getThumbnail(content, iconSize); return thumbnail;
}
@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 (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) { synchronized void setThumbSize(int iconSize) {
this.iconSize = iconSize; this.thumbSize = iconSize;
iconCache = null; thumbCache = null;
swingWorker = null; if (thumbTask != null) {
thumbTask.cancel(true);
thumbTask = null;
}
}
private class ThumbnailLoadTask extends FutureTask<Image> {
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 * Node representing a page of thumbnails, a parent of image nodes, with a
* children range * name showing children range
*/ */
private class ThumbnailPageNode extends AbstractNode { private class ThumbnailPageNode extends AbstractNode {
ThumbnailPageNode(Integer pageNum) { private ThumbnailPageNode(Integer pageNum, List<Node> childNodes) {
super(new ThumbnailPageNodeChildren(pages.get(pageNum)), Lookups.singleton(pageNum));
setName(Integer.toString(pageNum)); super(new ThumbnailPageNodeChildren(childNodes), Lookups.singleton(pageNum));
int from = 1 + ((pageNum - 1) * IMAGES_PER_PAGE); setName(Integer.toString(pageNum + 1));
int showImages = Math.min(IMAGES_PER_PAGE, totalImages - (from - 1)); int from = 1 + (pageNum * IMAGES_PER_PAGE);
int to = from + showImages - 1; int to = from + ((ThumbnailPageNodeChildren) getChildren()).getChildCount() - 1;
setDisplayName(from + "-" + to); setDisplayName(from + "-" + to);
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/Folder-icon.png"); //NON-NLS 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<Node> { private class ThumbnailPageNodeChildren extends Children.Keys<Node> {
//wrapped original nodes /*
private List<Node> contentImages = null; * wrapped original nodes
*/
private List<Node> keyNodes = null;
ThumbnailPageNodeChildren(List<Node> contentImages) { ThumbnailPageNodeChildren(List<Node> keyNodes) {
super(true); super(true);
this.keyNodes = keyNodes;
this.contentImages = contentImages;
} }
@Override @Override
protected void addNotify() { protected void addNotify() {
super.addNotify(); super.addNotify();
setKeys(keyNodes);
setKeys(contentImages);
} }
@Override @Override
protected void removeNotify() { protected void removeNotify() {
super.removeNotify(); super.removeNotify();
setKeys(Collections.emptyList());
}
setKeys(new ArrayList<Node>()); int getChildCount() {
return keyNodes.size();
} }
@Override @Override
protected Node[] createNodes(Node wrapped) { protected Node[] createNodes(Node wrapped) {
if (wrapped != null) { if (wrapped != null) {
final ThumbnailViewNode thumb = new ThumbnailViewNode(wrapped, iconSize); final ThumbnailViewNode thumb = new ThumbnailViewNode(wrapped, thumbSize);
return new Node[]{thumb}; return new Node[]{thumb};
} else { } else {
return new Node[]{}; return new Node[]{};

View File

@ -164,8 +164,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 +213,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 +239,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 +283,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 +296,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 +311,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 +328,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 +348,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 +358,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 +377,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 +436,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 +516,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 +533,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 +562,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 +617,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.
@ -642,19 +653,25 @@ public class ImageUtils {
@Override @Override
protected javafx.scene.image.Image call() throws Exception { protected javafx.scene.image.Image call() throws Exception {
if (isGIF(file)) {
return readImage();
}
if (isCancelled()) { if (isCancelled()) {
return null; return null;
} }
if (isGIF(file)) {
return readImage();
}
// If a thumbnail file is already saved locally, just read that. // If a thumbnail file is already saved locally, just read that.
if (cacheFile != null) { if (cacheFile != null) {
synchronized (cacheFile) { synchronized (cacheFile) {
if (cacheFile.exists()) { if (cacheFile.exists()) {
try { try {
if (isCancelled()) {
return null;
}
BufferedImage cachedThumbnail = ImageIO.read(cacheFile); BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
if (isCancelled()) {
return null;
}
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) {
return SwingFXUtils.toFXImage(cachedThumbnail, null); 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. //There was no correctly-sized cached thumbnail so make one.
BufferedImage thumbnail = null; BufferedImage thumbnail = null;
if (VideoUtils.isVideoThumbnailSupported(file)) { if (VideoUtils.isVideoThumbnailSupported(file)) {
if (OPEN_CV_LOADED) { if (OPEN_CV_LOADED) {
updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
if (isCancelled()) {
return null;
}
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
} }
if (null == thumbnail) { if (null == thumbnail) {
@ -686,6 +702,9 @@ public class ImageUtils {
} }
} else { } else {
if (isCancelled()) {
return null;
}
//read the image into a buffered image. //read the image into a buffered image.
//TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm //TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null); BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null);
@ -695,9 +714,14 @@ 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 {
if (isCancelled()) {
return null;
}
thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize);
} catch (IllegalArgumentException | OutOfMemoryError e) { } catch (IllegalArgumentException | OutOfMemoryError e) {
// if resizing does not work due to extreme aspect ratio or oom, crop the image instead. // 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 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;
}
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 +750,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 +834,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));
@ -823,10 +854,6 @@ public class ImageUtils {
} }
} }
//fall through to default image reading code if there was an error //fall through to default image reading code if there was an error
if (isCancelled()) {
return null;
}
return getImageProperty(file, "ImageIO could not read {0}: ", return getImageProperty(file, "ImageIO could not read {0}: ",
imageReader -> { imageReader -> {
imageReader.addIIOReadProgressListener(ReadImageTaskBase.this); imageReader.addIIOReadProgressListener(ReadImageTaskBase.this);
@ -840,6 +867,9 @@ public class ImageUtils {
BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0)); BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
param.setDestination(bufferedImage); param.setDestination(bufferedImage);
try { try {
if (isCancelled()) {
return null;
}
bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object
} catch (IOException iOException) { } catch (IOException iOException) {
LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS 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 @Override
protected void succeeded() { protected void succeeded() {
super.succeeded(); super.succeeded();
@ -976,7 +1015,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 +1034,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.
@ -1007,5 +1046,4 @@ public class ImageUtils {
return getCachedThumbnailFile(content, iconSize); return getCachedThumbnailFile(content, iconSize);
} }
} }

View File

@ -107,6 +107,9 @@ public class VideoUtils {
progress.start(100); progress.start(100);
try { try {
Files.createParentDirs(tempFile); Files.createParentDirs(tempFile);
if (Thread.interrupted()) {
return null;
}
ContentUtils.writeToFile(file, tempFile, progress, null, true); ContentUtils.writeToFile(file, tempFile, progress, null, true);
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS
@ -114,62 +117,69 @@ public class VideoUtils {
progress.finish(); progress.finish();
} }
} }
VideoCapture videoFile = new VideoCapture(); // will contain the video 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; BufferedImage bufferedImage = null;
for (int x = 0; x < THUMB_COLUMNS; x++) { try {
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
}
if (bufferedImage == null) { if (!videoFile.open(tempFile.toString())) {
bufferedImage = new BufferedImage(imageMatrix.cols() * THUMB_COLUMNS, imageMatrix.rows() * THUMB_ROWS, BufferedImage.TYPE_3BYTE_BGR); LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS
} return null;
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);
} }
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); return bufferedImage == null ? null : ScalrWrapper.resizeFast(bufferedImage, iconSize);
} }
} }