19 package org.sleuthkit.autopsy.contentviewers;
 
   21 import com.google.common.collect.Lists;
 
   22 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 
   23 import java.awt.EventQueue;
 
   24 import java.awt.event.ActionEvent;
 
   25 import java.awt.image.BufferedImage;
 
   26 import java.beans.PropertyChangeEvent;
 
   27 import java.beans.PropertyChangeListener;
 
   28 import java.beans.PropertyChangeSupport;
 
   30 import java.nio.file.Path;
 
   31 import java.nio.file.Paths;
 
   32 import java.util.ArrayList;
 
   33 import java.util.Collection;
 
   34 import java.util.Collections;
 
   35 import java.util.List;
 
   36 import static java.util.Objects.nonNull;
 
   37 import java.util.concurrent.ExecutionException;
 
   38 import java.util.concurrent.ExecutorService;
 
   39 import java.util.concurrent.Executors;
 
   40 import java.util.concurrent.FutureTask;
 
   41 import java.util.logging.Level;
 
   42 import java.util.stream.Collectors;
 
   43 import javafx.application.Platform;
 
   44 import javafx.collections.ListChangeListener.Change;
 
   45 import javafx.concurrent.Task;
 
   46 import javafx.embed.swing.JFXPanel;
 
   47 import javafx.geometry.Pos;
 
   48 import javafx.geometry.Rectangle2D;
 
   49 import javafx.scene.Cursor;
 
   50 import javafx.scene.Group;
 
   51 import javafx.scene.Scene;
 
   52 import javafx.scene.control.Button;
 
   53 import javafx.scene.control.Label;
 
   54 import javafx.scene.control.ProgressBar;
 
   55 import javafx.scene.control.ScrollPane;
 
   56 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
 
   57 import javafx.scene.image.Image;
 
   58 import javafx.scene.image.ImageView;
 
   59 import javafx.scene.layout.VBox;
 
   60 import javafx.scene.transform.Rotate;
 
   61 import javafx.scene.transform.Scale;
 
   62 import javafx.scene.transform.Translate;
 
   63 import javax.imageio.ImageIO;
 
   64 import javax.swing.JFileChooser;
 
   65 import javafx.scene.Node;
 
   66 import javax.annotation.concurrent.Immutable;
 
   67 import javax.swing.JMenuItem;
 
   68 import javax.swing.JOptionPane;
 
   69 import javax.swing.JPanel;
 
   70 import javax.swing.JPopupMenu;
 
   71 import javax.swing.JSeparator;
 
   72 import javax.swing.SwingUtilities;
 
   73 import javax.swing.SwingWorker;
 
   74 import org.apache.commons.io.FilenameUtils;
 
   75 import org.controlsfx.control.MaskerPane;
 
   76 import org.openide.util.NbBundle;
 
  107     "MediaViewImagePanel.externalViewerButton.text=Open in External Viewer  Ctrl+E",
 
  108     "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
 
  109     "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory." 
  111 @SuppressWarnings(
"PMD.SingularField") 
 
  112 class MediaViewImagePanel 
extends JPanel implements MediaFileViewer.MediaViewPanel {
 
  114     private static final long serialVersionUID = 1L;
 
  115     private static final Logger logger = Logger.getLogger(MediaViewImagePanel.class.getName());
 
  116     private static final double[] ZOOM_STEPS = {
 
  117         0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
 
  118         1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
 
  119     private static final double MIN_ZOOM_RATIO = 0.0625; 
 
  120     private static final double MAX_ZOOM_RATIO = 10.0; 
 
  121     private static final Image openInExternalViewerButtonImage = 
new Image(MediaViewImagePanel.class.getResource(
"/org/sleuthkit/autopsy/images/external.png").toExternalForm()); 
 
  123     private final PropertyChangeSupport pcs = 
new PropertyChangeSupport(
this);
 
  128     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  129     private final ProgressBar progressBar = new ProgressBar();
 
  130     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  131     private final MaskerPane maskerPane = new MaskerPane();
 
  132     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  133     private Group masterGroup;
 
  134     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  135     private ImageTagsGroup tagsGroup;
 
  136     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  137     private ImageTagCreator imageTagCreator;
 
  138     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  139     private ImageView fxImageView;
 
  140     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  141     private ScrollPane scrollPane;
 
  146     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  147     private final JPopupMenu imageTaggingOptions;
 
  148     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  149     private final JMenuItem createTagMenuItem;
 
  150     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  151     private final JMenuItem deleteTagMenuItem;
 
  152     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  153     private final JMenuItem hideTagsMenuItem;
 
  154     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  155     private final JMenuItem exportTagsMenuItem;
 
  156     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  157     private JFileChooser exportChooser;
 
  158     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  159     private final JFXPanel fxPanel;
 
  191     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  192     private AbstractFile imageFile;
 
  193     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  194     private Task<Image> readImageFileTask;
 
  195     private volatile ImageTransforms imageTransforms;
 
  199     private final FutureTask<JFileChooser> futureFileChooser = new FutureTask<>(JFileChooser::new);
 
  208         "MediaViewImagePanel.createTagOption=Create",
 
  209         "MediaViewImagePanel.deleteTagOption=Delete",
 
  210         "MediaViewImagePanel.hideTagOption=Hide",
 
  211         "MediaViewImagePanel.exportTagOption=Export" 
  213     MediaViewImagePanel() {
 
  216         imageTransforms = 
new ImageTransforms(0, 0, 
true);
 
  218         ExecutorService executor = Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat(
"JFileChooser-background-thread-MediaViewImagePanel").build());
 
  219         executor.execute(futureFileChooser);
 
  222         imageTaggingOptions = 
new JPopupMenu();
 
  223         createTagMenuItem = 
new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
 
  224         createTagMenuItem.addActionListener((event) -> createTag());
 
  225         imageTaggingOptions.add(createTagMenuItem);
 
  227         imageTaggingOptions.add(
new JSeparator());
 
  229         deleteTagMenuItem = 
new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
 
  230         deleteTagMenuItem.addActionListener((event) -> deleteTag());
 
  231         imageTaggingOptions.add(deleteTagMenuItem);
 
  233         imageTaggingOptions.add(
new JSeparator());
 
  235         hideTagsMenuItem = 
new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
 
  236         hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
 
  237         imageTaggingOptions.add(hideTagsMenuItem);
 
  239         imageTaggingOptions.add(
new JSeparator());
 
  241         exportTagsMenuItem = 
new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
 
  242         exportTagsMenuItem.addActionListener((event) -> exportTags());
 
  243         imageTaggingOptions.add(exportTagsMenuItem);
 
  245         imageTaggingOptions.setPopupSize(300, 150);
 
  248         if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
 
  249             tagsMenu.setEnabled(
false);
 
  250             imageTaggingOptions.setEnabled(
false);
 
  253         fxPanel = 
new JFXPanel();
 
  255             Platform.runLater(
new Runnable() {
 
  259                     fxImageView = 
new ImageView();  
 
  260                     masterGroup = 
new Group(fxImageView);
 
  261                     tagsGroup = 
new ImageTagsGroup(fxImageView);
 
  262                     tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
 
  263                         if (c.getList().isEmpty()) {
 
  264                             pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  265                                     "state", null, State.EMPTY));
 
  277                     subscribeTagMenuItemsToStateChanges();
 
  279                     masterGroup.getChildren().add(tagsGroup);
 
  282                     tagsGroup.addFocusChangeListener((event) -> {
 
  283                         if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
 
  284                             if (masterGroup.getChildren().contains(imageTagCreator)) {
 
  288                             if (tagsGroup.getChildren().isEmpty()) {
 
  289                                 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  290                                         "state", null, State.EMPTY));
 
  292                                 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  293                                         "state", null, State.CREATE));
 
  295                         } 
else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
 
  296                             pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  297                                     "state", null, State.SELECTED));
 
  301                     scrollPane = 
new ScrollPane(masterGroup); 
 
  302                     scrollPane.getStyleClass().add(
"bg"); 
 
  303                     scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
 
  304                     scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
 
  306                     Scene scene = 
new Scene(scrollPane); 
 
  307                     scene.getStylesheets().add(MediaViewImagePanel.class.getResource(
"MediaViewImagePanel.css").toExternalForm()); 
 
  308                     fxPanel.setScene(scene);
 
  310                     fxImageView.setSmooth(
true);
 
  311                     fxImageView.setCache(
true);
 
  313                     EventQueue.invokeLater(() -> {
 
  326     private void subscribeTagMenuItemsToStateChanges() {
 
  327         pcs.addPropertyChangeListener((event) -> {
 
  328             State currentState = (State) event.getNewValue();
 
  329             switch (currentState) {
 
  331                     SwingUtilities.invokeLater(() -> {
 
  332                         createTagMenuItem.setEnabled(
true);
 
  333                         deleteTagMenuItem.setEnabled(
false);
 
  334                         hideTagsMenuItem.setEnabled(
true);
 
  335                         exportTagsMenuItem.setEnabled(
true);
 
  339                     Platform.runLater(() -> {
 
  340                         if (masterGroup.getChildren().contains(imageTagCreator)) {
 
  341                             imageTagCreator.disconnect();
 
  342                             masterGroup.getChildren().remove(imageTagCreator);
 
  344                         SwingUtilities.invokeLater(() -> {
 
  345                             createTagMenuItem.setEnabled(
false);
 
  346                             deleteTagMenuItem.setEnabled(
true);
 
  347                             hideTagsMenuItem.setEnabled(
true);
 
  348                             exportTagsMenuItem.setEnabled(
true);
 
  353                     SwingUtilities.invokeLater(() -> {
 
  354                         createTagMenuItem.setEnabled(
false);
 
  355                         deleteTagMenuItem.setEnabled(
false);
 
  356                         hideTagsMenuItem.setEnabled(
true);
 
  357                         hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
 
  358                         exportTagsMenuItem.setEnabled(
false);
 
  362                     SwingUtilities.invokeLater(() -> {
 
  363                         createTagMenuItem.setEnabled(
true);
 
  364                         deleteTagMenuItem.setEnabled(
false);
 
  365                         hideTagsMenuItem.setEnabled(
true);
 
  366                         hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
 
  367                         exportTagsMenuItem.setEnabled(
true);
 
  372                     Platform.runLater(() -> {
 
  373                         if (masterGroup.getChildren().contains(imageTagCreator)) {
 
  374                             imageTagCreator.disconnect();
 
  376                         SwingUtilities.invokeLater(() -> {
 
  377                             createTagMenuItem.setEnabled(
true);
 
  378                             deleteTagMenuItem.setEnabled(
false);
 
  379                             hideTagsMenuItem.setEnabled(
false);
 
  380                             hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
 
  381                             exportTagsMenuItem.setEnabled(
false);
 
  386                     SwingUtilities.invokeLater(() -> {
 
  387                         createTagMenuItem.setEnabled(
true);
 
  388                         deleteTagMenuItem.setEnabled(
false);
 
  389                         hideTagsMenuItem.setEnabled(
true);
 
  390                         exportTagsMenuItem.setEnabled(
true);
 
  394                     SwingUtilities.invokeLater(() -> {
 
  395                         createTagMenuItem.setEnabled(
false);
 
  396                         deleteTagMenuItem.setEnabled(
false);
 
  397                         hideTagsMenuItem.setEnabled(
false);
 
  398                         exportTagsMenuItem.setEnabled(
false);
 
  411     final boolean isInited() {
 
  419         Platform.runLater(() -> {
 
  420             fxImageView.setViewport(
new Rectangle2D(0, 0, 0, 0));
 
  421             fxImageView.setImage(null);
 
  422             pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  423                     "state", null, State.DEFAULT));
 
  424             masterGroup.getChildren().clear();
 
  425             scrollPane.setContent(null);
 
  426             scrollPane.setContent(masterGroup);
 
  437     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  438     private 
void showErrorButton(String errorMessage, AbstractFile file) {
 
  440         final Button externalViewerButton = 
new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), 
new ImageView(openInExternalViewerButtonImage));
 
  441         externalViewerButton.setOnAction(actionEvent
 
  442                 -> 
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), 
new FileNode(file))
 
  443                         .actionPerformed(
new ActionEvent(
this, ActionEvent.ACTION_PERFORMED, 
""))
 
  445         final VBox errorNode = 
new VBox(10, 
new Label(errorMessage), externalViewerButton);
 
  446         errorNode.setAlignment(Pos.CENTER);
 
  454     @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
 
  455     final 
void loadFile(final AbstractFile file) {
 
  456         ensureInSwingThread();
 
  461         final double panelWidth = fxPanel.getWidth();
 
  462         final double panelHeight = fxPanel.getHeight();
 
  463         Platform.runLater(() -> {
 
  468             if (readImageFileTask != null) {
 
  469                 readImageFileTask.cancel();
 
  471             readImageFileTask = ImageUtils.newReadImageTask(file);
 
  472             readImageFileTask.setOnSucceeded(succeeded -> {
 
  473                 onReadImageTaskSucceeded(file, panelWidth, panelHeight);
 
  475             readImageFileTask.setOnFailed(failed -> {
 
  476                 onReadImageTaskFailed(file);
 
  483             maskerPane.setProgressNode(progressBar);
 
  484             progressBar.progressProperty().bind(readImageFileTask.progressProperty());
 
  485             maskerPane.textProperty().bind(readImageFileTask.messageProperty());
 
  486             scrollPane.setContent(null); 
 
  487             scrollPane.setCursor(Cursor.WAIT);
 
  488             new Thread(readImageFileTask).start();
 
  502     private void onReadImageTaskSucceeded(AbstractFile file, 
double panelWidth, 
double panelHeight) {
 
  503         if (!Case.isCaseOpen()) {
 
  514         Platform.runLater(() -> {
 
  516                 Image fxImage = readImageFileTask.get();
 
  517                 masterGroup.getChildren().clear();
 
  518                 tagsGroup.getChildren().clear();
 
  519                 this.imageFile = file;
 
  520                 if (nonNull(fxImage)) {
 
  522                     fxImageView.setImage(fxImage);
 
  523                     if (panelWidth != 0 && panelHeight != 0) {
 
  524                         resetView(panelWidth, panelHeight);
 
  526                     masterGroup.getChildren().add(fxImageView);
 
  527                     masterGroup.getChildren().add(tagsGroup);
 
  530                         List<ContentTag> tags = Case.getCurrentCase().getServices()
 
  531                                 .getTagsManager().getContentTagsByContent(file);
 
  533                         List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
 
  535                         tagsGroup = buildImageTagsGroup(contentViewerTags);
 
  536                         if (!tagsGroup.getChildren().isEmpty()) {
 
  537                             pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  538                                     "state", null, State.NONEMPTY));
 
  540                     } 
catch (TskCoreException | NoCurrentCaseException ex) {
 
  541                         logger.log(Level.WARNING, 
"Could not retrieve image tags for file in case db", ex); 
 
  543                     scrollPane.setContent(masterGroup);
 
  545                     showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  547             } 
catch (InterruptedException | ExecutionException ex) {
 
  548                 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  550             scrollPane.setCursor(Cursor.DEFAULT);
 
  561     private void onReadImageTaskFailed(AbstractFile file) {
 
  562         if (!Case.isCaseOpen()) {
 
  573         Platform.runLater(() -> {
 
  574             Throwable exception = readImageFileTask.getException();
 
  575             if (exception instanceof OutOfMemoryError
 
  576                     && exception.getMessage().contains(
"Java heap space")) {  
 
  577                 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
 
  579                 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
 
  582             scrollPane.setCursor(Cursor.DEFAULT);
 
  597     private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
 
  598             throws TskCoreException, NoCurrentCaseException {
 
  599         List<ContentViewerTag<ImageTagRegion>> contentViewerTags = 
new ArrayList<>();
 
  600         for (ContentTag contentTag : contentTags) {
 
  601             ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
 
  602                     .getTag(contentTag, ImageTagRegion.class);
 
  603             if (contentViewerTag == null) {
 
  607             contentViewerTags.add(contentViewerTag);
 
  609         return contentViewerTags;
 
  623     private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
 
  625         contentViewerTags.forEach(contentViewerTag -> {
 
  630             tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
 
  641     final public List<String> getSupportedMimeTypes() {
 
  642         return Collections.unmodifiableList(Lists.newArrayList(ImageUtils.getSupportedImageMimeTypes()));
 
  651     final public List<String> getSupportedExtensions() {
 
  652         return ImageUtils.getSupportedImageExtensions().stream()
 
  654                 .collect(Collectors.toList());
 
  658     final public boolean isSupported(AbstractFile file) {
 
  659         return ImageUtils.isImageThumbnailSupported(file);
 
  667     @SuppressWarnings(
"unchecked")
 
  669     private 
void initComponents() {
 
  671         toolbar = 
new javax.swing.JToolBar();
 
  672         rotationTextField = 
new javax.swing.JTextField();
 
  673         rotateLeftButton = 
new javax.swing.JButton();
 
  674         rotateRightButton = 
new javax.swing.JButton();
 
  675         jSeparator1 = 
new javax.swing.JToolBar.Separator();
 
  676         zoomTextField = 
new javax.swing.JTextField();
 
  677         zoomOutButton = 
new javax.swing.JButton();
 
  678         zoomInButton = 
new javax.swing.JButton();
 
  679         jSeparator2 = 
new javax.swing.JToolBar.Separator();
 
  680         zoomResetButton = 
new javax.swing.JButton();
 
  681         filler1 = 
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0), 
new java.awt.Dimension(0, 0), 
new java.awt.Dimension(0, 0));
 
  682         filler2 = 
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0), 
new java.awt.Dimension(0, 0), 
new java.awt.Dimension(32767, 0));
 
  683         jPanel1 = 
new javax.swing.JPanel();
 
  684         tagsMenu = 
new javax.swing.JButton();
 
  686         setBackground(
new java.awt.Color(0, 0, 0));
 
  687         addComponentListener(
new java.awt.event.ComponentAdapter() {
 
  688             public void componentResized(java.awt.event.ComponentEvent evt) {
 
  689                 formComponentResized(evt);
 
  692         setLayout(
new javax.swing.BoxLayout(
this, javax.swing.BoxLayout.Y_AXIS));
 
  694         toolbar.setFloatable(
false);
 
  695         toolbar.setRollover(
true);
 
  696         toolbar.setMaximumSize(
new java.awt.Dimension(32767, 23));
 
  699         rotationTextField.setEditable(
false);
 
  700         rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
 
  701         rotationTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotationTextField.text")); 
 
  702         rotationTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
 
  703         rotationTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
 
  704         rotationTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
 
  705         toolbar.add(rotationTextField);
 
  707         rotateLeftButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png"))); 
 
  708         org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateLeftButton.text")); 
 
  709         rotateLeftButton.setToolTipText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateLeftButton.toolTipText")); 
 
  710         rotateLeftButton.setFocusable(
false);
 
  711         rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  712         rotateLeftButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  713         rotateLeftButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  714         rotateLeftButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  715         rotateLeftButton.addActionListener(
new java.awt.event.ActionListener() {
 
  716             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  717                 rotateLeftButtonActionPerformed(evt);
 
  720         toolbar.add(rotateLeftButton);
 
  722         rotateRightButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png"))); 
 
  723         org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.rotateRightButton.text")); 
 
  724         rotateRightButton.setFocusable(
false);
 
  725         rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  726         rotateRightButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  727         rotateRightButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  728         rotateRightButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  729         rotateRightButton.addActionListener(
new java.awt.event.ActionListener() {
 
  730             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  731                 rotateRightButtonActionPerformed(evt);
 
  734         toolbar.add(rotateRightButton);
 
  736         jSeparator1.setMaximumSize(
new java.awt.Dimension(6, 20));
 
  737         toolbar.add(jSeparator1);
 
  739         zoomTextField.setEditable(
false);
 
  740         zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
 
  741         zoomTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomTextField.text")); 
 
  742         zoomTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
 
  743         zoomTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
 
  744         zoomTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
 
  745         toolbar.add(zoomTextField);
 
  747         zoomOutButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png"))); 
 
  748         org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomOutButton.text")); 
 
  749         zoomOutButton.setFocusable(
false);
 
  750         zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  751         zoomOutButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  752         zoomOutButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  753         zoomOutButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  754         zoomOutButton.addActionListener(
new java.awt.event.ActionListener() {
 
  755             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  756                 zoomOutButtonActionPerformed(evt);
 
  759         toolbar.add(zoomOutButton);
 
  761         zoomInButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png"))); 
 
  762         org.openide.awt.Mnemonics.setLocalizedText(zoomInButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomInButton.text")); 
 
  763         zoomInButton.setFocusable(
false);
 
  764         zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  765         zoomInButton.setMaximumSize(
new java.awt.Dimension(24, 24));
 
  766         zoomInButton.setMinimumSize(
new java.awt.Dimension(24, 24));
 
  767         zoomInButton.setPreferredSize(
new java.awt.Dimension(24, 24));
 
  768         zoomInButton.addActionListener(
new java.awt.event.ActionListener() {
 
  769             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  770                 zoomInButtonActionPerformed(evt);
 
  773         toolbar.add(zoomInButton);
 
  775         jSeparator2.setMaximumSize(
new java.awt.Dimension(6, 20));
 
  776         toolbar.add(jSeparator2);
 
  778         org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.zoomResetButton.text")); 
 
  779         zoomResetButton.setFocusable(
false);
 
  780         zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  781         zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
 
  782         zoomResetButton.addActionListener(
new java.awt.event.ActionListener() {
 
  783             public void actionPerformed(java.awt.event.ActionEvent evt) {
 
  784                 zoomResetButtonActionPerformed(evt);
 
  787         toolbar.add(zoomResetButton);
 
  788         toolbar.add(filler1);
 
  789         toolbar.add(filler2);
 
  790         toolbar.add(jPanel1);
 
  792         org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, 
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, 
"MediaViewImagePanel.tagsMenu.text_1")); 
 
  793         tagsMenu.setFocusable(
false);
 
  794         tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
 
  795         tagsMenu.setMaximumSize(
new java.awt.Dimension(75, 21));
 
  796         tagsMenu.setMinimumSize(
new java.awt.Dimension(75, 21));
 
  797         tagsMenu.setPreferredSize(
new java.awt.Dimension(75, 21));
 
  798         tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
 
  799         tagsMenu.addMouseListener(
new java.awt.event.MouseAdapter() {
 
  800             public void mousePressed(java.awt.event.MouseEvent evt) {
 
  801                 tagsMenuMousePressed(evt);
 
  804         toolbar.add(tagsMenu);
 
  809     private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  813     private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  817     private void rotateImage(
int angle) {
 
  818         final double panelWidth = fxPanel.getWidth();
 
  819         final double panelHeight = fxPanel.getHeight();
 
  820         ImageTransforms currentTransforms = imageTransforms;
 
  821         double newRotation = (currentTransforms.getRotation() + angle) % 360;
 
  822         final ImageTransforms newTransforms = 
new ImageTransforms(currentTransforms.getZoomRatio(), newRotation, 
false);
 
  823         imageTransforms = newTransforms;
 
  824         Platform.runLater(() -> {
 
  825             updateView(panelWidth, panelHeight, newTransforms);
 
  829     private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  830         zoomImage(ZoomDirection.IN);
 
  833     private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  834         zoomImage(ZoomDirection.OUT);
 
  837     private void zoomImage(ZoomDirection direction) {
 
  838         ensureInSwingThread();
 
  839         final double panelWidth = fxPanel.getWidth();
 
  840         final double panelHeight = fxPanel.getHeight();
 
  841         final ImageTransforms currentTransforms = imageTransforms;
 
  843         if (direction == ZoomDirection.IN) {
 
  844             newZoomRatio = zoomImageIn(currentTransforms.getZoomRatio());
 
  846             newZoomRatio = zoomImageOut(currentTransforms.getZoomRatio());
 
  848         final ImageTransforms newTransforms = 
new ImageTransforms(newZoomRatio, currentTransforms.getRotation(), 
false);
 
  849         imageTransforms = newTransforms;
 
  850         Platform.runLater(() -> {
 
  851             updateView(panelWidth, panelHeight, newTransforms);
 
  855     private double zoomImageIn(
double zoomRatio) {
 
  856         double newZoomRatio = zoomRatio;
 
  857         for (
int i = 0; i < ZOOM_STEPS.length; i++) {
 
  858             if (newZoomRatio < ZOOM_STEPS[i]) {
 
  859                 newZoomRatio = ZOOM_STEPS[i];
 
  866     private double zoomImageOut(
double zoomRatio) {
 
  867         double newZoomRatio = zoomRatio;
 
  868         for (
int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
 
  869             if (newZoomRatio > ZOOM_STEPS[i]) {
 
  870                 newZoomRatio = ZOOM_STEPS[i];
 
  877     private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {
 
  878         final ImageTransforms currentTransforms = imageTransforms;
 
  879         final ImageTransforms newTransforms = 
new ImageTransforms(0, currentTransforms.getRotation(), 
true);
 
  880         imageTransforms = newTransforms;
 
  884     private void formComponentResized(java.awt.event.ComponentEvent evt) {
 
  885         final ImageTransforms currentTransforms = imageTransforms;
 
  886         if (currentTransforms.shouldAutoResize()) {
 
  889             final double panelWidth = fxPanel.getWidth();
 
  890             final double panelHeight = fxPanel.getHeight();
 
  891             Platform.runLater(() -> {
 
  892                 updateView(panelWidth, panelHeight, currentTransforms);
 
  901     private void deleteTag() {
 
  902         Platform.runLater(() -> {
 
  903             ImageTag tagInFocus = tagsGroup.getFocus();
 
  904             if (tagInFocus == null) {
 
  909                 ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
 
  910                 scrollPane.setCursor(Cursor.WAIT);
 
  911                 ContentViewerTagManager.deleteTag(contentViewerTag);
 
  912                 Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
 
  913                 tagsGroup.getChildren().remove(tagInFocus);
 
  914             } 
catch (TskCoreException | NoCurrentCaseException ex) {
 
  915                 logger.log(Level.WARNING, 
"Could not delete image tag in case db", ex); 
 
  918             scrollPane.setCursor(Cursor.DEFAULT);
 
  921         pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  922                 "state", null, State.CREATE));
 
  929     private void createTag() {
 
  930         pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  931                 "state", null, State.DISABLE));
 
  932         Platform.runLater(() -> {
 
  933             imageTagCreator = 
new ImageTagCreator(fxImageView);
 
  935             PropertyChangeListener newTagListener = (event) -> {
 
  936                 SwingUtilities.invokeLater(() -> {
 
  937                     ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
 
  939                     TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
 
  940                     if (result != null) {
 
  942                         Platform.runLater(() -> {
 
  944                                 scrollPane.setCursor(Cursor.WAIT);
 
  945                                 ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
 
  946                                 ImageTag imageTag = buildImageTag(contentViewerTag);
 
  947                                 tagsGroup.getChildren().add(imageTag);
 
  948                             } 
catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
 
  949                                 logger.log(Level.WARNING, 
"Could not save new image tag in case db", ex); 
 
  952                             scrollPane.setCursor(Cursor.DEFAULT);
 
  956                     pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
  957                             "state", null, State.CREATE));
 
  961                 Platform.runLater(() -> {
 
  962                     imageTagCreator.disconnect();
 
  963                     masterGroup.getChildren().remove(imageTagCreator);
 
  967             imageTagCreator.addNewTagListener(newTagListener);
 
  968             masterGroup.getChildren().add(imageTagCreator);
 
  979     private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
 
  981         ImageTag imageTag = 
new ImageTag(contentViewerTag, fxImageView);
 
  984         imageTag.subscribeToEditEvents((edit) -> {
 
  986                 scrollPane.setCursor(Cursor.WAIT);
 
  987                 ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
 
  988                 ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
 
  989             } 
catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
 
  990                 logger.log(Level.WARNING, 
"Could not save edit for image tag in case db", ex); 
 
  992             scrollPane.setCursor(Cursor.DEFAULT);
 
 1004     private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result) 
throws TskCoreException, SerializationException, NoCurrentCaseException {
 
 1005         ensureInJfxThread();
 
 1006         scrollPane.setCursor(Cursor.WAIT);
 
 1008             ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
 
 1009                     .addContentTag(imageFile, result.getTagName(), result.getComment());
 
 1010             return ContentViewerTagManager.saveTag(contentTag, data);
 
 1012             scrollPane.setCursor(Cursor.DEFAULT);
 
 1020     private void showOrHideTags() {
 
 1021         Platform.runLater(() -> {
 
 1022             if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
 
 1024                 masterGroup.getChildren().remove(tagsGroup);
 
 1025                 hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
 
 1026                 tagsGroup.clearFocus();
 
 1027                 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
 1028                         "state", null, State.HIDDEN));
 
 1031                 masterGroup.getChildren().add(tagsGroup);
 
 1032                 hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
 
 1033                 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
 
 1034                         "state", null, State.VISIBLE));
 
 1039     @NbBundle.Messages({
 
 1040         "MediaViewImagePanel.exportSaveText=Save",
 
 1041         "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
 
 1042         "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
 
 1043         "MediaViewImagePanel.fileChooserTitle=Choose a save location" 
 1045     private void exportTags() {
 
 1046         Platform.runLater(() -> {
 
 1047             final AbstractFile file = imageFile;
 
 1048             tagsGroup.clearFocus();
 
 1049             SwingUtilities.invokeLater(() -> {
 
 1051                 if(exportChooser == null) {
 
 1053                         exportChooser = futureFileChooser.get();
 
 1054                     } 
catch (InterruptedException | ExecutionException ex) {
 
 1057                         logger.log(Level.WARNING, 
"A failure occurred in the JFileChooser background thread");
 
 1058                         exportChooser = 
new JFileChooser();
 
 1062                 exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
 
 1064                 exportChooser.setCurrentDirectory(
new File(Case.getCurrentCase().getExportDirectory()));
 
 1065                 int returnVal = exportChooser.showDialog(
this, Bundle.MediaViewImagePanel_exportSaveText());
 
 1066                 if (returnVal == JFileChooser.APPROVE_OPTION) {
 
 1067                     new SwingWorker<Void, Void>() {
 
 1069                         protected Void doInBackground() {
 
 1072                                 List<ContentTag> tags = Case.getCurrentCase().getServices()
 
 1073                                         .getTagsManager().getContentTagsByContent(file);
 
 1074                                 List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
 
 1077                                 Collection<ImageTagRegion> regions = contentViewerTags.stream()
 
 1078                                         .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
 
 1081                                 BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
 
 1082                                 Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
 
 1083                                         FilenameUtils.getBaseName(file.getName()) + 
"-with_tags.png"); 
 
 1084                                 ImageIO.write(taggedImage, 
"png", output.toFile());
 
 1086                                 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
 
 1087                             } 
catch (Exception ex) { 
 
 1089                                 logger.log(Level.WARNING, 
"Unable to export tagged image to disk", ex); 
 
 1090                                 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
 
 1100     private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {
 
 1101         if (imageTaggingOptions.isEnabled()) {
 
 1102             imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
 
 1140     private javax.swing.Box.Filler filler1;
 
 1141     private javax.swing.Box.Filler filler2;
 
 1142     private javax.swing.JPanel jPanel1;
 
 1143     private javax.swing.JToolBar.Separator jSeparator1;
 
 1144     private javax.swing.JToolBar.Separator jSeparator2;
 
 1145     private javax.swing.JButton rotateLeftButton;
 
 1146     private javax.swing.JButton rotateRightButton;
 
 1147     private javax.swing.JTextField rotationTextField;
 
 1148     private javax.swing.JButton tagsMenu;
 
 1149     private javax.swing.JToolBar toolbar;
 
 1150     private javax.swing.JButton zoomInButton;
 
 1151     private javax.swing.JButton zoomOutButton;
 
 1152     private javax.swing.JButton zoomResetButton;
 
 1153     private javax.swing.JTextField zoomTextField;
 
 1160     private void resetView() {
 
 1161         ensureInSwingThread();
 
 1162         final double panelWidth = fxPanel.getWidth();
 
 1163         final double panelHeight = fxPanel.getHeight();
 
 1164         Platform.runLater(() -> {
 
 1165             resetView(panelWidth, panelHeight);
 
 1179     private void resetView(
double panelWidth, 
double panelHeight) {
 
 1180         ensureInJfxThread();
 
 1182         Image image = fxImageView.getImage();
 
 1183         if (image == null) {
 
 1187         double imageWidth = image.getWidth();
 
 1188         double imageHeight = image.getHeight();
 
 1189         double scrollPaneWidth = panelWidth;
 
 1190         double scrollPaneHeight = panelHeight;
 
 1191         double zoomRatioWidth = scrollPaneWidth / imageWidth;
 
 1192         double zoomRatioHeight = scrollPaneHeight / imageHeight;
 
 1193         double newZoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight; 
 
 1194         final ImageTransforms newTransforms = 
new ImageTransforms(newZoomRatio, 0, 
true);
 
 1195         imageTransforms = newTransforms;
 
 1197         scrollPane.setHvalue(0);
 
 1198         scrollPane.setVvalue(0);
 
 1200         updateView(panelWidth, panelHeight, newTransforms);
 
 1225     private void updateView(
double panelWidth, 
double panelHeight, ImageTransforms imageTransforms) {
 
 1226         ensureInJfxThread();
 
 1227         Image image = fxImageView.getImage();
 
 1228         if (image == null) {
 
 1233         double imageWidth = image.getWidth();
 
 1234         double imageHeight = image.getHeight();
 
 1237         double currentZoomRatio = imageTransforms.getZoomRatio();
 
 1238         double adjustedImageWidth = imageWidth * currentZoomRatio;
 
 1239         double adjustedImageHeight = imageHeight * currentZoomRatio;
 
 1242         double viewportWidth;
 
 1243         double viewportHeight;
 
 1246         double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
 
 1247         double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
 
 1254         double scrollX = scrollPane.getHvalue();
 
 1255         double scrollY = scrollPane.getVvalue();
 
 1262         final double currentRotation = imageTransforms.getRotation();
 
 1263         if ((currentRotation % 180) == 0) {
 
 1265             viewportWidth = adjustedImageWidth;
 
 1266             viewportHeight = adjustedImageHeight;
 
 1267             leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
 
 1268             topOffsetY = (adjustedImageHeight - imageHeight) / 2;
 
 1269             maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
 
 1270             maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
 
 1273             viewportWidth = adjustedImageHeight;
 
 1274             viewportHeight = adjustedImageWidth;
 
 1275             leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
 
 1276             topOffsetY = (adjustedImageWidth - imageHeight) / 2;
 
 1277             maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
 
 1278             maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
 
 1282         if (viewportWidth < imageWidth) {
 
 1283             viewportWidth = imageWidth;
 
 1284             if (scrollX > maxScrollX) {
 
 1285                 scrollX = maxScrollX;
 
 1288         if (viewportHeight < imageHeight) {
 
 1289             viewportHeight = imageHeight;
 
 1290             if (scrollY > maxScrollY) {
 
 1291                 scrollY = maxScrollY;
 
 1296         fxImageView.setViewport(
new Rectangle2D(
 
 1297                 0, 0, viewportWidth, viewportHeight));
 
 1300         Scale scale = 
new Scale();
 
 1301         scale.setX(currentZoomRatio);
 
 1302         scale.setY(currentZoomRatio);
 
 1303         scale.setPivotX(imageWidth / 2);
 
 1304         scale.setPivotY(imageHeight / 2);
 
 1307         Rotate rotate = 
new Rotate();
 
 1308         rotate.setPivotX(imageWidth / 2);
 
 1309         rotate.setPivotY(imageHeight / 2);
 
 1310         rotate.setAngle(currentRotation);
 
 1313         Translate translate = 
new Translate();
 
 1314         translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
 
 1315         translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
 
 1320         masterGroup.getTransforms().clear();
 
 1321         masterGroup.getTransforms().addAll(translate, rotate, scale);
 
 1324         if (viewportWidth > fxPanel.getWidth()) {
 
 1325             scrollPane.setHvalue(scrollX);
 
 1327         if (viewportHeight > fxPanel.getHeight()) {
 
 1328             scrollPane.setVvalue(scrollY);
 
 1337         SwingUtilities.invokeLater(() -> {
 
 1339             zoomOutButton.setEnabled(currentZoomRatio > MIN_ZOOM_RATIO);
 
 1340             zoomInButton.setEnabled(currentZoomRatio < MAX_ZOOM_RATIO);
 
 1341             rotationTextField.setText((
int) currentRotation + 
"°");
 
 1342             zoomTextField.setText((Math.round(currentZoomRatio * 100.0)) + 
"%");
 
 1351     private void ensureInJfxThread() {
 
 1352         if (!Platform.isFxApplicationThread()) {
 
 1353             throw new IllegalStateException(
"Attempt to execute JFX code outside of JFX thread"); 
 
 1362     private void ensureInSwingThread() {
 
 1363         if (!SwingUtilities.isEventDispatchThread()) {
 
 1364             throw new IllegalStateException(
"Attempt to execute Swing code outside of EDT"); 
 
 1385         ImageTransforms(
double zoomRatio, 
double rotation, 
boolean autoResize) {
 
static boolean isJavaFxInited()
 
DisplayOptions(String name)
 
boolean shouldAutoResize()