19 package org.sleuthkit.autopsy.contentviewers;
 
   21 import java.awt.Dimension;
 
   22 import java.awt.EventQueue;
 
   23 import java.awt.event.ActionEvent;
 
   24 import java.util.Collections;
 
   25 import java.util.List;
 
   26 import static java.util.Objects.nonNull;
 
   27 import java.util.SortedSet;
 
   28 import java.util.concurrent.ExecutionException;
 
   29 import java.util.stream.Collectors;
 
   30 import javafx.application.Platform;
 
   31 import javafx.concurrent.Task;
 
   32 import javafx.embed.swing.JFXPanel;
 
   33 import javafx.geometry.Pos;
 
   34 import javafx.geometry.Rectangle2D;
 
   35 import javafx.scene.Cursor;
 
   36 import javafx.scene.Scene;
 
   37 import javafx.scene.control.Button;
 
   38 import javafx.scene.control.Label;
 
   39 import javafx.scene.control.ProgressBar;
 
   40 import javafx.scene.control.ScrollPane;
 
   41 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
 
   42 import javafx.scene.image.Image;
 
   43 import javafx.scene.image.ImageView;
 
   44 import javafx.scene.layout.VBox;
 
   45 import javafx.scene.transform.Rotate;
 
   46 import javafx.scene.transform.Scale;
 
   47 import javafx.scene.transform.Translate;
 
   48 import javax.imageio.ImageIO;
 
   49 import javax.swing.JPanel;
 
   50 import org.controlsfx.control.MaskerPane;
 
   51 import org.openide.util.NbBundle;
 
   52 import org.python.google.common.collect.Lists;
 
   63 @NbBundle.Messages({
"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer  Ctrl+E",
 
   64     "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
 
   65     "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."})
 
   66 @SuppressWarnings(
"PMD.SingularField") 
 
   67 class MediaViewImagePanel 
extends JPanel implements MediaFileViewer.MediaViewPanel {
 
   69     private static final Image EXTERNAL = 
new Image(MediaViewImagePanel.class.getResource(
"/org/sleuthkit/autopsy/images/external.png").toExternalForm());
 
   71     private final boolean fxInited;
 
   73     private JFXPanel fxPanel;
 
   74     private ImageView fxImageView;
 
   75     private ScrollPane scrollPane;
 
   76     private final ProgressBar progressBar = 
new ProgressBar();
 
   77     private final MaskerPane maskerPane = 
new MaskerPane();
 
   79     private double zoomRatio;
 
   80     private double rotation; 
 
   82     private static final double[] ZOOM_STEPS = {
 
   83         0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
 
   84         1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
 
   86     private static final double MIN_ZOOM_RATIO = 0.0625; 
 
   87     private static final double MAX_ZOOM_RATIO = 10.0; 
 
   90         ImageIO.scanForPlugins();
 
   97     static private final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
 
  102     static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
 
  104             .collect(Collectors.toList());
 
  106     private Task<Image> readImageTask;
 
  111     public MediaViewImagePanel() {
 
  115             Platform.runLater(() -> {
 
  118                 fxImageView = 
new ImageView();  
 
  119                 scrollPane = 
new ScrollPane(fxImageView); 
 
  120                 scrollPane.getStyleClass().add(
"bg"); 
 
  121                 scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
 
  122                 scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
 
  124                 fxPanel = 
new JFXPanel(); 
 
  125                 Scene scene = 
new Scene(scrollPane); 
 
  126                 scene.getStylesheets().add(MediaViewImagePanel.class.getResource(
"MediaViewImagePanel.css").toExternalForm()); 
 
  127                 fxPanel.setScene(scene);
 
  129                 fxImageView.setSmooth(
true);
 
  130                 fxImageView.setCache(
true);
 
  132                 EventQueue.invokeLater(() -> {
 
  139     public boolean isInited() {
 
  146     public void reset() {
 
  147         Platform.runLater(() -> {
 
  148             fxImageView.setViewport(
new Rectangle2D(0, 0, 0, 0));
 
  149             fxImageView.setImage(null);
 
  151             scrollPane.setContent(null);
 
  152             scrollPane.setContent(fxImageView);
 
  156     private void showErrorNode(String errorMessage, AbstractFile file) {
 
  157         final Button externalViewerButton = 
new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), 
new ImageView(EXTERNAL));
 
  158         externalViewerButton.setOnAction(actionEvent
 
  163  new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), 
new FileNode(file))
 
  164                 .actionPerformed(
new ActionEvent(
this, ActionEvent.ACTION_PERFORMED, 
"")) 
 
  167         final VBox errorNode = 
new VBox(10, 
new Label(errorMessage), externalViewerButton);
 
  168         errorNode.setAlignment(Pos.CENTER);
 
  177     void showImageFx(
final AbstractFile file, 
final Dimension dims) {
 
  182         Platform.runLater(() -> {
 
  183             if (readImageTask != null) {
 
  184                 readImageTask.cancel();
 
  186             readImageTask = ImageUtils.newReadImageTask(file);
 
  187             readImageTask.setOnSucceeded(succeeded -> {
 
  188                 if (!Case.isCaseOpen()) {
 
  200                     Image fxImage = readImageTask.get();
 
  201                     if (nonNull(fxImage)) {
 
  203                         fxImageView.setImage(fxImage);
 
  205                         scrollPane.setContent(fxImageView);
 
  207                         showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  209                 } 
catch (InterruptedException | ExecutionException ex) {
 
  210                     showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  212                 scrollPane.setCursor(Cursor.DEFAULT);
 
  214             readImageTask.setOnFailed(failed -> {
 
  215                 if (!Case.isCaseOpen()) {
 
  225                 Throwable exception = readImageTask.getException();
 
  226                 if (exception instanceof OutOfMemoryError
 
  227                         && exception.getMessage().contains(
"Java heap space")) {
 
  228                     showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
 
  230                     showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  233                 scrollPane.setCursor(Cursor.DEFAULT);
 
  236             maskerPane.setProgressNode(progressBar);
 
  237             progressBar.progressProperty().bind(readImageTask.progressProperty());
 
  238             maskerPane.textProperty().bind(readImageTask.messageProperty());
 
  239             scrollPane.setContent(null); 
 
  240             scrollPane.setCursor(Cursor.WAIT);
 
  241             new Thread(readImageTask).start();
 
  249     public List<String> getSupportedMimeTypes() {
 
  250         return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
 
  259     public List<String> getSupportedExtensions() {
 
  260         return getExtensions();
 
  268     public List<String> getExtensions() {
 
  269         return Collections.unmodifiableList(supportedExtensions);
 
  273     public boolean isSupported(AbstractFile file) {
 
  274         return ImageUtils.isImageThumbnailSupported(file);
 
  282     @SuppressWarnings(
"unchecked")
 
  284     private 
void initComponents() {
 
  286         toolbar = 
new javax.swing.JToolBar();
 
  287         rotationTextField = 
new javax.swing.JTextField();
 
  288         rotateLeftButton = 
new javax.swing.JButton();
 
  289         rotateRightButton = 
new javax.swing.JButton();
 
  290         jSeparator1 = 
new javax.swing.JToolBar.Separator();
 
  291         zoomTextField = 
new javax.swing.JTextField();
 
  292         zoomOutButton = 
new javax.swing.JButton();
 
  293         zoomInButton = 
new javax.swing.JButton();
 
  294         jSeparator2 = 
new javax.swing.JToolBar.Separator();
 
  295         zoomResetButton = 
new javax.swing.JButton();
 
  297         setBackground(
new java.awt.Color(0, 0, 0));
 
  298         addComponentListener(
new java.awt.event.ComponentAdapter() {
 
  299             public void componentResized(java.awt.event.ComponentEvent evt) {
 
  300                 formComponentResized(evt);
 
  303         setLayout(
new javax.swing.BoxLayout(
this, javax.swing.BoxLayout.Y_AXIS));
 
  305         toolbar.setFloatable(
false);
 
  306         toolbar.setRollover(
true);
 
  307         toolbar.setMaximumSize(
new java.awt.Dimension(32767, 23));
 
  310         rotationTextField.setEditable(
false);
 
  311         rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
 
  312         rotationTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotationTextField.text")); 
 
  313         rotationTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
 
  314         rotationTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
 
  315         rotationTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
 
  316         toolbar.add(rotationTextField);
 
  318         rotateLeftButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png"))); 
 
  319         org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateLeftButton.text")); 
 
  320         rotateLeftButton.setToolTipText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateLeftButton.toolTipText")); 
 
  321         rotateLeftButton.setFocusable(
false);
 
  322         rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  323         rotateLeftButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  324         rotateLeftButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  325         rotateLeftButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  326         rotateLeftButton.addActionListener(
new java.awt.event.ActionListener() {
 
  327             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  328                 rotateLeftButtonActionPerformed(evt);
 
  331         toolbar.add(rotateLeftButton);
 
  333         rotateRightButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png"))); 
 
  334         org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateRightButton.text")); 
 
  335         rotateRightButton.setFocusable(
false);
 
  336         rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  337         rotateRightButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  338         rotateRightButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  339         rotateRightButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  340         rotateRightButton.addActionListener(
new java.awt.event.ActionListener() {
 
  341             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  342                 rotateRightButtonActionPerformed(evt);
 
  345         toolbar.add(rotateRightButton);
 
  347         jSeparator1.setMaximumSize(
new java.awt.Dimension(6, 20));
 
  348         toolbar.add(jSeparator1);
 
  350         zoomTextField.setEditable(
false);
 
  351         zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
 
  352         zoomTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomTextField.text")); 
 
  353         zoomTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
 
  354         zoomTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
 
  355         zoomTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
 
  356         toolbar.add(zoomTextField);
 
  358         zoomOutButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png"))); 
 
  359         org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomOutButton.text")); 
 
  360         zoomOutButton.setFocusable(
false);
 
  361         zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  362         zoomOutButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  363         zoomOutButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  364         zoomOutButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  365         zoomOutButton.addActionListener(
new java.awt.event.ActionListener() {
 
  366             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  367                 zoomOutButtonActionPerformed(evt);
 
  370         toolbar.add(zoomOutButton);
 
  372         zoomInButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png"))); 
 
  373         org.openide.awt.Mnemonics.setLocalizedText(zoomInButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomInButton.text")); 
 
  374         zoomInButton.setFocusable(
false);
 
  375         zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  376         zoomInButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  377         zoomInButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  378         zoomInButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  379         zoomInButton.addActionListener(
new java.awt.event.ActionListener() {
 
  380             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  381                 zoomInButtonActionPerformed(evt);
 
  384         toolbar.add(zoomInButton);
 
  386         jSeparator2.setMaximumSize(
new java.awt.Dimension(6, 20));
 
  387         toolbar.add(jSeparator2);
 
  389         org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomResetButton.text")); 
 
  390         zoomResetButton.setFocusable(
false);
 
  391         zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  392         zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
 
  393         zoomResetButton.addActionListener(
new java.awt.event.ActionListener() {
 
  394             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  395                 zoomResetButtonActionPerformed(evt);
 
  398         toolbar.add(zoomResetButton);
 
  403     private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  404         rotation = (rotation + 270) % 360;
 
  408     private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  409         rotation = (rotation + 90) % 360;
 
  413     private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  415         for (
int i=0; i < ZOOM_STEPS.length; i++) {
 
  416             if (zoomRatio < ZOOM_STEPS[i]) {
 
  417                 zoomRatio = ZOOM_STEPS[i];
 
  424     private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  426         for (
int i=ZOOM_STEPS.length-1; i >= 0; i--) {
 
  427             if (zoomRatio > ZOOM_STEPS[i]) {
 
  428                 zoomRatio = ZOOM_STEPS[i];
 
  435     private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  439     private void formComponentResized(java.awt.event.ComponentEvent evt) {
 
  444     private javax.swing.JToolBar.Separator jSeparator1;
 
  445     private javax.swing.JToolBar.Separator jSeparator2;
 
  446     private javax.swing.JButton rotateLeftButton;
 
  447     private javax.swing.JButton rotateRightButton;
 
  448     private javax.swing.JTextField rotationTextField;
 
  449     private javax.swing.JToolBar toolbar;
 
  450     private javax.swing.JButton zoomInButton;
 
  451     private javax.swing.JButton zoomOutButton;
 
  452     private javax.swing.JButton zoomResetButton;
 
  453     private javax.swing.JTextField zoomTextField;
 
  464     private void resetView() {
 
  465         Image image = fxImageView.getImage();
 
  470         double imageWidth = image.getWidth();
 
  471         double imageHeight = image.getHeight();
 
  472         double scrollPaneWidth = fxPanel.getWidth();
 
  473         double scrollPaneHeight = fxPanel.getHeight();
 
  474         double zoomRatioWidth = scrollPaneWidth / imageWidth;
 
  475         double zoomRatioHeight = scrollPaneHeight / imageHeight;
 
  478         zoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
 
  482         scrollPane.setHvalue(0);
 
  483         scrollPane.setVvalue(0);
 
  498     private void updateView() {
 
  499         Image image = fxImageView.getImage();
 
  505         double imageWidth = image.getWidth();
 
  506         double imageHeight = image.getHeight();
 
  509         double adjustedImageWidth = imageWidth * zoomRatio;
 
  510         double adjustedImageHeight = imageHeight * zoomRatio;
 
  513         double viewportWidth;
 
  514         double viewportHeight;
 
  517         double panelWidth = fxPanel.getWidth();
 
  518         double panelHeight = fxPanel.getHeight();
 
  521         double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
 
  522         double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
 
  529         double scrollX = scrollPane.getHvalue();
 
  530         double scrollY = scrollPane.getVvalue();
 
  537         if ((rotation % 180) == 0) {
 
  539             viewportWidth = adjustedImageWidth;
 
  540             viewportHeight = adjustedImageHeight;
 
  541             leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
 
  542             topOffsetY = (adjustedImageHeight - imageHeight) / 2;
 
  543             maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
 
  544             maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
 
  547             viewportWidth = adjustedImageHeight;
 
  548             viewportHeight = adjustedImageWidth;
 
  549             leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
 
  550             topOffsetY = (adjustedImageWidth - imageHeight) / 2;
 
  551             maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
 
  552             maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
 
  556         if (viewportWidth < imageWidth) {
 
  557             viewportWidth = imageWidth;
 
  558             if (scrollX > maxScrollX) {
 
  559                 scrollX = maxScrollX;
 
  562         if (viewportHeight < imageHeight) {
 
  563             viewportHeight = imageHeight;
 
  564             if (scrollY > maxScrollY) {
 
  565                 scrollY = maxScrollY;
 
  570         fxImageView.setViewport(
new Rectangle2D(
 
  571                 0, 0, viewportWidth, viewportHeight));
 
  574         Scale scale = 
new Scale();
 
  575         scale.setX(zoomRatio);
 
  576         scale.setY(zoomRatio);
 
  577         scale.setPivotX(imageWidth / 2);
 
  578         scale.setPivotY(imageHeight / 2);
 
  581         Rotate rotate = 
new Rotate();
 
  582         rotate.setPivotX(imageWidth / 2);
 
  583         rotate.setPivotY(imageHeight / 2);
 
  584         rotate.setAngle(rotation);
 
  587         Translate translate = 
new Translate();
 
  588         translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
 
  589         translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
 
  594         fxImageView.getTransforms().clear();
 
  595         fxImageView.getTransforms().addAll(translate, rotate, scale);
 
  598         if (viewportWidth > fxPanel.getWidth()) {
 
  599             scrollPane.setHvalue(scrollX);
 
  601         if (viewportHeight > fxPanel.getHeight()) {
 
  602             scrollPane.setVvalue(scrollY);
 
  606         zoomOutButton.setEnabled(zoomRatio > MIN_ZOOM_RATIO);
 
  607         zoomInButton.setEnabled(zoomRatio < MAX_ZOOM_RATIO);
 
  608         rotationTextField.setText((
int) rotation + 
"°");
 
  609         zoomTextField.setText((Math.round(zoomRatio * 100.0)) + 
"%");
 
static boolean isJavaFxInited()