diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index 9a8b374302..aeff32aec9 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -21,23 +21,16 @@ 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; @@ -49,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; @@ -66,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 @@ -113,7 +98,7 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi .map("."::concat) //NOI18N .collect(Collectors.toList()); - private ReadImageTask readImageTask; + private Task readImageTask; /** * Creates new form MediaViewImagePanel @@ -186,36 +171,31 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi if (readImageTask != null) { readImageTask.cancel(); } - readImageTask = new ReadImageTask(file); - readImageTask.setOnSucceeded(new EventHandler() { - - @Override - public void handle(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 = 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() { @@ -230,11 +210,7 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi return; } - 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); + showErrorNode(file); borderpane.setCursor(Cursor.DEFAULT); } }); @@ -295,136 +271,4 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables - static private class ReadImageTask extends Task implements IIOReadProgressListener { - - private final AbstractFile file; - volatile private BufferedImage bufferedImage = null; - - ReadImageTask(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 - } - } - } - - public void logError(@Nullable Throwable e) { - String message = e == null ? "" : "It may be unsupported or corrupt: " + e.getLocalizedMessage(); //NOI18N - try { - LOGGER.log(Level.WARNING, "Could not read the image: {0}. {1}", new Object[]{file.getUniquePath(), message}); //NOI18N - } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.WARNING, "Could not read the image: {0}. {1}", new Object[]{file.getName(), message}); //NOI18N - LOGGER.log(Level.SEVERE, "Failed to get unique path for file", tskCoreException); //NOI18N - } - } - - @Override - protected void failed() { - super.failed(); - logError(getException()); - } - - @Override - protected void succeeded() { - super.succeeded(); - try { - Image fxImage = get(); - if (fxImage == null) { - logError(null); - } else { - if (fxImage.isError()) { - //if there was somekind of error, log it - logError(fxImage.getException()); - } - } - } catch (InterruptedException | ExecutionException ex) { - logError(ex.getCause()); - } - } - - @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..c7f98cc061 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -27,28 +27,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.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; @@ -141,8 +150,8 @@ public class ImageUtils { /** * thread that saves generated thumbnails to disk in the background */ - private static final Executor imageSaver = - Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() + private static final Executor imageSaver + = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() .namingPattern("icon saver-%d").build()); public static List getSupportedImageExtensions() { @@ -319,7 +328,7 @@ 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 @@ -333,7 +342,7 @@ public class ImageUtils { return thumbnail; } } catch (Exception ex) { - LOGGER.log(Level.WARNING, "Error while reading image: " + content.getName(), ex); //NON-NLS + LOGGER.log(Level.WARNING, "ImageIO had a problem reading thumbnail for image {0}: {1}", new Object[]{content.getName(), ex.getLocalizedMessage()}); //NON-NLS return generateAndSaveThumbnail(file, iconSize, cacheFile); } } else { @@ -490,7 +499,7 @@ public class ImageUtils { * * @return Generated icon or null on error */ - private static Image generateAndSaveThumbnail(AbstractFile file, int iconSize, File cacheFile) { + private static BufferedImage generateAndSaveThumbnail(AbstractFile file, int iconSize, File cacheFile) { BufferedImage thumbnail = null; try { if (VideoUtils.isVideoThumbnailSupported(file)) { @@ -537,13 +546,15 @@ public class ImageUtils { * there was a problem. */ @Nullable - private static BufferedImage generateImageThumbnail(Content content, int iconSize) { + private static BufferedImage generateImageThumbnail(AbstractFile content, int iconSize) { - try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(content));) { - BufferedImage bi = ImageIO.read(inputStream); + try { + final ReadImageTask readImageTask = new ReadImageTask(content); + + readImageTask.run(); + BufferedImage bi = SwingFXUtils.fromFXImage(readImageTask.get(), null); if (bi == null) { - LOGGER.log(Level.WARNING, "No image reader for file: {0}", content.getName()); //NON-NLS return null; } try { @@ -554,13 +565,191 @@ public class ImageUtils { 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 + LOGGER.log(Level.WARNING, "Could not scale image (too large) " + content.getName() + ": " + e.getLocalizedMessage()); //NON-NLS } catch (Exception e) { - LOGGER.log(Level.WARNING, "Could not load image " + content.getName(), e); //NON-NLS + LOGGER.log(Level.WARNING, "ImageIO could not load image " + content.getName() + ": " + e.getLocalizedMessage()); //NON-NLS } return null; } + static public int getWidth(AbstractFile file) throws IIOException, IOException { + + try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { + + try (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.setInput(input); + return reader.getWidth(0); + } else { + throw new IIOException("No ImageReader found for file." + file.getName()); //NOI18N + } + } + } + } + + static public int getHeight(AbstractFile file) throws IIOException, IOException { + try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { + + try (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.setInput(input); + + return reader.getHeight(0); + } else { + throw new IIOException("No ImageReader found for file." + file.getName()); //NOI18N + } + } + + } + } + + public static Task newReadImageTask(AbstractFile file) { + return new ReadImageTask(file); + } + + static private class ReadImageTask extends Task implements IIOReadProgressListener { + + private final AbstractFile file; + volatile private BufferedImage bufferedImage = null; + + ReadImageTask(AbstractFile file) { + this.file = file; + } + + @Override + @NbBundle.Messages({ + "# {0} - file name", + "LoadImageTask.mesageText=Reading image: {0}"}) + protected javafx.scene.image.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, + javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(inputStream)); + if (image.isError() == false) { + return image; + } + //fall through to default iamge reading code if there was an error + } + + try (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 + } + } + } + } + + public void logError(@Nullable Throwable e) { + String message = e == null ? "" : "It may be unsupported or corrupt: " + e.getLocalizedMessage(); //NOI18N + try { + LOGGER.log(Level.WARNING, "Could not read the image: {0}. {1}", new Object[]{file.getUniquePath(), message}); //NOI18N + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "Could not read the image: {0}. {1}", new Object[]{file.getName(), message}); //NOI18N + LOGGER.log(Level.SEVERE, "Failed to get unique path for file", tskCoreException); //NOI18N + } + } + + @Override + protected void failed() { + super.failed(); + logError(getException()); + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + javafx.scene.image.Image fxImage = get(); + if (fxImage == null) { + logError(null); + } else { + if (fxImage.isError()) { + //if there was somekind of error, log it + logError(fxImage.getException()); + } + } + } catch (InterruptedException | ExecutionException ex) { + logError(ex.getCause()); + } + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + //update this task with the progress reported by ImageReader.read + updateProgress(percentageDone, 100); + } + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + } + + @Override + public void imageComplete(ImageReader source) { + updateProgress(100, 100); + } + + @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) { + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index efad107ba2..f3b19a3df8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -391,6 +391,7 @@ public class DetailViewPane extends AbstractVisualizationPane { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java index 26275b48fa..19551d6f7f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java @@ -30,7 +30,6 @@ 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.property.SimpleIntegerProperty; import javafx.embed.swing.SwingFXUtils; @@ -99,7 +98,7 @@ public enum ThumbnailCache { try { return cache.get(file.getId(), () -> load(file)).orElse(null); } 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 +108,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; } } @@ -130,27 +129,24 @@ public enum ThumbnailCache { return Optional.of(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((File 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; @@ -176,7 +172,7 @@ public enum ThumbnailCache { 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(); } 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..f0f3268678 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -25,10 +25,12 @@ 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; @@ -91,12 +93,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 +239,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,7 +252,7 @@ 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. } @@ -234,7 +262,17 @@ public abstract class DrawableFile extends AbstractFile return ThumbnailCache.getDefault().get(this); } - public abstract Image getFullSizeImage(); + @Deprecated + + public Image getFullSizeImage() { + try { + return getReadFullSizeImageTask().get(); + } catch (InterruptedException | ExecutionException ex) { + return null; + } + } + + public abstract Task getReadFullSizeImageTask(); public void setAnalyzed(Boolean analyzed) { this.analyzed.set(analyzed); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java index c235870448..103561ce32 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/ImageFile.java @@ -18,17 +18,17 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import java.awt.image.BufferedImage; -import java.io.BufferedInputStream; +import java.io.IOException; import java.lang.ref.SoftReference; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import javafx.embed.swing.SwingFXUtils; +import javafx.beans.Observable; +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 +49,53 @@ public class ImageFile extends DrawableFile { } @Override - public Image getFullSizeImage() { + public Task getReadFullSizeImageTask() { 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()))); - } + final Task newReadImageTask = ImageUtils.newReadImageTask(this.getAbstractFile()); + newReadImageTask.stateProperty().addListener((Observable observable) -> { + switch (newReadImageTask.getState()) { + case CANCELLED: + break; + case FAILED: + break; + case SUCCEEDED: + try { + imageRef = new SoftReference<>(newReadImageTask.get()); + } catch (InterruptedException | ExecutionException interruptedException) { + } + break; + } + }); + return newReadImageTask; + } else { + return new Task() { + @Override + protected Image call() throws Exception { + return image; + } + }; } - 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 Double getWidth() { - final Image fullSizeImage = getFullSizeImage(); - if (fullSizeImage != null) { - return fullSizeImage.getWidth(); + try { + return (double) ImageUtils.getWidth(this.getAbstractFile()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "getWidth", ex); + return -1.0; } - return -1.0; } @Override Double getHeight() { - final Image fullSizeImage = getFullSizeImage(); - if (fullSizeImage != null) { - return fullSizeImage.getHeight(); + try { + return (double) ImageUtils.getHeight(this.getAbstractFile()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "getHeight", 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..fde2a890f1 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/VideoFile.java @@ -25,7 +25,10 @@ import java.io.IOException; import java.lang.ref.SoftReference; import java.nio.file.Paths; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; +import javafx.beans.Observable; +import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.Image; import javafx.scene.media.Media; @@ -51,16 +54,43 @@ public class VideoFile extends DrawableFile { } @Override - public Image getFullSizeImage() { - Image image = (null == imageRef) ? null : imageRef.get(); + public Task getReadFullSizeImageTask() { + Image image = (imageRef != null) ? imageRef.get() : null; + if (image == null || image.isError()) { + Task newReadImageTask = new Task() { - if (image == null) { - final BufferedImage bufferedImage = (BufferedImage) ImageUtils.getThumbnail(getAbstractFile(), 1024); - image = (bufferedImage == ImageUtils.getDefaultThumbnail()) ? null : SwingFXUtils.toFXImage(bufferedImage, null); - imageRef = new SoftReference<>(image); + @Override + protected Image call() throws Exception { + final BufferedImage bufferedImage = (BufferedImage) ImageUtils.getThumbnail(getAbstractFile(), 1024); + return (bufferedImage == ImageUtils.getDefaultThumbnail()) + ? null + : SwingFXUtils.toFXImage(bufferedImage, null); + } + }; + + newReadImageTask.stateProperty().addListener((Observable observable) -> { + switch (newReadImageTask.getState()) { + case CANCELLED: + break; + case FAILED: + break; + case SUCCEEDED: + try { + imageRef = new SoftReference<>(newReadImageTask.get()); + } catch (InterruptedException | ExecutionException interruptedException) { + } + break; + } + }); + return newReadImageTask; + } else { + return new Task() { + @Override + protected Image call() throws Exception { + return image; + } + }; } - - return image; } private SoftReference mediaRef; 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..6018c323c6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTile.java @@ -90,7 +90,7 @@ public class DrawableTile extends DrawableTileBase { } @Override - CachedLoaderTask> getNewImageLoadTask(DrawableFile file) { + CachedLoaderTask> newReadImageTask(DrawableFile file) { return new ThumbnailLoaderTask(file); } @@ -99,4 +99,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..aa16c22b5e 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,6 +67,7 @@ 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; @@ -221,15 +222,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,12 +307,12 @@ 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(); 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..8cc42c8306 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java @@ -18,32 +18,46 @@ */ 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.concurrent.WorkerStateEvent; 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 load 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 @@ -56,9 +70,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView private Optional> fileOpt = Optional.empty(); private Optional fileIDOpt = Optional.empty(); - private Task imageTask; - private SoftReference imageCache; - private ProgressIndicator progressIndicator; + protected volatile Task imageTask; public DrawableUIBase(ImageGalleryController controller) { this.controller = controller; @@ -107,20 +119,41 @@ 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); - } +// if (Objects.nonNull(newFileID)) { + setFileHelper(newFileID); +// } } - } else if (Objects.nonNull(newFileID)) { + } else {//if (Objects.nonNull(newFileID)) { setFileHelper(newFileID); } } synchronized protected void updateContent() { - Node content = getContentNode(); - Platform.runLater(() -> { - imageBorder.setCenter(content); + if (getFile().isPresent() == false) { + Platform.runLater(() -> imageBorder.setCenter(null)); + } else { + DrawableFile file = getFile().get(); + //is an image + doReadImageTask(file); + } + } + + synchronized Node doReadImageTask(DrawableFile file) { + disposeContent(); + final Task myTask = newReadImageTask(file); + imageTask = myTask; + Node progressNode = newProgressIndicator(myTask); + Platform.runLater(() -> imageBorder.setCenter(progressNode)); + + imageTask.setOnSucceeded((WorkerStateEvent event) -> { + showImage(file, myTask);//on fx thread already }); + imageTask.setOnFailed((WorkerStateEvent event) -> { + showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);//on fx thread already + }); + + exec.execute(myTask); + return progressNode; } synchronized protected void disposeContent() { @@ -128,50 +161,52 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView imageTask.cancel(true); } imageTask = null; - imageCache = null; + Platform.runLater(() -> imageView.setImage(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) + 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)); + + VBox vBox = new VBox(10, + new Label(errorMessage), createButton); + + vBox.setAlignment(Pos.CENTER); + imageBorder.setCenter(vBox); + } + + abstract Task newReadImageTask(DrawableFile file); abstract class CachedLoaderTask> extends Task { @@ -186,7 +221,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView return (isCancelled() == false) ? load() : null; } - abstract X load(); + abstract X load() throws Exception; @Override protected void succeeded() { @@ -194,7 +229,6 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView if (isCancelled() == false) { try { saveToCache(get()); - updateContent(); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.WARNING, "Failed to cache content for" + file.getName(), ex); } @@ -210,23 +244,9 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView abstract void saveToCache(X result); } - abstract class ImageLoaderTask extends CachedLoaderTask> { + class ThumbnailLoaderTask 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) { + ThumbnailLoaderTask(DrawableFile file) { super(file); } @@ -234,5 +254,10 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView Image load() { return isCancelled() ? null : file.getThumbnail(); } + + @Override + void saveToCache(Image result) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } } } 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..cb77dbd8b2 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java @@ -27,7 +27,6 @@ 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; @@ -52,6 +51,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,7 +166,7 @@ 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(() -> { @@ -177,25 +177,24 @@ public class MetaDataPane extends DrawableUIBase { }); } else { disposeContent(); - updateUI(); + updateAttributesTable(); + updateCategory(); updateContent(); } } @Override - CachedLoaderTask> getNewImageLoadTask(DrawableFile file) { + CachedLoaderTask> newReadImageTask(DrawableFile file) { return new ThumbnailLoaderTask(file); } - public void updateUI() { + public void updateAttributesTable() { getFile().ifPresent(file -> { final List, Collection>> attributesList = file.getAttributesList(); Platform.runLater(() -> { tableView.getItems().clear(); tableView.getItems().setAll(attributesList); }); - - updateCategory(); }); } @@ -204,13 +203,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 +221,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 +230,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 +242,5 @@ public class MetaDataPane extends DrawableUIBase { getValueDisplayString(selectedItem))); } } + } 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..13539084ca 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java @@ -23,15 +23,19 @@ 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; import javafx.collections.ObservableList; import javafx.concurrent.Task; +import javafx.concurrent.WorkerStateEvent; 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 +44,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,7 +55,9 @@ 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; +import org.sleuthkit.datamodel.TskCoreException; /** * Displays the files of a group one at a time. Designed to be embedded in a @@ -69,7 +76,7 @@ public class SlideShowView extends DrawableTileBase { @FXML private BorderPane footer; - private Task mediaTask; + SlideShowView(GroupPane gp, ImageGalleryController controller) { super(gp, controller); @@ -159,42 +166,87 @@ public class SlideShowView extends DrawableTileBase { stopVideo(); super.disposeContent(); - if (mediaTask != null) { - mediaTask.cancel(true); - } - mediaTask = null; +// if (mediaTask != null) { +// mediaTask.cancel(true); +// } +// mediaTask = null; mediaCache = null; } private SoftReference mediaCache; - /** - * {@inheritDoc } - */ @Override - Node getContentNode() { + synchronized protected void updateContent() { if (getFile().isPresent() == false) { mediaCache = null; - return super.getContentNode(); + Platform.runLater(() -> imageBorder.setCenter(null)); + } else { DrawableFile file = getFile().get(); if (file.isVideo()) { + //specially handling for videos Node mediaNode = (isNull(mediaCache)) ? null : mediaCache.get(); if (nonNull(mediaNode)) { - return mediaNode; + Platform.runLater(() -> imageBorder.setCenter(mediaNode)); } else { - if (isNull(mediaTask)) { - mediaTask = new MediaLoadTask(((VideoFile) file)); - new Thread(mediaTask).start(); - } else if (mediaTask.isDone()) { - return null; - } - return getLoadingProgressIndicator(); +// if (isNull(mediaTask)) { + MediaLoadTask mediaTask = new MediaLoadTask(((VideoFile) file)); + Node progressNode = newProgressIndicator(mediaTask); + + mediaTask.setOnSucceeded((WorkerStateEvent event) -> { + showMedia(file, mediaTask);//on fx thread already + }); + mediaTask.setOnFailed((WorkerStateEvent event) -> { + showErrorNode(getMediaLoadErrorLabel(mediaTask), file);//on fx thread already + }); + Platform.runLater(() -> imageBorder.setCenter(progressNode)); + exec.execute(mediaTask); +// } else { +// //not on fx thread; +// Platform.runLater(() -> showMedia(file)); +// } } + } else { + super.updateContent(); } - return super.getContentNode(); } } + @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,41 @@ 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 media: {0}"}) + private class MediaLoadTask extends Task { - public MediaLoadTask(VideoFile file) { - super(file); + private final VideoFile file; + + MediaLoadTask(VideoFile file) { + this.file = file; } @Override - void saveToCache(Node result) { - synchronized (SlideShowView.this) { - mediaCache = new SoftReference<>(result); - } - } - - @Override - Node load() { + protected Node call() throws Exception { + updateMessage(Bundle.MediaLoadTask_messageText(file.getName())); 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); + try { + Logger.getLogger(VideoFile.class.getName()).log(Level.WARNING, "Failed to initialize VideoPlayer for file {0} : {1}", new Object[]{file.getUniquePath(), ex.getLocalizedMessage()}); + } catch (TskCoreException tskCoreException) { + Logger.getLogger(VideoFile.class.getName()).log(Level.WARNING, "Failed to get unique path " + file.getName(), tskCoreException); + Logger.getLogger(VideoFile.class.getName()).log(Level.WARNING, "Failed to initialize VideoPlayer for file {0} : {1}", new Object[]{file.getName(), ex.getLocalizedMessage()}); + } if (file.isDisplayableAsImage()) { - Image fullSizeImage = file.getFullSizeImage(); - Platform.runLater(() -> { - imageView.setImage(fullSizeImage); - }); - return imageView; + return doReadImageTask(file); } - return new Text(ex.getLocalizedMessage() + "\nSee the logs for details.\n\nTry the \"Open In External Viewer\" action."); + + //if we couldn't even show it as an image. rethrow exception. + throw ex; } } } 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