From 75909f3be094f59c0e9c9b942902cf1b99e088ee Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 11 Dec 2015 16:31:51 -0500 Subject: [PATCH 1/2] use lower level API to read images and show progress in MediaViewImagePanel some minor cleanup, restyling and the addition of a button to "Open in External Viewer" --- .../corecomponents/MediaViewImagePanel.css | 15 +- .../corecomponents/MediaViewImagePanel.java | 249 +++++++++++++----- 2 files changed, 204 insertions(+), 60 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css index 8cd592b28b..71d63072e7 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css @@ -24,5 +24,18 @@ } .bg { - -fx-background-color:black; + -fx-background-color: rgba(0, 0, 0, .8); +} + +.masker-pane .masker-text { + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-font-size: 1.5em; +} + +.masker-pane .masker-center { + -fx-background-color: rgba(0, 0, 0, .6); + -fx-max-height: 1.50in; + -fx-padding: .25in; + -fx-background-radius: .1in; } \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index cf8fd11741..6613012e60 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -22,34 +22,46 @@ import java.awt.Dimension; import java.awt.EventQueue; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.Collections; +import java.util.Iterator; import java.util.List; 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.embed.swing.JFXPanel; import javafx.embed.swing.SwingFXUtils; +import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +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 javax.swing.SwingUtilities; -import org.openide.util.NbBundle; +import org.controlsfx.control.MaskerPane; import org.python.google.common.collect.Lists; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; +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 @@ -64,14 +76,16 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi private JFXPanel fxPanel; private ImageView fxImageView; private BorderPane borderpane; - + private final ProgressBar progressBar = new ProgressBar(); + private final MaskerPane maskerPane = new MaskerPane(); private final Label errorLabel = new Label("Could not load file into media view."); - private final Label tooLargeLabel = new Label("Could not load file into media view (too large)."); + private final Button externalViewerButton = new Button("Open in External Viewer"); + private final VBox errorNode = new VBox(10, errorLabel, externalViewerButton); static { ImageIO.scanForPlugins(); - } + /** * mime types we should be able to display. if the mimetype is unknown we * will fall back on extension and jpg/png header @@ -85,6 +99,8 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi .map("."::concat) .collect(Collectors.toList()); + private LoadImageTask readImageTask; + /** * Creates new form MediaViewImagePanel */ @@ -94,6 +110,8 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi if (fxInited) { Platform.runLater(() -> { + errorNode.setAlignment(Pos.CENTER); + // build jfx ui (we could do this in FXML?) fxImageView = new ImageView(); // will hold image borderpane = new BorderPane(fxImageView); // centers and sizes imageview @@ -137,63 +155,23 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi * @param file image file to show * @param dims dimension of the parent window (ignored) */ - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void showImageFx(final AbstractFile file, final Dimension dims) { if (!fxInited) { return; } - //hide the panel during loading/transformations - //TODO: repalce this with a progress indicator - fxPanel.setVisible(false); - - // load the image - Platform.runLater(new Runnable() { - @Override - public void run() { - if (!Case.isCaseOpen()) { - /* - * handle in-between condition when case is being closed and - * an image was previously selected - */ - return; - } - try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { - - BufferedImage bufferedImage = ImageIO.read(inputStream); - if (bufferedImage == null) { - LOGGER.log(Level.WARNING, "Image reader not found for file: {0}", file.getName()); //NON-NLS - borderpane.setCenter(errorLabel); - } else { - Image fxImage = SwingFXUtils.toFXImage(bufferedImage, null); - if (fxImage.isError()) { - LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), fxImage.getException()); //NON-NLS - borderpane.setCenter(errorLabel); - return; - } else { - fxImageView.setImage(fxImage); - borderpane.setCenter(fxImageView); - } - } - } catch (EOFException ex) { - LOGGER.log(Level.WARNING, "Could not load image file into media view (EOF): {0}", file.getName()); //NON-NLS - borderpane.setCenter(errorLabel); - } catch (IllegalArgumentException | IOException ex) { - LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), ex); //NON-NLS - borderpane.setCenter(errorLabel); - } catch (OutOfMemoryError ex) { // this might be redundant since we are not attempting to rescale the image anymore - LOGGER.log(Level.WARNING, "Could not load image file into media view (too large): " + file.getName(), ex); //NON-NLS - MessageNotifyUtil.Notify.warn( - NbBundle.getMessage(this.getClass(), "MediaViewImagePanel.imgFileTooLarge.msg", file.getName()), - ex.getMessage()); - borderpane.setCenter(tooLargeLabel); - } - - SwingUtilities.invokeLater(() -> { - //show the panel after fully loaded - fxPanel.setVisible(true); - }); + Platform.runLater(() -> { + if (readImageTask != null) { + readImageTask.cancel(); } + readImageTask = new LoadImageTask(file); + + maskerPane.setProgressNode(progressBar); + progressBar.progressProperty().bind(readImageTask.progressProperty()); + maskerPane.textProperty().bind(readImageTask.messageProperty()); + borderpane.setCenter(maskerPane); + borderpane.setCursor(Cursor.WAIT); + new Thread(readImageTask).start(); }); } @@ -243,4 +221,157 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi }// //GEN-END:initComponents // 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 + protected Image call() throws Exception { + updateMessage("Reading image: " + file.getName()); + try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { + ImageInputStream input = ImageIO.createImageInputStream(inputStream); // TODO: null check + if (input == null) { + throw new IIOException("Could not create ImageInputStream."); + } + Iterator readers = ImageIO.getImageReaders(input); // TODO: hasNext check + + 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) { + logError(iOException); + } + reader.removeIIOReadProgressListener(this); + return SwingFXUtils.toFXImage(bufferedImage, null); + } else { + throw new IIOException("No ImageReader found for file."); + } + } + } + + private void logError(Throwable e) { + String message = e != null ? " It may be unsupported or corrupt: " + e.getLocalizedMessage() : ""; + // Ignore this exception or display a warning or similar, for exceptions happening during decoding + try { + LOGGER.log(Level.WARNING, "There was a problem loading " + file.getUniquePath() + "." + message, e); + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "There was a problem loading " + file.getName() + "." + message, e); + LOGGER.log(Level.SEVERE, "Failes to get unique path for file", tskCoreException); + } + } + + @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 -> + new ExternalViewerAction("", new FileNode(file)).actionPerformed(new java.awt.event.ActionEvent(this, java.awt.event.ActionEvent.ACTION_PERFORMED, "")) + ); + 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) { + } + } + } From 7e2751ec7ddc143ba4cdefd0e8df719270a3cd00 Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 11 Dec 2015 17:03:50 -0500 Subject: [PATCH 2/2] cleanup and internationalization --- .../corecomponents/MediaViewImagePanel.css | 8 --- .../corecomponents/MediaViewImagePanel.java | 51 ++++++++++++------- .../gui/drawableviews/DrawableTileBase.java | 8 ++- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css index 71d63072e7..448b01a21b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css @@ -29,13 +29,5 @@ .masker-pane .masker-text { -fx-text-fill: white; - -fx-font-weight: bold; -fx-font-size: 1.5em; } - -.masker-pane .masker-center { - -fx-background-color: rgba(0, 0, 0, .6); - -fx-max-height: 1.50in; - -fx-padding: .25in; - -fx-background-radius: .1in; -} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index 6613012e60..f4b971961a 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -20,6 +20,7 @@ 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; @@ -45,6 +46,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; @@ -53,6 +55,7 @@ import javax.imageio.event.IIOReadProgressListener; import javax.imageio.stream.ImageInputStream; import javax.swing.JPanel; import org.controlsfx.control.MaskerPane; +import org.openide.util.NbBundle; import org.python.google.common.collect.Lists; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.ImageUtils; @@ -78,8 +81,16 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi private BorderPane borderpane; private final ProgressBar progressBar = new ProgressBar(); private final MaskerPane maskerPane = new MaskerPane(); - private final Label errorLabel = new Label("Could not load file into media view."); - private final Button externalViewerButton = new Button("Open in External Viewer"); + + @NbBundle.Messages({"MediaViewImagePanel.errorLabel.text=Could not load file into Media view."}) + private final Label errorLabel = new Label(Bundle.MediaViewImagePanel_errorLabel_text()); + + /** + * TODO: why is this passed to the action? it means we duplciate this string + * all over the place -jm + */ + @NbBundle.Messages({"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer"}) + private final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text()); private final VBox errorNode = new VBox(10, errorLabel, externalViewerButton); static { @@ -96,7 +107,7 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi * extensions we should be able to display */ static private final List supportedExtensions = ImageUtils.getSupportedImageExtensions().stream() - .map("."::concat) + .map("."::concat) //NOI18N .collect(Collectors.toList()); private LoadImageTask readImageTask; @@ -115,10 +126,10 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi // build jfx ui (we could do this in FXML?) fxImageView = new ImageView(); // will hold image borderpane = new BorderPane(fxImageView); // centers and sizes imageview - borderpane.getStyleClass().add("bg"); + borderpane.getStyleClass().add("bg"); //NOI18N fxPanel = new JFXPanel(); // bridge jfx-swing Scene scene = new Scene(borderpane); //root of jfx tree - scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); + scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N fxPanel.setScene(scene); //bind size of image to that of scene, while keeping proportions @@ -232,14 +243,17 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi } @Override + @NbBundle.Messages({ + "# {0} - file name", + "LoadImageTask.mesageText=Reading image: {0}"}) protected Image call() throws Exception { - updateMessage("Reading image: " + file.getName()); + updateMessage(Bundle.LoadImageTask_mesageText(file.getName())); try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { - ImageInputStream input = ImageIO.createImageInputStream(inputStream); // TODO: null check + ImageInputStream input = ImageIO.createImageInputStream(inputStream); if (input == null) { - throw new IIOException("Could not create ImageInputStream."); + throw new IIOException("Could not create ImageInputStream."); //NOI18N } - Iterator readers = ImageIO.getImageReaders(input); // TODO: hasNext check + Iterator readers = ImageIO.getImageReaders(input); if (readers.hasNext()) { ImageReader reader = readers.next(); @@ -257,24 +271,24 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi 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."); + throw new IIOException("No ImageReader found for file."); //NOI18N } } } - private void logError(Throwable e) { - String message = e != null ? " It may be unsupported or corrupt: " + e.getLocalizedMessage() : ""; - // Ignore this exception or display a warning or similar, for exceptions happening during decoding + private void logError(@Nullable Throwable e) { + String message = e == null ? "" : "It may be unsupported or corrupt: " + e.getLocalizedMessage(); //NOI18N try { - LOGGER.log(Level.WARNING, "There was a problem loading " + file.getUniquePath() + "." + message, e); + 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, "There was a problem loading " + file.getName() + "." + message, e); - LOGGER.log(Level.SEVERE, "Failes to get unique path for file", 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 } } @@ -297,8 +311,9 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi private void handleError(Throwable e) { logError(e); - externalViewerButton.setOnAction(actionEvent -> - new ExternalViewerAction("", new FileNode(file)).actionPerformed(new java.awt.event.ActionEvent(this, java.awt.event.ActionEvent.ACTION_PERFORMED, "")) + 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); } 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 12cb55358b..17687b54f7 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java @@ -48,6 +48,7 @@ import javafx.scene.paint.Color; import javax.swing.Action; import javax.swing.SwingUtilities; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.openide.util.actions.Presenter; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; @@ -80,7 +81,10 @@ import org.sleuthkit.datamodel.TskCoreException; * since they share a similar node tree and many behaviors, other implementors * of {@link DrawableView}s should implement the interface directly * + * + * TODO: refactor ExternalViewerAction to supply its own name */ +@NbBundle.Messages({"DrawableTileBase.externalViewerAction.text=Open in External Viewer"}) public abstract class DrawableTileBase extends DrawableUIBase { private static final Logger LOGGER = Logger.getLogger(DrawableTileBase.class.getName()); @@ -217,8 +221,8 @@ public abstract class DrawableTileBase extends DrawableUIBase { }); menuItems.add(contentViewer); - MenuItem externalViewer = new MenuItem("Open in External Viewer"); - final ExternalViewerAction externalViewerAction = new ExternalViewerAction("Open in External Viewer", new FileNode(file.getAbstractFile())); + 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) -> {