19 package org.sleuthkit.autopsy.contentviewers;
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;
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;
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."
108 @SuppressWarnings(
"PMD.SingularField")
109 class MediaViewImagePanel
extends JPanel implements MediaFileViewer.MediaViewPanel {
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()
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;
121 private static final double MAX_ZOOM_RATIO = 10.0;
122 private static final Image openInExternalViewerButtonImage =
new Image(MediaViewImagePanel.class.getResource(
"/org/sleuthkit/autopsy/images/external.png").toExternalForm());
124 private final PropertyChangeSupport pcs =
new PropertyChangeSupport(
this);
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;
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;
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;
199 ImageIO.scanForPlugins();
209 "MediaViewImagePanel.createTagOption=Create",
210 "MediaViewImagePanel.deleteTagOption=Delete",
211 "MediaViewImagePanel.hideTagOption=Hide",
212 "MediaViewImagePanel.exportTagOption=Export"
214 MediaViewImagePanel() {
217 imageTransforms =
new ImageTransforms(0, 0,
true);
219 exportChooser =
new JFileChooser();
220 exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle());
223 imageTaggingOptions =
new JPopupMenu();
224 createTagMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
225 createTagMenuItem.addActionListener((event) -> createTag());
226 imageTaggingOptions.add(createTagMenuItem);
228 imageTaggingOptions.add(
new JSeparator());
230 deleteTagMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
231 deleteTagMenuItem.addActionListener((event) -> deleteTag());
232 imageTaggingOptions.add(deleteTagMenuItem);
234 imageTaggingOptions.add(
new JSeparator());
236 hideTagsMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
237 hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
238 imageTaggingOptions.add(hideTagsMenuItem);
240 imageTaggingOptions.add(
new JSeparator());
242 exportTagsMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
243 exportTagsMenuItem.addActionListener((event) -> exportTags());
244 imageTaggingOptions.add(exportTagsMenuItem);
246 imageTaggingOptions.setPopupSize(300, 150);
249 if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
250 tagsMenu.setEnabled(
false);
251 imageTaggingOptions.setEnabled(
false);
254 fxPanel =
new JFXPanel();
256 Platform.runLater(
new Runnable() {
260 fxImageView =
new ImageView();
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));
278 subscribeTagMenuItemsToStateChanges();
280 masterGroup.getChildren().add(tagsGroup);
283 tagsGroup.addFocusChangeListener((event) -> {
284 if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
285 if (masterGroup.getChildren().contains(imageTagCreator)) {
289 if (tagsGroup.getChildren().isEmpty()) {
290 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
291 "state", null, State.EMPTY));
293 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
294 "state", null, State.CREATE));
296 }
else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
297 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
298 "state", null, State.SELECTED));
302 scrollPane =
new ScrollPane(masterGroup);
303 scrollPane.getStyleClass().add(
"bg");
304 scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
305 scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
307 Scene scene =
new Scene(scrollPane);
308 scene.getStylesheets().add(MediaViewImagePanel.class.getResource(
"MediaViewImagePanel.css").toExternalForm());
309 fxPanel.setScene(scene);
311 fxImageView.setSmooth(
true);
312 fxImageView.setCache(
true);
314 EventQueue.invokeLater(() -> {
327 private void subscribeTagMenuItemsToStateChanges() {
328 pcs.addPropertyChangeListener((event) -> {
329 State currentState = (State) event.getNewValue();
330 switch (currentState) {
332 SwingUtilities.invokeLater(() -> {
333 createTagMenuItem.setEnabled(
true);
334 deleteTagMenuItem.setEnabled(
false);
335 hideTagsMenuItem.setEnabled(
true);
336 exportTagsMenuItem.setEnabled(
true);
340 Platform.runLater(() -> {
341 if (masterGroup.getChildren().contains(imageTagCreator)) {
342 imageTagCreator.disconnect();
343 masterGroup.getChildren().remove(imageTagCreator);
345 SwingUtilities.invokeLater(() -> {
346 createTagMenuItem.setEnabled(
false);
347 deleteTagMenuItem.setEnabled(
true);
348 hideTagsMenuItem.setEnabled(
true);
349 exportTagsMenuItem.setEnabled(
true);
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);
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);
373 Platform.runLater(() -> {
374 if (masterGroup.getChildren().contains(imageTagCreator)) {
375 imageTagCreator.disconnect();
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);
387 SwingUtilities.invokeLater(() -> {
388 createTagMenuItem.setEnabled(
true);
389 deleteTagMenuItem.setEnabled(
false);
390 hideTagsMenuItem.setEnabled(
true);
391 exportTagsMenuItem.setEnabled(
true);
395 SwingUtilities.invokeLater(() -> {
396 createTagMenuItem.setEnabled(
false);
397 deleteTagMenuItem.setEnabled(
false);
398 hideTagsMenuItem.setEnabled(
false);
399 exportTagsMenuItem.setEnabled(
false);
412 final boolean isInited() {
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);
438 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
439 private
void showErrorButton(String errorMessage, AbstractFile file) {
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,
""))
446 final VBox errorNode =
new VBox(10,
new Label(errorMessage), externalViewerButton);
447 errorNode.setAlignment(Pos.CENTER);
455 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
456 final
void loadFile(final AbstractFile file) {
457 ensureInSwingThread();
462 final double panelWidth = fxPanel.getWidth();
463 final double panelHeight = fxPanel.getHeight();
464 Platform.runLater(() -> {
469 if (readImageFileTask != null) {
470 readImageFileTask.cancel();
472 readImageFileTask = ImageUtils.newReadImageTask(file);
473 readImageFileTask.setOnSucceeded(succeeded -> {
474 onReadImageTaskSucceeded(file, panelWidth, panelHeight);
476 readImageFileTask.setOnFailed(failed -> {
477 onReadImageTaskFailed(file);
484 maskerPane.setProgressNode(progressBar);
485 progressBar.progressProperty().bind(readImageFileTask.progressProperty());
486 maskerPane.textProperty().bind(readImageFileTask.messageProperty());
487 scrollPane.setContent(null);
488 scrollPane.setCursor(Cursor.WAIT);
489 new Thread(readImageFileTask).start();
503 private void onReadImageTaskSucceeded(AbstractFile file,
double panelWidth,
double panelHeight) {
504 if (!Case.isCaseOpen()) {
515 Platform.runLater(() -> {
517 Image fxImage = readImageFileTask.get();
518 masterGroup.getChildren().clear();
519 tagsGroup.getChildren().clear();
520 this.imageFile = file;
521 if (nonNull(fxImage)) {
523 fxImageView.setImage(fxImage);
524 if (panelWidth != 0 && panelHeight != 0) {
525 resetView(panelWidth, panelHeight);
527 masterGroup.getChildren().add(fxImageView);
528 masterGroup.getChildren().add(tagsGroup);
531 List<ContentTag> tags = Case.getCurrentCase().getServices()
532 .getTagsManager().getContentTagsByContent(file);
534 List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
536 tagsGroup = buildImageTagsGroup(contentViewerTags);
537 if (!tagsGroup.getChildren().isEmpty()) {
538 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
539 "state", null, State.NONEMPTY));
541 }
catch (TskCoreException | NoCurrentCaseException ex) {
542 logger.log(Level.WARNING,
"Could not retrieve image tags for file in case db", ex);
544 scrollPane.setContent(masterGroup);
546 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
548 }
catch (InterruptedException | ExecutionException ex) {
549 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
551 scrollPane.setCursor(Cursor.DEFAULT);
562 private void onReadImageTaskFailed(AbstractFile file) {
563 if (!Case.isCaseOpen()) {
574 Platform.runLater(() -> {
575 Throwable exception = readImageFileTask.getException();
576 if (exception instanceof OutOfMemoryError
577 && exception.getMessage().contains(
"Java heap space")) {
578 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
580 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
583 scrollPane.setCursor(Cursor.DEFAULT);
598 private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
599 throws TskCoreException, NoCurrentCaseException {
600 List<ContentViewerTag<ImageTagRegion>> contentViewerTags =
new ArrayList<>();
601 for (ContentTag contentTag : contentTags) {
602 ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
603 .getTag(contentTag, ImageTagRegion.class);
604 if (contentViewerTag == null) {
608 contentViewerTags.add(contentViewerTag);
610 return contentViewerTags;
624 private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
626 contentViewerTags.forEach(contentViewerTag -> {
631 tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
640 final public List<String> getSupportedMimeTypes() {
641 return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
650 final public List<String> getSupportedExtensions() {
651 return getExtensions();
659 final public List<String> getExtensions() {
660 return Collections.unmodifiableList(supportedExtensions);
664 final public boolean isSupported(AbstractFile file) {
665 return ImageUtils.isImageThumbnailSupported(file);
673 @SuppressWarnings(
"unchecked")
675 private
void initComponents() {
677 toolbar =
new javax.swing.JToolBar();
678 rotationTextField =
new javax.swing.JTextField();
679 rotateLeftButton =
new javax.swing.JButton();
680 rotateRightButton =
new javax.swing.JButton();
681 jSeparator1 =
new javax.swing.JToolBar.Separator();
682 zoomTextField =
new javax.swing.JTextField();
683 zoomOutButton =
new javax.swing.JButton();
684 zoomInButton =
new javax.swing.JButton();
685 jSeparator2 =
new javax.swing.JToolBar.Separator();
686 zoomResetButton =
new javax.swing.JButton();
687 filler1 =
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0));
688 filler2 =
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0),
new java.awt.Dimension(32767, 0));
689 jPanel1 =
new javax.swing.JPanel();
690 tagsMenu =
new javax.swing.JButton();
692 setBackground(
new java.awt.Color(0, 0, 0));
693 addComponentListener(
new java.awt.event.ComponentAdapter() {
694 public void componentResized(java.awt.event.ComponentEvent evt) {
695 formComponentResized(evt);
698 setLayout(
new javax.swing.BoxLayout(
this, javax.swing.BoxLayout.Y_AXIS));
700 toolbar.setFloatable(
false);
701 toolbar.setRollover(
true);
702 toolbar.setMaximumSize(
new java.awt.Dimension(32767, 23));
705 rotationTextField.setEditable(
false);
706 rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
707 rotationTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotationTextField.text"));
708 rotationTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
709 rotationTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
710 rotationTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
711 toolbar.add(rotationTextField);
713 rotateLeftButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png")));
714 org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.text"));
715 rotateLeftButton.setToolTipText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.toolTipText"));
716 rotateLeftButton.setFocusable(
false);
717 rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
718 rotateLeftButton.setMaximumSize(
new java.awt.Dimension(24, 24));
719 rotateLeftButton.setMinimumSize(
new java.awt.Dimension(24, 24));
720 rotateLeftButton.setPreferredSize(
new java.awt.Dimension(24, 24));
721 rotateLeftButton.addActionListener(
new java.awt.event.ActionListener() {
722 public void actionPerformed(java.awt.event.ActionEvent evt) {
723 rotateLeftButtonActionPerformed(evt);
726 toolbar.add(rotateLeftButton);
728 rotateRightButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png")));
729 org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateRightButton.text"));
730 rotateRightButton.setFocusable(
false);
731 rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
732 rotateRightButton.setMaximumSize(
new java.awt.Dimension(24, 24));
733 rotateRightButton.setMinimumSize(
new java.awt.Dimension(24, 24));
734 rotateRightButton.setPreferredSize(
new java.awt.Dimension(24, 24));
735 rotateRightButton.addActionListener(
new java.awt.event.ActionListener() {
736 public void actionPerformed(java.awt.event.ActionEvent evt) {
737 rotateRightButtonActionPerformed(evt);
740 toolbar.add(rotateRightButton);
742 jSeparator1.setMaximumSize(
new java.awt.Dimension(6, 20));
743 toolbar.add(jSeparator1);
745 zoomTextField.setEditable(
false);
746 zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
747 zoomTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomTextField.text"));
748 zoomTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
749 zoomTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
750 zoomTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
751 toolbar.add(zoomTextField);
753 zoomOutButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png")));
754 org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomOutButton.text"));
755 zoomOutButton.setFocusable(
false);
756 zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
757 zoomOutButton.setMaximumSize(
new java.awt.Dimension(24, 24));
758 zoomOutButton.setMinimumSize(
new java.awt.Dimension(24, 24));
759 zoomOutButton.setPreferredSize(
new java.awt.Dimension(24, 24));
760 zoomOutButton.addActionListener(
new java.awt.event.ActionListener() {
761 public void actionPerformed(java.awt.event.ActionEvent evt) {
762 zoomOutButtonActionPerformed(evt);
765 toolbar.add(zoomOutButton);
767 zoomInButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png")));
768 org.openide.awt.Mnemonics.setLocalizedText(zoomInButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomInButton.text"));
769 zoomInButton.setFocusable(
false);
770 zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
771 zoomInButton.setMaximumSize(
new java.awt.Dimension(24, 24));
772 zoomInButton.setMinimumSize(
new java.awt.Dimension(24, 24));
773 zoomInButton.setPreferredSize(
new java.awt.Dimension(24, 24));
774 zoomInButton.addActionListener(
new java.awt.event.ActionListener() {
775 public void actionPerformed(java.awt.event.ActionEvent evt) {
776 zoomInButtonActionPerformed(evt);
779 toolbar.add(zoomInButton);
781 jSeparator2.setMaximumSize(
new java.awt.Dimension(6, 20));
782 toolbar.add(jSeparator2);
784 org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomResetButton.text"));
785 zoomResetButton.setFocusable(
false);
786 zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
787 zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
788 zoomResetButton.addActionListener(
new java.awt.event.ActionListener() {
789 public void actionPerformed(java.awt.event.ActionEvent evt) {
790 zoomResetButtonActionPerformed(evt);
793 toolbar.add(zoomResetButton);
794 toolbar.add(filler1);
795 toolbar.add(filler2);
796 toolbar.add(jPanel1);
798 org.openide.awt.Mnemonics.setLocalizedText(tagsMenu,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.tagsMenu.text_1"));
799 tagsMenu.setFocusable(
false);
800 tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
801 tagsMenu.setMaximumSize(
new java.awt.Dimension(75, 21));
802 tagsMenu.setMinimumSize(
new java.awt.Dimension(75, 21));
803 tagsMenu.setPreferredSize(
new java.awt.Dimension(75, 21));
804 tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
805 tagsMenu.addMouseListener(
new java.awt.event.MouseAdapter() {
806 public void mousePressed(java.awt.event.MouseEvent evt) {
807 tagsMenuMousePressed(evt);
810 toolbar.add(tagsMenu);
815 private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {
819 private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {
823 private void rotateImage(
int angle) {
824 final double panelWidth = fxPanel.getWidth();
825 final double panelHeight = fxPanel.getHeight();
826 ImageTransforms currentTransforms = imageTransforms;
827 double newRotation = (currentTransforms.getRotation() + angle) % 360;
828 final ImageTransforms newTransforms =
new ImageTransforms(currentTransforms.getZoomRatio(), newRotation,
false);
829 imageTransforms = newTransforms;
830 Platform.runLater(() -> {
831 updateView(panelWidth, panelHeight, newTransforms);
835 private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {
836 zoomImage(ZoomDirection.IN);
839 private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {
840 zoomImage(ZoomDirection.OUT);
843 private void zoomImage(ZoomDirection direction) {
844 ensureInSwingThread();
845 final double panelWidth = fxPanel.getWidth();
846 final double panelHeight = fxPanel.getHeight();
847 final ImageTransforms currentTransforms = imageTransforms;
849 if (direction == ZoomDirection.IN) {
850 newZoomRatio = zoomImageIn(currentTransforms.getZoomRatio());
852 newZoomRatio = zoomImageOut(currentTransforms.getZoomRatio());
854 final ImageTransforms newTransforms =
new ImageTransforms(newZoomRatio, currentTransforms.getRotation(),
false);
855 imageTransforms = newTransforms;
856 Platform.runLater(() -> {
857 updateView(panelWidth, panelHeight, newTransforms);
861 private double zoomImageIn(
double zoomRatio) {
862 double newZoomRatio = zoomRatio;
863 for (
int i = 0; i < ZOOM_STEPS.length; i++) {
864 if (newZoomRatio < ZOOM_STEPS[i]) {
865 newZoomRatio = ZOOM_STEPS[i];
872 private double zoomImageOut(
double zoomRatio) {
873 double newZoomRatio = zoomRatio;
874 for (
int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
875 if (newZoomRatio > ZOOM_STEPS[i]) {
876 newZoomRatio = ZOOM_STEPS[i];
883 private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {
884 final ImageTransforms currentTransforms = imageTransforms;
885 final ImageTransforms newTransforms =
new ImageTransforms(0, currentTransforms.getRotation(),
true);
886 imageTransforms = newTransforms;
890 private void formComponentResized(java.awt.event.ComponentEvent evt) {
891 final ImageTransforms currentTransforms = imageTransforms;
892 if (currentTransforms.shouldAutoResize()) {
895 final double panelWidth = fxPanel.getWidth();
896 final double panelHeight = fxPanel.getHeight();
897 Platform.runLater(() -> {
898 updateView(panelWidth, panelHeight, currentTransforms);
907 private void deleteTag() {
908 Platform.runLater(() -> {
909 ImageTag tagInFocus = tagsGroup.getFocus();
910 if (tagInFocus == null) {
915 ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
916 scrollPane.setCursor(Cursor.WAIT);
917 ContentViewerTagManager.deleteTag(contentViewerTag);
918 Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
919 tagsGroup.getChildren().remove(tagInFocus);
920 }
catch (TskCoreException | NoCurrentCaseException ex) {
921 logger.log(Level.WARNING,
"Could not delete image tag in case db", ex);
924 scrollPane.setCursor(Cursor.DEFAULT);
927 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
928 "state", null, State.CREATE));
935 private void createTag() {
936 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
937 "state", null, State.DISABLE));
938 Platform.runLater(() -> {
939 imageTagCreator =
new ImageTagCreator(fxImageView);
941 PropertyChangeListener newTagListener = (event) -> {
942 SwingUtilities.invokeLater(() -> {
943 ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
945 TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
946 if (result != null) {
948 Platform.runLater(() -> {
950 scrollPane.setCursor(Cursor.WAIT);
951 ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
952 ImageTag imageTag = buildImageTag(contentViewerTag);
953 tagsGroup.getChildren().add(imageTag);
954 }
catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
955 logger.log(Level.WARNING,
"Could not save new image tag in case db", ex);
958 scrollPane.setCursor(Cursor.DEFAULT);
962 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
963 "state", null, State.CREATE));
967 Platform.runLater(() -> {
968 imageTagCreator.disconnect();
969 masterGroup.getChildren().remove(imageTagCreator);
973 imageTagCreator.addNewTagListener(newTagListener);
974 masterGroup.getChildren().add(imageTagCreator);
985 private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
987 ImageTag imageTag =
new ImageTag(contentViewerTag, fxImageView);
990 imageTag.subscribeToEditEvents((edit) -> {
992 scrollPane.setCursor(Cursor.WAIT);
993 ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
994 ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
995 }
catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
996 logger.log(Level.WARNING,
"Could not save edit for image tag in case db", ex);
998 scrollPane.setCursor(Cursor.DEFAULT);
1010 private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result)
throws TskCoreException, SerializationException, NoCurrentCaseException {
1011 ensureInJfxThread();
1012 scrollPane.setCursor(Cursor.WAIT);
1014 ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
1015 .addContentTag(imageFile, result.getTagName(), result.getComment());
1016 return ContentViewerTagManager.saveTag(contentTag, data);
1018 scrollPane.setCursor(Cursor.DEFAULT);
1026 private void showOrHideTags() {
1027 Platform.runLater(() -> {
1028 if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
1030 masterGroup.getChildren().remove(tagsGroup);
1031 hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
1032 tagsGroup.clearFocus();
1033 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
1034 "state", null, State.HIDDEN));
1037 masterGroup.getChildren().add(tagsGroup);
1038 hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
1039 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
1040 "state", null, State.VISIBLE));
1045 @NbBundle.Messages({
1046 "MediaViewImagePanel.exportSaveText=Save",
1047 "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
1048 "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
1049 "MediaViewImagePanel.fileChooserTitle=Choose a save location"
1051 private void exportTags() {
1052 Platform.runLater(() -> {
1053 final AbstractFile file = imageFile;
1054 tagsGroup.clearFocus();
1055 SwingUtilities.invokeLater(() -> {
1056 exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1058 exportChooser.setCurrentDirectory(
new File(Case.getCurrentCase().getExportDirectory()));
1059 int returnVal = exportChooser.showDialog(
this, Bundle.MediaViewImagePanel_exportSaveText());
1060 if (returnVal == JFileChooser.APPROVE_OPTION) {
1061 new SwingWorker<Void, Void>() {
1063 protected Void doInBackground() {
1066 List<ContentTag> tags = Case.getCurrentCase().getServices()
1067 .getTagsManager().getContentTagsByContent(file);
1068 List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
1071 Collection<ImageTagRegion> regions = contentViewerTags.stream()
1072 .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
1075 BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
1076 Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
1077 FilenameUtils.getBaseName(file.getName()) +
"-with_tags.png");
1078 ImageIO.write(taggedImage,
"png", output.toFile());
1080 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
1081 }
catch (Exception ex) {
1083 logger.log(Level.WARNING,
"Unable to export tagged image to disk", ex);
1084 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
1094 private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {
1095 if (imageTaggingOptions.isEnabled()) {
1096 imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
1134 private javax.swing.Box.Filler filler1;
1135 private javax.swing.Box.Filler filler2;
1136 private javax.swing.JPanel jPanel1;
1137 private javax.swing.JToolBar.Separator jSeparator1;
1138 private javax.swing.JToolBar.Separator jSeparator2;
1139 private javax.swing.JButton rotateLeftButton;
1140 private javax.swing.JButton rotateRightButton;
1141 private javax.swing.JTextField rotationTextField;
1142 private javax.swing.JButton tagsMenu;
1143 private javax.swing.JToolBar toolbar;
1144 private javax.swing.JButton zoomInButton;
1145 private javax.swing.JButton zoomOutButton;
1146 private javax.swing.JButton zoomResetButton;
1147 private javax.swing.JTextField zoomTextField;
1154 private void resetView() {
1155 ensureInSwingThread();
1156 final double panelWidth = fxPanel.getWidth();
1157 final double panelHeight = fxPanel.getHeight();
1158 Platform.runLater(() -> {
1159 resetView(panelWidth, panelHeight);
1173 private void resetView(
double panelWidth,
double panelHeight) {
1174 ensureInJfxThread();
1176 Image image = fxImageView.getImage();
1177 if (image == null) {
1181 double imageWidth = image.getWidth();
1182 double imageHeight = image.getHeight();
1183 double scrollPaneWidth = panelWidth;
1184 double scrollPaneHeight = panelHeight;
1185 double zoomRatioWidth = scrollPaneWidth / imageWidth;
1186 double zoomRatioHeight = scrollPaneHeight / imageHeight;
1187 double newZoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
1188 final ImageTransforms newTransforms =
new ImageTransforms(newZoomRatio, 0,
true);
1189 imageTransforms = newTransforms;
1191 scrollPane.setHvalue(0);
1192 scrollPane.setVvalue(0);
1194 updateView(panelWidth, panelHeight, newTransforms);
1219 private void updateView(
double panelWidth,
double panelHeight, ImageTransforms imageTransforms) {
1220 ensureInJfxThread();
1221 Image image = fxImageView.getImage();
1222 if (image == null) {
1227 double imageWidth = image.getWidth();
1228 double imageHeight = image.getHeight();
1231 double currentZoomRatio = imageTransforms.getZoomRatio();
1232 double adjustedImageWidth = imageWidth * currentZoomRatio;
1233 double adjustedImageHeight = imageHeight * currentZoomRatio;
1236 double viewportWidth;
1237 double viewportHeight;
1240 double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
1241 double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
1248 double scrollX = scrollPane.getHvalue();
1249 double scrollY = scrollPane.getVvalue();
1256 final double currentRotation = imageTransforms.getRotation();
1257 if ((currentRotation % 180) == 0) {
1259 viewportWidth = adjustedImageWidth;
1260 viewportHeight = adjustedImageHeight;
1261 leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
1262 topOffsetY = (adjustedImageHeight - imageHeight) / 2;
1263 maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
1264 maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
1267 viewportWidth = adjustedImageHeight;
1268 viewportHeight = adjustedImageWidth;
1269 leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
1270 topOffsetY = (adjustedImageWidth - imageHeight) / 2;
1271 maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
1272 maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
1276 if (viewportWidth < imageWidth) {
1277 viewportWidth = imageWidth;
1278 if (scrollX > maxScrollX) {
1279 scrollX = maxScrollX;
1282 if (viewportHeight < imageHeight) {
1283 viewportHeight = imageHeight;
1284 if (scrollY > maxScrollY) {
1285 scrollY = maxScrollY;
1290 fxImageView.setViewport(
new Rectangle2D(
1291 0, 0, viewportWidth, viewportHeight));
1294 Scale scale =
new Scale();
1295 scale.setX(currentZoomRatio);
1296 scale.setY(currentZoomRatio);
1297 scale.setPivotX(imageWidth / 2);
1298 scale.setPivotY(imageHeight / 2);
1301 Rotate rotate =
new Rotate();
1302 rotate.setPivotX(imageWidth / 2);
1303 rotate.setPivotY(imageHeight / 2);
1304 rotate.setAngle(currentRotation);
1307 Translate translate =
new Translate();
1308 translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
1309 translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
1314 masterGroup.getTransforms().clear();
1315 masterGroup.getTransforms().addAll(translate, rotate, scale);
1318 if (viewportWidth > fxPanel.getWidth()) {
1319 scrollPane.setHvalue(scrollX);
1321 if (viewportHeight > fxPanel.getHeight()) {
1322 scrollPane.setVvalue(scrollY);
1331 SwingUtilities.invokeLater(() -> {
1333 zoomOutButton.setEnabled(currentZoomRatio > MIN_ZOOM_RATIO);
1334 zoomInButton.setEnabled(currentZoomRatio < MAX_ZOOM_RATIO);
1335 rotationTextField.setText((
int) currentRotation +
"°");
1336 zoomTextField.setText((Math.round(currentZoomRatio * 100.0)) +
"%");
1345 private void ensureInJfxThread() {
1346 if (!Platform.isFxApplicationThread()) {
1347 throw new IllegalStateException(
"Attempt to execute JFX code outside of JFX thread");
1356 private void ensureInSwingThread() {
1357 if (!SwingUtilities.isEventDispatchThread()) {
1358 throw new IllegalStateException(
"Attempt to execute Swing code outside of EDT");
1379 ImageTransforms(
double zoomRatio,
double rotation,
boolean autoResize) {
static boolean isJavaFxInited()
DisplayOptions(String name)
boolean shouldAutoResize()