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.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.opencv.core.Core;
63 import org.openide.util.NbBundle;
84 private static final String
FORMAT =
"png";
93 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
107 private static final ConcurrentHashMap<Long, File>
cacheFileMap =
new ConcurrentHashMap<>();
110 ImageIO.scanForPlugins();
111 BufferedImage tempImage;
113 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
114 }
catch (IOException ex) {
115 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
118 DEFAULT_THUMBNAIL = tempImage;
121 boolean openCVLoadedTemp;
123 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
124 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
125 System.loadLibrary(
"opencv_ffmpeg248_64");
127 System.loadLibrary(
"opencv_ffmpeg248");
130 openCVLoadedTemp =
true;
131 }
catch (UnsatisfiedLinkError e) {
132 openCVLoadedTemp =
false;
133 LOGGER.log(Level.SEVERE,
"OpenCV Native code library failed to load", e);
138 OPEN_CV_LOADED = openCVLoadedTemp;
139 SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes()));
140 SUPPORTED_IMAGE_EXTENSIONS.add(
"tec");
141 SUPPORTED_IMAGE_EXTENSIONS.removeIf(
"db"::equals);
143 SUPPORTED_IMAGE_MIME_TYPES =
new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
148 SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList(
151 "image/x-portable-graymap",
152 "image/x-portable-bitmap",
153 "application/x-123"));
154 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
169 Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
170 .namingPattern(
"thumbnail-saver-%d").build());
173 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
177 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
202 if (!(content instanceof AbstractFile)) {
205 AbstractFile file = (AbstractFile) content;
220 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
231 public static boolean isGIF(AbstractFile file) {
232 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
255 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
256 if (
false == file.isFile() || file.getSize() <= 0) {
260 String extension = file.getNameExtension();
262 if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) {
267 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
270 return supportedMimeTypes.contains(mimeType);
271 }
catch (FileTypeDetectorInitException ex) {
272 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
290 if (fileTypeDetector == null) {
306 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
307 if (content instanceof AbstractFile) {
308 AbstractFile file = (AbstractFile) content;
315 if (Thread.interrupted()) {
318 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
320 if (Thread.interrupted()) {
325 }
catch (IOException iOException) {
326 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
332 if (Thread.interrupted()) {
337 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
338 }
catch (InterruptedException | ExecutionException ex) {
339 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
355 return new BufferedInputStream(
new ReadContentInputStream(file));
385 return cacheFileMap.computeIfAbsent(fileID,
id -> {
388 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
389 }
catch (IllegalStateException e) {
390 LOGGER.log(Level.WARNING,
"Could not get cached thumbnail location. No case is open.");
416 if (file.getSize() < 100) {
421 byte[] fileHeaderBuffer =
readHeader(file, 2);
426 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
427 }
catch (TskCoreException ex) {
443 byte[] fileHeaderBuffer;
446 length = file.getSize();
447 if (length % 2 != 0) {
450 if (length >= 1024) {
453 fileHeaderBuffer =
readHeader(file, (
int) length);
454 }
catch (TskCoreException ex) {
459 if (fileHeaderBuffer != null) {
460 for (
int index = 0; index < length; index += 2) {
462 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
480 if (file.getSize() < 10) {
485 byte[] fileHeaderBuffer =
readHeader(file, 8);
490 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
491 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
492 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
493 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
495 }
catch (TskCoreException ex) {
501 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
502 byte[] fileHeaderBuffer =
new byte[buffLength];
503 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
505 if (bytesRead != buffLength) {
507 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
509 return fileHeaderBuffer;
524 "ImageIO could not determine width of {0}: ",
525 imageReader -> imageReader.getWidth(0)
541 "ImageIO could not determine height of {0}: ",
542 imageReader -> imageReader.getHeight(0)
558 public T
extract(ImageReader reader)
throws IOException;
584 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
586 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
587 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
590 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
592 if (readers.hasNext()) {
593 ImageReader reader = readers.next();
594 reader.setInput(input);
596 return propertyExtractor.extract(reader);
597 }
catch (IOException ex) {
598 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
604 IIOException iioException =
new IIOException(
"No ImageReader found.");
605 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
627 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
643 @NbBundle.Messages({
"# {0} - file name",
644 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
646 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
649 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
656 protected javafx.scene.image.Image
call() throws Exception {
665 if (cacheFile != null) {
667 if (cacheFile.exists()) {
672 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
676 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
677 return SwingFXUtils.toFXImage(cachedThumbnail, null);
679 }
catch (Exception ex) {
680 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
688 BufferedImage thumbnail = null;
690 if (OPEN_CV_LOADED) {
691 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
695 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
697 if (null == thumbnail) {
698 if (defaultOnFailure) {
701 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
711 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
712 if (null == bufferedImage) {
713 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
714 LOGGER.log(Level.WARNING, msg);
715 throw new IIOException(msg);
717 updateProgress(-1, 1);
727 }
catch (IllegalArgumentException | OutOfMemoryError e) {
729 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
731 final int height = bufferedImage.getHeight();
732 final int width = bufferedImage.getWidth();
733 if (iconSize < height || iconSize < width) {
734 final int cropHeight = Math.min(iconSize, height);
735 final int cropWidth = Math.min(iconSize, width);
744 }
catch (Exception cropException) {
745 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
748 }
catch (Exception e) {
749 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
754 updateProgress(-1, 1);
757 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
763 return SwingFXUtils.toFXImage(thumbnail, null);
772 imageSaver.execute(() -> {
775 Files.createParentDirs(cacheFile);
776 if (cacheFile.exists()) {
779 ImageIO.write(thumbnail, FORMAT, cacheFile);
781 }
catch (Exception ex) {
782 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
811 "ReadImageTask.mesageText=Reading image: {0}"})
816 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
820 protected javafx.scene.image.Image
call() throws Exception {
828 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
831 final AbstractFile file;
837 protected javafx.scene.image.Image
readImage() throws IOException {
844 if (image.isError() ==
false) {
847 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
848 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
852 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
853 if (image.isError() ==
false) {
867 ImageReadParam param = imageReader.getDefaultReadParam();
868 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
869 param.setDestination(bufferedImage);
874 bufferedImage = imageReader.read(0, param);
875 }
catch (IOException iOException) {
876 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
883 return SwingFXUtils.toFXImage(bufferedImage, null);
891 updateProgress(percentageDone, 100);
893 reader.removeIIOReadProgressListener(
this);
901 if (Thread.interrupted()) {
905 return super.isCancelled();
912 javafx.scene.image.Image fxImage =
get();
913 if (fxImage == null) {
914 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
915 }
else if (fxImage.isError()) {
917 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
919 }
catch (InterruptedException | ExecutionException ex) {
927 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
932 updateProgress(100, 100);
972 static String getContentPathSafe(Content content) {
974 return content.getUniquePath();
975 }
catch (TskCoreException tskCoreException) {
976 String contentName = content.getName();
977 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
1026 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)
static final boolean OPEN_CV_LOADED
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 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)
static File getCachedThumbnailLocation(long fileID)
static final ConcurrentHashMap< Long, File > cacheFileMap
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 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()