19 package org.sleuthkit.autopsy.contentviewers;
21 import com.google.common.collect.Lists;
22 import java.awt.EventQueue;
23 import java.awt.event.ActionEvent;
24 import java.awt.image.BufferedImage;
25 import java.beans.PropertyChangeEvent;
26 import java.beans.PropertyChangeListener;
27 import java.beans.PropertyChangeSupport;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.List;
35 import static java.util.Objects.nonNull;
36 import java.util.concurrent.ExecutionException;
37 import java.util.concurrent.ExecutorService;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.FutureTask;
40 import java.util.logging.Level;
41 import java.util.stream.Collectors;
42 import javafx.application.Platform;
43 import javafx.collections.ListChangeListener.Change;
44 import javafx.concurrent.Task;
45 import javafx.embed.swing.JFXPanel;
46 import javafx.geometry.Pos;
47 import javafx.geometry.Rectangle2D;
48 import javafx.scene.Cursor;
49 import javafx.scene.Group;
50 import javafx.scene.Scene;
51 import javafx.scene.control.Button;
52 import javafx.scene.control.Label;
53 import javafx.scene.control.ProgressBar;
54 import javafx.scene.control.ScrollPane;
55 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
56 import javafx.scene.image.Image;
57 import javafx.scene.image.ImageView;
58 import javafx.scene.layout.VBox;
59 import javafx.scene.transform.Rotate;
60 import javafx.scene.transform.Scale;
61 import javafx.scene.transform.Translate;
62 import javax.imageio.ImageIO;
63 import javax.swing.JFileChooser;
64 import javafx.scene.Node;
65 import javax.annotation.concurrent.Immutable;
66 import javax.swing.JMenuItem;
67 import javax.swing.JOptionPane;
68 import javax.swing.JPanel;
69 import javax.swing.JPopupMenu;
70 import javax.swing.JSeparator;
71 import javax.swing.SwingUtilities;
72 import javax.swing.SwingWorker;
73 import org.apache.commons.io.FilenameUtils;
74 import org.controlsfx.control.MaskerPane;
75 import org.openide.util.NbBundle;
106 "MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E",
107 "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
108 "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."
110 @SuppressWarnings(
"PMD.SingularField")
111 class MediaViewImagePanel
extends JPanel implements MediaFileViewer.MediaViewPanel {
113 private static final long serialVersionUID = 1L;
114 private static final Logger logger = Logger.getLogger(MediaViewImagePanel.class.getName());
115 private static final double[] ZOOM_STEPS = {
116 0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
117 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
118 private static final double MIN_ZOOM_RATIO = 0.0625;
119 private static final double MAX_ZOOM_RATIO = 10.0;
120 private static final Image openInExternalViewerButtonImage =
new Image(MediaViewImagePanel.class.getResource(
"/org/sleuthkit/autopsy/images/external.png").toExternalForm());
122 private final PropertyChangeSupport pcs =
new PropertyChangeSupport(
this);
127 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
128 private final ProgressBar progressBar = new ProgressBar();
129 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
130 private final MaskerPane maskerPane = new MaskerPane();
131 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
132 private Group masterGroup;
133 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
134 private ImageTagsGroup tagsGroup;
135 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
136 private ImageTagCreator imageTagCreator;
137 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
138 private ImageView fxImageView;
139 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
140 private ScrollPane scrollPane;
145 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
146 private final JPopupMenu imageTaggingOptions;
147 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
148 private final JMenuItem createTagMenuItem;
149 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
150 private final JMenuItem deleteTagMenuItem;
151 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
152 private final JMenuItem hideTagsMenuItem;
153 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
154 private final JMenuItem exportTagsMenuItem;
155 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
156 private JFileChooser exportChooser;
157 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
158 private final JFXPanel fxPanel;
190 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
191 private AbstractFile imageFile;
192 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
193 private Task<Image> readImageFileTask;
194 private volatile ImageTransforms imageTransforms;
198 private final FutureTask<JFileChooser> futureFileChooser = new FutureTask<>(JFileChooser::new);
207 "MediaViewImagePanel.createTagOption=Create",
208 "MediaViewImagePanel.deleteTagOption=Delete",
209 "MediaViewImagePanel.hideTagOption=Hide",
210 "MediaViewImagePanel.exportTagOption=Export"
212 MediaViewImagePanel() {
215 imageTransforms =
new ImageTransforms(0, 0,
true);
217 ExecutorService executor = Executors.newSingleThreadExecutor();
218 executor.execute(futureFileChooser);
221 imageTaggingOptions =
new JPopupMenu();
222 createTagMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_createTagOption());
223 createTagMenuItem.addActionListener((event) -> createTag());
224 imageTaggingOptions.add(createTagMenuItem);
226 imageTaggingOptions.add(
new JSeparator());
228 deleteTagMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption());
229 deleteTagMenuItem.addActionListener((event) -> deleteTag());
230 imageTaggingOptions.add(deleteTagMenuItem);
232 imageTaggingOptions.add(
new JSeparator());
234 hideTagsMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption());
235 hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
236 imageTaggingOptions.add(hideTagsMenuItem);
238 imageTaggingOptions.add(
new JSeparator());
240 exportTagsMenuItem =
new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption());
241 exportTagsMenuItem.addActionListener((event) -> exportTags());
242 imageTaggingOptions.add(exportTagsMenuItem);
244 imageTaggingOptions.setPopupSize(300, 150);
247 if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.openCvIsLoaded()) {
248 tagsMenu.setEnabled(
false);
249 imageTaggingOptions.setEnabled(
false);
252 fxPanel =
new JFXPanel();
254 Platform.runLater(
new Runnable() {
258 fxImageView =
new ImageView();
259 masterGroup =
new Group(fxImageView);
260 tagsGroup =
new ImageTagsGroup(fxImageView);
261 tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
262 if (c.getList().isEmpty()) {
263 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
264 "state", null, State.EMPTY));
276 subscribeTagMenuItemsToStateChanges();
278 masterGroup.getChildren().add(tagsGroup);
281 tagsGroup.addFocusChangeListener((event) -> {
282 if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
283 if (masterGroup.getChildren().contains(imageTagCreator)) {
287 if (tagsGroup.getChildren().isEmpty()) {
288 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
289 "state", null, State.EMPTY));
291 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
292 "state", null, State.CREATE));
294 }
else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
295 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
296 "state", null, State.SELECTED));
300 scrollPane =
new ScrollPane(masterGroup);
301 scrollPane.getStyleClass().add(
"bg");
302 scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
303 scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
305 Scene scene =
new Scene(scrollPane);
306 scene.getStylesheets().add(MediaViewImagePanel.class.getResource(
"MediaViewImagePanel.css").toExternalForm());
307 fxPanel.setScene(scene);
309 fxImageView.setSmooth(
true);
310 fxImageView.setCache(
true);
312 EventQueue.invokeLater(() -> {
325 private void subscribeTagMenuItemsToStateChanges() {
326 pcs.addPropertyChangeListener((event) -> {
327 State currentState = (State) event.getNewValue();
328 switch (currentState) {
330 SwingUtilities.invokeLater(() -> {
331 createTagMenuItem.setEnabled(
true);
332 deleteTagMenuItem.setEnabled(
false);
333 hideTagsMenuItem.setEnabled(
true);
334 exportTagsMenuItem.setEnabled(
true);
338 Platform.runLater(() -> {
339 if (masterGroup.getChildren().contains(imageTagCreator)) {
340 imageTagCreator.disconnect();
341 masterGroup.getChildren().remove(imageTagCreator);
343 SwingUtilities.invokeLater(() -> {
344 createTagMenuItem.setEnabled(
false);
345 deleteTagMenuItem.setEnabled(
true);
346 hideTagsMenuItem.setEnabled(
true);
347 exportTagsMenuItem.setEnabled(
true);
352 SwingUtilities.invokeLater(() -> {
353 createTagMenuItem.setEnabled(
false);
354 deleteTagMenuItem.setEnabled(
false);
355 hideTagsMenuItem.setEnabled(
true);
356 hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
357 exportTagsMenuItem.setEnabled(
false);
361 SwingUtilities.invokeLater(() -> {
362 createTagMenuItem.setEnabled(
true);
363 deleteTagMenuItem.setEnabled(
false);
364 hideTagsMenuItem.setEnabled(
true);
365 hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
366 exportTagsMenuItem.setEnabled(
true);
371 Platform.runLater(() -> {
372 if (masterGroup.getChildren().contains(imageTagCreator)) {
373 imageTagCreator.disconnect();
375 SwingUtilities.invokeLater(() -> {
376 createTagMenuItem.setEnabled(
true);
377 deleteTagMenuItem.setEnabled(
false);
378 hideTagsMenuItem.setEnabled(
false);
379 hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
380 exportTagsMenuItem.setEnabled(
false);
385 SwingUtilities.invokeLater(() -> {
386 createTagMenuItem.setEnabled(
true);
387 deleteTagMenuItem.setEnabled(
false);
388 hideTagsMenuItem.setEnabled(
true);
389 exportTagsMenuItem.setEnabled(
true);
393 SwingUtilities.invokeLater(() -> {
394 createTagMenuItem.setEnabled(
false);
395 deleteTagMenuItem.setEnabled(
false);
396 hideTagsMenuItem.setEnabled(
false);
397 exportTagsMenuItem.setEnabled(
false);
410 final boolean isInited() {
418 Platform.runLater(() -> {
419 fxImageView.setViewport(
new Rectangle2D(0, 0, 0, 0));
420 fxImageView.setImage(null);
421 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
422 "state", null, State.DEFAULT));
423 masterGroup.getChildren().clear();
424 scrollPane.setContent(null);
425 scrollPane.setContent(masterGroup);
436 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
437 private
void showErrorButton(String errorMessage, AbstractFile file) {
439 final Button externalViewerButton =
new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(),
new ImageView(openInExternalViewerButtonImage));
440 externalViewerButton.setOnAction(actionEvent
441 ->
new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(),
new FileNode(file))
442 .actionPerformed(
new ActionEvent(
this, ActionEvent.ACTION_PERFORMED,
""))
444 final VBox errorNode =
new VBox(10,
new Label(errorMessage), externalViewerButton);
445 errorNode.setAlignment(Pos.CENTER);
453 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
454 final
void loadFile(final AbstractFile file) {
455 ensureInSwingThread();
460 final double panelWidth = fxPanel.getWidth();
461 final double panelHeight = fxPanel.getHeight();
462 Platform.runLater(() -> {
467 if (readImageFileTask != null) {
468 readImageFileTask.cancel();
470 readImageFileTask = ImageUtils.newReadImageTask(file);
471 readImageFileTask.setOnSucceeded(succeeded -> {
472 onReadImageTaskSucceeded(file, panelWidth, panelHeight);
474 readImageFileTask.setOnFailed(failed -> {
475 onReadImageTaskFailed(file);
482 maskerPane.setProgressNode(progressBar);
483 progressBar.progressProperty().bind(readImageFileTask.progressProperty());
484 maskerPane.textProperty().bind(readImageFileTask.messageProperty());
485 scrollPane.setContent(null);
486 scrollPane.setCursor(Cursor.WAIT);
487 new Thread(readImageFileTask).start();
501 private void onReadImageTaskSucceeded(AbstractFile file,
double panelWidth,
double panelHeight) {
502 if (!Case.isCaseOpen()) {
513 Platform.runLater(() -> {
515 Image fxImage = readImageFileTask.get();
516 masterGroup.getChildren().clear();
517 tagsGroup.getChildren().clear();
518 this.imageFile = file;
519 if (nonNull(fxImage)) {
521 fxImageView.setImage(fxImage);
522 if (panelWidth != 0 && panelHeight != 0) {
523 resetView(panelWidth, panelHeight);
525 masterGroup.getChildren().add(fxImageView);
526 masterGroup.getChildren().add(tagsGroup);
529 List<ContentTag> tags = Case.getCurrentCase().getServices()
530 .getTagsManager().getContentTagsByContent(file);
532 List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
534 tagsGroup = buildImageTagsGroup(contentViewerTags);
535 if (!tagsGroup.getChildren().isEmpty()) {
536 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
537 "state", null, State.NONEMPTY));
539 }
catch (TskCoreException | NoCurrentCaseException ex) {
540 logger.log(Level.WARNING,
"Could not retrieve image tags for file in case db", ex);
542 scrollPane.setContent(masterGroup);
544 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
546 }
catch (InterruptedException | ExecutionException ex) {
547 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
549 scrollPane.setCursor(Cursor.DEFAULT);
560 private void onReadImageTaskFailed(AbstractFile file) {
561 if (!Case.isCaseOpen()) {
572 Platform.runLater(() -> {
573 Throwable exception = readImageFileTask.getException();
574 if (exception instanceof OutOfMemoryError
575 && exception.getMessage().contains(
"Java heap space")) {
576 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
578 showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file);
581 scrollPane.setCursor(Cursor.DEFAULT);
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) {
606 contentViewerTags.add(contentViewerTag);
608 return contentViewerTags;
622 private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
624 contentViewerTags.forEach(contentViewerTag -> {
629 tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
640 final public List<String> getSupportedMimeTypes() {
641 return Collections.unmodifiableList(Lists.newArrayList(ImageUtils.getSupportedImageMimeTypes()));
650 final public List<String> getSupportedExtensions() {
651 return ImageUtils.getSupportedImageExtensions().stream()
653 .collect(Collectors.toList());
657 final public boolean isSupported(AbstractFile file) {
658 return ImageUtils.isImageThumbnailSupported(file);
666 @SuppressWarnings(
"unchecked")
668 private
void initComponents() {
670 toolbar =
new javax.swing.JToolBar();
671 rotationTextField =
new javax.swing.JTextField();
672 rotateLeftButton =
new javax.swing.JButton();
673 rotateRightButton =
new javax.swing.JButton();
674 jSeparator1 =
new javax.swing.JToolBar.Separator();
675 zoomTextField =
new javax.swing.JTextField();
676 zoomOutButton =
new javax.swing.JButton();
677 zoomInButton =
new javax.swing.JButton();
678 jSeparator2 =
new javax.swing.JToolBar.Separator();
679 zoomResetButton =
new javax.swing.JButton();
680 filler1 =
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0));
681 filler2 =
new javax.swing.Box.Filler(
new java.awt.Dimension(0, 0),
new java.awt.Dimension(0, 0),
new java.awt.Dimension(32767, 0));
682 jPanel1 =
new javax.swing.JPanel();
683 tagsMenu =
new javax.swing.JButton();
685 setBackground(
new java.awt.Color(0, 0, 0));
686 addComponentListener(
new java.awt.event.ComponentAdapter() {
687 public void componentResized(java.awt.event.ComponentEvent evt) {
688 formComponentResized(evt);
691 setLayout(
new javax.swing.BoxLayout(
this, javax.swing.BoxLayout.Y_AXIS));
693 toolbar.setFloatable(
false);
694 toolbar.setRollover(
true);
695 toolbar.setMaximumSize(
new java.awt.Dimension(32767, 23));
698 rotationTextField.setEditable(
false);
699 rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
700 rotationTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotationTextField.text"));
701 rotationTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
702 rotationTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
703 rotationTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
704 toolbar.add(rotationTextField);
706 rotateLeftButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png")));
707 org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.text"));
708 rotateLeftButton.setToolTipText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.toolTipText"));
709 rotateLeftButton.setFocusable(
false);
710 rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
711 rotateLeftButton.setMaximumSize(
new java.awt.Dimension(24, 24));
712 rotateLeftButton.setMinimumSize(
new java.awt.Dimension(24, 24));
713 rotateLeftButton.setPreferredSize(
new java.awt.Dimension(24, 24));
714 rotateLeftButton.addActionListener(
new java.awt.event.ActionListener() {
715 public void actionPerformed(java.awt.event.ActionEvent evt) {
716 rotateLeftButtonActionPerformed(evt);
719 toolbar.add(rotateLeftButton);
721 rotateRightButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png")));
722 org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateRightButton.text"));
723 rotateRightButton.setFocusable(
false);
724 rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
725 rotateRightButton.setMaximumSize(
new java.awt.Dimension(24, 24));
726 rotateRightButton.setMinimumSize(
new java.awt.Dimension(24, 24));
727 rotateRightButton.setPreferredSize(
new java.awt.Dimension(24, 24));
728 rotateRightButton.addActionListener(
new java.awt.event.ActionListener() {
729 public void actionPerformed(java.awt.event.ActionEvent evt) {
730 rotateRightButtonActionPerformed(evt);
733 toolbar.add(rotateRightButton);
735 jSeparator1.setMaximumSize(
new java.awt.Dimension(6, 20));
736 toolbar.add(jSeparator1);
738 zoomTextField.setEditable(
false);
739 zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
740 zoomTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomTextField.text"));
741 zoomTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
742 zoomTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
743 zoomTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
744 toolbar.add(zoomTextField);
746 zoomOutButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png")));
747 org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomOutButton.text"));
748 zoomOutButton.setFocusable(
false);
749 zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
750 zoomOutButton.setMaximumSize(
new java.awt.Dimension(24, 24));
751 zoomOutButton.setMinimumSize(
new java.awt.Dimension(24, 24));
752 zoomOutButton.setPreferredSize(
new java.awt.Dimension(24, 24));
753 zoomOutButton.addActionListener(
new java.awt.event.ActionListener() {
754 public void actionPerformed(java.awt.event.ActionEvent evt) {
755 zoomOutButtonActionPerformed(evt);
758 toolbar.add(zoomOutButton);
760 zoomInButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png")));
761 org.openide.awt.Mnemonics.setLocalizedText(zoomInButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomInButton.text"));
762 zoomInButton.setFocusable(
false);
763 zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
764 zoomInButton.setMaximumSize(
new java.awt.Dimension(24, 24));
765 zoomInButton.setMinimumSize(
new java.awt.Dimension(24, 24));
766 zoomInButton.setPreferredSize(
new java.awt.Dimension(24, 24));
767 zoomInButton.addActionListener(
new java.awt.event.ActionListener() {
768 public void actionPerformed(java.awt.event.ActionEvent evt) {
769 zoomInButtonActionPerformed(evt);
772 toolbar.add(zoomInButton);
774 jSeparator2.setMaximumSize(
new java.awt.Dimension(6, 20));
775 toolbar.add(jSeparator2);
777 org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomResetButton.text"));
778 zoomResetButton.setFocusable(
false);
779 zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
780 zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
781 zoomResetButton.addActionListener(
new java.awt.event.ActionListener() {
782 public void actionPerformed(java.awt.event.ActionEvent evt) {
783 zoomResetButtonActionPerformed(evt);
786 toolbar.add(zoomResetButton);
787 toolbar.add(filler1);
788 toolbar.add(filler2);
789 toolbar.add(jPanel1);
791 org.openide.awt.Mnemonics.setLocalizedText(tagsMenu,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.tagsMenu.text_1"));
792 tagsMenu.setFocusable(
false);
793 tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
794 tagsMenu.setMaximumSize(
new java.awt.Dimension(75, 21));
795 tagsMenu.setMinimumSize(
new java.awt.Dimension(75, 21));
796 tagsMenu.setPreferredSize(
new java.awt.Dimension(75, 21));
797 tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
798 tagsMenu.addMouseListener(
new java.awt.event.MouseAdapter() {
799 public void mousePressed(java.awt.event.MouseEvent evt) {
800 tagsMenuMousePressed(evt);
803 toolbar.add(tagsMenu);
808 private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {
812 private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {
816 private void rotateImage(
int angle) {
817 final double panelWidth = fxPanel.getWidth();
818 final double panelHeight = fxPanel.getHeight();
819 ImageTransforms currentTransforms = imageTransforms;
820 double newRotation = (currentTransforms.getRotation() + angle) % 360;
821 final ImageTransforms newTransforms =
new ImageTransforms(currentTransforms.getZoomRatio(), newRotation,
false);
822 imageTransforms = newTransforms;
823 Platform.runLater(() -> {
824 updateView(panelWidth, panelHeight, newTransforms);
828 private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {
829 zoomImage(ZoomDirection.IN);
832 private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {
833 zoomImage(ZoomDirection.OUT);
836 private void zoomImage(ZoomDirection direction) {
837 ensureInSwingThread();
838 final double panelWidth = fxPanel.getWidth();
839 final double panelHeight = fxPanel.getHeight();
840 final ImageTransforms currentTransforms = imageTransforms;
842 if (direction == ZoomDirection.IN) {
843 newZoomRatio = zoomImageIn(currentTransforms.getZoomRatio());
845 newZoomRatio = zoomImageOut(currentTransforms.getZoomRatio());
847 final ImageTransforms newTransforms =
new ImageTransforms(newZoomRatio, currentTransforms.getRotation(),
false);
848 imageTransforms = newTransforms;
849 Platform.runLater(() -> {
850 updateView(panelWidth, panelHeight, newTransforms);
854 private double zoomImageIn(
double zoomRatio) {
855 double newZoomRatio = zoomRatio;
856 for (
int i = 0; i < ZOOM_STEPS.length; i++) {
857 if (newZoomRatio < ZOOM_STEPS[i]) {
858 newZoomRatio = ZOOM_STEPS[i];
865 private double zoomImageOut(
double zoomRatio) {
866 double newZoomRatio = zoomRatio;
867 for (
int i = ZOOM_STEPS.length - 1; i >= 0; i--) {
868 if (newZoomRatio > ZOOM_STEPS[i]) {
869 newZoomRatio = ZOOM_STEPS[i];
876 private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {
877 final ImageTransforms currentTransforms = imageTransforms;
878 final ImageTransforms newTransforms =
new ImageTransforms(0, currentTransforms.getRotation(),
true);
879 imageTransforms = newTransforms;
883 private void formComponentResized(java.awt.event.ComponentEvent evt) {
884 final ImageTransforms currentTransforms = imageTransforms;
885 if (currentTransforms.shouldAutoResize()) {
888 final double panelWidth = fxPanel.getWidth();
889 final double panelHeight = fxPanel.getHeight();
890 Platform.runLater(() -> {
891 updateView(panelWidth, panelHeight, currentTransforms);
900 private void deleteTag() {
901 Platform.runLater(() -> {
902 ImageTag tagInFocus = tagsGroup.getFocus();
903 if (tagInFocus == null) {
908 ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
909 scrollPane.setCursor(Cursor.WAIT);
910 ContentViewerTagManager.deleteTag(contentViewerTag);
911 Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
912 tagsGroup.getChildren().remove(tagInFocus);
913 }
catch (TskCoreException | NoCurrentCaseException ex) {
914 logger.log(Level.WARNING,
"Could not delete image tag in case db", ex);
917 scrollPane.setCursor(Cursor.DEFAULT);
920 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
921 "state", null, State.CREATE));
928 private void createTag() {
929 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
930 "state", null, State.DISABLE));
931 Platform.runLater(() -> {
932 imageTagCreator =
new ImageTagCreator(fxImageView);
934 PropertyChangeListener newTagListener = (event) -> {
935 SwingUtilities.invokeLater(() -> {
936 ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
938 TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
939 if (result != null) {
941 Platform.runLater(() -> {
943 scrollPane.setCursor(Cursor.WAIT);
944 ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
945 ImageTag imageTag = buildImageTag(contentViewerTag);
946 tagsGroup.getChildren().add(imageTag);
947 }
catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
948 logger.log(Level.WARNING,
"Could not save new image tag in case db", ex);
951 scrollPane.setCursor(Cursor.DEFAULT);
955 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
956 "state", null, State.CREATE));
960 Platform.runLater(() -> {
961 imageTagCreator.disconnect();
962 masterGroup.getChildren().remove(imageTagCreator);
966 imageTagCreator.addNewTagListener(newTagListener);
967 masterGroup.getChildren().add(imageTagCreator);
978 private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
980 ImageTag imageTag =
new ImageTag(contentViewerTag, fxImageView);
983 imageTag.subscribeToEditEvents((edit) -> {
985 scrollPane.setCursor(Cursor.WAIT);
986 ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
987 ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
988 }
catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
989 logger.log(Level.WARNING,
"Could not save edit for image tag in case db", ex);
991 scrollPane.setCursor(Cursor.DEFAULT);
1003 private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result)
throws TskCoreException, SerializationException, NoCurrentCaseException {
1004 ensureInJfxThread();
1005 scrollPane.setCursor(Cursor.WAIT);
1007 ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
1008 .addContentTag(imageFile, result.getTagName(), result.getComment());
1009 return ContentViewerTagManager.saveTag(contentTag, data);
1011 scrollPane.setCursor(Cursor.DEFAULT);
1019 private void showOrHideTags() {
1020 Platform.runLater(() -> {
1021 if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
1023 masterGroup.getChildren().remove(tagsGroup);
1024 hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
1025 tagsGroup.clearFocus();
1026 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
1027 "state", null, State.HIDDEN));
1030 masterGroup.getChildren().add(tagsGroup);
1031 hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
1032 pcs.firePropertyChange(
new PropertyChangeEvent(
this,
1033 "state", null, State.VISIBLE));
1038 @NbBundle.Messages({
1039 "MediaViewImagePanel.exportSaveText=Save",
1040 "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
1041 "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
1042 "MediaViewImagePanel.fileChooserTitle=Choose a save location"
1044 private void exportTags() {
1045 Platform.runLater(() -> {
1046 final AbstractFile file = imageFile;
1047 tagsGroup.clearFocus();
1048 SwingUtilities.invokeLater(() -> {
1050 if(exportChooser == null) {
1052 exportChooser = futureFileChooser.get();
1053 }
catch (InterruptedException | ExecutionException ex) {
1056 logger.log(Level.WARNING,
"A failure occurred in the JFileChooser background thread");
1057 exportChooser =
new JFileChooser();
1061 exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1063 exportChooser.setCurrentDirectory(
new File(Case.getCurrentCase().getExportDirectory()));
1064 int returnVal = exportChooser.showDialog(
this, Bundle.MediaViewImagePanel_exportSaveText());
1065 if (returnVal == JFileChooser.APPROVE_OPTION) {
1066 new SwingWorker<Void, Void>() {
1068 protected Void doInBackground() {
1071 List<ContentTag> tags = Case.getCurrentCase().getServices()
1072 .getTagsManager().getContentTagsByContent(file);
1073 List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
1076 Collection<ImageTagRegion> regions = contentViewerTags.stream()
1077 .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
1080 BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions);
1081 Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
1082 FilenameUtils.getBaseName(file.getName()) +
"-with_tags.png");
1083 ImageIO.write(taggedImage,
"png", output.toFile());
1085 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
1086 }
catch (Exception ex) {
1088 logger.log(Level.WARNING,
"Unable to export tagged image to disk", ex);
1089 JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
1099 private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {
1100 if (imageTaggingOptions.isEnabled()) {
1101 imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
1139 private javax.swing.Box.Filler filler1;
1140 private javax.swing.Box.Filler filler2;
1141 private javax.swing.JPanel jPanel1;
1142 private javax.swing.JToolBar.Separator jSeparator1;
1143 private javax.swing.JToolBar.Separator jSeparator2;
1144 private javax.swing.JButton rotateLeftButton;
1145 private javax.swing.JButton rotateRightButton;
1146 private javax.swing.JTextField rotationTextField;
1147 private javax.swing.JButton tagsMenu;
1148 private javax.swing.JToolBar toolbar;
1149 private javax.swing.JButton zoomInButton;
1150 private javax.swing.JButton zoomOutButton;
1151 private javax.swing.JButton zoomResetButton;
1152 private javax.swing.JTextField zoomTextField;
1159 private void resetView() {
1160 ensureInSwingThread();
1161 final double panelWidth = fxPanel.getWidth();
1162 final double panelHeight = fxPanel.getHeight();
1163 Platform.runLater(() -> {
1164 resetView(panelWidth, panelHeight);
1178 private void resetView(
double panelWidth,
double panelHeight) {
1179 ensureInJfxThread();
1181 Image image = fxImageView.getImage();
1182 if (image == null) {
1186 double imageWidth = image.getWidth();
1187 double imageHeight = image.getHeight();
1188 double scrollPaneWidth = panelWidth;
1189 double scrollPaneHeight = panelHeight;
1190 double zoomRatioWidth = scrollPaneWidth / imageWidth;
1191 double zoomRatioHeight = scrollPaneHeight / imageHeight;
1192 double newZoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
1193 final ImageTransforms newTransforms =
new ImageTransforms(newZoomRatio, 0,
true);
1194 imageTransforms = newTransforms;
1196 scrollPane.setHvalue(0);
1197 scrollPane.setVvalue(0);
1199 updateView(panelWidth, panelHeight, newTransforms);
1224 private void updateView(
double panelWidth,
double panelHeight, ImageTransforms imageTransforms) {
1225 ensureInJfxThread();
1226 Image image = fxImageView.getImage();
1227 if (image == null) {
1232 double imageWidth = image.getWidth();
1233 double imageHeight = image.getHeight();
1236 double currentZoomRatio = imageTransforms.getZoomRatio();
1237 double adjustedImageWidth = imageWidth * currentZoomRatio;
1238 double adjustedImageHeight = imageHeight * currentZoomRatio;
1241 double viewportWidth;
1242 double viewportHeight;
1245 double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
1246 double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
1253 double scrollX = scrollPane.getHvalue();
1254 double scrollY = scrollPane.getVvalue();
1261 final double currentRotation = imageTransforms.getRotation();
1262 if ((currentRotation % 180) == 0) {
1264 viewportWidth = adjustedImageWidth;
1265 viewportHeight = adjustedImageHeight;
1266 leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
1267 topOffsetY = (adjustedImageHeight - imageHeight) / 2;
1268 maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
1269 maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
1272 viewportWidth = adjustedImageHeight;
1273 viewportHeight = adjustedImageWidth;
1274 leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
1275 topOffsetY = (adjustedImageWidth - imageHeight) / 2;
1276 maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
1277 maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
1281 if (viewportWidth < imageWidth) {
1282 viewportWidth = imageWidth;
1283 if (scrollX > maxScrollX) {
1284 scrollX = maxScrollX;
1287 if (viewportHeight < imageHeight) {
1288 viewportHeight = imageHeight;
1289 if (scrollY > maxScrollY) {
1290 scrollY = maxScrollY;
1295 fxImageView.setViewport(
new Rectangle2D(
1296 0, 0, viewportWidth, viewportHeight));
1299 Scale scale =
new Scale();
1300 scale.setX(currentZoomRatio);
1301 scale.setY(currentZoomRatio);
1302 scale.setPivotX(imageWidth / 2);
1303 scale.setPivotY(imageHeight / 2);
1306 Rotate rotate =
new Rotate();
1307 rotate.setPivotX(imageWidth / 2);
1308 rotate.setPivotY(imageHeight / 2);
1309 rotate.setAngle(currentRotation);
1312 Translate translate =
new Translate();
1313 translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
1314 translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
1319 masterGroup.getTransforms().clear();
1320 masterGroup.getTransforms().addAll(translate, rotate, scale);
1323 if (viewportWidth > fxPanel.getWidth()) {
1324 scrollPane.setHvalue(scrollX);
1326 if (viewportHeight > fxPanel.getHeight()) {
1327 scrollPane.setVvalue(scrollY);
1336 SwingUtilities.invokeLater(() -> {
1338 zoomOutButton.setEnabled(currentZoomRatio > MIN_ZOOM_RATIO);
1339 zoomInButton.setEnabled(currentZoomRatio < MAX_ZOOM_RATIO);
1340 rotationTextField.setText((
int) currentRotation +
"°");
1341 zoomTextField.setText((Math.round(currentZoomRatio * 100.0)) +
"%");
1350 private void ensureInJfxThread() {
1351 if (!Platform.isFxApplicationThread()) {
1352 throw new IllegalStateException(
"Attempt to execute JFX code outside of JFX thread");
1361 private void ensureInSwingThread() {
1362 if (!SwingUtilities.isEventDispatchThread()) {
1363 throw new IllegalStateException(
"Attempt to execute Swing code outside of EDT");
1384 ImageTransforms(
double zoomRatio,
double rotation,
boolean autoResize) {
static boolean isJavaFxInited()
DisplayOptions(String name)
boolean shouldAutoResize()