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

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.