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;
85 private static final String
FORMAT =
"png";
94 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
108 private static final ConcurrentHashMap<Long, File>
cacheFileMap =
new ConcurrentHashMap<>();
111 ImageIO.scanForPlugins();
112 BufferedImage tempImage;
114 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
115 }
catch (IOException ex) {
116 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
119 DEFAULT_THUMBNAIL = tempImage;
122 boolean openCVLoadedTemp;
124 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
125 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
126 System.loadLibrary(
"opencv_ffmpeg248_64");
128 System.loadLibrary(
"opencv_ffmpeg248");
131 openCVLoadedTemp =
true;
132 }
catch (UnsatisfiedLinkError e) {
133 openCVLoadedTemp =
false;
134 LOGGER.log(Level.SEVERE,
"OpenCV Native code library failed to load", e);
139 OPEN_CV_LOADED = openCVLoadedTemp;
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",
154 "application/x-123"));
155 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
170 Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
171 .namingPattern(
"thumbnail-saver-%d").build());
174 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
178 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
203 if (!(content instanceof AbstractFile)) {
206 AbstractFile file = (AbstractFile) content;
221 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
232 public static boolean isGIF(AbstractFile file) {
233 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
256 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
257 if (
false == file.isFile() || file.getSize() <= 0) {
261 String extension = file.getNameExtension();
263 if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) {
268 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
271 return supportedMimeTypes.contains(mimeType);
272 }
catch (FileTypeDetectorInitException ex) {
273 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
291 if (fileTypeDetector == null) {
307 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
308 if (content instanceof AbstractFile) {
309 AbstractFile file = (AbstractFile) content;
316 if (Thread.interrupted()) {
319 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
321 if (Thread.interrupted()) {
326 }
catch (IOException iOException) {
327 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
333 if (Thread.interrupted()) {
338 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
339 }
catch (InterruptedException | ExecutionException ex) {
340 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
356 return new BufferedInputStream(
new ReadContentInputStream(file));
386 return cacheFileMap.computeIfAbsent(fileID,
id -> {
389 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
391 LOGGER.log(Level.WARNING,
"Could not get cached thumbnail location. No case is open.");
417 if (file.getSize() < 100) {
422 byte[] fileHeaderBuffer =
readHeader(file, 2);
427 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
428 }
catch (TskCoreException ex) {
444 byte[] fileHeaderBuffer;
447 length = file.getSize();
448 if (length % 2 != 0) {
451 if (length >= 1024) {
454 fileHeaderBuffer =
readHeader(file, (
int) length);
455 }
catch (TskCoreException ex) {
460 if (fileHeaderBuffer != null) {
461 for (
int index = 0; index < length; index += 2) {
463 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
481 if (file.getSize() < 10) {
486 byte[] fileHeaderBuffer =
readHeader(file, 8);
491 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
492 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
493 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
494 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
496 }
catch (TskCoreException ex) {
502 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
503 byte[] fileHeaderBuffer =
new byte[buffLength];
504 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
506 if (bytesRead != buffLength) {
508 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
510 return fileHeaderBuffer;
525 "ImageIO could not determine width of {0}: ",
526 imageReader -> imageReader.getWidth(0)
542 "ImageIO could not determine height of {0}: ",
543 imageReader -> imageReader.getHeight(0)
559 public T
extract(ImageReader reader)
throws IOException;
585 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
587 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
588 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
591 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
593 if (readers.hasNext()) {
594 ImageReader reader = readers.next();
595 reader.setInput(input);
597 return propertyExtractor.extract(reader);
598 }
catch (IOException ex) {
599 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
605 IIOException iioException =
new IIOException(
"No ImageReader found.");
606 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
628 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
644 @NbBundle.Messages({
"# {0} - file name",
645 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
647 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
650 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
657 protected javafx.scene.image.Image
call() throws Exception {
666 if (cacheFile != null) {
668 if (cacheFile.exists()) {
673 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
677 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
678 return SwingFXUtils.toFXImage(cachedThumbnail, null);
680 }
catch (Exception ex) {
681 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
689 BufferedImage thumbnail = null;
691 if (OPEN_CV_LOADED) {
692 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
696 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
698 if (null == thumbnail) {
699 if (defaultOnFailure) {
702 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
712 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
713 if (null == bufferedImage) {
714 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
715 LOGGER.log(Level.WARNING, msg);
716 throw new IIOException(msg);
718 updateProgress(-1, 1);
728 }
catch (IllegalArgumentException | OutOfMemoryError e) {
730 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
732 final int height = bufferedImage.getHeight();
733 final int width = bufferedImage.getWidth();
734 if (iconSize < height || iconSize < width) {
735 final int cropHeight = Math.min(iconSize, height);
736 final int cropWidth = Math.min(iconSize, width);
745 }
catch (Exception cropException) {
746 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
749 }
catch (Exception e) {
750 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
755 updateProgress(-1, 1);
758 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
764 return SwingFXUtils.toFXImage(thumbnail, null);
773 imageSaver.execute(() -> {
776 Files.createParentDirs(cacheFile);
777 if (cacheFile.exists()) {
780 ImageIO.write(thumbnail, FORMAT, cacheFile);
782 }
catch (Exception ex) {
783 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
812 "ReadImageTask.mesageText=Reading image: {0}"})
817 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
821 protected javafx.scene.image.Image
call() throws Exception {
829 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
832 final AbstractFile file;
838 protected javafx.scene.image.Image
readImage() throws IOException {
845 if (image.isError() ==
false) {
848 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
849 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
853 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
854 if (image.isError() ==
false) {
868 ImageReadParam param = imageReader.getDefaultReadParam();
869 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
870 param.setDestination(bufferedImage);
875 bufferedImage = imageReader.read(0, param);
876 }
catch (IOException iOException) {
877 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
884 return SwingFXUtils.toFXImage(bufferedImage, null);
892 updateProgress(percentageDone, 100);
894 reader.removeIIOReadProgressListener(
this);
902 if (Thread.interrupted()) {
906 return super.isCancelled();
913 javafx.scene.image.Image fxImage =
get();
914 if (fxImage == null) {
915 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
916 }
else if (fxImage.isError()) {
918 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
920 }
catch (InterruptedException | ExecutionException ex) {
928 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
933 updateProgress(100, 100);
973 static String getContentPathSafe(Content content) {
975 return content.getUniquePath();
976 }
catch (TskCoreException tskCoreException) {
977 String contentName = content.getName();
978 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
1027 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 Case getOpenCase()
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
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()