19 package org.sleuthkit.autopsy.contentviewers;
21 import java.awt.Dimension;
22 import java.awt.EventQueue;
23 import java.awt.event.ActionEvent;
24 import java.util.Collections;
25 import java.util.List;
26 import static java.util.Objects.nonNull;
27 import java.util.SortedSet;
28 import java.util.concurrent.ExecutionException;
29 import java.util.stream.Collectors;
30 import javafx.application.Platform;
31 import javafx.concurrent.Task;
32 import javafx.embed.swing.JFXPanel;
33 import javafx.geometry.Pos;
34 import javafx.geometry.Rectangle2D;
35 import javafx.scene.Cursor;
36 import javafx.scene.Scene;
37 import javafx.scene.control.Button;
38 import javafx.scene.control.Label;
39 import javafx.scene.control.ProgressBar;
40 import javafx.scene.control.ScrollPane;
41 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
42 import javafx.scene.image.Image;
43 import javafx.scene.image.ImageView;
44 import javafx.scene.layout.VBox;
45 import javafx.scene.transform.Rotate;
46 import javafx.scene.transform.Scale;
47 import javafx.scene.transform.Translate;
48 import javax.imageio.ImageIO;
49 import javax.swing.JPanel;
50 import org.controlsfx.control.MaskerPane;
51 import org.openide.util.NbBundle;
52 import org.python.google.common.collect.Lists;
63 @NbBundle.Messages({
"MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E",
64 "MediaViewImagePanel.errorLabel.text=Could not load file into Media View.",
65 "MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory."})
66 @SuppressWarnings(
"PMD.SingularField")
67 class MediaViewImagePanel
extends JPanel implements MediaFileViewer.MediaViewPanel {
69 private static final Image EXTERNAL =
new Image(MediaViewImagePanel.class.getResource(
"/org/sleuthkit/autopsy/images/external.png").toExternalForm());
71 private final boolean fxInited;
73 private JFXPanel fxPanel;
74 private ImageView fxImageView;
75 private ScrollPane scrollPane;
76 private final ProgressBar progressBar =
new ProgressBar();
77 private final MaskerPane maskerPane =
new MaskerPane();
79 private double zoomRatio;
80 private double rotation;
82 private static final double[] ZOOM_STEPS = {
83 0.0625, 0.125, 0.25, 0.375, 0.5, 0.75,
84 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10};
86 private static final double MIN_ZOOM_RATIO = 0.0625;
87 private static final double MAX_ZOOM_RATIO = 10.0;
90 ImageIO.scanForPlugins();
97 static private final SortedSet<String> supportedMimes = ImageUtils.getSupportedImageMimeTypes();
102 static private final List<String> supportedExtensions = ImageUtils.getSupportedImageExtensions().stream()
104 .collect(Collectors.toList());
106 private Task<Image> readImageTask;
111 public MediaViewImagePanel() {
115 Platform.runLater(() -> {
118 fxImageView =
new ImageView();
119 scrollPane =
new ScrollPane(fxImageView);
120 scrollPane.getStyleClass().add(
"bg");
121 scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
122 scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
124 fxPanel =
new JFXPanel();
125 Scene scene =
new Scene(scrollPane);
126 scene.getStylesheets().add(MediaViewImagePanel.class.getResource(
"MediaViewImagePanel.css").toExternalForm());
127 fxPanel.setScene(scene);
129 fxImageView.setSmooth(
true);
130 fxImageView.setCache(
true);
132 EventQueue.invokeLater(() -> {
139 public boolean isInited() {
146 public void reset() {
147 Platform.runLater(() -> {
148 fxImageView.setViewport(
new Rectangle2D(0, 0, 0, 0));
149 fxImageView.setImage(null);
151 scrollPane.setContent(null);
152 scrollPane.setContent(fxImageView);
156 private void showErrorNode(String errorMessage, AbstractFile file) {
157 final Button externalViewerButton =
new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(),
new ImageView(EXTERNAL));
158 externalViewerButton.setOnAction(actionEvent
163 new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(),
new FileNode(file))
164 .actionPerformed(
new ActionEvent(
this, ActionEvent.ACTION_PERFORMED,
""))
167 final VBox errorNode =
new VBox(10,
new Label(errorMessage), externalViewerButton);
168 errorNode.setAlignment(Pos.CENTER);
177 void showImageFx(
final AbstractFile file,
final Dimension dims) {
182 Platform.runLater(() -> {
183 if (readImageTask != null) {
184 readImageTask.cancel();
186 readImageTask = ImageUtils.newReadImageTask(file);
187 readImageTask.setOnSucceeded(succeeded -> {
188 if (!Case.isCaseOpen()) {
200 Image fxImage = readImageTask.get();
201 if (nonNull(fxImage)) {
203 fxImageView.setImage(fxImage);
205 scrollPane.setContent(fxImageView);
207 showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
209 }
catch (InterruptedException | ExecutionException ex) {
210 showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
212 scrollPane.setCursor(Cursor.DEFAULT);
214 readImageTask.setOnFailed(failed -> {
215 if (!Case.isCaseOpen()) {
225 Throwable exception = readImageTask.getException();
226 if (exception instanceof OutOfMemoryError
227 && exception.getMessage().contains(
"Java heap space")) {
228 showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file);
230 showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
233 scrollPane.setCursor(Cursor.DEFAULT);
236 maskerPane.setProgressNode(progressBar);
237 progressBar.progressProperty().bind(readImageTask.progressProperty());
238 maskerPane.textProperty().bind(readImageTask.messageProperty());
239 scrollPane.setContent(null);
240 scrollPane.setCursor(Cursor.WAIT);
241 new Thread(readImageTask).start();
249 public List<String> getSupportedMimeTypes() {
250 return Collections.unmodifiableList(Lists.newArrayList(supportedMimes));
259 public List<String> getSupportedExtensions() {
260 return getExtensions();
268 public List<String> getExtensions() {
269 return Collections.unmodifiableList(supportedExtensions);
273 public boolean isSupported(AbstractFile file) {
274 return ImageUtils.isImageThumbnailSupported(file);
282 @SuppressWarnings(
"unchecked")
284 private
void initComponents() {
286 toolbar =
new javax.swing.JToolBar();
287 rotationTextField =
new javax.swing.JTextField();
288 rotateLeftButton =
new javax.swing.JButton();
289 rotateRightButton =
new javax.swing.JButton();
290 jSeparator1 =
new javax.swing.JToolBar.Separator();
291 zoomTextField =
new javax.swing.JTextField();
292 zoomOutButton =
new javax.swing.JButton();
293 zoomInButton =
new javax.swing.JButton();
294 jSeparator2 =
new javax.swing.JToolBar.Separator();
295 zoomResetButton =
new javax.swing.JButton();
297 setBackground(
new java.awt.Color(0, 0, 0));
298 addComponentListener(
new java.awt.event.ComponentAdapter() {
299 public void componentResized(java.awt.event.ComponentEvent evt) {
300 formComponentResized(evt);
303 setLayout(
new javax.swing.BoxLayout(
this, javax.swing.BoxLayout.Y_AXIS));
305 toolbar.setFloatable(
false);
306 toolbar.setRollover(
true);
307 toolbar.setMaximumSize(
new java.awt.Dimension(32767, 23));
310 rotationTextField.setEditable(
false);
311 rotationTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
312 rotationTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotationTextField.text"));
313 rotationTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
314 rotationTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
315 rotationTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
316 toolbar.add(rotationTextField);
318 rotateLeftButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-left.png")));
319 org.openide.awt.Mnemonics.setLocalizedText(rotateLeftButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.text"));
320 rotateLeftButton.setToolTipText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateLeftButton.toolTipText"));
321 rotateLeftButton.setFocusable(
false);
322 rotateLeftButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
323 rotateLeftButton.setMaximumSize(
new java.awt.Dimension(24, 24));
324 rotateLeftButton.setMinimumSize(
new java.awt.Dimension(24, 24));
325 rotateLeftButton.setPreferredSize(
new java.awt.Dimension(24, 24));
326 rotateLeftButton.addActionListener(
new java.awt.event.ActionListener() {
327 public void actionPerformed(java.awt.event.ActionEvent evt) {
328 rotateLeftButtonActionPerformed(evt);
331 toolbar.add(rotateLeftButton);
333 rotateRightButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/rotate-right.png")));
334 org.openide.awt.Mnemonics.setLocalizedText(rotateRightButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.rotateRightButton.text"));
335 rotateRightButton.setFocusable(
false);
336 rotateRightButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
337 rotateRightButton.setMaximumSize(
new java.awt.Dimension(24, 24));
338 rotateRightButton.setMinimumSize(
new java.awt.Dimension(24, 24));
339 rotateRightButton.setPreferredSize(
new java.awt.Dimension(24, 24));
340 rotateRightButton.addActionListener(
new java.awt.event.ActionListener() {
341 public void actionPerformed(java.awt.event.ActionEvent evt) {
342 rotateRightButtonActionPerformed(evt);
345 toolbar.add(rotateRightButton);
347 jSeparator1.setMaximumSize(
new java.awt.Dimension(6, 20));
348 toolbar.add(jSeparator1);
350 zoomTextField.setEditable(
false);
351 zoomTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
352 zoomTextField.setText(
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomTextField.text"));
353 zoomTextField.setMaximumSize(
new java.awt.Dimension(50, 2147483647));
354 zoomTextField.setMinimumSize(
new java.awt.Dimension(50, 20));
355 zoomTextField.setPreferredSize(
new java.awt.Dimension(50, 20));
356 toolbar.add(zoomTextField);
358 zoomOutButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-out.png")));
359 org.openide.awt.Mnemonics.setLocalizedText(zoomOutButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomOutButton.text"));
360 zoomOutButton.setFocusable(
false);
361 zoomOutButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
362 zoomOutButton.setMaximumSize(
new java.awt.Dimension(24, 24));
363 zoomOutButton.setMinimumSize(
new java.awt.Dimension(24, 24));
364 zoomOutButton.setPreferredSize(
new java.awt.Dimension(24, 24));
365 zoomOutButton.addActionListener(
new java.awt.event.ActionListener() {
366 public void actionPerformed(java.awt.event.ActionEvent evt) {
367 zoomOutButtonActionPerformed(evt);
370 toolbar.add(zoomOutButton);
372 zoomInButton.setIcon(
new javax.swing.ImageIcon(getClass().getResource(
"/org/sleuthkit/autopsy/contentviewers/images/zoom-in.png")));
373 org.openide.awt.Mnemonics.setLocalizedText(zoomInButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomInButton.text"));
374 zoomInButton.setFocusable(
false);
375 zoomInButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
376 zoomInButton.setMaximumSize(
new java.awt.Dimension(24, 24));
377 zoomInButton.setMinimumSize(
new java.awt.Dimension(24, 24));
378 zoomInButton.setPreferredSize(
new java.awt.Dimension(24, 24));
379 zoomInButton.addActionListener(
new java.awt.event.ActionListener() {
380 public void actionPerformed(java.awt.event.ActionEvent evt) {
381 zoomInButtonActionPerformed(evt);
384 toolbar.add(zoomInButton);
386 jSeparator2.setMaximumSize(
new java.awt.Dimension(6, 20));
387 toolbar.add(jSeparator2);
389 org.openide.awt.Mnemonics.setLocalizedText(zoomResetButton,
org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class,
"MediaViewImagePanel.zoomResetButton.text"));
390 zoomResetButton.setFocusable(
false);
391 zoomResetButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
392 zoomResetButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
393 zoomResetButton.addActionListener(
new java.awt.event.ActionListener() {
394 public void actionPerformed(java.awt.event.ActionEvent evt) {
395 zoomResetButtonActionPerformed(evt);
398 toolbar.add(zoomResetButton);
403 private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {
404 rotation = (rotation + 270) % 360;
408 private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {
409 rotation = (rotation + 90) % 360;
413 private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {
415 for (
int i=0; i < ZOOM_STEPS.length; i++) {
416 if (zoomRatio < ZOOM_STEPS[i]) {
417 zoomRatio = ZOOM_STEPS[i];
424 private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {
426 for (
int i=ZOOM_STEPS.length-1; i >= 0; i--) {
427 if (zoomRatio > ZOOM_STEPS[i]) {
428 zoomRatio = ZOOM_STEPS[i];
435 private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {
439 private void formComponentResized(java.awt.event.ComponentEvent evt) {
444 private javax.swing.JToolBar.Separator jSeparator1;
445 private javax.swing.JToolBar.Separator jSeparator2;
446 private javax.swing.JButton rotateLeftButton;
447 private javax.swing.JButton rotateRightButton;
448 private javax.swing.JTextField rotationTextField;
449 private javax.swing.JToolBar toolbar;
450 private javax.swing.JButton zoomInButton;
451 private javax.swing.JButton zoomOutButton;
452 private javax.swing.JButton zoomResetButton;
453 private javax.swing.JTextField zoomTextField;
464 private void resetView() {
465 Image image = fxImageView.getImage();
470 double imageWidth = image.getWidth();
471 double imageHeight = image.getHeight();
472 double scrollPaneWidth = fxPanel.getWidth();
473 double scrollPaneHeight = fxPanel.getHeight();
474 double zoomRatioWidth = scrollPaneWidth / imageWidth;
475 double zoomRatioHeight = scrollPaneHeight / imageHeight;
478 zoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight;
482 scrollPane.setHvalue(0);
483 scrollPane.setVvalue(0);
498 private void updateView() {
499 Image image = fxImageView.getImage();
505 double imageWidth = image.getWidth();
506 double imageHeight = image.getHeight();
509 double adjustedImageWidth = imageWidth * zoomRatio;
510 double adjustedImageHeight = imageHeight * zoomRatio;
513 double viewportWidth;
514 double viewportHeight;
517 double panelWidth = fxPanel.getWidth();
518 double panelHeight = fxPanel.getHeight();
521 double centerOffsetX = (panelWidth / 2) - (imageWidth / 2);
522 double centerOffsetY = (panelHeight / 2) - (imageHeight / 2);
529 double scrollX = scrollPane.getHvalue();
530 double scrollY = scrollPane.getVvalue();
537 if ((rotation % 180) == 0) {
539 viewportWidth = adjustedImageWidth;
540 viewportHeight = adjustedImageHeight;
541 leftOffsetX = (adjustedImageWidth - imageWidth) / 2;
542 topOffsetY = (adjustedImageHeight - imageHeight) / 2;
543 maxScrollX = (adjustedImageWidth - panelWidth) / (imageWidth - panelWidth);
544 maxScrollY = (adjustedImageHeight - panelHeight) / (imageHeight - panelHeight);
547 viewportWidth = adjustedImageHeight;
548 viewportHeight = adjustedImageWidth;
549 leftOffsetX = (adjustedImageHeight - imageWidth) / 2;
550 topOffsetY = (adjustedImageWidth - imageHeight) / 2;
551 maxScrollX = (adjustedImageHeight - panelWidth) / (imageWidth - panelWidth);
552 maxScrollY = (adjustedImageWidth - panelHeight) / (imageHeight - panelHeight);
556 if (viewportWidth < imageWidth) {
557 viewportWidth = imageWidth;
558 if (scrollX > maxScrollX) {
559 scrollX = maxScrollX;
562 if (viewportHeight < imageHeight) {
563 viewportHeight = imageHeight;
564 if (scrollY > maxScrollY) {
565 scrollY = maxScrollY;
570 fxImageView.setViewport(
new Rectangle2D(
571 0, 0, viewportWidth, viewportHeight));
574 Scale scale =
new Scale();
575 scale.setX(zoomRatio);
576 scale.setY(zoomRatio);
577 scale.setPivotX(imageWidth / 2);
578 scale.setPivotY(imageHeight / 2);
581 Rotate rotate =
new Rotate();
582 rotate.setPivotX(imageWidth / 2);
583 rotate.setPivotY(imageHeight / 2);
584 rotate.setAngle(rotation);
587 Translate translate =
new Translate();
588 translate.setX(viewportWidth > fxPanel.getWidth() ? leftOffsetX : centerOffsetX);
589 translate.setY(viewportHeight > fxPanel.getHeight() ? topOffsetY : centerOffsetY);
594 fxImageView.getTransforms().clear();
595 fxImageView.getTransforms().addAll(translate, rotate, scale);
598 if (viewportWidth > fxPanel.getWidth()) {
599 scrollPane.setHvalue(scrollX);
601 if (viewportHeight > fxPanel.getHeight()) {
602 scrollPane.setVvalue(scrollY);
606 zoomOutButton.setEnabled(zoomRatio > MIN_ZOOM_RATIO);
607 zoomInButton.setEnabled(zoomRatio < MAX_ZOOM_RATIO);
608 rotationTextField.setText((
int) rotation +
"°");
609 zoomTextField.setText((Math.round(zoomRatio * 100.0)) +
"%");
static boolean isJavaFxInited()