Merge pull request #1773 from millmanorama/read-image-refactor

Read image refactor
This commit is contained in:
Richard Cordovano 2015-12-16 15:49:27 -05:00
commit 6976fde73e
3 changed files with 220 additions and 65 deletions

View File

@ -24,5 +24,10 @@
}
.bg {
-fx-background-color:black;
}
-fx-background-color: rgba(0, 0, 0, .8);
}
.masker-pane .masker-text {
-fx-text-fill: white;
-fx-font-size: 1.5em;
}

View File

@ -20,36 +20,51 @@ 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.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.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 javax.swing.SwingUtilities;
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;
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 +79,24 @@ 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).");
@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 {
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
@ -82,9 +107,11 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
* extensions we should be able to display
*/
static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
.map("."::concat)
.map("."::concat) //NOI18N
.collect(Collectors.toList());
private LoadImageTask readImageTask;
/**
* Creates new form MediaViewImagePanel
*/
@ -94,13 +121,15 @@ 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
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
@ -137,63 +166,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 +232,161 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
private class LoadImageTask extends Task<Image> implements IIOReadProgressListener {
private final AbstractFile file;
volatile private BufferedImage bufferedImage = null;
LoadImageTask(AbstractFile file) {
this.file = file;
}
@Override
@NbBundle.Messages({
"# {0} - file name",
"LoadImageTask.mesageText=Reading image: {0}"})
protected Image call() throws Exception {
updateMessage(Bundle.LoadImageTask_mesageText(file.getName()));
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
ImageInputStream input = ImageIO.createImageInputStream(inputStream);
if (input == null) {
throw new IIOException("Could not create ImageInputStream."); //NOI18N
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.addIIOReadProgressListener(this);
reader.setInput(input);
/*
* This is the important part, get or create a ReadParam,
* create a destination image to hold the decoded result,
* then pass that image with the param.
*/
ImageReadParam param = reader.getDefaultReadParam();
bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
param.setDestination(bufferedImage);
try {
reader.read(0, param);
} catch (IOException iOException) {
// Ignore this exception or display a warning or similar, for exceptions happening during decoding
logError(iOException);
}
reader.removeIIOReadProgressListener(this);
return SwingFXUtils.toFXImage(bufferedImage, null);
} else {
throw new IIOException("No ImageReader found for file."); //NOI18N
}
}
}
private void logError(@Nullable Throwable e) {
String message = e == null ? "" : "It may be unsupported or corrupt: " + e.getLocalizedMessage(); //NOI18N
try {
LOGGER.log(Level.WARNING, "The MediaView tab could not read the image: {0}. {1}", new Object[]{file.getUniquePath(), message}); //NOI18N
} catch (TskCoreException tskCoreException) {
LOGGER.log(Level.WARNING, "The MediaView tab could not read the image: {0}. {1}", new Object[]{file.getName(), message}); //NOI18N
LOGGER.log(Level.SEVERE, "Failes to get unique path for file", tskCoreException); //NOI18N
}
}
@Override
protected void failed() {
super.failed();
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and an
* image was previously selected
*/
reset();
return;
}
handleError(getException());
borderpane.setCursor(Cursor.DEFAULT);
}
private void handleError(Throwable e) {
logError(e);
externalViewerButton.setOnAction(actionEvent -> //fx ActionEvent
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent //NOI18N
);
borderpane.setCenter(errorNode);
}
@Override
protected void succeeded() {
super.succeeded();
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and an
* image was previously selected
*/
reset();
return;
}
try {
Image fxImage = get();
if (fxImage == null) {
handleError(null);
} else {
//we have non-null image show it
fxImageView.setImage(fxImage);
borderpane.setCenter(fxImageView);
if (fxImage.isError()) {
//if there was somekind of error, log it
logError(fxImage.getException());
}
}
} catch (InterruptedException | ExecutionException ex) {
handleError(ex.getCause());
}
borderpane.setCursor(Cursor.DEFAULT);
}
@Override
public void imageProgress(ImageReader source, float percentageDone) {
//update this task with the progress reported by ImageReader.read
updateProgress(percentageDone, 100);
}
@Override
public void sequenceStarted(ImageReader source, int minIndex) {
}
@Override
public void sequenceComplete(ImageReader source) {
}
@Override
public void imageStarted(ImageReader source, int imageIndex) {
}
@Override
public void imageComplete(ImageReader source) {
}
@Override
public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
}
@Override
public void thumbnailProgress(ImageReader source, float percentageDone) {
}
@Override
public void thumbnailComplete(ImageReader source) {
}
@Override
public void readAborted(ImageReader source) {
}
}
}

View File

@ -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) -> {