Fixed the date in the bundle properties

This commit is contained in:
Oliver Spohngellert 2016-01-07 09:54:50 -05:00
commit aff4d7864c
23 changed files with 1087 additions and 646 deletions

View File

@ -21,21 +21,17 @@ package org.sleuthkit.autopsy.corecomponents;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import static java.util.Objects.nonNull;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import javafx.embed.swing.SwingFXUtils; import javafx.event.EventHandler;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Scene; import javafx.scene.Scene;
@ -46,13 +42,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javax.annotation.Nullable;
import javax.imageio.IIOException;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.event.IIOReadProgressListener;
import javax.imageio.stream.ImageInputStream;
import javax.swing.JPanel; import javax.swing.JPanel;
import org.controlsfx.control.MaskerPane; import org.controlsfx.control.MaskerPane;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
@ -63,8 +53,6 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.ReadContentInputStream;
import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Image viewer part of the Media View layered pane. Uses JavaFX to display the * Image viewer part of the Media View layered pane. Uses JavaFX to display the
@ -110,7 +98,7 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
.map("."::concat) //NOI18N .map("."::concat) //NOI18N
.collect(Collectors.toList()); .collect(Collectors.toList());
private LoadImageTask readImageTask; private Task<Image> readImageTask;
/** /**
* Creates new form MediaViewImagePanel * Creates new form MediaViewImagePanel
@ -160,6 +148,14 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
}); });
} }
private void showErrorNode(AbstractFile file) {
externalViewerButton.setOnAction(actionEvent -> //fx ActionEvent
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent //NOI18N
);
borderpane.setCenter(errorNode);
}
/** /**
* Show the contents of the given AbstractFile as a visual image. * Show the contents of the given AbstractFile as a visual image.
* *
@ -175,7 +171,49 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
if (readImageTask != null) { if (readImageTask != null) {
readImageTask.cancel(); readImageTask.cancel();
} }
readImageTask = new LoadImageTask(file); readImageTask = ImageUtils.newReadImageTask(file);
readImageTask.setOnSucceeded((WorkerStateEvent event) -> {
//Note that all error conditions are allready logged in readImageTask.succeeded()
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and
* an image was previously selected
*/
reset();
return;
}
try {
Image fxImage = readImageTask.get();
if (nonNull(fxImage)) {
//we have non-null image show it
fxImageView.setImage(fxImage);
borderpane.setCenter(fxImageView);
} else {
showErrorNode(file);
}
} catch (InterruptedException | ExecutionException ex) {
showErrorNode(file);
}
borderpane.setCursor(Cursor.DEFAULT);
});
readImageTask.setOnFailed(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed
* and an image was previously selected
*/
reset();
return;
}
showErrorNode(file);
borderpane.setCursor(Cursor.DEFAULT);
}
});
maskerPane.setProgressNode(progressBar); maskerPane.setProgressNode(progressBar);
progressBar.progressProperty().bind(readImageTask.progressProperty()); progressBar.progressProperty().bind(readImageTask.progressProperty());
@ -233,170 +271,4 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables // End of variables declaration//GEN-END:variables
private class LoadImageTask extends Task<Image> implements IIOReadProgressListener {
private final AbstractFile file;
volatile private BufferedImage bufferedImage = null;
LoadImageTask(AbstractFile file) {
this.file = file;
}
@Override
@NbBundle.Messages({
"# {0} - file name",
"LoadImageTask.mesageText=Reading image: {0}"})
protected Image call() throws Exception {
updateMessage(Bundle.LoadImageTask_mesageText(file.getName()));
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
if (ImageUtils.isGIF(file)) {
//directly read GIF to preserve potential animation,
Image image = new Image(new BufferedInputStream(inputStream));
if (image.isError() == false) {
return image;
}
//fall through to default iamge reading code if there was an error
}
ImageInputStream input = ImageIO.createImageInputStream(inputStream);
if (input == null) {
throw new IIOException("Could not create ImageInputStream."); //NOI18N
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.addIIOReadProgressListener(this);
reader.setInput(input);
/*
* This is the important part, get or create a ReadParam,
* create a destination image to hold the decoded result,
* then pass that image with the param.
*/
ImageReadParam param = reader.getDefaultReadParam();
bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
param.setDestination(bufferedImage);
try {
reader.read(0, param);
} catch (IOException iOException) {
// Ignore this exception or display a warning or similar, for exceptions happening during decoding
logError(iOException);
}
reader.removeIIOReadProgressListener(this);
return SwingFXUtils.toFXImage(bufferedImage, null);
} else {
throw new IIOException("No ImageReader found for file."); //NOI18N
}
}
}
private void logError(@Nullable Throwable e) {
String message = e == null ? "" : "It may be unsupported or corrupt: " + e.getLocalizedMessage(); //NOI18N
try {
LOGGER.log(Level.WARNING, "The MediaView tab could not read the image: {0}. {1}", new Object[]{file.getUniquePath(), message}); //NOI18N
} catch (TskCoreException tskCoreException) {
LOGGER.log(Level.WARNING, "The MediaView tab could not read the image: {0}. {1}", new Object[]{file.getName(), message}); //NOI18N
LOGGER.log(Level.SEVERE, "Failes to get unique path for file", tskCoreException); //NOI18N
}
}
@Override
protected void failed() {
super.failed();
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and an
* image was previously selected
*/
reset();
return;
}
handleError(getException());
borderpane.setCursor(Cursor.DEFAULT);
}
private void handleError(Throwable e) {
logError(e);
externalViewerButton.setOnAction(actionEvent -> //fx ActionEvent
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent //NOI18N
);
borderpane.setCenter(errorNode);
}
@Override
protected void succeeded() {
super.succeeded();
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and an
* image was previously selected
*/
reset();
return;
}
try {
Image fxImage = get();
if (fxImage == null) {
handleError(null);
} else {
//we have non-null image show it
fxImageView.setImage(fxImage);
borderpane.setCenter(fxImageView);
if (fxImage.isError()) {
//if there was somekind of error, log it
logError(fxImage.getException());
}
}
} catch (InterruptedException | ExecutionException ex) {
handleError(ex.getCause());
}
borderpane.setCursor(Cursor.DEFAULT);
}
@Override
public void imageProgress(ImageReader source, float percentageDone) {
//update this task with the progress reported by ImageReader.read
updateProgress(percentageDone, 100);
}
@Override
public void sequenceStarted(ImageReader source, int minIndex) {
}
@Override
public void sequenceComplete(ImageReader source) {
}
@Override
public void imageStarted(ImageReader source, int imageIndex) {
}
@Override
public void imageComplete(ImageReader source) {
}
@Override
public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
}
@Override
public void thumbnailProgress(ImageReader source, float percentageDone) {
}
@Override
public void thumbnailComplete(ImageReader source) {
}
@Override
public void readAborted(ImageReader source) {
}
}
} }

View File

@ -22,6 +22,8 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import org.openide.filesystems.FileObject; import org.openide.filesystems.FileObject;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* File and dir utilities * File and dir utilities
@ -29,6 +31,7 @@ import org.openide.filesystems.FileObject;
public class FileUtil { public class FileUtil {
private static final Logger logger = Logger.getLogger(FileUtil.class.getName()); private static final Logger logger = Logger.getLogger(FileUtil.class.getName());
private static String TEMP_FILE_PREFIX = "Autopsy";
/** /**
* Recursively delete all of the files and sub-directories in a directory. * Recursively delete all of the files and sub-directories in a directory.
@ -167,4 +170,28 @@ public class FileUtil {
//with underscores. We are only keeping \ as it could be part of the path. //with underscores. We are only keeping \ as it could be part of the path.
return fileName.replaceAll("[/:\"*?<>|]+", "_"); return fileName.replaceAll("[/:\"*?<>|]+", "_");
} }
/**
* Test if the current user has read and write access to the path.
*
* @param path The path to test for read and write access.
*
* @return True if we have both read and write access, false otherwise.
*/
public static boolean hasReadWriteAccess(Path path) {
Path p = null;
try {
p = Files.createTempFile(path, TEMP_FILE_PREFIX, null);
return (p.toFile().canRead() && p.toFile().canWrite());
} catch (IOException ex) {
return false;
} finally {
if (p != null) {
try {
p.toFile().delete();
} catch (Exception ignored) {
}
}
}
}
} }

View File

@ -1,5 +1,4 @@
/* /*
*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2012-15 Basis Technology Corp. * Copyright 2012-15 Basis Technology Corp.
@ -27,28 +26,37 @@ import com.google.common.io.Files;
import java.awt.Image; import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.imageio.IIOException;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.event.IIOReadProgressListener;
import javax.imageio.stream.ImageInputStream;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.opencv.core.Core; import org.opencv.core.Core;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.corelibs.ScalrWrapper;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
@ -59,31 +67,34 @@ import org.sleuthkit.datamodel.ReadContentInputStream;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Utilities for working with Images and creating thumbnails. Reuses thumbnails * Utilities for working with image files and creating thumbnails. Reuses
* by storing them in the case's cache directory. * thumbnails by storing them in the case's cache directory.
*/ */
public class ImageUtils { public class ImageUtils {
private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName());
private static final String COULD_NOT_WRITE_CACHE_THUMBNAIL = "Could not write cache thumbnail: "; //NOI18N
private static final String COULD_NOT_CREATE_IMAGE_INPUT_STREAM = "Could not create ImageInputStream."; //NOI18N
private static final String NO_IMAGE_READER_FOUND_FOR_ = "No ImageReader found for "; //NOI18N
/** /**
* save thumbnails to disk as this format * save thumbnails to disk as this format
*/ */
private static final String FORMAT = "png"; //NON-NLS private static final String FORMAT = "png"; //NON-NLS //NOI18N
public static final int ICON_SIZE_SMALL = 50; public static final int ICON_SIZE_SMALL = 50;
public static final int ICON_SIZE_MEDIUM = 100; public static final int ICON_SIZE_MEDIUM = 100;
public static final int ICON_SIZE_LARGE = 200; public static final int ICON_SIZE_LARGE = 200;
private static final Logger logger = LOGGER;
private static final BufferedImage DEFAULT_THUMBNAIL; private static final BufferedImage DEFAULT_THUMBNAIL;
private static final String IMAGE_GIF_MIME = "image/gif"; private static final String IMAGE_GIF_MIME = "image/gif"; //NOI18N
private static final SortedSet<String> GIF_MIME_SET = ImmutableSortedSet.copyOf(new String[]{IMAGE_GIF_MIME}); private static final SortedSet<String> GIF_MIME_SET = ImmutableSortedSet.copyOf(new String[]{IMAGE_GIF_MIME});
private static final List<String> SUPPORTED_IMAGE_EXTENSIONS; private static final List<String> SUPPORTED_IMAGE_EXTENSIONS;
private static final SortedSet<String> SUPPORTED_IMAGE_MIME_TYPES; private static final SortedSet<String> SUPPORTED_IMAGE_MIME_TYPES;
private static final List<String> CONDITIONAL_MIME_TYPES = Arrays.asList("audio/x-aiff", "application/octet-stream"); private static final List<String> CONDITIONAL_MIME_TYPES = Arrays.asList("audio/x-aiff", "application/octet-stream"); //NOI18N
private static final boolean openCVLoaded; private static final boolean openCVLoaded;
@ -91,9 +102,9 @@ public class ImageUtils {
ImageIO.scanForPlugins(); ImageIO.scanForPlugins();
BufferedImage tempImage; BufferedImage tempImage;
try { try {
tempImage = ImageIO.read(ImageUtils.class.getResourceAsStream("/org/sleuthkit/autopsy/images/file-icon.png"));//NON-NLS tempImage = ImageIO.read(ImageUtils.class.getResourceAsStream("/org/sleuthkit/autopsy/images/file-icon.png"));//NON-NLS //NOI18N
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Failed to load default icon.", ex); LOGGER.log(Level.SEVERE, "Failed to load default icon.", ex); //NOI18N
tempImage = null; tempImage = null;
} }
DEFAULT_THUMBNAIL = tempImage; DEFAULT_THUMBNAIL = tempImage;
@ -102,16 +113,16 @@ public class ImageUtils {
boolean openCVLoadedTemp; boolean openCVLoadedTemp;
try { try {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME); System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { //NOI18N
System.loadLibrary("opencv_ffmpeg248_64"); System.loadLibrary("opencv_ffmpeg248_64"); //NOI18N
} else { } else {
System.loadLibrary("opencv_ffmpeg248"); System.loadLibrary("opencv_ffmpeg248"); //NOI18N
} }
openCVLoadedTemp = true; openCVLoadedTemp = true;
} catch (UnsatisfiedLinkError e) { } catch (UnsatisfiedLinkError e) {
openCVLoadedTemp = false; openCVLoadedTemp = false;
LOGGER.log(Level.SEVERE, "OpenCV Native code library failed to load", e); LOGGER.log(Level.SEVERE, "OpenCV Native code library failed to load", e); //NOI18N
//TODO: show warning bubble //TODO: show warning bubble
} }
@ -129,8 +140,8 @@ public class ImageUtils {
"image/x-ms-bmp", "image/x-ms-bmp",
"image/x-portable-graymap", "image/x-portable-graymap",
"image/x-portable-bitmap", "image/x-portable-bitmap",
"application/x-123")); //TODO: is this correct? -jm "application/x-123")); //TODO: is this correct? -jm //NOI18N
SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); //NOI18N
} }
/** /**
@ -143,7 +154,7 @@ public class ImageUtils {
*/ */
private static final Executor imageSaver = private static final Executor imageSaver =
Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder()
.namingPattern("icon saver-%d").build()); .namingPattern("icon saver-%d").build()); //NOI18N
public static List<String> getSupportedImageExtensions() { public static List<String> getSupportedImageExtensions() {
return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS); return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
@ -179,10 +190,12 @@ public class ImageUtils {
/** /**
* Can a thumbnail be generated for the content? * Can a thumbnail be generated for the content?
* *
* @param content * Although this method accepts Content, it always returns false for objects
* that are not instances of AbstractFile.
* *
* @return * @param content A content object to test for thumbnail support.
* *
* @return true if a thumbnail can be generated for the given content.
*/ */
public static boolean thumbnailSupported(Content content) { public static boolean thumbnailSupported(Content content) {
@ -199,12 +212,26 @@ public class ImageUtils {
} }
/**
* is the file an image that we can read and generate a thumbnail for
*
* @param file
*
* @return true if the file is an image we can read and generate thumbnail
* for.
*/
public static boolean isImageThumbnailSupported(AbstractFile file) { public static boolean isImageThumbnailSupported(AbstractFile file) {
return isMediaThumbnailSupported(file, SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS, CONDITIONAL_MIME_TYPES) return isMediaThumbnailSupported(file, SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS, CONDITIONAL_MIME_TYPES)
|| hasImageFileHeader(file); || hasImageFileHeader(file);
} }
/**
* Does the image have a GIF mimetype.
*
* @param file
*
* @return true if the given file has a GIF mimetype
*/
public static boolean isGIF(AbstractFile file) { public static boolean isGIF(AbstractFile file) {
try { try {
final FileTypeDetector fileTypeDetector = getFileTypeDetector(); final FileTypeDetector fileTypeDetector = getFileTypeDetector();
@ -213,16 +240,16 @@ public class ImageUtils {
return IMAGE_GIF_MIME.equalsIgnoreCase(fileType); return IMAGE_GIF_MIME.equalsIgnoreCase(fileType);
} }
} catch (TskCoreException | FileTypeDetectorInitException ex) { } catch (TskCoreException | FileTypeDetectorInitException ex) {
LOGGER.log(Level.WARNING, "Failed to get mime type with FileTypeDetector.", ex); LOGGER.log(Level.WARNING, "Failed to get mime type with FileTypeDetector.", ex); //NOI18N
} }
LOGGER.log(Level.WARNING, "Falling back on direct mime type check."); LOGGER.log(Level.WARNING, "Falling back on direct mime type check."); //NOI18N
switch (file.isMimeType(GIF_MIME_SET)) { switch (file.isMimeType(GIF_MIME_SET)) {
case TRUE: case TRUE:
return true; return true;
case UNDEFINED: case UNDEFINED:
LOGGER.log(Level.WARNING, "Falling back on extension check."); LOGGER.log(Level.WARNING, "Falling back on extension check."); //NOI18N
return "gif".equals(file.getNameExtension()); return "gif".equals(file.getNameExtension()); //NOI18N
case FALSE: case FALSE:
default: default:
return false; return false;
@ -230,7 +257,7 @@ public class ImageUtils {
} }
/** /**
* Check if a file is "supported" by checking it mimetype and extension * Check if a file is "supported" by checking its mimetype and extension
* *
* //TODO: this should move to a better place. Should ImageUtils and * //TODO: this should move to a better place. Should ImageUtils and
* VideoUtils both implement/extend some base interface/abstract class. That * VideoUtils both implement/extend some base interface/abstract class. That
@ -245,8 +272,8 @@ public class ImageUtils {
* @param conditionalMimes a set of mimetypes that a file could have to be * @param conditionalMimes a set of mimetypes that a file could have to be
* supoprted if it also has a supported extension * supoprted if it also has a supported extension
* *
* @return true if a thumbnail can be generated for the given file with the * @return true if a thumbnail can be generated for the given file based on
* given lists of supported mimetype and extensions * the given lists of supported mimetype and extensions
*/ */
static boolean isMediaThumbnailSupported(AbstractFile file, final SortedSet<String> supportedMimeTypes, final List<String> supportedExtension, List<String> conditionalMimes) { static boolean isMediaThumbnailSupported(AbstractFile file, final SortedSet<String> supportedMimeTypes, final List<String> supportedExtension, List<String> conditionalMimes) {
if (file.getSize() == 0) { if (file.getSize() == 0) {
@ -260,8 +287,7 @@ public class ImageUtils {
|| (conditionalMimes.contains(mimeType.toLowerCase()) && supportedExtension.contains(extension)); || (conditionalMimes.contains(mimeType.toLowerCase()) && supportedExtension.contains(extension));
} }
} catch (FileTypeDetector.FileTypeDetectorInitException | TskCoreException ex) { } catch (FileTypeDetector.FileTypeDetectorInitException | TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to look up mimetype for " + file.getName() + " using FileTypeDetector. Fallingback on AbstractFile.isMimeType", ex); LOGGER.log(Level.WARNING, "Failed to look up mimetype for " + getContentPathSafe(file) + " using FileTypeDetector. Fallingback on AbstractFile.isMimeType", ex); //NOI18N
AbstractFile.MimeMatchEnum mimeMatch = file.isMimeType(supportedMimeTypes); AbstractFile.MimeMatchEnum mimeMatch = file.isMimeType(supportedMimeTypes);
if (mimeMatch == AbstractFile.MimeMatchEnum.TRUE) { if (mimeMatch == AbstractFile.MimeMatchEnum.TRUE) {
return true; return true;
@ -289,29 +315,27 @@ public class ImageUtils {
} }
/** /**
* Get a thumbnail of a specified size. Generates the image if it is not * Get a thumbnail of a specified size for the given image. Generates the
* already cached. * thumbnail if it is not already cached.
* *
* @param content * @param content
* @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 {@link #getThumbnail(org.sleuthkit.datamodel.Content, int) * @deprecated use {@link #getThumbnail(org.sleuthkit.datamodel.Content, int)
* } instead. * } instead.
*
*/ */
@Nonnull @Nonnull
@Deprecated @Deprecated
public static Image getIcon(Content content, int iconSize) { public static BufferedImage getIcon(Content content, int iconSize) {
return getThumbnail(content, iconSize); return getThumbnail(content, iconSize);
} }
/** /**
* Get a thumbnail of a specified size. Generates the image if it is not * Get a thumbnail of a specified size for the given image. Generates the
* already cached. * thumbnail if it is not already cached.
* *
* @param content * @param content
* @param iconSize * @param iconSize
@ -319,25 +343,17 @@ public class ImageUtils {
* @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 Image getThumbnail(Content content, int iconSize) { public static BufferedImage getThumbnail(Content content, int iconSize) {
if (content instanceof AbstractFile) { if (content instanceof AbstractFile) {
AbstractFile file = (AbstractFile) content; AbstractFile file = (AbstractFile) content;
// If a thumbnail file is already saved locally
File cacheFile = getCachedThumbnailLocation(content.getId()); Task<javafx.scene.image.Image> thumbnailTask = newGetThumbnailTask(file, iconSize, true);
if (cacheFile.exists()) { thumbnailTask.run();
try { try {
BufferedImage thumbnail = ImageIO.read(cacheFile); return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
if (isNull(thumbnail) || thumbnail.getWidth() != iconSize) { } catch (InterruptedException | ExecutionException ex) {
return generateAndSaveThumbnail(file, iconSize, cacheFile); LOGGER.log(Level.WARNING, "Failed to get thumbnail for {0}: " + ex.toString(), getContentPathSafe(content));
} else { return DEFAULT_THUMBNAIL;
return thumbnail;
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Error while reading image: " + content.getName(), ex); //NON-NLS
return generateAndSaveThumbnail(file, iconSize, cacheFile);
}
} else {
return generateAndSaveThumbnail(file, iconSize, cacheFile);
} }
} else { } else {
return DEFAULT_THUMBNAIL; return DEFAULT_THUMBNAIL;
@ -345,8 +361,8 @@ public class ImageUtils {
} }
/** /**
* Get a thumbnail of a specified size. Generates the image if it is not * Get a thumbnail of a specified size for the given image. Generates the
* already cached. * thumbnail if it is not already cached.
* *
* @param content * @param content
* @param iconSize * @param iconSize
@ -367,8 +383,8 @@ public class ImageUtils {
/** /**
* *
* Get a thumbnail of a specified size. Generates the image if it is not * Get a thumbnail of a specified size for the given image. Generates the
* already cached. * thumbnail if it is not already cached.
* *
* @param content * @param content
* @param iconSize * @param iconSize
@ -390,7 +406,6 @@ public class ImageUtils {
* *
* @return * @return
* *
*
* @deprecated use {@link #getCachedThumbnailLocation(long) } instead * @deprecated use {@link #getCachedThumbnailLocation(long) } instead
*/ */
@Deprecated @Deprecated
@ -400,18 +415,26 @@ public class ImageUtils {
} }
/** /**
* Get a file object for where the cached icon should exist. The returned * Get a file object for where the cached thumbnail should exist. The
* file may not exist. * returned file may not exist.
* *
* @param fileID * @param fileID
* *
* @return * @return a File object representing the location of the cached thumbnail.
* * This file may not actually exist(yet).
*/ */
private static File getCachedThumbnailLocation(long fileID) { private static File getCachedThumbnailLocation(long fileID) {
return Paths.get(Case.getCurrentCase().getCacheDirectory(), "thumbnails", fileID + ".png").toFile(); return Paths.get(Case.getCurrentCase().getCacheDirectory(), "thumbnails", fileID + ".png").toFile(); //NOI18N
} }
/**
* Do a direct check to see if the given file has an image file header.
* NOTE: Currently only jpeg and png are supported.
*
* @param file
*
* @return true if the given file has one of the supported image headers.
*/
public static boolean hasImageFileHeader(AbstractFile file) { public static boolean hasImageFileHeader(AbstractFile file) {
return isJpegFileHeader(file) || isPngFileHeader(file); return isJpegFileHeader(file) || isPngFileHeader(file);
} }
@ -476,91 +499,426 @@ public class ImageUtils {
if (bytesRead != buffLength) { if (bytesRead != buffLength) {
//ignore if can't read the first few bytes, not an image //ignore if can't read the first few bytes, not an image
throw new TskCoreException("Could not read " + buffLength + " bytes from " + file.getName()); throw new TskCoreException("Could not read " + buffLength + " bytes from " + file.getName()); //NOI18N
} }
return fileHeaderBuffer; return fileHeaderBuffer;
} }
/** /**
* Generate an icon and save it to specified location. * Get the width of the given image, in pixels.
* *
* @param file File to generate icon for * @param file
* @param iconSize
* @param cacheFile Location to save thumbnail to
* *
* @return Generated icon or null on error * @return the width in pixels
*
* @throws IOException If the file is not a supported image or the width
* could not be determined.
*/ */
private static Image generateAndSaveThumbnail(AbstractFile file, int iconSize, File cacheFile) { static public int getImageWidth(AbstractFile file) throws IOException {
BufferedImage thumbnail = null; return getImageProperty(file,
try { "ImageIO could not determine width of {0}: ", //NOI18N
if (VideoUtils.isVideoThumbnailSupported(file)) { imageReader -> imageReader.getWidth(0)
if (openCVLoaded) { );
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
} else {
return DEFAULT_THUMBNAIL;
}
} else {
thumbnail = generateImageThumbnail(file, iconSize);
}
if (thumbnail == null) {
return DEFAULT_THUMBNAIL;
} else {
BufferedImage toSave = thumbnail;
imageSaver.execute(() -> {
try {
Files.createParentDirs(cacheFile);
if (cacheFile.exists()) {
cacheFile.delete();
}
ImageIO.write(toSave, FORMAT, cacheFile);
} catch (IllegalArgumentException | IOException ex1) {
LOGGER.log(Level.WARNING, "Could not write cache thumbnail: " + file, ex1); //NON-NLS
}
});
}
} catch (NullPointerException ex) {
logger.log(Level.WARNING, "Could not write cache thumbnail: " + file, ex); //NON-NLS
}
return thumbnail;
} }
/** /**
* Get the height of the given image,in pixels.
* *
* Generate and return a scaled image * @param file
* *
* @param content * @return the height in pixels
* @param iconSize
* *
* @return a Thumbnail of the given content at the given size, or null if * @throws IOException If the file is not a supported image or the height
* there was a problem. * could not be determined.
*/ */
@Nullable static public int getImageHeight(AbstractFile file) throws IOException {
private static BufferedImage generateImageThumbnail(Content content, int iconSize) { return getImageProperty(file,
"ImageIO could not determine height of {0}: ", //NOI18N
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(content));) { imageReader -> imageReader.getHeight(0)
BufferedImage bi = ImageIO.read(inputStream); );
if (bi == null) {
LOGGER.log(Level.WARNING, "No image reader for file: {0}", content.getName()); //NON-NLS
return null;
}
try {
return ScalrWrapper.resizeFast(bi, iconSize);
} catch (IllegalArgumentException e) {
// if resizing does not work due to extreme aspect ratio,
// crop the image instead.
return ScalrWrapper.cropImage(bi, Math.min(iconSize, bi.getWidth()), Math.min(iconSize, bi.getHeight()));
}
} catch (OutOfMemoryError e) {
LOGGER.log(Level.WARNING, "Could not scale image (too large) " + content.getName(), e); //NON-NLS
} catch (EOFException e) {
LOGGER.log(Level.WARNING, "Could not load image (EOF) {0}", content.getName()); //NON-NLS
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not load image " + content.getName(), e); //NON-NLS
}
return null;
} }
/**
* Functional interface for methods that extract a property out of an
* ImageReader. Initially created to abstract over
* {@link #getImageHeight(org.sleuthkit.datamodel.AbstractFile)} and
* {@link #getImageWidth(org.sleuthkit.datamodel.AbstractFile)}
*
* @param <T> The type of the property.
*/
@FunctionalInterface
private static interface PropertyExtractor<T> {
public T extract(ImageReader reader) throws IOException;
}
/**
* Private template method designed to be used as the implementation of
* public methods that pull particular (usually meta-)data out of a image
* file. ./**
*
* @param <T> the type of the property to be retrieved.
* @param file the file to extract the data from
* @param errorTemplate a message template used to log errors. Should
* take one parameter: the file's unique path or
* name.
* @param propertyExtractor an implementation of {@link PropertyExtractor}
* used to retrieve the specific property.
*
* @return the the value of the property extracted by the given
* propertyExtractor
*
* @throws IOException if there was a problem reading the property from the
* file.
*
* @see PropertyExtractor
* @see #getImageHeight(org.sleuthkit.datamodel.AbstractFile)
*/
private static <T> T getImageProperty(AbstractFile file, final String errorTemplate, PropertyExtractor<T> propertyExtractor) throws IOException {
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
if (input == null) {
IIOException iioException = new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM);
LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
throw iioException;
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(input);
try {
return propertyExtractor.extract(reader);
} catch (IOException ex) {
LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
throw ex;
} finally {
reader.dispose();
}
} else {
IIOException iioException = new IIOException(NO_IMAGE_READER_FOUND_FOR_ + getContentPathSafe(file));
LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
throw iioException;
}
}
}
}
/**
* Create a new {@link Task} that will get a thumbnail for the given image
* of the specified size. If a cached thumbnail is available it will be
* returned as the result of the task, otherwise a new thumbnail will be
* created and cached.
*
* Note: the returned task is suitable for running in a background thread,
* but is not started automatically. Clients are responsible for running the
* task, monitoring its progress, and using its result.
*
* @param file the file to create a thumbnail for
* @param iconSize the size of the thumbnail
*
* @return a new Task that returns a thumbnail as its result.
*/
public static Task<javafx.scene.image.Image> newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) {
return new GetThumbnailTask(file, iconSize, defaultOnFailure);
}
/**
* A Task that gets cached thumbnails and makes new ones as needed.
*/
static private class GetThumbnailTask extends ReadImageTaskBase {
private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read image for thumbnail generation."; //NOI18N
private final int iconSize;
private final File cacheFile;
private final boolean defaultOnFailure;
@NbBundle.Messages({"# {0} - file name",
"GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", "# {0} - file name",
"GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) {
super(file);
updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
this.iconSize = iconSize;
this.defaultOnFailure = defaultOnFailure;
this.cacheFile = getCachedThumbnailLocation(file.getId());
}
@Override
protected javafx.scene.image.Image call() throws Exception {
// If a thumbnail file is already saved locally, just read that.
if (cacheFile.exists()) {
try {
BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) {
return SwingFXUtils.toFXImage(cachedThumbnail, null);
}
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "ImageIO had a problem reading thumbnail for image {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
}
}
//There was no correctly-sized cached thumbnail so make one.
BufferedImage thumbnail = null;
if (VideoUtils.isVideoThumbnailSupported(file)) {
if (openCVLoaded) {
updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
} else if (defaultOnFailure) {
thumbnail = DEFAULT_THUMBNAIL;
} else {
throw new IIOException("Failed to read image for thumbnail generation.");
}
} else {
//read the image into abuffered image.
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null);
if (null == bufferedImage) {
LOGGER.log(Level.WARNING, FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION);
throw new IIOException(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION);
}
updateProgress(-1, 1);
//resize, or if that fails, crop it
try {
thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize);
} catch (IllegalArgumentException | OutOfMemoryError e) {
// if resizing does not work due to extreme aspect ratio or oom, crop the image instead.
LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString() + ". Attemptying to crop {0} instead", ImageUtils.getContentPathSafe(file)); //NOI18N
final int height = bufferedImage.getHeight();
final int width = bufferedImage.getWidth();
if (iconSize < height || iconSize < width) {
final int cropHeight = Math.min(iconSize, height);
final int cropWidth = Math.min(iconSize, width);
try {
thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight);
} catch (Exception cropException) {
LOGGER.log(Level.WARNING, "Could not crop image {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
throw cropException;
}
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
throw e;
}
}
updateProgress(-1, 1);
//if we got a valid thumbnail save it
if (nonNull(thumbnail) && DEFAULT_THUMBNAIL != thumbnail) {
saveThumbnail(thumbnail);
}
return SwingFXUtils.toFXImage(thumbnail, null);
}
/**
* submit the thumbnail saving to another background thread.
*
* @param thumbnail
*/
private void saveThumbnail(BufferedImage thumbnail) {
imageSaver.execute(() -> {
try {
Files.createParentDirs(cacheFile);
if (cacheFile.exists()) {
cacheFile.delete();
}
ImageIO.write(thumbnail, FORMAT, cacheFile);
} catch (IllegalArgumentException | IOException ex) {
LOGGER.log(Level.WARNING, "Could not write thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
}
});
}
}
/**
* Create a new {@link Task} that will read the fileinto memory as an
* {@link javafx.scene.image.Image}
*
* Note: the returned task is suitable for running in a background thread,
* but is not started automatically. Clients are responsible for running the
* task, monitoring its progress, and using its result.
*
* @param file the file to read as an Image
*
* @return a new Task that returns an Image as its result
*/
public static Task<javafx.scene.image.Image> newReadImageTask(AbstractFile file) {
return new ReadImageTask(file);
}
/**
* A task that reads the content of a AbstractFile as a javafx Image.
*/
static private class ReadImageTask extends ReadImageTaskBase {
ReadImageTask(AbstractFile file) {
super(file);
updateMessage(Bundle.LoadImageTask_mesageText(file.getName()));
}
@Override
@NbBundle.Messages({
"# {0} - file name",
"LoadImageTask.mesageText=Reading image: {0}"})
protected javafx.scene.image.Image call() throws Exception {
return readImage();
}
}
/**
* Base class for tasks that need to read AbstractFiles as Images.
*/
static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
private static final String IMAGE_IO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NOI18N
final AbstractFile file;
private ImageReader reader;
ReadImageTaskBase(AbstractFile file) {
this.file = file;
}
protected javafx.scene.image.Image readImage() throws IOException {
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
if (ImageUtils.isGIF(file)) {
//use JavaFX to directly read GIF to preserve potential animation,
javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(inputStream));
if (image.isError() == false) {
return image;
}
//fall through to default image reading code if there was an error
}
try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
if (input == null) {
throw new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM);
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
//we use the first ImageReader, is there any point to trying the others?
if (readers.hasNext()) {
reader = readers.next();
reader.addIIOReadProgressListener(this);
reader.setInput(input);
/*
* This is the important part, get or create a
* ImageReadParam, create a destination image to hold
* the decoded result, then pass that image with the
* param.
*/
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
param.setDestination(bufferedImage);
try {
bufferedImage = reader.read(0, param); //should always be same bufferedImage object
if (isCancelled()) {
return null;
}
} catch (IOException iOException) {
// Ignore this exception or display a warning or similar, for exceptions happening during decoding
LOGGER.log(Level.WARNING, IMAGE_IO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N
} finally {
reader.removeIIOReadProgressListener(this);
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, null);
} else {
throw new IIOException(NO_IMAGE_READER_FOUND_FOR_ + ImageUtils.getContentPathSafe(file));
}
}
}
}
@Override
public void imageProgress(ImageReader source, float percentageDone) {
//update this task with the progress reported by ImageReader.read
updateProgress(percentageDone, 100);
if (isCancelled()) {
reader.abort();
reader.dispose();
}
}
@Override
protected void succeeded() {
super.succeeded();
try {
javafx.scene.image.Image fxImage = get();
if (fxImage == null) {
LOGGER.log(Level.WARNING, IMAGE_IO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT, ImageUtils.getContentPathSafe(file));
} else {
if (fxImage.isError()) {
//if there was somekind of error, log it
LOGGER.log(Level.WARNING, IMAGE_IO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file));
}
}
} catch (InterruptedException | ExecutionException ex) {
failed();
}
}
@Override
protected void failed() {
super.failed();
LOGGER.log(Level.WARNING, IMAGE_IO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file));
}
@Override
public void imageComplete(ImageReader source) {
updateProgress(100, 100);
}
@Override
public void imageStarted(ImageReader source, int imageIndex) {
}
@Override
public void sequenceStarted(ImageReader source, int minIndex) {
}
@Override
public void sequenceComplete(ImageReader source) {
}
@Override
public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
}
@Override
public void thumbnailProgress(ImageReader source, float percentageDone) {
}
@Override
public void thumbnailComplete(ImageReader source) {
}
@Override
public void readAborted(ImageReader source) {
}
}
/**
* Get the unique path for the content, or if that fails, just return the
* name.
*
* @param content
*
* @return
*/
private static String getContentPathSafe(Content content) {
try {
return content.getUniquePath();
} catch (TskCoreException tskCoreException) {
String contentName = content.getName();
LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NOI18N
return contentName;
}
}
} }

View File

@ -23,4 +23,6 @@ PhotoRecIngestModule.NotEnoughDiskSpace.detail.msg=PhotoRec error processing {0}
PhotoRecIngestModule.cancelledByUser=PhotoRec cancelled by user. PhotoRecIngestModule.cancelledByUser=PhotoRec cancelled by user.
PhotoRecIngestModule.error.exitValue=PhotoRec carver returned error exit value = {0} when scanning {1} PhotoRecIngestModule.error.exitValue=PhotoRec carver returned error exit value = {0} when scanning {1}
PhotoRecIngestModule.error.msg=Error processing {0} with PhotoRec carver. PhotoRecIngestModule.error.msg=Error processing {0} with PhotoRec carver.
PhotoRecIngestModule.complete.numberOfErrors=Number of Errors while Carving\: PhotoRecIngestModule.complete.numberOfErrors=Number of Errors while Carving\:
PhotoRecIngestModule.PermissionsNotSufficient=Insufficient permissions accessing
PhotoRecIngestModule.PermissionsNotSufficientSeeReference=See "Shared Drive Authentication" in Autopsy help.

View File

@ -77,6 +77,7 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule {
private static final String PHOTOREC_REPORT = "report.xml"; //NON-NLS private static final String PHOTOREC_REPORT = "report.xml"; //NON-NLS
private static final String LOG_FILE = "run_log.txt"; //NON-NLS private static final String LOG_FILE = "run_log.txt"; //NON-NLS
private static final String TEMP_DIR_NAME = "temp"; // NON-NLS private static final String TEMP_DIR_NAME = "temp"; // NON-NLS
private static final String SEP = System.getProperty("line.separator");
private static final Logger logger = Logger.getLogger(PhotoRecCarverFileIngestModule.class.getName()); private static final Logger logger = Logger.getLogger(PhotoRecCarverFileIngestModule.class.getName());
private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs = new HashMap<>(); private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs = new HashMap<>();
private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter(); private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter();
@ -377,15 +378,21 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule {
*/ */
synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException { synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException {
Path path = Paths.get(Case.getCurrentCase().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName()); Path path = Paths.get(Case.getCurrentCase().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName());
if (UNCPathUtilities.isUNC(path)) {
// if the UNC path is using an IP address, convert to hostname
path = uncPathUtilities.ipToHostName(path);
if (path == null) {
throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.nonHostnameUNCPathUsed"));
}
}
try { try {
Files.createDirectory(path); Files.createDirectory(path);
if (UNCPathUtilities.isUNC(path)) {
// if the UNC path is using an IP address, convert to hostname
path = uncPathUtilities.ipToHostName(path);
if (path == null) {
throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.nonHostnameUNCPathUsed"));
}
if (false == FileUtil.hasReadWriteAccess(path)) {
throw new IngestModule.IngestModuleException(
NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.PermissionsNotSufficient")
+ SEP + path.toString() + SEP // SEP is line breaks to make the dialog display nicely.
+ NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.PermissionsNotSufficientSeeReference"));
}
}
} catch (FileAlreadyExistsException ex) { } catch (FileAlreadyExistsException ex) {
// No worries. // No worries.
} catch (IOException | SecurityException | UnsupportedOperationException ex) { } catch (IOException | SecurityException | UnsupportedOperationException ex) {

View File

@ -391,6 +391,7 @@ public class DetailViewPane extends AbstractVisualizationPane<DateTime, EventStr
"DetailViewPane.loggedTask.queryDb=Retreiving event data", "DetailViewPane.loggedTask.queryDb=Retreiving event data",
"DetailViewPane.loggedTask.name=Updating Details View", "DetailViewPane.loggedTask.name=Updating Details View",
"DetailViewPane.loggedTask.updateUI=Populating visualization", "DetailViewPane.loggedTask.updateUI=Populating visualization",
"# {0} - number of events",
"DetailViewPane.loggedTask.prompt=You are about to show details for {0} events. This might be very slow or even crash Autopsy.\n\nDo you want to continue?"}) "DetailViewPane.loggedTask.prompt=You are about to show details for {0} events. This might be very slow or even crash Autopsy.\n\nDo you want to continue?"})
private class DetailsUpdateTask extends VisualizationUpdateTask<Interval> { private class DetailsUpdateTask extends VisualizationUpdateTask<Interval> {

View File

@ -30,14 +30,16 @@ import java.net.MalformedURLException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.beans.Observable;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.WritableImage; import javafx.scene.image.WritableImage;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.openide.util.Exceptions;
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.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
@ -64,7 +66,7 @@ public enum ThumbnailCache {
* in memory cache. keeps at most 1000 items each for up to 10 minutes. * in memory cache. keeps at most 1000 items each for up to 10 minutes.
* items may be garbage collected if there are no strong references to them. * items may be garbage collected if there are no strong references to them.
*/ */
private final Cache<Long, Optional<Image>> cache = CacheBuilder.newBuilder() private final Cache<Long, Image> cache = CacheBuilder.newBuilder()
.maximumSize(1000) .maximumSize(1000)
.softValues() .softValues()
.expireAfterAccess(10, TimeUnit.MINUTES).build(); .expireAfterAccess(10, TimeUnit.MINUTES).build();
@ -97,9 +99,9 @@ public enum ThumbnailCache {
@Nullable @Nullable
public Image get(DrawableFile<?> file) { public Image get(DrawableFile<?> file) {
try { try {
return cache.get(file.getId(), () -> load(file)).orElse(null); return cache.get(file.getId(), () -> load(file));
} catch (UncheckedExecutionException | CacheLoader.InvalidCacheLoadException | ExecutionException ex) { } catch (UncheckedExecutionException | CacheLoader.InvalidCacheLoadException | ExecutionException ex) {
LOGGER.log(Level.WARNING, "failed to load icon for file: " + file.getName(), ex.getCause()); LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + file.getName(), ex.getCause());
return null; return null;
} }
} }
@ -109,7 +111,7 @@ public enum ThumbnailCache {
try { try {
return get(ImageGalleryController.getDefault().getFileFromId(fileID)); return get(ImageGalleryController.getDefault().getFileFromId(fileID));
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "failed to load icon for file id : " + fileID, ex.getCause()); LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause());
return null; return null;
} }
} }
@ -122,35 +124,32 @@ public enum ThumbnailCache {
* *
* @return an (possibly empty) optional containing a thumbnail * @return an (possibly empty) optional containing a thumbnail
*/ */
private Optional<Image> load(DrawableFile<?> file) { private Image load(DrawableFile<?> file) {
if (FileTypeUtils.isGIF(file)) { if (FileTypeUtils.isGIF(file)) {
//directly read gif to preserve potential animation, //directly read gif to preserve potential animation,
//NOTE: not saved to disk! //NOTE: not saved to disk!
return Optional.of(new Image(new BufferedInputStream(new ReadContentInputStream(file.getAbstractFile())), MAX_THUMBNAIL_SIZE, MAX_THUMBNAIL_SIZE, true, true)); return new Image(new BufferedInputStream(new ReadContentInputStream(file.getAbstractFile())), MAX_THUMBNAIL_SIZE, MAX_THUMBNAIL_SIZE, true, true);
} }
BufferedImage thumbnail = getCacheFile(file).map(new Function<File, BufferedImage>() { BufferedImage thumbnail = getCacheFile(file).map(cachFile -> {
@Override if (cachFile.exists()) {
public BufferedImage apply(File cachFile) { // If a thumbnail file is already saved locally, load it
if (cachFile.exists()) { try {
// If a thumbnail file is already saved locally, load it BufferedImage cachedThumbnail = ImageIO.read(cachFile);
try {
BufferedImage cachedThumbnail = ImageIO.read(cachFile);
if (cachedThumbnail.getWidth() < MAX_THUMBNAIL_SIZE) { if (cachedThumbnail.getWidth() < MAX_THUMBNAIL_SIZE) {
return cachedThumbnail; return cachedThumbnail;
}
} catch (MalformedURLException ex) {
LOGGER.log(Level.WARNING, "Unable to parse cache file path: " + cachFile.getPath(), ex);
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to read cache file " + cachFile.getPath(), ex);
} }
} catch (MalformedURLException ex) {
LOGGER.log(Level.WARNING, "Unable to parse cache file path: " + cachFile.getPath(), ex);
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to read cache file " + cachFile.getPath(), ex);
} }
return null;
} }
return null;
}).orElseGet(() -> { }).orElseGet(() -> {
return (BufferedImage) ImageUtils.getThumbnail(file.getAbstractFile(), MAX_THUMBNAIL_SIZE); return ImageUtils.getThumbnail(file.getAbstractFile(), MAX_THUMBNAIL_SIZE);
}); });
WritableImage jfxthumbnail; WritableImage jfxthumbnail;
@ -161,7 +160,7 @@ public enum ThumbnailCache {
jfxthumbnail = SwingFXUtils.toFXImage(thumbnail, null); jfxthumbnail = SwingFXUtils.toFXImage(thumbnail, null);
} }
return Optional.ofNullable(jfxthumbnail); //return icon, or null if generation failed return jfxthumbnail; //return icon, or null if generation failed
} }
/** /**
@ -169,16 +168,40 @@ public enum ThumbnailCache {
* *
* @param id the obj id of the file to get a cache file for * @param id the obj id of the file to get a cache file for
* *
* @return a Optional containing a File to store the cahced icon in or an * @return a Optional containing a File to store the cached icon in or an
* empty optional if there was a problem. * empty optional if there was a problem.
*/ */
private static Optional<File> getCacheFile(DrawableFile<?> file) { private static Optional<File> getCacheFile(DrawableFile<?> file) {
try { try {
return Optional.of(ImageUtils.getCachedThumbnailFile(file.getAbstractFile(), MAX_THUMBNAIL_SIZE)); return Optional.of(ImageUtils.getCachedThumbnailFile(file.getAbstractFile(), MAX_THUMBNAIL_SIZE));
} catch (IllegalStateException e) { } catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to create cache file.{0}", e.getLocalizedMessage()); LOGGER.log(Level.WARNING, "Failed to create cache file.{0}", e.getLocalizedMessage());
return Optional.empty(); return Optional.empty();
} }
} }
public Task<Image> getThumbnailTask(DrawableFile<?> file) {
final Image thumbnail = cache.getIfPresent(file.getId());
if (thumbnail != null) {
return new Task<Image>() {
@Override
protected Image call() throws Exception {
return thumbnail;
}
};
}
final Task<Image> newGetThumbnailTask = ImageUtils.newGetThumbnailTask(file.getAbstractFile(), MAX_THUMBNAIL_SIZE, false);
newGetThumbnailTask.stateProperty().addListener((Observable observable) -> {
switch (newGetThumbnailTask.getState()) {
case SUCCEEDED:
try {
cache.put(Long.MIN_VALUE, newGetThumbnailTask.get());
} catch (InterruptedException | ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
}
});
return newGetThumbnailTask;
}
} }

View File

@ -0,0 +1,56 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.actions;
import java.awt.event.ActionEvent;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.swing.SwingUtilities;
import org.controlsfx.control.action.Action;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
import org.sleuthkit.datamodel.AbstractFile;
/**
* Wraps {@link ExternalViewerAction} in a ControlsFX {@link Action} with
* appropriate text and graphic
*/
@NbBundle.Messages({"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer"})
public class OpenExternalViewerAction extends Action {
private static final Image EXTERNAL = new Image(OpenExternalViewerAction.class.getResource("/org/sleuthkit/autopsy/imagegallery/images/external.png").toExternalForm());
private static final ActionEvent ACTION_EVENT = new ActionEvent(OpenExternalViewerAction.class, ActionEvent.ACTION_PERFORMED, ""); //Swing ActionEvent //NOI18N
public OpenExternalViewerAction(AbstractFile file) {
super("External Viewer");
/**
* TODO: why is the name passed to the action? it means we duplicate
* this string all over the place -jm
*/
ExternalViewerAction externalViewerAction = new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file));
setLongText(Bundle.MediaViewImagePanel_externalViewerButton_text());
setEventHandler(actionEvent -> //fx ActionEvent
SwingUtilities.invokeLater(() -> externalViewerAction.actionPerformed(ACTION_EVENT))
);
setGraphic(new ImageView(EXTERNAL));
}
}

View File

@ -23,12 +23,13 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.util.Pair; import javafx.util.Pair;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -38,6 +39,7 @@ import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; import org.sleuthkit.autopsy.imagegallery.ThumbnailCache;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute;
@ -77,6 +79,7 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
} }
SoftReference<Image> imageRef; SoftReference<Image> imageRef;
// SoftReference<Image> thumbref;
private String drawablePath; private String drawablePath;
@ -91,12 +94,36 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
private String model; private String model;
protected DrawableFile(T file, Boolean analyzed) { protected DrawableFile(T file, Boolean analyzed) {
/* @TODO: the two 'new Integer(0).shortValue()' values and null are /*
* @TODO: the two 'new Integer(0).shortValue()' values and null are
* placeholders because the super constructor expects values i can't get * placeholders because the super constructor expects values i can't get
* easily at the moment. I assume this is related to why * easily at the moment. I assume this is related to why
* ReadContentInputStream can't read from DrawableFiles. */ * ReadContentInputStream can't read from DrawableFiles.
*/
super(file.getSleuthkitCase(), file.getId(), file.getAttrType(), file.getAttrId(), file.getName(), file.getType(), file.getMetaAddr(), (int) file.getMetaSeq(), file.getDirType(), file.getMetaType(), null, new Integer(0).shortValue(), file.getSize(), file.getCtime(), file.getCrtime(), file.getAtime(), file.getMtime(), new Integer(0).shortValue(), file.getUid(), file.getGid(), file.getMd5Hash(), file.getKnown(), file.getParentPath()); super(file.getSleuthkitCase(),
file.getId(),
file.getAttrType(),
file.getAttrId(),
file.getName(),
file.getType(),
file.getMetaAddr(),
(int) file.getMetaSeq(),
file.getDirType(),
file.getMetaType(),
null,
new Integer(0).shortValue(),
file.getSize(),
file.getCtime(),
file.getCrtime(),
file.getAtime(),
file.getMtime(),
new Integer(0).shortValue(),
file.getUid(),
file.getGid(),
file.getMd5Hash(),
file.getKnown(),
file.getParentPath());
this.analyzed = new SimpleBooleanProperty(analyzed); this.analyzed = new SimpleBooleanProperty(analyzed);
this.file = file; this.file = file;
} }
@ -213,7 +240,9 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
return category; return category;
} }
/** set the category property to the most severe one found */ /**
* set the category property to the most severe one found
*/
private void updateCategory() { private void updateCategory() {
try { try {
category.set(getSleuthkitCase().getContentTagsByContent(this).stream() category.set(getSleuthkitCase().getContentTagsByContent(this).stream()
@ -224,17 +253,59 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
.orElse(Category.ZERO) .orElse(Category.ZERO)
); );
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "problem looking up category for file " + this.getName(), ex); LOGGER.log(Level.WARNING, "problem looking up category for file " + this.getName() + ex.getLocalizedMessage());
} catch (IllegalStateException ex) { } catch (IllegalStateException ex) {
// We get here many times if the case is closed during ingest, so don't print out a ton of warnings. // We get here many times if the case is closed during ingest, so don't print out a ton of warnings.
} }
} }
@Deprecated
public Image getThumbnail() { public Image getThumbnail() {
return ThumbnailCache.getDefault().get(this); try {
return getThumbnailTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
} }
public abstract Image getFullSizeImage(); public Task<Image> getThumbnailTask() {
return ThumbnailCache.getDefault().getThumbnailTask(this);
}
@Deprecated //use non-blocking getReadFullSizeImageTask instead for most cases
public Image getFullSizeImage() {
try {
return getReadFullSizeImageTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
}
public Task<Image> getReadFullSizeImageTask() {
Image image = (imageRef != null) ? imageRef.get() : null;
if (image == null || image.isError()) {
Task<Image> readImageTask = getReadFullSizeImageTaskHelper();
readImageTask.stateProperty().addListener(stateProperty -> {
switch (readImageTask.getState()) {
case SUCCEEDED:
try {
imageRef = new SoftReference<>(readImageTask.get());
} catch (InterruptedException | ExecutionException exception) {
LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
}
break;
}
});
return readImageTask;
} else {
return TaskUtils.taskFrom(() -> image);
}
}
abstract String getMessageTemplate(Exception exception);
abstract Task<Image> getReadFullSizeImageTaskHelper();
public void setAnalyzed(Boolean analyzed) { public void setAnalyzed(Boolean analyzed) {
this.analyzed.set(analyzed); this.analyzed.set(analyzed);
@ -266,11 +337,6 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
} }
} }
public boolean isDisplayableAsImage() {
Image thumbnail = getThumbnail();
return Objects.nonNull(thumbnail) && thumbnail.errorProperty().get() == false;
}
@Nonnull @Nonnull
public Set<String> getHashSetNamesUnchecked() { public Set<String> getHashSetNamesUnchecked() {
try { try {
@ -280,4 +346,22 @@ public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile
return Collections.emptySet(); return Collections.emptySet();
} }
} }
/**
* Get the unique path for this DrawableFile, or if that fails, just return
* the name.
*
* @param content
*
* @return
*/
public String getContentPathSafe() {
try {
return this.getUniquePath();
} catch (TskCoreException tskCoreException) {
String contentName = this.getName();
LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NOI18N
return contentName;
}
}
} }

View File

@ -18,17 +18,13 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.datamodel; package org.sleuthkit.autopsy.imagegallery.datamodel;
import java.awt.image.BufferedImage; import java.io.IOException;
import java.io.BufferedInputStream; import javafx.concurrent.Task;
import java.lang.ref.SoftReference;
import java.util.logging.Level;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.sleuthkit.autopsy.coreutils.ImageUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.ReadContentInputStream;
/** /**
* ImageGallery data model object that represents an image file. It is a * ImageGallery data model object that represents an image file. It is a
@ -49,43 +45,31 @@ public class ImageFile<T extends AbstractFile> extends DrawableFile<T> {
} }
@Override @Override
public Image getFullSizeImage() { String getMessageTemplate(final Exception exception) {
Image image = (imageRef != null) ? imageRef.get() : null; return "Failed to read image {0}: " + exception.toString();
if (image == null || image.isError()) { }
if (FileTypeUtils.isGIF(getAbstractFile())) {
//directly read gif to preserve potential animation, @Override
image = new Image(new BufferedInputStream(new ReadContentInputStream(getAbstractFile()))); Task<Image> getReadFullSizeImageTaskHelper() {
} return ImageUtils.newReadImageTask(this.getAbstractFile());
}
if (image == null || image.isError()) {
try (BufferedInputStream readContentInputStream = new BufferedInputStream(new ReadContentInputStream(this.getAbstractFile()))) {
BufferedImage read = ImageIO.read(readContentInputStream);
image = SwingFXUtils.toFXImage(read, null);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "unable to read file " + getName(), ex.getMessage());
return null;
}
}
imageRef = new SoftReference<>(image);
return image;
} }
@Override @Override
Double getWidth() { Double getWidth() {
final Image fullSizeImage = getFullSizeImage(); try {
if (fullSizeImage != null) { return (double) ImageUtils.getImageWidth(this.getAbstractFile());
return fullSizeImage.getWidth(); } catch (IOException ex) {
return -1.0;
} }
return -1.0;
} }
@Override @Override
Double getHeight() { Double getHeight() {
final Image fullSizeImage = getFullSizeImage(); try {
if (fullSizeImage != null) { return (double) ImageUtils.getImageHeight(this.getAbstractFile());
return fullSizeImage.getHeight(); } catch (IOException ex) {
return -1.0;
} }
return -1.0;
} }
@Override @Override

View File

@ -19,14 +19,11 @@
package org.sleuthkit.autopsy.imagegallery.datamodel; package org.sleuthkit.autopsy.imagegallery.datamodel;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Objects; import javafx.concurrent.Task;
import java.util.logging.Level;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.media.Media; import javafx.scene.media.Media;
import javafx.scene.media.MediaException; import javafx.scene.media.MediaException;
@ -40,6 +37,8 @@ import org.sleuthkit.datamodel.AbstractFile;
public class VideoFile<T extends AbstractFile> extends DrawableFile<T> { public class VideoFile<T extends AbstractFile> extends DrawableFile<T> {
private static final Logger LOGGER = Logger.getLogger(VideoFile.class.getName());
private static final Image VIDEO_ICON = new Image("org/sleuthkit/autopsy/imagegallery/images/Clapperboard.png"); private static final Image VIDEO_ICON = new Image("org/sleuthkit/autopsy/imagegallery/images/Clapperboard.png");
VideoFile(T file, Boolean analyzed) { VideoFile(T file, Boolean analyzed) {
@ -50,17 +49,16 @@ public class VideoFile<T extends AbstractFile> extends DrawableFile<T> {
return VIDEO_ICON; return VIDEO_ICON;
} }
@Override @Override
public Image getFullSizeImage() { String getMessageTemplate(final Exception exception) {
Image image = (null == imageRef) ? null : imageRef.get(); return "Failed to get image preview for video {0}: " + exception.toString();
}
if (image == null) { @Override
final BufferedImage bufferedImage = (BufferedImage) ImageUtils.getThumbnail(getAbstractFile(), 1024); Task<Image> getReadFullSizeImageTaskHelper() {
image = (bufferedImage == ImageUtils.getDefaultThumbnail()) ? null : SwingFXUtils.toFXImage(bufferedImage, null); return ImageUtils.newGetThumbnailTask(getAbstractFile(), 1024, false);
imageRef = new SoftReference<>(image);
}
return image;
} }
private SoftReference<Media> mediaRef; private SoftReference<Media> mediaRef;
@ -74,13 +72,11 @@ public class VideoFile<T extends AbstractFile> extends DrawableFile<T> {
final File cacheFile = VideoUtils.getTempVideoFile(this.getAbstractFile()); final File cacheFile = VideoUtils.getTempVideoFile(this.getAbstractFile());
if (cacheFile.exists() == false || cacheFile.length() < getAbstractFile().getSize()) { if (cacheFile.exists() == false || cacheFile.length() < getAbstractFile().getSize()) {
Files.createParentDirs(cacheFile); Files.createParentDirs(cacheFile);
ProgressHandle progressHandle = ProgressHandleFactory.createHandle("writing temporary file to disk"); ProgressHandle progressHandle = ProgressHandleFactory.createHandle("writing temporary file to disk");
progressHandle.start(100); progressHandle.start(100);
ContentUtils.writeToFile(this.getAbstractFile(), cacheFile, progressHandle, null, true); ContentUtils.writeToFile(this.getAbstractFile(), cacheFile, progressHandle, null, true);
progressHandle.finish(); progressHandle.finish();
} }
media = new Media(Paths.get(cacheFile.getAbsolutePath()).toUri().toString()); media = new Media(Paths.get(cacheFile.getAbsolutePath()).toUri().toString());
@ -89,18 +85,6 @@ public class VideoFile<T extends AbstractFile> extends DrawableFile<T> {
} }
public boolean isDisplayableAsMedia() {
try {
Media media = getMedia();
return Objects.nonNull(media) && Objects.isNull(media.getError());
} catch (IOException ex) {
Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "failed to write video to cache for playback.", ex);
return false;
} catch (MediaException ex) {
return false;
}
}
@Override @Override
Double getWidth() { Double getWidth() {
try { try {

View File

@ -34,11 +34,6 @@
<Insets bottom="1.0" left="1.0" right="1.0" top="1.0" fx:id="x1" /> <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" fx:id="x1" />
</HBox.margin> </HBox.margin>
</ImageView> </ImageView>
<ImageView fx:id="undisplayableImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/prohibition.png" />
</image>
</ImageView>
</children> </children>
<padding> <padding>
<Insets bottom="2.0" right="2.0" top="2.0" /> <Insets bottom="2.0" right="2.0" top="2.0" />

View File

@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.CacheHint; import javafx.scene.CacheHint;
import javafx.scene.control.Control; import javafx.scene.control.Control;
@ -90,8 +91,8 @@ public class DrawableTile extends DrawableTileBase {
} }
@Override @Override
CachedLoaderTask<Image, DrawableFile<?>> getNewImageLoadTask(DrawableFile<?> file) { Task<Image> newReadImageTask(DrawableFile<?> file) {
return new ThumbnailLoaderTask(file); return file.getThumbnailTask();
} }
@Override @Override
@ -99,4 +100,6 @@ public class DrawableTile extends DrawableTileBase {
return getFile().map(AbstractContent::getName).orElse(""); return getFile().map(AbstractContent::getName).orElse("");
} }
} }

View File

@ -47,6 +47,7 @@ import javafx.scene.layout.Region;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.Lookup; import org.openide.util.Lookup;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.actions.Presenter; import org.openide.util.actions.Presenter;
@ -58,7 +59,6 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.ExtractAction;
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
@ -67,10 +67,10 @@ import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction; import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction;
import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction; import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction;
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter; import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.VideoFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode;
import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
@ -111,8 +111,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
@FXML @FXML
private ImageView hashHitImageView; private ImageView hashHitImageView;
@FXML
protected ImageView undisplayableImageView;
/** /**
* displays the icon representing follow up tag * displays the icon representing follow up tag
*/ */
@ -221,15 +220,10 @@ public abstract class DrawableTileBase extends DrawableUIBase {
}); });
menuItems.add(contentViewer); menuItems.add(contentViewer);
MenuItem externalViewer = new MenuItem(Bundle.DrawableTileBase_externalViewerAction_text()); OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file.getAbstractFile());
final ExternalViewerAction externalViewerAction = new ExternalViewerAction(Bundle.DrawableTileBase_externalViewerAction_text(), new FileNode(file.getAbstractFile())); MenuItem externalViewer = ActionUtils.createMenuItem(openExternalViewerAction);
externalViewer.textProperty().unbind();
externalViewer.setDisable(externalViewerAction.isEnabled() == false); externalViewer.textProperty().bind(openExternalViewerAction.longTextProperty());
externalViewer.setOnAction((ActionEvent t) -> {
SwingUtilities.invokeLater(() -> {
externalViewerAction.actionPerformed(null);
});
});
menuItems.add(externalViewer); menuItems.add(externalViewer);
Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class); Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
@ -311,16 +305,16 @@ public abstract class DrawableTileBase extends DrawableUIBase {
updateSelectionState(); updateSelectionState();
updateCategory(); updateCategory();
updateFollowUpIcon(); updateFollowUpIcon();
updateUI();
updateContent(); updateContent();
updateMetaData();
} }
} }
private void updateUI() { private void updateMetaData() {
getFile().ifPresent(file -> { getFile().ifPresent(file -> {
final boolean isVideo = file.isVideo(); final boolean isVideo = file.isVideo();
final boolean hasHashSetHits = hasHashHit(); final boolean hasHashSetHits = hasHashHit();
final boolean isUndisplayable = (isVideo ? ((VideoFile<?>) file).isDisplayableAsMedia() : file.isDisplayableAsImage()) == false;
final String text = getTextForLabel(); final String text = getTextForLabel();
Platform.runLater(() -> { Platform.runLater(() -> {
@ -328,8 +322,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
fileTypeImageView.setVisible(isVideo); fileTypeImageView.setVisible(isVideo);
hashHitImageView.setManaged(hasHashSetHits); hashHitImageView.setManaged(hasHashSetHits);
hashHitImageView.setVisible(hasHashSetHits); hashHitImageView.setVisible(hasHashSetHits);
undisplayableImageView.setManaged(isUndisplayable);
undisplayableImageView.setVisible(isUndisplayable);
nameLabel.setText(text); nameLabel.setText(text);
nameLabel.setTooltip(new Tooltip(text)); nameLabel.setTooltip(new Tooltip(text));
}); });

View File

@ -18,47 +18,58 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import java.lang.ref.SoftReference;
import java.util.Objects; import java.util.Objects;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
* *
*/ */
@NbBundle.Messages({"MediaViewImagePanel.errorLabel.text=Could not read file."})
abstract public class DrawableUIBase extends AnchorPane implements DrawableView { abstract public class DrawableUIBase extends AnchorPane implements DrawableView {
static final Executor exec = Executors.newWorkStealingPool();
private static final Logger LOGGER = Logger.getLogger(DrawableUIBase.class.getName()); private static final Logger LOGGER = Logger.getLogger(DrawableUIBase.class.getName());
@FXML @FXML
protected BorderPane imageBorder; BorderPane imageBorder;
@FXML @FXML
protected ImageView imageView; ImageView imageView;
private final ImageGalleryController controller; private final ImageGalleryController controller;
private Optional<DrawableFile<?>> fileOpt = Optional.empty(); private Optional<DrawableFile<?>> fileOpt = Optional.empty();
private Optional<Long> fileIDOpt = Optional.empty(); private Optional<Long> fileIDOpt = Optional.empty();
private Task<Image> imageTask; private volatile Task<Image> imageTask;
private SoftReference<Image> imageCache;
private ProgressIndicator progressIndicator;
public DrawableUIBase(ImageGalleryController controller) { public DrawableUIBase(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
@ -107,132 +118,97 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
synchronized public void setFile(Long newFileID) { synchronized public void setFile(Long newFileID) {
if (getFileID().isPresent()) { if (getFileID().isPresent()) {
if (Objects.equals(newFileID, getFileID().get()) == false) { if (Objects.equals(newFileID, getFileID().get()) == false) {
if (Objects.nonNull(newFileID)) { setFileHelper(newFileID);
setFileHelper(newFileID);
}
} }
} else if (Objects.nonNull(newFileID)) { } else {
setFileHelper(newFileID); setFileHelper(newFileID);
} }
} }
synchronized protected void updateContent() { synchronized protected void updateContent() {
Node content = getContentNode(); if (getFile().isPresent()) {
Platform.runLater(() -> { doReadImageTask(getFile().get());
imageBorder.setCenter(content); }
}
synchronized Node doReadImageTask(DrawableFile<?> file) {
Task<Image> myTask = newReadImageTask(file);
imageTask = myTask;
Node progressNode = newProgressIndicator(myTask);
Platform.runLater(() -> imageBorder.setCenter(progressNode));
//called on fx thread
myTask.setOnSucceeded(succeeded -> {
showImage(file, myTask);
synchronized (DrawableUIBase.this) {
imageTask = null;
}
}); });
myTask.setOnFailed(failed -> {
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
synchronized (DrawableUIBase.this) {
imageTask = null;
}
});
myTask.setOnCancelled(cancelled -> {
disposeContent();
});
exec.execute(myTask);
return progressNode;
} }
synchronized protected void disposeContent() { synchronized protected void disposeContent() {
if (imageTask != null) { if (imageTask != null) {
imageTask.cancel(true); imageTask.cancel();
} }
imageTask = null; imageTask = null;
imageCache = null; Platform.runLater(() -> {
imageView.setImage(null);
imageBorder.setCenter(null);
});
} }
ProgressIndicator getLoadingProgressIndicator() { /**
if (progressIndicator == null) { *
progressIndicator = new ProgressIndicator(); * @param file the value of file
} * @param imageTask the value of imageTask
return progressIndicator; */
Node newProgressIndicator(final Task<?> imageTask) {
ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1);
loadingProgressIndicator.progressProperty().bind(imageTask.progressProperty());
return loadingProgressIndicator;
} }
Node getContentNode() { @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
if (getFile().isPresent() == false) { private void showImage(DrawableFile<?> file, Task<Image> imageTask) {
imageCache = null; //Note that all error conditions are allready logged in readImageTask.succeeded()
Platform.runLater(() -> { try {
if (imageView != null) { Image fxImage = imageTask.get();
imageView.setImage(null); if (nonNull(fxImage)) {
} //we have non-null image show it
}); imageView.setImage(fxImage);
return null; imageBorder.setCenter(imageView);
} else {
Image thumbnail = isNull(imageCache) ? null : imageCache.get();
if (nonNull(thumbnail)) {
Platform.runLater(() -> {
if (imageView != null) {
imageView.setImage(thumbnail);
}
});
return imageView;
} else { } else {
DrawableFile<?> file = getFile().get(); showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
if (isNull(imageTask)) {
imageTask = getNewImageLoadTask(file);
new Thread(imageTask).start();
} else if (imageTask.isDone()) {
return null;
}
return getLoadingProgressIndicator();
} }
} catch (CancellationException ex) {
} catch (InterruptedException | ExecutionException ex) {
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
} }
} }
abstract CachedLoaderTask<Image, DrawableFile<?>> getNewImageLoadTask(DrawableFile<?> file); @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void showErrorNode(String errorMessage, AbstractFile file) {
Button createButton = ActionUtils.createButton(new OpenExternalViewerAction(file));
abstract class CachedLoaderTask<X, Y extends DrawableFile<?>> extends Task<X> { VBox vBox = new VBox(10,
new Label(errorMessage), createButton);
protected final Y file; vBox.setAlignment(Pos.CENTER);
imageBorder.setCenter(vBox);
public CachedLoaderTask(Y file) {
this.file = file;
}
@Override
protected X call() throws Exception {
return (isCancelled() == false) ? load() : null;
}
abstract X load();
@Override
protected void succeeded() {
super.succeeded();
if (isCancelled() == false) {
try {
saveToCache(get());
updateContent();
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.WARNING, "Failed to cache content for" + file.getName(), ex);
}
}
}
@Override
protected void failed() {
super.failed();
LOGGER.log(Level.SEVERE, "Failed to cache content for" + file.getName(), getException());
}
abstract void saveToCache(X result);
} }
abstract class ImageLoaderTask extends CachedLoaderTask<Image, DrawableFile<?>> { abstract Task<Image> newReadImageTask(DrawableFile<?> file);
public ImageLoaderTask(DrawableFile<?> file) {
super(file);
}
@Override
void saveToCache(Image result) {
synchronized (DrawableUIBase.this) {
imageCache = new SoftReference<>(result);
}
}
}
class ThumbnailLoaderTask extends ImageLoaderTask {
public ThumbnailLoaderTask(DrawableFile<?> file) {
super(file);
}
@Override
Image load() {
return isCancelled() ? null : file.getThumbnail();
}
}
} }

View File

@ -27,11 +27,11 @@ import java.util.Objects;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.Optional; import java.util.Optional;
import org.sleuthkit.autopsy.coreutils.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -52,6 +52,7 @@ import javafx.scene.text.Text;
import javafx.util.Pair; import javafx.util.Pair;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
@ -166,36 +167,37 @@ public class MetaDataPane extends DrawableUIBase {
} }
@Override @Override
protected synchronized void setFileHelper(Long newFileID) { synchronized protected void setFileHelper(Long newFileID) {
setFileIDOpt(Optional.ofNullable(newFileID)); setFileIDOpt(Optional.ofNullable(newFileID));
if (newFileID == null) { disposeContent();
Platform.runLater(() -> { if (nonNull(newFileID)) {
imageView.setImage(null); updateAttributesTable();
imageBorder.setCenter(null); updateCategory();
tableView.getItems().clear();
getCategoryBorderRegion().setBorder(null);
});
} else {
disposeContent();
updateUI();
updateContent(); updateContent();
} }
} }
@Override @Override
CachedLoaderTask<Image, DrawableFile<?>> getNewImageLoadTask(DrawableFile<?> file) { protected synchronized void disposeContent() {
return new ThumbnailLoaderTask(file); super.disposeContent();
Platform.runLater(() -> {
tableView.getItems().clear();
getCategoryBorderRegion().setBorder(null);
});
} }
public void updateUI() { @Override
Task<Image> newReadImageTask(DrawableFile<?> file) {
return file.getThumbnailTask();
}
public void updateAttributesTable() {
getFile().ifPresent(file -> { getFile().ifPresent(file -> {
final List<Pair<DrawableAttribute<?>, Collection<?>>> attributesList = file.getAttributesList(); final List<Pair<DrawableAttribute<?>, Collection<?>>> attributesList = file.getAttributesList();
Platform.runLater(() -> { Platform.runLater(() -> {
tableView.getItems().clear(); tableView.getItems().clear();
tableView.getItems().setAll(attributesList); tableView.getItems().setAll(attributesList);
}); });
updateCategory();
}); });
} }
@ -204,13 +206,15 @@ public class MetaDataPane extends DrawableUIBase {
return imageBorder; return imageBorder;
} }
/** {@inheritDoc } */ /**
* {@inheritDoc }
*/
@Subscribe @Subscribe
@Override @Override
public void handleCategoryChanged(CategoryManager.CategoryChangeEvent evt) { public void handleCategoryChanged(CategoryManager.CategoryChangeEvent evt) {
getFileID().ifPresent(fileID -> { getFileID().ifPresent(fileID -> {
if (evt.getFileIDs().contains(fileID)) { if (evt.getFileIDs().contains(fileID)) {
updateUI(); updateAttributesTable();
} }
}); });
} }
@ -220,7 +224,7 @@ public class MetaDataPane extends DrawableUIBase {
public void handleTagAdded(ContentTagAddedEvent evt) { public void handleTagAdded(ContentTagAddedEvent evt) {
getFileID().ifPresent((fileID) -> { getFileID().ifPresent((fileID) -> {
if (Objects.equals(evt.getAddedTag().getContent().getId(), fileID)) { if (Objects.equals(evt.getAddedTag().getContent().getId(), fileID)) {
updateUI(); updateAttributesTable();
} }
}); });
} }
@ -229,7 +233,7 @@ public class MetaDataPane extends DrawableUIBase {
public void handleTagDeleted(ContentTagDeletedEvent evt) { public void handleTagDeleted(ContentTagDeletedEvent evt) {
getFileID().ifPresent((fileID) -> { getFileID().ifPresent((fileID) -> {
if (Objects.equals(evt.getDeletedTagInfo().getContentID(), fileID)) { if (Objects.equals(evt.getDeletedTagInfo().getContentID(), fileID)) {
updateUI(); updateAttributesTable();
} }
}); });
} }
@ -241,4 +245,5 @@ public class MetaDataPane extends DrawableUIBase {
getValueDisplayString(selectedItem))); getValueDisplayString(selectedItem)));
} }
} }
} }

View File

@ -55,11 +55,6 @@
<Insets bottom="1.0" left="1.0" right="1.0" top="1.0" /> <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" />
</HBox.margin> </HBox.margin>
</ImageView> </ImageView>
<ImageView fx:id="undisplayableImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/prohibition.png" />
</image>
</ImageView>
</children> </children>
<padding> <padding>
<Insets bottom="2.0" right="2.0" top="2.0" /> <Insets bottom="2.0" right="2.0" top="2.0" />

View File

@ -19,10 +19,9 @@
package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.SoftReference;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.Observable; import javafx.beans.Observable;
@ -32,6 +31,8 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import static javafx.scene.input.KeyCode.LEFT; import static javafx.scene.input.KeyCode.LEFT;
import static javafx.scene.input.KeyCode.RIGHT; import static javafx.scene.input.KeyCode.RIGHT;
@ -40,7 +41,8 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.media.Media; import javafx.scene.media.Media;
import javafx.scene.media.MediaException; import javafx.scene.media.MediaException;
import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaPlayer;
import javafx.scene.text.Text; import org.controlsfx.control.MaskerPane;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
@ -50,6 +52,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.VideoFile; import org.sleuthkit.autopsy.imagegallery.datamodel.VideoFile;
import org.sleuthkit.autopsy.imagegallery.gui.VideoPlayer; import org.sleuthkit.autopsy.imagegallery.gui.VideoPlayer;
import static org.sleuthkit.autopsy.imagegallery.gui.drawableviews.DrawableUIBase.exec;
import static org.sleuthkit.autopsy.imagegallery.gui.drawableviews.DrawableView.CAT_BORDER_WIDTH; import static org.sleuthkit.autopsy.imagegallery.gui.drawableviews.DrawableView.CAT_BORDER_WIDTH;
/** /**
@ -69,7 +72,8 @@ public class SlideShowView extends DrawableTileBase {
@FXML @FXML
private BorderPane footer; private BorderPane footer;
private Task<Node> mediaTask;
private volatile MediaLoadTask mediaTask;
SlideShowView(GroupPane gp, ImageGalleryController controller) { SlideShowView(GroupPane gp, ImageGalleryController controller) {
super(gp, controller); super(gp, controller);
@ -155,46 +159,94 @@ public class SlideShowView extends DrawableTileBase {
} }
@Override @Override
protected void disposeContent() { synchronized protected void disposeContent() {
stopVideo(); stopVideo();
super.disposeContent();
if (mediaTask != null) { if (mediaTask != null) {
mediaTask.cancel(true); mediaTask.cancel(true);
} }
mediaTask = null; mediaTask = null;
mediaCache = null; super.disposeContent();
} }
private SoftReference<Node> mediaCache;
/**
* {@inheritDoc }
*/
@Override @Override
Node getContentNode() { synchronized protected void updateContent() {
if (getFile().isPresent() == false) { disposeContent();
mediaCache = null; if (getFile().isPresent()) {
return super.getContentNode();
} else {
DrawableFile<?> file = getFile().get(); DrawableFile<?> file = getFile().get();
if (file.isVideo()) { if (file.isVideo()) {
Node mediaNode = (isNull(mediaCache)) ? null : mediaCache.get(); doMediaLoadTask((VideoFile<?>) file);
if (nonNull(mediaNode)) { } else {
return mediaNode; doReadImageTask(file);
} else {
if (isNull(mediaTask)) {
mediaTask = new MediaLoadTask(((VideoFile<?>) file));
new Thread(mediaTask).start();
} else if (mediaTask.isDone()) {
return null;
}
return getLoadingProgressIndicator();
}
} }
return super.getContentNode();
} }
} }
synchronized private Node doMediaLoadTask(VideoFile<?> file) {
//specially handling for videos
MediaLoadTask myTask = new MediaLoadTask(file);
mediaTask = myTask;
Node progressNode = newProgressIndicator(myTask);
Platform.runLater(() -> imageBorder.setCenter(progressNode));
//called on fx thread
mediaTask.setOnSucceeded(succeedded -> {
showMedia(file, myTask);
synchronized (SlideShowView.this) {
mediaTask = null;
}
});
mediaTask.setOnFailed(failed -> {
showErrorNode(getMediaLoadErrorLabel(myTask), file);
synchronized (SlideShowView.this) {
mediaTask = null;
}
});
mediaTask.setOnCancelled(cancelled -> {
disposeContent();
});
exec.execute(myTask);
return progressNode;
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void showMedia(DrawableFile<?> file, Task<Node> mediaTask) {
//Note that all error conditions are allready logged in readImageTask.succeeded()
try {
Node mediaNode = mediaTask.get();
if (nonNull(mediaNode)) {
//we have non-null media node show it
imageBorder.setCenter(mediaNode);
} else {
showErrorNode(getMediaLoadErrorLabel(mediaTask), file);
}
} catch (InterruptedException | ExecutionException ex) {
showErrorNode(getMediaLoadErrorLabel(mediaTask), file);
}
}
private String getMediaLoadErrorLabel(Task<Node> mediaTask) {
return Bundle.MediaViewImagePanel_errorLabel_text() + ": " + mediaTask.getException().getLocalizedMessage();
}
/**
*
* @param file the value of file
* @param imageTask the value of imageTask
*/
@Override
Node newProgressIndicator(final Task<?> imageTask) {
MaskerPane maskerPane = new MaskerPane();
ProgressIndicator loadingProgressIndicator = new ProgressBar(-1);
maskerPane.setProgressNode(loadingProgressIndicator);
maskerPane.textProperty().bind(imageTask.messageProperty());
loadingProgressIndicator.progressProperty().bind(imageTask.progressProperty());
return maskerPane;
}
/** /**
* {@inheritDoc } * {@inheritDoc }
*/ */
@ -250,46 +302,30 @@ public class SlideShowView extends DrawableTileBase {
} }
@Override @Override
CachedLoaderTask<Image, DrawableFile<?>> getNewImageLoadTask(DrawableFile<?> file) { Task<Image> newReadImageTask(DrawableFile<?> file) {
return file.getReadFullSizeImageTask();
return new ImageLoaderTask(file) {
@Override
Image load() {
return isCancelled() ? null : file.getFullSizeImage();
}
};
} }
private class MediaLoadTask extends CachedLoaderTask<Node, VideoFile<?>> { @NbBundle.Messages({"# {0} - file name",
"MediaLoadTask.messageText=Reading video: {0}"})
private class MediaLoadTask extends Task<Node> {
public MediaLoadTask(VideoFile<?> file) { private final VideoFile<?> file;
super(file);
MediaLoadTask(VideoFile<?> file) {
updateMessage(Bundle.MediaLoadTask_messageText(file.getName()));
this.file = file;
} }
@Override @Override
void saveToCache(Node result) { protected Node call() throws Exception {
synchronized (SlideShowView.this) {
mediaCache = new SoftReference<>(result);
}
}
@Override
Node load() {
try { try {
final Media media = file.getMedia(); final Media media = file.getMedia();
return new VideoPlayer(new MediaPlayer(media), file); return new VideoPlayer(new MediaPlayer(media), file);
} catch (MediaException | IOException | OutOfMemoryError ex) { } catch (MediaException | IOException | OutOfMemoryError ex) {
Logger.getLogger(VideoFile.class.getName()).log(Level.WARNING, "failed to initialize MediaControl for file " + file.getName(), ex); LOGGER.log(Level.WARNING, "Failed to initialize VideoPlayer for {0} : " + ex.toString(), file.getContentPathSafe());
return doReadImageTask(file);
if (file.isDisplayableAsImage()) {
Image fullSizeImage = file.getFullSizeImage();
Platform.runLater(() -> {
imageView.setImage(fullSizeImage);
});
return imageView;
}
return new Text(ex.getLocalizedMessage() + "\nSee the logs for details.\n\nTry the \"Open In External Viewer\" action.");
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View File

@ -0,0 +1,40 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.utils;
import java.util.concurrent.Callable;
import javafx.concurrent.Task;
/**
*
*/
public class TaskUtils {
public static <T> Task<T> taskFrom(Callable<T> callable) {
return new Task<T>() {
@Override
protected T call() throws Exception {
return callable.call();
}
};
}
private TaskUtils() {
}
}

View File

@ -1,5 +1,5 @@
#Updated by build script #Updated by build script
#Tue, 05 Jan 2016 16:31:20 -0500 #Mon, 21 Dec 2015 06:26:49 -0500
LBL_splash_window_title=Starting Autopsy LBL_splash_window_title=Starting Autopsy
SPLASH_HEIGHT=314 SPLASH_HEIGHT=314
SPLASH_WIDTH=538 SPLASH_WIDTH=538

View File

@ -1,4 +1,4 @@
#Updated by build script #Updated by build script
#Tue, 05 Jan 2016 16:31:20 -0500 #Mon, 21 Dec 2015 06:26:49 -0500
CTL_MainWindow_Title=Autopsy 4.0.0 CTL_MainWindow_Title=Autopsy 4.0.0
CTL_MainWindow_Title_No_Project=Autopsy 4.0.0 CTL_MainWindow_Title_No_Project=Autopsy 4.0.0

View File

@ -194,7 +194,7 @@ The first question that you must answer is what type of data do you want the use
\subsection ingest_modules_making_results_bb Posting Results to the Blackboard \subsection ingest_modules_making_results_bb Posting Results to the Blackboard
The blackboard is used to store results so that they are displayed in the results tree. The blackboard is used to store results so that they are displayed in the results tree.
See \ref platform_blackboard for details on posting results to it. You use the blackboard when you have specific items to show the user. if you want to just shown them a big report from another library or tool, see \ref ingest_modules_makeing_results_report. See \ref platform_blackboard for details on posting results to it. You use the blackboard when you have specific items to show the user. if you want to just shown them a big report from another library or tool, see \ref mod_report_page.
The blackboard defines artifacts for specific data types (such as web bookmarks). The blackboard defines artifacts for specific data types (such as web bookmarks).
You can use one of the standard artifact types or create your own. You can use one of the standard artifact types or create your own.