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",
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;
216 if (isSupportedMediaExtension(file, supportedExtensions)) {
233 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
244 public static boolean isGIF(AbstractFile file) {
245 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
268 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
269 if (
false == file.isFile() || file.getSize() <= 0) {
273 if (isSupportedMediaExtension(file, supportedExtension)) {
278 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
281 return supportedMimeTypes.contains(mimeType);
282 }
catch (FileTypeDetectorInitException ex) {
283 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
298 static boolean isSupportedMediaExtension(
final AbstractFile file,
final List<String> supportedExtensions) {
299 String extension = file.getNameExtension();
301 return (StringUtils.isNotBlank(extension) && supportedExtensions.contains(extension));
316 if (fileTypeDetector == null) {
332 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
333 if (content instanceof AbstractFile) {
334 AbstractFile file = (AbstractFile) content;
341 if (Thread.interrupted()) {
344 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
346 if (Thread.interrupted()) {
351 }
catch (IOException iOException) {
352 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
358 if (Thread.interrupted()) {
363 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
364 }
catch (InterruptedException | ExecutionException ex) {
365 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
381 return new BufferedInputStream(
new ReadContentInputStream(file));
414 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
416 LOGGER.log(Level.INFO,
"Could not get cached thumbnail location. No case is open.");
442 if (file.getSize() < 100) {
447 byte[] fileHeaderBuffer =
readHeader(file, 2);
452 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
453 }
catch (TskCoreException ex) {
469 byte[] fileHeaderBuffer;
472 length = file.getSize();
473 if (length % 2 != 0) {
476 if (length >= 1024) {
479 fileHeaderBuffer =
readHeader(file, (
int) length);
480 }
catch (TskCoreException ex) {
485 if (fileHeaderBuffer != null) {
486 for (
int index = 0; index < length; index += 2) {
488 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
506 if (file.getSize() < 10) {
511 byte[] fileHeaderBuffer =
readHeader(file, 8);
516 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
517 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
518 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
519 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
521 }
catch (TskCoreException ex) {
527 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
528 byte[] fileHeaderBuffer =
new byte[buffLength];
529 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
531 if (bytesRead != buffLength) {
533 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
535 return fileHeaderBuffer;
550 "ImageIO could not determine width of {0}: ",
551 imageReader -> imageReader.getWidth(0)
567 "ImageIO could not determine height of {0}: ",
568 imageReader -> imageReader.getHeight(0)
584 public T
extract(ImageReader reader)
throws IOException;
610 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
612 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
613 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
616 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
618 if (readers.hasNext()) {
619 ImageReader reader = readers.next();
620 reader.setInput(input);
622 return propertyExtractor.extract(reader);
623 }
catch (IOException ex) {
624 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
630 IIOException iioException =
new IIOException(
"No ImageReader found.");
631 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
653 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
669 @NbBundle.Messages({
"# {0} - file name",
670 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
672 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
675 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
682 protected javafx.scene.image.Image
call() throws Exception {
691 if (cacheFile != null) {
693 if (cacheFile.exists()) {
698 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
702 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
703 return SwingFXUtils.toFXImage(cachedThumbnail, null);
705 }
catch (Exception ex) {
706 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
714 BufferedImage thumbnail = null;
717 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
721 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
723 if (null == thumbnail) {
724 if (defaultOnFailure) {
727 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
737 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
738 if (null == bufferedImage) {
739 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
740 LOGGER.log(Level.WARNING, msg);
741 throw new IIOException(msg);
743 updateProgress(-1, 1);
753 }
catch (IllegalArgumentException | OutOfMemoryError e) {
755 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
757 final int height = bufferedImage.getHeight();
758 final int width = bufferedImage.getWidth();
759 if (iconSize < height || iconSize < width) {
760 final int cropHeight = Math.min(iconSize, height);
761 final int cropWidth = Math.min(iconSize, width);
770 }
catch (Exception cropException) {
771 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
774 }
catch (Exception e) {
775 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
780 updateProgress(-1, 1);
783 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
789 return SwingFXUtils.toFXImage(thumbnail, null);
801 Path path = Paths.get(cacheFile.getParent());
802 File thumbsDir = Paths.get(cacheFile.getParent()).toFile();
803 if (!thumbsDir.exists()) {
807 if (cacheFile.exists()) {
810 ImageIO.write(thumbnail, FORMAT, cacheFile);
812 }
catch (Exception ex) {
813 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
842 "ReadImageTask.mesageText=Reading image: {0}"})
847 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
851 protected javafx.scene.image.Image
call() throws Exception {
859 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
862 final AbstractFile file;
868 protected javafx.scene.image.Image
readImage() throws IOException {
875 if (image.isError() ==
false) {
878 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
879 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
883 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
884 if (image.isError() ==
false) {
898 ImageReadParam param = imageReader.getDefaultReadParam();
899 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
900 param.setDestination(bufferedImage);
905 bufferedImage = imageReader.read(0, param);
906 }
catch (IOException iOException) {
907 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
914 return SwingFXUtils.toFXImage(bufferedImage, null);
922 updateProgress(percentageDone, 100);
924 reader.removeIIOReadProgressListener(
this);
932 if (Thread.interrupted()) {
936 return super.isCancelled();
943 javafx.scene.image.Image fxImage =
get();
944 if (fxImage == null) {
945 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
946 }
else if (fxImage.isError()) {
948 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
950 }
catch (InterruptedException | ExecutionException ex) {
958 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
963 updateProgress(100, 100);
1003 static String getContentPathSafe(Content content) {
1005 return content.getUniquePath();
1006 }
catch (TskCoreException tskCoreException) {
1007 String contentName = content.getName();
1008 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
1057 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 boolean hasOpenCvLoaded()
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 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()