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.Iterator;
39 import java.util.List;
40 import static java.util.Objects.nonNull;
41 import java.util.SortedSet;
42 import java.util.TreeSet;
43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.concurrent.ExecutionException;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.Executors;
47 import java.util.logging.Level;
48 import javafx.concurrent.Task;
49 import javafx.embed.swing.SwingFXUtils;
50 import javax.annotation.Nonnull;
51 import javax.annotation.Nullable;
52 import javax.imageio.IIOException;
53 import javax.imageio.ImageIO;
54 import javax.imageio.ImageReadParam;
55 import javax.imageio.ImageReader;
56 import javax.imageio.event.IIOReadProgressListener;
57 import javax.imageio.stream.ImageInputStream;
58 import org.apache.commons.lang3.ObjectUtils;
59 import org.apache.commons.lang3.StringUtils;
60 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
61 import org.opencv.core.Core;
62 import org.openide.util.NbBundle;
83 private static final String
FORMAT =
"png";
92 private static final SortedSet<String>
GIF_MIME_SET = ImmutableSortedSet.copyOf(
new String[]{
"image/gif"});
106 private static final ConcurrentHashMap<Long, File>
cacheFileMap =
new ConcurrentHashMap<>();
109 ImageIO.scanForPlugins();
110 BufferedImage tempImage;
112 tempImage = ImageIO.read(
ImageUtils.class.getResourceAsStream(
"/org/sleuthkit/autopsy/images/file-icon.png"));
113 }
catch (IOException ex) {
114 LOGGER.log(Level.SEVERE,
"Failed to load default icon.", ex);
117 DEFAULT_THUMBNAIL = tempImage;
120 boolean openCVLoadedTemp;
122 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
123 if (System.getProperty(
"os.arch").equals(
"amd64") || System.getProperty(
"os.arch").equals(
"x86_64")) {
124 System.loadLibrary(
"opencv_ffmpeg248_64");
126 System.loadLibrary(
"opencv_ffmpeg248");
129 openCVLoadedTemp =
true;
130 }
catch (UnsatisfiedLinkError e) {
131 openCVLoadedTemp =
false;
132 LOGGER.log(Level.SEVERE,
"OpenCV Native code library failed to load", e);
137 OPEN_CV_LOADED = openCVLoadedTemp;
138 SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes()));
139 SUPPORTED_IMAGE_EXTENSIONS.add(
"tec");
140 SUPPORTED_IMAGE_EXTENSIONS.removeIf(
"db"::equals);
142 SUPPORTED_IMAGE_MIME_TYPES =
new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
147 SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList(
150 "image/x-portable-graymap",
151 "image/x-portable-bitmap",
152 "application/x-123"));
153 SUPPORTED_IMAGE_MIME_TYPES.removeIf(
"application/octet-stream"::equals);
168 = Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder()
169 .namingPattern(
"thumbnail-saver-%d").build());
172 return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS);
176 return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES);
204 AbstractFile file = (AbstractFile) content;
219 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
231 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
254 static boolean isMediaThumbnailSupported(
AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
261 if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) {
266 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
269 return supportedMimeTypes.contains(mimeType);
270 }
catch (FileTypeDetectorInitException | TskCoreException ex) {
271 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
289 if (fileTypeDetector == null) {
307 AbstractFile file = (AbstractFile) content;
314 final BufferedImage image = ImageIO.
read(bufferedReadContentStream);
318 }
catch (IOException iOException) {
319 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
327 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
328 }
catch (InterruptedException | ExecutionException ex) {
329 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
375 return cacheFileMap.computeIfAbsent(fileID,
id -> {
378 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
379 }
catch (IllegalStateException e) {
380 LOGGER.log(Level.WARNING,
"Could not get cached thumbnail location. No case is open.");
411 byte[] fileHeaderBuffer =
readHeader(file, 2);
416 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
433 byte[] fileHeaderBuffer;
437 if (length % 2 != 0) {
440 if (length >= 1024) {
443 fileHeaderBuffer =
readHeader(file, (
int) length);
449 if (fileHeaderBuffer != null) {
450 for (
int index = 0; index < length; index += 2) {
452 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
475 byte[] fileHeaderBuffer =
readHeader(file, 8);
480 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
481 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
482 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
483 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
492 byte[] fileHeaderBuffer =
new byte[buffLength];
493 int bytesRead = file.
read(fileHeaderBuffer, 0, buffLength);
495 if (bytesRead != buffLength) {
499 return fileHeaderBuffer;
514 "ImageIO could not determine width of {0}: ",
515 imageReader -> imageReader.getWidth(0)
531 "ImageIO could not determine height of {0}: ",
532 imageReader -> imageReader.getHeight(0)
548 public T
extract(ImageReader reader)
throws IOException;
574 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
576 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
577 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
580 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
582 if (readers.hasNext()) {
583 ImageReader reader = readers.next();
584 reader.setInput(input);
586 return propertyExtractor.extract(reader);
587 }
catch (IOException ex) {
588 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
594 IIOException iioException =
new IIOException(
"No ImageReader found.");
595 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
633 @NbBundle.Messages({
"# {0} - file name",
634 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
636 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
639 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.
getName()));
646 protected javafx.scene.image.Image
call() throws Exception {
655 if (cacheFile != null) {
657 if (cacheFile.exists()) {
659 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
660 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
661 return SwingFXUtils.toFXImage(cachedThumbnail, null);
663 }
catch (Exception ex) {
664 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
676 BufferedImage thumbnail = null;
678 if (OPEN_CV_LOADED) {
679 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.
getName()));
680 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
682 if (null == thumbnail) {
683 if (defaultOnFailure) {
686 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
693 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
694 if (null == bufferedImage) {
695 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
696 LOGGER.log(Level.WARNING, msg);
697 throw new IIOException(msg);
699 updateProgress(-1, 1);
704 }
catch (IllegalArgumentException | OutOfMemoryError e) {
706 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
708 final int height = bufferedImage.getHeight();
709 final int width = bufferedImage.getWidth();
710 if (iconSize < height || iconSize < width) {
711 final int cropHeight = Math.min(iconSize, height);
712 final int cropWidth = Math.min(iconSize, width);
715 }
catch (Exception cropException) {
716 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
719 }
catch (Exception e) {
720 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
729 updateProgress(-1, 1);
732 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
736 return SwingFXUtils.toFXImage(thumbnail, null);
748 Files.createParentDirs(cacheFile);
749 if (cacheFile.exists()) {
752 ImageIO.write(thumbnail, FORMAT, cacheFile);
754 }
catch (Exception ex) {
755 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
784 "ReadImageTask.mesageText=Reading image: {0}"})
789 updateMessage(Bundle.ReadImageTask_mesageText(file.
getName()));
793 protected javafx.scene.image.Image
call() throws Exception {
801 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
810 protected javafx.scene.image.Image
readImage() throws IOException {
814 if (image.isError() ==
false) {
822 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
823 if (image.isError() ==
false) {
841 ImageReadParam param = imageReader.getDefaultReadParam();
842 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
843 param.setDestination(bufferedImage);
845 bufferedImage = imageReader.
read(0, param);
846 }
catch (IOException iOException) {
847 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
854 return SwingFXUtils.toFXImage(bufferedImage, null);
862 updateProgress(percentageDone, 100);
864 reader.removeIIOReadProgressListener(
this);
874 javafx.scene.image.Image fxImage =
get();
875 if (fxImage == null) {
876 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
877 }
else if (fxImage.isError()) {
879 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
881 }
catch (InterruptedException | ExecutionException ex) {
889 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
894 updateProgress(100, 100);
934 static String getContentPathSafe(
Content content) {
937 }
catch (TskCoreException tskCoreException) {
938 String contentName = content.
getName();
939 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
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)
long seek(long newPosition)
void readAborted(ImageReader source)
static Task< javafx.scene.image.Image > newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure)
static final boolean OPEN_CV_LOADED
String getNameExtension()
final boolean defaultOnFailure
void imageStarted(ImageReader source, int imageIndex)
static final int ICON_SIZE_SMALL
static synchronized BufferedImage resizeFast(BufferedImage input, int size)
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)
int read(byte[] buf, long offset, long len)
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)
String detect(AbstractFile file)
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 BufferedInputStream getBufferedReadContentStream(AbstractFile file)
static boolean isImageThumbnailSupported(AbstractFile file)
static BufferedImage getThumbnail(Content content, int iconSize)
static final int ICON_SIZE_LARGE
final int read(byte[] buf, long offset, long len)
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 void addEventSubscriber(Set< String > eventNames, PropertyChangeListener subscriber)
static synchronized BufferedImage cropImage(BufferedImage input, int width, int height)
static SortedSet< String > getSupportedImageMimeTypes()