Autopsy  4.14.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 static final double[] ZOOM_STEPS = {
135  0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
136  1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
137 
138  private static final double MIN_ZOOM_RATIO = 0.0625; // 6.25%
139  private static final double MAX_ZOOM_RATIO = 10.0; // 1000%
140 
141  static {
142  ImageIO.scanForPlugins();
143  }
144 
149  static private final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
150 
154  static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
155  .map("."::concat) //NOI18N
156  .collect(Collectors.toList());
157 
158  private Task<Image> readImageTask;
159 
163  @NbBundle.Messages({
164  "MediaViewImagePanel.createTagOption=Create",
165  "MediaViewImagePanel.deleteTagOption=Delete",
166  "MediaViewImagePanel.hideTagOption=Hide",
167  "MediaViewImagePanel.exportTagOption=Export"
168  })
169  public MediaViewImagePanel() {
170  initComponents();
172 
173  exportChooser = new JFileChooser();
174  exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle());
175 
176  //Build popupMenu when Tags Menu button is pressed.
177  createTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
178  createTagMenuItem.addActionListener((event) -> createTag());
179  imageTaggingOptions.add(createTagMenuItem);
180 
181  imageTaggingOptions.add(new JSeparator());
182 
183  deleteTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
184  deleteTagMenuItem.addActionListener((event) -> deleteTag());
185  imageTaggingOptions.add(deleteTagMenuItem);
186 
187  imageTaggingOptions.add(new JSeparator());
188 
189  hideTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
190  hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
191  imageTaggingOptions.add(hideTagsMenuItem);
192 
193  imageTaggingOptions.add(new JSeparator());
194 
195  exportTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
196  exportTagsMenuItem.addActionListener((event) -> exportTags());
197  imageTaggingOptions.add(exportTagsMenuItem);
198 
199  imageTaggingOptions.setPopupSize(300, 150);
200 
201  //Disable image tagging for non-windows users or upon failure to load OpenCV.
202  if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
203  tagsMenu.setEnabled(false);
204  imageTaggingOptions.setEnabled(false);
205  }
206 
207  if (fxInited) {
208  Platform.runLater(new Runnable() {
209  @Override
210  public void run() {
211  // build jfx ui (we could do this in FXML?)
212  fxImageView = new ImageView(); // will hold image
213  masterGroup = new Group(fxImageView);
214  tagsGroup = new ImageTagsGroup(fxImageView);
215  tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
216  if (c.getList().isEmpty()) {
217  pcs.firePropertyChange(new PropertyChangeEvent(this,
218  "state", null, State.EMPTY));
219  }
220  });
221 
222  subscribeTagMenuItemsToStateChanges();
223 
224  masterGroup.getChildren().add(tagsGroup);
225 
226  //Update buttons when users select (or unselect) image tags.
227  tagsGroup.addFocusChangeListener((event) -> {
228  if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
229  if (masterGroup.getChildren().contains(imageTagCreator)) {
230  return;
231  }
232 
233  if (tagsGroup.getChildren().isEmpty()) {
234  pcs.firePropertyChange(new PropertyChangeEvent(this,
235  "state", null, State.EMPTY));
236  } else {
237  pcs.firePropertyChange(new PropertyChangeEvent(this,
238  "state", null, State.CREATE));
239  }
240  } else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
241  pcs.firePropertyChange(new PropertyChangeEvent(this,
242  "state", null, State.SELECTED));
243  }
244  });
245 
246  scrollPane = new ScrollPane(masterGroup); // scrolls and sizes imageview
247  scrollPane.getStyleClass().add("bg"); //NOI18N
248  scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
249  scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
250 
251  fxPanel = new JFXPanel(); // bridge jfx-swing
252  Scene scene = new Scene(scrollPane); //root of jfx tree
253  scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N
254  fxPanel.setScene(scene);
255 
256  fxImageView.setSmooth(true);
257  fxImageView.setCache(true);
258 
259  EventQueue.invokeLater(() -> {
260  add(fxPanel);//add jfx ui to JPanel
261  });
262  }
263  });
264  }
265  }
266 
272  private void subscribeTagMenuItemsToStateChanges() {
273  pcs.addPropertyChangeListener((event) -> {
274  State currentState = (State) event.getNewValue();
275  switch (currentState) {
276  case CREATE:
277  createTagMenuItem.setEnabled(true);
278  deleteTagMenuItem.setEnabled(false);
279  hideTagsMenuItem.setEnabled(true);
280  exportTagsMenuItem.setEnabled(true);
281  break;
282  case SELECTED:
283  if (masterGroup.getChildren().contains(imageTagCreator)) {
284  imageTagCreator.disconnect();
285  masterGroup.getChildren().remove(imageTagCreator);
286  }
287  createTagMenuItem.setEnabled(false);
288  deleteTagMenuItem.setEnabled(true);
289  hideTagsMenuItem.setEnabled(true);
290  exportTagsMenuItem.setEnabled(true);
291  break;
292  case HIDDEN:
293  createTagMenuItem.setEnabled(false);
294  deleteTagMenuItem.setEnabled(false);
295  hideTagsMenuItem.setEnabled(true);
296  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
297  exportTagsMenuItem.setEnabled(false);
298  break;
299  case VISIBLE:
300  createTagMenuItem.setEnabled(true);
301  deleteTagMenuItem.setEnabled(false);
302  hideTagsMenuItem.setEnabled(true);
303  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
304  exportTagsMenuItem.setEnabled(true);
305  break;
306  case DEFAULT:
307  case EMPTY:
308  if (masterGroup.getChildren().contains(imageTagCreator)) {
309  imageTagCreator.disconnect();
310  }
311  createTagMenuItem.setEnabled(true);
312  deleteTagMenuItem.setEnabled(false);
313  hideTagsMenuItem.setEnabled(false);
314  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
315  exportTagsMenuItem.setEnabled(false);
316  break;
317  case NONEMPTY:
318  createTagMenuItem.setEnabled(true);
319  deleteTagMenuItem.setEnabled(false);
320  hideTagsMenuItem.setEnabled(true);
321  exportTagsMenuItem.setEnabled(true);
322  break;
323  case DISABLE:
324  createTagMenuItem.setEnabled(false);
325  deleteTagMenuItem.setEnabled(false);
326  hideTagsMenuItem.setEnabled(false);
327  exportTagsMenuItem.setEnabled(false);
328  break;
329  default:
330  break;
331  }
332  });
333  }
334 
335  public boolean isInited() {
336  return fxInited;
337  }
338 
342  public void reset() {
343  Platform.runLater(() -> {
344  fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0));
345  fxImageView.setImage(null);
346  pcs.firePropertyChange(new PropertyChangeEvent(this,
347  "state", null, State.DEFAULT));
348  masterGroup.getChildren().clear();
349  scrollPane.setContent(null);
350  scrollPane.setContent(masterGroup);
351  });
352  }
353 
354  private void showErrorNode(String errorMessage, AbstractFile file) {
355  final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(EXTERNAL));
356  externalViewerButton.setOnAction(actionEvent
357  -> //fx ActionEvent
358  /*
359  * TODO: why is the name passed into the action constructor? it
360  * means we duplicate this string all over the place -jm
361  */ new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
362  .actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) //Swing ActionEvent
363  );
364 
365  final VBox errorNode = new VBox(10, new Label(errorMessage), externalViewerButton);
366  errorNode.setAlignment(Pos.CENTER);
367  }
368 
374  void showImageFx(final AbstractFile file) {
375  if (!fxInited) {
376  return;
377  }
378 
379  Platform.runLater(() -> {
380  if (readImageTask != null) {
381  readImageTask.cancel();
382  }
383  readImageTask = ImageUtils.newReadImageTask(file);
384  readImageTask.setOnSucceeded(succeeded -> {
385  if (!Case.isCaseOpen()) {
386  /*
387  * Handle the in-between condition when case is being closed
388  * and an image was previously selected
389  *
390  * NOTE: I think this is unnecessary -jm
391  */
392  reset();
393  return;
394  }
395 
396  try {
397  Image fxImage = readImageTask.get();
398  masterGroup.getChildren().clear();
399  tagsGroup.getChildren().clear();
400  this.file = file;
401  if (nonNull(fxImage)) {
402  // We have a non-null image, so let's show it.
403  fxImageView.setImage(fxImage);
404  resetView();
405  masterGroup.getChildren().add(fxImageView);
406  masterGroup.getChildren().add(tagsGroup);
407 
408  try {
409  List<ContentTag> tags = Case.getCurrentCase().getServices()
410  .getTagsManager().getContentTagsByContent(file);
411 
412  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
413  //Add all image tags
414  tagsGroup = buildImageTagsGroup(contentViewerTags);
415  if (!tagsGroup.getChildren().isEmpty()) {
416  pcs.firePropertyChange(new PropertyChangeEvent(this,
417  "state", null, State.NONEMPTY));
418  }
419  } catch (TskCoreException | NoCurrentCaseException ex) {
420  LOGGER.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS
421  }
422  scrollPane.setContent(masterGroup);
423  } else {
424  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
425  }
426  } catch (InterruptedException | ExecutionException ex) {
427  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
428  }
429  scrollPane.setCursor(Cursor.DEFAULT);
430  });
431  readImageTask.setOnFailed(failed -> {
432  if (!Case.isCaseOpen()) {
433  /*
434  * Handle in-between condition when case is being closed and
435  * an image was previously selected
436  *
437  * NOTE: I think this is unnecessary -jm
438  */
439  reset();
440  return;
441  }
442  Throwable exception = readImageTask.getException();
443  if (exception instanceof OutOfMemoryError
444  && exception.getMessage().contains("Java heap space")) {
445  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
446  } else {
447  showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
448  }
449 
450  scrollPane.setCursor(Cursor.DEFAULT);
451  });
452 
453  maskerPane.setProgressNode(progressBar);
454  progressBar.progressProperty().bind(readImageTask.progressProperty());
455  maskerPane.textProperty().bind(readImageTask.messageProperty());
456  scrollPane.setContent(null); // Prevent content display issues.
457  scrollPane.setCursor(Cursor.WAIT);
458  new Thread(readImageTask).start();
459  });
460  }
461 
471  private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
472  throws TskCoreException, NoCurrentCaseException {
473  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = new ArrayList<>();
474  for (ContentTag contentTag : contentTags) {
475  ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
476  .getTag(contentTag, ImageTagRegion.class);
477  if (contentViewerTag == null) {
478  continue;
479  }
480 
481  contentViewerTags.add(contentViewerTag);
482  }
483  return contentViewerTags;
484  }
485 
495  private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
496 
497  contentViewerTags.forEach(contentViewerTag -> {
502  tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
503  });
504 
505  return tagsGroup;
506  }
507 
511  @Override
512  public List<String> getSupportedMimeTypes() {
513  return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
514  }
515 
521  @Override
522  public List<String> getSupportedExtensions() {
523  return getExtensions();
524  }
525 
531  public List<String> getExtensions() {
532  return Collections.unmodifiableList(supportedExtensions);
533  }
534 
535  @Override
536  public boolean isSupported(AbstractFile file) {
537  return ImageUtils.isImageThumbnailSupported(file);
538  }
539 
545  @SuppressWarnings("unchecked")
546  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
547  private void initComponents() {
548 
549  toolbar = new javax.swing.JToolBar();
550  rotationTextField = new javax.swing.JTextField();
551  rotateLeftButton = new javax.swing.JButton();
552  rotateRightButton = new javax.swing.JButton();
553  jSeparator1 = new javax.swing.JToolBar.Separator();
554  zoomTextField = new javax.swing.JTextField();
555  zoomOutButton = new javax.swing.JButton();
556  zoomInButton = new javax.swing.JButton();
557  jSeparator2 = new javax.swing.JToolBar.Separator();
558  zoomResetButton = new javax.swing.JButton();
559  filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0));
560  filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
561  jPanel1 = new javax.swing.JPanel();
562  tagsMenu = new javax.swing.JButton();
563 
564  setBackground(new java.awt.Color(0, 0, 0));
565  addComponentListener(new java.awt.event.ComponentAdapter() {
566  public void componentResized(java.awt.event.ComponentEvent evt) {
567  formComponentResized(evt);
568  }
569  });
570  setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));
571 
572  toolbar.setFloatable(false);
573  toolbar.setRollover(true);
574  toolbar.setMaximumSize(new java.awt.Dimension(32767, 23));
575  toolbar.setName(""); // NOI18N
576 
577  rotationTextField.setEditable(false);
578  rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
579  rotationTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotationTextField.text")); // NOI18N
580  rotationTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
581  rotationTextField.setMinimumSize(new java.awt.Dimension(50, 20));
582  rotationTextField.setPreferredSize(new java.awt.Dimension(50, 20));
583  toolbar.add(rotationTextField);
584 
585  rotateLeftButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png"))); // NOI18N
586  org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.text")); // NOI18N
587  rotateLeftButton.setToolTipText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.toolTipText")); // NOI18N
588  rotateLeftButton.setFocusable(false);
589  rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
590  rotateLeftButton.setMaximumSize(new java.awt.Dimension(24, 24));
591  rotateLeftButton.setMinimumSize(new java.awt.Dimension(24, 24));
592  rotateLeftButton.setPreferredSize(new java.awt.Dimension(24, 24));
593  rotateLeftButton.addActionListener(new java.awt.event.ActionListener() {
594  public void actionPerformed(java.awt.event.ActionEvent evt) {
595  rotateLeftButtonActionPerformed(evt);
596  }
597  });
598  toolbar.add(rotateLeftButton);
599 
600  rotateRightButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png"))); // NOI18N
601  org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateRightButton.text")); // NOI18N
602  rotateRightButton.setFocusable(false);
603  rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
604  rotateRightButton.setMaximumSize(new java.awt.Dimension(24, 24));
605  rotateRightButton.setMinimumSize(new java.awt.Dimension(24, 24));
606  rotateRightButton.setPreferredSize(new java.awt.Dimension(24, 24));
607  rotateRightButton.addActionListener(new java.awt.event.ActionListener() {
608  public void actionPerformed(java.awt.event.ActionEvent evt) {
609  rotateRightButtonActionPerformed(evt);
610  }
611  });
612  toolbar.add(rotateRightButton);
613 
614  jSeparator1.setMaximumSize(new java.awt.Dimension(6, 20));
615  toolbar.add(jSeparator1);
616 
617  zoomTextField.setEditable(false);
618  zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
619  zoomTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomTextField.text")); // NOI18N
620  zoomTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
621  zoomTextField.setMinimumSize(new java.awt.Dimension(50, 20));
622  zoomTextField.setPreferredSize(new java.awt.Dimension(50, 20));
623  toolbar.add(zoomTextField);
624 
625  zoomOutButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png"))); // NOI18N
626  org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomOutButton.text")); // NOI18N
627  zoomOutButton.setFocusable(false);
628  zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
629  zoomOutButton.setMaximumSize(new java.awt.Dimension(24, 24));
630  zoomOutButton.setMinimumSize(new java.awt.Dimension(24, 24));
631  zoomOutButton.setPreferredSize(new java.awt.Dimension(24, 24));
632  zoomOutButton.addActionListener(new java.awt.event.ActionListener() {
633  public void actionPerformed(java.awt.event.ActionEvent evt) {
634  zoomOutButtonActionPerformed(evt);
635  }
636  });
637  toolbar.add(zoomOutButton);
638 
639  zoomInButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png"))); // NOI18N
640  org.openide.awt.Mnemonics.setLocalizedText(zoomInButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomInButton.text")); // NOI18N
641  zoomInButton.setFocusable(false);
642  zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
643  zoomInButton.setMaximumSize(new java.awt.Dimension(24, 24));
644  zoomInButton.setMinimumSize(new java.awt.Dimension(24, 24));
645  zoomInButton.setPreferredSize(new java.awt.Dimension(24, 24));
646  zoomInButton.addActionListener(new java.awt.event.ActionListener() {
647  public void actionPerformed(java.awt.event.ActionEvent evt) {
648  zoomInButtonActionPerformed(evt);
649  }
650  });
651  toolbar.add(zoomInButton);
652 
653  jSeparator2.setMaximumSize(new java.awt.Dimension(6, 20));
654  toolbar.add(jSeparator2);
655 
656  org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomResetButton.text")); // NOI18N
657  zoomResetButton.setFocusable(false);
658  zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
659  zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
660  zoomResetButton.addActionListener(new java.awt.event.ActionListener() {
661  public void actionPerformed(java.awt.event.ActionEvent evt) {
662  zoomResetButtonActionPerformed(evt);
663  }
664  });
665  toolbar.add(zoomResetButton);
666  toolbar.add(filler1);
667  toolbar.add(filler2);
668  toolbar.add(jPanel1);
669 
670  org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.tagsMenu.text_1")); // NOI18N
671  tagsMenu.setFocusable(false);
672  tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
673  tagsMenu.setMaximumSize(new java.awt.Dimension(75, 21));
674  tagsMenu.setMinimumSize(new java.awt.Dimension(75, 21));
675  tagsMenu.setPreferredSize(new java.awt.Dimension(75, 21));
676  tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
677  tagsMenu.addMouseListener(new java.awt.event.MouseAdapter() {
678  public void mousePressed(java.awt.event.MouseEvent evt) {
679  tagsMenuMousePressed(evt);
680  }
681  });
682  toolbar.add(tagsMenu);
683 
684  add(toolbar);
685  }// </editor-fold>//GEN-END:initComponents
686 
687  private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateLeftButtonActionPerformed
688  rotation = (rotation + 270) % 360;
689  updateView();
690  }//GEN-LAST:event_rotateLeftButtonActionPerformed
691 
692  private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateRightButtonActionPerformed
693  rotation = (rotation + 90) % 360;
694  updateView();
695  }//GEN-LAST:event_rotateRightButtonActionPerformed
696 
697  private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomInButtonActionPerformed
698  // Find the next zoom step.
699  for (int i = 0; i < ZOOM_STEPS.length; i++) {
700  if (zoomRatio < ZOOM_STEPS[i]) {
701  zoomRatio = ZOOM_STEPS[i];
702  break;
703  }
704  }
705  updateView();
706  }//GEN-LAST:event_zoomInButtonActionPerformed
707 
708  private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOutButtonActionPerformed
709  // Find the next zoom step.
710  for (int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
711  if (zoomRatio > ZOOM_STEPS[i]) {
712  zoomRatio = ZOOM_STEPS[i];
713  break;
714  }
715  }
716  updateView();
717  }//GEN-LAST:event_zoomOutButtonActionPerformed
718 
719  private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomResetButtonActionPerformed
720  resetView();
721  }//GEN-LAST:event_zoomResetButtonActionPerformed
722 
723  private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
724  updateView();
725  }//GEN-LAST:event_formComponentResized
726 
731  private void deleteTag() {
732  Platform.runLater(() -> {
733  ImageTag tagInFocus = tagsGroup.getFocus();
734  if (tagInFocus == null) {
735  return;
736  }
737 
738  try {
739  ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
740  scrollPane.setCursor(Cursor.WAIT);
741  ContentViewerTagManager.deleteTag(contentViewerTag);
742  Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
743  tagsGroup.getChildren().remove(tagInFocus);
744  } catch (TskCoreException | NoCurrentCaseException ex) {
745  LOGGER.log(Level.WARNING, "Could not delete image tag in case db", ex); //NON-NLS
746  }
747 
748  scrollPane.setCursor(Cursor.DEFAULT);
749  });
750 
751  pcs.firePropertyChange(new PropertyChangeEvent(this,
752  "state", null, State.CREATE));
753  }
754 
759  private void createTag() {
760  pcs.firePropertyChange(new PropertyChangeEvent(this,
761  "state", null, State.DISABLE));
762  imageTagCreator = new ImageTagCreator(fxImageView);
763 
764  PropertyChangeListener newTagListener = (event) -> {
765  SwingUtilities.invokeLater(() -> {
766  ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
767  //Ask the user for tag name and comment
768  TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
769  if (result != null) {
770  //Persist and build image tag
771  Platform.runLater(() -> {
772  try {
773  scrollPane.setCursor(Cursor.WAIT);
774  ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
775  ImageTag imageTag = buildImageTag(contentViewerTag);
776  tagsGroup.getChildren().add(imageTag);
777  } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
778  LOGGER.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS
779  }
780 
781  scrollPane.setCursor(Cursor.DEFAULT);
782  });
783  }
784 
785  pcs.firePropertyChange(new PropertyChangeEvent(this,
786  "state", null, State.CREATE));
787  });
788 
789  //Remove image tag creator from panel
790  Platform.runLater(() -> {
791  imageTagCreator.disconnect();
792  masterGroup.getChildren().remove(imageTagCreator);
793  });
794  };
795 
796  imageTagCreator.addNewTagListener(newTagListener);
797  Platform.runLater(() -> masterGroup.getChildren().add(imageTagCreator));
798  }
799 
806  private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
807  ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView);
808 
809  //Automatically persist edits made by user
810  imageTag.subscribeToEditEvents((edit) -> {
811  try {
812  scrollPane.setCursor(Cursor.WAIT);
813  ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
814  ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
815  } catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
816  LOGGER.log(Level.WARNING, "Could not save edit for image tag in case db", ex); //NON-NLS
817  }
818  scrollPane.setCursor(Cursor.DEFAULT);
819  });
820  return imageTag;
821  }
822 
830  private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result)
831  throws TskCoreException, SerializationException, NoCurrentCaseException {
832  scrollPane.setCursor(Cursor.WAIT);
833  try {
834  ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
835  .addContentTag(file, result.getTagName(), result.getComment());
836  return ContentViewerTagManager.saveTag(contentTag, data);
837  } finally {
838  scrollPane.setCursor(Cursor.DEFAULT);
839  }
840  }
841 
846  private void showOrHideTags() {
847  Platform.runLater(() -> {
848  if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
849  //Temporarily remove the tags group and update buttons
850  masterGroup.getChildren().remove(tagsGroup);
851  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
852  tagsGroup.clearFocus();
853  pcs.firePropertyChange(new PropertyChangeEvent(this,
854  "state", null, State.HIDDEN));
855  } else {
856  //Add tags group back in and update buttons
857  masterGroup.getChildren().add(tagsGroup);
858  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
859  pcs.firePropertyChange(new PropertyChangeEvent(this,
860  "state", null, State.VISIBLE));
861  }
862  });
863  }
864 
865  @NbBundle.Messages({
866  "MediaViewImagePanel.exportSaveText=Save",
867  "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
868  "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
869  "MediaViewImagePanel.fileChooserTitle=Choose a save location"
870  })
871  private void exportTags() {
872  tagsGroup.clearFocus();
873  exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
874  //Always base chooser location to export folder
875  exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
876  int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText());
877  if (returnVal == JFileChooser.APPROVE_OPTION) {
878  new SwingWorker<Void, Void>() {
879  @Override
880  protected Void doInBackground() {
881  try {
882  //Retrieve content viewer tags
883  List<ContentTag> tags = Case.getCurrentCase().getServices()
884  .getTagsManager().getContentTagsByContent(file);
885  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
886 
887  //Pull out image tag regions
888  Collection<ImageTagRegion> regions = contentViewerTags.stream()
889  .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
890 
891  //Apply tags to image and write to file
892  BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
893  Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
894  FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS
895  ImageIO.write(taggedImage, "png", output.toFile());
896 
897  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
898  } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX.
899  //This ensures we (devs and users) have something when it doesn't work.
900  LOGGER.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS
901  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
902  }
903  return null;
904  }
905  }.execute();
906  }
907  }
908 
909  private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed
910  if (imageTaggingOptions.isEnabled()) {
911  imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
912  }
913  }//GEN-LAST:event_tagsMenuMousePressed
914 
918  enum DisplayOptions {
919  HIDE_TAGS("Hide"),
920  SHOW_TAGS("Show");
921 
922  private final String name;
923 
924  DisplayOptions(String name) {
925  this.name = name;
926  }
927 
928  String getName() {
929  return name;
930  }
931  }
932 
937  enum State {
938  HIDDEN,
939  VISIBLE,
940  SELECTED,
941  CREATE,
942  EMPTY,
943  NONEMPTY,
944  DEFAULT,
945  DISABLE;
946  }
947 
948  // Variables declaration - do not modify//GEN-BEGIN:variables
949  private javax.swing.Box.Filler filler1;
950  private javax.swing.Box.Filler filler2;
951  private javax.swing.JPanel jPanel1;
952  private javax.swing.JToolBar.Separator jSeparator1;
953  private javax.swing.JToolBar.Separator jSeparator2;
954  private javax.swing.JButton rotateLeftButton;
955  private javax.swing.JButton rotateRightButton;
956  private javax.swing.JTextField rotationTextField;
957  private javax.swing.JButton tagsMenu;
958  private javax.swing.JToolBar toolbar;
959  private javax.swing.JButton zoomInButton;
960  private javax.swing.JButton zoomOutButton;
961  private javax.swing.JButton zoomResetButton;
962  private javax.swing.JTextField zoomTextField;
963  // End of variables declaration//GEN-END:variables
964 
973  private void resetView() {
974  Image image = fxImageView.getImage();
975  if (image == null) {
976  return;
977  }
978 
979  double imageWidth = image.getWidth();
980  double imageHeight = image.getHeight();
981  double scrollPaneWidth = fxPanel.getWidth();
982  double scrollPaneHeight = fxPanel.getHeight();
983  double zoomRatioWidth = scrollPaneWidth / imageWidth;
984  double zoomRatioHeight = scrollPaneHeight / imageHeight;
985 
986  // Use the smallest ratio size to fit the entire image in the view area.
987  zoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
988 
989  rotation = 0;
990 
991  scrollPane.setHvalue(0);
992  scrollPane.setVvalue(0);
993 
994  updateView();
995  }
996 
1007  private void updateView() {
1008  Image image = fxImageView.getImage();
1009  if (image == null) {
1010  return;
1011  }
1012 
1013  // Image dimensions
1014  double imageWidth = image.getWidth();
1015  double imageHeight = image.getHeight();
1016 
1017  // Image dimensions with zooming applied
1018  double adjustedImageWidth = imageWidth * zoomRatio;
1019  double adjustedImageHeight = imageHeight * zoomRatio;
1020 
1021  // ImageView viewport dimensions
1022  double viewportWidth;
1023  double viewportHeight;
1024 
1025  // Panel dimensions
1026  double panelWidth = fxPanel.getWidth();
1027  double panelHeight = fxPanel.getHeight();
1028 
1029  // Coordinates to center the image on the panel
1030  double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
1031  double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
1032 
1033  // Coordinates to keep the image inside the left/top boundaries
1034  double leftOffsetX;
1035  double topOffsetY;
1036 
1037  // Scroll bar positions
1038  double scrollX = scrollPane.getHvalue();
1039  double scrollY = scrollPane.getVvalue();
1040 
1041  // Scroll bar position boundaries (work-around for viewport size bug)
1042  double maxScrollX;
1043  double maxScrollY;
1044 
1045  // Set viewport size and translation offsets.
1046  if ((rotation % 180) == 0) {
1047  // Rotation is 0 or 180.
1048  viewportWidth = adjustedImageWidth;
1049  viewportHeight = adjustedImageHeight;
1050  leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
1051  topOffsetY = (adjustedImageHeight - imageHeight) / 2;
1052  maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
1053  maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
1054  } else {
1055  // Rotation is 90 or 270.
1056  viewportWidth = adjustedImageHeight;
1057  viewportHeight = adjustedImageWidth;
1058  leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
1059  topOffsetY = (adjustedImageWidth - imageHeight) / 2;
1060  maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
1061  maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
1062  }
1063 
1064  // Work around bug that truncates image if dimensions are too small.
1065  if (viewportWidth < imageWidth) {
1066  viewportWidth = imageWidth;
1067  if (scrollX > maxScrollX) {
1068  scrollX = maxScrollX;
1069  }
1070  }
1071  if (viewportHeight < imageHeight) {
1072  viewportHeight = imageHeight;
1073  if (scrollY > maxScrollY) {
1074  scrollY = maxScrollY;
1075  }
1076  }
1077 
1078  // Update the viewport size.
1079  fxImageView.setViewport(new Rectangle2D(
1080  0, 0, viewportWidth, viewportHeight));
1081 
1082  // Step 1: Zoom
1083  Scale scale = new Scale();
1084  scale.setX(zoomRatio);
1085  scale.setY(zoomRatio);
1086  scale.setPivotX(imageWidth / 2);
1087  scale.setPivotY(imageHeight / 2);
1088 
1089  // Step 2: Rotate
1090  Rotate rotate = new Rotate();
1091  rotate.setPivotX(imageWidth / 2);
1092  rotate.setPivotY(imageHeight / 2);
1093  rotate.setAngle(rotation);
1094 
1095  // Step 3: Position
1096  Translate translate = new Translate();
1097  translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
1098  translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
1099 
1100  // Add the transforms in reverse order of intended execution.
1101  // Note: They MUST be added in this order to ensure translate is
1102  // executed last.
1103  masterGroup.getTransforms().clear();
1104  masterGroup.getTransforms().addAll(translate, rotate, scale);
1105 
1106  // Adjust scroll bar positions for view changes.
1107  if (viewportWidth > fxPanel.getWidth()) {
1108  scrollPane.setHvalue(scrollX);
1109  }
1110  if (viewportHeight > fxPanel.getHeight()) {
1111  scrollPane.setVvalue(scrollY);
1112  }
1113 
1114  // Update all image controls to reflect the current values.
1115  zoomOutButton.setEnabled(zoomRatio > MIN_ZOOM_RATIO);
1116  zoomInButton.setEnabled(zoomRatio < MAX_ZOOM_RATIO);
1117  rotationTextField.setText((int) rotation + "°");
1118  zoomTextField.setText((Math.round(zoomRatio * 100.0)) + "%");
1119  }
1120 }

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