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

Copyright © 2012-2018 Basis Technology. Generated on: Fri Jun 21 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.