Autopsy  4.15.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
MediaViewImagePanel.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.contentviewers;
20 
21 import java.awt.EventQueue;
22 import java.awt.event.ActionEvent;
23 import java.awt.image.BufferedImage;
24 import java.beans.PropertyChangeEvent;
25 import java.beans.PropertyChangeListener;
26 import java.beans.PropertyChangeSupport;
27 import java.io.File;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.List;
34 import static java.util.Objects.nonNull;
35 import java.util.SortedSet;
36 import java.util.concurrent.ExecutionException;
37 import java.util.logging.Level;
38 import java.util.stream.Collectors;
39 import javafx.application.Platform;
40 import javafx.collections.ListChangeListener.Change;
41 import javafx.concurrent.Task;
42 import javafx.embed.swing.JFXPanel;
43 import javafx.geometry.Pos;
44 import javafx.geometry.Rectangle2D;
45 import javafx.scene.Cursor;
46 import javafx.scene.Group;
47 import javafx.scene.Scene;
48 import javafx.scene.control.Button;
49 import javafx.scene.control.Label;
50 import javafx.scene.control.ProgressBar;
51 import javafx.scene.control.ScrollPane;
52 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
53 import javafx.scene.image.Image;
54 import javafx.scene.image.ImageView;
55 import javafx.scene.layout.VBox;
56 import javafx.scene.transform.Rotate;
57 import javafx.scene.transform.Scale;
58 import javafx.scene.transform.Translate;
59 import javax.imageio.ImageIO;
60 import javax.swing.JFileChooser;
61 import javafx.scene.Node;
62 import javax.swing.JMenuItem;
63 import javax.swing.JOptionPane;
64 import javax.swing.JPanel;
65 import javax.swing.JPopupMenu;
66 import javax.swing.JSeparator;
67 import javax.swing.SwingUtilities;
68 import javax.swing.SwingWorker;
69 import org.apache.commons.io.FilenameUtils;
70 import org.controlsfx.control.MaskerPane;
71 import org.openide.util.NbBundle;
72 import org.python.google.common.collect.Lists;
92 import org.sleuthkit.datamodel.AbstractFile;
93 import org.sleuthkit.datamodel.ContentTag;
94 import org.sleuthkit.datamodel.TskCoreException;
95 
100 @NbBundle.Messages({"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E",
101  "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
102  "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."})
103 @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
104 class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPanel {
105 
106  private static final Image EXTERNAL = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm());
107  private final static Logger LOGGER = Logger.getLogger(MediaViewImagePanel.class.getName());
108 
109  private final boolean fxInited;
110 
111  private JFXPanel fxPanel;
112  private AbstractFile file;
113  private Group masterGroup;
114  private ImageTagsGroup tagsGroup;
115  private ImageTagCreator imageTagCreator;
116  private ImageView fxImageView;
117  private ScrollPane scrollPane;
118  private final ProgressBar progressBar = new ProgressBar();
119  private final MaskerPane maskerPane = new MaskerPane();
120 
121  private final JPopupMenu imageTaggingOptions = new JPopupMenu();
122  private final JMenuItem createTagMenuItem;
123  private final JMenuItem deleteTagMenuItem;
124  private final JMenuItem hideTagsMenuItem;
125  private final JMenuItem exportTagsMenuItem;
126 
127  private final JFileChooser exportChooser;
128 
129  private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
130 
131  private double zoomRatio;
132  private double rotation; // Can be 0, 90, 180, and 270.
133 
134  private boolean autoResize = true; // Auto resize when the user changes the size
135  // of the content viewer unless the user has used the zoom buttons.
136  private static final double[] ZOOM_STEPS = {
137  0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
138  1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
139 
140  private static final double MIN_ZOOM_RATIO = 0.0625; // 6.25%
141  private static final double MAX_ZOOM_RATIO = 10.0; // 1000%
142 
143  static {
144  ImageIO.scanForPlugins();
145  }
146 
151  static private final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
152 
156  static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
157  .map("."::concat) //NOI18N
158  .collect(Collectors.toList());
159 
160  private Task<Image> readImageTask;
161 
165  @NbBundle.Messages({
166  "MediaViewImagePanel.createTagOption=Create",
167  "MediaViewImagePanel.deleteTagOption=Delete",
168  "MediaViewImagePanel.hideTagOption=Hide",
169  "MediaViewImagePanel.exportTagOption=Export"
170  })
171  public MediaViewImagePanel() {
172  initComponents();
174 
175  exportChooser = new JFileChooser();
176  exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle());
177 
178  //Build popupMenu when Tags Menu button is pressed.
179  createTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
180  createTagMenuItem.addActionListener((event) -> createTag());
181  imageTaggingOptions.add(createTagMenuItem);
182 
183  imageTaggingOptions.add(new JSeparator());
184 
185  deleteTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
186  deleteTagMenuItem.addActionListener((event) -> deleteTag());
187  imageTaggingOptions.add(deleteTagMenuItem);
188 
189  imageTaggingOptions.add(new JSeparator());
190 
191  hideTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
192  hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
193  imageTaggingOptions.add(hideTagsMenuItem);
194 
195  imageTaggingOptions.add(new JSeparator());
196 
197  exportTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
198  exportTagsMenuItem.addActionListener((event) -> exportTags());
199  imageTaggingOptions.add(exportTagsMenuItem);
200 
201  imageTaggingOptions.setPopupSize(300, 150);
202 
203  //Disable image tagging for non-windows users or upon failure to load OpenCV.
204  if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
205  tagsMenu.setEnabled(false);
206  imageTaggingOptions.setEnabled(false);
207  }
208 
209  if (fxInited) {
210  Platform.runLater(new Runnable() {
211  @Override
212  public void run() {
213  // build jfx ui (we could do this in FXML?)
214  fxImageView = new ImageView(); // will hold image
215  masterGroup = new Group(fxImageView);
216  tagsGroup = new ImageTagsGroup(fxImageView);
217  tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
218  if (c.getList().isEmpty()) {
219  pcs.firePropertyChange(new PropertyChangeEvent(this,
220  "state", null, State.EMPTY));
221  }
222  });
223 
224  subscribeTagMenuItemsToStateChanges();
225 
226  masterGroup.getChildren().add(tagsGroup);
227 
228  //Update buttons when users select (or unselect) image tags.
229  tagsGroup.addFocusChangeListener((event) -> {
230  if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
231  if (masterGroup.getChildren().contains(imageTagCreator)) {
232  return;
233  }
234 
235  if (tagsGroup.getChildren().isEmpty()) {
236  pcs.firePropertyChange(new PropertyChangeEvent(this,
237  "state", null, State.EMPTY));
238  } else {
239  pcs.firePropertyChange(new PropertyChangeEvent(this,
240  "state", null, State.CREATE));
241  }
242  } else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
243  pcs.firePropertyChange(new PropertyChangeEvent(this,
244  "state", null, State.SELECTED));
245  }
246  });
247 
248  scrollPane = new ScrollPane(masterGroup); // scrolls and sizes imageview
249  scrollPane.getStyleClass().add("bg"); //NOI18N
250  scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
251  scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
252 
253  fxPanel = new JFXPanel(); // bridge jfx-swing
254  Scene scene = new Scene(scrollPane); //root of jfx tree
255  scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N
256  fxPanel.setScene(scene);
257 
258  fxImageView.setSmooth(true);
259  fxImageView.setCache(true);
260 
261  EventQueue.invokeLater(() -> {
262  add(fxPanel);//add jfx ui to JPanel
263  });
264  }
265  });
266  }
267  }
268 
274  private void subscribeTagMenuItemsToStateChanges() {
275  pcs.addPropertyChangeListener((event) -> {
276  State currentState = (State) event.getNewValue();
277  switch (currentState) {
278  case CREATE:
279  createTagMenuItem.setEnabled(true);
280  deleteTagMenuItem.setEnabled(false);
281  hideTagsMenuItem.setEnabled(true);
282  exportTagsMenuItem.setEnabled(true);
283  break;
284  case SELECTED:
285  if (masterGroup.getChildren().contains(imageTagCreator)) {
286  imageTagCreator.disconnect();
287  masterGroup.getChildren().remove(imageTagCreator);
288  }
289  createTagMenuItem.setEnabled(false);
290  deleteTagMenuItem.setEnabled(true);
291  hideTagsMenuItem.setEnabled(true);
292  exportTagsMenuItem.setEnabled(true);
293  break;
294  case HIDDEN:
295  createTagMenuItem.setEnabled(false);
296  deleteTagMenuItem.setEnabled(false);
297  hideTagsMenuItem.setEnabled(true);
298  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
299  exportTagsMenuItem.setEnabled(false);
300  break;
301  case VISIBLE:
302  createTagMenuItem.setEnabled(true);
303  deleteTagMenuItem.setEnabled(false);
304  hideTagsMenuItem.setEnabled(true);
305  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
306  exportTagsMenuItem.setEnabled(true);
307  break;
308  case DEFAULT:
309  case EMPTY:
310  if (masterGroup.getChildren().contains(imageTagCreator)) {
311  imageTagCreator.disconnect();
312  }
313  createTagMenuItem.setEnabled(true);
314  deleteTagMenuItem.setEnabled(false);
315  hideTagsMenuItem.setEnabled(false);
316  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
317  exportTagsMenuItem.setEnabled(false);
318  break;
319  case NONEMPTY:
320  createTagMenuItem.setEnabled(true);
321  deleteTagMenuItem.setEnabled(false);
322  hideTagsMenuItem.setEnabled(true);
323  exportTagsMenuItem.setEnabled(true);
324  break;
325  case DISABLE:
326  createTagMenuItem.setEnabled(false);
327  deleteTagMenuItem.setEnabled(false);
328  hideTagsMenuItem.setEnabled(false);
329  exportTagsMenuItem.setEnabled(false);
330  break;
331  default:
332  break;
333  }
334  });
335  }
336 
337  public boolean isInited() {
338  return fxInited;
339  }
340 
344  public void reset() {
345  Platform.runLater(() -> {
346  fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0));
347  fxImageView.setImage(null);
348  pcs.firePropertyChange(new PropertyChangeEvent(this,
349  "state", null, State.DEFAULT));
350  masterGroup.getChildren().clear();
351  scrollPane.setContent(null);
352  scrollPane.setContent(masterGroup);
353  });
354  }
355 
356  private void showErrorNode(String errorMessage, AbstractFile file) {
357  final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(EXTERNAL));
358  externalViewerButton.setOnAction(actionEvent
359  -> //fx ActionEvent
360  /*
361  * TODO: why is the name passed into the action constructor? it
362  * means we duplicate this string all over the place -jm
363  */ new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
364  .actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent
365  );
366 
367  final VBox errorNode = new VBox(10, new Label(errorMessage), externalViewerButton);
368  errorNode.setAlignment(Pos.CENTER);
369  }
370 
376  void showImageFx(final AbstractFile file) {
377  if (!fxInited) {
378  return;
379  }
380 
381  Platform.runLater(() -> {
382  if (readImageTask != null) {
383  readImageTask.cancel();
384  }
385  readImageTask = ImageUtils.newReadImageTask(file);
386  readImageTask.setOnSucceeded(succeeded -> {
387  if (!Case.isCaseOpen()) {
388  /*
389  * Handle the in-between condition when case is being closed
390  * and an image was previously selected
391  *
392  * NOTE: I think this is unnecessary -jm
393  */
394  reset();
395  return;
396  }
397 
398  try {
399  autoResize = true;
400  Image fxImage = readImageTask.get();
401  masterGroup.getChildren().clear();
402  tagsGroup.getChildren().clear();
403  this.file = file;
404  if (nonNull(fxImage)) {
405  // We have a non-null image, so let's show it.
406  fxImageView.setImage(fxImage);
407  resetView();
408  masterGroup.getChildren().add(fxImageView);
409  masterGroup.getChildren().add(tagsGroup);
410 
411  try {
412  List<ContentTag> tags = Case.getCurrentCase().getServices()
413  .getTagsManager().getContentTagsByContent(file);
414 
415  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
416  //Add all image tags
417  tagsGroup = buildImageTagsGroup(contentViewerTags);
418  if (!tagsGroup.getChildren().isEmpty()) {
419  pcs.firePropertyChange(new PropertyChangeEvent(this,
420  "state", null, State.NONEMPTY));
421  }
422  } catch (TskCoreException | NoCurrentCaseException ex) {
423  LOGGER.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS
424  }
425  scrollPane.setContent(masterGroup);
426  } else {
427  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
428  }
429  } catch (InterruptedException | ExecutionException ex) {
430  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
431  }
432  scrollPane.setCursor(Cursor.DEFAULT);
433  });
434  readImageTask.setOnFailed(failed -> {
435  if (!Case.isCaseOpen()) {
436  /*
437  * Handle in-between condition when case is being closed and
438  * an image was previously selected
439  *
440  * NOTE: I think this is unnecessary -jm
441  */
442  reset();
443  return;
444  }
445  Throwable exception = readImageTask.getException();
446  if (exception instanceof OutOfMemoryError
447  && exception.getMessage().contains("Java heap space")) {
448  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
449  } else {
450  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
451  }
452 
453  scrollPane.setCursor(Cursor.DEFAULT);
454  });
455 
456  maskerPane.setProgressNode(progressBar);
457  progressBar.progressProperty().bind(readImageTask.progressProperty());
458  maskerPane.textProperty().bind(readImageTask.messageProperty());
459  scrollPane.setContent(null); // Prevent content display issues.
460  scrollPane.setCursor(Cursor.WAIT);
461  new Thread(readImageTask).start();
462  });
463  }
464 
476  private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
477  throws TskCoreException, NoCurrentCaseException {
478  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = new ArrayList<>();
479  for (ContentTag contentTag : contentTags) {
480  ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
481  .getTag(contentTag, ImageTagRegion.class);
482  if (contentViewerTag == null) {
483  continue;
484  }
485 
486  contentViewerTags.add(contentViewerTag);
487  }
488  return contentViewerTags;
489  }
490 
502  private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
503 
504  contentViewerTags.forEach(contentViewerTag -> {
509  tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
510  });
511 
512  return tagsGroup;
513  }
514 
518  @Override
519  public List<String> getSupportedMimeTypes() {
520  return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
521  }
522 
528  @Override
529  public List<String> getSupportedExtensions() {
530  return getExtensions();
531  }
532 
538  public List<String> getExtensions() {
539  return Collections.unmodifiableList(supportedExtensions);
540  }
541 
542  @Override
543  public boolean isSupported(AbstractFile file) {
544  return ImageUtils.isImageThumbnailSupported(file);
545  }
546 
552  @SuppressWarnings("unchecked")
553  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
554  private void initComponents() {
555 
556  toolbar = new javax.swing.JToolBar();
557  rotationTextField = new javax.swing.JTextField();
558  rotateLeftButton = new javax.swing.JButton();
559  rotateRightButton = new javax.swing.JButton();
560  jSeparator1 = new javax.swing.JToolBar.Separator();
561  zoomTextField = new javax.swing.JTextField();
562  zoomOutButton = new javax.swing.JButton();
563  zoomInButton = new javax.swing.JButton();
564  jSeparator2 = new javax.swing.JToolBar.Separator();
565  zoomResetButton = new javax.swing.JButton();
566  filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0));
567  filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
568  jPanel1 = new javax.swing.JPanel();
569  tagsMenu = new javax.swing.JButton();
570 
571  setBackground(new java.awt.Color(0, 0, 0));
572  addComponentListener(new java.awt.event.ComponentAdapter() {
573  public void componentResized(java.awt.event.ComponentEvent evt) {
574  formComponentResized(evt);
575  }
576  });
577  setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));
578 
579  toolbar.setFloatable(false);
580  toolbar.setRollover(true);
581  toolbar.setMaximumSize(new java.awt.Dimension(32767, 23));
582  toolbar.setName(""); // NOI18N
583 
584  rotationTextField.setEditable(false);
585  rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
586  rotationTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotationTextField.text")); // NOI18N
587  rotationTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
588  rotationTextField.setMinimumSize(new java.awt.Dimension(50, 20));
589  rotationTextField.setPreferredSize(new java.awt.Dimension(50, 20));
590  toolbar.add(rotationTextField);
591 
592  rotateLeftButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png"))); // NOI18N
593  org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.text")); // NOI18N
594  rotateLeftButton.setToolTipText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.toolTipText")); // NOI18N
595  rotateLeftButton.setFocusable(false);
596  rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
597  rotateLeftButton.setMaximumSize(new java.awt.Dimension(24, 24));
598  rotateLeftButton.setMinimumSize(new java.awt.Dimension(24, 24));
599  rotateLeftButton.setPreferredSize(new java.awt.Dimension(24, 24));
600  rotateLeftButton.addActionListener(new java.awt.event.ActionListener() {
601  public void actionPerformed(java.awt.event.ActionEvent evt) {
602  rotateLeftButtonActionPerformed(evt);
603  }
604  });
605  toolbar.add(rotateLeftButton);
606 
607  rotateRightButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png"))); // NOI18N
608  org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateRightButton.text")); // NOI18N
609  rotateRightButton.setFocusable(false);
610  rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
611  rotateRightButton.setMaximumSize(new java.awt.Dimension(24, 24));
612  rotateRightButton.setMinimumSize(new java.awt.Dimension(24, 24));
613  rotateRightButton.setPreferredSize(new java.awt.Dimension(24, 24));
614  rotateRightButton.addActionListener(new java.awt.event.ActionListener() {
615  public void actionPerformed(java.awt.event.ActionEvent evt) {
616  rotateRightButtonActionPerformed(evt);
617  }
618  });
619  toolbar.add(rotateRightButton);
620 
621  jSeparator1.setMaximumSize(new java.awt.Dimension(6, 20));
622  toolbar.add(jSeparator1);
623 
624  zoomTextField.setEditable(false);
625  zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
626  zoomTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomTextField.text")); // NOI18N
627  zoomTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
628  zoomTextField.setMinimumSize(new java.awt.Dimension(50, 20));
629  zoomTextField.setPreferredSize(new java.awt.Dimension(50, 20));
630  toolbar.add(zoomTextField);
631 
632  zoomOutButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png"))); // NOI18N
633  org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomOutButton.text")); // NOI18N
634  zoomOutButton.setFocusable(false);
635  zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
636  zoomOutButton.setMaximumSize(new java.awt.Dimension(24, 24));
637  zoomOutButton.setMinimumSize(new java.awt.Dimension(24, 24));
638  zoomOutButton.setPreferredSize(new java.awt.Dimension(24, 24));
639  zoomOutButton.addActionListener(new java.awt.event.ActionListener() {
640  public void actionPerformed(java.awt.event.ActionEvent evt) {
641  zoomOutButtonActionPerformed(evt);
642  }
643  });
644  toolbar.add(zoomOutButton);
645 
646  zoomInButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png"))); // NOI18N
647  org.openide.awt.Mnemonics.setLocalizedText(zoomInButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomInButton.text")); // NOI18N
648  zoomInButton.setFocusable(false);
649  zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
650  zoomInButton.setMaximumSize(new java.awt.Dimension(24, 24));
651  zoomInButton.setMinimumSize(new java.awt.Dimension(24, 24));
652  zoomInButton.setPreferredSize(new java.awt.Dimension(24, 24));
653  zoomInButton.addActionListener(new java.awt.event.ActionListener() {
654  public void actionPerformed(java.awt.event.ActionEvent evt) {
655  zoomInButtonActionPerformed(evt);
656  }
657  });
658  toolbar.add(zoomInButton);
659 
660  jSeparator2.setMaximumSize(new java.awt.Dimension(6, 20));
661  toolbar.add(jSeparator2);
662 
663  org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomResetButton.text")); // NOI18N
664  zoomResetButton.setFocusable(false);
665  zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
666  zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
667  zoomResetButton.addActionListener(new java.awt.event.ActionListener() {
668  public void actionPerformed(java.awt.event.ActionEvent evt) {
669  zoomResetButtonActionPerformed(evt);
670  }
671  });
672  toolbar.add(zoomResetButton);
673  toolbar.add(filler1);
674  toolbar.add(filler2);
675  toolbar.add(jPanel1);
676 
677  org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.tagsMenu.text_1")); // NOI18N
678  tagsMenu.setFocusable(false);
679  tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
680  tagsMenu.setMaximumSize(new java.awt.Dimension(75, 21));
681  tagsMenu.setMinimumSize(new java.awt.Dimension(75, 21));
682  tagsMenu.setPreferredSize(new java.awt.Dimension(75, 21));
683  tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
684  tagsMenu.addMouseListener(new java.awt.event.MouseAdapter() {
685  public void mousePressed(java.awt.event.MouseEvent evt) {
686  tagsMenuMousePressed(evt);
687  }
688  });
689  toolbar.add(tagsMenu);
690 
691  add(toolbar);
692  }// </editor-fold>//GEN-END:initComponents
693 
694  private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateLeftButtonActionPerformed
695  autoResize = false;
696 
697  rotation = (rotation + 270) % 360;
698  updateView();
699  }//GEN-LAST:event_rotateLeftButtonActionPerformed
700 
701  private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateRightButtonActionPerformed
702  autoResize = false;
703 
704  rotation = (rotation + 90) % 360;
705  updateView();
706  }//GEN-LAST:event_rotateRightButtonActionPerformed
707 
708  private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomInButtonActionPerformed
709  autoResize = false;
710  // Find the next zoom step.
711  for (int i = 0; i < ZOOM_STEPS.length; i++) {
712  if (zoomRatio < ZOOM_STEPS[i]) {
713  zoomRatio = ZOOM_STEPS[i];
714  break;
715  }
716  }
717  updateView();
718  }//GEN-LAST:event_zoomInButtonActionPerformed
719 
720  private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOutButtonActionPerformed
721  autoResize = false;
722  // Find the next zoom step.
723  for (int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
724  if (zoomRatio > ZOOM_STEPS[i]) {
725  zoomRatio = ZOOM_STEPS[i];
726  break;
727  }
728  }
729  updateView();
730  }//GEN-LAST:event_zoomOutButtonActionPerformed
731 
732  private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomResetButtonActionPerformed
733  autoResize = true;
734  resetView();
735  }//GEN-LAST:event_zoomResetButtonActionPerformed
736 
737  private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
738  if (autoResize) {
739  resetView();
740  } else {
741  updateView();
742  }
743  }//GEN-LAST:event_formComponentResized
744 
749  private void deleteTag() {
750  Platform.runLater(() -> {
751  ImageTag tagInFocus = tagsGroup.getFocus();
752  if (tagInFocus == null) {
753  return;
754  }
755 
756  try {
757  ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
758  scrollPane.setCursor(Cursor.WAIT);
759  ContentViewerTagManager.deleteTag(contentViewerTag);
760  Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
761  tagsGroup.getChildren().remove(tagInFocus);
762  } catch (TskCoreException | NoCurrentCaseException ex) {
763  LOGGER.log(Level.WARNING, "Could not delete image tag in case db", ex); //NON-NLS
764  }
765 
766  scrollPane.setCursor(Cursor.DEFAULT);
767  });
768 
769  pcs.firePropertyChange(new PropertyChangeEvent(this,
770  "state", null, State.CREATE));
771  }
772 
777  private void createTag() {
778  pcs.firePropertyChange(new PropertyChangeEvent(this,
779  "state", null, State.DISABLE));
780  imageTagCreator = new ImageTagCreator(fxImageView);
781 
782  PropertyChangeListener newTagListener = (event) -> {
783  SwingUtilities.invokeLater(() -> {
784  ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
785  //Ask the user for tag name and comment
786  TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
787  if (result != null) {
788  //Persist and build image tag
789  Platform.runLater(() -> {
790  try {
791  scrollPane.setCursor(Cursor.WAIT);
792  ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
793  ImageTag imageTag = buildImageTag(contentViewerTag);
794  tagsGroup.getChildren().add(imageTag);
795  } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
796  LOGGER.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS
797  }
798 
799  scrollPane.setCursor(Cursor.DEFAULT);
800  });
801  }
802 
803  pcs.firePropertyChange(new PropertyChangeEvent(this,
804  "state", null, State.CREATE));
805  });
806 
807  //Remove image tag creator from panel
808  Platform.runLater(() -> {
809  imageTagCreator.disconnect();
810  masterGroup.getChildren().remove(imageTagCreator);
811  });
812  };
813 
814  imageTagCreator.addNewTagListener(newTagListener);
815  Platform.runLater(() -> masterGroup.getChildren().add(imageTagCreator));
816  }
817 
825  private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
826  ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView);
827 
828  //Automatically persist edits made by user
829  imageTag.subscribeToEditEvents((edit) -> {
830  try {
831  scrollPane.setCursor(Cursor.WAIT);
832  ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
833  ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
834  } catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
835  LOGGER.log(Level.WARNING, "Could not save edit for image tag in case db", ex); //NON-NLS
836  }
837  scrollPane.setCursor(Cursor.DEFAULT);
838  });
839  return imageTag;
840  }
841 
849  private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result)
850  throws TskCoreException, SerializationException, NoCurrentCaseException {
851  scrollPane.setCursor(Cursor.WAIT);
852  try {
853  ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
854  .addContentTag(file, result.getTagName(), result.getComment());
855  return ContentViewerTagManager.saveTag(contentTag, data);
856  } finally {
857  scrollPane.setCursor(Cursor.DEFAULT);
858  }
859  }
860 
865  private void showOrHideTags() {
866  Platform.runLater(() -> {
867  if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
868  //Temporarily remove the tags group and update buttons
869  masterGroup.getChildren().remove(tagsGroup);
870  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
871  tagsGroup.clearFocus();
872  pcs.firePropertyChange(new PropertyChangeEvent(this,
873  "state", null, State.HIDDEN));
874  } else {
875  //Add tags group back in and update buttons
876  masterGroup.getChildren().add(tagsGroup);
877  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
878  pcs.firePropertyChange(new PropertyChangeEvent(this,
879  "state", null, State.VISIBLE));
880  }
881  });
882  }
883 
884  @NbBundle.Messages({
885  "MediaViewImagePanel.exportSaveText=Save",
886  "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
887  "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
888  "MediaViewImagePanel.fileChooserTitle=Choose a save location"
889  })
890  private void exportTags() {
891  tagsGroup.clearFocus();
892  exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
893  //Always base chooser location to export folder
894  exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
895  int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText());
896  if (returnVal == JFileChooser.APPROVE_OPTION) {
897  new SwingWorker<Void, Void>() {
898  @Override
899  protected Void doInBackground() {
900  try {
901  //Retrieve content viewer tags
902  List<ContentTag> tags = Case.getCurrentCase().getServices()
903  .getTagsManager().getContentTagsByContent(file);
904  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
905 
906  //Pull out image tag regions
907  Collection<ImageTagRegion> regions = contentViewerTags.stream()
908  .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
909 
910  //Apply tags to image and write to file
911  BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
912  Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
913  FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS
914  ImageIO.write(taggedImage, "png", output.toFile());
915 
916  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
917  } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX.
918  //This ensures we (devs and users) have something when it doesn't work.
919  LOGGER.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS
920  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
921  }
922  return null;
923  }
924  }.execute();
925  }
926  }
927 
928  private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed
929  if (imageTaggingOptions.isEnabled()) {
930  imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
931  }
932  }//GEN-LAST:event_tagsMenuMousePressed
933 
937  enum DisplayOptions {
938  HIDE_TAGS("Hide"),
939  SHOW_TAGS("Show");
940 
941  private final String name;
942 
943  DisplayOptions(String name) {
944  this.name = name;
945  }
946 
947  String getName() {
948  return name;
949  }
950  }
951 
956  enum State {
957  HIDDEN,
958  VISIBLE,
959  SELECTED,
960  CREATE,
961  EMPTY,
962  NONEMPTY,
963  DEFAULT,
964  DISABLE;
965  }
966 
967  // Variables declaration - do not modify//GEN-BEGIN:variables
968  private javax.swing.Box.Filler filler1;
969  private javax.swing.Box.Filler filler2;
970  private javax.swing.JPanel jPanel1;
971  private javax.swing.JToolBar.Separator jSeparator1;
972  private javax.swing.JToolBar.Separator jSeparator2;
973  private javax.swing.JButton rotateLeftButton;
974  private javax.swing.JButton rotateRightButton;
975  private javax.swing.JTextField rotationTextField;
976  private javax.swing.JButton tagsMenu;
977  private javax.swing.JToolBar toolbar;
978  private javax.swing.JButton zoomInButton;
979  private javax.swing.JButton zoomOutButton;
980  private javax.swing.JButton zoomResetButton;
981  private javax.swing.JTextField zoomTextField;
982  // End of variables declaration//GEN-END:variables
983 
992  private void resetView() {
993  Image image = fxImageView.getImage();
994  if (image == null) {
995  return;
996  }
997 
998  double imageWidth = image.getWidth();
999  double imageHeight = image.getHeight();
1000  double scrollPaneWidth = fxPanel.getWidth();
1001  double scrollPaneHeight = fxPanel.getHeight();
1002  double zoomRatioWidth = scrollPaneWidth / imageWidth;
1003  double zoomRatioHeight = scrollPaneHeight / imageHeight;
1004 
1005  // Use the smallest ratio size to fit the entire image in the view area.
1006  zoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
1007 
1008  rotation = 0;
1009 
1010  scrollPane.setHvalue(0);
1011  scrollPane.setVvalue(0);
1012 
1013  updateView();
1014  }
1015 
1026  private void updateView() {
1027  Image image = fxImageView.getImage();
1028  if (image == null) {
1029  return;
1030  }
1031 
1032  // Image dimensions
1033  double imageWidth = image.getWidth();
1034  double imageHeight = image.getHeight();
1035 
1036  // Image dimensions with zooming applied
1037  double adjustedImageWidth = imageWidth * zoomRatio;
1038  double adjustedImageHeight = imageHeight * zoomRatio;
1039 
1040  // ImageView viewport dimensions
1041  double viewportWidth;
1042  double viewportHeight;
1043 
1044  // Panel dimensions
1045  double panelWidth = fxPanel.getWidth();
1046  double panelHeight = fxPanel.getHeight();
1047 
1048  // Coordinates to center the image on the panel
1049  double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
1050  double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
1051 
1052  // Coordinates to keep the image inside the left/top boundaries
1053  double leftOffsetX;
1054  double topOffsetY;
1055 
1056  // Scroll bar positions
1057  double scrollX = scrollPane.getHvalue();
1058  double scrollY = scrollPane.getVvalue();
1059 
1060  // Scroll bar position boundaries (work-around for viewport size bug)
1061  double maxScrollX;
1062  double maxScrollY;
1063 
1064  // Set viewport size and translation offsets.
1065  if ((rotation % 180) == 0) {
1066  // Rotation is 0 or 180.
1067  viewportWidth = adjustedImageWidth;
1068  viewportHeight = adjustedImageHeight;
1069  leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
1070  topOffsetY = (adjustedImageHeight - imageHeight) / 2;
1071  maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
1072  maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
1073  } else {
1074  // Rotation is 90 or 270.
1075  viewportWidth = adjustedImageHeight;
1076  viewportHeight = adjustedImageWidth;
1077  leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
1078  topOffsetY = (adjustedImageWidth - imageHeight) / 2;
1079  maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
1080  maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
1081  }
1082 
1083  // Work around bug that truncates image if dimensions are too small.
1084  if (viewportWidth < imageWidth) {
1085  viewportWidth = imageWidth;
1086  if (scrollX > maxScrollX) {
1087  scrollX = maxScrollX;
1088  }
1089  }
1090  if (viewportHeight < imageHeight) {
1091  viewportHeight = imageHeight;
1092  if (scrollY > maxScrollY) {
1093  scrollY = maxScrollY;
1094  }
1095  }
1096 
1097  // Update the viewport size.
1098  fxImageView.setViewport(new Rectangle2D(
1099  0, 0, viewportWidth, viewportHeight));
1100 
1101  // Step 1: Zoom
1102  Scale scale = new Scale();
1103  scale.setX(zoomRatio);
1104  scale.setY(zoomRatio);
1105  scale.setPivotX(imageWidth / 2);
1106  scale.setPivotY(imageHeight / 2);
1107 
1108  // Step 2: Rotate
1109  Rotate rotate = new Rotate();
1110  rotate.setPivotX(imageWidth / 2);
1111  rotate.setPivotY(imageHeight / 2);
1112  rotate.setAngle(rotation);
1113 
1114  // Step 3: Position
1115  Translate translate = new Translate();
1116  translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
1117  translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
1118 
1119  // Add the transforms in reverse order of intended execution.
1120  // Note: They MUST be added in this order to ensure translate is
1121  // executed last.
1122  masterGroup.getTransforms().clear();
1123  masterGroup.getTransforms().addAll(translate, rotate, scale);
1124 
1125  // Adjust scroll bar positions for view changes.
1126  if (viewportWidth > fxPanel.getWidth()) {
1127  scrollPane.setHvalue(scrollX);
1128  }
1129  if (viewportHeight > fxPanel.getHeight()) {
1130  scrollPane.setVvalue(scrollY);
1131  }
1132 
1133  // Update all image controls to reflect the current values.
1134  zoomOutButton.setEnabled(zoomRatio > MIN_ZOOM_RATIO);
1135  zoomInButton.setEnabled(zoomRatio < MAX_ZOOM_RATIO);
1136  rotationTextField.setText((int) rotation + "°");
1137  zoomTextField.setText((Math.round(zoomRatio * 100.0)) + "%");
1138  }
1139 }

Copyright © 2012-2020 Basis Technology. Generated on: Mon Jul 6 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.