22 package org.sleuthkit.autopsy.coreutils;
24 import com.google.common.collect.ImmutableSortedSet;
25 import com.google.common.io.Files;
26 import java.awt.Image;
27 import java.awt.image.BufferedImage;
28 import java.io.BufferedInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.nio.file.Paths;
33 import java.text.MessageFormat;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.Iterator;
39 import java.util.List;
40 import static java.util.Objects.nonNull;
41 import java.util.SortedSet;
42 import java.util.TreeSet;
43 import java.util.concurrent.ExecutionException;
44 import java.util.concurrent.Executor;
45 import java.util.concurrent.Executors;
46 import java.util.logging.Level;
47 import javafx.concurrent.Task;
48 import javafx.embed.swing.SwingFXUtils;
49 import javax.annotation.Nonnull;
50 import javax.annotation.Nullable;
51 import javax.imageio.IIOException;
52 import javax.imageio.ImageIO;
53 import javax.imageio.ImageReadParam;
54 import javax.imageio.ImageReader;
55 import javax.imageio.event.IIOReadProgressListener;
56 import javax.imageio.stream.ImageInputStream;
57 import org.apache.commons.lang3.ObjectUtils;
58 import org.apache.commons.lang3.StringUtils;
59 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
60 import org.opencv.core.Core;
61 import org.openide.util.NbBundle;
82 private static final String
FORMAT =
"png";
91 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
99 ImageIO.scanForPlugins();
100 BufferedImage tempImage;
102 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
103 }
catch (IOException ex) {
104 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
107 DEFAULT_THUMBNAIL = tempImage;
110 boolean openCVLoadedTemp;
112 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
113 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
114 System.loadLibrary(
"opencv_ffmpeg248_64");
116 System.loadLibrary(
"opencv_ffmpeg248");
119 openCVLoadedTemp =
true;
120 }
catch (UnsatisfiedLinkError e) {
121 openCVLoadedTemp =
false;
122 LOGGER.log(Level.SEVERE,
"OpenCV Native code library failed to load", e);
127 openCVLoaded = openCVLoadedTemp;
128 SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes()));
129 SUPPORTED_IMAGE_EXTENSIONS.add(
"tec");
130 SUPPORTED_IMAGE_MIME_TYPES =
new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
135 SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList(
138 "image/x-portable-graymap",
139 "image/x-portable-bitmap",
140 "application/x-123"));
141 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
153 = Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
154 .namingPattern(
"thumbnail-saver-%d").build());
157 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
161 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
186 if (!(content instanceof AbstractFile)) {
189 AbstractFile file = (AbstractFile) content;
204 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
215 public static boolean isGIF(AbstractFile file) {
216 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
239 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
240 if (
false == file.isFile() || file.getSize() <= 0) {
244 String extension = file.getNameExtension();
246 if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) {
251 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
254 return supportedMimeTypes.contains(mimeType);
255 }
catch (FileTypeDetectorInitException | TskCoreException ex) {
256 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
274 if (fileTypeDetector == null) {
290 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
291 if (content instanceof AbstractFile) {
292 AbstractFile file = (AbstractFile) content;
297 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
298 }
catch (InterruptedException | ExecutionException ex) {
299 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for {0}: " + ex.toString(), getContentPathSafe(content));
337 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
338 }
catch (IllegalStateException e) {
339 LOGGER.log(Level.WARNING,
"Could not get cached thumbnail location. No case is open.");
364 if (file.getSize() < 100) {
369 byte[] fileHeaderBuffer =
readHeader(file, 2);
374 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
375 }
catch (TskCoreException ex) {
391 byte[] fileHeaderBuffer;
394 length = file.getSize();
395 if (length % 2 != 0) {
398 if (length >= 1024) {
401 fileHeaderBuffer =
readHeader(file, (
int) length);
402 }
catch (TskCoreException ex) {
407 if (fileHeaderBuffer != null) {
408 for (
int index = 0; index < length; index += 2) {
410 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
428 if (file.getSize() < 10) {
433 byte[] fileHeaderBuffer =
readHeader(file, 8);
438 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
439 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
440 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
441 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
443 }
catch (TskCoreException ex) {
449 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
450 byte[] fileHeaderBuffer =
new byte[buffLength];
451 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
453 if (bytesRead != buffLength) {
455 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
457 return fileHeaderBuffer;
472 "ImageIO could not determine width of {0}: ",
473 imageReader -> imageReader.getWidth(0)
489 "ImageIO could not determine height of {0}: ",
490 imageReader -> imageReader.getHeight(0)
505 public T
extract(ImageReader reader)
throws IOException;
530 try (InputStream inputStream =
new BufferedInputStream(
new ReadContentInputStream(file));) {
531 try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
533 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
534 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
537 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
539 if (readers.hasNext()) {
540 ImageReader reader = readers.next();
541 reader.setInput(input);
544 return propertyExtractor.extract(reader);
545 }
catch (IOException ex) {
546 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
552 IIOException iioException =
new IIOException(
"No ImageReader found.");
553 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
577 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
592 @NbBundle.Messages({
"# {0} - file name",
593 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
595 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
598 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
605 protected javafx.scene.image.Image
call() throws Exception {
613 if (cacheFile != null && cacheFile.exists()) {
615 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
616 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
617 return SwingFXUtils.toFXImage(cachedThumbnail, null);
619 }
catch (Exception ex) {
620 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
630 BufferedImage thumbnail = null;
633 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
634 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
636 if (null == thumbnail) {
637 if (defaultOnFailure) {
640 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
647 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
648 if (null == bufferedImage) {
649 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
650 LOGGER.log(Level.WARNING, msg);
651 throw new IIOException(msg);
653 updateProgress(-1, 1);
658 }
catch (IllegalArgumentException | OutOfMemoryError e) {
660 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
662 final int height = bufferedImage.getHeight();
663 final int width = bufferedImage.getWidth();
664 if (iconSize < height || iconSize < width) {
665 final int cropHeight = Math.min(iconSize, height);
666 final int cropWidth = Math.min(iconSize, width);
669 }
catch (Exception cropException) {
670 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
673 }
catch (Exception e) {
674 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
683 updateProgress(-1, 1);
686 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
690 return SwingFXUtils.toFXImage(thumbnail, null);
701 Files.createParentDirs(cacheFile);
702 if (cacheFile.exists()) {
705 ImageIO.write(thumbnail, FORMAT, cacheFile);
706 }
catch (IllegalArgumentException | IOException ex) {
707 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
735 "ReadImageTask.mesageText=Reading image: {0}"})
740 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
744 protected javafx.scene.image.Image
call() throws Exception {
752 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
755 final AbstractFile file;
762 protected javafx.scene.image.Image
readImage() throws IOException {
765 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(
new ReadContentInputStream(file)));
766 if (image.isError() ==
false) {
769 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
770 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
774 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
775 if (image.isError() ==
false) {
793 ImageReadParam param = imageReader.getDefaultReadParam();
794 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
795 param.setDestination(bufferedImage);
797 bufferedImage = imageReader.read(0, param);
798 }
catch (IOException iOException) {
799 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
806 return SwingFXUtils.toFXImage(bufferedImage, null);
814 updateProgress(percentageDone, 100);
816 reader.removeIIOReadProgressListener(
this);
826 javafx.scene.image.Image fxImage =
get();
827 if (fxImage == null) {
828 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
829 }
else if (fxImage.isError()) {
831 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
833 }
catch (InterruptedException | ExecutionException ex) {
841 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
846 updateProgress(100, 100);
886 static String getContentPathSafe(Content content) {
888 return content.getUniquePath();
889 }
catch (TskCoreException tskCoreException) {
890 String contentName = content.getName();
891 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
940 public static BufferedImage
getIcon(Content content,
int iconSize) {
static File getIconFile(Content content, int iconSize)
static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT
static boolean isGIF(AbstractFile file)
static final List< String > SUPPORTED_IMAGE_EXTENSIONS
javafx.scene.image.Image call()
static boolean isPngFileHeader(AbstractFile file)
static boolean thumbnailSupported(Content content)
static File getFile(long id)
void sequenceComplete(ImageReader source)
static final Logger LOGGER
void imageProgress(ImageReader reader, float percentageDone)
void readAborted(ImageReader source)
static Task< javafx.scene.image.Image > newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure)
final boolean defaultOnFailure
void imageStarted(ImageReader source, int imageIndex)
static final int ICON_SIZE_SMALL
static synchronized BufferedImage resizeFast(BufferedImage input, int size)
static FileTypeDetector fileTypeDetector
static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION
static final SortedSet< String > GIF_MIME_SET
static Task< javafx.scene.image.Image > newReadImageTask(AbstractFile file)
void sequenceStarted(ImageReader source, int minIndex)
static boolean isJpegFileHeader(AbstractFile file)
javafx.scene.image.Image readImage()
static final Executor imageSaver
void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex)
static final int ICON_SIZE_MEDIUM
static final boolean openCVLoaded
static Image getDefaultIcon()
GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure)
static int getImageHeight(AbstractFile file)
void thumbnailProgress(ImageReader source, float percentageDone)
static List< String > getSupportedImageExtensions()
String getCacheDirectory()
void thumbnailComplete(ImageReader source)
static Image getDefaultThumbnail()
static final BufferedImage DEFAULT_THUMBNAIL
static long getJfifStartOfImageOffset(AbstractFile file)
javafx.scene.image.Image call()
static File getCachedThumbnailFile(Content content, int iconSize)
static boolean hasImageFileHeader(AbstractFile file)
static byte[] readHeader(AbstractFile file, int buffLength)
String detect(AbstractFile file)
static File getCachedThumbnailLocation(long fileID)
static Case getCurrentCase()
synchronized static Logger getLogger(String name)
static int getImageWidth(AbstractFile file)
static< T > T getImageProperty(AbstractFile file, final String errorTemplate, PropertyExtractor< T > propertyExtractor)
static boolean isImageThumbnailSupported(AbstractFile file)
static BufferedImage getThumbnail(Content content, int iconSize)
static final int ICON_SIZE_LARGE
static final List< String > GIF_EXTENSION_LIST
static final String FORMAT
synchronized static FileTypeDetector getFileTypeDetector()
void imageComplete(ImageReader source)
void saveThumbnail(BufferedImage thumbnail)
static BufferedImage getIcon(Content content, int iconSize)
static boolean isVideoThumbnailSupported(AbstractFile file)
static final SortedSet< String > SUPPORTED_IMAGE_MIME_TYPES
static synchronized BufferedImage cropImage(BufferedImage input, int width, int height)
static SortedSet< String > getSupportedImageMimeTypes()