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 java.util.Objects;
42 import static java.util.Objects.nonNull;
43 import java.util.SortedSet;
44 import java.util.TreeSet;
45 import java.util.concurrent.ConcurrentHashMap;
46 import java.util.concurrent.ExecutionException;
47 import java.util.concurrent.Executor;
48 import java.util.concurrent.Executors;
49 import java.util.logging.Level;
50 import java.util.stream.Collectors;
51 import java.util.stream.Stream;
52 import javafx.concurrent.Task;
53 import javafx.embed.swing.SwingFXUtils;
54 import javax.annotation.Nonnull;
55 import javax.annotation.Nullable;
56 import javax.imageio.IIOException;
57 import javax.imageio.ImageIO;
58 import javax.imageio.ImageReadParam;
59 import javax.imageio.ImageReader;
60 import javax.imageio.event.IIOReadProgressListener;
61 import javax.imageio.stream.ImageInputStream;
62 import org.apache.commons.lang3.StringUtils;
63 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
64 import org.openide.util.NbBundle;
65 import org.openide.util.NbBundle.Messages;
88 private static final String
FORMAT =
"png";
97 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
111 @Messages({
"ImageUtils.ffmpegLoadedError.title=OpenCV FFMpeg",
112 "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
113 private static final ConcurrentHashMap<Long, File>
cacheFileMap =
new ConcurrentHashMap<>();
116 ImageIO.scanForPlugins();
117 BufferedImage tempImage;
119 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
120 }
catch (IOException ex) {
121 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
124 DEFAULT_THUMBNAIL = tempImage;
125 boolean tempFfmpegLoaded =
false;
128 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
129 System.loadLibrary(
"opencv_ffmpeg248_64");
131 System.loadLibrary(
"opencv_ffmpeg248");
133 tempFfmpegLoaded =
true;
134 }
catch (UnsatisfiedLinkError e) {
135 tempFfmpegLoaded =
false;
136 LOGGER.log(Level.SEVERE, Bundle.ImageUtils_ffmpegLoadedError_msg(), e);
140 FFMPEG_LOADED = tempFfmpegLoaded;
145 List<String> imageSuffixList = Arrays.stream(ImageIO.getReaderFileSuffixes())
146 .filter((extension) -> StringUtils.isNotBlank(extension))
147 .collect(Collectors.toList());
149 SUPPORTED_IMAGE_EXTENSIONS.addAll(imageSuffixList);
150 SUPPORTED_IMAGE_EXTENSIONS.add(
"tec");
151 SUPPORTED_IMAGE_EXTENSIONS.removeIf(
"db"::equals);
153 List<String> mimeTypeList = Stream.of(ImageIO.getReaderMIMETypes())
157 .filter((mimeType) -> StringUtils.isNotBlank(mimeType))
158 .collect(Collectors.toList());
160 SUPPORTED_IMAGE_MIME_TYPES =
new TreeSet<>(mimeTypeList);
165 SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList(
168 "image/x-portable-graymap",
169 "image/x-portable-bitmap",
171 "application/x-123"));
172 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
187 = Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
188 .namingPattern(
"thumbnail-saver-%d").build());
191 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
195 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
220 if (!(content instanceof AbstractFile)) {
223 AbstractFile file = (AbstractFile) content;
233 if (isSupportedMediaExtension(file, supportedExtensions)) {
250 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
261 public static boolean isGIF(AbstractFile file) {
262 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
285 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
286 if (
false == file.isFile() || file.getSize() <= 0) {
290 if (isSupportedMediaExtension(file, supportedExtension)) {
295 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
298 return supportedMimeTypes.contains(mimeType);
299 }
catch (FileTypeDetectorInitException ex) {
300 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
315 static boolean isSupportedMediaExtension(
final AbstractFile file,
final List<String> supportedExtensions) {
316 String extension = file.getNameExtension();
318 return (StringUtils.isNotBlank(extension) && supportedExtensions.contains(extension));
333 if (fileTypeDetector == null) {
349 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
350 if (content instanceof AbstractFile) {
351 AbstractFile file = (AbstractFile) content;
358 if (Thread.interrupted()) {
361 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
363 if (Thread.interrupted()) {
368 }
catch (IOException iOException) {
369 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
375 if (Thread.interrupted()) {
380 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
381 }
catch (InterruptedException | ExecutionException ex) {
382 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
398 return new BufferedInputStream(
new ReadContentInputStream(file));
431 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
433 LOGGER.log(Level.INFO,
"Could not get cached thumbnail location. No case is open.");
459 if (file.getSize() < 100) {
464 byte[] fileHeaderBuffer =
readHeader(file, 2);
469 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
470 }
catch (TskCoreException ex) {
486 byte[] fileHeaderBuffer;
489 length = file.getSize();
490 if (length % 2 != 0) {
493 if (length >= 1024) {
496 fileHeaderBuffer =
readHeader(file, (
int) length);
497 }
catch (TskCoreException ex) {
502 if (fileHeaderBuffer != null) {
503 for (
int index = 0; index < length; index += 2) {
505 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
523 if (file.getSize() < 10) {
528 byte[] fileHeaderBuffer =
readHeader(file, 8);
533 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
534 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
535 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
536 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
538 }
catch (TskCoreException ex) {
544 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
545 byte[] fileHeaderBuffer =
new byte[buffLength];
546 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
548 if (bytesRead != buffLength) {
550 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
552 return fileHeaderBuffer;
567 "ImageIO could not determine width of {0}: ",
568 imageReader -> imageReader.getWidth(0)
584 "ImageIO could not determine height of {0}: ",
585 imageReader -> imageReader.getHeight(0)
601 public T
extract(ImageReader reader)
throws IOException;
627 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
629 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
630 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
633 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
635 if (readers.hasNext()) {
636 ImageReader reader = readers.next();
637 reader.setInput(input);
639 return propertyExtractor.extract(reader);
640 }
catch (IOException ex) {
641 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
647 IIOException iioException =
new IIOException(
"No ImageReader found.");
648 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
670 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
686 @NbBundle.Messages({
"# {0} - file name",
687 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
689 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
692 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
699 protected javafx.scene.image.Image
call() throws Exception {
708 if (cacheFile != null) {
710 if (cacheFile.exists()) {
715 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
719 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
720 return SwingFXUtils.toFXImage(cachedThumbnail, null);
722 }
catch (Exception ex) {
723 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
731 BufferedImage thumbnail = null;
734 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
738 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
740 if (null == thumbnail) {
741 if (defaultOnFailure) {
744 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
754 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
755 if (null == bufferedImage) {
756 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
757 LOGGER.log(Level.WARNING, msg);
758 throw new IIOException(msg);
760 updateProgress(-1, 1);
770 }
catch (IllegalArgumentException | OutOfMemoryError e) {
772 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
774 final int height = bufferedImage.getHeight();
775 final int width = bufferedImage.getWidth();
776 if (iconSize < height || iconSize < width) {
777 final int cropHeight = Math.min(iconSize, height);
778 final int cropWidth = Math.min(iconSize, width);
787 }
catch (Exception cropException) {
788 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
791 }
catch (Exception e) {
792 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
797 updateProgress(-1, 1);
800 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
806 return SwingFXUtils.toFXImage(thumbnail, null);
818 Path path = Paths.get(cacheFile.getParent());
819 File thumbsDir = Paths.get(cacheFile.getParent()).toFile();
820 if (!thumbsDir.exists()) {
824 if (cacheFile.exists()) {
827 ImageIO.write(thumbnail, FORMAT, cacheFile);
829 }
catch (Exception ex) {
830 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
859 "ReadImageTask.mesageText=Reading image: {0}"})
864 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
868 protected javafx.scene.image.Image
call() throws Exception {
876 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
879 final AbstractFile file;
885 protected javafx.scene.image.Image
readImage() throws IOException {
892 if (image.isError() ==
false) {
895 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
896 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
900 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
901 if (image.isError() ==
false) {
915 ImageReadParam param = imageReader.getDefaultReadParam();
916 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
917 param.setDestination(bufferedImage);
922 bufferedImage = imageReader.read(0, param);
923 }
catch (IOException iOException) {
924 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
931 return SwingFXUtils.toFXImage(bufferedImage, null);
939 updateProgress(percentageDone, 100);
941 reader.removeIIOReadProgressListener(
this);
949 if (Thread.interrupted()) {
953 return super.isCancelled();
960 javafx.scene.image.Image fxImage =
get();
961 if (fxImage == null) {
962 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
963 }
else if (fxImage.isError()) {
965 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + Objects.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
967 }
catch (InterruptedException | ExecutionException ex) {
975 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + Objects.toString(getException()),
ImageUtils.getContentPathSafe(file));
980 updateProgress(100, 100);
1020 static String getContentPathSafe(Content content) {
1022 return content.getUniquePath();
1023 }
catch (TskCoreException tskCoreException) {
1024 String contentName = content.getName();
1025 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
1074 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()