minor improvements to new image support

restore isImageSupported code lost in merge
more cleanup in ImageUtils and MediaViewImagePanel
lazily instantiate static fileTypeDetector
better notification of errorsl loading images
special case to display iff images incorectly identifiead as audio/x-aiff
show image icon in result viewers for more files
This commit is contained in:
jmillman 2015-07-13 12:28:04 -04:00
parent dc65afc62b
commit 5e375dd557
5 changed files with 115 additions and 80 deletions

View File

@ -218,8 +218,8 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo
* @return True if an image file that can be displayed * @return True if an image file that can be displayed
*/ */
private boolean isImageSupported(AbstractFile file) { private boolean isImageSupported(AbstractFile file) {
return ImageUtils.thumbnailSupported(file);
return ImageUtils.thumbnailSupported(file);
} }
@Override @Override

View File

@ -0,0 +1,28 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> 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;
}

View File

@ -32,14 +32,11 @@ import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
@ -67,6 +64,10 @@ public class MediaViewImagePanel extends JPanel {
private ImageView fxImageView; private ImageView fxImageView;
private BorderPane borderpane; 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 * mime types we shoul dbe able to display. if the mimetype is unknown we
* will fall back on extension (and jpg/png header * will fall back on extension (and jpg/png header
@ -80,7 +81,6 @@ public class MediaViewImagePanel extends JPanel {
.map("."::concat) .map("."::concat)
.collect(Collectors.toList()); .collect(Collectors.toList());
/** /**
* Creates new form MediaViewImagePanel * Creates new form MediaViewImagePanel
*/ */
@ -88,12 +88,15 @@ public class MediaViewImagePanel extends JPanel {
initComponents(); initComponents();
fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited();
if (fxInited) { 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 fxImageView = new ImageView(); // will hold image
borderpane = new BorderPane(fxImageView); // centers and sizes imageview 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 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); fxPanel.setScene(scene);
//bind size of image to that of scene, while keeping proportions //bind size of image to that of scene, while keeping proportions
@ -120,6 +123,7 @@ public class MediaViewImagePanel extends JPanel {
public void reset() { public void reset() {
Platform.runLater(() -> { Platform.runLater(() -> {
fxImageView.setImage(null); fxImageView.setImage(null);
borderpane.setCenter(null);
}); });
} }
@ -144,41 +148,38 @@ public class MediaViewImagePanel extends JPanel {
@Override @Override
public void run() { public void run() {
if (!Case.isCaseOpen()) { if (!Case.isCaseOpen()) {
//handle in-between condition when case is being closed /* handle in-between condition when case is being closed
//and an image was previously selected * and an image was previously selected */
return; return;
} }
final Image fxImage;
try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) {
BufferedImage bufferedImage = ImageIO.read(inputStream); BufferedImage bufferedImage = ImageIO.read(inputStream);
if (bufferedImage == null) { if (bufferedImage == null) {
LOGGER.log(Level.WARNING, "Could image reader not found for file: {0}", file.getName()); //NON-NLS 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; return;
} else {
fxImageView.setImage(fxImage);
borderpane.setCenter(fxImageView);
}
} }
fxImage = SwingFXUtils.toFXImage(bufferedImage, null);
} catch (IllegalArgumentException | IOException ex) { } catch (IllegalArgumentException | IOException ex) {
LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), ex); //NON-NLS LOGGER.log(Level.WARNING, "Could not load image file into media view: " + file.getName(), ex); //NON-NLS
borderpane.setCenter(errorLabel);
return;
} catch (OutOfMemoryError ex) { } catch (OutOfMemoryError ex) {
LOGGER.log(Level.WARNING, "Could not load image file into media view (too large): " + file.getName(), ex); //NON-NLS LOGGER.log(Level.WARNING, "Could not load image file into media view (too large): " + file.getName(), ex); //NON-NLS
MessageNotifyUtil.Notify.warn( MessageNotifyUtil.Notify.warn(
NbBundle.getMessage(this.getClass(), "MediaViewImagePanel.imgFileTooLarge.msg", file.getName()), NbBundle.getMessage(this.getClass(), "MediaViewImagePanel.imgFileTooLarge.msg", file.getName()),
ex.getMessage()); 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(() -> { SwingUtilities.invokeLater(() -> {
//show the panel after fully loaded //show the panel after fully loaded
fxPanel.setVisible(true); fxPanel.setVisible(true);

View File

@ -25,7 +25,7 @@ package org.sleuthkit.autopsy.coreutils;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.awt.Image; import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.casemodule.Case;
import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.corelibs.ScalrWrapper;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector.FileTypeDetectorInitException;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.ReadContentInputStream;
@ -63,20 +64,16 @@ public class ImageUtils {
private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName());
/** save thumbnails to disk as this format */ /** 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_SMALL = 50;
public static final int ICON_SIZE_MEDIUM = 100; public static final int ICON_SIZE_MEDIUM = 100;
public static final int ICON_SIZE_LARGE = 200; 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 private static final Image DEFAULT_ICON = new ImageIcon("/org/sleuthkit/autopsy/images/file-icon.png").getImage(); //NON-NLS
public static List<String> getSupportedExtensions() { //initialized lazily
return Collections.unmodifiableList(SUPP_EXTENSIONS); private static FileTypeDetector fileTypeDetector;
}
public static SortedSet<String> getSupportedMimeTypes() {
return Collections.unmodifiableSortedSet(SUPP_MIME_TYPES);
}
private static final List<String> SUPP_EXTENSIONS; private static final List<String> SUPP_EXTENSIONS;
private static final TreeSet<String> SUPP_MIME_TYPES; private static final TreeSet<String> SUPP_MIME_TYPES;
@ -90,11 +87,20 @@ public class ImageUtils {
SUPP_MIME_TYPES = new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes())); 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.addAll(Arrays.asList("image/x-ms-bmp", "application/x-123"));
SUPP_MIME_TYPES.removeIf("application/octet-stream"::equals);
} }
private ImageUtils() { private ImageUtils() {
} }
public static List<String> getSupportedExtensions() {
return Collections.unmodifiableList(SUPP_EXTENSIONS);
}
public static SortedSet<String> getSupportedMimeTypes() {
return Collections.unmodifiableSortedSet(SUPP_MIME_TYPES);
}
/** /**
* Get the default Icon, which is the icon for a file. * Get the default Icon, which is the icon for a file.
* *
@ -122,12 +128,12 @@ public class ImageUtils {
} }
try { try {
String mimeType = new FileTypeDetector().getFileType(file); String mimeType = getFileTypeDetector().getFileType(file);
if (Objects.nonNull(mimeType)) { 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) { } catch (FileTypeDetector.FileTypeDetectorInitException | TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to look up mimetype for " + file.getName() + " using FileTypeDetector. Fallingback on AbstractFile.isMimeType", 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()) { if (!SUPP_MIME_TYPES.isEmpty()) {
AbstractFile.MimeMatchEnum mimeMatch = file.isMimeType(SUPP_MIME_TYPES); AbstractFile.MimeMatchEnum mimeMatch = file.isMimeType(SUPP_MIME_TYPES);
@ -141,15 +147,29 @@ public class ImageUtils {
// if we have an extension, check it // if we have an extension, check it
final String extension = file.getNameExtension(); final String extension = file.getNameExtension();
if (StringUtils.isNotBlank(extension)) { if (StringUtils.isNotBlank(extension) && SUPP_EXTENSIONS.contains(extension)) {
if (SUPP_EXTENSIONS.contains(extension)) {
return true; return true;
} }
}
// if no extension or one that is not for an image, then read the content // if no extension or one that is not for an image, then read the content
return isJpegFileHeader(file) || isPngFileHeader(file); 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 * Get a thumbnail of a specified size. Generates the image if it is
* not already cached. * not already cached.
@ -283,19 +303,26 @@ public class ImageUtils {
* *
* @param content File to generate icon for * @param content File to generate icon for
* @param size the size of thumbnail to generate in pixels * @param size the size of thumbnail to generate in pixels
* @param saveFile Location to save thumbnail to * @param cacheFile Location to save thumbnail to
* *
* @return Generated icon or a default icon if a thumbnail could not be * @return Generated icon or a default icon if a thumbnail could not be
* made. * made.
*/ */
private static Image generateAndSaveThumbnail(Content content, int size, File saveFile) { private static Image generateAndSaveThumbnail(Content content, int size, File cacheFile) {
BufferedImage thumbNail = generateThumbnail(content, size); BufferedImage thumbnail = generateThumbnail(content, size);
if (Objects.nonNull(thumbNail)) { if (Objects.nonNull(thumbnail)) {
imageSaver.execute(() -> { imageSaver.execute(() -> {
try {
saveThumbnail(content, thumbNail, saveFile); 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 { } else {
return getDefaultIcon(); return getDefaultIcon();
} }
@ -313,8 +340,7 @@ public class ImageUtils {
@Nullable @Nullable
private static BufferedImage generateThumbnail(Content content, int iconSize) { 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); BufferedImage bi = ImageIO.read(inputStream);
if (bi == null) { if (bi == null) {
LOGGER.log(Level.WARNING, "No image reader for file: {0}", content.getName()); //NON-NLS LOGGER.log(Level.WARNING, "No image reader for file: {0}", content.getName()); //NON-NLS
@ -323,7 +349,7 @@ public class ImageUtils {
try { try {
return ScalrWrapper.resizeFast(bi, iconSize); return ScalrWrapper.resizeFast(bi, iconSize);
} catch (IllegalArgumentException e) { } 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. // crop the image instead.
return ScalrWrapper.cropImage(bi, Math.min(iconSize, bi.getWidth()), Math.min(iconSize, bi.getHeight())); return ScalrWrapper.cropImage(bi, Math.min(iconSize, bi.getWidth()), Math.min(iconSize, bi.getHeight()));
} }
@ -335,24 +361,4 @@ public class ImageUtils {
return null; 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
}
}
} }

View File

@ -21,10 +21,10 @@ package org.sleuthkit.autopsy.datamodel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.swing.Action; import javax.swing.Action;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
import org.sleuthkit.autopsy.actions.AddContentTagAction; 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.ExternalViewerAction;
import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.ExtractAction;
import org.sleuthkit.autopsy.directorytree.HashSearchAction; import org.sleuthkit.autopsy.directorytree.HashSearchAction;
@ -118,7 +118,7 @@ public class FileNode extends AbstractFsContentNode<AbstractFile> {
// Images // Images
for (String s : FileTypeExtensions.getImageExtensions()) { 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 return "org/sleuthkit/autopsy/images/image-file.png"; //NON-NLS
} }
} }