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);
201 if (!(content instanceof AbstractFile)) {
204 AbstractFile file = (AbstractFile) content;
219 return isMediaThumbnailSupported(file,
"image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) ||
hasImageFileHeader(file);
230 public static boolean isGIF(AbstractFile file) {
231 return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST);
253 static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix,
final Collection<String> supportedMimeTypes,
final List<String> supportedExtension) {
254 if (
false == file.isFile() || file.getSize() <= 0) {
258 String extension = file.getNameExtension();
260 if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) {
265 if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) {
268 return supportedMimeTypes.contains(mimeType);
269 }
catch (FileTypeDetectorInitException | TskCoreException ex) {
270 LOGGER.log(Level.SEVERE,
"Error determining MIME type of " + getContentPathSafe(file), ex);
288 if (fileTypeDetector == null) {
304 public static BufferedImage
getThumbnail(Content content,
int iconSize) {
305 if (content instanceof AbstractFile) {
306 AbstractFile file = (AbstractFile) content;
313 final BufferedImage image = ImageIO.read(bufferedReadContentStream);
317 }
catch (IOException iOException) {
318 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), iOException);
326 return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
327 }
catch (InterruptedException | ExecutionException ex) {
328 LOGGER.log(Level.WARNING,
"Failed to get thumbnail for " + getContentPathSafe(content), ex);
344 return new BufferedInputStream(
new ReadContentInputStream(file));
374 return cacheFileMap.computeIfAbsent(fileID,
id -> {
377 return Paths.get(cacheDirectory,
"thumbnails", fileID +
".png").toFile();
378 }
catch (IllegalStateException e) {
379 LOGGER.log(Level.WARNING,
"Could not get cached thumbnail location. No case is open.");
405 if (file.getSize() < 100) {
410 byte[] fileHeaderBuffer =
readHeader(file, 2);
415 return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8));
416 }
catch (TskCoreException ex) {
432 byte[] fileHeaderBuffer;
435 length = file.getSize();
436 if (length % 2 != 0) {
439 if (length >= 1024) {
442 fileHeaderBuffer =
readHeader(file, (
int) length);
443 }
catch (TskCoreException ex) {
448 if (fileHeaderBuffer != null) {
449 for (
int index = 0; index < length; index += 2) {
451 if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) {
469 if (file.getSize() < 10) {
474 byte[] fileHeaderBuffer =
readHeader(file, 8);
479 return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E)
480 && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D)
481 && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A)
482 && ((fileHeaderBuffer[7] & 0xff) == 0x0A));
484 }
catch (TskCoreException ex) {
490 private static byte[]
readHeader(AbstractFile file,
int buffLength)
throws TskCoreException {
491 byte[] fileHeaderBuffer =
new byte[buffLength];
492 int bytesRead = file.read(fileHeaderBuffer, 0, buffLength);
494 if (bytesRead != buffLength) {
496 throw new TskCoreException(
"Could not read " + buffLength +
" bytes from " + file.getName());
498 return fileHeaderBuffer;
513 "ImageIO could not determine width of {0}: ",
514 imageReader -> imageReader.getWidth(0)
530 "ImageIO could not determine height of {0}: ",
531 imageReader -> imageReader.getHeight(0)
547 public T
extract(ImageReader reader)
throws IOException;
572 ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
574 IIOException iioException =
new IIOException(
"Could not create ImageInputStream.");
575 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
578 Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
580 if (readers.hasNext()) {
581 ImageReader reader = readers.next();
582 reader.setInput(input);
584 return propertyExtractor.extract(reader);
585 }
catch (IOException ex) {
586 LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file));
592 IIOException iioException =
new IIOException(
"No ImageReader found.");
593 LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file));
615 public static Task<javafx.scene.image.Image>
newGetThumbnailTask(AbstractFile file,
int iconSize,
boolean defaultOnFailure) {
631 @NbBundle.Messages({
"# {0} - file name",
632 "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}",
634 "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"})
637 updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName()));
644 protected javafx.scene.image.Image
call() throws Exception {
653 if (cacheFile != null) {
655 if (cacheFile.exists()) {
657 BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
658 if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() ==
iconSize) {
659 return SwingFXUtils.toFXImage(cachedThumbnail, null);
661 }
catch (Exception ex) {
662 LOGGER.log(Level.WARNING,
"ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
674 BufferedImage thumbnail = null;
676 if (OPEN_CV_LOADED) {
677 updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
678 thumbnail =
VideoUtils.generateVideoThumbnail(file, iconSize);
680 if (null == thumbnail) {
681 if (defaultOnFailure) {
684 throw new IIOException(
"Failed to generate a thumbnail for " + getContentPathSafe(file));
691 BufferedImage bufferedImage = SwingFXUtils.fromFXImage(
readImage(), null);
692 if (null == bufferedImage) {
693 String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file));
694 LOGGER.log(Level.WARNING, msg);
695 throw new IIOException(msg);
697 updateProgress(-1, 1);
702 }
catch (IllegalArgumentException | OutOfMemoryError e) {
704 LOGGER.log(Level.WARNING,
"Cropping {0}, because it could not be scaled: " + e.toString(),
ImageUtils.getContentPathSafe(file));
706 final int height = bufferedImage.getHeight();
707 final int width = bufferedImage.getWidth();
708 if (iconSize < height || iconSize < width) {
709 final int cropHeight = Math.min(iconSize, height);
710 final int cropWidth = Math.min(iconSize, width);
713 }
catch (Exception cropException) {
714 LOGGER.log(Level.WARNING,
"Could not crop {0}: " + cropException.toString(),
ImageUtils.getContentPathSafe(file));
717 }
catch (Exception e) {
718 LOGGER.log(Level.WARNING,
"Could not scale {0}: " + e.toString(),
ImageUtils.getContentPathSafe(file));
727 updateProgress(-1, 1);
730 if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) {
734 return SwingFXUtils.toFXImage(thumbnail, null);
746 Files.createParentDirs(cacheFile);
747 if (cacheFile.exists()) {
750 ImageIO.write(thumbnail, FORMAT, cacheFile);
752 }
catch (Exception ex) {
753 LOGGER.log(Level.WARNING,
"Could not write thumbnail for {0}: " + ex.toString(),
ImageUtils.getContentPathSafe(file));
782 "ReadImageTask.mesageText=Reading image: {0}"})
787 updateMessage(Bundle.ReadImageTask_mesageText(file.getName()));
791 protected javafx.scene.image.Image
call() throws Exception {
799 static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener {
802 final AbstractFile file;
808 protected javafx.scene.image.Image
readImage() throws IOException {
812 if (image.isError() ==
false) {
815 }
else if (file.getNameExtension().equalsIgnoreCase(
"tec")) {
816 ReadContentInputStream readContentInputStream =
new ReadContentInputStream(file);
820 javafx.scene.image.Image image =
new javafx.scene.image.Image(
new BufferedInputStream(readContentInputStream));
821 if (image.isError() ==
false) {
839 ImageReadParam param = imageReader.getDefaultReadParam();
840 BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0));
841 param.setDestination(bufferedImage);
843 bufferedImage = imageReader.read(0, param);
844 }
catch (IOException iOException) {
845 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + iOException.toString(),
ImageUtils.getContentPathSafe(file));
852 return SwingFXUtils.toFXImage(bufferedImage, null);
860 updateProgress(percentageDone, 100);
862 reader.removeIIOReadProgressListener(
this);
872 javafx.scene.image.Image fxImage =
get();
873 if (fxImage == null) {
874 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT,
ImageUtils.getContentPathSafe(file));
875 }
else if (fxImage.isError()) {
877 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(fxImage.getException()),
ImageUtils.getContentPathSafe(file));
879 }
catch (InterruptedException | ExecutionException ex) {
887 LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT +
": " + ObjectUtils.toString(getException()),
ImageUtils.getContentPathSafe(file));
892 updateProgress(100, 100);
932 static String getContentPathSafe(Content content) {
934 return content.getUniquePath();
935 }
catch (TskCoreException tskCoreException) {
936 String contentName = content.getName();
937 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
986 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)
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)
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
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()