diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java index 0090cb47db..08fc169d49 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java @@ -206,7 +206,7 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo } } } - + return false; } @@ -218,8 +218,8 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo * @return True if an image file that can be displayed */ private boolean isImageSupported(AbstractFile file) { - return ImageUtils.thumbnailSupported(file); + return ImageUtils.thumbnailSupported(file); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css new file mode 100644 index 0000000000..8cd592b28b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.css @@ -0,0 +1,28 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.label { + -fx-wrap-text:true; + -fx-text-fill: red; + -fx-font-size: 2em; +} + +.bg { + -fx-background-color:black; +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index 792621e8e2..36e3bf9812 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -32,14 +32,11 @@ import java.util.stream.Collectors; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.embed.swing.SwingFXUtils; -import javafx.geometry.Insets; import javafx.scene.Scene; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.CornerRadii; import javax.imageio.ImageIO; import javax.swing.JPanel; import javax.swing.SwingUtilities; @@ -67,6 +64,10 @@ public class MediaViewImagePanel extends JPanel { private ImageView fxImageView; private BorderPane borderpane; + private final Label errorLabel = new Label("Could not load image file into media view."); + private final Label tooLargeLabel = new Label("Could not load image file into media view (too large)."); + private final Label noReaderLabel = new Label("Image reader not found for file."); + /** * mime types we shoul dbe able to display. if the mimetype is unknown we * will fall back on extension (and jpg/png header @@ -79,7 +80,6 @@ public class MediaViewImagePanel extends JPanel { static private final List supportedExtensions = ImageUtils.getSupportedExtensions().stream() .map("."::concat) .collect(Collectors.toList()); - /** * Creates new form MediaViewImagePanel @@ -88,12 +88,15 @@ public class MediaViewImagePanel extends JPanel { initComponents(); fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); if (fxInited) { - Platform.runLater(() -> { // build jfx ui (we could do this in FXML?) + Platform.runLater(() -> { + + // build jfx ui (we could do this in FXML?) fxImageView = new ImageView(); // will hold image borderpane = new BorderPane(fxImageView); // centers and sizes imageview - borderpane.setBackground(new Background(new BackgroundFill(javafx.scene.paint.Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY))); + borderpane.getStyleClass().add("bg"); fxPanel = new JFXPanel(); // bridge jfx-swing - Scene scene = new Scene(borderpane, javafx.scene.paint.Color.BLACK); //root of jfx tree + Scene scene = new Scene(borderpane); //root of jfx tree + scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); fxPanel.setScene(scene); //bind size of image to that of scene, while keeping proportions @@ -120,6 +123,7 @@ public class MediaViewImagePanel extends JPanel { public void reset() { Platform.runLater(() -> { fxImageView.setImage(null); + borderpane.setCenter(null); }); } @@ -144,41 +148,38 @@ public class MediaViewImagePanel extends JPanel { @Override public void run() { if (!Case.isCaseOpen()) { - //handle in-between condition when case is being closed - //and an image was previously selected + /* handle in-between condition when case is being closed + * and an image was previously selected */ return; } - - final Image fxImage; - try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { BufferedImage bufferedImage = ImageIO.read(inputStream); if (bufferedImage == null) { - LOGGER.log(Level.WARNING, "Could image reader not found for file: {0}", file.getName()); //NON-NLS - return; + LOGGER.log(Level.WARNING, "Image reader not found for file: {0}", file.getName()); //NON-NLS + borderpane.setCenter(noReaderLabel); + } else { + Image fxImage = SwingFXUtils.toFXImage(bufferedImage, null); + if (fxImage.isError()) { + LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), fxImage.getException()); //NON-NLS + borderpane.setCenter(errorLabel); + return; + } else { + fxImageView.setImage(fxImage); + borderpane.setCenter(fxImageView); + } } - fxImage = SwingFXUtils.toFXImage(bufferedImage, null); } catch (IllegalArgumentException | IOException ex) { LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), ex); //NON-NLS - - return; + borderpane.setCenter(errorLabel); } catch (OutOfMemoryError ex) { LOGGER.log(Level.WARNING, "Could not load image file into media view (too large): " + file.getName(), ex); //NON-NLS MessageNotifyUtil.Notify.warn( NbBundle.getMessage(this.getClass(), "MediaViewImagePanel.imgFileTooLarge.msg", file.getName()), ex.getMessage()); - return; + borderpane.setCenter(tooLargeLabel); } - if (fxImage.isError()) { - - LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), fxImage.getException()); //NON-NLS - return; - } - fxImageView.setImage(fxImage); - borderpane.setCenter(fxImageView); - SwingUtilities.invokeLater(() -> { //show the panel after fully loaded fxPanel.setVisible(true); diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 78aee5b3a9..00ff448a76 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -25,7 +25,7 @@ package org.sleuthkit.autopsy.coreutils; import com.google.common.io.Files; import java.awt.Image; import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; +import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -49,6 +49,7 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; +import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector.FileTypeDetectorInitException; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ReadContentInputStream; @@ -63,20 +64,16 @@ public class ImageUtils { private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); /** save thumbnails to disk as this format */ - private static final String FORMAT = "png"; + private static final String FORMAT = "png"; //NON-NLS public static final int ICON_SIZE_SMALL = 50; public static final int ICON_SIZE_MEDIUM = 100; public static final int ICON_SIZE_LARGE = 200; + private static final Image DEFAULT_ICON = new ImageIcon("/org/sleuthkit/autopsy/images/file-icon.png").getImage(); //NON-NLS - public static List getSupportedExtensions() { - return Collections.unmodifiableList(SUPP_EXTENSIONS); - } - - public static SortedSet getSupportedMimeTypes() { - return Collections.unmodifiableSortedSet(SUPP_MIME_TYPES); - } + //initialized lazily + private static FileTypeDetector fileTypeDetector; private static final List SUPP_EXTENSIONS; private static final TreeSet SUPP_MIME_TYPES; @@ -90,11 +87,20 @@ public class ImageUtils { SUPP_MIME_TYPES = new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes())); SUPP_MIME_TYPES.addAll(Arrays.asList("image/x-ms-bmp", "application/x-123")); + SUPP_MIME_TYPES.removeIf("application/octet-stream"::equals); } private ImageUtils() { } + public static List getSupportedExtensions() { + return Collections.unmodifiableList(SUPP_EXTENSIONS); + } + + public static SortedSet getSupportedMimeTypes() { + return Collections.unmodifiableSortedSet(SUPP_MIME_TYPES); + } + /** * Get the default Icon, which is the icon for a file. * @@ -122,12 +128,12 @@ public class ImageUtils { } try { - String mimeType = new FileTypeDetector().getFileType(file); + String mimeType = getFileTypeDetector().getFileType(file); if (Objects.nonNull(mimeType)) { - return SUPP_MIME_TYPES.contains(mimeType); + return SUPP_MIME_TYPES.contains(mimeType) + || (mimeType.equalsIgnoreCase("audio/x-aiff") && "iff".equalsIgnoreCase(file.getNameExtension())); } } catch (FileTypeDetector.FileTypeDetectorInitException | TskCoreException ex) { - LOGGER.log(Level.WARNING, "Failed to look up mimetype for " + file.getName() + " using FileTypeDetector. Fallingback on AbstractFile.isMimeType", ex); if (!SUPP_MIME_TYPES.isEmpty()) { AbstractFile.MimeMatchEnum mimeMatch = file.isMimeType(SUPP_MIME_TYPES); @@ -141,15 +147,29 @@ public class ImageUtils { // if we have an extension, check it final String extension = file.getNameExtension(); - if (StringUtils.isNotBlank(extension)) { - if (SUPP_EXTENSIONS.contains(extension)) { - return true; - } + if (StringUtils.isNotBlank(extension) && SUPP_EXTENSIONS.contains(extension)) { + return true; } + // if no extension or one that is not for an image, then read the content return isJpegFileHeader(file) || isPngFileHeader(file); } + /** + * lazily instantiates and returns a FileTypeDetector + * + * @return a FileTypeDetector + * + * @throws FileTypeDetectorInitException if a initializing the + * FileTypeDetector failed. + */ + synchronized private static FileTypeDetector getFileTypeDetector() throws FileTypeDetector.FileTypeDetectorInitException { + if (fileTypeDetector == null) { + fileTypeDetector = new FileTypeDetector(); + } + return fileTypeDetector; + } + /** * Get a thumbnail of a specified size. Generates the image if it is * not already cached. @@ -281,21 +301,28 @@ public class ImageUtils { /** * Generate a thumbnail and save it to specified location. * - * @param content File to generate icon for - * @param size the size of thumbnail to generate in pixels - * @param saveFile Location to save thumbnail to + * @param content File to generate icon for + * @param size the size of thumbnail to generate in pixels + * @param cacheFile Location to save thumbnail to * * @return Generated icon or a default icon if a thumbnail could not be * made. */ - private static Image generateAndSaveThumbnail(Content content, int size, File saveFile) { - BufferedImage thumbNail = generateThumbnail(content, size); - if (Objects.nonNull(thumbNail)) { + private static Image generateAndSaveThumbnail(Content content, int size, File cacheFile) { + BufferedImage thumbnail = generateThumbnail(content, size); + if (Objects.nonNull(thumbnail)) { imageSaver.execute(() -> { - - saveThumbnail(content, thumbNail, saveFile); + try { + Files.createParentDirs(cacheFile); + if (cacheFile.exists()) { + cacheFile.delete(); + } + ImageIO.write(thumbnail, FORMAT, cacheFile); + } catch (IllegalArgumentException | IOException ex1) { + LOGGER.log(Level.WARNING, "Could not write cache thumbnail: " + content, ex1); //NON-NLS + } }); - return thumbNail; + return thumbnail; } else { return getDefaultIcon(); } @@ -313,8 +340,7 @@ public class ImageUtils { @Nullable private static BufferedImage generateThumbnail(Content content, int iconSize) { - try (InputStream inputStream = new ReadContentInputStream(content);) { - + try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(content));) { BufferedImage bi = ImageIO.read(inputStream); if (bi == null) { LOGGER.log(Level.WARNING, "No image reader for file: {0}", content.getName()); //NON-NLS @@ -323,7 +349,7 @@ public class ImageUtils { try { return ScalrWrapper.resizeFast(bi, iconSize); } catch (IllegalArgumentException e) { - // if resizing does not work due to extremely small height/width ratio, + // if resizing does not work due to extreme aspect ratio, // crop the image instead. return ScalrWrapper.cropImage(bi, Math.min(iconSize, bi.getWidth()), Math.min(iconSize, bi.getHeight())); } @@ -335,24 +361,4 @@ public class ImageUtils { return null; } } - - /** - * save the generated thumbnail to disk in the cache folder with - * the obj_id as the name. - * - * @param file the file the given image is a thumbnail for - * @param thumbnail the thumbnail to save for the given DrawableFile - */ - static private void saveThumbnail(Content content, final RenderedImage thumbnail, File cacheFile) { - try { - Files.createParentDirs(cacheFile); - if (cacheFile.exists()) { - cacheFile.delete(); - } - //convert back to swing to save - ImageIO.write(thumbnail, FORMAT, cacheFile); - } catch (IllegalArgumentException | IOException ex) { - LOGGER.log(Level.WARNING, "Could not write cache thumbnail: " + content, ex); //NON-NLS - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index ef7d82e4b8..47f2c30940 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -21,10 +21,10 @@ package org.sleuthkit.autopsy.datamodel; import java.util.ArrayList; import java.util.List; import javax.swing.Action; - import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.actions.AddContentTagAction; +import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.HashSearchAction; @@ -118,7 +118,7 @@ public class FileNode extends AbstractFsContentNode { // Images for (String s : FileTypeExtensions.getImageExtensions()) { - if (ext.equals(s)) { + if (ImageUtils.thumbnailSupported(file) || ext.equals(s)) { return "org/sleuthkit/autopsy/images/image-file.png"; //NON-NLS } }