diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index c34f465589..aeff32aec9 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -21,21 +21,17 @@ package org.sleuthkit.autopsy.corecomponents; import java.awt.Dimension; import java.awt.EventQueue; 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.Iterator; import java.util.List; +import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.concurrent.ExecutionException; -import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.concurrent.Task; +import javafx.concurrent.WorkerStateEvent; import javafx.embed.swing.JFXPanel; -import javafx.embed.swing.SwingFXUtils; +import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Scene; @@ -46,13 +42,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; -import javax.annotation.Nullable; -import javax.imageio.IIOException; 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 org.controlsfx.control.MaskerPane; 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.directorytree.ExternalViewerAction; 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 @@ -110,7 +98,7 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi .map("."::concat) //NOI18N .collect(Collectors.toList()); - private LoadImageTask readImageTask; + private Task readImageTask; /** * 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. * @@ -175,7 +171,49 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi if (readImageTask != null) { 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() { + + @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); progressBar.progressProperty().bind(readImageTask.progressProperty()); @@ -233,170 +271,4 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables - private class LoadImageTask extends Task 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 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) { - } - } - } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index bec4a9acbd..fc418a005f 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -1,5 +1,4 @@ /* - * * Autopsy Forensic Browser * * Copyright 2012-15 Basis Technology Corp. @@ -27,28 +26,37 @@ import com.google.common.io.Files; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; -import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; -import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.logging.Level; +import javafx.concurrent.Task; +import javafx.embed.swing.SwingFXUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.imageio.IIOException; 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.concurrent.BasicThreadFactory; import org.opencv.core.Core; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; @@ -59,31 +67,34 @@ import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskCoreException; /** - * Utilities for working with Images and creating thumbnails. Reuses thumbnails - * by storing them in the case's cache directory. + * Utilities for working with image files and creating thumbnails. Reuses + * thumbnails by storing them in the case's cache directory. */ public class ImageUtils { 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 */ - 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_MEDIUM = 100; public static final int ICON_SIZE_LARGE = 200; - private static final Logger logger = LOGGER; 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 GIF_MIME_SET = ImmutableSortedSet.copyOf(new String[]{IMAGE_GIF_MIME}); private static final List SUPPORTED_IMAGE_EXTENSIONS; private static final SortedSet SUPPORTED_IMAGE_MIME_TYPES; - private static final List CONDITIONAL_MIME_TYPES = Arrays.asList("audio/x-aiff", "application/octet-stream"); + private static final List CONDITIONAL_MIME_TYPES = Arrays.asList("audio/x-aiff", "application/octet-stream"); //NOI18N private static final boolean openCVLoaded; @@ -91,9 +102,9 @@ public class ImageUtils { ImageIO.scanForPlugins(); BufferedImage tempImage; 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) { - LOGGER.log(Level.SEVERE, "Failed to load default icon.", ex); + LOGGER.log(Level.SEVERE, "Failed to load default icon.", ex); //NOI18N tempImage = null; } DEFAULT_THUMBNAIL = tempImage; @@ -102,16 +113,16 @@ public class ImageUtils { boolean openCVLoadedTemp; try { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); - if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { - System.loadLibrary("opencv_ffmpeg248_64"); + if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { //NOI18N + System.loadLibrary("opencv_ffmpeg248_64"); //NOI18N } else { - System.loadLibrary("opencv_ffmpeg248"); + System.loadLibrary("opencv_ffmpeg248"); //NOI18N } openCVLoadedTemp = true; } catch (UnsatisfiedLinkError e) { 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 } @@ -129,8 +140,8 @@ public class ImageUtils { "image/x-ms-bmp", "image/x-portable-graymap", "image/x-portable-bitmap", - "application/x-123")); //TODO: is this correct? -jm - SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); + "application/x-123")); //TODO: is this correct? -jm //NOI18N + SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); //NOI18N } /** @@ -143,7 +154,7 @@ public class ImageUtils { */ private static final Executor imageSaver = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() - .namingPattern("icon saver-%d").build()); + .namingPattern("icon saver-%d").build()); //NOI18N public static List getSupportedImageExtensions() { return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS); @@ -179,10 +190,12 @@ public class ImageUtils { /** * 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) { @@ -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) { - return isMediaThumbnailSupported(file, SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS, CONDITIONAL_MIME_TYPES) || 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) { try { final FileTypeDetector fileTypeDetector = getFileTypeDetector(); @@ -213,16 +240,16 @@ public class ImageUtils { return IMAGE_GIF_MIME.equalsIgnoreCase(fileType); } } 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)) { case TRUE: return true; case UNDEFINED: - LOGGER.log(Level.WARNING, "Falling back on extension check."); - return "gif".equals(file.getNameExtension()); + LOGGER.log(Level.WARNING, "Falling back on extension check."); //NOI18N + return "gif".equals(file.getNameExtension()); //NOI18N case FALSE: default: 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 * 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 * supoprted if it also has a supported extension * - * @return true if a thumbnail can be generated for the given file with the - * given lists of supported mimetype and extensions + * @return true if a thumbnail can be generated for the given file based on + * the given lists of supported mimetype and extensions */ static boolean isMediaThumbnailSupported(AbstractFile file, final SortedSet supportedMimeTypes, final List supportedExtension, List conditionalMimes) { if (file.getSize() == 0) { @@ -260,8 +287,7 @@ public class ImageUtils { || (conditionalMimes.contains(mimeType.toLowerCase()) && supportedExtension.contains(extension)); } } 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); if (mimeMatch == AbstractFile.MimeMatchEnum.TRUE) { return true; @@ -289,29 +315,27 @@ public class ImageUtils { } /** - * Get a thumbnail of a specified size. Generates the image if it is not - * already cached. + * Get a thumbnail of a specified size for the given image. Generates the + * thumbnail if it is not already cached. * * @param content * @param iconSize * - * * @return a thumbnail for the given image or a default one if there was a * problem making a thumbnail. * * @deprecated use {@link #getThumbnail(org.sleuthkit.datamodel.Content, int) * } instead. - * */ @Nonnull @Deprecated - public static Image getIcon(Content content, int iconSize) { + public static BufferedImage getIcon(Content content, int iconSize) { return getThumbnail(content, iconSize); } /** - * Get a thumbnail of a specified size. Generates the image if it is not - * already cached. + * Get a thumbnail of a specified size for the given image. Generates the + * thumbnail if it is not already cached. * * @param content * @param iconSize @@ -319,25 +343,17 @@ public class ImageUtils { * @return a thumbnail for the given image or a default one if there was a * problem making a thumbnail. */ - public static Image getThumbnail(Content content, int iconSize) { + public static BufferedImage getThumbnail(Content content, int iconSize) { if (content instanceof AbstractFile) { AbstractFile file = (AbstractFile) content; - // If a thumbnail file is already saved locally - File cacheFile = getCachedThumbnailLocation(content.getId()); - if (cacheFile.exists()) { - try { - BufferedImage thumbnail = ImageIO.read(cacheFile); - if (isNull(thumbnail) || thumbnail.getWidth() != iconSize) { - return generateAndSaveThumbnail(file, iconSize, cacheFile); - } else { - 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); + + Task thumbnailTask = newGetThumbnailTask(file, iconSize, true); + thumbnailTask.run(); + try { + return SwingFXUtils.fromFXImage(thumbnailTask.get(), null); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.WARNING, "Failed to get thumbnail for {0}: " + ex.toString(), getContentPathSafe(content)); + return DEFAULT_THUMBNAIL; } } else { return DEFAULT_THUMBNAIL; @@ -345,8 +361,8 @@ public class ImageUtils { } /** - * Get a thumbnail of a specified size. Generates the image if it is not - * already cached. + * Get a thumbnail of a specified size for the given image. Generates the + * thumbnail if it is not already cached. * * @param content * @param iconSize @@ -367,8 +383,8 @@ public class ImageUtils { /** * - * Get a thumbnail of a specified size. Generates the image if it is not - * already cached. + * Get a thumbnail of a specified size for the given image. Generates the + * thumbnail if it is not already cached. * * @param content * @param iconSize @@ -390,7 +406,6 @@ public class ImageUtils { * * @return * - * * @deprecated use {@link #getCachedThumbnailLocation(long) } instead */ @Deprecated @@ -400,18 +415,26 @@ public class ImageUtils { } /** - * Get a file object for where the cached icon should exist. The returned - * file may not exist. + * Get a file object for where the cached thumbnail should exist. The + * returned file may not exist. * * @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) { - 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) { return isJpegFileHeader(file) || isPngFileHeader(file); } @@ -476,91 +499,426 @@ public class ImageUtils { if (bytesRead != buffLength) { //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; } /** - * 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 iconSize - * @param cacheFile Location to save thumbnail to + * @param file * - * @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) { - BufferedImage thumbnail = null; - try { - if (VideoUtils.isVideoThumbnailSupported(file)) { - 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; + static public int getImageWidth(AbstractFile file) throws IOException { + return getImageProperty(file, + "ImageIO could not determine width of {0}: ", //NOI18N + imageReader -> imageReader.getWidth(0) + ); } /** + * Get the height of the given image,in pixels. * - * Generate and return a scaled image + * @param file * - * @param content - * @param iconSize + * @return the height in pixels * - * @return a Thumbnail of the given content at the given size, or null if - * there was a problem. + * @throws IOException If the file is not a supported image or the height + * could not be determined. */ - @Nullable - private static BufferedImage generateImageThumbnail(Content content, int iconSize) { - - try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(content));) { - 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; + static public int getImageHeight(AbstractFile file) throws IOException { + return getImageProperty(file, + "ImageIO could not determine height of {0}: ", //NOI18N + imageReader -> imageReader.getHeight(0) + ); } + /** + * 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 The type of the property. + */ + @FunctionalInterface + private static interface PropertyExtractor { + + 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 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 getImageProperty(AbstractFile file, final String errorTemplate, PropertyExtractor 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 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 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 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 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 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; + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java index 26275b48fa..4392f59e2d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java @@ -30,14 +30,16 @@ import java.net.MalformedURLException; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.function.Function; import java.util.logging.Level; +import javafx.beans.Observable; import javafx.beans.property.SimpleIntegerProperty; +import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.Image; import javafx.scene.image.WritableImage; import javax.annotation.Nullable; import javax.imageio.ImageIO; +import org.openide.util.Exceptions; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; 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. * items may be garbage collected if there are no strong references to them. */ - private final Cache> cache = CacheBuilder.newBuilder() + private final Cache cache = CacheBuilder.newBuilder() .maximumSize(1000) .softValues() .expireAfterAccess(10, TimeUnit.MINUTES).build(); @@ -97,9 +99,9 @@ public enum ThumbnailCache { @Nullable public Image get(DrawableFile file) { try { - return cache.get(file.getId(), () -> load(file)).orElse(null); + return cache.get(file.getId(), () -> load(file)); } 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; } } @@ -109,7 +111,7 @@ public enum ThumbnailCache { try { return get(ImageGalleryController.getDefault().getFileFromId(fileID)); } 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; } } @@ -122,35 +124,32 @@ public enum ThumbnailCache { * * @return an (possibly empty) optional containing a thumbnail */ - private Optional load(DrawableFile file) { + private Image load(DrawableFile file) { if (FileTypeUtils.isGIF(file)) { //directly read gif to preserve potential animation, //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() { - @Override - public BufferedImage apply(File cachFile) { - if (cachFile.exists()) { - // If a thumbnail file is already saved locally, load it - try { - BufferedImage cachedThumbnail = ImageIO.read(cachFile); + BufferedImage thumbnail = getCacheFile(file).map(cachFile -> { + if (cachFile.exists()) { + // If a thumbnail file is already saved locally, load it + try { + BufferedImage cachedThumbnail = ImageIO.read(cachFile); - if (cachedThumbnail.getWidth() < MAX_THUMBNAIL_SIZE) { - 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); + if (cachedThumbnail.getWidth() < MAX_THUMBNAIL_SIZE) { + 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); } - return null; } + return null; }).orElseGet(() -> { - return (BufferedImage) ImageUtils.getThumbnail(file.getAbstractFile(), MAX_THUMBNAIL_SIZE); + return ImageUtils.getThumbnail(file.getAbstractFile(), MAX_THUMBNAIL_SIZE); }); WritableImage jfxthumbnail; @@ -161,7 +160,7 @@ public enum ThumbnailCache { 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 * - * @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. */ private static Optional getCacheFile(DrawableFile file) { try { 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()); return Optional.empty(); } } + + public Task getThumbnailTask(DrawableFile file) { + final Image thumbnail = cache.getIfPresent(file.getId()); + if (thumbnail != null) { + return new Task() { + @Override + protected Image call() throws Exception { + return thumbnail; + } + }; + } + final Task 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; + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenExternalViewerAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenExternalViewerAction.java new file mode 100644 index 0000000000..c132a8789f --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenExternalViewerAction.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit 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)); + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index 4726341eed..fb08256f83 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -23,12 +23,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Task; import javafx.scene.image.Image; import javafx.util.Pair; import javax.annotation.Nonnull; @@ -38,6 +39,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; +import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -77,6 +79,7 @@ public abstract class DrawableFile extends AbstractFile } SoftReference imageRef; +// SoftReference thumbref; private String drawablePath; @@ -91,12 +94,36 @@ public abstract class DrawableFile extends AbstractFile private String model; 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 * 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.file = file; } @@ -213,7 +240,9 @@ public abstract class DrawableFile extends AbstractFile 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() { try { category.set(getSleuthkitCase().getContentTagsByContent(this).stream() @@ -224,17 +253,59 @@ public abstract class DrawableFile extends AbstractFile .orElse(Category.ZERO) ); } 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) { // 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() { - return ThumbnailCache.getDefault().get(this); + try { + return getThumbnailTask().get(); + } catch (InterruptedException | ExecutionException ex) { + return null; + } + } - public abstract Image getFullSizeImage(); + public Task 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 getReadFullSizeImageTask() { + Image image = (imageRef != null) ? imageRef.get() : null; + if (image == null || image.isError()) { + Task 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 getReadFullSizeImageTaskHelper(); public void setAnalyzed(Boolean analyzed) { this.analyzed.set(analyzed); @@ -266,11 +337,6 @@ public abstract class DrawableFile extends AbstractFile } } - public boolean isDisplayableAsImage() { - Image thumbnail = getThumbnail(); - return Objects.nonNull(thumbnail) && thumbnail.errorProperty().get() == false; - } - @Nonnull public Set getHashSetNamesUnchecked() { try { @@ -280,4 +346,22 @@ public abstract class DrawableFile extends AbstractFile 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; + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java index c235870448..4249fe3c24 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java @@ -18,17 +18,13 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import java.awt.image.BufferedImage; -import java.io.BufferedInputStream; -import java.lang.ref.SoftReference; -import java.util.logging.Level; -import javafx.embed.swing.SwingFXUtils; +import java.io.IOException; +import javafx.concurrent.Task; import javafx.scene.image.Image; import javax.imageio.ImageIO; +import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.ReadContentInputStream; /** * ImageGallery data model object that represents an image file. It is a @@ -49,43 +45,31 @@ public class ImageFile extends DrawableFile { } @Override - public Image getFullSizeImage() { - Image image = (imageRef != null) ? imageRef.get() : null; - if (image == null || image.isError()) { - if (FileTypeUtils.isGIF(getAbstractFile())) { - //directly read gif to preserve potential animation, - image = new Image(new BufferedInputStream(new ReadContentInputStream(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; + String getMessageTemplate(final Exception exception) { + return "Failed to read image {0}: " + exception.toString(); + } + + @Override + Task getReadFullSizeImageTaskHelper() { + return ImageUtils.newReadImageTask(this.getAbstractFile()); } @Override Double getWidth() { - final Image fullSizeImage = getFullSizeImage(); - if (fullSizeImage != null) { - return fullSizeImage.getWidth(); + try { + return (double) ImageUtils.getImageWidth(this.getAbstractFile()); + } catch (IOException ex) { + return -1.0; } - return -1.0; } @Override Double getHeight() { - final Image fullSizeImage = getFullSizeImage(); - if (fullSizeImage != null) { - return fullSizeImage.getHeight(); + try { + return (double) ImageUtils.getImageHeight(this.getAbstractFile()); + } catch (IOException ex) { + return -1.0; } - return -1.0; } @Override diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java index 19fa08a8c7..20724ef0bc 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java @@ -19,14 +19,11 @@ package org.sleuthkit.autopsy.imagegallery.datamodel; import com.google.common.io.Files; -import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.lang.ref.SoftReference; import java.nio.file.Paths; -import java.util.Objects; -import java.util.logging.Level; -import javafx.embed.swing.SwingFXUtils; +import javafx.concurrent.Task; import javafx.scene.image.Image; import javafx.scene.media.Media; import javafx.scene.media.MediaException; @@ -40,6 +37,8 @@ import org.sleuthkit.datamodel.AbstractFile; public class VideoFile extends DrawableFile { + 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"); VideoFile(T file, Boolean analyzed) { @@ -50,17 +49,16 @@ public class VideoFile extends DrawableFile { return VIDEO_ICON; } + + @Override - public Image getFullSizeImage() { - Image image = (null == imageRef) ? null : imageRef.get(); + String getMessageTemplate(final Exception exception) { + return "Failed to get image preview for video {0}: " + exception.toString(); + } - if (image == null) { - final BufferedImage bufferedImage = (BufferedImage) ImageUtils.getThumbnail(getAbstractFile(), 1024); - image = (bufferedImage == ImageUtils.getDefaultThumbnail()) ? null : SwingFXUtils.toFXImage(bufferedImage, null); - imageRef = new SoftReference<>(image); - } - - return image; + @Override + Task getReadFullSizeImageTaskHelper() { + return ImageUtils.newGetThumbnailTask(getAbstractFile(), 1024, false); } private SoftReference mediaRef; @@ -74,13 +72,11 @@ public class VideoFile extends DrawableFile { final File cacheFile = VideoUtils.getTempVideoFile(this.getAbstractFile()); if (cacheFile.exists() == false || cacheFile.length() < getAbstractFile().getSize()) { - Files.createParentDirs(cacheFile); ProgressHandle progressHandle = ProgressHandleFactory.createHandle("writing temporary file to disk"); progressHandle.start(100); ContentUtils.writeToFile(this.getAbstractFile(), cacheFile, progressHandle, null, true); progressHandle.finish(); - } media = new Media(Paths.get(cacheFile.getAbsolutePath()).toUri().toString()); @@ -89,18 +85,6 @@ public class VideoFile extends DrawableFile { } - 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 Double getWidth() { try { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.fxml index 2d6c8304e4..acfcdc1336 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.fxml @@ -34,11 +34,6 @@ - - - - - diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java index 6088540262..ba7c275063 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import java.util.Objects; import java.util.logging.Level; import javafx.application.Platform; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.CacheHint; import javafx.scene.control.Control; @@ -90,8 +91,8 @@ public class DrawableTile extends DrawableTileBase { } @Override - CachedLoaderTask> getNewImageLoadTask(DrawableFile file) { - return new ThumbnailLoaderTask(file); + Task newReadImageTask(DrawableFile file) { + return file.getThumbnailTask(); } @Override @@ -99,4 +100,6 @@ public class DrawableTile extends DrawableTileBase { return getFile().map(AbstractContent::getName).orElse(""); } + + } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java index 17687b54f7..809f268902 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java @@ -47,6 +47,7 @@ import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javax.swing.Action; import javax.swing.SwingUtilities; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.Lookup; import org.openide.util.NbBundle; 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.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.FileNode; -import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; 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.CategorizeAction; 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.datamodel.DrawableAttribute; 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.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; @@ -111,8 +111,7 @@ public abstract class DrawableTileBase extends DrawableUIBase { @FXML private ImageView hashHitImageView; - @FXML - protected ImageView undisplayableImageView; + /** * displays the icon representing follow up tag */ @@ -221,15 +220,10 @@ public abstract class DrawableTileBase extends DrawableUIBase { }); menuItems.add(contentViewer); - MenuItem externalViewer = new MenuItem(Bundle.DrawableTileBase_externalViewerAction_text()); - final ExternalViewerAction externalViewerAction = new ExternalViewerAction(Bundle.DrawableTileBase_externalViewerAction_text(), new FileNode(file.getAbstractFile())); - - externalViewer.setDisable(externalViewerAction.isEnabled() == false); - externalViewer.setOnAction((ActionEvent t) -> { - SwingUtilities.invokeLater(() -> { - externalViewerAction.actionPerformed(null); - }); - }); + OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file.getAbstractFile()); + MenuItem externalViewer = ActionUtils.createMenuItem(openExternalViewerAction); + externalViewer.textProperty().unbind(); + externalViewer.textProperty().bind(openExternalViewerAction.longTextProperty()); menuItems.add(externalViewer); Collection menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class); @@ -311,16 +305,16 @@ public abstract class DrawableTileBase extends DrawableUIBase { updateSelectionState(); updateCategory(); updateFollowUpIcon(); - updateUI(); updateContent(); + updateMetaData(); } } - private void updateUI() { + private void updateMetaData() { getFile().ifPresent(file -> { final boolean isVideo = file.isVideo(); final boolean hasHashSetHits = hasHashHit(); - final boolean isUndisplayable = (isVideo ? ((VideoFile) file).isDisplayableAsMedia() : file.isDisplayableAsImage()) == false; + final String text = getTextForLabel(); Platform.runLater(() -> { @@ -328,8 +322,7 @@ public abstract class DrawableTileBase extends DrawableUIBase { fileTypeImageView.setVisible(isVideo); hashHitImageView.setManaged(hasHashSetHits); hashHitImageView.setVisible(hasHashSetHits); - undisplayableImageView.setManaged(isUndisplayable); - undisplayableImageView.setVisible(isUndisplayable); + nameLabel.setText(text); nameLabel.setTooltip(new Tooltip(text)); }); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java index 6983f49180..5e3b9ae798 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java @@ -18,47 +18,58 @@ */ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; -import java.lang.ref.SoftReference; import java.util.Objects; -import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.logging.Level; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.fxml.FXML; +import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; 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.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; +import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; +import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; /** * */ +@NbBundle.Messages({"MediaViewImagePanel.errorLabel.text=Could not read file."}) abstract public class DrawableUIBase extends AnchorPane implements DrawableView { + static final Executor exec = Executors.newWorkStealingPool(); + private static final Logger LOGGER = Logger.getLogger(DrawableUIBase.class.getName()); @FXML - protected BorderPane imageBorder; + BorderPane imageBorder; @FXML - protected ImageView imageView; + ImageView imageView; private final ImageGalleryController controller; private Optional> fileOpt = Optional.empty(); private Optional fileIDOpt = Optional.empty(); - private Task imageTask; - private SoftReference imageCache; - private ProgressIndicator progressIndicator; + private volatile Task imageTask; public DrawableUIBase(ImageGalleryController controller) { this.controller = controller; @@ -107,132 +118,97 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView synchronized public void setFile(Long newFileID) { if (getFileID().isPresent()) { if (Objects.equals(newFileID, getFileID().get()) == false) { - if (Objects.nonNull(newFileID)) { - setFileHelper(newFileID); - } + setFileHelper(newFileID); } - } else if (Objects.nonNull(newFileID)) { + } else { setFileHelper(newFileID); } } synchronized protected void updateContent() { - Node content = getContentNode(); - Platform.runLater(() -> { - imageBorder.setCenter(content); + if (getFile().isPresent()) { + doReadImageTask(getFile().get()); + } + } + + synchronized Node doReadImageTask(DrawableFile file) { + Task 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() { if (imageTask != null) { - imageTask.cancel(true); + imageTask.cancel(); } imageTask = null; - imageCache = null; + Platform.runLater(() -> { + imageView.setImage(null); + imageBorder.setCenter(null); + }); } - ProgressIndicator getLoadingProgressIndicator() { - if (progressIndicator == null) { - progressIndicator = new ProgressIndicator(); - } - return progressIndicator; + /** + * + * @param file the value of file + * @param imageTask the value of imageTask + */ + Node newProgressIndicator(final Task imageTask) { + ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1); + loadingProgressIndicator.progressProperty().bind(imageTask.progressProperty()); + return loadingProgressIndicator; } - Node getContentNode() { - if (getFile().isPresent() == false) { - imageCache = null; - Platform.runLater(() -> { - if (imageView != null) { - imageView.setImage(null); - } - }); - return null; - } else { - Image thumbnail = isNull(imageCache) ? null : imageCache.get(); - - if (nonNull(thumbnail)) { - Platform.runLater(() -> { - if (imageView != null) { - imageView.setImage(thumbnail); - } - }); - return imageView; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void showImage(DrawableFile file, Task imageTask) { + //Note that all error conditions are allready logged in readImageTask.succeeded() + try { + Image fxImage = imageTask.get(); + if (nonNull(fxImage)) { + //we have non-null image show it + imageView.setImage(fxImage); + imageBorder.setCenter(imageView); } else { - DrawableFile file = getFile().get(); - - if (isNull(imageTask)) { - imageTask = getNewImageLoadTask(file); - new Thread(imageTask).start(); - } else if (imageTask.isDone()) { - return null; - } - return getLoadingProgressIndicator(); + showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); } + } catch (CancellationException ex) { + + } catch (InterruptedException | ExecutionException ex) { + showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); } } - abstract CachedLoaderTask> getNewImageLoadTask(DrawableFile file); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void showErrorNode(String errorMessage, AbstractFile file) { + Button createButton = ActionUtils.createButton(new OpenExternalViewerAction(file)); - abstract class CachedLoaderTask> extends Task { + VBox vBox = new VBox(10, + new Label(errorMessage), createButton); - protected final Y file; - - 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); + vBox.setAlignment(Pos.CENTER); + imageBorder.setCenter(vBox); } - abstract class ImageLoaderTask extends CachedLoaderTask> { - - 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(); - } - } + abstract Task newReadImageTask(DrawableFile file); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java index f0a828f644..3b672ebf49 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java @@ -27,11 +27,11 @@ import java.util.Objects; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; -import org.sleuthkit.autopsy.coreutils.Logger; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -52,6 +52,7 @@ import javafx.scene.text.Text; import javafx.util.Pair; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; @@ -166,36 +167,37 @@ public class MetaDataPane extends DrawableUIBase { } @Override - protected synchronized void setFileHelper(Long newFileID) { + synchronized protected void setFileHelper(Long newFileID) { setFileIDOpt(Optional.ofNullable(newFileID)); - if (newFileID == null) { - Platform.runLater(() -> { - imageView.setImage(null); - imageBorder.setCenter(null); - tableView.getItems().clear(); - getCategoryBorderRegion().setBorder(null); - }); - } else { - disposeContent(); - updateUI(); + disposeContent(); + if (nonNull(newFileID)) { + updateAttributesTable(); + updateCategory(); updateContent(); } } @Override - CachedLoaderTask> getNewImageLoadTask(DrawableFile file) { - return new ThumbnailLoaderTask(file); + protected synchronized void disposeContent() { + super.disposeContent(); + Platform.runLater(() -> { + tableView.getItems().clear(); + getCategoryBorderRegion().setBorder(null); + }); } - public void updateUI() { + @Override + Task newReadImageTask(DrawableFile file) { + return file.getThumbnailTask(); + } + + public void updateAttributesTable() { getFile().ifPresent(file -> { final List, Collection>> attributesList = file.getAttributesList(); Platform.runLater(() -> { tableView.getItems().clear(); tableView.getItems().setAll(attributesList); }); - - updateCategory(); }); } @@ -204,13 +206,15 @@ public class MetaDataPane extends DrawableUIBase { return imageBorder; } - /** {@inheritDoc } */ + /** + * {@inheritDoc } + */ @Subscribe @Override public void handleCategoryChanged(CategoryManager.CategoryChangeEvent evt) { getFileID().ifPresent(fileID -> { if (evt.getFileIDs().contains(fileID)) { - updateUI(); + updateAttributesTable(); } }); } @@ -220,7 +224,7 @@ public class MetaDataPane extends DrawableUIBase { public void handleTagAdded(ContentTagAddedEvent evt) { getFileID().ifPresent((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) { getFileID().ifPresent((fileID) -> { if (Objects.equals(evt.getDeletedTagInfo().getContentID(), fileID)) { - updateUI(); + updateAttributesTable(); } }); } @@ -241,4 +245,5 @@ public class MetaDataPane extends DrawableUIBase { getValueDisplayString(selectedItem))); } } + } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.fxml index 712f3a953a..d7a631854e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.fxml @@ -55,11 +55,6 @@ - - - - - diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java index f8555046d3..1e8c98117f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java @@ -19,10 +19,9 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import java.io.IOException; -import java.lang.ref.SoftReference; -import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.Observable; @@ -32,6 +31,8 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ProgressIndicator; import javafx.scene.image.Image; import static javafx.scene.input.KeyCode.LEFT; 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.MediaException; 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.ThreadConfined; 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.VideoFile; 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; /** @@ -69,7 +72,8 @@ public class SlideShowView extends DrawableTileBase { @FXML private BorderPane footer; - private Task mediaTask; + + private volatile MediaLoadTask mediaTask; SlideShowView(GroupPane gp, ImageGalleryController controller) { super(gp, controller); @@ -155,46 +159,94 @@ public class SlideShowView extends DrawableTileBase { } @Override - protected void disposeContent() { + synchronized protected void disposeContent() { stopVideo(); - super.disposeContent(); if (mediaTask != null) { mediaTask.cancel(true); } mediaTask = null; - mediaCache = null; + super.disposeContent(); } - private SoftReference mediaCache; - /** - * {@inheritDoc } - */ @Override - Node getContentNode() { - if (getFile().isPresent() == false) { - mediaCache = null; - return super.getContentNode(); - } else { + synchronized protected void updateContent() { + disposeContent(); + if (getFile().isPresent()) { DrawableFile file = getFile().get(); if (file.isVideo()) { - Node mediaNode = (isNull(mediaCache)) ? null : mediaCache.get(); - if (nonNull(mediaNode)) { - return mediaNode; - } else { - if (isNull(mediaTask)) { - mediaTask = new MediaLoadTask(((VideoFile) file)); - new Thread(mediaTask).start(); - } else if (mediaTask.isDone()) { - return null; - } - return getLoadingProgressIndicator(); - } + doMediaLoadTask((VideoFile) file); + } else { + doReadImageTask(file); } - 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 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 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 } */ @@ -250,46 +302,30 @@ public class SlideShowView extends DrawableTileBase { } @Override - CachedLoaderTask> getNewImageLoadTask(DrawableFile file) { + Task newReadImageTask(DrawableFile file) { + return file.getReadFullSizeImageTask(); - return new ImageLoaderTask(file) { - - @Override - Image load() { - return isCancelled() ? null : file.getFullSizeImage(); - } - }; } - private class MediaLoadTask extends CachedLoaderTask> { + @NbBundle.Messages({"# {0} - file name", + "MediaLoadTask.messageText=Reading video: {0}"}) + private class MediaLoadTask extends Task { - public MediaLoadTask(VideoFile file) { - super(file); + private final VideoFile file; + + MediaLoadTask(VideoFile file) { + updateMessage(Bundle.MediaLoadTask_messageText(file.getName())); + this.file = file; } @Override - void saveToCache(Node result) { - synchronized (SlideShowView.this) { - mediaCache = new SoftReference<>(result); - } - } - - @Override - Node load() { + protected Node call() throws Exception { try { final Media media = file.getMedia(); return new VideoPlayer(new MediaPlayer(media), file); } catch (MediaException | IOException | OutOfMemoryError ex) { - Logger.getLogger(VideoFile.class.getName()).log(Level.WARNING, "failed to initialize MediaControl for file " + file.getName(), ex); - - 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."); + LOGGER.log(Level.WARNING, "Failed to initialize VideoPlayer for {0} : " + ex.toString(), file.getContentPathSafe()); + return doReadImageTask(file); } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/external.png b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/external.png new file mode 100644 index 0000000000..976dcacaa1 Binary files /dev/null and b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/external.png differ diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java new file mode 100644 index 0000000000..7b648b5db7 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java @@ -0,0 +1,40 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit 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 Task taskFrom(Callable callable) { + return new Task() { + @Override + protected T call() throws Exception { + return callable.call(); + } + }; + } + + private TaskUtils() { + } +} diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index a4c8f7f8cf..f0d04d8afb 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Wed, 18 Nov 2015 20:51:12 -0500 +#Mon, 21 Dec 2015 06:26:49 -0500 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index 4792059f24..960ee135b3 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,4 +1,4 @@ #Updated by build script -#Wed, 18 Nov 2015 20:51:12 -0500 +#Mon, 21 Dec 2015 06:26:49 -0500 CTL_MainWindow_Title=Autopsy 4.0.0 CTL_MainWindow_Title_No_Project=Autopsy 4.0.0