22 package org.sleuthkit.autopsy.coreutils;
24 import com.google.common.collect.ImmutableSortedSet;
25 import java.awt.Image;
26 import java.awt.image.BufferedImage;
27 import java.io.BufferedInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.nio.file.Path;
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.EnumSet;
39 import java.util.Iterator;
40 import java.util.List;
41 import static java.util.Objects.nonNull;
42 import java.util.SortedSet;
43 import java.util.TreeSet;
44 import java.util.concurrent.ConcurrentHashMap;
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.Executor;
47 import java.util.concurrent.Executors;
48 import java.util.logging.Level;
49 import javafx.concurrent.Task;
50 import javafx.embed.swing.SwingFXUtils;
51 import javax.annotation.Nonnull;
52 import javax.annotation.Nullable;
53 import javax.imageio.IIOException;
54 import javax.imageio.ImageIO;
55 import javax.imageio.ImageReadParam;
56 import javax.imageio.ImageReader;
57 import javax.imageio.event.IIOReadProgressListener;
58 import javax.imageio.stream.ImageInputStream;
59 import org.apache.commons.lang3.ObjectUtils;
60 import org.apache.commons.lang3.StringUtils;
61 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
62 import org.openide.util.NbBundle;
63 import org.openide.util.NbBundle.Messages;
86 private static final String
FORMAT =
"png";
95 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
109 @Messages({
"ImageUtils.ffmpegLoadedError.title=OpenCV FFMpeg",
110 "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
111 private static final ConcurrentHashMap<Long, File>
cacheFileMap =
new ConcurrentHashMap<>();
114 ImageIO.scanForPlugins();
115 BufferedImage tempImage;
117 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
118 }
catch (IOException ex) {
119 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
122 DEFAULT_THUMBNAIL = tempImage;
123 boolean tempFfmpegLoaded =
false;
126 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
127 System.loadLibrary(
"opencv_ffmpeg248_64");
129 System.loadLibrary(
"opencv_ffmpeg248");
131 tempFfmpegLoaded =
true;
132 }
catch (UnsatisfiedLinkError e) {
133 tempFfmpegLoaded =
false;
134 LOGGER.log(Level.SEVERE, Bundle.ImageUtils_ffmpegLoadedError_msg(), e);
138 FFMPEG_LOADED = tempFfmpegLoaded;
140 SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes()));
141 SUPPORTED_IMAGE_EXTENSIONS.add(
"tec");
142 SUPPORTED_IMAGE_EXTENSIONS.removeIf(
"db"::equals);
144 SUPPORTED_IMAGE_MIME_TYPES =
new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
149 SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList(
152 "image/x-portable-graymap",
153 "image/x-portable-bitmap",
155 "application/x-123"));
156 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
171 = Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
172 .namingPattern(
"thumbnail-saver-%d").build());
175 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
179 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
204 if (!(content instanceof AbstractFile)) {
207 AbstractFile file = (AbstractFile) content;
217 if (isSupportedMediaExtension(file, supportedExtensions)) {
234 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
245 public static boolean isGIF(AbstractFile file) {
246 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
269 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
270 if (
false == file.isFile() || file.getSize() <= 0) {
274 if (isSupportedMediaExtension(file, supportedExtension)) {
279 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
282 return supportedMimeTypes.contains(mimeType);
283 }
catch (FileTypeDetectorInitException ex) {
284 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
299 static boolean isSupportedMediaExtension(
final AbstractFile file,
final List<String> supportedExtensions) {
300 String extension = file.getNameExtension();
302 return (StringUtils.isNotBlank(extension) && supportedExtensions.contains(extension));
317 if (fileTypeDetector == null) {
333 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
334 if (content instanceof AbstractFile) {
335 AbstractFile file = (AbstractFile) content;
342 if (Thread.interrupted()) {
345 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
347 if (Thread.interrupted()) {
352 }
catch (IOException iOException) {
353 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
359 if (Thread.interrupted()) {
364 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
365 }
catch (InterruptedException | ExecutionException ex) {
366 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
382 return new BufferedInputStream(
new ReadContentInputStream(file));
415 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
417 LOGGER.log(Level.INFO,
"Could not get cached thumbnail location. No case is open.");
443 if (file.getSize() < 100) {
448 byte[] fileHeaderBuffer =
readHeader(file, 2);
453 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
454 }
catch (TskCoreException ex) {
470 byte[] fileHeaderBuffer;
473 length = file.getSize();
474 if (length % 2 != 0) {
477 if (length >= 1024) {
480 fileHeaderBuffer =
readHeader(file, (
int) length);
481 }
catch (TskCoreException ex) {
486 if (fileHeaderBuffer != null) {
487 for (
int index = 0; index < length; index += 2) {
489 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
507 if (file.getSize() < 10) {
512 byte[] fileHeaderBuffer =
readHeader(file, 8);
517 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
518 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
519 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
520 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
522 }
catch (TskCoreException ex) {
528 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
529 byte[] fileHeaderBuffer =
new byte[buffLength];
530 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
532 if (bytesRead != buffLength) {
534 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
536 return fileHeaderBuffer;
551 "ImageIO could not determine width of {0}: ",
552 imageReader -> imageReader.getWidth(0)
568 "ImageIO could not determine height of {0}: ",
569 imageReader -> imageReader.getHeight(0)
585 public T
extract(ImageReader reader)
throws IOException;
611 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
613 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
614 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
617 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
619 if (readers.hasNext()) {
620 ImageReader reader = readers.next();
621 reader.setInput(input);
623 return propertyExtractor.extract(reader);
624 }
catch (IOException ex) {
625 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
631 IIOException iioException =
new IIOException(
"No ImageReader found.");
632 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
654 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
670 @NbBundle.Messages({
"# {0} - file name",
671 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
673 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
676 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
683 protected javafx.scene.image.Image
call() throws Exception {
692 if (cacheFile != null) {
694 if (cacheFile.exists()) {
699 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
703 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
704 return SwingFXUtils.toFXImage(cachedThumbnail, null);
706 }
catch (Exception ex) {
707 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
715 BufferedImage thumbnail = null;
718 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
722 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
724 if (null == thumbnail) {
725 if (defaultOnFailure) {
728 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
738 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
739 if (null == bufferedImage) {
740 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
741 LOGGER.log(Level.WARNING, msg);
742 throw new IIOException(msg);
744 updateProgress(-1, 1);
754 }
catch (IllegalArgumentException | OutOfMemoryError e) {
756 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
758 final int height = bufferedImage.getHeight();
759 final int width = bufferedImage.getWidth();
760 if (iconSize < height || iconSize < width) {
761 final int cropHeight = Math.min(iconSize, height);
762 final int cropWidth = Math.min(iconSize, width);
771 }
catch (Exception cropException) {
772 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
775 }
catch (Exception e) {
776 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
781 updateProgress(-1, 1);
784 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
790 return SwingFXUtils.toFXImage(thumbnail, null);
802 Path path = Paths.get(cacheFile.getParent());
803 File thumbsDir = Paths.get(cacheFile.getParent()).toFile();
804 if (!thumbsDir.exists()) {
808 if (cacheFile.exists()) {
811 ImageIO.write(thumbnail, FORMAT, cacheFile);
813 }
catch (Exception ex) {
814 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
843 "ReadImageTask.mesageText=Reading image: {0}"})
848 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
852 protected javafx.scene.image.Image
call() throws Exception {
860 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
863 final AbstractFile file;
869 protected javafx.scene.image.Image
readImage() throws IOException {
876 if (image.isError() ==
false) {
879 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
880 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
884 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
885 if (image.isError() ==
false) {
899 ImageReadParam param = imageReader.getDefaultReadParam();
900 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
901 param.setDestination(bufferedImage);
906 bufferedImage = imageReader.read(0, param);
907 }
catch (IOException iOException) {
908 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
915 return SwingFXUtils.toFXImage(bufferedImage, null);
923 updateProgress(percentageDone, 100);
925 reader.removeIIOReadProgressListener(
this);
933 if (Thread.interrupted()) {
937 return super.isCancelled();
944 javafx.scene.image.Image fxImage =
get();
945 if (fxImage == null) {
946 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
947 }
else if (fxImage.isError()) {
949 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
951 }
catch (InterruptedException | ExecutionException ex) {
959 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
964 updateProgress(100, 100);
1004 static String getContentPathSafe(Content content) {
1006 return content.getUniquePath();
1007 }
catch (TskCoreException tskCoreException) {
1008 String contentName = content.getName();
1009 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
1058 public static BufferedImage
getIcon(Content content,
int iconSize) {
static List< String > getSupportedVideoExtensions()
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)
String getMIMEType(AbstractFile file)
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 synchronized BufferedImage resizeHighQuality(BufferedImage input, int width, int height)
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 Image getDefaultIcon()
GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure)
static boolean openCvIsLoaded()
static int getImageHeight(AbstractFile file)
void thumbnailProgress(ImageReader source, float percentageDone)
static List< String > getSupportedImageExtensions()
static final boolean FFMPEG_LOADED
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)
static File getCachedThumbnailLocation(long fileID)
static final ConcurrentHashMap< Long, File > cacheFileMap
synchronized static Logger getLogger(String name)
static int getImageWidth(AbstractFile file)
static void show(String title, String message, MessageType type, ActionListener actionListener)
static< T > T getImageProperty(AbstractFile file, final String errorTemplate, PropertyExtractor< T > propertyExtractor)
static Case getCurrentCaseThrows()
static void addEventTypeSubscriber(Set< Events > eventTypes, PropertyChangeListener subscriber)
static BufferedInputStream getBufferedReadContentStream(AbstractFile file)
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()