mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-12 16:06:15 +00:00
Merge pull request #2851 from millmanorama/2608-video_thumbnails
2608 video thumbnails
This commit is contained in:
commit
7de08414d2
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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[]{};
|
||||||
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user