/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.corecomponents;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.util.Collections;
import java.util.List;
import static java.util.Objects.nonNull;
import java.util.SortedSet;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.embed.swing.JFXPanel;
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.ImageIO;
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;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
import org.sleuthkit.datamodel.AbstractFile;
/**
* Image viewer part of the Media View layered pane. Uses JavaFX to display the
* image.
*/
@NbBundle.Messages({"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer",
"MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
"MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."})
public class MediaViewImagePanel extends JPanel implements DataContentViewerMedia.MediaViewPanel {
private static final Image EXTERNAL = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm());
private static final Logger LOGGER = Logger.getLogger(MediaViewImagePanel.class.getName());
private final boolean fxInited;
private JFXPanel fxPanel;
private ImageView fxImageView;
private BorderPane borderpane;
private final ProgressBar progressBar = new ProgressBar();
private final MaskerPane maskerPane = new MaskerPane();
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
*/
static private final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
/**
* extensions we should be able to display
*/
static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
.map("."::concat) //NOI18N
.collect(Collectors.toList());
private Task<Image> readImageTask;
/**
* Creates new form MediaViewImagePanel
*/
public MediaViewImagePanel() {
initComponents();
fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited();
if (fxInited) {
Platform.runLater(() -> {
// 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"); //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()); //NOI18N
fxPanel.setScene(scene);
//bind size of image to that of scene, while keeping proportions
fxImageView.fitWidthProperty().bind(scene.widthProperty());
fxImageView.fitHeightProperty().bind(scene.heightProperty());
fxImageView.setPreserveRatio(true);
fxImageView.setSmooth(true);
fxImageView.setCache(true);
EventQueue.invokeLater(() -> {
add(fxPanel);//add jfx ui to JPanel
});
});
}
}
public boolean isInited() {
return fxInited;
}
/**
* clear the displayed image
*/
public void reset() {
Platform.runLater(() -> {
fxImageView.setImage(null);
borderpane.setCenter(null);
});
}
private void showErrorNode(String errorMessage, AbstractFile file) {
final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(EXTERNAL));
externalViewerButton.setOnAction(actionEvent -> //fx ActionEvent
/*
* TODO: why is the name passed into the action constructor? it
* means we duplicate this string all over the place -jm
*/
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent
);
final VBox errorNode = new VBox(10, new Label(errorMessage), externalViewerButton);
errorNode.setAlignment(Pos.CENTER);
borderpane.setCenter(errorNode);
}
/**
* Show the contents of the given AbstractFile as a visual image.
*
* @param file image file to show
* @param dims dimension of the parent window (ignored)
*/
void showImageFx(final AbstractFile file, final Dimension dims) {
if (!fxInited) {
return;
}
Platform.runLater(() -> {
if (readImageTask != null) {
readImageTask.cancel();
}
readImageTask = ImageUtils.newReadImageTask(file);
readImageTask.setOnSucceeded(succeeded -> {
//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
*
* NOTE: I think this is unnecessary -jm
*/
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(Bundle.MediaViewImagePanel_errorLabel_text(), file);
}
} catch (InterruptedException | ExecutionException ex) {
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
}
borderpane.setCursor(Cursor.DEFAULT);
});
readImageTask.setOnFailed(failed -> {
if (!Case.isCaseOpen()) {
/*
* handle in-between condition when case is being closed and
* an image was previously selected
*
* NOTE: I think this is unnecessary -jm
*/
reset();
return;
}
Throwable exception = readImageTask.getException();
if (exception instanceof OutOfMemoryError
&& exception.getMessage().contains("Java heap space")) {
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
} else {
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
}
borderpane.setCursor(Cursor.DEFAULT);
});
maskerPane.setProgressNode(progressBar);
progressBar.progressProperty().bind(readImageTask.progressProperty());
maskerPane.textProperty().bind(readImageTask.messageProperty());
borderpane.setCenter(maskerPane);
borderpane.setCursor(Cursor.WAIT);
new Thread(readImageTask).start();
});
}
/**
* @return supported mime types
*/
@Override
public List<String> getMimeTypes() {
return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
}
/**
* returns supported extensions (each starting with .)
*
* @return
*/
@Override
public List<String> getExtensionsList() {
return getExtensions();
}
/**
* returns supported extensions (each starting with .)
*
* @return
*/
public List<String> getExtensions() {
return Collections.unmodifiableList(supportedExtensions);
}
@Override
public boolean isSupported(AbstractFile file) {
return ImageUtils.isImageThumbnailSupported(file);
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
setBackground(new java.awt.Color(0, 0, 0));
setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
}