Autopsy  4.16.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 2018-2020 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.annotation.concurrent.Immutable;
63 import javax.swing.JMenuItem;
64 import javax.swing.JOptionPane;
65 import javax.swing.JPanel;
66 import javax.swing.JPopupMenu;
67 import javax.swing.JSeparator;
68 import javax.swing.SwingUtilities;
69 import javax.swing.SwingWorker;
70 import org.apache.commons.io.FilenameUtils;
71 import org.controlsfx.control.MaskerPane;
72 import org.openide.util.NbBundle;
73 import org.python.google.common.collect.Lists;
94 import org.sleuthkit.datamodel.AbstractFile;
95 import org.sleuthkit.datamodel.ContentTag;
96 import org.sleuthkit.datamodel.TskCoreException;
97 
103 @NbBundle.Messages({
104  "MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E",
105  "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
106  "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."
107 })
108 @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
109 class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPanel {
110 
111  private static final long serialVersionUID = 1L;
112  private static final Logger logger = Logger.getLogger(MediaViewImagePanel.class.getName());
113  private static final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
114  private static final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
115  .map("."::concat) //NOI18N
116  .collect(Collectors.toList());
117  private static final double[] ZOOM_STEPS = {
118  0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
119  1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
120  private static final double MIN_ZOOM_RATIO = 0.0625; // 6.25%
121  private static final double MAX_ZOOM_RATIO = 10.0; // 1000%
122  private static final Image openInExternalViewerButtonImage = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm()); //NOI18N
123  private final boolean jfxIsInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited();
124  private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
125 
126  /*
127  * Threading policy: JFX UI components, must be accessed in JFX thread only.
128  */
129  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
130  private final ProgressBar progressBar = new ProgressBar();
131  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
132  private final MaskerPane maskerPane = new MaskerPane();
133  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
134  private Group masterGroup;
135  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
136  private ImageTagsGroup tagsGroup;
137  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
138  private ImageTagCreator imageTagCreator;
139  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
140  private ImageView fxImageView;
141  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
142  private ScrollPane scrollPane;
143 
144  /*
145  * Threading policy: Swing UI components, must be accessed in EDT only.
146  */
147  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
148  private final JPopupMenu imageTaggingOptions;
149  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
150  private final JMenuItem createTagMenuItem;
151  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
152  private final JMenuItem deleteTagMenuItem;
153  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
154  private final JMenuItem hideTagsMenuItem;
155  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
156  private final JMenuItem exportTagsMenuItem;
157  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
158  private final JFileChooser exportChooser;
159  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
160  private final JFXPanel fxPanel;
161 
162  /*
163  * Panel state variables threading policy:
164  *
165  * imageFile: The loadFile() method kicks off a JFX background task to read
166  * the content of the currently selected file into a JFX Image object. If
167  * the task succeeds and is not cancelled, the AbstractFile reference is
168  * saved as imageFile. The reference is used for tagging operations which
169  * are done in the JFX thread. IMPORTANT: Thread confinement is maintained
170  * by capturing the reference in a local variable before dispatching a tag
171  * export task to the SwingWorker thread pool. The imageFile field should
172  * not be read directly in the JFX thread.
173  *
174  * readImageFileTask: This is a reference to a JFX background task that
175  * reads the content of the currently selected file into a JFX Image object.
176  * A reference is maintained so that the task can be cancelled if it is
177  * running when the selected image file changes. Only accessed in the JFX
178  * thread.
179  *
180  * imageTransforms: These values are mostly written in the EDT based on user
181  * interactions with Swing components and then read in the JFX thread when
182  * rendering the image. The exception is recalculation of the zoom ratio
183  * based on the image size when a) the selected image file is changed, b)
184  * the panel is resized or c) the user pushes the reset button to clear any
185  * transforms they have specified. In these three cases, the zoom ratio
186  * update happens in the JFX thread since the image must be accessed.
187  * IMPORTANT: The image transforms are bundled as atomic state and a
188  * snapshot should be captured for each rendering operation on the JFX
189  * thread so that the image transforms do not change during rendering due to
190  * user interactions in the EDT.
191  */
192  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
193  private AbstractFile imageFile;
194  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
195  private Task<Image> readImageFileTask;
196  private volatile ImageTransforms imageTransforms;
197 
198  static {
199  ImageIO.scanForPlugins();
200  }
201 
208  @NbBundle.Messages({
209  "MediaViewImagePanel.createTagOption=Create",
210  "MediaViewImagePanel.deleteTagOption=Delete",
211  "MediaViewImagePanel.hideTagOption=Hide",
212  "MediaViewImagePanel.exportTagOption=Export"
213  })
214  MediaViewImagePanel() {
215  initComponents();
216 
217  imageTransforms = new ImageTransforms(0, 0, true);
218 
219  exportChooser = new JFileChooser();
220  exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle());
221 
222  //Build popupMenu when Tags Menu button is pressed.
223  imageTaggingOptions = new JPopupMenu();
224  createTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
225  createTagMenuItem.addActionListener((event) -> createTag());
226  imageTaggingOptions.add(createTagMenuItem);
227 
228  imageTaggingOptions.add(new JSeparator());
229 
230  deleteTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
231  deleteTagMenuItem.addActionListener((event) -> deleteTag());
232  imageTaggingOptions.add(deleteTagMenuItem);
233 
234  imageTaggingOptions.add(new JSeparator());
235 
236  hideTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
237  hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
238  imageTaggingOptions.add(hideTagsMenuItem);
239 
240  imageTaggingOptions.add(new JSeparator());
241 
242  exportTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
243  exportTagsMenuItem.addActionListener((event) -> exportTags());
244  imageTaggingOptions.add(exportTagsMenuItem);
245 
246  imageTaggingOptions.setPopupSize(300, 150);
247 
248  //Disable image tagging for non-windows users or upon failure to load OpenCV.
249  if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
250  tagsMenu.setEnabled(false);
251  imageTaggingOptions.setEnabled(false);
252  }
253 
254  fxPanel = new JFXPanel();
255  if (isInited()) {
256  Platform.runLater(new Runnable() {
257  @Override
258  public void run() {
259  // build jfx ui (we could do this in FXML?)
260  fxImageView = new ImageView(); // will hold image
261  masterGroup = new Group(fxImageView);
262  tagsGroup = new ImageTagsGroup(fxImageView);
263  tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
264  if (c.getList().isEmpty()) {
265  pcs.firePropertyChange(new PropertyChangeEvent(this,
266  "state", null, State.EMPTY));
267  }
268  });
269 
270  /*
271  * RC: I'm not sure exactly why this is located precisely
272  * here. At least putting this call outside of the
273  * constructor avoids leaking the "this" reference of a
274  * partially constructed instance of this class that is
275  * given to the PropertyChangeSupport object created at the
276  * very beginning of construction.
277  */
278  subscribeTagMenuItemsToStateChanges();
279 
280  masterGroup.getChildren().add(tagsGroup);
281 
282  //Update buttons when users select (or unselect) image tags.
283  tagsGroup.addFocusChangeListener((event) -> {
284  if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
285  if (masterGroup.getChildren().contains(imageTagCreator)) {
286  return;
287  }
288 
289  if (tagsGroup.getChildren().isEmpty()) {
290  pcs.firePropertyChange(new PropertyChangeEvent(this,
291  "state", null, State.EMPTY));
292  } else {
293  pcs.firePropertyChange(new PropertyChangeEvent(this,
294  "state", null, State.CREATE));
295  }
296  } else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
297  pcs.firePropertyChange(new PropertyChangeEvent(this,
298  "state", null, State.SELECTED));
299  }
300  });
301 
302  scrollPane = new ScrollPane(masterGroup); // scrolls and sizes imageview
303  scrollPane.getStyleClass().add("bg"); //NOI18N
304  scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
305  scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
306 
307  Scene scene = new Scene(scrollPane); //root of jfx tree
308  scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N
309  fxPanel.setScene(scene);
310 
311  fxImageView.setSmooth(true);
312  fxImageView.setCache(true);
313 
314  EventQueue.invokeLater(() -> {
315  add(fxPanel);//add jfx ui to JPanel
316  });
317  }
318  });
319  }
320  }
321 
327  private void subscribeTagMenuItemsToStateChanges() {
328  pcs.addPropertyChangeListener((event) -> {
329  State currentState = (State) event.getNewValue();
330  switch (currentState) {
331  case CREATE:
332  SwingUtilities.invokeLater(() -> {
333  createTagMenuItem.setEnabled(true);
334  deleteTagMenuItem.setEnabled(false);
335  hideTagsMenuItem.setEnabled(true);
336  exportTagsMenuItem.setEnabled(true);
337  });
338  break;
339  case SELECTED:
340  Platform.runLater(() -> {
341  if (masterGroup.getChildren().contains(imageTagCreator)) {
342  imageTagCreator.disconnect();
343  masterGroup.getChildren().remove(imageTagCreator);
344  }
345  SwingUtilities.invokeLater(() -> {
346  createTagMenuItem.setEnabled(false);
347  deleteTagMenuItem.setEnabled(true);
348  hideTagsMenuItem.setEnabled(true);
349  exportTagsMenuItem.setEnabled(true);
350  });
351  });
352  break;
353  case HIDDEN:
354  SwingUtilities.invokeLater(() -> {
355  createTagMenuItem.setEnabled(false);
356  deleteTagMenuItem.setEnabled(false);
357  hideTagsMenuItem.setEnabled(true);
358  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
359  exportTagsMenuItem.setEnabled(false);
360  });
361  break;
362  case VISIBLE:
363  SwingUtilities.invokeLater(() -> {
364  createTagMenuItem.setEnabled(true);
365  deleteTagMenuItem.setEnabled(false);
366  hideTagsMenuItem.setEnabled(true);
367  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
368  exportTagsMenuItem.setEnabled(true);
369  });
370  break;
371  case DEFAULT:
372  case EMPTY:
373  Platform.runLater(() -> {
374  if (masterGroup.getChildren().contains(imageTagCreator)) {
375  imageTagCreator.disconnect();
376  }
377  SwingUtilities.invokeLater(() -> {
378  createTagMenuItem.setEnabled(true);
379  deleteTagMenuItem.setEnabled(false);
380  hideTagsMenuItem.setEnabled(false);
381  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
382  exportTagsMenuItem.setEnabled(false);
383  });
384  });
385  break;
386  case NONEMPTY:
387  SwingUtilities.invokeLater(() -> {
388  createTagMenuItem.setEnabled(true);
389  deleteTagMenuItem.setEnabled(false);
390  hideTagsMenuItem.setEnabled(true);
391  exportTagsMenuItem.setEnabled(true);
392  });
393  break;
394  case DISABLE:
395  SwingUtilities.invokeLater(() -> {
396  createTagMenuItem.setEnabled(false);
397  deleteTagMenuItem.setEnabled(false);
398  hideTagsMenuItem.setEnabled(false);
399  exportTagsMenuItem.setEnabled(false);
400  });
401  break;
402  default:
403  break;
404  }
405  });
406  }
407 
408  /*
409  * Indicates whether or not the panel can be used, i.e., JavaFX has been
410  * intitialized.
411  */
412  final boolean isInited() {
413  return jfxIsInited;
414  }
415 
419  final void reset() {
420  Platform.runLater(() -> {
421  fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0));
422  fxImageView.setImage(null);
423  pcs.firePropertyChange(new PropertyChangeEvent(this,
424  "state", null, State.DEFAULT));
425  masterGroup.getChildren().clear();
426  scrollPane.setContent(null);
427  scrollPane.setContent(masterGroup);
428  });
429  }
430 
438  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
439  private void showErrorButton(String errorMessage, AbstractFile file) {
440  ensureInJfxThread();
441  final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(openInExternalViewerButtonImage));
442  externalViewerButton.setOnAction(actionEvent
443  -> new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file))
444  .actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, ""))
445  );
446  final VBox errorNode = new VBox(10, new Label(errorMessage), externalViewerButton);
447  errorNode.setAlignment(Pos.CENTER);
448  }
449 
455  @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
456  final void loadFile(final AbstractFile file) {
457  ensureInSwingThread();
458  if (!isInited()) {
459  return;
460  }
461 
462  final double panelWidth = fxPanel.getWidth();
463  final double panelHeight = fxPanel.getHeight();
464  Platform.runLater(() -> {
465  /*
466  * Set up a new task to get the contents of the image file in
467  * displayable form and cancel any previous task in progress.
468  */
469  if (readImageFileTask != null) {
470  readImageFileTask.cancel();
471  }
472  readImageFileTask = ImageUtils.newReadImageTask(file);
473  readImageFileTask.setOnSucceeded(succeeded -> {
474  onReadImageTaskSucceeded(file, panelWidth, panelHeight);
475  });
476  readImageFileTask.setOnFailed(failed -> {
477  onReadImageTaskFailed(file);
478  });
479 
480  /*
481  * Update the JFX components to a "task in progress" state and start
482  * the task.
483  */
484  maskerPane.setProgressNode(progressBar);
485  progressBar.progressProperty().bind(readImageFileTask.progressProperty());
486  maskerPane.textProperty().bind(readImageFileTask.messageProperty());
487  scrollPane.setContent(null); // Prevent content display issues.
488  scrollPane.setCursor(Cursor.WAIT);
489  new Thread(readImageFileTask).start();
490  });
491  }
492 
503  private void onReadImageTaskSucceeded(AbstractFile file, double panelWidth, double panelHeight) {
504  if (!Case.isCaseOpen()) {
505  /*
506  * Handle the in-between condition when case is being closed and an
507  * image was previously selected
508  *
509  * NOTE: I think this is unnecessary -jm
510  */
511  reset();
512  return;
513  }
514 
515  Platform.runLater(() -> {
516  try {
517  Image fxImage = readImageFileTask.get();
518  masterGroup.getChildren().clear();
519  tagsGroup.getChildren().clear();
520  this.imageFile = file;
521  if (nonNull(fxImage)) {
522  // We have a non-null image, so let's show it.
523  fxImageView.setImage(fxImage);
524  resetView(panelWidth, panelHeight);
525  masterGroup.getChildren().add(fxImageView);
526  masterGroup.getChildren().add(tagsGroup);
527 
528  try {
529  List<ContentTag> tags = Case.getCurrentCase().getServices()
530  .getTagsManager().getContentTagsByContent(file);
531 
532  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
533  //Add all image tags
534  tagsGroup = buildImageTagsGroup(contentViewerTags);
535  if (!tagsGroup.getChildren().isEmpty()) {
536  pcs.firePropertyChange(new PropertyChangeEvent(this,
537  "state", null, State.NONEMPTY));
538  }
539  } catch (TskCoreException | NoCurrentCaseException ex) {
540  logger.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS
541  }
542  scrollPane.setContent(masterGroup);
543  } else {
544  showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
545  }
546  } catch (InterruptedException | ExecutionException ex) {
547  showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
548  }
549  scrollPane.setCursor(Cursor.DEFAULT);
550  });
551  }
552 
560  private void onReadImageTaskFailed(AbstractFile file) {
561  if (!Case.isCaseOpen()) {
562  /*
563  * Handle in-between condition when case is being closed and an
564  * image was previously selected
565  *
566  * NOTE: I think this is unnecessary -jm
567  */
568  reset();
569  return;
570  }
571 
572  Platform.runLater(() -> {
573  Throwable exception = readImageFileTask.getException();
574  if (exception instanceof OutOfMemoryError
575  && exception.getMessage().contains("Java heap space")) { //NON-NLS
576  showErrorButton(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
577  } else {
578  showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
579  }
580 
581  scrollPane.setCursor(Cursor.DEFAULT);
582  });
583  }
584 
596  private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
597  throws TskCoreException, NoCurrentCaseException {
598  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = new ArrayList<>();
599  for (ContentTag contentTag : contentTags) {
600  ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
601  .getTag(contentTag, ImageTagRegion.class);
602  if (contentViewerTag == null) {
603  continue;
604  }
605 
606  contentViewerTags.add(contentViewerTag);
607  }
608  return contentViewerTags;
609  }
610 
622  private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
623  ensureInJfxThread();
624  contentViewerTags.forEach(contentViewerTag -> {
629  tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
630  });
631  return tagsGroup;
632  }
633 
637  @Override
638  final public List<String> getSupportedMimeTypes() {
639  return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
640  }
641 
647  @Override
648  final public List<String> getSupportedExtensions() {
649  return getExtensions();
650  }
651 
657  final public List<String> getExtensions() {
658  return Collections.unmodifiableList(supportedExtensions);
659  }
660 
661  @Override
662  final public boolean isSupported(AbstractFile file) {
663  return ImageUtils.isImageThumbnailSupported(file);
664  }
665 
671  @SuppressWarnings("unchecked")
672  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
673  private void initComponents() {
674 
675  toolbar = new javax.swing.JToolBar();
676  rotationTextField = new javax.swing.JTextField();
677  rotateLeftButton = new javax.swing.JButton();
678  rotateRightButton = new javax.swing.JButton();
679  jSeparator1 = new javax.swing.JToolBar.Separator();
680  zoomTextField = new javax.swing.JTextField();
681  zoomOutButton = new javax.swing.JButton();
682  zoomInButton = new javax.swing.JButton();
683  jSeparator2 = new javax.swing.JToolBar.Separator();
684  zoomResetButton = new javax.swing.JButton();
685  filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0));
686  filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
687  jPanel1 = new javax.swing.JPanel();
688  tagsMenu = new javax.swing.JButton();
689 
690  setBackground(new java.awt.Color(0, 0, 0));
691  addComponentListener(new java.awt.event.ComponentAdapter() {
692  public void componentResized(java.awt.event.ComponentEvent evt) {
693  formComponentResized(evt);
694  }
695  });
696  setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));
697 
698  toolbar.setFloatable(false);
699  toolbar.setRollover(true);
700  toolbar.setMaximumSize(new java.awt.Dimension(32767, 23));
701  toolbar.setName(""); // NOI18N
702 
703  rotationTextField.setEditable(false);
704  rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
705  rotationTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotationTextField.text")); // NOI18N
706  rotationTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
707  rotationTextField.setMinimumSize(new java.awt.Dimension(50, 20));
708  rotationTextField.setPreferredSize(new java.awt.Dimension(50, 20));
709  toolbar.add(rotationTextField);
710 
711  rotateLeftButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png"))); // NOI18N
712  org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.text")); // NOI18N
713  rotateLeftButton.setToolTipText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateLeftButton.toolTipText")); // NOI18N
714  rotateLeftButton.setFocusable(false);
715  rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
716  rotateLeftButton.setMaximumSize(new java.awt.Dimension(24, 24));
717  rotateLeftButton.setMinimumSize(new java.awt.Dimension(24, 24));
718  rotateLeftButton.setPreferredSize(new java.awt.Dimension(24, 24));
719  rotateLeftButton.addActionListener(new java.awt.event.ActionListener() {
720  public void actionPerformed(java.awt.event.ActionEvent evt) {
721  rotateLeftButtonActionPerformed(evt);
722  }
723  });
724  toolbar.add(rotateLeftButton);
725 
726  rotateRightButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png"))); // NOI18N
727  org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.rotateRightButton.text")); // NOI18N
728  rotateRightButton.setFocusable(false);
729  rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
730  rotateRightButton.setMaximumSize(new java.awt.Dimension(24, 24));
731  rotateRightButton.setMinimumSize(new java.awt.Dimension(24, 24));
732  rotateRightButton.setPreferredSize(new java.awt.Dimension(24, 24));
733  rotateRightButton.addActionListener(new java.awt.event.ActionListener() {
734  public void actionPerformed(java.awt.event.ActionEvent evt) {
735  rotateRightButtonActionPerformed(evt);
736  }
737  });
738  toolbar.add(rotateRightButton);
739 
740  jSeparator1.setMaximumSize(new java.awt.Dimension(6, 20));
741  toolbar.add(jSeparator1);
742 
743  zoomTextField.setEditable(false);
744  zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
745  zoomTextField.setText(org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomTextField.text")); // NOI18N
746  zoomTextField.setMaximumSize(new java.awt.Dimension(50, 2147483647));
747  zoomTextField.setMinimumSize(new java.awt.Dimension(50, 20));
748  zoomTextField.setPreferredSize(new java.awt.Dimension(50, 20));
749  toolbar.add(zoomTextField);
750 
751  zoomOutButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png"))); // NOI18N
752  org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomOutButton.text")); // NOI18N
753  zoomOutButton.setFocusable(false);
754  zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
755  zoomOutButton.setMaximumSize(new java.awt.Dimension(24, 24));
756  zoomOutButton.setMinimumSize(new java.awt.Dimension(24, 24));
757  zoomOutButton.setPreferredSize(new java.awt.Dimension(24, 24));
758  zoomOutButton.addActionListener(new java.awt.event.ActionListener() {
759  public void actionPerformed(java.awt.event.ActionEvent evt) {
760  zoomOutButtonActionPerformed(evt);
761  }
762  });
763  toolbar.add(zoomOutButton);
764 
765  zoomInButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png"))); // NOI18N
766  org.openide.awt.Mnemonics.setLocalizedText(zoomInButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomInButton.text")); // NOI18N
767  zoomInButton.setFocusable(false);
768  zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
769  zoomInButton.setMaximumSize(new java.awt.Dimension(24, 24));
770  zoomInButton.setMinimumSize(new java.awt.Dimension(24, 24));
771  zoomInButton.setPreferredSize(new java.awt.Dimension(24, 24));
772  zoomInButton.addActionListener(new java.awt.event.ActionListener() {
773  public void actionPerformed(java.awt.event.ActionEvent evt) {
774  zoomInButtonActionPerformed(evt);
775  }
776  });
777  toolbar.add(zoomInButton);
778 
779  jSeparator2.setMaximumSize(new java.awt.Dimension(6, 20));
780  toolbar.add(jSeparator2);
781 
782  org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.zoomResetButton.text")); // NOI18N
783  zoomResetButton.setFocusable(false);
784  zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
785  zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
786  zoomResetButton.addActionListener(new java.awt.event.ActionListener() {
787  public void actionPerformed(java.awt.event.ActionEvent evt) {
788  zoomResetButtonActionPerformed(evt);
789  }
790  });
791  toolbar.add(zoomResetButton);
792  toolbar.add(filler1);
793  toolbar.add(filler2);
794  toolbar.add(jPanel1);
795 
796  org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.tagsMenu.text_1")); // NOI18N
797  tagsMenu.setFocusable(false);
798  tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
799  tagsMenu.setMaximumSize(new java.awt.Dimension(75, 21));
800  tagsMenu.setMinimumSize(new java.awt.Dimension(75, 21));
801  tagsMenu.setPreferredSize(new java.awt.Dimension(75, 21));
802  tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
803  tagsMenu.addMouseListener(new java.awt.event.MouseAdapter() {
804  public void mousePressed(java.awt.event.MouseEvent evt) {
805  tagsMenuMousePressed(evt);
806  }
807  });
808  toolbar.add(tagsMenu);
809 
810  add(toolbar);
811  }// </editor-fold>//GEN-END:initComponents
812 
813  private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateLeftButtonActionPerformed
814  rotateImage(270);
815  }//GEN-LAST:event_rotateLeftButtonActionPerformed
816 
817  private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateRightButtonActionPerformed
818  rotateImage(90);
819  }//GEN-LAST:event_rotateRightButtonActionPerformed
820 
821  private void rotateImage(int angle) {
822  final double panelWidth = fxPanel.getWidth();
823  final double panelHeight = fxPanel.getHeight();
824  ImageTransforms currentTransforms = imageTransforms;
825  double newRotation = (currentTransforms.getRotation() + angle) % 360;
826  final ImageTransforms newTransforms = new ImageTransforms(currentTransforms.getZoomRatio(), newRotation, false);
827  imageTransforms = newTransforms;
828  Platform.runLater(() -> {
829  updateView(panelWidth, panelHeight, newTransforms);
830  });
831  }
832 
833  private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomInButtonActionPerformed
834  zoomImage(ZoomDirection.IN);
835  }//GEN-LAST:event_zoomInButtonActionPerformed
836 
837  private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOutButtonActionPerformed
838  zoomImage(ZoomDirection.OUT);
839  }//GEN-LAST:event_zoomOutButtonActionPerformed
840 
841  private void zoomImage(ZoomDirection direction) {
842  ensureInSwingThread();
843  final double panelWidth = fxPanel.getWidth();
844  final double panelHeight = fxPanel.getHeight();
845  final ImageTransforms currentTransforms = imageTransforms;
846  double newZoomRatio;
847  if (direction == ZoomDirection.IN) {
848  newZoomRatio = zoomImageIn(currentTransforms.getZoomRatio());
849  } else {
850  newZoomRatio = zoomImageOut(currentTransforms.getZoomRatio());
851  }
852  final ImageTransforms newTransforms = new ImageTransforms(newZoomRatio, currentTransforms.getRotation(), false);
853  imageTransforms = newTransforms;
854  Platform.runLater(() -> {
855  updateView(panelWidth, panelHeight, newTransforms);
856  });
857  }
858 
859  private double zoomImageIn(double zoomRatio) {
860  double newZoomRatio = zoomRatio;
861  for (int i = 0; i < ZOOM_STEPS.length; i++) {
862  if (newZoomRatio < ZOOM_STEPS[i]) {
863  newZoomRatio = ZOOM_STEPS[i];
864  break;
865  }
866  }
867  return newZoomRatio;
868  }
869 
870  private double zoomImageOut(double zoomRatio) {
871  double newZoomRatio = zoomRatio;
872  for (int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
873  if (newZoomRatio > ZOOM_STEPS[i]) {
874  newZoomRatio = ZOOM_STEPS[i];
875  break;
876  }
877  }
878  return newZoomRatio;
879  }
880 
881  private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomResetButtonActionPerformed
882  final ImageTransforms currentTransforms = imageTransforms;
883  final ImageTransforms newTransforms = new ImageTransforms(0, currentTransforms.getRotation(), true);
884  imageTransforms = newTransforms;
885  resetView();
886  }//GEN-LAST:event_zoomResetButtonActionPerformed
887 
888  private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
889  final ImageTransforms currentTransforms = imageTransforms;
890  if (currentTransforms.shouldAutoResize()) {
891  resetView();
892  } else {
893  final double panelWidth = fxPanel.getWidth();
894  final double panelHeight = fxPanel.getHeight();
895  Platform.runLater(() -> {
896  updateView(panelWidth, panelHeight, currentTransforms);
897  });
898  }
899  }//GEN-LAST:event_formComponentResized
900 
905  private void deleteTag() {
906  Platform.runLater(() -> {
907  ImageTag tagInFocus = tagsGroup.getFocus();
908  if (tagInFocus == null) {
909  return;
910  }
911 
912  try {
913  ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
914  scrollPane.setCursor(Cursor.WAIT);
915  ContentViewerTagManager.deleteTag(contentViewerTag);
916  Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
917  tagsGroup.getChildren().remove(tagInFocus);
918  } catch (TskCoreException | NoCurrentCaseException ex) {
919  logger.log(Level.WARNING, "Could not delete image tag in case db", ex); //NON-NLS
920  }
921 
922  scrollPane.setCursor(Cursor.DEFAULT);
923  });
924 
925  pcs.firePropertyChange(new PropertyChangeEvent(this,
926  "state", null, State.CREATE));
927  }
928 
933  private void createTag() {
934  pcs.firePropertyChange(new PropertyChangeEvent(this,
935  "state", null, State.DISABLE));
936  Platform.runLater(() -> {
937  imageTagCreator = new ImageTagCreator(fxImageView);
938 
939  PropertyChangeListener newTagListener = (event) -> {
940  SwingUtilities.invokeLater(() -> {
941  ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
942  //Ask the user for tag name and comment
943  TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
944  if (result != null) {
945  //Persist and build image tag
946  Platform.runLater(() -> {
947  try {
948  scrollPane.setCursor(Cursor.WAIT);
949  ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
950  ImageTag imageTag = buildImageTag(contentViewerTag);
951  tagsGroup.getChildren().add(imageTag);
952  } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
953  logger.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS
954  }
955 
956  scrollPane.setCursor(Cursor.DEFAULT);
957  });
958  }
959 
960  pcs.firePropertyChange(new PropertyChangeEvent(this,
961  "state", null, State.CREATE));
962  });
963 
964  //Remove image tag creator from panel
965  Platform.runLater(() -> {
966  imageTagCreator.disconnect();
967  masterGroup.getChildren().remove(imageTagCreator);
968  });
969  };
970 
971  imageTagCreator.addNewTagListener(newTagListener);
972  masterGroup.getChildren().add(imageTagCreator);
973  });
974  }
975 
983  private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
984  ensureInJfxThread();
985  ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView);
986 
987  //Automatically persist edits made by user
988  imageTag.subscribeToEditEvents((edit) -> {
989  try {
990  scrollPane.setCursor(Cursor.WAIT);
991  ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
992  ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
993  } catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
994  logger.log(Level.WARNING, "Could not save edit for image tag in case db", ex); //NON-NLS
995  }
996  scrollPane.setCursor(Cursor.DEFAULT);
997  });
998  return imageTag;
999  }
1000 
1008  private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result) throws TskCoreException, SerializationException, NoCurrentCaseException {
1009  ensureInJfxThread();
1010  scrollPane.setCursor(Cursor.WAIT);
1011  try {
1012  ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
1013  .addContentTag(imageFile, result.getTagName(), result.getComment());
1014  return ContentViewerTagManager.saveTag(contentTag, data);
1015  } finally {
1016  scrollPane.setCursor(Cursor.DEFAULT);
1017  }
1018  }
1019 
1024  private void showOrHideTags() {
1025  Platform.runLater(() -> {
1026  if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
1027  //Temporarily remove the tags group and update buttons
1028  masterGroup.getChildren().remove(tagsGroup);
1029  hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
1030  tagsGroup.clearFocus();
1031  pcs.firePropertyChange(new PropertyChangeEvent(this,
1032  "state", null, State.HIDDEN));
1033  } else {
1034  //Add tags group back in and update buttons
1035  masterGroup.getChildren().add(tagsGroup);
1036  hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
1037  pcs.firePropertyChange(new PropertyChangeEvent(this,
1038  "state", null, State.VISIBLE));
1039  }
1040  });
1041  }
1042 
1043  @NbBundle.Messages({
1044  "MediaViewImagePanel.exportSaveText=Save",
1045  "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
1046  "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
1047  "MediaViewImagePanel.fileChooserTitle=Choose a save location"
1048  })
1049  private void exportTags() {
1050  Platform.runLater(() -> {
1051  final AbstractFile file = imageFile;
1052  tagsGroup.clearFocus();
1053  SwingUtilities.invokeLater(() -> {
1054  exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1055  //Always base chooser location to export folder
1056  exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
1057  int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText());
1058  if (returnVal == JFileChooser.APPROVE_OPTION) {
1059  new SwingWorker<Void, Void>() {
1060  @Override
1061  protected Void doInBackground() {
1062  try {
1063  //Retrieve content viewer tags
1064  List<ContentTag> tags = Case.getCurrentCase().getServices()
1065  .getTagsManager().getContentTagsByContent(file);
1066  List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
1067 
1068  //Pull out image tag regions
1069  Collection<ImageTagRegion> regions = contentViewerTags.stream()
1070  .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
1071 
1072  //Apply tags to image and write to file
1073  BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
1074  Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
1075  FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS
1076  ImageIO.write(taggedImage, "png", output.toFile());
1077 
1078  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
1079  } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX.
1080  //This ensures we (devs and users) have something when it doesn't work.
1081  logger.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS
1082  JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
1083  }
1084  return null;
1085  }
1086  }.execute();
1087  }
1088  });
1089  });
1090  }
1091 
1092  private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed
1093  if (imageTaggingOptions.isEnabled()) {
1094  imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
1095  }
1096  }//GEN-LAST:event_tagsMenuMousePressed
1097 
1101  private enum DisplayOptions {
1102  HIDE_TAGS("Hide"),
1103  SHOW_TAGS("Show");
1104 
1105  private final String name;
1106 
1107  DisplayOptions(String name) {
1108  this.name = name;
1109  }
1110 
1111  String getName() {
1112  return name;
1113  }
1114  }
1115 
1120  private enum State {
1129  }
1130 
1131  // Variables declaration - do not modify//GEN-BEGIN:variables
1132  private javax.swing.Box.Filler filler1;
1133  private javax.swing.Box.Filler filler2;
1134  private javax.swing.JPanel jPanel1;
1135  private javax.swing.JToolBar.Separator jSeparator1;
1136  private javax.swing.JToolBar.Separator jSeparator2;
1137  private javax.swing.JButton rotateLeftButton;
1138  private javax.swing.JButton rotateRightButton;
1139  private javax.swing.JTextField rotationTextField;
1140  private javax.swing.JButton tagsMenu;
1141  private javax.swing.JToolBar toolbar;
1142  private javax.swing.JButton zoomInButton;
1143  private javax.swing.JButton zoomOutButton;
1144  private javax.swing.JButton zoomResetButton;
1145  private javax.swing.JTextField zoomTextField;
1146  // End of variables declaration//GEN-END:variables
1147 
1152  private void resetView() {
1153  ensureInSwingThread();
1154  final double panelWidth = fxPanel.getWidth();
1155  final double panelHeight = fxPanel.getHeight();
1156  Platform.runLater(() -> {
1157  resetView(panelWidth, panelHeight);
1158  });
1159  }
1160 
1171  private void resetView(double panelWidth, double panelHeight) {
1172  ensureInJfxThread();
1173 
1174  Image image = fxImageView.getImage();
1175  if (image == null) {
1176  return;
1177  }
1178 
1179  double imageWidth = image.getWidth();
1180  double imageHeight = image.getHeight();
1181  double scrollPaneWidth = panelWidth;
1182  double scrollPaneHeight = panelHeight;
1183  double zoomRatioWidth = scrollPaneWidth / imageWidth;
1184  double zoomRatioHeight = scrollPaneHeight / imageHeight;
1185  double newZoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight; // Use the smallest ratio size to fit the entire image in the view area.
1186  final ImageTransforms newTransforms = new ImageTransforms(newZoomRatio, 0, true);
1187  imageTransforms = newTransforms;
1188 
1189  scrollPane.setHvalue(0);
1190  scrollPane.setVvalue(0);
1191 
1192  updateView(panelWidth, panelHeight, newTransforms);
1193  }
1194 
1217  private void updateView(double panelWidth, double panelHeight, ImageTransforms imageTransforms) {
1218  ensureInJfxThread();
1219  Image image = fxImageView.getImage();
1220  if (image == null) {
1221  return;
1222  }
1223 
1224  // Image dimensions
1225  double imageWidth = image.getWidth();
1226  double imageHeight = image.getHeight();
1227 
1228  // Image dimensions with zooming applied
1229  double currentZoomRatio = imageTransforms.getZoomRatio();
1230  double adjustedImageWidth = imageWidth * currentZoomRatio;
1231  double adjustedImageHeight = imageHeight * currentZoomRatio;
1232 
1233  // ImageView viewport dimensions
1234  double viewportWidth;
1235  double viewportHeight;
1236 
1237  // Coordinates to center the image on the panel
1238  double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
1239  double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
1240 
1241  // Coordinates to keep the image inside the left/top boundaries
1242  double leftOffsetX;
1243  double topOffsetY;
1244 
1245  // Scroll bar positions
1246  double scrollX = scrollPane.getHvalue();
1247  double scrollY = scrollPane.getVvalue();
1248 
1249  // Scroll bar position boundaries (work-around for viewport size bug)
1250  double maxScrollX;
1251  double maxScrollY;
1252 
1253  // Set viewport size and translation offsets.
1254  final double currentRotation = imageTransforms.getRotation();
1255  if ((currentRotation % 180) == 0) {
1256  // Rotation is 0 or 180.
1257  viewportWidth = adjustedImageWidth;
1258  viewportHeight = adjustedImageHeight;
1259  leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
1260  topOffsetY = (adjustedImageHeight - imageHeight) / 2;
1261  maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
1262  maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
1263  } else {
1264  // Rotation is 90 or 270.
1265  viewportWidth = adjustedImageHeight;
1266  viewportHeight = adjustedImageWidth;
1267  leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
1268  topOffsetY = (adjustedImageWidth - imageHeight) / 2;
1269  maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
1270  maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
1271  }
1272 
1273  // Work around bug that truncates image if dimensions are too small.
1274  if (viewportWidth < imageWidth) {
1275  viewportWidth = imageWidth;
1276  if (scrollX > maxScrollX) {
1277  scrollX = maxScrollX;
1278  }
1279  }
1280  if (viewportHeight < imageHeight) {
1281  viewportHeight = imageHeight;
1282  if (scrollY > maxScrollY) {
1283  scrollY = maxScrollY;
1284  }
1285  }
1286 
1287  // Update the viewport size.
1288  fxImageView.setViewport(new Rectangle2D(
1289  0, 0, viewportWidth, viewportHeight));
1290 
1291  // Step 1: Zoom
1292  Scale scale = new Scale();
1293  scale.setX(currentZoomRatio);
1294  scale.setY(currentZoomRatio);
1295  scale.setPivotX(imageWidth / 2);
1296  scale.setPivotY(imageHeight / 2);
1297 
1298  // Step 2: Rotate
1299  Rotate rotate = new Rotate();
1300  rotate.setPivotX(imageWidth / 2);
1301  rotate.setPivotY(imageHeight / 2);
1302  rotate.setAngle(currentRotation);
1303 
1304  // Step 3: Position
1305  Translate translate = new Translate();
1306  translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
1307  translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
1308 
1309  // Add the transforms in reverse order of intended execution.
1310  // Note: They MUST be added in this order to ensure translate is
1311  // executed last.
1312  masterGroup.getTransforms().clear();
1313  masterGroup.getTransforms().addAll(translate, rotate, scale);
1314 
1315  // Adjust scroll bar positions for view changes.
1316  if (viewportWidth > fxPanel.getWidth()) {
1317  scrollPane.setHvalue(scrollX);
1318  }
1319  if (viewportHeight > fxPanel.getHeight()) {
1320  scrollPane.setVvalue(scrollY);
1321  }
1322 
1323  /*
1324  * RC: There is a race condition here, but it will probably be corrected
1325  * so fast the user will never see it. See Jira-6848 for details and a
1326  * solution that will simplify this class greatly in terms of thread
1327  * safety.
1328  */
1329  SwingUtilities.invokeLater(() -> {
1330  // Update all image controls to reflect the current values.
1331  zoomOutButton.setEnabled(currentZoomRatio > MIN_ZOOM_RATIO);
1332  zoomInButton.setEnabled(currentZoomRatio < MAX_ZOOM_RATIO);
1333  rotationTextField.setText((int) currentRotation + "°");
1334  zoomTextField.setText((Math.round(currentZoomRatio * 100.0)) + "%");
1335  });
1336  }
1337 
1343  private void ensureInJfxThread() {
1344  if (!Platform.isFxApplicationThread()) {
1345  throw new IllegalStateException("Attempt to execute JFX code outside of JFX thread"); //NON-NLS
1346  }
1347  }
1348 
1354  private void ensureInSwingThread() {
1355  if (!SwingUtilities.isEventDispatchThread()) {
1356  throw new IllegalStateException("Attempt to execute Swing code outside of EDT"); //NON-NLS
1357  }
1358  }
1359 
1363  private enum ZoomDirection {
1364  IN, OUT
1365  };
1366 
1370  @Immutable
1371  private static class ImageTransforms {
1372 
1373  private final double zoomRatio;
1374  private final double rotation;
1375  private final boolean autoResize;
1376 
1377  ImageTransforms(double zoomRatio, double rotation, boolean autoResize) {
1378  this.zoomRatio = zoomRatio;
1379  this.rotation = rotation;
1380  this.autoResize = autoResize;
1381  }
1382 
1388  private double getZoomRatio() {
1389  return zoomRatio;
1390  }
1391 
1398  private double getRotation() {
1399  return rotation;
1400  }
1401 
1409  private boolean shouldAutoResize() {
1410  return autoResize;
1411  }
1412 
1413  }
1414 
1415 }

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